@bragduck/cli 2.6.0 → 2.7.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.
@@ -392,7 +392,7 @@ var init_storage_service = __esm({
392
392
  });
393
393
 
394
394
  // src/utils/errors.ts
395
- var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError;
395
+ var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError, JiraError, ConfluenceError;
396
396
  var init_errors = __esm({
397
397
  "src/utils/errors.ts"() {
398
398
  "use strict";
@@ -471,6 +471,18 @@ var init_errors = __esm({
471
471
  this.name = "GitLabError";
472
472
  }
473
473
  };
474
+ JiraError = class extends BragduckError {
475
+ constructor(message, details) {
476
+ super(message, "JIRA_ERROR", details);
477
+ this.name = "JiraError";
478
+ }
479
+ };
480
+ ConfluenceError = class extends BragduckError {
481
+ constructor(message, details) {
482
+ super(message, "CONFLUENCE_ERROR", details);
483
+ this.name = "ConfluenceError";
484
+ }
485
+ };
474
486
  }
475
487
  });
476
488
 
@@ -1621,9 +1633,11 @@ async function authCommand(subcommand) {
1621
1633
  await authBitbucket();
1622
1634
  } else if (subcommand === "gitlab") {
1623
1635
  await authGitLab();
1636
+ } else if (subcommand === "atlassian") {
1637
+ await authAtlassian();
1624
1638
  } else {
1625
1639
  logger.error(`Unknown auth subcommand: ${subcommand}`);
1626
- logger.info("Available subcommands: login, status, bitbucket, gitlab");
1640
+ logger.info("Available subcommands: login, status, bitbucket, gitlab, atlassian");
1627
1641
  process.exit(1);
1628
1642
  }
1629
1643
  }
@@ -1858,6 +1872,87 @@ User: ${user.name} (@${user.username})`,
1858
1872
  process.exit(1);
1859
1873
  }
1860
1874
  }
1875
+ async function authAtlassian() {
1876
+ logger.log("");
1877
+ logger.log(
1878
+ boxen(
1879
+ theme.info("Atlassian API Token Authentication") + "\n\nThis token works for Jira, Confluence, and Bitbucket\n\nCreate an API Token at:\n" + colors.highlight("https://id.atlassian.com/manage-profile/security/api-tokens") + "\n\nRequired access:\n \u2022 Jira: Read issues\n \u2022 Confluence: Read pages\n \u2022 Bitbucket: Read repositories",
1880
+ boxStyles.info
1881
+ )
1882
+ );
1883
+ logger.log("");
1884
+ try {
1885
+ const instanceUrl = await input({
1886
+ message: "Atlassian instance URL (e.g., company.atlassian.net):",
1887
+ validate: (value) => value.length > 0 ? true : "Instance URL cannot be empty"
1888
+ });
1889
+ const email = await input({
1890
+ message: "Atlassian account email:",
1891
+ validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
1892
+ });
1893
+ const apiToken = await input({
1894
+ message: "API Token:",
1895
+ validate: (value) => value.length > 0 ? true : "API token cannot be empty"
1896
+ });
1897
+ const testInstanceUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
1898
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
1899
+ const response = await fetch(`${testInstanceUrl}/rest/api/2/myself`, {
1900
+ headers: { Authorization: `Basic ${auth}` }
1901
+ });
1902
+ if (!response.ok) {
1903
+ logger.log("");
1904
+ logger.log(
1905
+ boxen(
1906
+ theme.error("\u2717 Authentication Failed") + "\n\nInvalid instance URL, email, or API token\nMake sure the instance URL is correct (e.g., company.atlassian.net)",
1907
+ boxStyles.error
1908
+ )
1909
+ );
1910
+ logger.log("");
1911
+ process.exit(1);
1912
+ }
1913
+ const user = await response.json();
1914
+ const credentials = {
1915
+ accessToken: apiToken,
1916
+ username: email,
1917
+ instanceUrl: instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`
1918
+ };
1919
+ await storageService.setServiceCredentials("jira", credentials);
1920
+ await storageService.setServiceCredentials("confluence", credentials);
1921
+ await storageService.setServiceCredentials("bitbucket", {
1922
+ ...credentials,
1923
+ instanceUrl: "https://api.bitbucket.org"
1924
+ // Bitbucket uses different API base
1925
+ });
1926
+ logger.log("");
1927
+ logger.log(
1928
+ boxen(
1929
+ theme.success("\u2713 Successfully authenticated with Atlassian") + `
1930
+
1931
+ Instance: ${instanceUrl}
1932
+ User: ${user.displayName}
1933
+ Email: ${user.emailAddress}
1934
+
1935
+ Services configured:
1936
+ \u2022 Jira
1937
+ \u2022 Confluence
1938
+ \u2022 Bitbucket`,
1939
+ boxStyles.success
1940
+ )
1941
+ );
1942
+ logger.log("");
1943
+ } catch (error) {
1944
+ const err = error;
1945
+ logger.log("");
1946
+ logger.log(
1947
+ boxen(
1948
+ theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
1949
+ boxStyles.error
1950
+ )
1951
+ );
1952
+ logger.log("");
1953
+ process.exit(1);
1954
+ }
1955
+ }
1861
1956
 
