@bragduck/cli 2.39.0 → 2.40.0

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.
@@ -109,6 +109,9 @@ var init_constants = __esm({
109
109
  CREATE: "/v1/users/{userId}/stories",
110
110
  DELETE: "/v1/users/{userId}/stories/{storyId}"
111
111
  },
112
+ INTEGRATIONS: {
113
+ ACCESS_TOKEN: "/v1/integrations/{provider}/access-token"
114
+ },
112
115
  VERSION: "/v1/cli/version",
113
116
  /**
114
117
  * @deprecated Use BRAGS.REFINE instead
@@ -193,7 +196,7 @@ var init_env_loader = __esm({
193
196
  });
194
197
 
195
198
  // src/utils/errors.ts
196
- var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError, JiraError, ConfluenceError, AtlassianError, GoogleDocsError, SlackError;
199
+ var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError, JiraError, ConfluenceError, AtlassianError, GoogleDocsError, SlackError, IntegrationError;
197
200
  var init_errors = __esm({
198
201
  "src/utils/errors.ts"() {
199
202
  "use strict";
@@ -302,6 +305,12 @@ var init_errors = __esm({
302
305
  this.name = "SlackError";
303
306
  }
304
307
  };
308
+ IntegrationError = class extends BragduckError {
309
+ constructor(message, details) {
310
+ super(message, "INTEGRATION_ERROR", details);
311
+ this.name = "IntegrationError";
312
+ }
313
+ };
305
314
  }
306
315
  });
307
316
 
@@ -1300,8 +1309,8 @@ __export(version_exports, {
1300
1309
  import { readFileSync as readFileSync6 } from "fs";
1301
1310
  import { fileURLToPath as fileURLToPath7 } from "url";
1302
1311
  import { dirname as dirname6, join as join8 } from "path";
1303
- import chalk4 from "chalk";
1304
- import boxen2 from "boxen";
1312
+ import chalk2 from "chalk";
1313
+ import boxen from "boxen";
1305
1314
  function getCurrentVersion() {
1306
1315
  try {
1307
1316
  const packageJsonPath2 = join8(__dirname7, "../../package.json");
@@ -1361,16 +1370,16 @@ async function checkForUpdates(options = {}) {
1361
1370
  }
1362
1371
  }
1363
1372
  function displayUpdateNotification(currentVersion, latestVersion, critical = false) {
1364
- const urgency = critical ? chalk4.red.bold("CRITICAL UPDATE") : chalk4.yellow.bold("Update Available");
1373
+ const urgency = critical ? chalk2.red.bold("CRITICAL UPDATE") : chalk2.yellow.bold("Update Available");
1365
1374
  const message = `${urgency}
1366
1375
 
1367
- Current version: ${chalk4.dim(currentVersion)}
1368
- Latest version: ${chalk4.green(latestVersion)}
1376
+ Current version: ${chalk2.dim(currentVersion)}
1377
+ Latest version: ${chalk2.green(latestVersion)}
1369
1378
 
1370
- Update with: ${chalk4.cyan("npm install -g @bragduck/cli@latest")}`;
1379
+ Update with: ${chalk2.cyan("npm install -g @bragduck/cli@latest")}`;
1371
1380
  console.log("");
1372
1381
  console.log(
1373
- boxen2(message, {
1382
+ boxen(message, {
1374
1383
  padding: 1,
1375
1384
  margin: { top: 0, right: 1, bottom: 0, left: 1 },
1376
1385
  borderStyle: "round",
@@ -1422,7 +1431,7 @@ function getPlatformInfo() {
1422
1431
  const arch = process.arch;
1423
1432
  return `${platform}-${arch}`;
1424
1433
  }
1425
- var __filename8, __dirname8, ApiService, apiService;
1434
+ var __filename8, __dirname8, REFINE_BATCH_SIZE, CREATE_BATCH_SIZE, inBatches, ApiService, apiService;
1426
1435
  var init_api_service = __esm({
1427
1436
  "src/services/api.service.ts"() {
1428
1437
  "use strict";
@@ -1433,6 +1442,15 @@ var init_api_service = __esm({
1433
1442
  init_logger();
1434
1443
  __filename8 = fileURLToPath8(import.meta.url);
1435
1444
  __dirname8 = dirname7(__filename8);
1445
+ REFINE_BATCH_SIZE = 20;
1446
+ CREATE_BATCH_SIZE = 50;
1447
+ inBatches = (items, size) => {
1448
+ const batches = [];
1449
+ for (let start = 0; start < items.length; start += size) {
1450
+ batches.push(items.slice(start, start + size));
1451
+ }
1452
+ return batches;
1453
+ };
1436
1454
  ApiService = class {
1437
1455
  baseURL;
1438
1456
  client;
@@ -1548,10 +1566,23 @@ var init_api_service = __esm({
1548
1566
  };
1549
1567
  logger.debug(`Refining ${trimmedRequest.brags.length} brags`);
1550
1568
  try {
1551
- const response = await this.makeRequest(API_ENDPOINTS.BRAGS.REFINE, {
1552
- method: "POST",
1553
- body: trimmedRequest
1554
- });
1569
+ const response = {
1570
+ refined_brags: [],
1571
+ processing_time_ms: 0,
1572
+ tokens_used: 0
1573
+ };
1574
+ for (const batch of inBatches(trimmedRequest.brags, REFINE_BATCH_SIZE)) {
1575
+ const batchResponse = await this.makeRequest(
1576
+ API_ENDPOINTS.BRAGS.REFINE,
1577
+ {
1578
+ method: "POST",
1579
+ body: { ...trimmedRequest, brags: batch }
1580
+ }
1581
+ );
1582
+ response.refined_brags.push(...batchResponse.refined_brags);
1583
+ response.processing_time_ms += batchResponse.processing_time_ms;
1584
+ response.tokens_used += batchResponse.tokens_used;
1585
+ }
1555
1586
  logger.debug(`Successfully refined ${response.refined_brags.length} brags`);
1556
1587
  return response;
1557
1588
  } catch (_error) {
@@ -1585,10 +1616,18 @@ var init_api_service = __esm({
1585
1616
  logger.debug(`Creating ${request.brags.length} brags`);
1586
1617
  logger.debug(`Request body: ${JSON.stringify(request, null, 2)}`);
1587
1618
  try {
1588
- const response = await this.makeRequest(API_ENDPOINTS.BRAGS.CREATE, {
1589
- method: "POST",
1590
- body: request
1591
- });
1619
+ const response = { created: 0, brags: [] };
1620
+ for (const batch of inBatches(request.brags, CREATE_BATCH_SIZE)) {
1621
+ const batchResponse = await this.makeRequest(
1622
+ API_ENDPOINTS.BRAGS.CREATE,
1623
+ {
1624
+ method: "POST",
1625
+ body: { ...request, brags: batch }
1626
+ }
1627
+ );
1628
+ response.created += batchResponse.created;
1629
+ response.brags.push(...batchResponse.brags);
1630
+ }
1592
1631
  logger.debug(`Response: ${JSON.stringify(response, null, 2)}`);
1593
1632
  logger.debug(`Successfully created ${response.created} brags`);
1594
1633
  return response;
@@ -1729,6 +1768,18 @@ var init_api_service = __esm({
1729
1768
  throw error;
1730
1769
  }
1731
1770
  }
1771
+ /**
1772
+ * Get the access token of a tool connected on the BragDuck web app.
1773
+ * The server refreshes it; error statuses are not retried.
1774
+ */
1775
+ async getIntegrationAccessToken(provider) {
1776
+ logger.debug(`Fetching web-connected ${provider} access token`);
1777
+ const url = API_ENDPOINTS.INTEGRATIONS.ACCESS_TOKEN.replace("{provider}", provider);
1778
+ return this.makeRequest(url, {
1779
+ method: "GET",
1780
+ retry: 0
1781
+ });
1782
+ }
1732
1783
  /**
1733
1784
  * Check for CLI updates
1734
1785
  */
@@ -1790,8 +1841,8 @@ import { dirname as dirname8, join as join10 } from "path";
1790
1841
  // src/commands/auth.ts
1791
1842
  init_esm_shims();
1792
1843
  init_auth_service();
1793
- import boxen from "boxen";
1794
- import chalk3 from "chalk";
1844
+ import boxen2 from "boxen";
1845
+ import chalk4 from "chalk";
1795
1846
  import { input } from "@inquirer/prompts";
1796
1847
 
1797
1848
  // src/services/atlassian-auth.service.ts
@@ -2629,20 +2680,123 @@ var GitService = class {
2629
2680
  };
2630
2681
  var gitService = new GitService();
2631
2682
 
2683
+ // src/services/integrations.service.ts
2684
+ init_esm_shims();
2685
+ init_api_service();
2686
+ init_auth_service();
2687
+ init_storage_service();
2688
+ init_errors();
2689
+ init_logger();
2690
+ var WEB_APP_HOST = "app.bragduck.com";
2691
+ var EXPIRY_MARGIN_MS = 60 * 1e3;
2692
+ var PROVIDER_LABELS = {
2693
+ atlassian: "Atlassian",
2694
+ github: "GitHub",
2695
+ slack: "Slack",
2696
+ google: "Google Docs"
2697
+ };
2698
+ function toHost(url) {
2699
+ return url.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/+$/, "");
2700
+ }
2701
+ var IntegrationsService = class {
2702
+ cache = /* @__PURE__ */ new Map();
2703
+ loggedSites = /* @__PURE__ */ new Set();
2704
+ /**
2705
+ * Get the access token for a web-connected tool.
2706
+ * Returns null when the user is not logged in to BragDuck or the tool is not connected.
2707
+ */
2708
+ async getConnectedAccessToken(provider) {
2709
+ const cached = this.cache.get(provider);
2710
+ if (cached && this.isFresh(cached)) {
2711
+ return cached;
2712
+ }
2713
+ if (!await authService.isAuthenticated()) {
2714
+ return null;
2715
+ }
2716
+ const label = PROVIDER_LABELS[provider];
2717
+ try {
2718
+ const connection = await apiService.getIntegrationAccessToken(provider);
2719
+ this.cache.set(provider, connection);
2720
+ logger.debug(`Using ${label} connected via web (${connection.accountName})`);
2721
+ return connection;
2722
+ } catch (error) {
2723
+ if (!(error instanceof ApiError)) {
2724
+ throw error;
2725
+ }
2726
+ if (error.statusCode === 404) {
2727
+ logger.debug(`${label} is not connected on the web`);
2728
+ return null;
2729
+ }
2730
+ if (error.statusCode === 409) {
2731
+ throw new IntegrationError(
2732
+ `Your ${label} connection has expired. Reconnect ${label} at ${WEB_APP_HOST}`,
2733
+ { hint: `Reconnect ${label} at https://${WEB_APP_HOST}` }
2734
+ );
2735
+ }
2736
+ if (error.statusCode === 502) {
2737
+ throw new IntegrationError(`Could not refresh your ${label} token. Try again shortly.`, {
2738
+ hint: `If this keeps happening, reconnect ${label} at https://${WEB_APP_HOST}`
2739
+ });
2740
+ }
2741
+ throw error;
2742
+ }
2743
+ }
2744
+ /**
2745
+ * Build Jira/Confluence OAuth credentials from the web-connected Atlassian account.
2746
+ * Uses the site matching the configured instance, otherwise the first site.
2747
+ */
2748
+ async getAtlassianCredentials(service) {
2749
+ const connection = await this.getConnectedAccessToken("atlassian");
2750
+ const sites = connection?.metadata.sites ?? [];
2751
+ if (!connection || sites.length === 0) {
2752
+ return null;
2753
+ }
2754
+ const configuredInstance = await storageService.getConfigWithHierarchy(`${service}Instance`);
2755
+ const matchingSite = configuredInstance ? sites.find((s) => toHost(s.url) === toHost(configuredInstance)) : void 0;
2756
+ const site = matchingSite ?? sites[0];
2757
+ if (sites.length > 1 && !this.loggedSites.has(site.id)) {
2758
+ this.loggedSites.add(site.id);
2759
+ logger.info(`Using Atlassian site ${site.name} (${site.url}) connected via web`);
2760
+ }
2761
+ return {
2762
+ accessToken: connection.accessToken,
2763
+ cloudId: site.id,
2764
+ instanceUrl: site.url,
2765
+ authMethod: "oauth"
2766
+ };
2767
+ }
2768
+ isFresh(connection) {
2769
+ if (!connection.expiresAt) {
2770
+ return true;
2771
+ }
2772
+ return new Date(connection.expiresAt).getTime() - EXPIRY_MARGIN_MS > Date.now();
2773
+ }
2774
+ };
2775
+ var integrationsService = new IntegrationsService();
2776
+
2632
2777
  // src/services/github.service.ts
