@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.
- package/dist/bin/bragduck.js +671 -13
- package/dist/bin/bragduck.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/bragduck.js
CHANGED
|
@@ -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
|
-
|
|
3648
|
-
if (
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
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
|
};
|