@bragduck/cli 2.29.2 → 2.36.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/README.md +138 -300
- package/dist/bin/bragduck.js +1419 -676
- package/dist/bin/bragduck.js.map +1 -1
- package/dist/index.js +92 -83
- package/dist/index.js.map +1 -1
- package/package.json +3 -12
package/dist/bin/bragduck.js
CHANGED
|
@@ -9,11 +9,11 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// node_modules/tsup/assets/esm_shims.js
|
|
12
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/esm_shims.js
|
|
13
13
|
import path from "path";
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
15
15
|
var init_esm_shims = __esm({
|
|
16
|
-
"node_modules/tsup/assets/esm_shims.js"() {
|
|
16
|
+
"../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/esm_shims.js"() {
|
|
17
17
|
"use strict";
|
|
18
18
|
}
|
|
19
19
|
});
|
|
@@ -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";
|
|
@@ -55,7 +55,7 @@ var init_constants = __esm({
|
|
|
55
55
|
TIMEOUT_MS: 12e4,
|
|
56
56
|
// 2 minutes
|
|
57
57
|
MIN_PORT: 8e3,
|
|
58
|
-
MAX_PORT:
|
|
58
|
+
MAX_PORT: 8004
|
|
59
59
|
};
|
|
60
60
|
ATLASSIAN_OAUTH_CONFIG = {
|
|
61
61
|
CLIENT_ID: "kJlsd66DLTnENE8p7Ru2JwqZg1Sie4yZ",
|
|
@@ -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
|
|
|
@@ -788,92 +817,95 @@ async function findAvailablePort() {
|
|
|
788
817
|
async function startOAuthCallbackServer(expectedState) {
|
|
789
818
|
const port = await findAvailablePort();
|
|
790
819
|
const timeout = OAUTH_CONFIG.TIMEOUT_MS;
|
|
791
|
-
|
|
820
|
+
const callbackUrl = `http://127.0.0.1:${port}${OAUTH_CONFIG.CALLBACK_PATH}`;
|
|
821
|
+
return new Promise((resolveHandle, rejectHandle) => {
|
|
792
822
|
let server = null;
|
|
793
823
|
let timeoutId = null;
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (server) {
|
|
799
|
-
if (typeof server.closeAllConnections === "function") {
|
|
800
|
-
server.closeAllConnections();
|
|
801
|
-
}
|
|
802
|
-
server.close(() => {
|
|
803
|
-
logger.debug("OAuth server closed");
|
|
804
|
-
});
|
|
805
|
-
server.unref();
|
|
806
|
-
}
|
|
807
|
-
};
|
|
808
|
-
const handleRequest = (req, res) => {
|
|
809
|
-
const parsedUrl = parse(req.url || "", true);
|
|
810
|
-
logger.debug(`OAuth callback received: ${req.url}`);
|
|
811
|
-
if (parsedUrl.pathname === OAUTH_CONFIG.CALLBACK_PATH) {
|
|
812
|
-
const { code, state, error, error_description } = parsedUrl.query;
|
|
813
|
-
if (error) {
|
|
814
|
-
const errorMsg = error_description || error;
|
|
815
|
-
logger.debug(`OAuth error: ${errorMsg}`);
|
|
816
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
817
|
-
res.end(ERROR_HTML(String(errorMsg)));
|
|
818
|
-
cleanup();
|
|
819
|
-
reject(new OAuthError(`OAuth error: ${errorMsg}`));
|
|
820
|
-
return;
|
|
824
|
+
const resultPromise = new Promise((resolveResult, rejectResult) => {
|
|
825
|
+
const cleanup = () => {
|
|
826
|
+
if (timeoutId) {
|
|
827
|
+
globalThis.clearTimeout(timeoutId);
|
|
821
828
|
}
|
|
822
|
-
if (
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
829
|
+
if (server) {
|
|
830
|
+
if (typeof server.closeAllConnections === "function") {
|
|
831
|
+
server.closeAllConnections();
|
|
832
|
+
}
|
|
833
|
+
server.close(() => {
|
|
834
|
+
logger.debug("OAuth server closed");
|
|
835
|
+
});
|
|
836
|
+
server.unref();
|
|
830
837
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
+
};
|
|
839
|
+
const handleRequest = (req, res) => {
|
|
840
|
+
const parsedUrl = parse(req.url || "", true);
|
|
841
|
+
logger.debug(`OAuth callback received: ${req.url}`);
|
|
842
|
+
if (parsedUrl.pathname === OAUTH_CONFIG.CALLBACK_PATH) {
|
|
843
|
+
const { code, state, error, error_description } = parsedUrl.query;
|
|
844
|
+
if (error) {
|
|
845
|
+
const errorMsg = error_description || error;
|
|
846
|
+
logger.debug(`OAuth error: ${errorMsg}`);
|
|
847
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
848
|
+
res.end(ERROR_HTML(String(errorMsg)));
|
|
849
|
+
cleanup();
|
|
850
|
+
rejectResult(new OAuthError(`OAuth error: ${errorMsg}`));
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (!code || !state) {
|
|
854
|
+
const errorMsg = "Missing code or state parameter";
|
|
855
|
+
logger.debug(errorMsg);
|
|
856
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
857
|
+
res.end(ERROR_HTML(errorMsg));
|
|
858
|
+
cleanup();
|
|
859
|
+
rejectResult(new OAuthError(errorMsg));
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (state !== expectedState) {
|
|
863
|
+
const errorMsg = "Invalid state parameter (possible CSRF attack)";
|
|
864
|
+
logger.debug(errorMsg);
|
|
865
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
866
|
+
res.end(ERROR_HTML(errorMsg));
|
|
867
|
+
cleanup();
|
|
868
|
+
rejectResult(new OAuthError(errorMsg));
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
872
|
+
res.end(SUCCESS_HTML);
|
|
873
|
+
globalThis.setTimeout(() => {
|
|
874
|
+
cleanup();
|
|
875
|
+
resolveResult({
|
|
876
|
+
code: String(code),
|
|
877
|
+
state: String(state),
|
|
878
|
+
port
|
|
879
|
+
});
|
|
880
|
+
}, 100);
|
|
838
881
|
return;
|
|
839
882
|
}
|
|
840
|
-
res.writeHead(
|
|
841
|
-
res.end(
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
`OAuth callback server listening on http://127.0.0.1:${port}${OAUTH_CONFIG.CALLBACK_PATH}`
|
|
864
|
-
);
|
|
883
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
884
|
+
res.end("Not Found");
|
|
885
|
+
};
|
|
886
|
+
server = createServer(handleRequest);
|
|
887
|
+
server.on("error", (error) => {
|
|
888
|
+
logger.debug(`OAuth server error: ${error.message}`);
|
|
889
|
+
cleanup();
|
|
890
|
+
rejectResult(new OAuthError(`OAuth server error: ${error.message}`));
|
|
891
|
+
rejectHandle(new OAuthError(`OAuth server error: ${error.message}`));
|
|
892
|
+
});
|
|
893
|
+
server.listen(port, "127.0.0.1", () => {
|
|
894
|
+
logger.debug(
|
|
895
|
+
`OAuth callback server listening on http://127.0.0.1:${port}${OAUTH_CONFIG.CALLBACK_PATH}`
|
|
896
|
+
);
|
|
897
|
+
resolveHandle({ callbackUrl, resultPromise });
|
|
898
|
+
});
|
|
899
|
+
timeoutId = globalThis.setTimeout(() => {
|
|
900
|
+
logger.debug("OAuth callback timeout");
|
|
901
|
+
cleanup();
|
|
902
|
+
rejectResult(
|
|
903
|
+
new OAuthError("Authentication timeout - no callback received within 2 minutes")
|
|
904
|
+
);
|
|
905
|
+
}, timeout);
|
|
865
906
|
});
|
|
866
|
-
timeoutId = globalThis.setTimeout(() => {
|
|
867
|
-
logger.debug("OAuth callback timeout");
|
|
868
|
-
cleanup();
|
|
869
|
-
reject(new OAuthError("Authentication timeout - no callback received within 2 minutes"));
|
|
870
|
-
}, timeout);
|
|
871
907
|
});
|
|
872
908
|
}
|
|
873
|
-
async function getCallbackUrl() {
|
|
874
|
-
const port = await findAvailablePort();
|
|
875
|
-
return `http://127.0.0.1:${port}${OAUTH_CONFIG.CALLBACK_PATH}`;
|
|
876
|
-
}
|
|
877
909
|
var SUCCESS_HTML, ERROR_HTML;
|
|
878
910
|
var init_oauth_server = __esm({
|
|
879
911
|
"src/utils/oauth-server.ts"() {
|
|
@@ -1039,7 +1071,7 @@ var init_browser = __esm({
|
|
|
1039
1071
|
// src/services/auth.service.ts
|
|
1040
1072
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
1041
1073
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1042
|
-
import { fileURLToPath as fileURLToPath3, URLSearchParams } from "url";
|
|
1074
|
+
import { fileURLToPath as fileURLToPath3, URLSearchParams as URLSearchParams2 } from "url";
|
|
1043
1075
|
import { dirname as dirname2, join as join3 } from "path";
|
|
1044
1076
|
import { ofetch } from "ofetch";
|
|
1045
1077
|
function getUserAgent() {
|
|
@@ -1083,7 +1115,7 @@ var init_auth_service = __esm({
|
|
|
1083
1115
|
* Build the OAuth authorization URL
|
|
1084
1116
|
*/
|
|
1085
1117
|
async buildAuthUrl(state, callbackUrl) {
|
|
1086
|
-
const params = new
|
|
1118
|
+
const params = new URLSearchParams2({
|
|
1087
1119
|
client_id: OAUTH_CONFIG.CLIENT_ID,
|
|
1088
1120
|
redirect_uri: callbackUrl,
|
|
1089
1121
|
state
|
|
@@ -1136,7 +1168,7 @@ var init_auth_service = __esm({
|
|
|
1136
1168
|
async login() {
|
|
1137
1169
|
logger.debug("Starting OAuth login flow");
|
|
1138
1170
|
const state = this.generateState();
|
|
1139
|
-
const callbackUrl = await
|
|
1171
|
+
const { callbackUrl, resultPromise } = await startOAuthCallbackServer(state);
|
|
1140
1172
|
storageService.setOAuthState({
|
|
1141
1173
|
state,
|
|
1142
1174
|
createdAt: Date.now()
|
|
@@ -1145,7 +1177,6 @@ var init_auth_service = __esm({
|
|
|
1145
1177
|
logger.debug(`Callback URL: ${callbackUrl}`);
|
|
1146
1178
|
const authUrl = await this.buildAuthUrl(state, callbackUrl);
|
|
1147
1179
|
logger.debug(`Authorization URL: ${authUrl}`);
|
|
1148
|
-
const serverPromise = startOAuthCallbackServer(state);
|
|
1149
1180
|
try {
|
|
1150
1181
|
await openBrowser(authUrl);
|
|
1151
1182
|
} catch {
|
|
@@ -1155,7 +1186,7 @@ var init_auth_service = __esm({
|
|
|
1155
1186
|
}
|
|
1156
1187
|
let callbackResult;
|
|
1157
1188
|
try {
|
|
1158
|
-
callbackResult = await
|
|
1189
|
+
callbackResult = await resultPromise;
|
|
1159
1190
|
} catch (error) {
|
|
1160
1191
|
storageService.deleteOAuthState();
|
|
1161
1192
|
throw error;
|
|
@@ -1259,15 +1290,15 @@ __export(version_exports, {
|
|
|
1259
1290
|
getCurrentVersion: () => getCurrentVersion,
|
|
1260
1291
|
version: () => version
|
|
1261
1292
|
});
|
|
1262
|
-
import { readFileSync as
|
|
1263
|
-
import { fileURLToPath as
|
|
1264
|
-
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";
|
|
1265
1296
|
import chalk4 from "chalk";
|
|
1266
1297
|
import boxen2 from "boxen";
|
|
1267
1298
|
function getCurrentVersion() {
|
|
1268
1299
|
try {
|
|
1269
|
-
const packageJsonPath2 =
|
|
1270
|
-
const packageJson2 = JSON.parse(
|
|
1300
|
+
const packageJsonPath2 = join8(__dirname7, "../../package.json");
|
|
1301
|
+
const packageJson2 = JSON.parse(readFileSync6(packageJsonPath2, "utf-8"));
|
|
1271
1302
|
return packageJson2.version;
|
|
1272
1303
|
} catch {
|
|
1273
1304
|
logger.debug("Failed to read package.json version");
|
|
@@ -1349,7 +1380,7 @@ function formatVersion(includePrefix = true) {
|
|
|
1349
1380
|
const prefix = includePrefix ? "v" : "";
|
|
1350
1381
|
return `${prefix}${version}`;
|
|
1351
1382
|
}
|
|
1352
|
-
var
|
|
1383
|
+
var __filename7, __dirname7, version;
|
|
1353
1384
|
var init_version = __esm({
|
|
1354
1385
|
"src/utils/version.ts"() {
|
|
1355
1386
|
"use strict";
|
|
@@ -1357,22 +1388,22 @@ var init_version = __esm({
|
|
|
1357
1388
|
init_api_service();
|
|
1358
1389
|
init_storage_service();
|
|
1359
1390
|
init_logger();
|
|
1360
|
-
|
|
1361
|
-
|
|
1391
|
+
__filename7 = fileURLToPath7(import.meta.url);
|
|
1392
|
+
__dirname7 = dirname6(__filename7);
|
|
1362
1393
|
version = getCurrentVersion();
|
|
1363
1394
|
}
|
|
1364
1395
|
});
|
|
1365
1396
|
|
|
1366
1397
|
// src/services/api.service.ts
|
|
1367
|
-
import { ofetch as
|
|
1368
|
-
import { readFileSync as
|
|
1369
|
-
import { fileURLToPath as
|
|
1370
|
-
import { URLSearchParams as
|
|
1371
|
-
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";
|
|
1372
1403
|
function getCliVersion() {
|
|
1373
1404
|
try {
|
|
1374
|
-
const packageJsonPath2 =
|
|
1375
|
-
const packageJson2 = JSON.parse(
|
|
1405
|
+
const packageJsonPath2 = join9(__dirname8, "../../package.json");
|
|
1406
|
+
const packageJson2 = JSON.parse(readFileSync7(packageJsonPath2, "utf-8"));
|
|
1376
1407
|
return packageJson2.version;
|
|
1377
1408
|
} catch {
|
|
1378
1409
|
logger.debug("Failed to read package.json version");
|
|
@@ -1384,7 +1415,7 @@ function getPlatformInfo() {
|
|
|
1384
1415
|
const arch = process.arch;
|
|
1385
1416
|
return `${platform}-${arch}`;
|
|
1386
1417
|
}
|
|
1387
|
-
var
|
|
1418
|
+
var __filename8, __dirname8, ApiService, apiService;
|
|
1388
1419
|
var init_api_service = __esm({
|
|
1389
1420
|
"src/services/api.service.ts"() {
|
|
1390
1421
|
"use strict";
|
|
@@ -1393,14 +1424,14 @@ var init_api_service = __esm({
|
|
|
1393
1424
|
init_constants();
|
|
1394
1425
|
init_errors();
|
|
1395
1426
|
init_logger();
|
|
1396
|
-
|
|
1397
|
-
|
|
1427
|
+
__filename8 = fileURLToPath8(import.meta.url);
|
|
1428
|
+
__dirname8 = dirname7(__filename8);
|
|
1398
1429
|
ApiService = class {
|
|
1399
1430
|
baseURL;
|
|
1400
1431
|
client;
|
|
1401
1432
|
constructor() {
|
|
1402
1433
|
this.baseURL = process.env.API_BASE_URL || "https://api.bragduck.com";
|
|
1403
|
-
this.client =
|
|
1434
|
+
this.client = ofetch5.create({
|
|
1404
1435
|
baseURL: this.baseURL,
|
|
1405
1436
|
// Request interceptor
|
|
1406
1437
|
onRequest: async ({ request, options }) => {
|
|
@@ -1564,7 +1595,7 @@ var init_api_service = __esm({
|
|
|
1564
1595
|
const { limit = 50, offset = 0, tags, search } = params;
|
|
1565
1596
|
logger.debug(`Listing brags: limit=${limit}, offset=${offset}`);
|
|
1566
1597
|
try {
|
|
1567
|
-
const queryParams = new
|
|
1598
|
+
const queryParams = new URLSearchParams6({
|
|
1568
1599
|
limit: limit.toString(),
|
|
1569
1600
|
offset: offset.toString()
|
|
1570
1601
|
});
|
|
@@ -1661,7 +1692,7 @@ var init_api_service = __esm({
|
|
|
1661
1692
|
*/
|
|
1662
1693
|
setBaseURL(url) {
|
|
1663
1694
|
this.baseURL = url;
|
|
1664
|
-
this.client =
|
|
1695
|
+
this.client = ofetch5.create({
|
|
1665
1696
|
baseURL: url
|
|
1666
1697
|
});
|
|
1667
1698
|
}
|
|
@@ -1679,9 +1710,9 @@ var init_api_service = __esm({
|
|
|
1679
1710
|
// src/cli.ts
|
|
1680
1711
|
init_esm_shims();
|
|
1681
1712
|
import { Command } from "commander";
|
|
1682
|
-
import { readFileSync as
|
|
1683
|
-
import { fileURLToPath as
|
|
1684
|
-
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";
|
|
1685
1716
|
|
|
1686
1717
|
// src/commands/auth.ts
|
|
1687
1718
|
init_esm_shims();
|
|
@@ -1700,7 +1731,7 @@ init_errors();
|
|
|
1700
1731
|
init_logger();
|
|
1701
1732
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
1702
1733
|
import { readFileSync as readFileSync3 } from "fs";
|
|
1703
|
-
import { fileURLToPath as fileURLToPath4, URLSearchParams as
|
|
1734
|
+
import { fileURLToPath as fileURLToPath4, URLSearchParams as URLSearchParams3 } from "url";
|
|
1704
1735
|
import { dirname as dirname3, join as join4 } from "path";
|
|
1705
1736
|
import { ofetch as ofetch2 } from "ofetch";
|
|
1706
1737
|
import { select } from "@inquirer/prompts";
|
|
@@ -1735,7 +1766,7 @@ var AtlassianAuthService = class {
|
|
|
1735
1766
|
* Build the Atlassian OAuth authorization URL
|
|
1736
1767
|
*/
|
|
1737
1768
|
buildAuthUrl(state, callbackUrl) {
|
|
1738
|
-
const params = new
|
|
1769
|
+
const params = new URLSearchParams3({
|
|
1739
1770
|
audience: ATLASSIAN_OAUTH_CONFIG.AUDIENCE,
|
|
1740
1771
|
client_id: ATLASSIAN_OAUTH_CONFIG.CLIENT_ID,
|
|
1741
1772
|
scope: ATLASSIAN_OAUTH_CONFIG.SCOPES,
|
|
@@ -1833,7 +1864,7 @@ var AtlassianAuthService = class {
|
|
|
1833
1864
|
async login() {
|
|
1834
1865
|
logger.debug("Starting Atlassian OAuth login flow");
|
|
1835
1866
|
const state = this.generateState();
|
|
1836
|
-
const callbackUrl = await
|
|
1867
|
+
const { callbackUrl, resultPromise } = await startOAuthCallbackServer(state);
|
|
1837
1868
|
storageService.setOAuthState({
|
|
1838
1869
|
state,
|
|
1839
1870
|
createdAt: Date.now()
|
|
@@ -1842,7 +1873,6 @@ var AtlassianAuthService = class {
|
|
|
1842
1873
|
logger.debug(`Callback URL: ${callbackUrl}`);
|
|
1843
1874
|
const authUrl = this.buildAuthUrl(state, callbackUrl);
|
|
1844
1875
|
logger.debug(`Authorization URL: ${authUrl}`);
|
|
1845
|
-
const serverPromise = startOAuthCallbackServer(state);
|
|
1846
1876
|
try {
|
|
1847
1877
|
await openBrowser(authUrl);
|
|
1848
1878
|
} catch {
|
|
@@ -1852,7 +1882,7 @@ var AtlassianAuthService = class {
|
|
|
1852
1882
|
}
|
|
1853
1883
|
let callbackResult;
|
|
1854
1884
|
try {
|
|
1855
|
-
callbackResult = await
|
|
1885
|
+
callbackResult = await resultPromise;
|
|
1856
1886
|
} catch (error) {
|
|
1857
1887
|
storageService.deleteOAuthState();
|
|
1858
1888
|
throw error;
|
|
@@ -1937,6 +1967,348 @@ var AtlassianAuthService = class {
|
|
|
1937
1967
|
};
|
|
1938
1968
|
var atlassianAuthService = new AtlassianAuthService();
|
|
1939
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
|
+
|
|
1940
2312
|
// src/commands/auth.ts
|
|
1941
2313
|
init_storage_service();
|
|
1942
2314
|
|
|
@@ -1955,9 +2327,9 @@ import simpleGit from "simple-git";
|
|
|
1955
2327
|
init_esm_shims();
|
|
1956
2328
|
init_errors();
|
|
1957
2329
|
import { existsSync as existsSync2 } from "fs";
|
|
1958
|
-
import { join as
|
|
2330
|
+
import { join as join7 } from "path";
|
|
1959
2331
|
function validateGitRepository(path4) {
|
|
1960
|
-
const gitDir =
|
|
2332
|
+
const gitDir = join7(path4, ".git");
|
|
1961
2333
|
if (!existsSync2(gitDir)) {
|
|
1962
2334
|
throw new GitError(
|
|
1963
2335
|
"Not a git repository. Please run this command from within a git repository.",
|
|
@@ -2268,6 +2640,18 @@ var GitHubService = class {
|
|
|
2268
2640
|
});
|
|
2269
2641
|
}
|
|
2270
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
|
+
}
|
|
2271
2655
|
throw new GitHubError("This repository is not hosted on GitHub", {
|
|
2272
2656
|
hint: "Only GitHub repositories are currently supported for PR scanning"
|
|
2273
2657
|
});
|
|
@@ -2575,9 +2959,15 @@ async function authCommand(subcommand) {
|
|
|
2575
2959
|
await authGitLab();
|
|
2576
2960
|
} else if (subcommand === "atlassian") {
|
|
2577
2961
|
await authAtlassian();
|
|
2962
|
+
} else if (subcommand === "google") {
|
|
2963
|
+
await authGoogle();
|
|
2964
|
+
} else if (subcommand === "slack") {
|
|
2965
|
+
await authSlack();
|
|
2578
2966
|
} else {
|
|
2579
2967
|
logger.error(`Unknown auth subcommand: ${subcommand}`);
|
|
2580
|
-
logger.info(
|
|
2968
|
+
logger.info(
|
|
2969
|
+
"Available subcommands: login, status, bitbucket, gitlab, atlassian, google, slack"
|
|
2970
|
+
);
|
|
2581
2971
|
process.exit(1);
|
|
2582
2972
|
}
|
|
2583
2973
|
}
|
|
@@ -2649,43 +3039,87 @@ async function authStatus() {
|
|
|
2649
3039
|
logger.log("");
|
|
2650
3040
|
logger.info("Authentication Status:");
|
|
2651
3041
|
logger.log("");
|
|
2652
|
-
const services = await storageService.getAuthenticatedServices();
|
|
2653
3042
|
const bragduckAuth = await storageService.isServiceAuthenticated("bragduck");
|
|
2654
3043
|
if (bragduckAuth) {
|
|
2655
3044
|
const userInfo = authService.getUserInfo();
|
|
2656
3045
|
logger.info(`${colors.success("\u2713")} Bragduck: ${userInfo?.name || "Authenticated"}`);
|
|
2657
3046
|
} else {
|
|
2658
3047
|
logger.info(`${colors.error("\u2717")} Bragduck: Not authenticated`);
|
|
3048
|
+
logger.info(theme.secondary(` Run: bragduck auth login`));
|
|
2659
3049
|
}
|
|
3050
|
+
logger.log("");
|
|
3051
|
+
logger.info("Git Sources:");
|
|
3052
|
+
logger.log("");
|
|
2660
3053
|
const ghStatus = await githubService.getAuthStatus();
|
|
2661
3054
|
if (ghStatus.installed) {
|
|
2662
3055
|
if (ghStatus.authenticated) {
|
|
2663
|
-
logger.info(`${colors.success("\u2713")} GitHub
|
|
3056
|
+
logger.info(`${colors.success("\u2713")} GitHub: Authenticated`);
|
|
2664
3057
|
} else {
|
|
2665
|
-
logger.info(`${colors.error("\u2717")} GitHub
|
|
3058
|
+
logger.info(`${colors.error("\u2717")} GitHub: Not authenticated`);
|
|
2666
3059
|
logger.info(theme.secondary(" Run: gh auth login"));
|
|
2667
3060
|
}
|
|
2668
3061
|
} else {
|
|
2669
|
-
logger.info(`${colors.error("\u2717")} GitHub CLI
|
|
3062
|
+
logger.info(`${colors.error("\u2717")} GitHub: gh CLI not installed`);
|
|
2670
3063
|
logger.info(theme.secondary(" Install from: https://cli.github.com"));
|
|
2671
3064
|
}
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
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"));
|
|
2683
3082
|
}
|
|
2684
3083
|
logger.log("");
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
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"));
|
|
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"));
|
|
2688
3121
|
}
|
|
3122
|
+
logger.log("");
|
|
2689
3123
|
}
|
|
2690
3124
|
async function authBitbucket() {
|
|
2691
3125
|
logger.log("");
|
|
@@ -2862,11 +3296,75 @@ Services configured:
|
|
|
2862
3296
|
process.exit(1);
|
|
2863
3297
|
}
|
|
2864
3298
|
}
|
|
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
|
+
}
|
|
2865
3363
|
|
|
2866
3364
|
// src/commands/sync.ts
|
|
2867
3365
|
init_esm_shims();
|
|
2868
3366
|
|
|
2869
|
-
// node_modules/@inquirer/core/dist/esm/lib/errors.mjs
|
|
3367
|
+
// ../../node_modules/.pnpm/@inquirer+core@9.2.1/node_modules/@inquirer/core/dist/esm/lib/errors.mjs
|
|
2870
3368
|
init_esm_shims();
|
|
2871
3369
|
var CancelPromptError = class extends Error {
|
|
2872
3370
|
name = "CancelPromptError";
|
|
@@ -2878,7 +3376,7 @@ init_api_service();
|
|
|
2878
3376
|
init_storage_service();
|
|
2879
3377
|
init_auth_service();
|
|
2880
3378
|
import { select as select4 } from "@inquirer/prompts";
|
|
2881
|
-
import
|
|
3379
|
+
import boxen5 from "boxen";
|
|
2882
3380
|
|
|
2883
3381
|
// src/utils/source-detector.ts
|
|
2884
3382
|
init_esm_shims();
|
|
@@ -3458,7 +3956,7 @@ init_logger();
|
|
|
3458
3956
|
init_storage_service();
|
|
3459
3957
|
import { exec as exec5 } from "child_process";
|
|
3460
3958
|
import { promisify as promisify5 } from "util";
|
|
3461
|
-
import { URLSearchParams as
|
|
3959
|
+
import { URLSearchParams as URLSearchParams7 } from "url";
|
|
3462
3960
|
var execAsync5 = promisify5(exec5);
|
|
3463
3961
|
var GitLabService = class {
|
|
3464
3962
|
DEFAULT_INSTANCE = "https://gitlab.com";
|
|
@@ -3604,7 +4102,7 @@ var GitLabService = class {
|
|
|
3604
4102
|
async getMergedMRs(options = {}) {
|
|
3605
4103
|
const { projectPath } = await this.getProjectFromGit();
|
|
3606
4104
|
const encodedPath = encodeURIComponent(projectPath);
|
|
3607
|
-
const params = new
|
|
4105
|
+
const params = new URLSearchParams7({
|
|
3608
4106
|
state: "merged",
|
|
3609
4107
|
order_by: "updated_at",
|
|
3610
4108
|
sort: "desc",
|
|
@@ -4072,6 +4570,9 @@ var JiraService = class {
|
|
|
4072
4570
|
logger.debug(
|
|
4073
4571
|
`Fetched ${newIssuesCount} new issues (total: ${allIssues.length} of ${response.total})${startAt + maxResults < response.total ? ", fetching next page..." : ""}`
|
|
4074
4572
|
);
|
|
4573
|
+
if (options.onProgress) {
|
|
4574
|
+
options.onProgress(allIssues.length, response.total);
|
|
4575
|
+
}
|
|
4075
4576
|
if (newIssuesCount === 0) {
|
|
4076
4577
|
logger.debug("All results are duplicates, stopping pagination");
|
|
4077
4578
|
break;
|
|
@@ -4239,7 +4740,8 @@ var jiraSyncAdapter = {
|
|
|
4239
4740
|
return jiraService.getIssues({
|
|
4240
4741
|
days: options.days,
|
|
4241
4742
|
limit: options.limit,
|
|
4242
|
-
author: author || void 0
|
|
4743
|
+
author: author || void 0,
|
|
4744
|
+
onProgress: options.onProgress
|
|
4243
4745
|
});
|
|
4244
4746
|
},
|
|
4245
4747
|
async isAuthenticated() {
|
|
@@ -4557,6 +5059,9 @@ var ConfluenceService = class {
|
|
|
4557
5059
|
logger.debug(
|
|
4558
5060
|
`Fetched ${newPagesCount} new pages (total: ${allPages.length}, duplicates: ${response.results.length - newPagesCount})${response.size === limit ? ", fetching next page..." : ""}`
|
|
4559
5061
|
);
|
|
5062
|
+
if (options.onProgress) {
|
|
5063
|
+
options.onProgress(allPages.length);
|
|
5064
|
+
}
|
|
4560
5065
|
if (newPagesCount === 0) {
|
|
4561
5066
|
logger.debug("All results are duplicates, stopping pagination");
|
|
4562
5067
|
break;
|
|
@@ -4706,7 +5211,8 @@ var confluenceSyncAdapter = {
|
|
|
4706
5211
|
return confluenceService.getPages({
|
|
4707
5212
|
days: options.days,
|
|
4708
5213
|
limit: options.limit,
|
|
4709
|
-
author: author || void 0
|
|
5214
|
+
author: author || void 0,
|
|
5215
|
+
onProgress: options.onProgress ? (fetched) => options.onProgress(fetched, 0) : void 0
|
|
4710
5216
|
});
|
|
4711
5217
|
},
|
|
4712
5218
|
async isAuthenticated() {
|
|
@@ -4722,174 +5228,526 @@ var confluenceSyncAdapter = {
|
|
|
4722
5228
|
}
|
|
4723
5229
|
};
|
|
4724
5230
|
|
|
4725
|
-
// src/sync/adapter
|
|
4726
|
-
|
|
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 {
|
|
4727
5240
|
/**
|
|
4728
|
-
* Get
|
|
5241
|
+
* Get stored Google credentials, auto-refreshing OAuth tokens if expired
|
|
4729
5242
|
*/
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
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";
|
|
4743
5364
|
default:
|
|
4744
|
-
|
|
5365
|
+
return "Doc";
|
|
4745
5366
|
}
|
|
4746
5367
|
}
|
|
4747
5368
|
/**
|
|
4748
|
-
*
|
|
5369
|
+
* Transform a Google Drive file to GitCommit format
|
|
4749
5370
|
*/
|
|
4750
|
-
|
|
4751
|
-
|
|
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));
|
|
4752
5451
|
}
|
|
4753
5452
|
};
|
|
5453
|
+
var googleDocsService = new GoogleDocsService();
|
|
4754
5454
|
|
|
4755
|
-
// src/
|
|
4756
|
-
|
|
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
|
+
};
|
|
4757
5490
|
|
|
4758
|
-
// src/
|
|
5491
|
+
// src/sync/slack-adapter.ts
|
|
4759
5492
|
init_esm_shims();
|
|
4760
|
-
init_auth_service();
|
|
4761
|
-
import boxen5 from "boxen";
|
|
4762
5493
|
|
|
4763
|
-
// src/
|
|
5494
|
+
// src/services/slack.service.ts
|
|
4764
5495
|
init_esm_shims();
|
|
4765
|
-
|
|
5496
|
+
init_errors();
|
|
4766
5497
|
init_logger();
|
|
4767
|
-
|
|
4768
|
-
import
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
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"
|
|
4785
5524
|
}
|
|
4786
|
-
)
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
borderStyle: "round",
|
|
4807
|
-
borderColor: "yellow"
|
|
4808
|
-
}
|
|
4809
|
-
)
|
|
4810
|
-
);
|
|
4811
|
-
logger.log("");
|
|
4812
|
-
globalThis.setTimeout(() => {
|
|
4813
|
-
process.exit(0);
|
|
4814
|
-
}, 100);
|
|
4815
|
-
return;
|
|
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}`);
|
|
4816
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;
|
|
4817
5556
|
}
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
const err = error;
|
|
4849
|
-
logger.log(
|
|
4850
|
-
boxen3(
|
|
4851
|
-
`${chalk5.red.bold("\u2717 Authentication Failed")}
|
|
4852
|
-
|
|
4853
|
-
${err.message}
|
|
4854
|
-
|
|
4855
|
-
${chalk5.dim("Hint:")} ${getErrorHint(err)}`,
|
|
4856
|
-
{
|
|
4857
|
-
padding: 1,
|
|
4858
|
-
margin: 1,
|
|
4859
|
-
borderStyle: "round",
|
|
4860
|
-
borderColor: "red"
|
|
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"
|
|
4861
5587
|
}
|
|
4862
|
-
)
|
|
4863
|
-
|
|
4864
|
-
|
|
5588
|
+
});
|
|
5589
|
+
const data = await response.json();
|
|
5590
|
+
return data.ok ? data.user : null;
|
|
5591
|
+
} catch {
|
|
5592
|
+
return null;
|
|
5593
|
+
}
|
|
4865
5594
|
}
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
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
|
+
});
|
|
4871
5605
|
}
|
|
4872
|
-
|
|
4873
|
-
|
|
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}`;
|
|
4874
5612
|
}
|
|
4875
|
-
|
|
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));
|
|
4876
5639
|
}
|
|
4877
|
-
|
|
4878
|
-
|
|
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
|
+
|
|
5704
|
+
// src/sync/adapter-factory.ts
|
|
5705
|
+
var AdapterFactory = class {
|
|
5706
|
+
/**
|
|
5707
|
+
* Get adapter for a specific source type
|
|
5708
|
+
*/
|
|
5709
|
+
static getAdapter(source, repoPath) {
|
|
5710
|
+
switch (source) {
|
|
5711
|
+
case "github":
|
|
5712
|
+
return repoPath ? new GitHubSyncAdapter(repoPath) : githubSyncAdapter;
|
|
5713
|
+
case "bitbucket":
|
|
5714
|
+
case "atlassian":
|
|
5715
|
+
return repoPath ? new BitbucketSyncAdapter(repoPath) : bitbucketSyncAdapter;
|
|
5716
|
+
case "gitlab":
|
|
5717
|
+
return repoPath ? new GitLabSyncAdapter(repoPath) : gitlabSyncAdapter;
|
|
5718
|
+
case "jira":
|
|
5719
|
+
return jiraSyncAdapter;
|
|
5720
|
+
case "confluence":
|
|
5721
|
+
return confluenceSyncAdapter;
|
|
5722
|
+
case "google-docs":
|
|
5723
|
+
return googleDocsSyncAdapter;
|
|
5724
|
+
case "slack":
|
|
5725
|
+
return slackSyncAdapter;
|
|
5726
|
+
default:
|
|
5727
|
+
throw new Error(`Unknown source type: ${source}`);
|
|
5728
|
+
}
|
|
4879
5729
|
}
|
|
4880
|
-
|
|
4881
|
-
|
|
5730
|
+
/**
|
|
5731
|
+
* Check if adapter is available for source
|
|
5732
|
+
*/
|
|
5733
|
+
static isSupported(source) {
|
|
5734
|
+
return source === "github" || source === "bitbucket" || source === "atlassian" || source === "gitlab" || source === "jira" || source === "confluence" || source === "google-docs" || source === "slack";
|
|
4882
5735
|
}
|
|
4883
|
-
|
|
4884
|
-
|
|
5736
|
+
};
|
|
5737
|
+
|
|
5738
|
+
// src/commands/sync.ts
|
|
5739
|
+
init_logger();
|
|
4885
5740
|
|
|
4886
5741
|
// src/utils/auth-helper.ts
|
|
5742
|
+
init_esm_shims();
|
|
5743
|
+
init_auth_service();
|
|
5744
|
+
import boxen4 from "boxen";
|
|
4887
5745
|
init_logger();
|
|
4888
5746
|
|
|
4889
5747
|
// src/ui/prompts.ts
|
|
4890
5748
|
init_esm_shims();
|
|
4891
5749
|
import { checkbox, confirm, input as input2, select as select3, editor } from "@inquirer/prompts";
|
|
4892
|
-
import
|
|
5750
|
+
import boxen3 from "boxen";
|
|
4893
5751
|
|
|
4894
5752
|
// src/ui/formatters.ts
|
|
4895
5753
|
init_esm_shims();
|
|
@@ -4978,7 +5836,13 @@ function formatRefinedCommitsTable(brags, selectedCommits) {
|
|
|
4978
5836
|
} else {
|
|
4979
5837
|
displaySha = `#${index + 1}`;
|
|
4980
5838
|
}
|
|
4981
|
-
|
|
5839
|
+
let original;
|
|
5840
|
+
if (isNewBragType) {
|
|
5841
|
+
const input3 = brag.original_input;
|
|
5842
|
+
original = typeof input3 === "string" ? input3.split("\n")[0] || "" : "Raw input";
|
|
5843
|
+
} else {
|
|
5844
|
+
original = brag.original_message.split("\n")[0] || "";
|
|
5845
|
+
}
|
|
4982
5846
|
const title = brag.refined_title;
|
|
4983
5847
|
const description = brag.refined_description.length > 100 ? brag.refined_description.substring(0, 97) + "..." : brag.refined_description;
|
|
4984
5848
|
const tags = (brag.suggested_tags || []).join(", ") || "none";
|
|
@@ -5231,7 +6095,7 @@ ${theme.label("Impact Score")} ${colors.highlight(impactScore)}`;
|
|
|
5231
6095
|
|
|
5232
6096
|
${theme.label("PR Link")} ${colors.link(prUrl)}`;
|
|
5233
6097
|
}
|
|
5234
|
-
console.log(
|
|
6098
|
+
console.log(boxen3(bragDetails, boxStyles.info));
|
|
5235
6099
|
console.log("");
|
|
5236
6100
|
const action = await select3({
|
|
5237
6101
|
message: `What would you like to do with this brag?`,
|
|
@@ -5308,7 +6172,7 @@ async function ensureAuthenticated() {
|
|
|
5308
6172
|
}
|
|
5309
6173
|
logger.log("");
|
|
5310
6174
|
logger.log(
|
|
5311
|
-
|
|
6175
|
+
boxen4(
|
|
5312
6176
|
theme.warning("Not authenticated") + "\n\nYou need to be logged in to use this command.\n\nWould you like to authenticate now?",
|
|
5313
6177
|
boxStyles.warning
|
|
5314
6178
|
)
|
|
@@ -5324,7 +6188,7 @@ async function ensureAuthenticated() {
|
|
|
5324
6188
|
return false;
|
|
5325
6189
|
}
|
|
5326
6190
|
try {
|
|
5327
|
-
await
|
|
6191
|
+
await authCommand("login");
|
|
5328
6192
|
return true;
|
|
5329
6193
|
} catch {
|
|
5330
6194
|
return false;
|
|
@@ -5333,24 +6197,35 @@ async function ensureAuthenticated() {
|
|
|
5333
6197
|
|
|
5334
6198
|
// src/ui/spinners.ts
|
|
5335
6199
|
init_esm_shims();
|
|
5336
|
-
import
|
|
5337
|
-
function
|
|
5338
|
-
return
|
|
6200
|
+
import ora from "ora";
|
|
6201
|
+
function createFetchSpinner(text) {
|
|
6202
|
+
return ora({
|
|
5339
6203
|
text,
|
|
5340
6204
|
color: "cyan",
|
|
5341
|
-
spinner: "
|
|
6205
|
+
spinner: "bouncingBar"
|
|
6206
|
+
});
|
|
6207
|
+
}
|
|
6208
|
+
function createValidateSpinner(text) {
|
|
6209
|
+
return ora({
|
|
6210
|
+
text,
|
|
6211
|
+
color: "cyan",
|
|
6212
|
+
spinner: "line"
|
|
5342
6213
|
});
|
|
5343
6214
|
}
|
|
5344
6215
|
function createStepSpinner(currentStep, totalSteps, text) {
|
|
5345
6216
|
const stepIndicator = theme.step(currentStep, totalSteps);
|
|
5346
|
-
return
|
|
6217
|
+
return ora({
|
|
5347
6218
|
text: `${stepIndicator} ${text}`,
|
|
5348
6219
|
color: "cyan",
|
|
5349
6220
|
spinner: "dots"
|
|
5350
6221
|
});
|
|
5351
6222
|
}
|
|
6223
|
+
function updateStepSpinner(spinner, currentStep, totalSteps, text) {
|
|
6224
|
+
const stepIndicator = theme.step(currentStep, totalSteps);
|
|
6225
|
+
spinner.text = `${stepIndicator} ${text}`;
|
|
6226
|
+
}
|
|
5352
6227
|
function fetchingBragsSpinner() {
|
|
5353
|
-
return
|
|
6228
|
+
return createFetchSpinner("Fetching your brags...");
|
|
5354
6229
|
}
|
|
5355
6230
|
function succeedSpinner(spinner, text) {
|
|
5356
6231
|
if (text) {
|
|
@@ -5375,6 +6250,20 @@ function failStepSpinner(spinner, currentStep, totalSteps, text) {
|
|
|
5375
6250
|
spinner.fail(`${stepIndicator} ${colors.error(text)}`);
|
|
5376
6251
|
}
|
|
5377
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
|
+
|
|
5378
6267
|
// src/utils/repo-scanner.ts
|
|
5379
6268
|
init_esm_shims();
|
|
5380
6269
|
import { promises as fs2 } from "fs";
|
|
@@ -5627,12 +6516,12 @@ async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, opt
|
|
|
5627
6516
|
return { repoName: repo.name, service: repo.service, created: 0, skipped: duplicates.length };
|
|
5628
6517
|
}
|
|
5629
6518
|
logger.log("");
|
|
5630
|
-
const
|
|
6519
|
+
const createSpinner = createStepSpinner(
|
|
5631
6520
|
4,
|
|
5632
6521
|
TOTAL_STEPS,
|
|
5633
6522
|
`Creating ${theme.count(acceptedBrags.length)} brag${acceptedBrags.length > 1 ? "s" : ""}`
|
|
5634
6523
|
);
|
|
5635
|
-
|
|
6524
|
+
createSpinner.start();
|
|
5636
6525
|
const createRequest = {
|
|
5637
6526
|
brags: acceptedBrags.map((refined, index) => {
|
|
5638
6527
|
const originalCommit = selectedCommits[index];
|
|
@@ -5646,7 +6535,10 @@ async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, opt
|
|
|
5646
6535
|
repository: repoInfo.url,
|
|
5647
6536
|
date: originalCommit?.date || (/* @__PURE__ */ new Date()).toISOString(),
|
|
5648
6537
|
commit_url: originalCommit?.url || "",
|
|
6538
|
+
impactLevel: refined.suggested_impactLevel,
|
|
6539
|
+
typeId: refined.suggested_typeId,
|
|
5649
6540
|
impact_score: refined.suggested_impactLevel,
|
|
6541
|
+
impactDescription: refined.impact_description,
|
|
5650
6542
|
impact_description: refined.impact_description,
|
|
5651
6543
|
attachments: originalCommit?.url ? [originalCommit.url] : [],
|
|
5652
6544
|
orgId: orgId || void 0,
|
|
@@ -5673,14 +6565,14 @@ async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, opt
|
|
|
5673
6565
|
]);
|
|
5674
6566
|
logger.debug(`API response: ${createResponse.created} brags created`);
|
|
5675
6567
|
succeedStepSpinner(
|
|
5676
|
-
|
|
6568
|
+
createSpinner,
|
|
5677
6569
|
4,
|
|
5678
6570
|
TOTAL_STEPS,
|
|
5679
6571
|
`Created ${theme.count(createResponse.created)} brag${createResponse.created > 1 ? "s" : ""}`
|
|
5680
6572
|
);
|
|
5681
6573
|
logger.log("");
|
|
5682
6574
|
} catch (error) {
|
|
5683
|
-
failStepSpinner(
|
|
6575
|
+
failStepSpinner(createSpinner, 4, TOTAL_STEPS, "Failed to create brags");
|
|
5684
6576
|
logger.log("");
|
|
5685
6577
|
throw error;
|
|
5686
6578
|
}
|
|
@@ -5812,7 +6704,7 @@ async function syncMultipleRepositories(repos, options) {
|
|
|
5812
6704
|
|
|
5813
6705
|
// src/commands/sync.ts
|
|
5814
6706
|
async function promptSelectService() {
|
|
5815
|
-
const nonGitServices = ["atlassian", "jira", "confluence"];
|
|
6707
|
+
const nonGitServices = ["atlassian", "jira", "confluence", "google-docs", "slack"];
|
|
5816
6708
|
const authenticatedServices = await storageService.getAuthenticatedServices();
|
|
5817
6709
|
const allServices = [
|
|
5818
6710
|
"github",
|
|
@@ -5820,7 +6712,9 @@ async function promptSelectService() {
|
|
|
5820
6712
|
"bitbucket",
|
|
5821
6713
|
"atlassian",
|
|
5822
6714
|
"jira",
|
|
5823
|
-
"confluence"
|
|
6715
|
+
"confluence",
|
|
6716
|
+
"google-docs",
|
|
6717
|
+
"slack"
|
|
5824
6718
|
];
|
|
5825
6719
|
const authenticatedSyncServices = authenticatedServices.filter(
|
|
5826
6720
|
(service) => service !== "bragduck" && allServices.includes(service)
|
|
@@ -5835,7 +6729,7 @@ async function promptSelectService() {
|
|
|
5835
6729
|
for (const service of nonGitServices) {
|
|
5836
6730
|
const isAuth = await storageService.isServiceAuthenticated(service);
|
|
5837
6731
|
const indicator = isAuth ? "\u2713" : "\u2717";
|
|
5838
|
-
const serviceLabel =
|
|
6732
|
+
const serviceLabel = getServiceLabel(service);
|
|
5839
6733
|
serviceChoices.push({
|
|
5840
6734
|
name: `${indicator} ${serviceLabel}`,
|
|
5841
6735
|
value: service,
|
|
@@ -5843,7 +6737,7 @@ async function promptSelectService() {
|
|
|
5843
6737
|
});
|
|
5844
6738
|
}
|
|
5845
6739
|
if (authenticatedSyncServices.length > 0) {
|
|
5846
|
-
const serviceNames = authenticatedSyncServices.map(
|
|
6740
|
+
const serviceNames = authenticatedSyncServices.map(getServiceLabel).join(", ");
|
|
5847
6741
|
serviceChoices.push({
|
|
5848
6742
|
name: `\u2713 All authenticated services (${serviceNames})`,
|
|
5849
6743
|
value: "all",
|
|
@@ -5858,7 +6752,8 @@ async function promptSelectService() {
|
|
|
5858
6752
|
}
|
|
5859
6753
|
async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, sharedOrgId) {
|
|
5860
6754
|
const adapter = AdapterFactory.getAdapter(sourceType);
|
|
5861
|
-
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);
|
|
5862
6757
|
repoSpinner.start();
|
|
5863
6758
|
const VALIDATION_TIMEOUT = 3e4;
|
|
5864
6759
|
let repoInfo;
|
|
@@ -5904,7 +6799,16 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
5904
6799
|
fetchSpinner.start();
|
|
5905
6800
|
const workItems = await adapter.fetchWorkItems({
|
|
5906
6801
|
days,
|
|
5907
|
-
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
|
+
}
|
|
5908
6812
|
});
|
|
5909
6813
|
if (workItems.length === 0) {
|
|
5910
6814
|
failStepSpinner(fetchSpinner, 3, TOTAL_STEPS, `No work items found in the last ${days} days`);
|
|
@@ -5921,6 +6825,8 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
5921
6825
|
logger.log("");
|
|
5922
6826
|
logger.log(formatCommitStats(workItems));
|
|
5923
6827
|
logger.log("");
|
|
6828
|
+
const dupSpinner = createFetchSpinner("Checking for duplicates...");
|
|
6829
|
+
dupSpinner.start();
|
|
5924
6830
|
const existingBrags = await apiService.listBrags({ limit: 100 });
|
|
5925
6831
|
logger.debug(`Fetched ${existingBrags.brags.length} existing brags`);
|
|
5926
6832
|
const existingUrls = new Set(existingBrags.brags.flatMap((b) => b.attachments || []));
|
|
@@ -5929,13 +6835,14 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
5929
6835
|
const newWorkItems = workItems.filter((c) => !c.url || !existingUrls.has(c.url));
|
|
5930
6836
|
logger.debug(`Found ${duplicates.length} duplicates, ${newWorkItems.length} new items`);
|
|
5931
6837
|
if (duplicates.length > 0) {
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
`\u2139 ${duplicates.length} work item${duplicates.length > 1 ? "s" : ""} already exist in Bragduck (will be skipped)`
|
|
5936
|
-
)
|
|
6838
|
+
succeedSpinner(
|
|
6839
|
+
dupSpinner,
|
|
6840
|
+
`${duplicates.length} item${duplicates.length > 1 ? "s" : ""} already synced, skipping`
|
|
5937
6841
|
);
|
|
5938
6842
|
logger.log("");
|
|
6843
|
+
} else {
|
|
6844
|
+
succeedSpinner(dupSpinner, "No duplicates found");
|
|
6845
|
+
logger.log("");
|
|
5939
6846
|
}
|
|
5940
6847
|
if (newWorkItems.length === 0) {
|
|
5941
6848
|
logger.log("");
|
|
@@ -6022,23 +6929,27 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
6022
6929
|
if (sharedOrgId === void 0) {
|
|
6023
6930
|
const userInfo = authService.getUserInfo();
|
|
6024
6931
|
if (userInfo?.id) {
|
|
6932
|
+
const orgSpinner = createFetchSpinner("Loading organizations...");
|
|
6933
|
+
orgSpinner.start();
|
|
6025
6934
|
try {
|
|
6026
6935
|
const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
|
|
6936
|
+
orgSpinner.stop();
|
|
6027
6937
|
if (orgsResponse.items.length > 0) {
|
|
6028
6938
|
selectedOrgId = await promptSelectOrganisation(orgsResponse.items);
|
|
6029
6939
|
logger.log("");
|
|
6030
6940
|
}
|
|
6031
6941
|
} catch {
|
|
6942
|
+
orgSpinner.stop();
|
|
6032
6943
|
logger.debug("Failed to fetch organisations, skipping org selection");
|
|
6033
6944
|
}
|
|
6034
6945
|
}
|
|
6035
6946
|
}
|
|
6036
|
-
const
|
|
6947
|
+
const createSpinner = createStepSpinner(
|
|
6037
6948
|
5,
|
|
6038
6949
|
TOTAL_STEPS,
|
|
6039
6950
|
`Creating ${theme.count(acceptedBrags.length)} brag${acceptedBrags.length > 1 ? "s" : ""}`
|
|
6040
6951
|
);
|
|
6041
|
-
|
|
6952
|
+
createSpinner.start();
|
|
6042
6953
|
const createRequest = {
|
|
6043
6954
|
brags: acceptedBrags.map((refined, index) => {
|
|
6044
6955
|
const originalCommit = selectedCommits[index];
|
|
@@ -6056,7 +6967,10 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
6056
6967
|
repository: repoInfo.url,
|
|
6057
6968
|
date: originalCommit?.date || (/* @__PURE__ */ new Date()).toISOString(),
|
|
6058
6969
|
commit_url: originalCommit?.url || "",
|
|
6970
|
+
impactLevel: refined.suggested_impactLevel,
|
|
6971
|
+
typeId: refined.suggested_typeId,
|
|
6059
6972
|
impact_score: refined.suggested_impactLevel,
|
|
6973
|
+
impactDescription: refined.impact_description,
|
|
6060
6974
|
impact_description: refined.impact_description,
|
|
6061
6975
|
attachments: originalCommit?.url ? [originalCommit.url] : [],
|
|
6062
6976
|
orgId: selectedOrgId || void 0,
|
|
@@ -6083,14 +6997,14 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
|
|
|
6083
6997
|
]);
|
|
6084
6998
|
logger.debug(`API response: ${createResponse.created} brags created`);
|
|
6085
6999
|
succeedStepSpinner(
|
|
6086
|
-
|
|
7000
|
+
createSpinner,
|
|
6087
7001
|
5,
|
|
6088
7002
|
TOTAL_STEPS,
|
|
6089
7003
|
`Created ${theme.count(createResponse.created)} brag${createResponse.created > 1 ? "s" : ""}`
|
|
6090
7004
|
);
|
|
6091
7005
|
logger.log("");
|
|
6092
7006
|
} catch (error) {
|
|
6093
|
-
failStepSpinner(
|
|
7007
|
+
failStepSpinner(createSpinner, 5, TOTAL_STEPS, "Failed to create brags");
|
|
6094
7008
|
logger.log("");
|
|
6095
7009
|
throw error;
|
|
6096
7010
|
}
|
|
@@ -6109,7 +7023,9 @@ async function syncAllAuthenticatedServices(options) {
|
|
|
6109
7023
|
"bitbucket",
|
|
6110
7024
|
"atlassian",
|
|
6111
7025
|
"jira",
|
|
6112
|
-
"confluence"
|
|
7026
|
+
"confluence",
|
|
7027
|
+
"google-docs",
|
|
7028
|
+
"slack"
|
|
6113
7029
|
];
|
|
6114
7030
|
const authenticatedServices = await storageService.getAuthenticatedServices();
|
|
6115
7031
|
let servicesToSync = authenticatedServices.filter(
|
|
@@ -6176,14 +7092,18 @@ async function syncAllAuthenticatedServices(options) {
|
|
|
6176
7092
|
if (!options.turbo) {
|
|
6177
7093
|
const userInfo = authService.getUserInfo();
|
|
6178
7094
|
if (userInfo?.id) {
|
|
7095
|
+
const orgSpinner = createFetchSpinner("Loading organizations...");
|
|
7096
|
+
orgSpinner.start();
|
|
6179
7097
|
try {
|
|
6180
7098
|
const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
|
|
7099
|
+
orgSpinner.stop();
|
|
6181
7100
|
if (orgsResponse.items.length > 0) {
|
|
6182
7101
|
const defaultOrgId = storageService.getConfig("defaultCompany");
|
|
6183
7102
|
sharedOrgId = await promptSelectOrganisationWithDefault(orgsResponse.items, defaultOrgId);
|
|
6184
7103
|
logger.log("");
|
|
6185
7104
|
}
|
|
6186
7105
|
} catch {
|
|
7106
|
+
orgSpinner.stop();
|
|
6187
7107
|
logger.debug("Failed to fetch organisations, skipping org selection");
|
|
6188
7108
|
}
|
|
6189
7109
|
}
|
|
@@ -6192,7 +7112,7 @@ async function syncAllAuthenticatedServices(options) {
|
|
|
6192
7112
|
for (let i = 0; i < servicesToSync.length; i++) {
|
|
6193
7113
|
const service = servicesToSync[i];
|
|
6194
7114
|
if (!service) continue;
|
|
6195
|
-
const serviceLabel =
|
|
7115
|
+
const serviceLabel = getServiceLabel(service);
|
|
6196
7116
|
logger.log(
|
|
6197
7117
|
colors.highlight(`\u2501\u2501\u2501 Syncing ${i + 1}/${servicesToSync.length}: ${serviceLabel} \u2501\u2501\u2501`)
|
|
6198
7118
|
);
|
|
@@ -6233,7 +7153,7 @@ async function syncAllAuthenticatedServices(options) {
|
|
|
6233
7153
|
)
|
|
6234
7154
|
);
|
|
6235
7155
|
for (const result of successful) {
|
|
6236
|
-
const serviceLabel =
|
|
7156
|
+
const serviceLabel = getServiceLabel(result.service);
|
|
6237
7157
|
if (result.created === 0 && result.skipped > 0) {
|
|
6238
7158
|
logger.info(
|
|
6239
7159
|
` \u2022 ${serviceLabel}: ${colors.dim(`All ${result.skipped} item${result.skipped !== 1 ? "s" : ""} already synced`)}`
|
|
@@ -6253,17 +7173,17 @@ async function syncAllAuthenticatedServices(options) {
|
|
|
6253
7173
|
colors.warning(`\u2717 Failed to sync ${failed.length} service${failed.length > 1 ? "s" : ""}:`)
|
|
6254
7174
|
);
|
|
6255
7175
|
for (const result of failed) {
|
|
6256
|
-
const serviceLabel =
|
|
7176
|
+
const serviceLabel = getServiceLabel(result.service);
|
|
6257
7177
|
logger.info(` \u2022 ${serviceLabel}: ${result.error}`);
|
|
6258
7178
|
}
|
|
6259
7179
|
logger.log("");
|
|
6260
7180
|
}
|
|
6261
7181
|
if (totalCreated > 0) {
|
|
6262
7182
|
const allCreatedBrags = results.filter((r) => r.createdBrags).flatMap((r) => r.createdBrags);
|
|
6263
|
-
logger.log(
|
|
7183
|
+
logger.log(boxen5(formatSuccessMessage(totalCreated, allCreatedBrags), boxStyles.success));
|
|
6264
7184
|
} else if (successful.length > 0) {
|
|
6265
7185
|
logger.log(
|
|
6266
|
-
|
|
7186
|
+
boxen5(
|
|
6267
7187
|
theme.secondary("No new brags created (all items already exist or were skipped)"),
|
|
6268
7188
|
boxStyles.info
|
|
6269
7189
|
)
|
|
@@ -6281,13 +7201,22 @@ async function syncCommand(options = {}) {
|
|
|
6281
7201
|
process.exit(1);
|
|
6282
7202
|
}
|
|
6283
7203
|
logger.debug("Fetching subscription status...");
|
|
6284
|
-
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
|
+
}
|
|
6285
7213
|
logger.debug("Subscription status response:", JSON.stringify(subscriptionStatus, null, 2));
|
|
6286
7214
|
if (subscriptionStatus.tier === "FREE") {
|
|
6287
7215
|
logger.debug("FREE tier detected - blocking sync command");
|
|
7216
|
+
failSpinner(subSpinner, "Free plan \u2014 upgrade to use CLI sync");
|
|
6288
7217
|
logger.log("");
|
|
6289
7218
|
logger.log(
|
|
6290
|
-
|
|
7219
|
+
boxen5(
|
|
6291
7220
|
theme.warning("CLI Access Requires Subscription") + "\n\nThe Bragduck CLI is available for Plus and Pro subscribers.\nUpgrade now to unlock:\n\n \u2022 Automatic work item scanning\n \u2022 AI-powered brag generation\n \u2022 Unlimited brags\n\n" + colors.highlight("Start your free trial today"),
|
|
6292
7221
|
{
|
|
6293
7222
|
...boxStyles.warning,
|
|
@@ -6299,7 +7228,13 @@ async function syncCommand(options = {}) {
|
|
|
6299
7228
|
logger.log("");
|
|
6300
7229
|
return;
|
|
6301
7230
|
}
|
|
7231
|
+
succeedSpinner(
|
|
7232
|
+
subSpinner,
|
|
7233
|
+
`${subscriptionStatus.tier.charAt(0).toUpperCase() + subscriptionStatus.tier.slice(1).toLowerCase()} plan \xB7 Ready`
|
|
7234
|
+
);
|
|
6302
7235
|
logger.debug(`Subscription tier "${subscriptionStatus.tier}" - proceeding with sync`);
|
|
7236
|
+
const userInfo = authService.getUserInfo();
|
|
7237
|
+
showCommandHeader(userInfo, subscriptionStatus.tier);
|
|
6303
7238
|
let selectedSource;
|
|
6304
7239
|
if (options.all) {
|
|
6305
7240
|
selectedSource = "all";
|
|
@@ -6310,7 +7245,9 @@ async function syncCommand(options = {}) {
|
|
|
6310
7245
|
logger.log("");
|
|
6311
7246
|
logger.error(`Unsupported source: ${options.source}`);
|
|
6312
7247
|
logger.log("");
|
|
6313
|
-
logger.info(
|
|
7248
|
+
logger.info(
|
|
7249
|
+
"Supported sources: github, gitlab, bitbucket, atlassian, jira, confluence, google-docs"
|
|
7250
|
+
);
|
|
6314
7251
|
return;
|
|
6315
7252
|
}
|
|
6316
7253
|
selectedSource = sourceType;
|
|
@@ -6325,6 +7262,69 @@ async function syncCommand(options = {}) {
|
|
|
6325
7262
|
const detectionSpinner = createStepSpinner(1, TOTAL_STEPS, "Preparing sync");
|
|
6326
7263
|
detectionSpinner.start();
|
|
6327
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
|
+
}
|
|
6328
7328
|
succeedStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Discovering repositories");
|
|
6329
7329
|
logger.log("");
|
|
6330
7330
|
const repos = await discoverRepositories();
|
|
@@ -6346,7 +7346,7 @@ async function syncCommand(options = {}) {
|
|
|
6346
7346
|
const result2 = await syncMultipleRepositories(repos, options);
|
|
6347
7347
|
if (result2.totalCreated > 0 || result2.totalSkipped > 0) {
|
|
6348
7348
|
logger.log("");
|
|
6349
|
-
logger.log(
|
|
7349
|
+
logger.log(boxen5(formatMultiRepoSummary(result2), boxStyles.success));
|
|
6350
7350
|
} else {
|
|
6351
7351
|
logger.log("");
|
|
6352
7352
|
logger.info("No brags created.");
|
|
@@ -6373,13 +7373,31 @@ async function syncCommand(options = {}) {
|
|
|
6373
7373
|
);
|
|
6374
7374
|
return;
|
|
6375
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
|
+
}
|
|
6376
7394
|
}
|
|
6377
7395
|
succeedStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `Source: ${theme.value(sourceType)}`);
|
|
6378
7396
|
logger.log("");
|
|
6379
7397
|
const result = await syncSingleService(sourceType, options, TOTAL_STEPS);
|
|
6380
7398
|
if (result.created > 0) {
|
|
6381
7399
|
logger.log(
|
|
6382
|
-
|
|
7400
|
+
boxen5(formatSuccessMessage(result.created, result.createdBrags), boxStyles.success)
|
|
6383
7401
|
);
|
|
6384
7402
|
} else if (result.skipped > 0) {
|
|
6385
7403
|
logger.log("");
|
|
@@ -6406,12 +7424,29 @@ async function syncCommand(options = {}) {
|
|
|
6406
7424
|
const err = error;
|
|
6407
7425
|
logger.log("");
|
|
6408
7426
|
logger.log(
|
|
6409
|
-
|
|
7427
|
+
boxen5(formatErrorMessage(err.message, getErrorHint(err, sourceType)), boxStyles.error)
|
|
6410
7428
|
);
|
|
6411
7429
|
process.exit(1);
|
|
6412
7430
|
}
|
|
6413
7431
|
}
|
|
6414
|
-
function
|
|
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
|
+
}
|
|
7449
|
+
function getErrorHint(error, sourceType) {
|
|
6415
7450
|
if (error.name === "GitHubError") {
|
|
6416
7451
|
return 'Make sure you are in a GitHub repository and have authenticated with "gh auth login"';
|
|
6417
7452
|
}
|
|
@@ -6424,6 +7459,9 @@ function getErrorHint2(error, sourceType) {
|
|
|
6424
7459
|
}
|
|
6425
7460
|
return 'Run "bragduck auth login" to login again';
|
|
6426
7461
|
}
|
|
7462
|
+
if (error.name === "GoogleDocsError") {
|
|
7463
|
+
return 'Run "bragduck auth google" to re-authenticate with Google';
|
|
7464
|
+
}
|
|
6427
7465
|
if (error.name === "NetworkError") {
|
|
6428
7466
|
return "Check your internet connection and try again";
|
|
6429
7467
|
}
|
|
@@ -6438,17 +7476,17 @@ init_esm_shims();
|
|
|
6438
7476
|
init_auth_service();
|
|
6439
7477
|
init_logger();
|
|
6440
7478
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
6441
|
-
import
|
|
6442
|
-
import
|
|
6443
|
-
import
|
|
7479
|
+
import boxen6 from "boxen";
|
|
7480
|
+
import chalk5 from "chalk";
|
|
7481
|
+
import ora2 from "ora";
|
|
6444
7482
|
async function logoutCommand() {
|
|
6445
7483
|
const isAuthenticated = await authService.isAuthenticated();
|
|
6446
7484
|
if (!isAuthenticated) {
|
|
6447
7485
|
logger.log(
|
|
6448
|
-
|
|
6449
|
-
`${
|
|
7486
|
+
boxen6(
|
|
7487
|
+
`${chalk5.yellow("Not currently authenticated")}
|
|
6450
7488
|
|
|
6451
|
-
${
|
|
7489
|
+
${chalk5.dim("Nothing to logout from")}`,
|
|
6452
7490
|
{
|
|
6453
7491
|
padding: 1,
|
|
6454
7492
|
margin: 1,
|
|
@@ -6463,25 +7501,25 @@ ${chalk6.dim("Nothing to logout from")}`,
|
|
|
6463
7501
|
const userName = userInfo?.name || "Unknown User";
|
|
6464
7502
|
logger.log("");
|
|
6465
7503
|
const shouldLogout = await confirm2({
|
|
6466
|
-
message: `Are you sure you want to logout? (${
|
|
7504
|
+
message: `Are you sure you want to logout? (${chalk5.cyan(userName)})`,
|
|
6467
7505
|
default: false
|
|
6468
7506
|
});
|
|
6469
7507
|
if (!shouldLogout) {
|
|
6470
7508
|
logger.info("Logout cancelled");
|
|
6471
7509
|
return;
|
|
6472
7510
|
}
|
|
6473
|
-
const spinner =
|
|
7511
|
+
const spinner = ora2("Logging out...").start();
|
|
6474
7512
|
try {
|
|
6475
7513
|
await authService.logout();
|
|
6476
7514
|
spinner.succeed("Logged out successfully");
|
|
6477
7515
|
logger.log("");
|
|
6478
7516
|
logger.log(
|
|
6479
|
-
|
|
6480
|
-
`${
|
|
7517
|
+
boxen6(
|
|
7518
|
+
`${chalk5.green.bold("\u2713 Logged out successfully")}
|
|
6481
7519
|
|
|
6482
|
-
${
|
|
7520
|
+
${chalk5.dim("Your credentials have been cleared")}
|
|
6483
7521
|
|
|
6484
|
-
${
|
|
7522
|
+
${chalk5.dim("Run")} ${chalk5.cyan("bragduck init")} ${chalk5.dim("to login again")}`,
|
|
6485
7523
|
{
|
|
6486
7524
|
padding: 1,
|
|
6487
7525
|
margin: 1,
|
|
@@ -6497,295 +7535,13 @@ ${chalk6.dim("Run")} ${chalk6.cyan("bragduck init")} ${chalk6.dim("to login agai
|
|
|
6497
7535
|
}
|
|
6498
7536
|
}
|
|
6499
7537
|
|
|
6500
|
-
// src/commands/scan.ts
|
|
6501
|
-
init_esm_shims();
|
|
6502
|
-
import boxen8 from "boxen";
|
|
6503
|
-
import chalk7 from "chalk";
|
|
6504
|
-
init_api_service();
|
|
6505
|
-
init_storage_service();
|
|
6506
|
-
init_logger();
|
|
6507
|
-
init_browser();
|
|
6508
|
-
init_auth_service();
|
|
6509
|
-
async function scanCommand(options = {}) {
|
|
6510
|
-
logger.log("");
|
|
6511
|
-
logger.log(
|
|
6512
|
-
boxen8(
|
|
6513
|
-
chalk7.yellow.bold("\u26A0 Deprecation Notice") + `
|
|
6514
|
-
|
|
6515
|
-
The ${chalk7.cyan("scan")} command is deprecated.
|
|
6516
|
-
Please use ${chalk7.cyan("bragduck sync")} instead.
|
|
6517
|
-
|
|
6518
|
-
` + chalk7.dim("This command will be removed in v3.0.0"),
|
|
6519
|
-
{
|
|
6520
|
-
padding: 1,
|
|
6521
|
-
borderStyle: "round",
|
|
6522
|
-
borderColor: "yellow",
|
|
6523
|
-
dimBorder: true
|
|
6524
|
-
}
|
|
6525
|
-
)
|
|
6526
|
-
);
|
|
6527
|
-
logger.log("");
|
|
6528
|
-
const TOTAL_STEPS = 5;
|
|
6529
|
-
try {
|
|
6530
|
-
const isAuthenticated = await ensureAuthenticated();
|
|
6531
|
-
if (!isAuthenticated) {
|
|
6532
|
-
process.exit(1);
|
|
6533
|
-
}
|
|
6534
|
-
logger.debug("Fetching subscription status...");
|
|
6535
|
-
const subscriptionStatus = await apiService.getSubscriptionStatus();
|
|
6536
|
-
logger.debug("Subscription status response:", JSON.stringify(subscriptionStatus, null, 2));
|
|
6537
|
-
logger.debug(
|
|
6538
|
-
`Checking tier: "${subscriptionStatus.tier}" (type: ${typeof subscriptionStatus.tier})`
|
|
6539
|
-
);
|
|
6540
|
-
logger.debug(`Tier === 'FREE': ${subscriptionStatus.tier === "FREE"}`);
|
|
6541
|
-
if (subscriptionStatus.tier === "FREE") {
|
|
6542
|
-
logger.debug("FREE tier detected - blocking scan command");
|
|
6543
|
-
logger.log("");
|
|
6544
|
-
logger.log(
|
|
6545
|
-
boxen8(
|
|
6546
|
-
theme.warning("CLI Access Requires Subscription") + "\n\nThe Bragduck CLI is available for Plus and Pro subscribers.\nUpgrade now to unlock:\n\n \u2022 Automatic PR scanning\n \u2022 AI-powered brag generation\n \u2022 Unlimited brags\n\n" + colors.highlight("Start your free trial today!"),
|
|
6547
|
-
{
|
|
6548
|
-
...boxStyles.warning,
|
|
6549
|
-
padding: 1,
|
|
6550
|
-
margin: 1
|
|
6551
|
-
}
|
|
6552
|
-
)
|
|
6553
|
-
);
|
|
6554
|
-
logger.log("");
|
|
6555
|
-
const shouldOpenBrowser = await promptConfirm(
|
|
6556
|
-
"Open subscription plans in your browser?",
|
|
6557
|
-
true
|
|
6558
|
-
);
|
|
6559
|
-
if (shouldOpenBrowser) {
|
|
6560
|
-
logger.log("");
|
|
6561
|
-
const plansUrl = "https://bragduck.com/app/settings/plans";
|
|
6562
|
-
try {
|
|
6563
|
-
await openBrowser(plansUrl);
|
|
6564
|
-
logger.info(theme.secondary("Opening browser..."));
|
|
6565
|
-
} catch {
|
|
6566
|
-
logger.warning("Could not open browser automatically");
|
|
6567
|
-
logger.info(`Please visit: ${theme.value(plansUrl)}`);
|
|
6568
|
-
}
|
|
6569
|
-
}
|
|
6570
|
-
logger.log("");
|
|
6571
|
-
return;
|
|
6572
|
-
}
|
|
6573
|
-
logger.debug(`Subscription tier "${subscriptionStatus.tier}" - proceeding with scan`);
|
|
6574
|
-
const repoSpinner = createStepSpinner(1, TOTAL_STEPS, "Validating GitHub repository");
|
|
6575
|
-
repoSpinner.start();
|
|
6576
|
-
await githubService.validateGitHubRepository();
|
|
6577
|
-
const repoInfo = await githubService.getRepositoryInfo();
|
|
6578
|
-
succeedStepSpinner(
|
|
6579
|
-
repoSpinner,
|
|
6580
|
-
1,
|
|
6581
|
-
TOTAL_STEPS,
|
|
6582
|
-
`Repository: ${theme.value(repoInfo.fullName)}`
|
|
6583
|
-
);
|
|
6584
|
-
logger.log("");
|
|
6585
|
-
let days = options.days;
|
|
6586
|
-
if (!days) {
|
|
6587
|
-
const defaultDays = storageService.getConfig("defaultCommitDays");
|
|
6588
|
-
days = await promptDaysToScan(defaultDays);
|
|
6589
|
-
logger.log("");
|
|
6590
|
-
}
|
|
6591
|
-
const prSpinner = createStepSpinner(
|
|
6592
|
-
2,
|
|
6593
|
-
TOTAL_STEPS,
|
|
6594
|
-
`Fetching merged PRs from the last ${days} days`
|
|
6595
|
-
);
|
|
6596
|
-
prSpinner.start();
|
|
6597
|
-
let prs;
|
|
6598
|
-
if (options.all) {
|
|
6599
|
-
prs = await githubService.getMergedPRs({ days });
|
|
6600
|
-
} else {
|
|
6601
|
-
prs = await githubService.getPRsByCurrentUser({ days });
|
|
6602
|
-
}
|
|
6603
|
-
const commits = prs.map((pr) => githubService.transformPRToCommit(pr));
|
|
6604
|
-
if (commits.length === 0) {
|
|
6605
|
-
failStepSpinner(prSpinner, 2, TOTAL_STEPS, `No merged PRs found in the last ${days} days`);
|
|
6606
|
-
logger.log("");
|
|
6607
|
-
logger.info("Try increasing the number of days or check your GitHub activity");
|
|
6608
|
-
return;
|
|
6609
|
-
}
|
|
6610
|
-
succeedStepSpinner(
|
|
6611
|
-
prSpinner,
|
|
6612
|
-
2,
|
|
6613
|
-
TOTAL_STEPS,
|
|
6614
|
-
`Found ${theme.count(commits.length)} PR${commits.length > 1 ? "s" : ""}`
|
|
6615
|
-
);
|
|
6616
|
-
logger.log("");
|
|
6617
|
-
logger.log(formatCommitStats(commits));
|
|
6618
|
-
logger.log("");
|
|
6619
|
-
let sortedCommits = [...commits];
|
|
6620
|
-
if (commits.length > 1) {
|
|
6621
|
-
const sortOption = await promptSortOption();
|
|
6622
|
-
logger.log("");
|
|
6623
|
-
if (sortOption === "date") {
|
|
6624
|
-
sortedCommits.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
6625
|
-
} else if (sortOption === "size") {
|
|
6626
|
-
sortedCommits.sort((a, b) => {
|
|
6627
|
-
const sizeA = (a.diffStats?.insertions || 0) + (a.diffStats?.deletions || 0);
|
|
6628
|
-
const sizeB = (b.diffStats?.insertions || 0) + (b.diffStats?.deletions || 0);
|
|
6629
|
-
return sizeB - sizeA;
|
|
6630
|
-
});
|
|
6631
|
-
} else if (sortOption === "files") {
|
|
6632
|
-
sortedCommits.sort((a, b) => {
|
|
6633
|
-
const filesA = a.diffStats?.filesChanged || 0;
|
|
6634
|
-
const filesB = b.diffStats?.filesChanged || 0;
|
|
6635
|
-
return filesB - filesA;
|
|
6636
|
-
});
|
|
6637
|
-
}
|
|
6638
|
-
}
|
|
6639
|
-
const selectedShas = await promptSelectCommits(sortedCommits);
|
|
6640
|
-
if (selectedShas.length === 0) {
|
|
6641
|
-
logger.log("");
|
|
6642
|
-
logger.info(theme.secondary("No PRs selected. Scan cancelled."));
|
|
6643
|
-
logger.log("");
|
|
6644
|
-
return;
|
|
6645
|
-
}
|
|
6646
|
-
const selectedCommits = sortedCommits.filter((c) => selectedShas.includes(c.sha));
|
|
6647
|
-
logger.log(formatSelectionSummary(selectedCommits.length, selectedCommits));
|
|
6648
|
-
logger.log("");
|
|
6649
|
-
const existingBrags = await apiService.listBrags({ limit: 100 });
|
|
6650
|
-
logger.debug(`Fetched ${existingBrags.brags.length} existing brags`);
|
|
6651
|
-
const existingUrls = new Set(existingBrags.brags.flatMap((b) => b.attachments || []));
|
|
6652
|
-
logger.debug(`Existing PR URLs in attachments: ${existingUrls.size}`);
|
|
6653
|
-
const duplicates = selectedCommits.filter((c) => c.url && existingUrls.has(c.url));
|
|
6654
|
-
const newCommits = selectedCommits.filter((c) => !c.url || !existingUrls.has(c.url));
|
|
6655
|
-
logger.debug(`Duplicates: ${duplicates.length}, New: ${newCommits.length}`);
|
|
6656
|
-
if (duplicates.length > 0) {
|
|
6657
|
-
logger.log("");
|
|
6658
|
-
logger.info(
|
|
6659
|
-
colors.warning(
|
|
6660
|
-
`${duplicates.length} PR${duplicates.length > 1 ? "s" : ""} already added to Bragduck - skipping`
|
|
6661
|
-
)
|
|
6662
|
-
);
|
|
6663
|
-
logger.log("");
|
|
6664
|
-
}
|
|
6665
|
-
if (newCommits.length === 0) {
|
|
6666
|
-
logger.log("");
|
|
6667
|
-
logger.info(
|
|
6668
|
-
theme.secondary("All selected PRs already exist in Bragduck. Nothing to refine.")
|
|
6669
|
-
);
|
|
6670
|
-
logger.log("");
|
|
6671
|
-
return;
|
|
6672
|
-
}
|
|
6673
|
-
const refineSpinner = createStepSpinner(
|
|
6674
|
-
3,
|
|
6675
|
-
TOTAL_STEPS,
|
|
6676
|
-
`Refining ${theme.count(newCommits.length)} PR${newCommits.length > 1 ? "s" : ""} with AI`
|
|
6677
|
-
);
|
|
6678
|
-
refineSpinner.start();
|
|
6679
|
-
const refineRequest = {
|
|
6680
|
-
brags: newCommits.map((c) => ({
|
|
6681
|
-
text: c.message,
|
|
6682
|
-
date: c.date,
|
|
6683
|
-
title: c.message.split("\n")[0]
|
|
6684
|
-
// First line as initial title
|
|
6685
|
-
}))
|
|
6686
|
-
};
|
|
6687
|
-
const refineResponse = await apiService.refineBrags(refineRequest);
|
|
6688
|
-
let refinedBrags = refineResponse.refined_brags;
|
|
6689
|
-
succeedStepSpinner(refineSpinner, 3, TOTAL_STEPS, "PRs refined successfully");
|
|
6690
|
-
logger.log("");
|
|
6691
|
-
logger.info("Preview of refined brags:");
|
|
6692
|
-
logger.log("");
|
|
6693
|
-
logger.log(formatRefinedCommitsTable(refinedBrags, newCommits));
|
|
6694
|
-
logger.log("");
|
|
6695
|
-
const acceptedBrags = await promptReviewBrags(refinedBrags, newCommits);
|
|
6696
|
-
if (acceptedBrags.length === 0) {
|
|
6697
|
-
logger.log("");
|
|
6698
|
-
logger.info(theme.secondary("No brags selected for creation. Scan cancelled."));
|
|
6699
|
-
logger.log("");
|
|
6700
|
-
return;
|
|
6701
|
-
}
|
|
6702
|
-
logger.log("");
|
|
6703
|
-
let selectedOrgId = null;
|
|
6704
|
-
const userInfo = authService.getUserInfo();
|
|
6705
|
-
if (userInfo?.id) {
|
|
6706
|
-
try {
|
|
6707
|
-
const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
|
|
6708
|
-
if (orgsResponse.items.length > 0) {
|
|
6709
|
-
selectedOrgId = await promptSelectOrganisation(orgsResponse.items);
|
|
6710
|
-
logger.log("");
|
|
6711
|
-
}
|
|
6712
|
-
} catch {
|
|
6713
|
-
logger.debug("Failed to fetch organisations, skipping org selection");
|
|
6714
|
-
}
|
|
6715
|
-
}
|
|
6716
|
-
const createSpinner2 = createStepSpinner(
|
|
6717
|
-
4,
|
|
6718
|
-
TOTAL_STEPS,
|
|
6719
|
-
`Creating ${theme.count(acceptedBrags.length)} brag${acceptedBrags.length > 1 ? "s" : ""}`
|
|
6720
|
-
);
|
|
6721
|
-
createSpinner2.start();
|
|
6722
|
-
const createRequest = {
|
|
6723
|
-
brags: acceptedBrags.map((refined, index) => {
|
|
6724
|
-
const originalCommit = newCommits[index];
|
|
6725
|
-
const repoTag = repoInfo.name;
|
|
6726
|
-
const tagsWithRepo = refined.suggested_tags.includes(repoTag) ? refined.suggested_tags : [repoTag, ...refined.suggested_tags];
|
|
6727
|
-
return {
|
|
6728
|
-
commit_sha: originalCommit?.sha || `brag-${index}`,
|
|
6729
|
-
title: refined.refined_title,
|
|
6730
|
-
description: refined.refined_description,
|
|
6731
|
-
tags: tagsWithRepo,
|
|
6732
|
-
repository: repoInfo.url,
|
|
6733
|
-
date: originalCommit?.date || "",
|
|
6734
|
-
commit_url: originalCommit?.url || "",
|
|
6735
|
-
impact_score: refined.suggested_impactLevel,
|
|
6736
|
-
impact_description: refined.impact_description,
|
|
6737
|
-
attachments: originalCommit?.url ? [originalCommit.url] : [],
|
|
6738
|
-
orgId: selectedOrgId || void 0
|
|
6739
|
-
};
|
|
6740
|
-
})
|
|
6741
|
-
};
|
|
6742
|
-
const createResponse = await apiService.createBrags(createRequest);
|
|
6743
|
-
succeedStepSpinner(
|
|
6744
|
-
createSpinner2,
|
|
6745
|
-
4,
|
|
6746
|
-
TOTAL_STEPS,
|
|
6747
|
-
`Created ${theme.count(createResponse.created)} brag${createResponse.created > 1 ? "s" : ""}`
|
|
6748
|
-
);
|
|
6749
|
-
logger.log("");
|
|
6750
|
-
logger.log(boxen8(formatSuccessMessage(createResponse.created), boxStyles.success));
|
|
6751
|
-
} catch (error) {
|
|
6752
|
-
if (error instanceof CancelPromptError) {
|
|
6753
|
-
logger.log("");
|
|
6754
|
-
logger.info(theme.secondary("Scan cancelled."));
|
|
6755
|
-
logger.log("");
|
|
6756
|
-
return;
|
|
6757
|
-
}
|
|
6758
|
-
const err = error;
|
|
6759
|
-
logger.log("");
|
|
6760
|
-
logger.log(boxen8(formatErrorMessage(err.message, getErrorHint3(err)), boxStyles.error));
|
|
6761
|
-
process.exit(1);
|
|
6762
|
-
}
|
|
6763
|
-
}
|
|
6764
|
-
function getErrorHint3(error) {
|
|
6765
|
-
if (error.name === "GitHubError") {
|
|
6766
|
-
return 'Make sure you are in a GitHub repository and have authenticated with "gh auth login"';
|
|
6767
|
-
}
|
|
6768
|
-
if (error.name === "GitError") {
|
|
6769
|
-
return "Make sure you are in a git repository. Note: Only GitHub repositories are supported for PR scanning.";
|
|
6770
|
-
}
|
|
6771
|
-
if (error.name === "TokenExpiredError" || error.name === "AuthenticationError") {
|
|
6772
|
-
return 'Run "bragduck init" to login again';
|
|
6773
|
-
}
|
|
6774
|
-
if (error.name === "NetworkError") {
|
|
6775
|
-
return "Check your internet connection and try again";
|
|
6776
|
-
}
|
|
6777
|
-
if (error.name === "ApiError") {
|
|
6778
|
-
return "The server might be experiencing issues. Try again later";
|
|
6779
|
-
}
|
|
6780
|
-
return "Try running with DEBUG=* for more information";
|
|
6781
|
-
}
|
|
6782
|
-
|
|
6783
7538
|
// src/commands/list.ts
|
|
6784
7539
|
init_esm_shims();
|
|
6785
7540
|
init_api_service();
|
|
6786
|
-
import
|
|
7541
|
+
import boxen7 from "boxen";
|
|
6787
7542
|
import Table2 from "cli-table3";
|
|
6788
7543
|
init_logger();
|
|
7544
|
+
init_auth_service();
|
|
6789
7545
|
async function listCommand(options = {}) {
|
|
6790
7546
|
logger.log("");
|
|
6791
7547
|
try {
|
|
@@ -6818,6 +7574,7 @@ async function listCommand(options = {}) {
|
|
|
6818
7574
|
return;
|
|
6819
7575
|
}
|
|
6820
7576
|
succeedSpinner(spinner, `Found ${response.total} brag${response.total > 1 ? "s" : ""}`);
|
|
7577
|
+
showCommandHeader(authService.getUserInfo());
|
|
6821
7578
|
logger.log("");
|
|
6822
7579
|
if (options.oneline) {
|
|
6823
7580
|
logger.log(formatBragsOneline(response.brags));
|
|
@@ -6850,7 +7607,7 @@ async function listCommand(options = {}) {
|
|
|
6850
7607
|
} catch (error) {
|
|
6851
7608
|
const err = error;
|
|
6852
7609
|
logger.log("");
|
|
6853
|
-
logger.log(
|
|
7610
|
+
logger.log(boxen7(formatErrorMessage(err.message, getErrorHint2(err)), boxStyles.error));
|
|
6854
7611
|
process.exit(1);
|
|
6855
7612
|
}
|
|
6856
7613
|
}
|
|
@@ -6872,7 +7629,7 @@ function formatBragsTable(brags) {
|
|
|
6872
7629
|
const title = brag.title;
|
|
6873
7630
|
const description = truncateText(brag.description, 100);
|
|
6874
7631
|
const tags = brag.tags.length > 0 ? brag.tags.join(", ") : colors.dim("none");
|
|
6875
|
-
const repository = brag.
|
|
7632
|
+
const repository = brag.externalUrl ? truncateText(extractRepoName(brag.externalUrl), 25) : colors.dim("none");
|
|
6876
7633
|
table.push([
|
|
6877
7634
|
colors.highlight(date),
|
|
6878
7635
|
colors.white(title),
|
|
@@ -6903,7 +7660,7 @@ function extractRepoName(url) {
|
|
|
6903
7660
|
return url;
|
|
6904
7661
|
}
|
|
6905
7662
|
}
|
|
6906
|
-
function
|
|
7663
|
+
function getErrorHint2(error) {
|
|
6907
7664
|
if (error.name === "TokenExpiredError" || error.name === "AuthenticationError") {
|
|
6908
7665
|
return 'Run "bragduck init" to login again';
|
|
6909
7666
|
}
|
|
@@ -6921,8 +7678,8 @@ init_esm_shims();
|
|
|
6921
7678
|
init_storage_service();
|
|
6922
7679
|
init_logger();
|
|
6923
7680
|
init_constants();
|
|
6924
|
-
import
|
|
6925
|
-
import
|
|
7681
|
+
import boxen8 from "boxen";
|
|
7682
|
+
import chalk6 from "chalk";
|
|
6926
7683
|
import Table3 from "cli-table3";
|
|
6927
7684
|
init_errors();
|
|
6928
7685
|
var VALID_CONFIG_KEYS = Object.values(CONFIG_KEYS);
|
|
@@ -6957,7 +7714,7 @@ async function configCommand(subcommand, key, value) {
|
|
|
6957
7714
|
const err = error;
|
|
6958
7715
|
logger.log("");
|
|
6959
7716
|
logger.log(
|
|
6960
|
-
|
|
7717
|
+
boxen8(formatErrorMessage(err.message, getConfigHint(err)), {
|
|
6961
7718
|
padding: 1,
|
|
6962
7719
|
margin: 1,
|
|
6963
7720
|
borderStyle: "round",
|
|
@@ -6978,7 +7735,7 @@ async function handleListConfig() {
|
|
|
6978
7735
|
gitlabInstance: storageService.getConfig("gitlabInstance")
|
|
6979
7736
|
};
|
|
6980
7737
|
const table = new Table3({
|
|
6981
|
-
head: [
|
|
7738
|
+
head: [chalk6.cyan("Key"), chalk6.cyan("Value"), chalk6.cyan("Default")],
|
|
6982
7739
|
colWidths: [25, 40, 40],
|
|
6983
7740
|
wordWrap: true,
|
|
6984
7741
|
style: {
|
|
@@ -6987,61 +7744,61 @@ async function handleListConfig() {
|
|
|
6987
7744
|
}
|
|
6988
7745
|
});
|
|
6989
7746
|
table.push([
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
7747
|
+
chalk6.white("defaultCommitDays"),
|
|
7748
|
+
chalk6.yellow(String(config2.defaultCommitDays)),
|
|
7749
|
+
chalk6.dim(String(DEFAULT_CONFIG.defaultCommitDays))
|
|
6993
7750
|
]);
|
|
6994
7751
|
table.push([
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
7752
|
+
chalk6.white("autoVersionCheck"),
|
|
7753
|
+
chalk6.yellow(String(config2.autoVersionCheck)),
|
|
7754
|
+
chalk6.dim(String(DEFAULT_CONFIG.autoVersionCheck))
|
|
6998
7755
|
]);
|
|
6999
7756
|
table.push([
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7757
|
+
chalk6.white("defaultSource"),
|
|
7758
|
+
chalk6.yellow(config2.defaultSource || "not set"),
|
|
7759
|
+
chalk6.dim("not set")
|
|
7003
7760
|
]);
|
|
7004
7761
|
table.push([
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7762
|
+
chalk6.white("sourcePriority"),
|
|
7763
|
+
chalk6.yellow(config2.sourcePriority ? config2.sourcePriority.join(", ") : "not set"),
|
|
7764
|
+
chalk6.dim("not set")
|
|
7008
7765
|
]);
|
|
7009
7766
|
table.push([
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7767
|
+
chalk6.white("jiraInstance"),
|
|
7768
|
+
chalk6.yellow(config2.jiraInstance || "not set"),
|
|
7769
|
+
chalk6.dim("not set")
|
|
7013
7770
|
]);
|
|
7014
7771
|
table.push([
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7772
|
+
chalk6.white("confluenceInstance"),
|
|
7773
|
+
chalk6.yellow(config2.confluenceInstance || "not set"),
|
|
7774
|
+
chalk6.dim("not set")
|
|
7018
7775
|
]);
|
|
7019
7776
|
table.push([
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7777
|
+
chalk6.white("gitlabInstance"),
|
|
7778
|
+
chalk6.yellow(config2.gitlabInstance || "not set"),
|
|
7779
|
+
chalk6.dim("not set")
|
|
7023
7780
|
]);
|
|
7024
7781
|
logger.info("Current configuration:");
|
|
7025
7782
|
logger.log("");
|
|
7026
7783
|
logger.log(table.toString());
|
|
7027
7784
|
logger.log("");
|
|
7028
|
-
logger.info(
|
|
7785
|
+
logger.info(chalk6.dim("To change a value: ") + chalk6.cyan("bragduck config set <key> <value>"));
|
|
7029
7786
|
logger.log("");
|
|
7030
7787
|
}
|
|
7031
7788
|
async function handleGetConfig(key) {
|
|
7032
7789
|
validateConfigKey(key);
|
|
7033
7790
|
const value = storageService.getConfig(key);
|
|
7034
7791
|
const defaultValue = DEFAULT_CONFIG[key];
|
|
7035
|
-
logger.info(`Configuration for ${
|
|
7792
|
+
logger.info(`Configuration for ${chalk6.cyan(key)}:`);
|
|
7036
7793
|
logger.log("");
|
|
7037
|
-
logger.log(` ${
|
|
7038
|
-
logger.log(` ${
|
|
7794
|
+
logger.log(` ${chalk6.white("Current:")} ${chalk6.yellow(String(value))}`);
|
|
7795
|
+
logger.log(` ${chalk6.white("Default:")} ${chalk6.dim(String(defaultValue))}`);
|
|
7039
7796
|
logger.log("");
|
|
7040
7797
|
if (value === defaultValue) {
|
|
7041
|
-
logger.info(
|
|
7798
|
+
logger.info(chalk6.dim("Using default value"));
|
|
7042
7799
|
} else {
|
|
7043
7800
|
logger.info(
|
|
7044
|
-
|
|
7801
|
+
chalk6.dim("Custom value set. Reset with: ") + chalk6.cyan(`bragduck config set ${key} ${defaultValue}`)
|
|
7045
7802
|
);
|
|
7046
7803
|
}
|
|
7047
7804
|
logger.log("");
|
|
@@ -7051,10 +7808,10 @@ async function handleSetConfig(key, value) {
|
|
|
7051
7808
|
const typedValue = validateAndConvertValue(key, value);
|
|
7052
7809
|
storageService.setConfig(key, typedValue);
|
|
7053
7810
|
logger.log(
|
|
7054
|
-
|
|
7055
|
-
`${
|
|
7811
|
+
boxen8(
|
|
7812
|
+
`${chalk6.green.bold("\u2713 Configuration updated")}
|
|
7056
7813
|
|
|
7057
|
-
${
|
|
7814
|
+
${chalk6.white(key)}: ${chalk6.yellow(String(typedValue))}`,
|
|
7058
7815
|
{
|
|
7059
7816
|
padding: 1,
|
|
7060
7817
|
margin: 1,
|
|
@@ -7151,13 +7908,15 @@ function getConfigHint(error) {
|
|
|
7151
7908
|
// src/cli.ts
|
|
7152
7909
|
init_version();
|
|
7153
7910
|
init_logger();
|
|
7154
|
-
var
|
|
7155
|
-
var
|
|
7156
|
-
var packageJsonPath =
|
|
7157
|
-
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"));
|
|
7158
7915
|
var program = new Command();
|
|
7159
|
-
program.name("bragduck").description("CLI tool for managing developer achievements and brags\nAliases:
|
|
7160
|
-
program.command("auth [subcommand]").description(
|
|
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)");
|
|
7917
|
+
program.command("auth [subcommand]").description(
|
|
7918
|
+
"Manage authentication (subcommands: login, status, github, gitlab, bitbucket, atlassian, google, slack)"
|
|
7919
|
+
).action(async (subcommand) => {
|
|
7161
7920
|
try {
|
|
7162
7921
|
await authCommand(subcommand);
|
|
7163
7922
|
} catch (error) {
|
|
@@ -7173,22 +7932,6 @@ program.command("sync").description("Sync work items and create brags").option("
|
|
|
7173
7932
|
process.exit(1);
|
|
7174
7933
|
}
|
|
7175
7934
|
});
|
|
7176
|
-
program.command("init").description("Authenticate with Bragduck").action(async () => {
|
|
7177
|
-
try {
|
|
7178
|
-
await initCommand();
|
|
7179
|
-
} catch (error) {
|
|
7180
|
-
console.error(error);
|
|
7181
|
-
process.exit(1);
|
|
7182
|
-
}
|
|
7183
|
-
});
|
|
7184
|
-
program.command("scan").description("Scan git commits and create brags (deprecated, use sync)").option("-d, --days <number>", "Number of days to scan", (val) => parseInt(val, 10)).option("-a, --all", "Include all commits (not just current user)").action(async (options) => {
|
|
7185
|
-
try {
|
|
7186
|
-
await scanCommand(options);
|
|
7187
|
-
} catch (error) {
|
|
7188
|
-
console.error(error);
|
|
7189
|
-
process.exit(1);
|
|
7190
|
-
}
|
|
7191
|
-
});
|
|
7192
7935
|
program.command("list").description("List your existing brags").option("-l, --limit <number>", "Number of brags to display", (val) => parseInt(val, 10), 50).option("-o, --offset <number>", "Number of brags to skip", (val) => parseInt(val, 10), 0).option("-t, --tags <tags>", "Filter by tags (comma-separated)").option("-s, --search <query>", "Search brags by keyword").option("--oneline", "Show only date and title for each brag").action(async (options) => {
|
|
7193
7936
|
try {
|
|
7194
7937
|
await listCommand(options);
|