@bragduck/cli 2.8.0 → 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.
@@ -484,17 +484,10 @@ var init_storage_service = __esm({
484
484
  }
485
485
  }
486
486
  /**
487
- * Check if user is authenticated
487
+ * Check if user is authenticated (checks bragduck service)
488
488
  */
489
489
  async isAuthenticated() {
490
- const credentials = await this.getCredentials();
491
- if (!credentials || !credentials.accessToken) {
492
- return false;
493
- }
494
- if (credentials.expiresAt && credentials.expiresAt < Date.now()) {
495
- return false;
496
- }
497
- return true;
490
+ return this.isServiceAuthenticated("bragduck");
498
491
  }
499
492
  /**
500
493
  * Get credentials for a specific service
@@ -1229,7 +1222,9 @@ var init_auth_service = __esm({
1229
1222
  } catch (error) {
1230
1223
  logger.debug(`Token refresh failed: ${error.message}`);
1231
1224
  await this.logout();
1232
- 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
+ );
1233
1228
  }
1234
1229
  }
1235
1230
  };
@@ -1435,7 +1430,7 @@ var init_api_service = __esm({
1435
1430
  throw error;
1436
1431
  }
1437
1432
  throw new TokenExpiredError(
1438
- '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.'
1439
1434
  );
1440
1435
  }
1441
1436
  }
@@ -2184,188 +2179,10 @@ var CancelPromptError = class extends Error {
2184
2179
  init_api_service();
2185
2180
  init_storage_service();
2186
2181
  init_auth_service();
2187
- import boxen6 from "boxen";
2188
-
2189
- // src/utils/source-detector.ts
2190
- init_esm_shims();
2191
- init_errors();
2192
- init_storage_service();
2193
- import { exec as exec2 } from "child_process";
2194
- import { promisify as promisify2 } from "util";
2195
- import { select } from "@inquirer/prompts";
2196
- var execAsync2 = promisify2(exec2);
2197
- var SourceDetector = class {
2198
- /**
2199
- * Detect all possible sources from git remotes
2200
- */
2201
- async detectSources(options = {}) {
2202
- const detected = [];
2203
- try {
2204
- const { stdout } = await execAsync2("git remote -v");
2205
- const remotes = this.parseRemotes(stdout);
2206
- for (const remote of remotes) {
2207
- const source = this.parseRemoteUrl(remote.url);
2208
- if (source) {
2209
- const isAuthenticated = await this.checkAuthentication(source.type);
2210
- detected.push({
2211
- ...source,
2212
- remoteUrl: remote.url,
2213
- isAuthenticated
2214
- });
2215
- }
2216
- }
2217
- } catch {
2218
- throw new GitError("Not a git repository");
2219
- }
2220
- let recommended;
2221
- if (detected.length > 1 && options.allowInteractive && process.stdout.isTTY) {
2222
- try {
2223
- recommended = await this.promptSourceSelection(detected, options.showAuthStatus);
2224
- } catch {
2225
- }
2226
- }
2227
- if (!recommended && options.respectPriority) {
2228
- const priority = await storageService.getConfigWithHierarchy("sourcePriority");
2229
- if (priority) {
2230
- recommended = this.applyPriority(detected, priority);
2231
- }
2232
- }
2233
- if (!recommended) {
2234
- recommended = this.selectRecommendedSource(detected);
2235
- }
2236
- return { detected, recommended };
2237
- }
2238
- /**
2239
- * Prompt user to select a source interactively
2240
- */
2241
- async promptSourceSelection(sources, showAuthStatus = true) {
2242
- const choices = sources.map((source) => {
2243
- const authStatus2 = showAuthStatus ? source.isAuthenticated ? "\u2713 authenticated" : "\u2717 not authenticated" : "";
2244
- const repo = source.owner && source.repo ? `${source.owner}/${source.repo}` : source.host;
2245
- const name = `${source.type}${authStatus2 ? ` (${authStatus2})` : ""} - ${repo}`;
2246
- return {
2247
- name,
2248
- value: source.type,
2249
- description: source.remoteUrl
2250
- };
2251
- });
2252
- return await select({
2253
- message: "Multiple sources detected. Which do you want to sync?",
2254
- choices
2255
- });
2256
- }
2257
- /**
2258
- * Apply configured priority to select source
2259
- */
2260
- applyPriority(sources, priority) {
2261
- for (const sourceType of priority) {
2262
- const found = sources.find((s) => s.type === sourceType);
2263
- if (found) return found.type;
2264
- }
2265
- return void 0;
2266
- }
2267
- /**
2268
- * Parse git remote -v output
2269
- */
2270
- parseRemotes(output) {
2271
- const lines = output.split("\n").filter(Boolean);
2272
- const remotes = /* @__PURE__ */ new Map();
2273
- for (const line of lines) {
2274
- const match = line.match(/^(\S+)\s+(\S+)\s+\(fetch\)$/);
2275
- if (match && match[1] && match[2]) {
2276
- remotes.set(match[1], match[2]);
2277
- }
2278
- }
2279
- return Array.from(remotes.entries()).map(([name, url]) => ({ name, url }));
2280
- }
2281
- /**
2282
- * Parse remote URL to detect source type
2283
- */
2284
- parseRemoteUrl(url) {
2285
- if (url.includes("github.com")) {
2286
- const match = url.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
2287
- if (match && match[1] && match[2]) {
2288
- return {
2289
- type: "github",
2290
- host: "github.com",
2291
- owner: match[1],
2292
- repo: match[2]
2293
- };
2294
- }
2295
- }
2296
- if (url.includes("gitlab.com")) {
2297
- const match = url.match(/gitlab\.com[:/]([^/]+)\/([^/.]+)/);
2298
- if (match && match[1] && match[2]) {
2299
- return {
2300
- type: "gitlab",
2301
- host: "gitlab.com",
2302
- owner: match[1],
2303
- repo: match[2]
2304
- };
2305
- }
2306
- }
2307
- if (url.includes("bitbucket.org")) {
2308
- const match = url.match(/bitbucket\.org[:/]([^/]+)\/([^/.]+)/);
2309
- if (match && match[1] && match[2]) {
2310
- return {
2311
- type: "bitbucket",
2312
- host: "bitbucket.org",
2313
- owner: match[1],
2314
- repo: match[2]
2315
- };
2316
- }
2317
- }
2318
- if (url.match(/\/scm\/|bitbucket\./)) {
2319
- const match = url.match(/([^/:]+)[:/]scm\/([^/]+)\/([^/.]+)/);
2320
- if (match && match[1] && match[2] && match[3]) {
2321
- return {
2322
- type: "atlassian",
2323
- host: match[1],
2324
- owner: match[2],
2325
- repo: match[3]
2326
- };
2327
- }
2328
- }
2329
- return null;
2330
- }
2331
- /**
2332
- * Check if user is authenticated with a specific source
2333
- */
2334
- async checkAuthentication(type) {
2335
- try {
2336
- if (type === "github") {
2337
- await execAsync2("command gh auth status");
2338
- return true;
2339
- } else if (type === "bitbucket" || type === "atlassian") {
2340
- return await storageService.isServiceAuthenticated("bitbucket");
2341
- } else if (type === "gitlab") {
2342
- return await storageService.isServiceAuthenticated("gitlab");
2343
- } else if (type === "jira") {
2344
- return await storageService.isServiceAuthenticated("jira");
2345
- } else if (type === "confluence") {
2346
- return await storageService.isServiceAuthenticated("confluence");
2347
- }
2348
- return false;
2349
- } catch {
2350
- return false;
2351
- }
2352
- }
2353
- /**
2354
- * Select recommended source (prefer authenticated GitHub)
2355
- */
2356
- selectRecommendedSource(sources) {
2357
- const authenticated = sources.find((s) => s.isAuthenticated);
2358
- if (authenticated) return authenticated.type;
2359
- const github = sources.find((s) => s.type === "github");
2360
- if (github) return "github";
2361
- return sources[0]?.type;
2362
- }
2363
- };
2364
- var sourceDetector = new SourceDetector();
2365
-
2366
- // src/commands/sync.ts
2367
2182
  init_env_loader();
