@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.
@@ -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: 9e3
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
- return new Promise((resolve, reject) => {
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 cleanup = () => {
795
- if (timeoutId) {
796
- globalThis.clearTimeout(timeoutId);
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 (!code || !state) {
823
- const errorMsg = "Missing code or state parameter";
824
- logger.debug(errorMsg);
825
- res.writeHead(400, { "Content-Type": "text/html" });
826
- res.end(ERROR_HTML(errorMsg));
827
- cleanup();
828
- reject(new OAuthError(errorMsg));
829
- return;
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
- if (state !== expectedState) {
832
- const errorMsg = "Invalid state parameter (possible CSRF attack)";
833
- logger.debug(errorMsg);
834
- res.writeHead(400, { "Content-Type": "text/html" });
835
- res.end(ERROR_HTML(errorMsg));
836
- cleanup();
837
- reject(new OAuthError(errorMsg));
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(200, { "Content-Type": "text/html" });
841
- res.end(SUCCESS_HTML);
842
- globalThis.setTimeout(() => {
843
- cleanup();
844
- resolve({
845
- code: String(code),
846
- state: String(state),
847
- port
848
- });
849
- }, 100);
850
- return;
851
- }
852
- res.writeHead(404, { "Content-Type": "text/plain" });
853
- res.end("Not Found");
854
- };
855
- server = createServer(handleRequest);
856
- server.on("error", (error) => {
857
- logger.debug(`OAuth server error: ${error.message}`);
858
- cleanup();
859
- reject(new OAuthError(`OAuth server error: ${error.message}`));
860
- });
861
- server.listen(port, "127.0.0.1", () => {
862
- logger.debug(
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 URLSearchParams({
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 getCallbackUrl();
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 serverPromise;
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 readFileSync4 } from "fs";
1263
- import { fileURLToPath as fileURLToPath5 } from "url";
1264
- import { dirname as dirname4, join as join6 } from "path";
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 = join6(__dirname5, "../../package.json");
1270
- const packageJson2 = JSON.parse(readFileSync4(packageJsonPath2, "utf-8"));
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 __filename5, __dirname5, version;
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
- __filename5 = fileURLToPath5(import.meta.url);
1361
- __dirname5 = dirname4(__filename5);
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 ofetch3 } from "ofetch";
1368
- import { readFileSync as readFileSync5 } from "fs";
1369
- import { fileURLToPath as fileURLToPath6 } from "url";
1370
- import { URLSearchParams as URLSearchParams3 } from "url";
1371
- import { dirname as dirname5, join as join7 } from "path";
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 = join7(__dirname6, "../../package.json");
1375
- const packageJson2 = JSON.parse(readFileSync5(packageJsonPath2, "utf-8"));
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 __filename6, __dirname6, ApiService, apiService;
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
- __filename6 = fileURLToPath6(import.meta.url);
1397
- __dirname6 = dirname5(__filename6);
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 = ofetch3.create({
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 URLSearchParams3({
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 = ofetch3.create({
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 readFileSync6 } from "fs";
1683
- import { fileURLToPath as fileURLToPath7 } from "url";
1684
- import { dirname as dirname6, join as join8 } from "path";
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 URLSearchParams2 } from "url";
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 URLSearchParams2({
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 getCallbackUrl();
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 serverPromise;
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 join5 } from "path";
2330
+ import { join as join7 } from "path";
1959
2331
  function validateGitRepository(path4) {
1960
- const gitDir = join5(path4, ".git");
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("Available subcommands: login, status, bitbucket, gitlab, atlassian");
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 CLI (gh): Authenticated`);
3056
+ logger.info(`${colors.success("\u2713")} GitHub: Authenticated`);
2664
3057
  } else {
2665
- logger.info(`${colors.error("\u2717")} GitHub CLI (gh): Not authenticated`);
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 (gh): Not installed`);
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
- for (const service of services) {
2673
- if (service !== "bragduck") {
2674
- const creds = await storageService.getServiceCredentials(service);
2675
- let methodLabel = "";
2676
- if (creds?.authMethod === "oauth") {
2677
- methodLabel = " (Cloud - OAuth)";
2678
- } else if (creds?.authMethod === "basic") {
2679
- methodLabel = " (Server - API Token)";
2680
- }
2681
- logger.info(`${colors.success("\u2713")} ${service}: Authenticated${methodLabel}`);
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
- if (services.length === 0) {
2686
- logger.info(`Run ${theme.command("bragduck auth login")} to authenticate with Bragduck`);
2687
- logger.log("");
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 boxen6 from "boxen";
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 URLSearchParams4 } from "url";
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 URLSearchParams4({
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-factory.ts
4726
- var AdapterFactory = class {
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 adapter for a specific source type
5241
+ * Get stored Google credentials, auto-refreshing OAuth tokens if expired
4729
5242
  */
4730
- static getAdapter(source, repoPath) {
4731
- switch (source) {
4732
- case "github":
4733
- return repoPath ? new GitHubSyncAdapter(repoPath) : githubSyncAdapter;
4734
- case "bitbucket":
4735
- case "atlassian":
4736
- return repoPath ? new BitbucketSyncAdapter(repoPath) : bitbucketSyncAdapter;
4737
- case "gitlab":
4738
- return repoPath ? new GitLabSyncAdapter(repoPath) : gitlabSyncAdapter;
4739
- case "jira":
4740
- return jiraSyncAdapter;
4741
- case "confluence":
4742
- return confluenceSyncAdapter;
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
- throw new Error(`Unknown source type: ${source}`);
5365
+ return "Doc";
4745
5366
  }
4746
5367
  }
4747
5368
  /**
4748
- * Check if adapter is available for source
5369
+ * Transform a Google Drive file to GitCommit format
4749
5370
  */
4750
- static isSupported(source) {
4751
- return source === "github" || source === "bitbucket" || source === "atlassian" || source === "gitlab" || source === "jira" || source === "confluence";
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/commands/sync.ts
4756
- init_logger();
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/utils/auth-helper.ts
5491
+ // src/sync/slack-adapter.ts
4759
5492
  init_esm_shims();
4760
- init_auth_service();
4761
- import boxen5 from "boxen";
4762
5493
 
4763
- // src/commands/init.ts
5494
+ // src/services/slack.service.ts
4764
5495
  init_esm_shims();
4765
- init_auth_service();
5496
+ init_errors();
4766
5497
  init_logger();
4767
- import ora from "ora";
4768
- import boxen3 from "boxen";
4769
- import chalk5 from "chalk";
4770
- async function initCommand() {
4771
- logger.log("");
4772
- logger.log(
4773
- boxen3(
4774
- chalk5.yellow.bold("\u26A0 Deprecation Notice") + `
4775
-
4776
- The ${chalk5.cyan("init")} command is deprecated.
4777
- Please use ${chalk5.cyan("bragduck auth login")} instead.
4778
-
4779
- ` + chalk5.dim("This command will be removed in v3.0.0"),
4780
- {
4781
- padding: 1,
4782
- borderStyle: "round",
4783
- borderColor: "yellow",
4784
- dimBorder: true
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
- logger.log("");
4789
- logger.info("Starting authentication flow...");
4790
- logger.log("");
4791
- const isAuthenticated = await authService.isAuthenticated();
4792
- if (isAuthenticated) {
4793
- const userInfo = authService.getUserInfo();
4794
- if (userInfo) {
4795
- logger.log(
4796
- boxen3(
4797
- `${chalk5.yellow("Already authenticated!")}
4798
-
4799
- ${chalk5.gray("User:")} ${userInfo.name}
4800
- ${chalk5.gray("Email:")} ${userInfo.email}
4801
-
4802
- ${chalk5.dim("Run")} ${chalk5.cyan("bragduck logout")} ${chalk5.dim("to sign out")}`,
4803
- {
4804
- padding: 1,
4805
- margin: 1,
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
- const spinner = ora("Opening browser for authentication...").start();
4819
- try {
4820
- spinner.text = "Waiting for authentication...";
4821
- const userInfo = await authService.login();
4822
- spinner.succeed("Authentication successful!");
4823
- logger.log("");
4824
- logger.log(
4825
- boxen3(
4826
- `${chalk5.green.bold("\u2713 Successfully authenticated!")}
4827
-
4828
- ${chalk5.gray("Welcome,")} ${chalk5.cyan(userInfo.name)}
4829
- ${chalk5.gray("Email:")} ${userInfo.email}
4830
-
4831
- ${chalk5.dim("You can now use")} ${chalk5.cyan("bragduck scan")} ${chalk5.dim("to create brags!")}`,
4832
- {
4833
- padding: 1,
4834
- margin: 1,
4835
- borderStyle: "round",
4836
- borderColor: "green"
4837
- }
4838
- )
4839
- );
4840
- logger.log("");
4841
- globalThis.setTimeout(() => {
4842
- process.exit(0);
4843
- }, 100);
4844
- return;
4845
- } catch (error) {
4846
- spinner.fail("Authentication failed");
4847
- logger.log("");
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
- process.exit(1);
5588
+ });
5589
+ const data = await response.json();
5590
+ return data.ok ? data.user : null;
5591
+ } catch {
5592
+ return null;
5593
+ }
4865
5594
  }
4866
- }
4867
- function getErrorHint(error) {
4868
- if (error.name === "OAuthError") {
4869
- if (error.message.includes("timeout")) {
4870
- return "Try again and complete the authentication within 2 minutes";
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
- if (error.message.includes("CSRF")) {
4873
- return "This might be a security issue. Try running the command again";
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
- return "Check your internet connection and try again";
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
- if (error.name === "NetworkError") {
4878
- return "Check your internet connection and firewall settings";
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
- if (error.name === "AuthenticationError") {
4881
- return "Verify your credentials and try again";
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
- return "Try running the command again or check the logs with DEBUG=* bragduck init";
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 boxen4 from "boxen";
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
- const original = isNewBragType ? typeof brag.original_input === "string" ? brag.original_input.split("\n")[0] || "" : "Raw input" : brag.original_message.split("\n")[0] || "";
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(boxen4(bragDetails, boxStyles.info));
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
- boxen5(
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 initCommand();
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 ora2 from "ora";
5337
- function createSpinner(text) {
5338
- return ora2({
6200
+ import ora from "ora";
6201
+ function createFetchSpinner(text) {
6202
+ return ora({
5339
6203
  text,
5340
6204
  color: "cyan",
5341
- spinner: "dots"
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 ora2({
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 createSpinner("Fetching your brags...");
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 createSpinner2 = createStepSpinner(
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
- createSpinner2.start();
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
- createSpinner2,
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(createSpinner2, 4, TOTAL_STEPS, "Failed to create brags");
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 = service.charAt(0).toUpperCase() + service.slice(1);
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((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(", ");
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 repoSpinner = createStepSpinner(2, TOTAL_STEPS, "Validating repository");
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
- logger.log("");
5933
- logger.info(
5934
- colors.dim(
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 createSpinner2 = createStepSpinner(
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
- createSpinner2.start();
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
- createSpinner2,
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(createSpinner2, 5, TOTAL_STEPS, "Failed to create brags");
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 = service.charAt(0).toUpperCase() + service.slice(1);
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 = result.service.charAt(0).toUpperCase() + result.service.slice(1);
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 = result.service.charAt(0).toUpperCase() + result.service.slice(1);
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(boxen6(formatSuccessMessage(totalCreated, allCreatedBrags), boxStyles.success));
7183
+ logger.log(boxen5(formatSuccessMessage(totalCreated, allCreatedBrags), boxStyles.success));
6264
7184
  } else if (successful.length > 0) {
6265
7185
  logger.log(
6266
- boxen6(
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 subscriptionStatus = await apiService.getSubscriptionStatus();
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
- boxen6(
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("Supported sources: github, gitlab, bitbucket, atlassian, jira, confluence");
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(boxen6(formatMultiRepoSummary(result2), boxStyles.success));
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
- boxen6(formatSuccessMessage(result.created, result.createdBrags), boxStyles.success)
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
- boxen6(formatErrorMessage(err.message, getErrorHint2(err, sourceType)), boxStyles.error)
7427
+ boxen5(formatErrorMessage(err.message, getErrorHint(err, sourceType)), boxStyles.error)
6410
7428
  );
6411
7429
  process.exit(1);
6412
7430
  }
6413
7431
  }
6414
- function getErrorHint2(error, sourceType) {
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 boxen7 from "boxen";
6442
- import chalk6 from "chalk";
6443
- import ora3 from "ora";
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
- boxen7(
6449
- `${chalk6.yellow("Not currently authenticated")}
7486
+ boxen6(
7487
+ `${chalk5.yellow("Not currently authenticated")}
6450
7488
 
6451
- ${chalk6.dim("Nothing to logout from")}`,
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? (${chalk6.cyan(userName)})`,
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 = ora3("Logging out...").start();
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
- boxen7(
6480
- `${chalk6.green.bold("\u2713 Logged out successfully")}
7517
+ boxen6(
7518
+ `${chalk5.green.bold("\u2713 Logged out successfully")}
6481
7519
 
6482
- ${chalk6.dim("Your credentials have been cleared")}
7520
+ ${chalk5.dim("Your credentials have been cleared")}
6483
7521
 
6484
- ${chalk6.dim("Run")} ${chalk6.cyan("bragduck init")} ${chalk6.dim("to login again")}`,
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 boxen9 from "boxen";
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(boxen9(formatErrorMessage(err.message, getErrorHint4(err)), boxStyles.error));
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.repository ? truncateText(extractRepoName(brag.repository), 25) : colors.dim("none");
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 getErrorHint4(error) {
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 boxen10 from "boxen";
6925
- import chalk8 from "chalk";
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
- boxen10(formatErrorMessage(err.message, getConfigHint(err)), {
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: [chalk8.cyan("Key"), chalk8.cyan("Value"), chalk8.cyan("Default")],
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
- chalk8.white("defaultCommitDays"),
6991
- chalk8.yellow(String(config2.defaultCommitDays)),
6992
- chalk8.dim(String(DEFAULT_CONFIG.defaultCommitDays))
7747
+ chalk6.white("defaultCommitDays"),
7748
+ chalk6.yellow(String(config2.defaultCommitDays)),
7749
+ chalk6.dim(String(DEFAULT_CONFIG.defaultCommitDays))
6993
7750
  ]);
6994
7751
  table.push([
6995
- chalk8.white("autoVersionCheck"),
6996
- chalk8.yellow(String(config2.autoVersionCheck)),
6997
- chalk8.dim(String(DEFAULT_CONFIG.autoVersionCheck))
7752
+ chalk6.white("autoVersionCheck"),
7753
+ chalk6.yellow(String(config2.autoVersionCheck)),
7754
+ chalk6.dim(String(DEFAULT_CONFIG.autoVersionCheck))
6998
7755
  ]);
6999
7756
  table.push([
7000
- chalk8.white("defaultSource"),
7001
- chalk8.yellow(config2.defaultSource || "not set"),
7002
- chalk8.dim("not set")
7757
+ chalk6.white("defaultSource"),
7758
+ chalk6.yellow(config2.defaultSource || "not set"),
7759
+ chalk6.dim("not set")
7003
7760
  ]);
7004
7761
  table.push([
7005
- chalk8.white("sourcePriority"),
7006
- chalk8.yellow(config2.sourcePriority ? config2.sourcePriority.join(", ") : "not set"),
7007
- chalk8.dim("not set")
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
- chalk8.white("jiraInstance"),
7011
- chalk8.yellow(config2.jiraInstance || "not set"),
7012
- chalk8.dim("not set")
7767
+ chalk6.white("jiraInstance"),
7768
+ chalk6.yellow(config2.jiraInstance || "not set"),
7769
+ chalk6.dim("not set")
7013
7770
  ]);
7014
7771
  table.push([
7015
- chalk8.white("confluenceInstance"),
7016
- chalk8.yellow(config2.confluenceInstance || "not set"),
7017
- chalk8.dim("not set")
7772
+ chalk6.white("confluenceInstance"),
7773
+ chalk6.yellow(config2.confluenceInstance || "not set"),
7774
+ chalk6.dim("not set")
7018
7775
  ]);
7019
7776
  table.push([
7020
- chalk8.white("gitlabInstance"),
7021
- chalk8.yellow(config2.gitlabInstance || "not set"),
7022
- chalk8.dim("not set")
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(chalk8.dim("To change a value: ") + chalk8.cyan("bragduck config set <key> <value>"));
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 ${chalk8.cyan(key)}:`);
7792
+ logger.info(`Configuration for ${chalk6.cyan(key)}:`);
7036
7793
  logger.log("");
7037
- logger.log(` ${chalk8.white("Current:")} ${chalk8.yellow(String(value))}`);
7038
- logger.log(` ${chalk8.white("Default:")} ${chalk8.dim(String(defaultValue))}`);
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(chalk8.dim("Using default value"));
7798
+ logger.info(chalk6.dim("Using default value"));
7042
7799
  } else {
7043
7800
  logger.info(
7044
- chalk8.dim("Custom value set. Reset with: ") + chalk8.cyan(`bragduck config set ${key} ${defaultValue}`)
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
- boxen10(
7055
- `${chalk8.green.bold("\u2713 Configuration updated")}
7811
+ boxen8(
7812
+ `${chalk6.green.bold("\u2713 Configuration updated")}
7056
7813
 
7057
- ${chalk8.white(key)}: ${chalk8.yellow(String(typedValue))}`,
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 __filename7 = fileURLToPath7(import.meta.url);
7155
- var __dirname7 = dirname6(__filename7);
7156
- var packageJsonPath = join8(__dirname7, "../../package.json");
7157
- var packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
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: bd, duck, brag").version(packageJson.version, "-v, --version", "Display version number").helpOption("-h, --help", "Display help information").option("--skip-version-check", "Skip automatic version check on startup").option("--debug", "Enable debug mode (shows detailed logs)");
7160
- program.command("auth [subcommand]").description("Manage authentication (subcommands: login, status, bitbucket, gitlab)").action(async (subcommand) => {
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);