1862
1957
  // src/commands/sync.ts
1863
1958
  init_esm_shims();
@@ -1984,6 +2079,10 @@ var SourceDetector = class {
1984
2079
  return await storageService.isServiceAuthenticated("bitbucket");
1985
2080
  } else if (type === "gitlab") {
1986
2081
  return await storageService.isServiceAuthenticated("gitlab");
2082
+ } else if (type === "jira") {
2083
+ return await storageService.isServiceAuthenticated("jira");
2084
+ } else if (type === "confluence") {
2085
+ return await storageService.isServiceAuthenticated("confluence");
1987
2086
  }
1988
2087
  return false;
1989
2088
  } catch {
@@ -3056,6 +3155,535 @@ var GitLabSyncAdapter = class {
3056
3155
  };
3057
3156
  var gitlabSyncAdapter = new GitLabSyncAdapter();
3058
3157
 
3158
+ // src/sync/jira-adapter.ts
3159
+ init_esm_shims();
3160
+
3161
+ // src/services/jira.service.ts
3162
+ init_esm_shims();
3163
+ init_errors();
3164
+ init_logger();
3165
+ init_storage_service();
3166
+ var JiraService = class {
3167
+ MAX_DESCRIPTION_LENGTH = 5e3;
3168
+ /**
3169
+ * Get stored Jira credentials
3170
+ */
3171
+ async getCredentials() {
3172
+ const creds = await storageService.getServiceCredentials("jira");
3173
+ if (!creds || !creds.username || !creds.accessToken || !creds.instanceUrl) {
3174
+ throw new JiraError("Not authenticated with Jira", {
3175
+ hint: "Run: bragduck auth atlassian"
3176
+ });
3177
+ }
3178
+ if (creds.expiresAt && creds.expiresAt < Date.now()) {
3179
+ throw new JiraError("API token has expired", {
3180
+ hint: "Run: bragduck auth atlassian"
3181
+ });
3182
+ }
3183
+ return {
3184
+ email: creds.username,
3185
+ apiToken: creds.accessToken,
3186
+ instanceUrl: creds.instanceUrl
3187
+ };
3188
+ }
3189
+ /**
3190
+ * Make authenticated request to Jira API
3191
+ */
3192
+ async request(endpoint, method = "GET", body) {
3193
+ const { email, apiToken, instanceUrl } = await this.getCredentials();
3194
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
3195
+ const baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
3196
+ logger.debug(`Jira API: ${method} ${endpoint}`);
3197
+ const options = {
3198
+ method,
3199
+ headers: {
3200
+ Authorization: `Basic ${auth}`,
3201
+ "Content-Type": "application/json",
3202
+ Accept: "application/json"
3203
+ }
3204
+ };
3205
+ if (body) {
3206
+ options.body = JSON.stringify(body);
3207
+ }
3208
+ const response = await fetch(`${baseUrl}${endpoint}`, options);
3209
+ if (!response.ok) {
3210
+ const statusText = response.statusText;
3211
+ const status = response.status;
3212
+ if (status === 401) {
3213
+ throw new JiraError("Invalid or expired API token", {
3214
+ hint: "Run: bragduck auth atlassian",
3215
+ originalError: statusText
3216
+ });
3217
+ } else if (status === 403) {
3218
+ throw new JiraError("Forbidden - check token permissions", {
3219
+ hint: "Token needs: read access to issues",
3220
+ originalError: statusText
3221
+ });
3222
+ } else if (status === 404) {
3223
+ throw new JiraError("Resource not found", {
3224
+ originalError: statusText
3225
+ });
3226
+ } else if (status === 429) {
3227
+ throw new JiraError("Rate limit exceeded", {
3228
+ hint: "Wait a few minutes before trying again",
3229
+ originalError: statusText
3230
+ });
3231
+ }
3232
+ throw new JiraError(`API request failed: ${statusText}`, {
3233
+ originalError: statusText
3234
+ });
3235
+ }
3236
+ return response.json();
3237
+ }
3238
+ /**
3239
+ * Validate Jira instance and credentials
3240
+ */
3241
+ async validateJiraInstance() {
3242
+ try {
3243
+ await this.request("/rest/api/2/myself");
3244
+ } catch (error) {
3245
+ if (error instanceof JiraError) {
3246
+ throw error;
3247
+ }
3248
+ throw new JiraError("Could not access Jira instance via API", {
3249
+ hint: "Check that the instance URL is correct and your credentials are valid",
3250
+ originalError: error instanceof Error ? error.message : String(error)
3251
+ });
3252
+ }
3253
+ }
3254
+ /**
3255
+ * Get current user's email
3256
+ */
3257
+ async getCurrentUser() {
3258
+ try {
3259
+ const user = await this.request("/rest/api/2/myself");
3260
+ return user.emailAddress;
3261
+ } catch {
3262
+ return null;
3263
+ }
3264
+ }
3265
+ /**
3266
+ * Build JQL query from options
3267
+ */
3268
+ buildJQL(options) {
3269
+ const queries = [];
3270
+ queries.push("status IN (Done, Resolved, Closed)");
3271
+ if (options.days) {
3272
+ queries.push(`updated >= -${options.days}d`);
3273
+ }
3274
+ if (options.author) {
3275
+ queries.push(`creator = "${options.author}"`);
3276
+ }
3277
+ if (options.jql) {
3278
+ queries.push(`(${options.jql})`);
3279
+ }
3280
+ return queries.join(" AND ");
3281
+ }
3282
+ /**
3283
+ * Estimate issue complexity for impact scoring
3284
+ */
3285
+ estimateComplexity(issue) {
3286
+ const typeScores = {
3287
+ Epic: 500,
3288
+ Story: 200,
3289
+ Task: 100,
3290
+ Bug: 100,
3291
+ "Sub-task": 50
3292
+ };
3293
+ return typeScores[issue.fields.issuetype.name] || 100;
3294
+ }
3295
+ /**
3296
+ * Fetch issues with optional filtering
3297
+ */
3298
+ async getIssues(options = {}) {
3299
+ const jql = this.buildJQL(options);
3300
+ const fields = [
3301
+ "summary",
3302
+ "description",
3303
+ "created",
3304
+ "updated",
3305
+ "resolutiondate",
3306
+ "creator",
3307
+ "status",
3308
+ "issuetype"
3309
+ ];
3310
+ const allIssues = [];
3311
+ let startAt = 0;
3312
+ const maxResults = 100;
3313
+ while (true) {
3314
+ const endpoint = `/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}&fields=${fields.join(",")}`;
3315
+ const response = await this.request(endpoint);
3316
+ allIssues.push(...response.issues);
3317
+ logger.debug(
3318
+ `Fetched ${response.issues.length} issues (total: ${allIssues.length} of ${response.total})${startAt + maxResults < response.total ? ", fetching next page..." : ""}`
3319
+ );
3320
+ if (startAt + maxResults >= response.total) {
3321
+ break;
3322
+ }
3323
+ if (options.limit && allIssues.length >= options.limit) {
3324
+ return allIssues.slice(0, options.limit).map((issue) => this.transformIssueToCommit(issue));
3325
+ }
3326
+ startAt += maxResults;
3327
+ }
3328
+ return allIssues.map((issue) => this.transformIssueToCommit(issue));
3329
+ }
3330
+ /**
3331
+ * Fetch issues for the current authenticated user
3332
+ */
3333
+ async getIssuesByCurrentUser(options = {}) {
3334
+ const email = await this.getCurrentUser();
3335
+ if (!email) {
3336
+ throw new JiraError("Could not get current user");
3337
+ }
3338
+ return this.getIssues({
3339
+ ...options,
3340
+ author: email
3341
+ });
3342
+ }
3343
+ /**
3344
+ * Transform Jira issue to GitCommit format with external fields
3345
+ */
3346
+ transformIssueToCommit(issue, instanceUrl) {
3347
+ let message = issue.fields.summary;
3348
+ if (issue.fields.description) {
3349
+ const truncatedDesc = issue.fields.description.substring(0, this.MAX_DESCRIPTION_LENGTH);
3350
+ message = `${issue.fields.summary}
3351
+
3352
+ ${truncatedDesc}`;
3353
+ }
3354
+ const date = issue.fields.resolutiondate || issue.fields.updated;
3355
+ let baseUrl = "https://jira.atlassian.net";
3356
+ if (instanceUrl) {
3357
+ baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
3358
+ } else {
3359
+ try {
3360
+ const creds = storageService.getServiceCredentials("jira");
3361
+ if (creds?.instanceUrl) {
3362
+ baseUrl = creds.instanceUrl.startsWith("http") ? creds.instanceUrl : `https://${creds.instanceUrl}`;
3363
+ }
3364
+ } catch {
3365
+ }
3366
+ }
3367
+ const url = `${baseUrl}/browse/${issue.key}`;
3368
+ return {
3369
+ sha: issue.key,
3370
+ message,
3371
+ author: issue.fields.creator.displayName,
3372
+ authorEmail: issue.fields.creator.emailAddress,
3373
+ date,
3374
+ url,
3375
+ diffStats: {
3376
+ filesChanged: 0,
3377
+ insertions: this.estimateComplexity(issue),
3378
+ deletions: 0
3379
+ },
3380
+ externalId: issue.key,
3381
+ externalType: "issue",
3382
+ externalSource: "jira",
3383
+ externalUrl: url
3384
+ };
3385
+ }
3386
+ };
3387
+ var jiraService = new JiraService();
3388
+
3389
+ // src/sync/jira-adapter.ts
3390
+ var jiraSyncAdapter = {
3391
+ name: "jira",
3392
+ async validate() {
3393
+ await jiraService.validateJiraInstance();
3394
+ },
3395
+ async getRepositoryInfo() {
3396
+ const user = await jiraService.getCurrentUser();
3397
+ const creds = await jiraService.getCredentials();
3398
+ const userName = user || "Unknown User";
3399
+ const baseUrl = creds.instanceUrl.startsWith("http") ? creds.instanceUrl : `https://${creds.instanceUrl}`;
3400
+ return {
3401
+ owner: userName,
3402
+ name: "Jira Issues",
3403
+ fullName: `${userName}'s Jira Issues`,
3404
+ url: baseUrl
3405
+ };
3406
+ },
3407
+ async fetchWorkItems(options) {
3408
+ const author = options.author === "current" ? await this.getCurrentUser() : options.author;
3409
+ return jiraService.getIssues({
3410
+ days: options.days,
3411
+ limit: options.limit,
3412
+ author: author || void 0
3413
+ });
3414
+ },
3415
+ async isAuthenticated() {
3416
+ try {
3417
+ await this.validate();
3418
+ return true;
3419
+ } catch {
3420
+ return false;
3421
+ }
3422
+ },
3423
+ async getCurrentUser() {
3424
+ return jiraService.getCurrentUser();
3425
+ }
3426
+ };
3427
+
3428
+ // src/sync/confluence-adapter.ts
3429
+ init_esm_shims();
3430
+
3431
+ // src/services/confluence.service.ts
3432
+ init_esm_shims();
3433
+ init_errors();
3434
+ init_logger();
3435
+ init_storage_service();
3436
+ var ConfluenceService = class {
3437
+ /**
3438
+ * Get stored Confluence credentials
3439
+ */
3440
+ async getCredentials() {
3441
+ const creds = await storageService.getServiceCredentials("confluence");
3442
+ if (!creds || !creds.username || !creds.accessToken || !creds.instanceUrl) {
3443
+ throw new ConfluenceError("Not authenticated with Confluence", {
3444
+ hint: "Run: bragduck auth atlassian"
3445
+ });
3446
+ }
3447
+ if (creds.expiresAt && creds.expiresAt < Date.now()) {
3448
+ throw new ConfluenceError("API token has expired", {
3449
+ hint: "Run: bragduck auth atlassian"
3450
+ });
3451
+ }
3452
+ return {
3453
+ email: creds.username,
3454
+ apiToken: creds.accessToken,
3455
+ instanceUrl: creds.instanceUrl
3456
+ };
3457
+ }
3458
+ /**
3459
+ * Make authenticated request to Confluence API
3460
+ */
3461
+ async request(endpoint, method = "GET", body) {
3462
+ const { email, apiToken, instanceUrl } = await this.getCredentials();
3463
+ const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
3464
+ const baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
3465
+ logger.debug(`Confluence API: ${method} ${endpoint}`);
3466
+ const options = {
3467
+ method,
3468
+ headers: {
3469
+ Authorization: `Basic ${auth}`,
3470
+ "Content-Type": "application/json",
3471
+ Accept: "application/json"
3472
+ }
3473
+ };
3474
+ if (body) {
3475
+ options.body = JSON.stringify(body);
3476
+ }
3477
+ const response = await fetch(`${baseUrl}${endpoint}`, options);
3478
+ if (!response.ok) {
3479
+ const statusText = response.statusText;
3480
+ const status = response.status;
3481
+ if (status === 401) {
3482
+ throw new ConfluenceError("Invalid or expired API token", {
3483
+ hint: "Run: bragduck auth atlassian",
3484
+ originalError: statusText
3485
+ });
3486
+ } else if (status === 403) {
3487
+ throw new ConfluenceError("Forbidden - check token permissions", {
3488
+ hint: "Token needs: read access to pages",
3489
+ originalError: statusText
3490
+ });
3491
+ } else if (status === 404) {
3492
+ throw new ConfluenceError("Resource not found", {
3493
+ originalError: statusText
3494
+ });
3495
+ } else if (status === 429) {
3496
+ throw new ConfluenceError("Rate limit exceeded", {
3497
+ hint: "Wait a few minutes before trying again",
3498
+ originalError: statusText
3499
+ });
3500
+ }
3501
+ throw new ConfluenceError(`API request failed: ${statusText}`, {
3502
+ originalError: statusText
3503
+ });
3504
+ }
3505
+ return response.json();
3506
+ }
3507
+ /**
3508
+ * Validate Confluence instance and credentials
3509
+ */
3510
+ async validateConfluenceInstance() {
3511
+ try {
3512
+ await this.request("/wiki/rest/api/content?type=page&limit=1");
3513
+ } catch (error) {
3514
+ if (error instanceof ConfluenceError) {
3515
+ throw error;
3516
+ }
3517
+ throw new ConfluenceError("Could not access Confluence instance via API", {
3518
+ hint: "Check that the instance URL is correct and your credentials are valid",
3519
+ originalError: error instanceof Error ? error.message : String(error)
3520
+ });
3521
+ }
3522
+ }
3523
+ /**
3524
+ * Get current user's email
3525
+ */
3526
+ async getCurrentUser() {
3527
+ try {
3528
+ const creds = await this.getCredentials();
3529
+ return creds.email;
3530
+ } catch {
3531
+ return null;
3532
+ }
3533
+ }
3534
+ /**
3535
+ * Build CQL query from options
3536
+ */
3537
+ buildCQL(options) {
3538
+ const queries = [];
3539
+ queries.push("type=page");
3540
+ if (options.days) {
3541
+ queries.push(`lastModified >= now("-${options.days}d")`);
3542
+ }
3543
+ if (options.author) {
3544
+ queries.push(`creator = "${options.author}"`);
3545
+ }
3546
+ if (options.cql) {
3547
+ queries.push(`(${options.cql})`);
3548
+ }
3549
+ return queries.join(" AND ");
3550
+ }
3551
+ /**
3552
+ * Estimate page size for impact scoring
3553
+ */
3554
+ estimatePageSize(page) {
3555
+ const content = page.body?.storage?.value || "";
3556
+ return Math.ceil(content.length / 80);
3557
+ }
3558
+ /**
3559
+ * Fetch pages with optional filtering
3560
+ */
3561
+ async getPages(options = {}) {
3562
+ const cql = this.buildCQL(options);
3563
+ const allPages = [];
3564
+ let start = 0;
3565
+ const limit = 100;
3566
+ while (true) {
3567
+ const params = {
3568
+ type: "page",
3569
+ status: "current",
3570
+ start: start.toString(),
3571
+ limit: limit.toString(),
3572
+ expand: "version,body.storage"
3573
+ };
3574
+ if (cql) {
3575
+ params.cql = cql;
3576
+ }
3577
+ const queryString = Object.entries(params).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
3578
+ const endpoint = `/wiki/rest/api/content?${queryString}`;
3579
+ const response = await this.request(endpoint);
3580
+ allPages.push(...response.results);
3581
+ logger.debug(
3582
+ `Fetched ${response.results.length} pages (total: ${allPages.length})${response.results.length === limit ? ", fetching next page..." : ""}`
3583
+ );
3584
+ if (response.results.length < limit) {
3585
+ break;
3586
+ }
3587
+ if (options.limit && allPages.length >= options.limit) {
3588
+ return allPages.slice(0, options.limit).map((page) => this.transformPageToCommit(page));
3589
+ }
3590
+ start += limit;
3591
+ }
3592
+ return allPages.map((page) => this.transformPageToCommit(page));
3593
+ }
3594
+ /**
3595
+ * Fetch pages for the current authenticated user
3596
+ */
3597
+ async getPagesByCurrentUser(options = {}) {
3598
+ const email = await this.getCurrentUser();
3599
+ if (!email) {
3600
+ throw new ConfluenceError("Could not get current user");
3601
+ }
3602
+ return this.getPages({
3603
+ ...options,
3604
+ author: email
3605
+ });
3606
+ }
3607
+ /**
3608
+ * Transform Confluence page to GitCommit format with external fields
3609
+ */
3610
+ transformPageToCommit(page, instanceUrl) {
3611
+ const message = `${page.title}
3612
+
3613
+ [Confluence Page v${page.version.number}]`;
3614
+ let baseUrl = "https://confluence.atlassian.net";
3615
+ if (instanceUrl) {
3616
+ baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
3617
+ } else {
3618
+ try {
3619
+ const creds = storageService.getServiceCredentials("confluence");
3620
+ if (creds?.instanceUrl) {
3621
+ baseUrl = creds.instanceUrl.startsWith("http") ? creds.instanceUrl : `https://${creds.instanceUrl}`;
3622
+ }
3623
+ } catch {
3624
+ }
3625
+ }
3626
+ const url = `${baseUrl}/wiki${page._links.webui}`;
3627
+ return {
3628
+ sha: page.id,
3629
+ message,
3630
+ author: page.version.by.displayName,
3631
+ authorEmail: page.version.by.email,
3632
+ date: page.version.when,
3633
+ url,
3634
+ diffStats: {
3635
+ filesChanged: 1,
3636
+ insertions: this.estimatePageSize(page),
3637
+ deletions: 0
3638
+ },
3639
+ externalId: page.id,
3640
+ externalType: "page",
3641
+ externalSource: "confluence",
3642
+ externalUrl: url
3643
+ };
3644
+ }
3645
+ };
3646
+ var confluenceService = new ConfluenceService();
3647
+
3648
+ // src/sync/confluence-adapter.ts
3649
+ var confluenceSyncAdapter = {
3650
+ name: "confluence",
3651
+ async validate() {
3652
+ await confluenceService.validateConfluenceInstance();
3653
+ },
3654
+ async getRepositoryInfo() {
3655
+ const user = await confluenceService.getCurrentUser();
3656
+ const creds = await confluenceService.getCredentials();
3657
+ const userName = user || "Unknown User";
3658
+ const baseUrl = creds.instanceUrl.startsWith("http") ? creds.instanceUrl : `https://${creds.instanceUrl}`;
3659
+ return {
3660
+ owner: userName,
3661
+ name: "Confluence Pages",
3662
+ fullName: `${userName}'s Confluence Pages`,
3663
+ url: `${baseUrl}/wiki`
3664
+ };
3665
+ },
3666
+ async fetchWorkItems(options) {
3667
+ const author = options.author === "current" ? await this.getCurrentUser() : options.author;
3668
+ return confluenceService.getPages({
3669
+ days: options.days,
3670
+ limit: options.limit,
3671
+ author: author || void 0
3672
+ });
3673
+ },
3674
+ async isAuthenticated() {
3675
+ try {
3676
+ await this.validate();
3677
+ return true;
3678
+ } catch {
3679
+ return false;
3680
+ }
3681
+ },
3682
+ async getCurrentUser() {
3683
+ return confluenceService.getCurrentUser();
3684
+ }
3685
+ };
3686
+
3059
3687
  // src/sync/adapter-factory.ts
3060
3688
  var AdapterFactory = class {
3061
3689
  /**
@@ -3071,6 +3699,10 @@ var AdapterFactory = class {
3071
3699
  // Bitbucket Cloud and Server use same adapter
3072
3700
  case "gitlab":
3073
3701
  return gitlabSyncAdapter;
3702
+ case "jira":
3703
+ return jiraSyncAdapter;
3704
+ case "confluence":
3705
+ return confluenceSyncAdapter;
3074
3706
  default:
3075
3707
  throw new Error(`Unknown source type: ${source}`);
3076
3708
  }
@@ -3079,7 +3711,7 @@ var AdapterFactory = class {
3079
3711
  * Check if adapter is available for source
3080
3712
  */
3081
3713
  static isSupported(source) {
3082
- return source === "github" || source === "bitbucket" || source === "atlassian" || source === "gitlab";
3714
+ return source === "github" || source === "bitbucket" || source === "atlassian" || source === "gitlab" || source === "jira" || source === "confluence";
3083
3715
  }
3084
3716
  };
3085
3717
 
@@ -3644,25 +4276,46 @@ async function syncCommand(options = {}) {
3644
4276
  logger.debug(`Subscription tier "${subscriptionStatus.tier}" - proceeding with sync`);
3645
4277
  const detectionSpinner = createStepSpinner(1, TOTAL_STEPS, "Detecting repository source");
3646
4278
  detectionSpinner.start();
3647
- const detectionResult = await sourceDetector.detectSources();
3648
- if (detectionResult.detected.length === 0) {
3649
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "No supported sources detected");
3650
- logger.log("");
3651
- logger.info("Make sure you are in a git repository with a remote URL");
3652
- return;
4279
+ let sourceType = options.source;
4280
+ if (!sourceType) {
4281
+ try {
4282
+ const detectionResult = await sourceDetector.detectSources();
4283
+ sourceType = detectionResult.recommended;
4284
+ if (!sourceType && detectionResult.detected.length === 0) {
4285
+ failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "No supported sources detected");
4286
+ logger.log("");
4287
+ logger.info("Make sure you are in a git repository with a remote URL");
4288
+ logger.info("Or use --source flag for non-git sources: --source jira|confluence");
4289
+ return;
4290
+ }
4291
+ } catch {
4292
+ failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Not a git repository");
4293
+ logger.log("");
4294
+ logger.info("Use --source flag to specify source: --source jira|confluence");
4295
+ return;
4296
+ }
3653
4297
  }
3654
- const sourceType = options.source || detectionResult.recommended;
3655
4298
  if (!sourceType) {
3656
4299
  failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Could not determine source");
4300
+ logger.log("");
4301
+ logger.info("Use --source flag: --source github|gitlab|bitbucket|jira|confluence");
3657
4302
  return;
3658
4303
  }
3659
4304
  if (!AdapterFactory.isSupported(sourceType)) {
3660
4305
  failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `Source ${sourceType} not yet supported`);
3661
4306
  logger.log("");
3662
- logger.info(`Currently supported: GitHub`);
3663
- logger.info(`Coming soon: GitLab, Atlassian, Bitbucket`);
4307
+ logger.info(`Currently supported: GitHub, GitLab, Bitbucket, Jira, Confluence`);
3664
4308
  return;
3665
4309
  }