2633
2778
  var execAsync2 = promisify2(exec2);
2634
2779
  var GitHubService = class {
2635
2780
  MAX_BODY_LENGTH = 5e3;
2636
2781
  PR_SEARCH_FIELDS = "number,title,body,author,mergedAt,additions,deletions,changedFiles,url,labels";
2782
+ // Set when gh has no login of its own and GitHub is connected on the BragDuck web app
2783
+ useWebConnection = false;
2637
2784
  /**
2638
2785
  * Execute a gh CLI command with clean environment
2639
2786
  * Unsets GITHUB_TOKEN and GH_TOKEN to prevent .envrc tokens from interfering
2640
- * with gh CLI's own authentication
2787
+ * with gh CLI's own authentication. When using the web connection, GH_TOKEN
2788
+ * is set to the server-held token instead.
2641
2789
  */
2642
2790
  async execGhCommand(command, cwd) {
2643
2791
  const env = { ...process.env };
2644
2792
  delete env.GITHUB_TOKEN;
2645
2793
  delete env.GH_TOKEN;
2794
+ if (this.useWebConnection) {
2795
+ const connection = await integrationsService.getConnectedAccessToken("github");
2796
+ if (connection) {
2797
+ env.GH_TOKEN = connection.accessToken;
2798
+ }
2799
+ }
2646
2800
  return execAsync2(command, { env, cwd: cwd || process.cwd() });
2647
2801
  }
2648
2802
  /**
@@ -2668,14 +2822,30 @@ var GitHubService = class {
2668
2822
  }
2669
2823
  }
2670
2824
  /**
2671
- * Check if user is authenticated with GitHub CLI
2825
+ * Check if user is authenticated with GitHub CLI, falling back to
2826
+ * the GitHub account connected on the BragDuck web app
2672
2827
  */
2673
2828
  async checkAuthentication() {
2674
2829
  try {
2675
2830
  await this.execGhCommand("command gh auth status");
2676
2831
  return true;
2677
2832
  } catch {
2678
- return false;
2833
+ return this.checkWebConnection();
2834
+ }
2835
+ }
2836
+ /**
2837
+ * Use the server-held GitHub token when GitHub is connected on the web
2838
+ */
2839
+ async checkWebConnection() {
2840
+ try {
2841
+ const connection = await integrationsService.getConnectedAccessToken("github");
2842
+ this.useWebConnection = connection !== null;
2843
+ return this.useWebConnection;
2844
+ } catch (error) {
2845
+ if (error instanceof IntegrationError) {
2846
+ throw new GitHubError(error.message, error.details);
2847
+ }
2848
+ throw error;
2679
2849
  }
2680
2850
  }
2681
2851
  /**
@@ -2699,6 +2869,9 @@ var GitHubService = class {
2699
2869
  return { installed: false, authenticated: false };
2700
2870
  }
2701
2871
  const authenticated = await this.checkAuthentication();
2872
+ if (this.useWebConnection) {
2873
+ return { installed, authenticated, connectedViaWeb: true };
2874
+ }
2702
2875
  return { installed, authenticated };
2703
2876
  }
2704
2877
  /**
@@ -2873,33 +3046,33 @@ init_logger();
2873
3046
 
2874
3047
  // src/ui/theme.ts
2875
3048
  init_esm_shims();
2876
- import chalk2 from "chalk";
3049
+ import chalk3 from "chalk";
2877
3050
  var colors = {
2878
3051
  // Primary colors for main actions and interactive elements
2879
- primary: chalk2.cyan,
3052
+ primary: chalk3.cyan,
2880
3053
  // Success states
2881
- success: chalk2.green,
2882
- successBold: chalk2.green.bold,
3054
+ success: chalk3.green,
3055
+ successBold: chalk3.green.bold,
2883
3056
  // Warning states
2884
- warning: chalk2.yellow,
2885
- warningBold: chalk2.yellow.bold,
3057
+ warning: chalk3.yellow,
3058
+ warningBold: chalk3.yellow.bold,
2886
3059
  // Error states
2887
- error: chalk2.red,
2888
- errorBold: chalk2.red.bold,
3060
+ error: chalk3.red,
3061
+ errorBold: chalk3.red.bold,
2889
3062
  // Info and metadata
2890
- info: chalk2.gray,
2891
- infoDim: chalk2.dim,
3063
+ info: chalk3.gray,
3064
+ infoDim: chalk3.dim,
2892
3065
  // Highlighted/important data
2893
- highlight: chalk2.yellow.bold,
2894
- highlightCyan: chalk2.cyan.bold,
3066
+ highlight: chalk3.yellow.bold,
3067
+ highlightCyan: chalk3.cyan.bold,
2895
3068
  // Text emphasis
2896
- bold: chalk2.bold,
2897
- dim: chalk2.dim,
3069
+ bold: chalk3.bold,
3070
+ dim: chalk3.dim,
2898
3071
  // Special purpose
2899
- white: chalk2.white,
2900
- gray: chalk2.gray,
3072
+ white: chalk3.white,
3073
+ gray: chalk3.gray,
2901
3074
  // Links and URLs
2902
- link: chalk2.blue.underline
3075
+ link: chalk3.blue.underline
2903
3076
  };
2904
3077
  var theme = {
2905
3078
  /**
@@ -3068,13 +3241,13 @@ async function authLogin() {
3068
3241
  const userInfo = authService.getUserInfo();
3069
3242
  if (userInfo) {
3070
3243
  logger.log(
3071
- boxen(
3072
- `${chalk3.yellow("Already authenticated!")}
3244
+ boxen2(
3245
+ `${chalk4.yellow("Already authenticated!")}
3073
3246
 
3074
- ${chalk3.gray("User:")} ${userInfo.name}
3075
- ${chalk3.gray("Email:")} ${userInfo.email}
3247
+ ${chalk4.gray("User:")} ${userInfo.name}
3248
+ ${chalk4.gray("Email:")} ${userInfo.email}
3076
3249
 
3077
- ${chalk3.dim("Run")} ${chalk3.cyan("bragduck logout")} ${chalk3.dim("to sign out")}`,
3250
+ ${chalk4.dim("Run")} ${chalk4.cyan("bragduck logout")} ${chalk4.dim("to sign out")}`,
3078
3251
  {
3079
3252
  padding: 1,
3080
3253
  margin: 1,
@@ -3091,13 +3264,13 @@ ${chalk3.dim("Run")} ${chalk3.cyan("bragduck logout")} ${chalk3.dim("to sign out
3091
3264
  const userInfo = await authService.login();
3092
3265
  logger.log("");
3093
3266
  logger.log(
3094
- boxen(
3095
- `${chalk3.green.bold("\u2713 Successfully authenticated!")}
3267
+ boxen2(
3268
+ `${chalk4.green.bold("\u2713 Successfully authenticated!")}
3096
3269
 
3097
- ${chalk3.gray("Welcome,")} ${chalk3.cyan(userInfo.name)}
3098
- ${chalk3.gray("Email:")} ${userInfo.email}
3270
+ ${chalk4.gray("Welcome,")} ${chalk4.cyan(userInfo.name)}
3271
+ ${chalk4.gray("Email:")} ${userInfo.email}
3099
3272
 
3100
- ${chalk3.dim("Use")} ${chalk3.cyan("bragduck sync")} ${chalk3.dim("to create brags")}`,
3273
+ ${chalk4.dim("Use")} ${chalk4.cyan("bragduck sync")} ${chalk4.dim("to create brags")}`,
3101
3274
  {
3102
3275
  padding: 1,
3103
3276
  margin: 1,
@@ -3111,7 +3284,7 @@ ${chalk3.dim("Use")} ${chalk3.cyan("bragduck sync")} ${chalk3.dim("to create bra
3111
3284
  const err = error;
3112
3285
  logger.log("");
3113
3286
  logger.log(
3114
- boxen(`${chalk3.red.bold("\u2717 Authentication Failed")}
3287
+ boxen2(`${chalk4.red.bold("\u2717 Authentication Failed")}
3115
3288
 
3116
3289
  ${err.message}`, {
3117
3290
  padding: 1,
@@ -3123,6 +3296,12 @@ ${err.message}`, {
3123
3296
  process.exit(1);
3124
3297
  }
3125
3298
  }
3299
+ async function getWebConnection(provider) {
3300
+ return integrationsService.getConnectedAccessToken(provider).catch((error) => {
3301
+ logger.debug(`Could not check web-connected ${provider}: ${error}`);
3302
+ return null;
3303
+ });
3304
+ }
3126
3305
  async function authStatus() {
3127
3306
  logger.log("");
3128
3307
  logger.info("Authentication Status:");
@@ -3138,9 +3317,14 @@ async function authStatus() {
3138
3317
  logger.log("");
3139
3318
  logger.info("Git Sources:");
3140
3319
  logger.log("");
3141
- const ghStatus = await githubService.getAuthStatus();
3320
+ const ghStatus = await githubService.getAuthStatus().catch((error) => {
3321
+ logger.debug(`Could not check GitHub auth status: ${error}`);
3322
+ return { installed: true, authenticated: false, connectedViaWeb: false };
3323
+ });
3142
3324
  if (ghStatus.installed) {
3143
- if (ghStatus.authenticated) {
3325
+ if (ghStatus.connectedViaWeb) {
3326
+ logger.info(`${colors.success("\u2713")} GitHub: connected via web`);
3327
+ } else if (ghStatus.authenticated) {
3144
3328
  logger.info(`${colors.success("\u2713")} GitHub: Authenticated`);
3145
3329
  } else {
3146
3330
  logger.info(`${colors.error("\u2717")} GitHub: Not authenticated`);
@@ -3185,8 +3369,15 @@ async function authStatus() {
3185
3369
  if (jiraAuth) logger.info(theme.secondary(" \xB7 Jira"));
3186
3370
  if (confluenceAuth) logger.info(theme.secondary(" \xB7 Confluence"));
3187
3371
  } else {
3188
- logger.info(`${colors.error("\u2717")} Atlassian (Jira + Confluence): Not connected`);
3189
- logger.info(theme.secondary(" Run: bragduck auth atlassian"));
3372
+ const webAtlassian = await getWebConnection("atlassian");
3373
+ if (webAtlassian) {
3374
+ logger.info(
3375
+ `${colors.success("\u2713")} Jira/Confluence: connected via web (${webAtlassian.accountName})`
3376
+ );
3377
+ } else {
3378
+ logger.info(`${colors.error("\u2717")} Atlassian (Jira + Confluence): Not connected`);
3379
+ logger.info(theme.secondary(" Run: bragduck auth atlassian"));
3380
+ }
3190
3381
  }
3191
3382
  logger.log("");
3192
3383
  logger.info("Document Sources:");
@@ -3195,8 +3386,15 @@ async function authStatus() {
3195
3386
  if (googleDocsAuth) {
3196
3387
  logger.info(`${colors.success("\u2713")} Google Docs (OAuth)`);
3197
3388
  } else {
3198
- logger.info(`${colors.error("\u2717")} Google Docs: Not connected`);
3199
- logger.info(theme.secondary(" Run: bragduck auth google"));
3389
+ const webGoogle = await getWebConnection("google");
3390
+ if (webGoogle) {
3391
+ logger.info(
3392
+ `${colors.success("\u2713")} Google Docs: connected via web (${webGoogle.accountName})`
3393
+ );
3394
+ } else {
3395
+ logger.info(`${colors.error("\u2717")} Google Docs: Not connected`);
3396
+ logger.info(theme.secondary(" Run: bragduck auth google"));
3397
+ }
3200
3398
  }
3201
3399
  const slackAuth = await storageService.isServiceAuthenticated("slack");
3202
3400
  if (slackAuth) {
@@ -3204,15 +3402,20 @@ async function authStatus() {
3204
3402
  const userLabel = creds?.username ? ` (user: ${creds.username})` : "";
3205
3403
  logger.info(`${colors.success("\u2713")} Slack: Connected${userLabel}`);
3206
3404
  } else {
3207
- logger.info(`${colors.error("\u2717")} Slack: Not connected`);
3208
- logger.info(theme.secondary(" Run: bragduck auth slack"));
3405
+ const webSlack = await getWebConnection("slack");
3406
+ if (webSlack) {
3407
+ logger.info(`${colors.success("\u2713")} Slack: connected via web (${webSlack.accountName})`);
3408
+ } else {
3409
+ logger.info(`${colors.error("\u2717")} Slack: Not connected`);
3410
+ logger.info(theme.secondary(" Run: bragduck auth slack"));
3411
+ }
3209
3412
  }
3210
3413
  logger.log("");
3211
3414
  }
3212
3415
  async function authBitbucket() {
3213
3416
  logger.log("");
3214
3417
  logger.log(
3215
- boxen(
3418
+ boxen2(
3216
3419
  theme.info("Bitbucket API Token Authentication") + "\n\nCreate an API Token at:\n" + colors.highlight("https://bitbucket.org/account/settings/api-token/new") + "\n\nRequired scopes:\n \u2022 pullrequest:read\n \u2022 repository:read\n \u2022 account:read\n\n" + theme.warning("Note: API tokens expire (max 1 year)"),
3217
3420
  boxStyles.info
3218
3421
  )
@@ -3243,7 +3446,7 @@ async function authBitbucket() {
3243
3446
  if (!response.ok) {
3244
3447
  logger.log("");
3245
3448
  logger.log(
3246
- boxen(
3449
+ boxen2(
3247
3450
  theme.error("\u2717 Authentication Failed") + "\n\nInvalid email or API token",
3248
3451
  boxStyles.error
3249
3452
  )
@@ -3264,7 +3467,7 @@ async function authBitbucket() {
3264
3467
  await storageService.setServiceCredentials("bitbucket", credentials);
3265
3468
  logger.log("");
3266
3469
  logger.log(
3267
- boxen(
3470
+ boxen2(
3268
3471
  theme.success("\u2713 Successfully authenticated with Bitbucket") + `
3269
3472
 
3270
3473
  Email: ${email}
@@ -3278,7 +3481,7 @@ User: ${user.display_name}
3278
3481
  const err = error;
3279
3482
  logger.log("");
3280
3483
  logger.log(
3281
- boxen(
3484
+ boxen2(
3282
3485
  theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
3283
3486
  boxStyles.error
3284
3487
  )
@@ -3290,7 +3493,7 @@ User: ${user.display_name}
3290
3493
  async function authGitLab() {
3291
3494
  logger.log("");
3292
3495
  logger.log(
3293
- boxen(
3496
+ boxen2(
3294
3497
  theme.info("GitLab Personal Access Token Authentication") + "\n\nCreate a Personal Access Token at:\n" + colors.highlight("https://gitlab.com/-/profile/personal_access_tokens") + "\n\nRequired scopes:\n \u2022 api (full API access)\n \u2022 read_api (read API)\n \u2022 read_user (read user info)\n\n" + theme.warning("For self-hosted GitLab, check your instance docs"),
3295
3498
  boxStyles.info
3296
3499
  )
@@ -3312,7 +3515,7 @@ async function authGitLab() {
3312
3515
  if (!response.ok) {
3313
3516
  logger.log("");
3314
3517
  logger.log(
3315
- boxen(
3518
+ boxen2(
3316
3519
  theme.error("\u2717 Authentication Failed") + "\n\nInvalid instance URL or access token",
3317
3520
  boxStyles.error
3318
3521
  )
@@ -3328,7 +3531,7 @@ async function authGitLab() {
3328
3531
  await storageService.setServiceCredentials("gitlab", credentials);
3329
3532
  logger.log("");
3330
3533
  logger.log(
3331
- boxen(
3534
+ boxen2(
3332
3535
  theme.success("\u2713 Successfully authenticated with GitLab") + `
3333
3536
 
3334
3537
  Instance: ${instanceUrl}
@@ -3341,7 +3544,7 @@ User: ${user.name} (@${user.username})`,
3341
3544
  const err = error;
3342
3545
  logger.log("");
3343
3546
  logger.log(
3344
- boxen(
3547
+ boxen2(
3345
3548
  theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
3346
3549
  boxStyles.error
3347
3550
  )
@@ -3358,7 +3561,7 @@ async function authAtlassian() {
3358
3561
  const result = await atlassianAuthService.login();
3359
3562
  logger.log("");
3360
3563
  logger.log(
3361
- boxen(
3564
+ boxen2(
3362
3565
  theme.success("\u2713 Successfully authenticated with Atlassian Cloud") + `
3363
3566
 
3364
3567
  Site: ${result.siteName}
@@ -3375,7 +3578,7 @@ Services configured:
3375
3578
  const err = error;
3376
3579
  logger.log("");
3377
3580
  logger.log(
3378
- boxen(
3581
+ boxen2(
3379
3582
  theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
3380
3583
  boxStyles.error
3381
3584
  )
@@ -3392,7 +3595,7 @@ async function authGoogle() {
3392
3595
  const result = await googleAuthService.login();
3393
3596
  logger.log("");
3394
3597
  logger.log(
3395
- boxen(
3598
+ boxen2(
3396
3599
  theme.success("\u2713 Successfully authenticated with Google") + `
3397
3600
 
3398
3601
  Account: ${result.email}
@@ -3407,7 +3610,7 @@ Services configured:
3407
3610
  const err = error;
3408
3611
  logger.log("");
3409
3612
  logger.log(
3410
- boxen(
3613
+ boxen2(
3411
3614
  theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
3412
3615
  boxStyles.error
3413
3616
  )
@@ -3424,7 +3627,7 @@ async function authSlack() {
3424
3627
  const result = await slackAuthService.login();
3425
3628
  logger.log("");
3426
3629
  logger.log(
3427
- boxen(
3630
+ boxen2(
3428
3631
  theme.success("\u2713 Successfully authenticated with Slack") + `
3429
3632
 
3430
3633
  Workspace: ${result.teamName}
@@ -3439,7 +3642,7 @@ Services configured:
3439
3642
  const err = error;
3440
3643
  logger.log("");
3441
3644
  logger.log(
3442
- boxen(
3645
+ boxen2(
3443
3646
  theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
3444
3647
  boxStyles.error
3445
3648
  )
@@ -3622,8 +3825,8 @@ var SourceDetector = class {
3622
3825
  async checkAuthentication(type) {
3623
3826
  try {
3624
3827
  if (type === "github") {
3625
- await execAsync3("command gh auth status");
3626
- return true;
3828
+ const { authenticated } = await githubService.getAuthStatus();
3829
+ return authenticated;
3627
3830
  } else if (type === "bitbucket" || type === "atlassian") {
3628
3831
  return await storageService.isServiceAuthenticated("bitbucket");
3629
3832
  } else if (type === "gitlab") {
@@ -4372,6 +4575,10 @@ var JiraService = class {
4372
4575
  async getCredentials() {
4373
4576
  const creds = await storageService.getServiceCredentials("jira");
4374
4577
  if (!creds || !creds.accessToken) {
4578
+ const webCreds = await integrationsService.getAtlassianCredentials("jira");
4579
+ if (webCreds) {
4580
+ return webCreds;
4581
+ }
4375
4582
  throw new JiraError("Not authenticated with Jira", {
4376
4583
  hint: "Run: bragduck auth atlassian"
4377
4584
  });
@@ -4498,7 +4705,7 @@ var JiraService = class {
4498
4705
  try {
4499
4706
  await this.request("/rest/api/3/myself");
4500
4707
  } catch (error) {
4501
- if (error instanceof JiraError) {
4708
+ if (error instanceof JiraError || error instanceof IntegrationError) {
4502
4709
  throw error;
4503
4710
  }
4504
4711
  throw new JiraError("Could not access Jira instance via API", {
@@ -4632,7 +4839,8 @@ var JiraService = class {
4632
4839
  "reporter",
4633
4840
  "assignee",
4634
4841
  "status",
4635
- "issuetype"
4842
+ "issuetype",
4843
+ "project"
4636
4844
  ];
4637
4845
  const allIssues = [];
4638
4846
  const seenIssueKeys = /* @__PURE__ */ new Set();
@@ -4809,7 +5017,8 @@ ${truncatedDesc}`;
4809
5017
  externalId: issue.key,
4810
5018
  externalType: contribution?.type || "issue",
4811
5019
  externalSource: "jira",
4812
- externalUrl: url
5020
+ externalUrl: url,
5021
+ sourceName: issue.fields.project?.name || issue.fields.project?.key
4813
5022
  };
4814
5023
  }
4815
5024
  };
@@ -4871,6 +5080,10 @@ var ConfluenceService = class {
4871
5080
  async getCredentials() {
4872
5081
  const creds = await storageService.getServiceCredentials("confluence");
4873
5082
  if (!creds || !creds.accessToken) {
5083
+ const webCreds = await integrationsService.getAtlassianCredentials("confluence");
5084
+ if (webCreds) {
5085
+ return webCreds;
5086
+ }
4874
5087
  throw new ConfluenceError("Not authenticated with Confluence", {
4875
5088
  hint: "Run: bragduck auth atlassian"
4876
5089
  });
@@ -4991,7 +5204,7 @@ var ConfluenceService = class {
4991
5204
  try {
4992
5205
  await this.request("/wiki/rest/api/content?type=page&limit=1");
4993
5206
  } catch (error) {
4994
- if (error instanceof ConfluenceError) {
5207
+ if (error instanceof ConfluenceError || error instanceof IntegrationError) {
4995
5208
  throw error;
4996
5209
  }
4997
5210
  throw new ConfluenceError("Could not access Confluence instance via API", {
@@ -5115,7 +5328,7 @@ var ConfluenceService = class {
5115
5328
  cql,
5116
5329
  start: start.toString(),
5117
5330
  limit: limit.toString(),
5118
- expand: "version,history,children.comment,body.storage"
5331
+ expand: "version,history,children.comment,body.storage,space"
5119
5332
  };
5120
5333
  const queryString = Object.entries(params).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
5121
5334
  const endpoint = `/wiki/rest/api/content/search?${queryString}`;
@@ -5259,7 +5472,8 @@ ${contribution.details}
5259
5472
  externalId: page.id,
5260
5473
  externalType: contribution?.type || "page",
5261
5474
  externalSource: "confluence",
5262
- externalUrl: url
5475
+ externalUrl: url,
5476
+ sourceName: page.space?.name || page.space?.key
5263
5477
  };
5264
5478
  }
5265
5479
  };
@@ -5320,6 +5534,10 @@ var GoogleDocsService = class {
5320
5534
  async getCredentials() {
5321
5535
  const creds = await storageService.getServiceCredentials("google-docs");
5322
5536
  if (!creds || !creds.accessToken) {
5537
+ const connection = await integrationsService.getConnectedAccessToken("google");
5538
+ if (connection) {
5539
+ return { accessToken: connection.accessToken };
5540
+ }
5323
5541
  throw new GoogleDocsError("Not authenticated with Google Docs", {
5324
5542
  hint: "Run: bragduck auth google"
5325
5543
  });
@@ -5418,7 +5636,7 @@ var GoogleDocsService = class {
5418
5636
  "https://www.googleapis.com/drive/v3/about?fields=user"
5419
5637
  );
5420
5638
  } catch (error) {
5421
- if (error instanceof GoogleDocsError) {
5639
+ if (error instanceof GoogleDocsError || error instanceof IntegrationError) {
5422
5640
  throw error;
5423
5641
  }
5424
5642
  throw new GoogleDocsError("Could not access Google Drive API", {
@@ -5575,6 +5793,8 @@ init_logger();
5575
5793
  init_storage_service();
5576
5794
  import { URLSearchParams as URLSearchParams8 } from "url";
5577
5795
  var SlackService = class {
5796
+ // Slack user ID of the web-connected account, looked up once per process
5797
+ webUserId = null;
5578
5798
  /**
5579
5799
  * Get stored Slack credentials.
5580
5800
  * Slack user tokens (xoxp-) don't expire by default — no expiry check needed.
@@ -5582,12 +5802,39 @@ var SlackService = class {
5582
5802
  async getCredentials() {
5583
5803
  const creds = await storageService.getServiceCredentials("slack");
5584
5804
  if (!creds || !creds.accessToken) {
5805
+ const webCreds = await this.getWebCredentials();
5806
+ if (webCreds) {
5807
+ return webCreds;
5808
+ }
5585
5809
  throw new SlackError("Not authenticated with Slack", {
5586
5810
  hint: "Run: bragduck auth slack"
5587
5811
  });
5588
5812
  }
5589
5813
  return creds;
5590
5814
  }
5815
+ /**
5816
+ * Build credentials from the Slack account connected on the BragDuck web app.
5817
+ * The server does not return the Slack user ID, so it is looked up via auth.test.
5818
+ */
5819
+ async getWebCredentials() {
5820
+ const connection = await integrationsService.getConnectedAccessToken("slack");
5821
+ if (!connection) {
5822
+ return null;
5823
+ }
5824
+ if (!this.webUserId) {
5825
+ const data = await this.authTest(connection.accessToken);
5826
+ if (!data.ok) {
5827
+ throw new SlackError(`Slack credentials invalid: ${data.error || "unknown error"}`, {
5828
+ hint: "Reconnect Slack at https://app.bragduck.com"
5829
+ });
5830
+ }
5831
+ this.webUserId = data.user_id;
5832
+ }
5833
+ return {
5834
+ accessToken: connection.accessToken,
5835
+ username: this.webUserId
5836
+ };
5837
+ }
5591
5838
  /**
5592
5839
  * Make authenticated GET request to Slack Web API
5593
5840
  */
@@ -5632,18 +5879,24 @@ var SlackService = class {
5632
5879
  return data;
5633
5880
  }
5634
5881
  /**
5635
- * Validate Slack credentials via auth.test
5882
+ * Call Slack auth.test with the given token
5636
5883
  */
5637
- async validateCredentials() {
5638
- const creds = await this.getCredentials();
5884
+ async authTest(accessToken) {
5639
5885
  const response = await fetch("https://slack.com/api/auth.test", {
5640
5886
  method: "POST",
5641
5887
  headers: {
5642
- Authorization: `Bearer ${creds.accessToken}`,
5888
+ Authorization: `Bearer ${accessToken}`,
5643
5889
  "Content-Type": "application/json"
5644
5890
  }
5645
5891
  });
5646
- const data = await response.json();
5892
+ return await response.json();
5893
+ }
5894
+ /**
5895
+ * Validate Slack credentials via auth.test
5896
+ */
5897
+ async validateCredentials() {
5898
+ const creds = await this.getCredentials();
5899
+ const data = await this.authTest(creds.accessToken);
5647
5900
  if (!data.ok) {
5648
5901
  throw new SlackError(`Slack credentials invalid: ${data.error || "unknown error"}`, {
5649
5902
  hint: "Run: bragduck auth slack"
@@ -5656,14 +5909,7 @@ var SlackService = class {
5656
5909
  async getCurrentUser() {
5657
5910
  try {
5658
5911
  const creds = await this.getCredentials();
5659
- const response = await fetch("https://slack.com/api/auth.test", {
5660
- method: "POST",
5661
- headers: {
5662
- Authorization: `Bearer ${creds.accessToken}`,
5663
- "Content-Type": "application/json"
5664
- }
5665
- });
5666
- const data = await response.json();
5912
+ const data = await this.authTest(creds.accessToken);
5667
5913
  return data.ok ? data.user : null;
5668
5914
  } catch {
5669
5915
  return null;
@@ -5736,7 +5982,9 @@ var SlackService = class {
5736
5982
  externalId: msg.ts,
5737
5983
  externalType: "message",
5738
5984
  externalSource: "slack",
5739
- externalUrl: msg.permalink
5985
+ externalUrl: msg.permalink,
5986
+ // Direct messages have no channel name to tag
5987
+ sourceName: msg.channel?.name || void 0
5740
5988
  };
5741
5989
  }
5742
5990
  };
@@ -5815,6 +6063,22 @@ var AdapterFactory = class {
5815
6063
  // src/commands/sync.ts
5816
6064
  init_logger();
5817
6065
 
6066
+ // src/utils/external-source.ts
6067
+ init_esm_shims();
6068
+ function getGitExternalSource(source) {
6069
+ switch (source) {
6070
+ case "github":
6071
+ return "github";
6072
+ case "gitlab":
6073
+ return "gitlab";
6074
+ case "bitbucket":
6075
+ case "atlassian":
6076
+ return "bitbucket";
6077
+ default:
6078
+ return void 0;
6079
+ }
6080
+ }
6081
+
5818
6082
  // src/utils/auth-helper.ts
5819
6083
  init_esm_shims();
5820
6084
  init_auth_service();
@@ -6111,9 +6375,9 @@ async function promptSortOption() {
6111
6375
  default: "date"
6112
6376
  });
6113
6377
  }
6114
- async function promptSelectOrganisation(organisations) {
6378
+ async function promptSelectOrganisation(organisations, allowNone = true) {
6115
6379
  const choices = [
6116
- { name: "No company", value: "none" },
6380
+ ...allowNone ? [{ name: "No company", value: "none" }] : [],
6117
6381
  ...organisations.map((org) => ({
6118
6382
  name: org.name,
6119
6383
  value: org.id
@@ -6122,11 +6386,11 @@ async function promptSelectOrganisation(organisations) {
6122
6386
  const selected = await select3({
6123
6387
  message: "Attach brags to which company?",
6124
6388
  choices,
6125
- default: "none"
6389
+ default: allowNone ? "none" : void 0
6126
6390
  });
6127
6391
  return selected === "none" ? null : selected;
6128
6392
  }
6129
- async function promptSelectOrganisationWithDefault(organisations, defaultOrgId) {
6393
+ async function promptSelectOrganisationWithDefault(organisations, defaultOrgId, allowNone = true) {
6130
6394
  if (defaultOrgId) {
6131
6395
  const defaultOrg = organisations.find((org) => org.id === defaultOrgId);
6132
6396
  if (defaultOrg) {
@@ -6139,7 +6403,7 @@ async function promptSelectOrganisationWithDefault(organisations, defaultOrgId)
6139
6403
  }
6140
6404
  }
6141
6405
  }
6142
- return await promptSelectOrganisation(organisations);
6406
+ return await promptSelectOrganisation(organisations, allowNone);
6143
6407
  }
6144
6408
  async function promptReviewBrags(refinedBrags, selectedCommits) {
6145
6409
  const acceptedBrags = [];
@@ -6469,9 +6733,67 @@ async function discoverRepositories() {
6469
6733
  // src/sync/multi-repo-sync.ts
6470
6734
  init_esm_shims();
6471
6735
  init_api_service();
6736
+ init_storage_service();
6737
+
6738
+ // src/sync/organisation.ts
6739
+ init_esm_shims();
6740
+ init_api_service();
6472
6741
  init_auth_service();
6473
6742
  init_storage_service();
6474
6743
  init_logger();
6744
+ init_errors();
6745
+ var CREATE_COMPANY_URL = "https://bragduck.com/app";
6746
+ async function loadSyncOrganisations() {
6747
+ const userInfo = authService.getUserInfo();
6748
+ if (!userInfo?.id) {
6749
+ throw new AuthenticationError("Could not read your account to load your companies");
6750
+ }
6751
+ const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
6752
+ const companies = orgsResponse.items.filter((org) => org.role !== "__virtual__");
6753
+ if (companies.length === 0) {
6754
+ throw new ValidationError(
6755
+ `Synced brags belong to a company, and you don't have one yet. Create a company at ${CREATE_COMPANY_URL}, then run sync again.`,
6756
+ { hint: `Create a company at ${CREATE_COMPANY_URL}` }
6757
+ );
6758
+ }
6759
+ return companies;
6760
+ }
6761
+ async function selectSyncOrganisation(organisations, turbo) {
6762
+ if (organisations.length === 1) {
6763
+ return organisations[0].id;
6764
+ }
6765
+ const defaultOrgId = storageService.getConfig("defaultCompany");
6766
+ if (turbo) {
6767
+ if (defaultOrgId && organisations.some((org) => org.id === defaultOrgId)) {
6768
+ logger.debug(`Turbo mode: using default company ${defaultOrgId}`);
6769
+ return defaultOrgId;
6770
+ }
6771
+ const companies = organisations.map((org) => `${org.name} (${org.id})`).join(", ");
6772
+ throw new ValidationError(
6773
+ `Turbo mode needs a default company when you belong to more than one: ${companies}`,
6774
+ {
6775
+ hint: 'Run "bragduck config set defaultCompany <company-id>", or sync without --turbo to pick one'
6776
+ }
6777
+ );
6778
+ }
6779
+ const orgId = await promptSelectOrganisationWithDefault(organisations, defaultOrgId, false);
6780
+ logger.log("");
6781
+ return orgId;
6782
+ }
6783
+
6784
+ // src/sync/multi-repo-sync.ts
6785
+ init_logger();
6786
+
6787
+ // src/utils/source-tag.ts
6788
+ init_esm_shims();
6789
+ var MAX_SOURCE_TAG_LENGTH = 50;
6790
+ function withSourceTag(sourceName, tags) {
6791
+ const tag = sourceName?.replace(/,/g, " ").replace(/\s+/g, " ").trim().slice(0, MAX_SOURCE_TAG_LENGTH).trim();
6792
+ if (!tag) return tags;
6793
+ return [tag, ...tags.filter((existing) => existing.toLowerCase() !== tag.toLowerCase())];
6794
+ }
6795
+
6796
+ // src/sync/multi-repo-sync.ts
6475
6797
  async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, options) {
6476
6798
  const TOTAL_STEPS = 5;
6477
6799
  logger.debug(`Syncing repository: ${repo.name} at ${repo.path}`);
@@ -6590,6 +6912,7 @@ async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, opt
6590
6912
  );
