@bragduck/cli 2.8.1 → 2.8.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.
@@ -1222,7 +1222,9 @@ var init_auth_service = __esm({
1222
1222
  } catch (error) {
1223
1223
  logger.debug(`Token refresh failed: ${error.message}`);
1224
1224
  await this.logout();
1225
- throw new AuthenticationError("Token refresh failed. Please log in again.");
1225
+ throw new AuthenticationError(
1226
+ 'Bragduck platform token refresh failed. Please run "bragduck auth login" to re-authenticate.'
1227
+ );
1226
1228
  }
1227
1229
  }
1228
1230
  };
@@ -1428,7 +1430,7 @@ var init_api_service = __esm({
1428
1430
  throw error;
1429
1431
  }
1430
1432
  throw new TokenExpiredError(
1431
- 'Your session has expired. Please run "bragduck init" to login again.'
1433
+ 'Your Bragduck platform session has expired. Please run "bragduck auth login" to re-authenticate.'
1432
1434
  );
1433
1435
  }
1434
1436
  }
@@ -2177,188 +2179,10 @@ var CancelPromptError = class extends Error {
2177
2179
  init_api_service();
2178
2180
  init_storage_service();
2179
2181
  init_auth_service();
2180
- import boxen6 from "boxen";
2181
-
2182
- // src/utils/source-detector.ts
2183
- init_esm_shims();
2184
- init_errors();
2185
- init_storage_service();
2186
- import { exec as exec2 } from "child_process";
2187
- import { promisify as promisify2 } from "util";
2188
- import { select } from "@inquirer/prompts";
2189
- var execAsync2 = promisify2(exec2);
2190
- var SourceDetector = class {
2191
- /**
2192
- * Detect all possible sources from git remotes
2193
- */
2194
- async detectSources(options = {}) {
2195
- const detected = [];
2196
- try {
2197
- const { stdout } = await execAsync2("git remote -v");
2198
- const remotes = this.parseRemotes(stdout);
2199
- for (const remote of remotes) {
2200
- const source = this.parseRemoteUrl(remote.url);
2201
- if (source) {
2202
- const isAuthenticated = await this.checkAuthentication(source.type);
2203
- detected.push({
2204
- ...source,
2205
- remoteUrl: remote.url,
2206
- isAuthenticated
2207
- });
2208
- }
2209
- }
2210
- } catch {
2211
- throw new GitError("Not a git repository");
2212
- }
2213
- let recommended;
2214
- if (detected.length > 1 && options.allowInteractive && process.stdout.isTTY) {
2215
- try {
2216
- recommended = await this.promptSourceSelection(detected, options.showAuthStatus);
2217
- } catch {
2218
- }
2219
- }
2220
- if (!recommended && options.respectPriority) {
2221
- const priority = await storageService.getConfigWithHierarchy("sourcePriority");
2222
- if (priority) {
2223
- recommended = this.applyPriority(detected, priority);
2224
- }
2225
- }
2226
- if (!recommended) {
2227
- recommended = this.selectRecommendedSource(detected);
2228
- }
2229
- return { detected, recommended };
2230
- }
2231
- /**
2232
- * Prompt user to select a source interactively
2233
- */
2234
- async promptSourceSelection(sources, showAuthStatus = true) {
2235
- const choices = sources.map((source) => {
2236
- const authStatus2 = showAuthStatus ? source.isAuthenticated ? "\u2713 authenticated" : "\u2717 not authenticated" : "";
2237
- const repo = source.owner && source.repo ? `${source.owner}/${source.repo}` : source.host;
2238
- const name = `${source.type}${authStatus2 ? ` (${authStatus2})` : ""} - ${repo}`;
2239
- return {
2240
- name,
2241
- value: source.type,
2242
- description: source.remoteUrl
2243
- };
2244
- });
2245
- return await select({
2246
- message: "Multiple sources detected. Which do you want to sync?",
2247
- choices
2248
- });
2249
- }
2250
- /**
2251
- * Apply configured priority to select source
2252
- */
2253
- applyPriority(sources, priority) {
2254
- for (const sourceType of priority) {
2255
- const found = sources.find((s) => s.type === sourceType);
2256
- if (found) return found.type;
2257
- }
2258
- return void 0;
2259
- }
2260
- /**
2261
- * Parse git remote -v output
2262
- */
2263
- parseRemotes(output) {
2264
- const lines = output.split("\n").filter(Boolean);
2265
- const remotes = /* @__PURE__ */ new Map();
2266
- for (const line of lines) {
2267
- const match = line.match(/^(\S+)\s+(\S+)\s+\(fetch\)$/);
2268
- if (match && match[1] && match[2]) {
2269
- remotes.set(match[1], match[2]);
2270
- }
2271
- }
2272
- return Array.from(remotes.entries()).map(([name, url]) => ({ name, url }));
2273
- }
2274
- /**
2275
- * Parse remote URL to detect source type
2276
- */
2277
- parseRemoteUrl(url) {
2278
- if (url.includes("github.com")) {
2279
- const match = url.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
2280
- if (match && match[1] && match[2]) {
2281
- return {
2282
- type: "github",
2283
- host: "github.com",
2284
- owner: match[1],
2285
- repo: match[2]
2286
- };
2287
- }
2288
- }
2289
- if (url.includes("gitlab.com")) {
2290
- const match = url.match(/gitlab\.com[:/]([^/]+)\/([^/.]+)/);
2291
- if (match && match[1] && match[2]) {
2292
- return {
2293
- type: "gitlab",
2294
- host: "gitlab.com",
2295
- owner: match[1],
2296
- repo: match[2]
2297
- };
2298
- }
2299
- }
2300
- if (url.includes("bitbucket.org")) {
2301
- const match = url.match(/bitbucket\.org[:/]([^/]+)\/([^/.]+)/);
2302
- if (match && match[1] && match[2]) {
2303
- return {
2304
- type: "bitbucket",
2305
- host: "bitbucket.org",
2306
- owner: match[1],
2307
- repo: match[2]
2308
- };
2309
- }
2310
- }
2311
- if (url.match(/\/scm\/|bitbucket\./)) {
2312
- const match = url.match(/([^/:]+)[:/]scm\/([^/]+)\/([^/.]+)/);
2313
- if (match && match[1] && match[2] && match[3]) {
2314
- return {
2315
- type: "atlassian",
2316
- host: match[1],
2317
- owner: match[2],
2318
- repo: match[3]
2319
- };
2320
- }
2321
- }
2322
- return null;
2323
- }
2324
- /**
2325
- * Check if user is authenticated with a specific source
2326
- */
2327
- async checkAuthentication(type) {
2328
- try {
2329
- if (type === "github") {
2330
- await execAsync2("command gh auth status");
2331
- return true;
2332
- } else if (type === "bitbucket" || type === "atlassian") {
2333
- return await storageService.isServiceAuthenticated("bitbucket");
2334
- } else if (type === "gitlab") {
2335
- return await storageService.isServiceAuthenticated("gitlab");
2336
- } else if (type === "jira") {
2337
- return await storageService.isServiceAuthenticated("jira");
2338
- } else if (type === "confluence") {
2339
- return await storageService.isServiceAuthenticated("confluence");
2340
- }
2341
- return false;
2342
- } catch {
2343
- return false;
2344
- }
2345
- }
2346
- /**
2347
- * Select recommended source (prefer authenticated GitHub)
2348
- */
2349
- selectRecommendedSource(sources) {
2350
- const authenticated = sources.find((s) => s.isAuthenticated);
2351
- if (authenticated) return authenticated.type;
2352
- const github = sources.find((s) => s.type === "github");
2353
- if (github) return "github";
2354
- return sources[0]?.type;
2355
- }
2356
- };
2357
- var sourceDetector = new SourceDetector();
2358
-
2359
- // src/commands/sync.ts
2360
2182
  init_env_loader();
2361
2183
  init_config_loader();
2184
+ import { select as select2 } from "@inquirer/prompts";
2185
+ import boxen6 from "boxen";
2362
2186
 
2363
2187
  // src/sync/adapter-factory.ts
2364
2188
  init_esm_shims();
@@ -2370,8 +2194,8 @@ init_esm_shims();
2370
2194
  init_esm_shims();
2371
2195
  init_errors();
2372
2196
  init_logger();
2373
- import { exec as exec3 } from "child_process";
2374
- import { promisify as promisify3 } from "util";
2197
+ import { exec as exec2 } from "child_process";
2198
+ import { promisify as promisify2 } from "util";
2375
2199
 
2376
2200
  // src/services/git.service.ts
2377
2201
  init_esm_shims();
@@ -2596,7 +2420,7 @@ var GitService = class {
2596
2420
  var gitService = new GitService();
2597
2421
 
2598
2422
  // src/services/github.service.ts
2599
- var execAsync3 = promisify3(exec3);
2423
+ var execAsync2 = promisify2(exec2);
2600
2424
  var GitHubService = class {
2601
2425
  MAX_BODY_LENGTH = 5e3;
2602
2426
  PR_SEARCH_FIELDS = "number,title,body,author,mergedAt,additions,deletions,changedFiles,url,labels";
@@ -2605,7 +2429,7 @@ var GitHubService = class {
2605
2429
  */
2606
2430
  async checkGitHubCLI() {
2607
2431
  try {
2608
- await execAsync3("command gh --version");
2432
+ await execAsync2("command gh --version");
2609
2433
  return true;
2610
2434
  } catch {
2611
2435
  return false;
@@ -2627,7 +2451,7 @@ var GitHubService = class {
2627
2451
  */
2628
2452
  async checkAuthentication() {
2629
2453
  try {
2630
- await execAsync3("command gh auth status");
2454
+ await execAsync2("command gh auth status");
2631
2455
  return true;
2632
2456
  } catch {
2633
2457
  return false;
@@ -2652,7 +2476,7 @@ var GitHubService = class {
2652
2476
  await this.ensureGitHubCLI();
2653
2477
  await this.ensureAuthentication();
2654
2478
  await gitService.validateRepository();
2655
- const { stdout } = await execAsync3("command gh repo view --json url");
2479
+ const { stdout } = await execAsync2("command gh repo view --json url");
2656
2480
  const data = JSON.parse(stdout);
2657
2481
  if (!data.url) {
2658
2482
  throw new GitHubError("This repository is not hosted on GitHub", {
@@ -2684,7 +2508,7 @@ var GitHubService = class {
2684
2508
  async getRepositoryInfo() {
2685
2509
  try {
2686
2510
  await this.ensureGitHubCLI();
2687
- const { stdout } = await execAsync3(
2511
+ const { stdout } = await execAsync2(
2688
2512
  "command gh repo view --json owner,name,url,nameWithOwner"
2689
2513
  );
2690
2514
  const data = JSON.parse(stdout);
@@ -2709,7 +2533,7 @@ var GitHubService = class {
2709
2533
  */
2710
2534
  async getCurrentGitHubUser() {
2711
2535
  try {
2712
- const { stdout } = await execAsync3("command gh api user --jq .login");
2536
+ const { stdout } = await execAsync2("command gh api user --jq .login");
2713
2537
  return stdout.trim() || null;
2714
2538
  } catch {
2715
2539
  logger.debug("Failed to get GitHub user");
@@ -2735,7 +2559,7 @@ var GitHubService = class {
2735
2559
  const limitArg = limit ? `--limit ${limit}` : "";
2736
2560
  const command = `command gh pr list --state merged --json ${this.PR_SEARCH_FIELDS} --search "${searchQuery}" ${limitArg}`;
2737
2561
  logger.debug(`Running: ${command}`);
2738
- const { stdout } = await execAsync3(command);
2562
+ const { stdout } = await execAsync2(command);
2739
2563
  const prs = JSON.parse(stdout);
2740
2564
  logger.debug(`Found ${prs.length} merged PRs`);
2741
2565
  return prs;
@@ -2846,9 +2670,9 @@ init_esm_shims();
2846
2670
  init_errors();
2847
2671
  init_logger();
2848
2672
  init_storage_service();
2849
- import { exec as exec4 } from "child_process";
2850
- import { promisify as promisify4 } from "util";
2851
- var execAsync4 = promisify4(exec4);
2673
+ import { exec as exec3 } from "child_process";
2674
+ import { promisify as promisify3 } from "util";
2675
+ var execAsync3 = promisify3(exec3);
2852
2676
  var BitbucketService = class {
2853
2677
  BITBUCKET_API_BASE = "https://api.bitbucket.org/2.0";
2854
2678
  MAX_DESCRIPTION_LENGTH = 5e3;
@@ -2933,7 +2757,7 @@ var BitbucketService = class {
2933
2757
  */
2934
2758
  async getRepoFromGit() {
2935
2759
  try {
2936
- const { stdout } = await execAsync4("command git remote get-url origin");
2760
+ const { stdout } = await execAsync3("command git remote get-url origin");
2937
2761
  const remoteUrl = stdout.trim();
2938
2762
  const parsed = this.parseRemoteUrl(remoteUrl);
2939
2763
  if (!parsed) {
@@ -3128,10 +2952,10 @@ init_esm_shims();
3128
2952
  init_errors();
3129
2953
  init_logger();
3130
2954
  init_storage_service();
3131
- import { exec as exec5 } from "child_process";
3132
- import { promisify as promisify5 } from "util";
2955
+ import { exec as exec4 } from "child_process";
2956
+ import { promisify as promisify4 } from "util";
3133
2957
  import { URLSearchParams as URLSearchParams3 } from "url";
3134
- var execAsync5 = promisify5(exec5);
2958
+ var execAsync4 = promisify4(exec4);
3135
2959
  var GitLabService = class {
3136
2960
  DEFAULT_INSTANCE = "https://gitlab.com";
3137
2961
  MAX_DESCRIPTION_LENGTH = 5e3;
@@ -3217,7 +3041,7 @@ var GitLabService = class {
3217
3041
  */
3218
3042
  async getProjectFromGit() {
3219
3043
  try {
3220
- const { stdout } = await execAsync5("git remote get-url origin");
3044
+ const { stdout } = await execAsync4("git remote get-url origin");
3221
3045
  const remoteUrl = stdout.trim();
3222
3046
  const parsed = this.parseRemoteUrl(remoteUrl);
3223
3047
  if (!parsed) {
@@ -4109,7 +3933,7 @@ init_logger();
4109
3933
 
4110
3934
  // src/ui/prompts.ts
4111
3935
  init_esm_shims();
4112
- import { checkbox, confirm, input as input2, select as select2, editor } from "@inquirer/prompts";
3936
+ import { checkbox, confirm, input as input2, select, editor } from "@inquirer/prompts";
4113
3937
  import boxen4 from "boxen";
4114
3938
 
4115
3939
  // src/ui/formatters.ts
@@ -4265,7 +4089,7 @@ async function promptDaysToScan(defaultDays = 30) {
4265
4089
  { name: "90 days", value: "90", description: "Last 3 months" },
4266
4090
  { name: "Custom", value: "custom", description: "Enter custom number of days" }
4267
4091
  ];
4268
- const selected = await select2({
4092
+ const selected = await select({
4269
4093
  message: "How many days back should we scan for PRs?",
4270
4094
  choices,
4271
4095
  default: "30"
@@ -4297,7 +4121,7 @@ async function promptSortOption() {
4297
4121
  { name: "By files (most files)", value: "files", description: "Most files changed" },
4298
4122
  { name: "No sorting", value: "none", description: "Keep original order" }
4299
4123
  ];
4300
- return await select2({
4124
+ return await select({
4301
4125
  message: "How would you like to sort the PRs?",
4302
4126
  choices,
4303
4127
  default: "date"
@@ -4311,7 +4135,7 @@ async function promptSelectOrganisation(organisations) {
4311
4135
  value: org.id
4312
4136
  }))
4313
4137
  ];
4314
- const selected = await select2({
4138
+ const selected = await select({
4315
4139
  message: "Attach brags to which company?",
4316
4140
  choices,
4317
4141
  default: "none"
@@ -4361,7 +4185,7 @@ ${theme.label("PR Link")} ${colors.link(prUrl)}`;
4361
4185
  }
4362
4186
  console.log(boxen4(bragDetails, boxStyles.info));
4363
4187
  console.log("");
4364
- const action = await select2({
4188
+ const action = await select({
4365
4189
  message: `What would you like to do with this brag?`,
4366
4190
  choices: [
4367
4191
  { name: "\u2713 Accept", value: "accept", description: "Add this brag as-is" },
@@ -4446,7 +4270,7 @@ async function ensureAuthenticated() {
4446
4270
  if (!shouldAuth) {
4447
4271
  logger.log("");
4448
4272
  logger.info(
4449
- theme.secondary("Authentication skipped. Run ") + theme.command("bragduck init") + theme.secondary(" when you're ready to authenticate.")
4273
+ theme.secondary("Authentication skipped. Run ") + theme.command("bragduck auth login") + theme.secondary(" when you're ready to authenticate.")
4450
4274
  );
4451
4275
  logger.log("");
4452
4276
  return false;
@@ -4459,9 +4283,6 @@ async function ensureAuthenticated() {
4459
4283
  }
4460
4284
  }
4461
4285
 
4462
- // src/commands/sync.ts
4463
- init_errors();
4464
-
4465
4286
  // src/ui/spinners.ts
4466
4287
  init_esm_shims();
4467
4288
  import ora2 from "ora";
@@ -4507,9 +4328,313 @@ function failStepSpinner(spinner, currentStep, totalSteps, text) {
4507
4328
  }
4508
4329
 
4509
4330
  // src/commands/sync.ts
4331
+ async function promptSelectService() {
4332
+ const allServices = [
4333
+ "github",
4334
+ "gitlab",
4335
+ "bitbucket",
4336
+ "atlassian",
4337
+ "jira",
4338
+ "confluence"
4339
+ ];
4340
+ const authenticatedServices = await storageService.getAuthenticatedServices();
4341
+ const authenticatedSyncServices = authenticatedServices.filter(
4342
+ (service) => service !== "bragduck" && allServices.includes(service)
4343
+ );
4344
+ const serviceChoices = await Promise.all(
4345
+ allServices.map(async (service) => {
4346
+ const isAuth = await storageService.isServiceAuthenticated(service);
4347
+ const indicator = isAuth ? "\u2713" : "\u2717";
4348
+ const serviceLabel = service.charAt(0).toUpperCase() + service.slice(1);
4349
+ return {
4350
+ name: `${indicator} ${serviceLabel}`,
4351
+ value: service,
4352
+ description: isAuth ? "Authenticated" : "Not authenticated"
4353
+ };
4354
+ })
4355
+ );
4356
+ if (authenticatedSyncServices.length > 0) {
4357
+ const serviceNames = authenticatedSyncServices.map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(", ");
4358
+ serviceChoices.push({
4359
+ name: `\u2713 All authenticated services (${serviceNames})`,
4360
+ value: "all",
4361
+ description: `Sync all ${authenticatedSyncServices.length} authenticated service${authenticatedSyncServices.length > 1 ? "s" : ""}`
4362
+ });
4363
+ }
4364
+ const selected = await select2({
4365
+ message: "Select a service to sync:",
4366
+ choices: serviceChoices
4367
+ });
4368
+ return selected;
4369
+ }
4370
+ async function syncSingleService(sourceType, options, TOTAL_STEPS) {
4371
+ const adapter = AdapterFactory.getAdapter(sourceType);
4372
+ const repoSpinner = createStepSpinner(2, TOTAL_STEPS, "Validating repository");
4373
+ repoSpinner.start();
4374
+ await adapter.validate();
4375
+ const repoInfo = await adapter.getRepositoryInfo();
4376
+ succeedStepSpinner(repoSpinner, 2, TOTAL_STEPS, `Repository: ${theme.value(repoInfo.fullName)}`);
4377
+ logger.log("");
4378
+ let days = options.days;
4379
+ if (!days) {
4380
+ const defaultDays = storageService.getConfig("defaultCommitDays");
4381
+ days = await promptDaysToScan(defaultDays);
4382
+ logger.log("");
4383
+ }
4384
+ const fetchSpinner = createStepSpinner(
4385
+ 3,
4386
+ TOTAL_STEPS,
4387
+ `Fetching work items from the last ${days} days`
4388
+ );
4389
+ fetchSpinner.start();
4390
+ const workItems = await adapter.fetchWorkItems({
4391
+ days,
4392
+ author: options.all ? void 0 : await adapter.getCurrentUser() || void 0
4393
+ });
4394
+ if (workItems.length === 0) {
4395
+ failStepSpinner(fetchSpinner, 3, TOTAL_STEPS, `No work items found in the last ${days} days`);
4396
+ logger.log("");
4397
+ logger.info("Try increasing the number of days or check your activity");
4398
+ return { created: 0, skipped: 0 };
4399
+ }
4400
+ succeedStepSpinner(
4401
+ fetchSpinner,
4402
+ 3,
4403
+ TOTAL_STEPS,
4404
+ `Found ${theme.count(workItems.length)} work item${workItems.length > 1 ? "s" : ""}`
4405
+ );
4406
+ logger.log("");
4407
+ logger.log(formatCommitStats(workItems));
4408
+ logger.log("");
4409
+ let sortedCommits = [...workItems];
4410
+ if (workItems.length > 1) {
4411
+ const sortOption = await promptSortOption();
4412
+ logger.log("");
4413
+ if (sortOption === "date") {
4414
+ sortedCommits.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
4415
+ } else if (sortOption === "size") {
4416
+ sortedCommits.sort((a, b) => {
4417
+ const sizeA = (a.diffStats?.insertions || 0) + (a.diffStats?.deletions || 0);
4418
+ const sizeB = (b.diffStats?.insertions || 0) + (b.diffStats?.deletions || 0);
4419
+ return sizeB - sizeA;
4420
+ });
4421
+ } else if (sortOption === "files") {
4422
+ sortedCommits.sort((a, b) => {
4423
+ const filesA = a.diffStats?.filesChanged || 0;
4424
+ const filesB = b.diffStats?.filesChanged || 0;
4425
+ return filesB - filesA;
4426
+ });
4427
+ }
4428
+ }
4429
+ const selectedShas = await promptSelectCommits(sortedCommits);
4430
+ if (selectedShas.length === 0) {
4431
+ logger.log("");
4432
+ logger.info(theme.secondary("No work items selected. Sync cancelled."));
4433
+ logger.log("");
4434
+ return { created: 0, skipped: 0 };
4435
+ }
4436
+ const selectedCommits = sortedCommits.filter((c) => selectedShas.includes(c.sha));
4437
+ logger.log(formatSelectionSummary(selectedCommits.length, selectedCommits));
4438
+ logger.log("");
4439
+ const existingBrags = await apiService.listBrags({ limit: 100 });
4440
+ logger.debug(`Fetched ${existingBrags.brags.length} existing brags`);
4441
+ const existingUrls = new Set(existingBrags.brags.flatMap((b) => b.attachments || []));
4442
+ logger.debug(`Existing URLs in attachments: ${existingUrls.size}`);
4443
+ const duplicates = selectedCommits.filter((c) => c.url && existingUrls.has(c.url));
4444
+ const newCommits = selectedCommits.filter((c) => !c.url || !existingUrls.has(c.url));
4445
+ logger.debug(`Duplicates: ${duplicates.length}, New: ${newCommits.length}`);
4446
+ if (duplicates.length > 0) {
4447
+ logger.log("");
4448
+ logger.info(
4449
+ colors.warning(
4450
+ `${duplicates.length} work item${duplicates.length > 1 ? "s" : ""} already added to Bragduck - skipping`
4451
+ )
4452
+ );
4453
+ logger.log("");
4454
+ }
4455
+ if (newCommits.length === 0) {
4456
+ logger.log("");
4457
+ logger.info(
4458
+ theme.secondary("All selected work items already exist in Bragduck. Nothing to refine.")
4459
+ );
4460
+ logger.log("");
4461
+ return { created: 0, skipped: duplicates.length };
4462
+ }
4463
+ const refineSpinner = createStepSpinner(
4464
+ 4,
4465
+ TOTAL_STEPS,
4466
+ `Refining ${theme.count(newCommits.length)} work item${newCommits.length > 1 ? "s" : ""} with AI`
4467
+ );
4468
+ refineSpinner.start();
4469
+ const refineRequest = {
4470
+ brags: newCommits.map((c) => ({
4471
+ text: c.message,
4472
+ date: c.date,
4473
+ title: c.message.split("\n")[0]
4474
+ }))
4475
+ };
4476
+ const refineResponse = await apiService.refineBrags(refineRequest);
4477
+ let refinedBrags = refineResponse.refined_brags;
4478
+ succeedStepSpinner(refineSpinner, 4, TOTAL_STEPS, "Work items refined successfully");
4479
+ logger.log("");
4480
+ logger.info("Preview of refined brags:");
4481
+ logger.log("");
4482
+ logger.log(formatRefinedCommitsTable(refinedBrags, newCommits));
4483
+ logger.log("");
4484
+ const acceptedBrags = await promptReviewBrags(refinedBrags, newCommits);
4485
+ if (acceptedBrags.length === 0) {
4486
+ logger.log("");
4487
+ logger.info(theme.secondary("No brags selected for creation. Sync cancelled."));
4488
+ logger.log("");
4489
+ return { created: 0, skipped: duplicates.length };
4490
+ }
4491
+ logger.log("");
4492
+ let selectedOrgId = null;
4493
+ const userInfo = authService.getUserInfo();
4494
+ if (userInfo?.id) {
4495
+ try {
4496
+ const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
4497
+ if (orgsResponse.items.length > 0) {
4498
+ selectedOrgId = await promptSelectOrganisation(orgsResponse.items);
4499
+ logger.log("");
4500
+ }
4501
+ } catch {
4502
+ logger.debug("Failed to fetch organisations, skipping org selection");
4503
+ }
4504
+ }
4505
+ const createSpinner2 = createStepSpinner(
4506
+ 5,
4507
+ TOTAL_STEPS,
4508
+ `Creating ${theme.count(acceptedBrags.length)} brag${acceptedBrags.length > 1 ? "s" : ""}`
4509
+ );
4510
+ createSpinner2.start();
4511
+ const createRequest = {
4512
+ brags: acceptedBrags.map((refined, index) => {
4513
+ const originalCommit = newCommits[index];
4514
+ return {
4515
+ commit_sha: originalCommit?.sha || `brag-${index}`,
4516
+ title: refined.refined_title,
4517
+ description: refined.refined_description,
4518
+ tags: refined.suggested_tags,
4519
+ repository: repoInfo.url,
4520
+ date: originalCommit?.date || "",
4521
+ commit_url: originalCommit?.url || "",
4522
+ impact_score: refined.suggested_impactLevel,
4523
+ impact_description: refined.impact_description,
4524
+ attachments: originalCommit?.url ? [originalCommit.url] : [],
4525
+ orgId: selectedOrgId || void 0,
4526
+ // External fields for non-git sources (Jira, Confluence, etc.)
4527
+ externalId: originalCommit?.externalId,
4528
+ externalType: originalCommit?.externalType,
4529
+ externalSource: originalCommit?.externalSource,
4530
+ externalUrl: originalCommit?.externalUrl
4531
+ };
4532
+ })
4533
+ };
4534
+ const createResponse = await apiService.createBrags(createRequest);
4535
+ succeedStepSpinner(
4536
+ createSpinner2,
4537
+ 5,
4538
+ TOTAL_STEPS,
4539
+ `Created ${theme.count(createResponse.created)} brag${createResponse.created > 1 ? "s" : ""}`
4540
+ );
4541
+ logger.log("");
4542
+ return { created: createResponse.created, skipped: duplicates.length };
4543
+ }
4544
+ async function syncAllAuthenticatedServices(options) {
4545
+ const TOTAL_STEPS = 5;
4546
+ const allServices = [
4547
+ "github",
4548
+ "gitlab",
4549
+ "bitbucket",
4550
+ "atlassian",
4551
+ "jira",
4552
+ "confluence"
4553
+ ];
4554
+ const authenticatedServices = await storageService.getAuthenticatedServices();
4555
+ const servicesToSync = authenticatedServices.filter(
4556
+ (service) => service !== "bragduck" && allServices.includes(service)
4557
+ );
4558
+ if (servicesToSync.length === 0) {
4559
+ logger.log("");
4560
+ logger.error("No authenticated services found.");
4561
+ logger.log("");
4562
+ logger.info("Authenticate a service first:");
4563
+ logger.info(` ${theme.command("bragduck auth github")}`);
4564
+ logger.info(` ${theme.command("bragduck auth atlassian")}`);
4565
+ return;
4566
+ }
4567
+ logger.info(
4568
+ theme.highlight(
4569
+ `Syncing ${servicesToSync.length} authenticated service${servicesToSync.length > 1 ? "s" : ""}...`
4570
+ )
4571
+ );
4572
+ logger.log("");
4573
+ const results = [];
4574
+ for (let i = 0; i < servicesToSync.length; i++) {
4575
+ const service = servicesToSync[i];
4576
+ const serviceLabel = service.charAt(0).toUpperCase() + service.slice(1);
4577
+ logger.log(
4578
+ colors.highlight(`\u2501\u2501\u2501 Syncing ${i + 1}/${servicesToSync.length}: ${serviceLabel} \u2501\u2501\u2501`)
4579
+ );
4580
+ logger.log("");
4581
+ try {
4582
+ const result = await syncSingleService(service, options, TOTAL_STEPS);
4583
+ results.push({ service, created: result.created, skipped: result.skipped });
4584
+ logger.log("");
4585
+ } catch (error) {
4586
+ const err = error;
4587
+ logger.log("");
4588
+ logger.warn(`Failed to sync ${serviceLabel}: ${err.message}`);
4589
+ logger.log("");
4590
+ results.push({ service, created: 0, skipped: 0, error: err.message });
4591
+ }
4592
+ }
4593
+ logger.log("");
4594
+ logger.log(colors.highlight("\u2501\u2501\u2501 Sync Summary \u2501\u2501\u2501"));
4595
+ logger.log("");
4596
+ const successful = results.filter((r) => !r.error);
4597
+ const failed = results.filter((r) => r.error);
4598
+ const totalCreated = successful.reduce((sum, r) => sum + r.created, 0);
4599
+ if (successful.length > 0) {
4600
+ logger.log(
4601
+ theme.success(
4602
+ `\u2713 Successfully synced ${successful.length}/${servicesToSync.length} service${servicesToSync.length > 1 ? "s" : ""}:`
4603
+ )
4604
+ );
4605
+ for (const result of successful) {
4606
+ const serviceLabel = result.service.charAt(0).toUpperCase() + result.service.slice(1);
4607
+ logger.info(
4608
+ ` \u2022 ${serviceLabel}: ${result.created} brag${result.created !== 1 ? "s" : ""} created${result.skipped > 0 ? `, ${result.skipped} skipped` : ""}`
4609
+ );
4610
+ }
4611
+ logger.log("");
4612
+ }
4613
+ if (failed.length > 0) {
4614
+ logger.log(
4615
+ colors.warning(`\u2717 Failed to sync ${failed.length} service${failed.length > 1 ? "s" : ""}:`)
4616
+ );
4617
+ for (const result of failed) {
4618
+ const serviceLabel = result.service.charAt(0).toUpperCase() + result.service.slice(1);
4619
+ logger.info(` \u2022 ${serviceLabel}: ${result.error}`);
4620
+ }
4621
+ logger.log("");
4622
+ }
4623
+ if (totalCreated > 0) {
4624
+ logger.log(boxen6(formatSuccessMessage(totalCreated), boxStyles.success));
4625
+ } else if (successful.length > 0) {
4626
+ logger.log(
4627
+ boxen6(
4628
+ theme.secondary("No new brags created (all items already exist or were skipped)"),
4629
+ boxStyles.info
4630
+ )
4631
+ );
4632
+ }
4633
+ }
4510
4634
  async function syncCommand(options = {}) {
4511
4635
  logger.log("");
4512
4636
  const TOTAL_STEPS = 5;
4637
+ let sourceType;
4513
4638
  try {
4514
4639
  const isAuthenticated = await ensureAuthenticated();
4515
4640
  if (!isAuthenticated) {
@@ -4535,274 +4660,54 @@ async function syncCommand(options = {}) {
4535
4660
  return;
4536
4661
  }
4537
4662
  logger.debug(`Subscription tier "${subscriptionStatus.tier}" - proceeding with sync`);
4538
- const detectionSpinner = createStepSpinner(1, TOTAL_STEPS, "Detecting repository source");
4539
- detectionSpinner.start();
4540
- let sourceType = options.source;
4541
- if (!sourceType) {
4542
- const envConfig = loadEnvConfig();
4543
- sourceType = envConfig.source;
4544
- }
4545
- if (!sourceType) {
4546
- const projectConfig = await findProjectConfig();
4547
- sourceType = projectConfig?.defaultSource;
4548
- }
4549
- if (!sourceType) {
4550
- try {
4551
- const detectionResult = await sourceDetector.detectSources({
4552
- allowInteractive: true,
4553
- respectPriority: true,
4554
- showAuthStatus: true
4555
- });
4556
- sourceType = detectionResult.recommended;
4557
- if (detectionResult.detected.length > 1) {
4558
- logger.debug(
4559
- `Detected sources: ${detectionResult.detected.map((s) => s.type).join(", ")}`
4560
- );
4561
- }
4562
- if (!sourceType && detectionResult.detected.length === 0) {
4563
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "No supported sources detected");
4564
- logger.log("");
4565
- logger.info("Make sure you are in a git repository with a remote URL");
4566
- logger.info("Or use --source flag for non-git sources:");
4567
- logger.info(` ${theme.command("bragduck sync --source jira")}`);
4568
- logger.info(` ${theme.command("bragduck sync --source confluence")}`);
4569
- return;
4570
- }
4571
- } catch (error) {
4572
- if (error instanceof GitError) {
4573
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Not a git repository");
4574
- logger.log("");
4575
- logger.info("For non-git sources, use --source flag:");
4576
- logger.info(` ${theme.command("bragduck sync --source jira")}`);
4577
- logger.info(` ${theme.command("bragduck sync --source confluence")}`);
4578
- logger.log("");
4579
- logger.info("Or set default source in config:");
4580
- logger.info(` ${theme.command("bragduck config set defaultSource jira")}`);
4581
- return;
4582
- }
4583
- throw error;
4584
- }
4585
- }
4586
- if (!sourceType || !AdapterFactory.isSupported(sourceType)) {
4587
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Could not determine source");
4588
- try {
4589
- const detected = await sourceDetector.detectSources();
4590
- if (detected.detected.length > 0) {
4591
- logger.log("");
4592
- logger.info("Detected sources:");
4593
- for (const source of detected.detected) {
4594
- const authStatus2 = source.isAuthenticated ? "\u2713 authenticated" : "\u2717 not authenticated";
4595
- const repo = source.owner && source.repo ? `${source.owner}/${source.repo}` : source.host || "configured";
4596
- logger.info(` \u2022 ${source.type} (${authStatus2}) - ${repo}`);
4597
- }
4598
- logger.log("");
4599
- }
4600
- } catch {
4601
- }
4602
- logger.info("Specify source explicitly:");
4603
- logger.info(` ${theme.command("bragduck sync --source <type>")}`);
4604
- logger.log("");
4605
- logger.info("Supported sources: github, gitlab, bitbucket, jira, confluence");
4606
- return;
4607
- }
4608
- if (sourceType === "jira" || sourceType === "confluence") {
4609
- const creds = await storageService.getServiceCredentials(sourceType);
4610
- const envInstance = loadEnvConfig()[`${sourceType}Instance`];
4611
- const projectConfig = await findProjectConfig();
4612
- const configInstance = projectConfig?.[`${sourceType}Instance`];
4613
- if (!creds?.instanceUrl && !envInstance && !configInstance) {
4614
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `No ${sourceType} instance configured`);
4663
+ let selectedSource;
4664
+ if (options.source) {
4665
+ sourceType = options.source;
4666
+ if (!AdapterFactory.isSupported(sourceType)) {
4615
4667
  logger.log("");
4616
- logger.info("Configure instance via:");
4617
- logger.info(` ${theme.command("bragduck auth atlassian")} (interactive)`);
4618
- logger.info(
4619
- ` ${theme.command(`bragduck config set ${sourceType}Instance company.atlassian.net`)}`
4620
- );
4621
- logger.info(
4622
- ` ${theme.command(`export BRAGDUCK_${sourceType.toUpperCase()}_INSTANCE=company.atlassian.net`)}`
4623
- );
4668
+ logger.error(`Unsupported source: ${options.source}`);
4669
+ logger.log("");
4670
+ logger.info("Supported sources: github, gitlab, bitbucket, atlassian, jira, confluence");
4624
4671
  return;
4625
4672
  }
4626
- }
4627
- succeedStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `Source: ${theme.value(sourceType)}`);
4628
- logger.log("");
4629
- const adapter = AdapterFactory.getAdapter(sourceType);
4630
- const repoSpinner = createStepSpinner(2, TOTAL_STEPS, "Validating repository");
4631
- repoSpinner.start();
4632
- await adapter.validate();
4633
- const repoInfo = await adapter.getRepositoryInfo();
4634
- succeedStepSpinner(
4635
- repoSpinner,
4636
- 2,
4637
- TOTAL_STEPS,
4638
- `Repository: ${theme.value(repoInfo.fullName)}`
4639
- );
4640
- logger.log("");
4641
- let days = options.days;
4642
- if (!days) {
4643
- const defaultDays = storageService.getConfig("defaultCommitDays");
4644
- days = await promptDaysToScan(defaultDays);
4645
- logger.log("");
4646
- }
4647
- const fetchSpinner = createStepSpinner(
4648
- 3,
4649
- TOTAL_STEPS,
4650
- `Fetching work items from the last ${days} days`
4651
- );
4652
- fetchSpinner.start();
4653
- const workItems = await adapter.fetchWorkItems({
4654
- days,
4655
- author: options.all ? void 0 : await adapter.getCurrentUser() || void 0
4656
- });
4657
- if (workItems.length === 0) {
4658
- failStepSpinner(fetchSpinner, 3, TOTAL_STEPS, `No work items found in the last ${days} days`);
4659
- logger.log("");
4660
- logger.info("Try increasing the number of days or check your activity");
4661
- return;
4662
- }
4663
- succeedStepSpinner(
4664
- fetchSpinner,
4665
- 3,
4666
- TOTAL_STEPS,
4667
- `Found ${theme.count(workItems.length)} work item${workItems.length > 1 ? "s" : ""}`
4668
- );
4669
- logger.log("");
4670
- logger.log(formatCommitStats(workItems));
4671
- logger.log("");
4672
- let sortedCommits = [...workItems];
4673
- if (workItems.length > 1) {
4674
- const sortOption = await promptSortOption();
4675
- logger.log("");
4676
- if (sortOption === "date") {
4677
- sortedCommits.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
4678
- } else if (sortOption === "size") {
4679
- sortedCommits.sort((a, b) => {
4680
- const sizeA = (a.diffStats?.insertions || 0) + (a.diffStats?.deletions || 0);
4681
- const sizeB = (b.diffStats?.insertions || 0) + (b.diffStats?.deletions || 0);
4682
- return sizeB - sizeA;
4683
- });
4684
- } else if (sortOption === "files") {
4685
- sortedCommits.sort((a, b) => {
4686
- const filesA = a.diffStats?.filesChanged || 0;
4687
- const filesB = b.diffStats?.filesChanged || 0;
4688
- return filesB - filesA;
4689
- });
4690
- }
4691
- }
4692
- const selectedShas = await promptSelectCommits(sortedCommits);
4693
- if (selectedShas.length === 0) {
4694
- logger.log("");
4695
- logger.info(theme.secondary("No work items selected. Sync cancelled."));
4696
- logger.log("");
4697
- return;
4698
- }
4699
- const selectedCommits = sortedCommits.filter((c) => selectedShas.includes(c.sha));
4700
- logger.log(formatSelectionSummary(selectedCommits.length, selectedCommits));
4701
- logger.log("");
4702
- const existingBrags = await apiService.listBrags({ limit: 100 });
4703
- logger.debug(`Fetched ${existingBrags.brags.length} existing brags`);
4704
- const existingUrls = new Set(existingBrags.brags.flatMap((b) => b.attachments || []));
4705
- logger.debug(`Existing URLs in attachments: ${existingUrls.size}`);
4706
- const duplicates = selectedCommits.filter((c) => c.url && existingUrls.has(c.url));
4707
- const newCommits = selectedCommits.filter((c) => !c.url || !existingUrls.has(c.url));
4708
- logger.debug(`Duplicates: ${duplicates.length}, New: ${newCommits.length}`);
4709
- if (duplicates.length > 0) {
4710
- logger.log("");
4711
- logger.info(
4712
- colors.warning(
4713
- `${duplicates.length} work item${duplicates.length > 1 ? "s" : ""} already added to Bragduck - skipping`
4714
- )
4715
- );
4716
- logger.log("");
4717
- }
4718
- if (newCommits.length === 0) {
4719
- logger.log("");
4720
- logger.info(
4721
- theme.secondary("All selected work items already exist in Bragduck. Nothing to refine.")
4722
- );
4723
- logger.log("");
4724
- return;
4725
- }
4726
- const refineSpinner = createStepSpinner(
4727
- 4,
4728
- TOTAL_STEPS,
4729
- `Refining ${theme.count(newCommits.length)} work item${newCommits.length > 1 ? "s" : ""} with AI`
4730
- );
4731
- refineSpinner.start();
4732
- const refineRequest = {
4733
- brags: newCommits.map((c) => ({
4734
- text: c.message,
4735
- date: c.date,
4736
- title: c.message.split("\n")[0]
4737
- }))
4738
- };
4739
- const refineResponse = await apiService.refineBrags(refineRequest);
4740
- let refinedBrags = refineResponse.refined_brags;
4741
- succeedStepSpinner(refineSpinner, 4, TOTAL_STEPS, "Work items refined successfully");
4742
- logger.log("");
4743
- logger.info("Preview of refined brags:");
4744
- logger.log("");
4745
- logger.log(formatRefinedCommitsTable(refinedBrags, newCommits));
4746
- logger.log("");
4747
- const acceptedBrags = await promptReviewBrags(refinedBrags, newCommits);
4748
- if (acceptedBrags.length === 0) {
4673
+ selectedSource = sourceType;
4674
+ } else {
4749
4675
  logger.log("");
4750
- logger.info(theme.secondary("No brags selected for creation. Sync cancelled."));
4676
+ selectedSource = await promptSelectService();
4751
4677
  logger.log("");
4752
- return;
4753
4678
  }
4754
- logger.log("");
4755
- let selectedOrgId = null;
4756
- const userInfo = authService.getUserInfo();
4757
- if (userInfo?.id) {
4758
- try {
4759
- const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
4760
- if (orgsResponse.items.length > 0) {
4761
- selectedOrgId = await promptSelectOrganisation(orgsResponse.items);
4679
+ if (selectedSource === "all") {
4680
+ await syncAllAuthenticatedServices(options);
4681
+ } else {
4682
+ const detectionSpinner = createStepSpinner(1, TOTAL_STEPS, "Preparing sync");
4683
+ detectionSpinner.start();
4684
+ sourceType = selectedSource;
4685
+ if (sourceType === "jira" || sourceType === "confluence") {
4686
+ const creds = await storageService.getServiceCredentials(sourceType);
4687
+ const envInstance = loadEnvConfig()[`${sourceType}Instance`];
4688
+ const projectConfig = await findProjectConfig();
4689
+ const configInstance = projectConfig?.[`${sourceType}Instance`];
4690
+ if (!creds?.instanceUrl && !envInstance && !configInstance) {
4691
+ failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `No ${sourceType} instance configured`);
4762
4692
  logger.log("");
4693
+ logger.info("Configure instance via:");
4694
+ logger.info(` ${theme.command("bragduck auth atlassian")} (interactive)`);
4695
+ logger.info(
4696
+ ` ${theme.command(`bragduck config set ${sourceType}Instance company.atlassian.net`)}`
4697
+ );
4698
+ logger.info(
4699
+ ` ${theme.command(`export BRAGDUCK_${sourceType.toUpperCase()}_INSTANCE=company.atlassian.net`)}`
4700
+ );
4701
+ return;
4763
4702
  }
4764
- } catch {
4765
- logger.debug("Failed to fetch organisations, skipping org selection");
4703
+ }
4704
+ succeedStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `Source: ${theme.value(sourceType)}`);
4705
+ logger.log("");
4706
+ const result = await syncSingleService(sourceType, options, TOTAL_STEPS);
4707
+ if (result.created > 0) {
4708
+ logger.log(boxen6(formatSuccessMessage(result.created), boxStyles.success));
4766
4709
  }
4767
4710
  }
4768
- const createSpinner2 = createStepSpinner(
4769
- 5,
4770
- TOTAL_STEPS,
4771
- `Creating ${theme.count(acceptedBrags.length)} brag${acceptedBrags.length > 1 ? "s" : ""}`
4772
- );
4773
- createSpinner2.start();
4774
- const createRequest = {
4775
- brags: acceptedBrags.map((refined, index) => {
4776
- const originalCommit = newCommits[index];
4777
- return {
4778
- commit_sha: originalCommit?.sha || `brag-${index}`,
4779
- title: refined.refined_title,
4780
- description: refined.refined_description,
4781
- tags: refined.suggested_tags,
4782
- repository: repoInfo.url,
4783
- date: originalCommit?.date || "",
4784
- commit_url: originalCommit?.url || "",
4785
- impact_score: refined.suggested_impactLevel,
4786
- impact_description: refined.impact_description,
4787
- attachments: originalCommit?.url ? [originalCommit.url] : [],
4788
- orgId: selectedOrgId || void 0,
4789
- // External fields for non-git sources (Jira, Confluence, etc.)
4790
- externalId: originalCommit?.externalId,
4791
- externalType: originalCommit?.externalType,
4792
- externalSource: originalCommit?.externalSource,
4793
- externalUrl: originalCommit?.externalUrl
4794
- };
4795
- })
4796
- };
4797
- const createResponse = await apiService.createBrags(createRequest);
4798
- succeedStepSpinner(
4799
- createSpinner2,
4800
- 5,
4801
- TOTAL_STEPS,
4802
- `Created ${theme.count(createResponse.created)} brag${createResponse.created > 1 ? "s" : ""}`
4803
- );
4804
- logger.log("");
4805
- logger.log(boxen6(formatSuccessMessage(createResponse.created), boxStyles.success));
4806
4711
  } catch (error) {
4807
4712
  if (error instanceof CancelPromptError) {
4808
4713
  logger.log("");
@@ -4812,11 +4717,13 @@ async function syncCommand(options = {}) {
4812
4717
  }
4813
4718
  const err = error;
4814
4719
  logger.log("");
4815
- logger.log(boxen6(formatErrorMessage(err.message, getErrorHint2(err)), boxStyles.error));
4720
+ logger.log(
4721
+ boxen6(formatErrorMessage(err.message, getErrorHint2(err, sourceType)), boxStyles.error)
4722
+ );
4816
4723
  process.exit(1);
4817
4724
  }
4818
4725
  }
4819
- function getErrorHint2(error) {
4726
+ function getErrorHint2(error, sourceType) {
4820
4727
  if (error.name === "GitHubError") {
4821
4728
  return 'Make sure you are in a GitHub repository and have authenticated with "gh auth login"';
4822
4729
  }
@@ -4824,6 +4731,9 @@ function getErrorHint2(error) {
4824
4731
  return "Make sure you are in a git repository";
4825
4732
  }
4826
4733
  if (error.name === "TokenExpiredError" || error.name === "AuthenticationError") {
4734
+ if (sourceType === "jira" || sourceType === "confluence") {
4735
+ return 'This is your Bragduck platform session. Run "bragduck auth login" (not "bragduck auth atlassian")';
4736
+ }
4827
4737
  return 'Run "bragduck auth login" to login again';
4828
4738
  }
4829
4739
  if (error.name === "NetworkError") {