2368
2183
  init_config_loader();
2184
+ import { select as select2 } from "@inquirer/prompts";
2185
+ import boxen6 from "boxen";
2369
2186
 
2370
2187
  // src/sync/adapter-factory.ts
2371
2188
  init_esm_shims();
@@ -2377,8 +2194,8 @@ init_esm_shims();
2377
2194
  init_esm_shims();
2378
2195
  init_errors();
2379
2196
  init_logger();
2380
- import { exec as exec3 } from "child_process";
2381
- import { promisify as promisify3 } from "util";
2197
+ import { exec as exec2 } from "child_process";
2198
+ import { promisify as promisify2 } from "util";
2382
2199
 
2383
2200
  // src/services/git.service.ts
2384
2201
  init_esm_shims();
@@ -2603,7 +2420,7 @@ var GitService = class {
2603
2420
  var gitService = new GitService();
2604
2421
 
2605
2422
  // src/services/github.service.ts
2606
- var execAsync3 = promisify3(exec3);
2423
+ var execAsync2 = promisify2(exec2);
2607
2424
  var GitHubService = class {
2608
2425
  MAX_BODY_LENGTH = 5e3;
2609
2426
  PR_SEARCH_FIELDS = "number,title,body,author,mergedAt,additions,deletions,changedFiles,url,labels";
@@ -2612,7 +2429,7 @@ var GitHubService = class {
2612
2429
  */
2613
2430
  async checkGitHubCLI() {
2614
2431
  try {
2615
- await execAsync3("command gh --version");
2432
+ await execAsync2("command gh --version");
2616
2433
  return true;
2617
2434
  } catch {
2618
2435
  return false;
@@ -2634,7 +2451,7 @@ var GitHubService = class {
2634
2451
  */
2635
2452
  async checkAuthentication() {
2636
2453
  try {
2637
- await execAsync3("command gh auth status");
2454
+ await execAsync2("command gh auth status");
2638
2455
  return true;
2639
2456
  } catch {
2640
2457
  return false;
@@ -2659,7 +2476,7 @@ var GitHubService = class {
2659
2476
  await this.ensureGitHubCLI();
2660
2477
  await this.ensureAuthentication();
2661
2478
  await gitService.validateRepository();
2662
- const { stdout } = await execAsync3("command gh repo view --json url");
2479
+ const { stdout } = await execAsync2("command gh repo view --json url");
2663
2480
  const data = JSON.parse(stdout);
2664
2481
  if (!data.url) {
2665
2482
  throw new GitHubError("This repository is not hosted on GitHub", {
@@ -2691,7 +2508,7 @@ var GitHubService = class {
2691
2508
  async getRepositoryInfo() {
2692
2509
  try {
2693
2510
  await this.ensureGitHubCLI();
2694
- const { stdout } = await execAsync3(
2511
+ const { stdout } = await execAsync2(
2695
2512
  "command gh repo view --json owner,name,url,nameWithOwner"
2696
2513
  );
2697
2514
  const data = JSON.parse(stdout);
@@ -2716,7 +2533,7 @@ var GitHubService = class {
2716
2533
  */
2717
2534
  async getCurrentGitHubUser() {
2718
2535
  try {
2719
- const { stdout } = await execAsync3("command gh api user --jq .login");
2536
+ const { stdout } = await execAsync2("command gh api user --jq .login");
2720
2537
  return stdout.trim() || null;
2721
2538
  } catch {
2722
2539
  logger.debug("Failed to get GitHub user");
@@ -2742,7 +2559,7 @@ var GitHubService = class {
2742
2559
  const limitArg = limit ? `--limit ${limit}` : "";
2743
2560
  const command = `command gh pr list --state merged --json ${this.PR_SEARCH_FIELDS} --search "${searchQuery}" ${limitArg}`;
2744
2561
  logger.debug(`Running: ${command}`);
2745
- const { stdout } = await execAsync3(command);
2562
+ const { stdout } = await execAsync2(command);
2746
2563
  const prs = JSON.parse(stdout);
2747
2564
  logger.debug(`Found ${prs.length} merged PRs`);
2748
2565
  return prs;
@@ -2853,9 +2670,9 @@ init_esm_shims();
2853
2670
  init_errors();
2854
2671
  init_logger();
2855
2672
  init_storage_service();
2856
- import { exec as exec4 } from "child_process";
2857
- import { promisify as promisify4 } from "util";
2858
- var execAsync4 = promisify4(exec4);
2673
+ import { exec as exec3 } from "child_process";
2674
+ import { promisify as promisify3 } from "util";
2675
+ var execAsync3 = promisify3(exec3);
2859
2676
  var BitbucketService = class {
2860
2677
  BITBUCKET_API_BASE = "https://api.bitbucket.org/2.0";
2861
2678
  MAX_DESCRIPTION_LENGTH = 5e3;
@@ -2940,7 +2757,7 @@ var BitbucketService = class {
2940
2757
  */
2941
2758
  async getRepoFromGit() {
2942
2759
  try {
2943
- const { stdout } = await execAsync4("command git remote get-url origin");
2760
+ const { stdout } = await execAsync3("command git remote get-url origin");
2944
2761
  const remoteUrl = stdout.trim();
2945
2762
  const parsed = this.parseRemoteUrl(remoteUrl);
2946
2763
  if (!parsed) {
@@ -3135,10 +2952,10 @@ init_esm_shims();
3135
2952
  init_errors();
3136
2953
  init_logger();
3137
2954
  init_storage_service();
3138
- import { exec as exec5 } from "child_process";
3139
- import { promisify as promisify5 } from "util";
2955
+ import { exec as exec4 } from "child_process";
2956
+ import { promisify as promisify4 } from "util";
3140
2957
  import { URLSearchParams as URLSearchParams3 } from "url";
3141
- var execAsync5 = promisify5(exec5);
2958
+ var execAsync4 = promisify4(exec4);
3142
2959
  var GitLabService = class {
3143
2960
  DEFAULT_INSTANCE = "https://gitlab.com";
3144
2961
  MAX_DESCRIPTION_LENGTH = 5e3;
@@ -3224,7 +3041,7 @@ var GitLabService = class {
3224
3041
  */
3225
3042
  async getProjectFromGit() {
3226
3043
  try {
3227
- const { stdout } = await execAsync5("git remote get-url origin");
3044
+ const { stdout } = await execAsync4("git remote get-url origin");
3228
3045
  const remoteUrl = stdout.trim();
3229
3046
  const parsed = this.parseRemoteUrl(remoteUrl);
3230
3047
  if (!parsed) {
@@ -4116,7 +3933,7 @@ init_logger();
4116
3933
 
4117
3934
  // src/ui/prompts.ts
4118
3935
  init_esm_shims();
4119
- 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";
4120
3937
  import boxen4 from "boxen";
4121
3938
 
4122
3939
  // src/ui/formatters.ts
@@ -4272,7 +4089,7 @@ async function promptDaysToScan(defaultDays = 30) {
4272
4089
  { name: "90 days", value: "90", description: "Last 3 months" },
4273
4090
  { name: "Custom", value: "custom", description: "Enter custom number of days" }
4274
4091
  ];
4275
- const selected = await select2({
4092
+ const selected = await select({
4276
4093
  message: "How many days back should we scan for PRs?",
4277
4094
  choices,
4278
4095
  default: "30"
@@ -4304,7 +4121,7 @@ async function promptSortOption() {
4304
4121
  { name: "By files (most files)", value: "files", description: "Most files changed" },
4305
4122
  { name: "No sorting", value: "none", description: "Keep original order" }
4306
4123
  ];
4307
- return await select2({
4124
+ return await select({
4308
4125
  message: "How would you like to sort the PRs?",
4309
4126
  choices,
4310
4127
  default: "date"
@@ -4318,7 +4135,7 @@ async function promptSelectOrganisation(organisations) {
4318
4135
  value: org.id
4319
4136
  }))
4320
4137
  ];
4321
- const selected = await select2({
4138
+ const selected = await select({
4322
4139
  message: "Attach brags to which company?",
4323
4140
  choices,
4324
4141
  default: "none"
@@ -4368,7 +4185,7 @@ ${theme.label("PR Link")} ${colors.link(prUrl)}`;
4368
4185
  }
4369
4186
  console.log(boxen4(bragDetails, boxStyles.info));
4370
4187
  console.log("");
4371
- const action = await select2({
4188
+ const action = await select({
4372
4189
  message: `What would you like to do with this brag?`,
4373
4190
  choices: [
4374
4191
  { name: "\u2713 Accept", value: "accept", description: "Add this brag as-is" },
@@ -4453,7 +4270,7 @@ async function ensureAuthenticated() {
4453
4270
  if (!shouldAuth) {
4454
4271
  logger.log("");
4455
4272
  logger.info(
4456
- 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.")
4457
4274
  );
4458
4275
  logger.log("");
4459
4276
  return false;
@@ -4466,9 +4283,6 @@ async function ensureAuthenticated() {
4466
4283
  }
4467
4284
  }
4468
4285
 
4469
- // src/commands/sync.ts
4470
- init_errors();
4471
-
4472
4286
  // src/ui/spinners.ts
4473
4287
  init_esm_shims();
4474
4288
  import ora2 from "ora";
@@ -4514,9 +4328,313 @@ function failStepSpinner(spinner, currentStep, totalSteps, text) {
4514
4328
  }
4515
4329
 
4516
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
+ }
4517
4634
  async function syncCommand(options = {}) {
4518
4635
  logger.log("");
4519
4636
  const TOTAL_STEPS = 5;
4637
+ let sourceType;
4520
4638
  try {
4521
4639
  const isAuthenticated = await ensureAuthenticated();
4522
4640
  if (!isAuthenticated) {
@@ -4542,274 +4660,54 @@ async function syncCommand(options = {}) {
4542
4660
  return;
4543
4661
  }
4544
4662
  logger.debug(`Subscription tier "${subscriptionStatus.tier}" - proceeding with sync`);
4545
- const detectionSpinner = createStepSpinner(1, TOTAL_STEPS, "Detecting repository source");
4546
- detectionSpinner.start();
4547
- let sourceType = options.source;
4548
- if (!sourceType) {
4549
- const envConfig = loadEnvConfig();
4550
- sourceType = envConfig.source;
4551
- }
4552
- if (!sourceType) {
4553
- const projectConfig = await findProjectConfig();
4554
- sourceType = projectConfig?.defaultSource;
4555
- }
4556
- if (!sourceType) {
4557
- try {
4558
- const detectionResult = await sourceDetector.detectSources({
4559
- allowInteractive: true,
4560
- respectPriority: true,
4561
- showAuthStatus: true
4562
- });
4563
- sourceType = detectionResult.recommended;
4564
- if (detectionResult.detected.length > 1) {
4565
- logger.debug(
4566
- `Detected sources: ${detectionResult.detected.map((s) => s.type).join(", ")}`
4567
- );
4568
- }
4569
- if (!sourceType && detectionResult.detected.length === 0) {
4570
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "No supported sources detected");
4571
- logger.log("");
4572
- logger.info("Make sure you are in a git repository with a remote URL");
4573
- logger.info("Or use --source flag for non-git sources:");
4574
- logger.info(` ${theme.command("bragduck sync --source jira")}`);
4575
- logger.info(` ${theme.command("bragduck sync --source confluence")}`);
4576
- return;
4577
- }
4578
- } catch (error) {
4579
- if (error instanceof GitError) {
4580
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Not a git repository");
4581
- logger.log("");
4582
- logger.info("For non-git sources, use --source flag:");
4583
- logger.info(` ${theme.command("bragduck sync --source jira")}`);
4584
- logger.info(` ${theme.command("bragduck sync --source confluence")}`);
4585
- logger.log("");
4586
- logger.info("Or set default source in config:");
4587
- logger.info(` ${theme.command("bragduck config set defaultSource jira")}`);
4588
- return;
4589
- }
4590
- throw error;
4591
- }
4592
- }
4593
- if (!sourceType || !AdapterFactory.isSupported(sourceType)) {
4594
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Could not determine source");
4595
- try {
4596
- const detected = await sourceDetector.detectSources();
4597
- if (detected.detected.length > 0) {
4598
- logger.log("");
4599
- logger.info("Detected sources:");
4600
- for (const source of detected.detected) {
4601
- const authStatus2 = source.isAuthenticated ? "\u2713 authenticated" : "\u2717 not authenticated";
4602
- const repo = source.owner && source.repo ? `${source.owner}/${source.repo}` : source.host || "configured";
4603
- logger.info(` \u2022 ${source.type} (${authStatus2}) - ${repo}`);
4604
- }
4605
- logger.log("");
4606
- }
4607
- } catch {
4608
- }
4609
- logger.info("Specify source explicitly:");
4610
- logger.info(` ${theme.command("bragduck sync --source <type>")}`);
4611
- logger.log("");
4612
- logger.info("Supported sources: github, gitlab, bitbucket, jira, confluence");
4613
- return;
4614
- }
4615
- if (sourceType === "jira" || sourceType === "confluence") {
4616
- const creds = await storageService.getServiceCredentials(sourceType);
4617
- const envInstance = loadEnvConfig()[`${sourceType}Instance`];
4618
- const projectConfig = await findProjectConfig();
4619
- const configInstance = projectConfig?.[`${sourceType}Instance`];
4620
- if (!creds?.instanceUrl && !envInstance && !configInstance) {
4621
- 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)) {
4622
4667
  logger.log("");
4623
- logger.info("Configure instance via:");
4624
- logger.info(` ${theme.command("bragduck auth atlassian")} (interactive)`);
4625
- logger.info(
4626
- ` ${theme.command(`bragduck config set ${sourceType}Instance company.atlassian.net`)}`
4627
- );
4628
- logger.info(
4629
- ` ${theme.command(`export BRAGDUCK_${sourceType.toUpperCase()}_INSTANCE=company.atlassian.net`)}`
4630
- );
4668
+ logger.error(`Unsupported source: ${options.source}`);
4669
+ logger.log("");
4670
+ logger.info("Supported sources: github, gitlab, bitbucket, atlassian, jira, confluence");
4631
4671
  return;
4632
4672
  }
4633
- }
4634
- succeedStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `Source: ${theme.value(sourceType)}`);
4635
- logger.log("");
4636
- const adapter = AdapterFactory.getAdapter(sourceType);
4637
- const repoSpinner = createStepSpinner(2, TOTAL_STEPS, "Validating repository");
4638
- repoSpinner.start();
4639
- await adapter.validate();
4640
- const repoInfo = await adapter.getRepositoryInfo();
4641
- succeedStepSpinner(
4642
- repoSpinner,
4643
- 2,
4644
- TOTAL_STEPS,
4645
- `Repository: ${theme.value(repoInfo.fullName)}`
4646
- );
4647
- logger.log("");
4648
- let days = options.days;
4649
- if (!days) {
4650
- const defaultDays = storageService.getConfig("defaultCommitDays");
4651
- days = await promptDaysToScan(defaultDays);
4652
- logger.log("");
4653
- }
4654
- const fetchSpinner = createStepSpinner(
4655
- 3,
4656
- TOTAL_STEPS,
4657
- `Fetching work items from the last ${days} days`
4658
- );
4659
- fetchSpinner.start();
4660
- const workItems = await adapter.fetchWorkItems({
4661
- days,
4662
- author: options.all ? void 0 : await adapter.getCurrentUser() || void 0
4663
- });
4664
- if (workItems.length === 0) {
4665
- failStepSpinner(fetchSpinner, 3, TOTAL_STEPS, `No work items found in the last ${days} days`);
4666
- logger.log("");
4667
- logger.info("Try increasing the number of days or check your activity");
4668
- return;
4669
- }
4670
- succeedStepSpinner(
4671
- fetchSpinner,
4672
- 3,
4673
- TOTAL_STEPS,
4674
- `Found ${theme.count(workItems.length)} work item${workItems.length > 1 ? "s" : ""}`
4675
- );
4676
- logger.log("");
4677
- logger.log(formatCommitStats(workItems));
4678
- logger.log("");
4679
- let sortedCommits = [...workItems];
4680
- if (workItems.length > 1) {
4681
- const sortOption = await promptSortOption();
4682
- logger.log("");
4683
- if (sortOption === "date") {
4684
- sortedCommits.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
4685
- } else if (sortOption === "size") {
4686
- sortedCommits.sort((a, b) => {
4687
- const sizeA = (a.diffStats?.insertions || 0) + (a.diffStats?.deletions || 0);
4688
- const sizeB = (b.diffStats?.insertions || 0) + (b.diffStats?.deletions || 0);
4689
- return sizeB - sizeA;
4690
- });
4691
- } else if (sortOption === "files") {
4692
- sortedCommits.sort((a, b) => {
4693
- const filesA = a.diffStats?.filesChanged || 0;
4694
- const filesB = b.diffStats?.filesChanged || 0;
4695
- return filesB - filesA;
4696
- });
4697
- }
4698
- }
4699
- const selectedShas = await promptSelectCommits(sortedCommits);
4700
- if (selectedShas.length === 0) {
4701
- logger.log("");
4702
- logger.info(theme.secondary("No work items selected. Sync cancelled."));
4703
- logger.log("");
4704
- return;
4705
- }
4706
- const selectedCommits = sortedCommits.filter((c) => selectedShas.includes(c.sha));
4707
- logger.log(formatSelectionSummary(selectedCommits.length, selectedCommits));
4708
- logger.log("");
4709
- const existingBrags = await apiService.listBrags({ limit: 100 });
4710
- logger.debug(`Fetched ${existingBrags.brags.length} existing brags`);
4711
- const existingUrls = new Set(existingBrags.brags.flatMap((b) => b.attachments || []));
4712
- logger.debug(`Existing URLs in attachments: ${existingUrls.size}`);
4713
- const duplicates = selectedCommits.filter((c) => c.url && existingUrls.has(c.url));
4714
- const newCommits = selectedCommits.filter((c) => !c.url || !existingUrls.has(c.url));
4715
- logger.debug(`Duplicates: ${duplicates.length}, New: ${newCommits.length}`);
4716
- if (duplicates.length > 0) {
4717
- logger.log("");
4718
- logger.info(
4719
- colors.warning(
4720
- `${duplicates.length} work item${duplicates.length > 1 ? "s" : ""} already added to Bragduck - skipping`
4721
- )
4722
- );
4723
- logger.log("");
4724
- }
4725
- if (newCommits.length === 0) {
4726
- logger.log("");
4727
- logger.info(
4728
- theme.secondary("All selected work items already exist in Bragduck. Nothing to refine.")
4729
- );
4730
- logger.log("");
4731
- return;
4732
- }
4733
- const refineSpinner = createStepSpinner(
4734
- 4,
4735
- TOTAL_STEPS,
4736
- `Refining ${theme.count(newCommits.length)} work item${newCommits.length > 1 ? "s" : ""} with AI`
4737
- );
4738
- refineSpinner.start();
4739
- const refineRequest = {
4740
- brags: newCommits.map((c) => ({
4741
- text: c.message,
4742
- date: c.date,
4743
- title: c.message.split("\n")[0]
4744
- }))
4745
- };
4746
- const refineResponse = await apiService.refineBrags(refineRequest);
4747
- let refinedBrags = refineResponse.refined_brags;
4748
- succeedStepSpinner(refineSpinner, 4, TOTAL_STEPS, "Work items refined successfully");
4749
- logger.log("");
4750
- logger.info("Preview of refined brags:");
4751
- logger.log("");
4752
- logger.log(formatRefinedCommitsTable(refinedBrags, newCommits));
4753
- logger.log("");
4754
- const acceptedBrags = await promptReviewBrags(refinedBrags, newCommits);
4755
- if (acceptedBrags.length === 0) {
4673
+ selectedSource = sourceType;
4674
+ } else {
4756
4675
  logger.log("");
4757
- logger.info(theme.secondary("No brags selected for creation. Sync cancelled."));
4676
+ selectedSource = await promptSelectService();
4758
4677
  logger.log("");
4759
- return;
4760
4678
  }
4761
- logger.log("");
4762
- let selectedOrgId = null;
4763
- const userInfo = authService.getUserInfo();
4764
- if (userInfo?.id) {
4765
- try {
4766
- const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
4767
- if (orgsResponse.items.length > 0) {
4768
- 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`);
4769
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;
4770
4702
  }
4771
- } catch {
4772
- 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));
4773
4709
  }
4774
4710
  }
4775
- const createSpinner2 = createStepSpinner(
4776
- 5,
4777
- TOTAL_STEPS,
4778
- `Creating ${theme.count(acceptedBrags.length)} brag${acceptedBrags.length > 1 ? "s" : ""}`
4779
- );
4780
- createSpinner2.start();
4781
- const createRequest = {
4782
- brags: acceptedBrags.map((refined, index) => {
4783
- const originalCommit = newCommits[index];
4784
- return {
4785
- commit_sha: originalCommit?.sha || `brag-${index}`,
4786
- title: refined.refined_title,
4787
- description: refined.refined_description,
4788
- tags: refined.suggested_tags,
4789
- repository: repoInfo.url,
4790
- date: originalCommit?.date || "",
4791
- commit_url: originalCommit?.url || "",
4792
- impact_score: refined.suggested_impactLevel,
4793
- impact_description: refined.impact_description,
4794
- attachments: originalCommit?.url ? [originalCommit.url] : [],
4795
- orgId: selectedOrgId || void 0,
4796
- // External fields for non-git sources (Jira, Confluence, etc.)
4797
- externalId: originalCommit?.externalId,
4798
- externalType: originalCommit?.externalType,
4799
- externalSource: originalCommit?.externalSource,
4800
- externalUrl: originalCommit?.externalUrl
4801
- };
4802
- })
4803
- };
4804
- const createResponse = await apiService.createBrags(createRequest);
4805
- succeedStepSpinner(
4806
- createSpinner2,
4807
- 5,
4808
- TOTAL_STEPS,
4809
- `Created ${theme.count(createResponse.created)} brag${createResponse.created > 1 ? "s" : ""}`
4810
- );
4811
- logger.log("");
4812
- logger.log(boxen6(formatSuccessMessage(createResponse.created), boxStyles.success));
4813
4711
  } catch (error) {
4814
4712
  if (error instanceof CancelPromptError) {
4815
4713
  logger.log("");
@@ -4819,11 +4717,13 @@ async function syncCommand(options = {}) {
4819
4717
  }
4820
4718
  const err = error;
4821
4719
  logger.log("");
4822
- logger.log(boxen6(formatErrorMessage(err.message, getErrorHint2(err)), boxStyles.error));
4720
+ logger.log(
4721
+ boxen6(formatErrorMessage(err.message, getErrorHint2(err, sourceType)), boxStyles.error)
4722
+ );
4823
4723
  process.exit(1);
4824
4724
  }
4825
4725
  }
4826
- function getErrorHint2(error) {
4726
+ function getErrorHint2(error, sourceType) {
4827
4727
  if (error.name === "GitHubError") {
4828
4728
  return 'Make sure you are in a GitHub repository and have authenticated with "gh auth login"';
4829
4729
  }
@@ -4831,6 +4731,9 @@ function getErrorHint2(error) {
4831
4731
  return "Make sure you are in a git repository";
4832
4732
  }
4833
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
+ }
4834
4737
  return 'Run "bragduck auth login" to login again';
4835
4738
  }
4836
4739
  if (error.name === "NetworkError") {