6591
6913
  refineSpinner.start();
6592
6914
  const refineRequest = {
6915
+ orgId,
6593
6916
  brags: selectedCommits.map((c) => ({
6594
6917
  text: c.message,
6595
6918
  date: c.date,
@@ -6629,8 +6952,7 @@ async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, opt
6629
6952
  mode: options.turbo ? "turbo" : "manual",
6630
6953
  brags: acceptedBrags.map((refined, index) => {
6631
6954
  const originalCommit = selectedCommits[index];
6632
- const repoTag = repo.name;
6633
- const tagsWithRepo = refined.suggested_tags.includes(repoTag) ? refined.suggested_tags : [repoTag, ...refined.suggested_tags];
6955
+ const tagsWithRepo = withSourceTag(repo.name, refined.suggested_tags);
6634
6956
  return {
6635
6957
  commit_sha: originalCommit?.sha || `brag-${index}`,
6636
6958
  title: refined.refined_title,
@@ -6645,11 +6967,11 @@ async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, opt
6645
6967
  impactDescription: refined.impact_description,
6646
6968
  impact_description: refined.impact_description,
6647
6969
  attachments: originalCommit?.url ? [originalCommit.url] : [],
6648
- orgId: orgId || void 0,
6970
+ orgId,
6649
6971
  // External fields for non-git sources
6650
6972
  externalId: originalCommit?.externalId,
6651
6973
  externalType: originalCommit?.externalType,
6652
- externalSource: originalCommit?.externalSource,
6974
+ externalSource: originalCommit?.externalSource ?? getGitExternalSource(repo.service),
6653
6975
  externalUrl: originalCommit?.externalUrl
6654
6976
  };
6655
6977
  })
@@ -6695,6 +7017,8 @@ async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, opt
6695
7017
  }
6696
7018
  async function syncMultipleRepositories(repos, options) {
6697
7019
  logger.debug(`Starting multi-repo sync for ${repos.length} repositories`);
7020
+ const organisations = await loadSyncOrganisations();
7021
+ const turboOrgId = options.turbo ? await selectSyncOrganisation(organisations, true) : void 0;
6698
7022
  let days = options.days;
6699
7023
  if (!days && options.today) {
6700
7024
  days = 1;
@@ -6736,25 +7060,7 @@ async function syncMultipleRepositories(repos, options) {
6736
7060
  logger.log(colors.highlight(`\u2501\u2501\u2501 Syncing ${current}/${total}: ${repo.name} \u2501\u2501\u2501`));
6737
7061
  logger.log("");
6738
7062
  try {
6739
- let selectedOrgId = null;
6740
- if (!options.turbo) {
6741
- const userInfo = authService.getUserInfo();
6742
- if (userInfo?.id) {
6743
- try {
6744
- const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
6745
- if (orgsResponse.items.length > 0) {
6746
- const defaultOrgId = storageService.getConfig("defaultCompany");
6747
- selectedOrgId = await promptSelectOrganisationWithDefault(
6748
- orgsResponse.items,
6749
- defaultOrgId
6750
- );
6751
- logger.log("");
6752
- }
6753
- } catch {
6754
- logger.debug("Failed to fetch organisations, skipping org selection");
6755
- }
6756
- }
6757
- }
7063
+ const selectedOrgId = turboOrgId ?? await selectSyncOrganisation(organisations);
6758
7064
  const result = await syncSingleRepository(
6759
7065
  repo,
6760
7066
  days,
@@ -6855,6 +7161,7 @@ async function promptSelectService() {
6855
7161
  return selected;
6856
7162
  }
6857
7163
  async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, sharedOrgId) {
7164
+ const selectedOrgId = sharedOrgId ?? await pickSyncOrganisation(options);
6858
7165
  const adapter = AdapterFactory.getAdapter(sourceType);
6859
7166
  const validationText = sourceType === "jira" || sourceType === "confluence" ? "Connecting to Atlassian instance" : sourceType === "google-docs" ? "Connecting to Google Drive" : "Validating repository";
6860
7167
  const repoSpinner = createStepSpinner(2, TOTAL_STEPS, validationText);
@@ -7000,6 +7307,7 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
7000
7307
  );
7001
7308
  refineSpinner.start();
7002
7309
  const refineRequest = {
7310
+ orgId: selectedOrgId,
7003
7311
  brags: selectedCommits.map((c) => ({
7004
7312
  text: c.message,
7005
7313
  date: c.date,
@@ -7029,25 +7337,6 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
7029
7337
  return { created: 0, skipped: duplicates.length };
7030
7338
  }
7031
7339
  logger.log("");
7032
- let selectedOrgId = sharedOrgId !== void 0 ? sharedOrgId : null;
7033
- if (sharedOrgId === void 0) {
7034
- const userInfo = authService.getUserInfo();
7035
- if (userInfo?.id) {
7036
- const orgSpinner = createFetchSpinner("Loading organizations...");
7037
- orgSpinner.start();
7038
- try {
7039
- const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
7040
- orgSpinner.stop();
7041
- if (orgsResponse.items.length > 0) {
7042
- selectedOrgId = await promptSelectOrganisation(orgsResponse.items);
7043
- logger.log("");
7044
- }
7045
- } catch {
7046
- orgSpinner.stop();
7047
- logger.debug("Failed to fetch organisations, skipping org selection");
7048
- }
7049
- }
7050
- }
7051
7340
  const createSpinner = createStepSpinner(
7052
7341
  5,
7053
7342
  TOTAL_STEPS,
@@ -7059,11 +7348,10 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
7059
7348
  brags: acceptedBrags.map((refined, index) => {
7060
7349
  const originalCommit = selectedCommits[index];
7061
7350
  const isGitSource = sourceType === "github" || sourceType === "gitlab" || sourceType === "bitbucket" || sourceType === "atlassian";
7062
- let tagsWithRepo = refined.suggested_tags;
7063
- if (isGitSource && repoInfo.name) {
7064
- const repoTag = repoInfo.name;
7065
- tagsWithRepo = refined.suggested_tags.includes(repoTag) ? refined.suggested_tags : [repoTag, ...refined.suggested_tags];
7066
- }
7351
+ const tagsWithRepo = withSourceTag(
7352
+ isGitSource ? repoInfo.name : originalCommit?.sourceName,
7353
+ refined.suggested_tags
7354
+ );
7067
7355
  return {
7068
7356
  commit_sha: originalCommit?.sha || `brag-${index}`,
7069
7357
  title: refined.refined_title,
@@ -7077,11 +7365,11 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
7077
7365
  impactDescription: refined.impact_description,
7078
7366
  impact_description: refined.impact_description,
7079
7367
  attachments: [],
7080
- orgId: selectedOrgId || void 0,
7368
+ orgId: selectedOrgId,
7081
7369
  // External fields for non-git sources (Jira, Confluence, etc.)
7082
7370
  externalId: originalCommit?.externalId,
7083
7371
  externalType: originalCommit?.externalType,
7084
- externalSource: originalCommit?.externalSource,
7372
+ externalSource: originalCommit?.externalSource ?? getGitExternalSource(sourceType),
7085
7373
  externalUrl: originalCommit?.externalUrl
7086
7374
  };
7087
7375
  })
@@ -7193,26 +7481,7 @@ async function syncAllAuthenticatedServices(options) {
7193
7481
  sharedDays = storageService.getConfig("defaultCommitDays") || 30;
7194
7482
  logger.debug(`Turbo mode: using default ${sharedDays} days`);
7195
7483
  }
7196
- let sharedOrgId = null;
7197
- if (!options.turbo) {
7198
- const userInfo = authService.getUserInfo();
7199
- if (userInfo?.id) {
7200
- const orgSpinner = createFetchSpinner("Loading organizations...");
7201
- orgSpinner.start();
7202
- try {
7203
- const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
7204
- orgSpinner.stop();
7205
- if (orgsResponse.items.length > 0) {
7206
- const defaultOrgId = storageService.getConfig("defaultCompany");
7207
- sharedOrgId = await promptSelectOrganisationWithDefault(orgsResponse.items, defaultOrgId);
7208
- logger.log("");
7209
- }
7210
- } catch {
7211
- orgSpinner.stop();
7212
- logger.debug("Failed to fetch organisations, skipping org selection");
7213
- }
7214
- }
7215
- }
7484
+ const sharedOrgId = await pickSyncOrganisation(options);
7216
7485
  const results = [];
7217
7486
  for (let i = 0; i < servicesToSync.length; i++) {
7218
7487
  const service = servicesToSync[i];
@@ -7461,7 +7730,8 @@ To authenticate:
7461
7730
  sourceType = selectedSource;
7462
7731
  }
7463
7732
  if (sourceType === "jira" || sourceType === "confluence") {
7464
- const creds = await storageService.getServiceCredentials(sourceType);
7733
+ const localCreds = await storageService.getServiceCredentials(sourceType);
7734
+ const creds = localCreds?.accessToken ? localCreds : await integrationsService.getAtlassianCredentials(sourceType);
7465
7735
  const envInstance = loadEnvConfig()[`${sourceType}Instance`];
7466
7736
  const projectConfig = await findProjectConfig();
7467
7737
  const configInstance = projectConfig?.[`${sourceType}Instance`];
@@ -7534,6 +7804,17 @@ To authenticate:
7534
7804
  process.exit(1);
7535
7805
  }
7536
7806
  }
7807
+ async function pickSyncOrganisation(options) {
7808
+ const orgSpinner = createFetchSpinner("Loading organizations...");
7809
+ orgSpinner.start();
7810
+ let organisations;
7811
+ try {
7812
+ organisations = await loadSyncOrganisations();
7813
+ } finally {
7814
+ orgSpinner.stop();
7815
+ }
7816
+ return await selectSyncOrganisation(organisations, options.turbo);
7817
+ }
7537
7818
  function getServiceLabel(service) {
7538
7819
  if (service === "google-docs") return "Google Docs";
7539
7820
  if (service === "slack") return "Slack";
@@ -7564,6 +7845,9 @@ function getErrorHint(error, sourceType) {
7564
7845
  }
7565
7846
  return 'Run "bragduck auth login" to login again';
7566
7847
  }
7848
+ if ((error.name === "IntegrationError" || error.name === "ValidationError") && error.details?.hint) {
7849
+ return error.details.hint;
7850
+ }
7567
7851
  if (error.name === "GoogleDocsError") {
7568
7852
  return 'Run "bragduck auth google" to re-authenticate with Google';
7569
7853
  }