4310
+ if (sourceType === "jira" || sourceType === "confluence") {
4311
+ const creds = await storageService.getServiceCredentials(sourceType);
4312
+ if (!creds || !creds.instanceUrl) {
4313
+ failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `No ${sourceType} instance configured`);
4314
+ logger.log("");
4315
+ logger.info(`Run: ${theme.command("bragduck auth atlassian")}`);
4316
+ return;
4317
+ }
4318
+ }
3666
4319
  succeedStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `Source: ${theme.value(sourceType)}`);
3667
4320
  logger.log("");
3668
4321
  const adapter = AdapterFactory.getAdapter(sourceType);
@@ -3824,7 +4477,12 @@ async function syncCommand(options = {}) {
3824
4477
  impact_score: refined.suggested_impactLevel,
3825
4478
  impact_description: refined.impact_description,
3826
4479
  attachments: originalCommit?.url ? [originalCommit.url] : [],
3827
- orgId: selectedOrgId || void 0
4480
+ orgId: selectedOrgId || void 0,
4481
+ // External fields for non-git sources (Jira, Confluence, etc.)
4482
+ externalId: originalCommit?.externalId,
4483
+ externalType: originalCommit?.externalType,
4484
+ externalSource: originalCommit?.externalSource,
4485
+ externalUrl: originalCommit?.externalUrl
3828
4486
  };
3829
4487
  })
3830
4488
  };