@bragduck/cli 2.30.2 → 2.36.3

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