@bragduck/cli 2.6.0 → 2.8.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 +1175 -135
- package/dist/bin/bragduck.js.map +1 -1
- package/dist/index.js +275 -58
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/bragduck.js
CHANGED
|
@@ -22,7 +22,7 @@ var init_esm_shims = __esm({
|
|
|
22
22
|
import { config } from "dotenv";
|
|
23
23
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
24
24
|
import { dirname, join } from "path";
|
|
25
|
-
var __filename, __dirname, APP_NAME, CONFIG_KEYS, DEFAULT_CONFIG, OAUTH_CONFIG, API_ENDPOINTS, ENCRYPTION_CONFIG, STORAGE_PATHS, HTTP_STATUS;
|
|
25
|
+
var __filename, __dirname, APP_NAME, CONFIG_KEYS, DEFAULT_CONFIG, OAUTH_CONFIG, API_ENDPOINTS, ENCRYPTION_CONFIG, STORAGE_PATHS, HTTP_STATUS, CONFIG_FILES;
|
|
26
26
|
var init_constants = __esm({
|
|
27
27
|
"src/constants.ts"() {
|
|
28
28
|
"use strict";
|
|
@@ -33,11 +33,21 @@ var init_constants = __esm({
|
|
|
33
33
|
APP_NAME = "bragduck";
|
|
34
34
|
CONFIG_KEYS = {
|
|
35
35
|
DEFAULT_COMMIT_DAYS: "defaultCommitDays",
|
|
36
|
-
AUTO_VERSION_CHECK: "autoVersionCheck"
|
|
36
|
+
AUTO_VERSION_CHECK: "autoVersionCheck",
|
|
37
|
+
DEFAULT_SOURCE: "defaultSource",
|
|
38
|
+
SOURCE_PRIORITY: "sourcePriority",
|
|
39
|
+
JIRA_INSTANCE: "jiraInstance",
|
|
40
|
+
CONFLUENCE_INSTANCE: "confluenceInstance",
|
|
41
|
+
GITLAB_INSTANCE: "gitlabInstance"
|
|
37
42
|
};
|
|
38
43
|
DEFAULT_CONFIG = {
|
|
39
44
|
defaultCommitDays: 30,
|
|
40
|
-
autoVersionCheck: true
|
|
45
|
+
autoVersionCheck: true,
|
|
46
|
+
defaultSource: void 0,
|
|
47
|
+
sourcePriority: void 0,
|
|
48
|
+
jiraInstance: void 0,
|
|
49
|
+
confluenceInstance: void 0,
|
|
50
|
+
gitlabInstance: void 0
|
|
41
51
|
};
|
|
42
52
|
OAUTH_CONFIG = {
|
|
43
53
|
CLIENT_ID: "bragduck-cli",
|
|
@@ -92,6 +102,258 @@ var init_constants = __esm({
|
|
|
92
102
|
NOT_FOUND: 404,
|
|
93
103
|
INTERNAL_SERVER_ERROR: 500
|
|
94
104
|
};
|
|
105
|
+
CONFIG_FILES = [".bragduckrc", ".bragduck/config.json", ".bragduckrc.json"];
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// src/utils/env-loader.ts
|
|
110
|
+
function parseEnvNumber(key) {
|
|
111
|
+
const value = process.env[key];
|
|
112
|
+
if (!value) return void 0;
|
|
113
|
+
const parsed = parseInt(value, 10);
|
|
114
|
+
return isNaN(parsed) ? void 0 : parsed;
|
|
115
|
+
}
|
|
116
|
+
function parseEnvBoolean(key) {
|
|
117
|
+
const value = process.env[key]?.toLowerCase();
|
|
118
|
+
if (!value) return void 0;
|
|
119
|
+
if (["true", "yes", "1"].includes(value)) return true;
|
|
120
|
+
if (["false", "no", "0"].includes(value)) return false;
|
|
121
|
+
return void 0;
|
|
122
|
+
}
|
|
123
|
+
function parseEnvArray(key) {
|
|
124
|
+
const value = process.env[key];
|
|
125
|
+
if (!value) return void 0;
|
|
126
|
+
const items = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
127
|
+
return items.length > 0 ? items : void 0;
|
|
128
|
+
}
|
|
129
|
+
function parseEnvSource(key) {
|
|
130
|
+
const value = process.env[key];
|
|
131
|
+
if (!value) return void 0;
|
|
132
|
+
const validSources = ["github", "gitlab", "bitbucket", "jira", "confluence"];
|
|
133
|
+
return validSources.includes(value) ? value : void 0;
|
|
134
|
+
}
|
|
135
|
+
function loadEnvConfig() {
|
|
136
|
+
return {
|
|
137
|
+
// Source configuration
|
|
138
|
+
source: parseEnvSource("BRAGDUCK_SOURCE"),
|
|
139
|
+
jiraInstance: process.env.BRAGDUCK_JIRA_INSTANCE,
|
|
140
|
+
confluenceInstance: process.env.BRAGDUCK_CONFLUENCE_INSTANCE,
|
|
141
|
+
gitlabInstance: process.env.BRAGDUCK_GITLAB_INSTANCE,
|
|
142
|
+
// Behavior configuration
|
|
143
|
+
defaultCommitDays: parseEnvNumber("BRAGDUCK_DEFAULT_COMMIT_DAYS"),
|
|
144
|
+
autoVersionCheck: parseEnvBoolean("BRAGDUCK_AUTO_VERSION_CHECK"),
|
|
145
|
+
sourcePriority: parseEnvArray("BRAGDUCK_SOURCE_PRIORITY"),
|
|
146
|
+
// Credential overrides (for CI/CD)
|
|
147
|
+
jiraToken: process.env.BRAGDUCK_JIRA_TOKEN,
|
|
148
|
+
confluenceToken: process.env.BRAGDUCK_CONFLUENCE_TOKEN,
|
|
149
|
+
gitlabToken: process.env.BRAGDUCK_GITLAB_TOKEN
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
var init_env_loader = __esm({
|
|
153
|
+
"src/utils/env-loader.ts"() {
|
|
154
|
+
"use strict";
|
|
155
|
+
init_esm_shims();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// src/utils/errors.ts
|
|
160
|
+
var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError, JiraError, ConfluenceError;
|
|
161
|
+
var init_errors = __esm({
|
|
162
|
+
"src/utils/errors.ts"() {
|
|
163
|
+
"use strict";
|
|
164
|
+
init_esm_shims();
|
|
165
|
+
BragduckError = class extends Error {
|
|
166
|
+
code;
|
|
167
|
+
details;
|
|
168
|
+
constructor(message, code, details) {
|
|
169
|
+
super(message);
|
|
170
|
+
this.name = "BragduckError";
|
|
171
|
+
this.code = code;
|
|
172
|
+
this.details = details;
|
|
173
|
+
Error.captureStackTrace(this, this.constructor);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
AuthenticationError = class extends BragduckError {
|
|
177
|
+
constructor(message, details) {
|
|
178
|
+
super(message, "AUTH_ERROR", details);
|
|
179
|
+
this.name = "AuthenticationError";
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
GitError = class extends BragduckError {
|
|
183
|
+
constructor(message, details) {
|
|
184
|
+
super(message, "GIT_ERROR", details);
|
|
185
|
+
this.name = "GitError";
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
ApiError = class extends BragduckError {
|
|
189
|
+
statusCode;
|
|
190
|
+
constructor(message, statusCode, details) {
|
|
191
|
+
super(message, "API_ERROR", details);
|
|
192
|
+
this.name = "ApiError";
|
|
193
|
+
this.statusCode = statusCode;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
NetworkError = class extends BragduckError {
|
|
197
|
+
constructor(message, details) {
|
|
198
|
+
super(message, "NETWORK_ERROR", details);
|
|
199
|
+
this.name = "NetworkError";
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
ValidationError = class extends BragduckError {
|
|
203
|
+
constructor(message, details) {
|
|
204
|
+
super(message, "VALIDATION_ERROR", details);
|
|
205
|
+
this.name = "ValidationError";
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
OAuthError = class extends AuthenticationError {
|
|
209
|
+
constructor(message, details) {
|
|
210
|
+
super(message, details);
|
|
211
|
+
this.name = "OAuthError";
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
TokenExpiredError = class extends AuthenticationError {
|
|
215
|
+
constructor(message = "Authentication token has expired") {
|
|
216
|
+
super(message);
|
|
217
|
+
this.name = "TokenExpiredError";
|
|
218
|
+
this.code = "TOKEN_EXPIRED";
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
GitHubError = class extends BragduckError {
|
|
222
|
+
constructor(message, details) {
|
|
223
|
+
super(message, "GITHUB_ERROR", details);
|
|
224
|
+
this.name = "GitHubError";
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
BitbucketError = class extends BragduckError {
|
|
228
|
+
constructor(message, details) {
|
|
229
|
+
super(message, "BITBUCKET_ERROR", details);
|
|
230
|
+
this.name = "BitbucketError";
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
GitLabError = class extends BragduckError {
|
|
234
|
+
constructor(message, details) {
|
|
235
|
+
super(message, "GITLAB_ERROR", details);
|
|
236
|
+
this.name = "GitLabError";
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
JiraError = class extends BragduckError {
|
|
240
|
+
constructor(message, details) {
|
|
241
|
+
super(message, "JIRA_ERROR", details);
|
|
242
|
+
this.name = "JiraError";
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
ConfluenceError = class extends BragduckError {
|
|
246
|
+
constructor(message, details) {
|
|
247
|
+
super(message, "CONFLUENCE_ERROR", details);
|
|
248
|
+
this.name = "ConfluenceError";
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// src/utils/config-loader.ts
|
|
255
|
+
import { promises as fs } from "fs";
|
|
256
|
+
import path2 from "path";
|
|
257
|
+
async function findProjectConfig() {
|
|
258
|
+
const cwd = process.cwd();
|
|
259
|
+
if (configCache.has(cwd)) {
|
|
260
|
+
return configCache.get(cwd) || null;
|
|
261
|
+
}
|
|
262
|
+
for (const filename of CONFIG_FILES) {
|
|
263
|
+
const configPath = path2.join(cwd, filename);
|
|
264
|
+
try {
|
|
265
|
+
await fs.access(configPath);
|
|
266
|
+
const config2 = await loadAndValidateConfig(configPath);
|
|
267
|
+
configCache.set(cwd, config2);
|
|
268
|
+
return config2;
|
|
269
|
+
} catch {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
configCache.set(cwd, null);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
async function loadAndValidateConfig(configPath) {
|
|
277
|
+
try {
|
|
278
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
279
|
+
const config2 = JSON.parse(content);
|
|
280
|
+
validateProjectConfig(config2);
|
|
281
|
+
return config2;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (error instanceof SyntaxError) {
|
|
284
|
+
throw new ValidationError(`Invalid JSON in config file: ${configPath}`, {
|
|
285
|
+
originalError: error.message
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function validateProjectConfig(config2) {
|
|
292
|
+
if (typeof config2 !== "object" || config2 === null) {
|
|
293
|
+
throw new ValidationError("Config must be a JSON object");
|
|
294
|
+
}
|
|
295
|
+
const cfg = config2;
|
|
296
|
+
if (cfg.defaultCommitDays !== void 0) {
|
|
297
|
+
if (typeof cfg.defaultCommitDays !== "number") {
|
|
298
|
+
throw new ValidationError("defaultCommitDays must be a number");
|
|
299
|
+
}
|
|
300
|
+
if (cfg.defaultCommitDays < 1 || cfg.defaultCommitDays > 365) {
|
|
301
|
+
throw new ValidationError("defaultCommitDays must be between 1 and 365");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (cfg.autoVersionCheck !== void 0) {
|
|
305
|
+
if (typeof cfg.autoVersionCheck !== "boolean") {
|
|
306
|
+
throw new ValidationError("autoVersionCheck must be a boolean");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (cfg.defaultSource !== void 0) {
|
|
310
|
+
const validSources = ["github", "gitlab", "bitbucket", "jira", "confluence"];
|
|
311
|
+
if (!validSources.includes(cfg.defaultSource)) {
|
|
312
|
+
throw new ValidationError(
|
|
313
|
+
`Invalid defaultSource: ${cfg.defaultSource}. Must be one of: ${validSources.join(", ")}`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (cfg.sourcePriority !== void 0) {
|
|
318
|
+
if (!Array.isArray(cfg.sourcePriority)) {
|
|
319
|
+
throw new ValidationError("sourcePriority must be an array");
|
|
320
|
+
}
|
|
321
|
+
const validSources = ["github", "gitlab", "bitbucket", "jira", "confluence"];
|
|
322
|
+
for (const source of cfg.sourcePriority) {
|
|
323
|
+
if (!validSources.includes(source)) {
|
|
324
|
+
throw new ValidationError(
|
|
325
|
+
`Invalid source in sourcePriority: ${source}. Must be one of: ${validSources.join(", ")}`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const instanceFields = ["jiraInstance", "confluenceInstance", "gitlabInstance"];
|
|
331
|
+
for (const field of instanceFields) {
|
|
332
|
+
if (cfg[field] !== void 0) {
|
|
333
|
+
if (typeof cfg[field] !== "string") {
|
|
334
|
+
throw new ValidationError(`${field} must be a string`);
|
|
335
|
+
}
|
|
336
|
+
const value = cfg[field];
|
|
337
|
+
if (!value.match(/^[a-zA-Z0-9.-]+(:[0-9]+)?(\/.*)?$/)) {
|
|
338
|
+
throw new ValidationError(`Invalid ${field}: ${value}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const booleanFields = ["requireAuthentication", "skipSourceDetection"];
|
|
343
|
+
for (const field of booleanFields) {
|
|
344
|
+
if (cfg[field] !== void 0 && typeof cfg[field] !== "boolean") {
|
|
345
|
+
throw new ValidationError(`${field} must be a boolean`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
var configCache;
|
|
350
|
+
var init_config_loader = __esm({
|
|
351
|
+
"src/utils/config-loader.ts"() {
|
|
352
|
+
"use strict";
|
|
353
|
+
init_esm_shims();
|
|
354
|
+
init_constants();
|
|
355
|
+
init_errors();
|
|
356
|
+
configCache = /* @__PURE__ */ new Map();
|
|
95
357
|
}
|
|
96
358
|
});
|
|
97
359
|
|
|
@@ -107,6 +369,8 @@ var init_storage_service = __esm({
|
|
|
107
369
|
"use strict";
|
|
108
370
|
init_esm_shims();
|
|
109
371
|
init_constants();
|
|
372
|
+
init_env_loader();
|
|
373
|
+
init_config_loader();
|
|
110
374
|
StorageService = class {
|
|
111
375
|
config;
|
|
112
376
|
storageBackend;
|
|
@@ -345,7 +609,27 @@ var init_storage_service = __esm({
|
|
|
345
609
|
this.config.delete("oauthState");
|
|
346
610
|
}
|
|
347
611
|
/**
|
|
348
|
-
* Get configuration value
|
|
612
|
+
* Get configuration value with hierarchy resolution
|
|
613
|
+
* Priority: env vars > project config > user config > defaults
|
|
614
|
+
*/
|
|
615
|
+
async getConfigWithHierarchy(key) {
|
|
616
|
+
const envConfig = loadEnvConfig();
|
|
617
|
+
const envKey = key === "defaultSource" ? "source" : key;
|
|
618
|
+
if (envConfig[envKey] !== void 0) {
|
|
619
|
+
return envConfig[envKey];
|
|
620
|
+
}
|
|
621
|
+
const projectConfig = await findProjectConfig();
|
|
622
|
+
if (projectConfig && projectConfig[key] !== void 0) {
|
|
623
|
+
return projectConfig[key];
|
|
624
|
+
}
|
|
625
|
+
const userValue = this.config.get(key);
|
|
626
|
+
if (userValue !== void 0) {
|
|
627
|
+
return userValue;
|
|
628
|
+
}
|
|
629
|
+
return DEFAULT_CONFIG[key];
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Get configuration value (uses hierarchy resolution)
|
|
349
633
|
*/
|
|
350
634
|
getConfig(key) {
|
|
351
635
|
return this.config.get(key);
|
|
@@ -357,7 +641,32 @@ var init_storage_service = __esm({
|
|
|
357
641
|
this.config.set(key, value);
|
|
358
642
|
}
|
|
359
643
|
/**
|
|
360
|
-
* Get all configuration
|
|
644
|
+
* Get all configuration (merges all layers)
|
|
645
|
+
* Priority: env vars > project config > user config > defaults
|
|
646
|
+
*/
|
|
647
|
+
async getAllConfigWithHierarchy() {
|
|
648
|
+
const envConfig = loadEnvConfig();
|
|
649
|
+
const projectConfig = await findProjectConfig();
|
|
650
|
+
const userConfig = this.config.store;
|
|
651
|
+
return {
|
|
652
|
+
...DEFAULT_CONFIG,
|
|
653
|
+
...userConfig,
|
|
654
|
+
...projectConfig,
|
|
655
|
+
...envConfig.source && { defaultSource: envConfig.source },
|
|
656
|
+
...envConfig.jiraInstance && { jiraInstance: envConfig.jiraInstance },
|
|
657
|
+
...envConfig.confluenceInstance && { confluenceInstance: envConfig.confluenceInstance },
|
|
658
|
+
...envConfig.gitlabInstance && { gitlabInstance: envConfig.gitlabInstance },
|
|
659
|
+
...envConfig.defaultCommitDays !== void 0 && {
|
|
660
|
+
defaultCommitDays: envConfig.defaultCommitDays
|
|
661
|
+
},
|
|
662
|
+
...envConfig.autoVersionCheck !== void 0 && {
|
|
663
|
+
autoVersionCheck: envConfig.autoVersionCheck
|
|
664
|
+
},
|
|
665
|
+
...envConfig.sourcePriority && { sourcePriority: envConfig.sourcePriority }
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Get all configuration (user config only)
|
|
361
670
|
*/
|
|
362
671
|
getAllConfig() {
|
|
363
672
|
return this.config.store;
|
|
@@ -368,7 +677,9 @@ var init_storage_service = __esm({
|
|
|
368
677
|
resetConfig() {
|
|
369
678
|
this.config.clear();
|
|
370
679
|
Object.entries(DEFAULT_CONFIG).forEach(([key, value]) => {
|
|
371
|
-
|
|
680
|
+
if (value !== void 0) {
|
|
681
|
+
this.config.set(key, value);
|
|
682
|
+
}
|
|
372
683
|
});
|
|
373
684
|
}
|
|
374
685
|
/**
|
|
@@ -391,89 +702,6 @@ var init_storage_service = __esm({
|
|
|
391
702
|
}
|
|
392
703
|
});
|
|
393
704
|
|
|
394
|
-
// src/utils/errors.ts
|
|
395
|
-
var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError;
|
|
396
|
-
var init_errors = __esm({
|
|
397
|
-
"src/utils/errors.ts"() {
|
|
398
|
-
"use strict";
|
|
399
|
-
init_esm_shims();
|
|
400
|
-
BragduckError = class extends Error {
|
|
401
|
-
code;
|
|
402
|
-
details;
|
|
403
|
-
constructor(message, code, details) {
|
|
404
|
-
super(message);
|
|
405
|
-
this.name = "BragduckError";
|
|
406
|
-
this.code = code;
|
|
407
|
-
this.details = details;
|
|
408
|
-
Error.captureStackTrace(this, this.constructor);
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
AuthenticationError = class extends BragduckError {
|
|
412
|
-
constructor(message, details) {
|
|
413
|
-
super(message, "AUTH_ERROR", details);
|
|
414
|
-
this.name = "AuthenticationError";
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
GitError = class extends BragduckError {
|
|
418
|
-
constructor(message, details) {
|
|
419
|
-
super(message, "GIT_ERROR", details);
|
|
420
|
-
this.name = "GitError";
|
|
421
|
-
}
|
|
422
|
-
};
|
|
423
|
-
ApiError = class extends BragduckError {
|
|
424
|
-
statusCode;
|
|
425
|
-
constructor(message, statusCode, details) {
|
|
426
|
-
super(message, "API_ERROR", details);
|
|
427
|
-
this.name = "ApiError";
|
|
428
|
-
this.statusCode = statusCode;
|
|
429
|
-
}
|
|
430
|
-
};
|
|
431
|
-
NetworkError = class extends BragduckError {
|
|
432
|
-
constructor(message, details) {
|
|
433
|
-
super(message, "NETWORK_ERROR", details);
|
|
434
|
-
this.name = "NetworkError";
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
ValidationError = class extends BragduckError {
|
|
438
|
-
constructor(message, details) {
|
|
439
|
-
super(message, "VALIDATION_ERROR", details);
|
|
440
|
-
this.name = "ValidationError";
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
OAuthError = class extends AuthenticationError {
|
|
444
|
-
constructor(message, details) {
|
|
445
|
-
super(message, details);
|
|
446
|
-
this.name = "OAuthError";
|
|
447
|
-
}
|
|
448
|
-
};
|
|
449
|
-
TokenExpiredError = class extends AuthenticationError {
|
|
450
|
-
constructor(message = "Authentication token has expired") {
|
|
451
|
-
super(message);
|
|
452
|
-
this.name = "TokenExpiredError";
|
|
453
|
-
this.code = "TOKEN_EXPIRED";
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
GitHubError = class extends BragduckError {
|
|
457
|
-
constructor(message, details) {
|
|
458
|
-
super(message, "GITHUB_ERROR", details);
|
|
459
|
-
this.name = "GitHubError";
|
|
460
|
-
}
|
|
461
|
-
};
|
|
462
|
-
BitbucketError = class extends BragduckError {
|
|
463
|
-
constructor(message, details) {
|
|
464
|
-
super(message, "BITBUCKET_ERROR", details);
|
|
465
|
-
this.name = "BitbucketError";
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
GitLabError = class extends BragduckError {
|
|
469
|
-
constructor(message, details) {
|
|
470
|
-
super(message, "GITLAB_ERROR", details);
|
|
471
|
-
this.name = "GitLabError";
|
|
472
|
-
}
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
|
|
477
705
|
// src/utils/logger.ts
|
|
478
706
|
import chalk from "chalk";
|
|
479
707
|
var logger;
|
|
@@ -1621,9 +1849,11 @@ async function authCommand(subcommand) {
|
|
|
1621
1849
|
await authBitbucket();
|
|
1622
1850
|
} else if (subcommand === "gitlab") {
|
|
1623
1851
|
await authGitLab();
|
|
1852
|
+
} else if (subcommand === "atlassian") {
|
|
1853
|
+
await authAtlassian();
|
|
1624
1854
|
} else {
|
|
1625
1855
|
logger.error(`Unknown auth subcommand: ${subcommand}`);
|
|
1626
|
-
logger.info("Available subcommands: login, status, bitbucket, gitlab");
|
|
1856
|
+
logger.info("Available subcommands: login, status, bitbucket, gitlab, atlassian");
|
|
1627
1857
|
process.exit(1);
|
|
1628
1858
|
}
|
|
1629
1859
|
}
|
|
@@ -1858,35 +2088,117 @@ User: ${user.name} (@${user.username})`,
|
|
|
1858
2088
|
process.exit(1);
|
|
1859
2089
|
}
|
|
1860
2090
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
2091
|
+
async function authAtlassian() {
|
|
2092
|
+
logger.log("");
|
|
2093
|
+
logger.log(
|
|
2094
|
+
boxen(
|
|
2095
|
+
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",
|
|
2096
|
+
boxStyles.info
|
|
2097
|
+
)
|
|
2098
|
+
);
|
|
2099
|
+
logger.log("");
|
|
2100
|
+
try {
|
|
2101
|
+
const instanceUrl = await input({
|
|
2102
|
+
message: "Atlassian instance URL (e.g., company.atlassian.net):",
|
|
2103
|
+
validate: (value) => value.length > 0 ? true : "Instance URL cannot be empty"
|
|
2104
|
+
});
|
|
2105
|
+
const email = await input({
|
|
2106
|
+
message: "Atlassian account email:",
|
|
2107
|
+
validate: (value) => value.includes("@") ? true : "Please enter a valid email address"
|
|
2108
|
+
});
|
|
2109
|
+
const apiToken = await input({
|
|
2110
|
+
message: "API Token:",
|
|
2111
|
+
validate: (value) => value.length > 0 ? true : "API token cannot be empty"
|
|
2112
|
+
});
|
|
2113
|
+
const testInstanceUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
|
|
2114
|
+
const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
2115
|
+
const response = await fetch(`${testInstanceUrl}/rest/api/2/myself`, {
|
|
2116
|
+
headers: { Authorization: `Basic ${auth}` }
|
|
2117
|
+
});
|
|
2118
|
+
if (!response.ok) {
|
|
2119
|
+
logger.log("");
|
|
2120
|
+
logger.log(
|
|
2121
|
+
boxen(
|
|
2122
|
+
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)",
|
|
2123
|
+
boxStyles.error
|
|
2124
|
+
)
|
|
2125
|
+
);
|
|
2126
|
+
logger.log("");
|
|
2127
|
+
process.exit(1);
|
|
2128
|
+
}
|
|
2129
|
+
const user = await response.json();
|
|
2130
|
+
const credentials = {
|
|
2131
|
+
accessToken: apiToken,
|
|
2132
|
+
username: email,
|
|
2133
|
+
instanceUrl: instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`
|
|
2134
|
+
};
|
|
2135
|
+
await storageService.setServiceCredentials("jira", credentials);
|
|
2136
|
+
await storageService.setServiceCredentials("confluence", credentials);
|
|
2137
|
+
await storageService.setServiceCredentials("bitbucket", {
|
|
2138
|
+
...credentials,
|
|
2139
|
+
instanceUrl: "https://api.bitbucket.org"
|
|
2140
|
+
// Bitbucket uses different API base
|
|
2141
|
+
});
|
|
2142
|
+
logger.log("");
|
|
2143
|
+
logger.log(
|
|
2144
|
+
boxen(
|
|
2145
|
+
theme.success("\u2713 Successfully authenticated with Atlassian") + `
|
|
2146
|
+
|
|
2147
|
+
Instance: ${instanceUrl}
|
|
2148
|
+
User: ${user.displayName}
|
|
2149
|
+
Email: ${user.emailAddress}
|
|
2150
|
+
|
|
2151
|
+
Services configured:
|
|
2152
|
+
\u2022 Jira
|
|
2153
|
+
\u2022 Confluence
|
|
2154
|
+
\u2022 Bitbucket`,
|
|
2155
|
+
boxStyles.success
|
|
2156
|
+
)
|
|
2157
|
+
);
|
|
2158
|
+
logger.log("");
|
|
2159
|
+
} catch (error) {
|
|
2160
|
+
const err = error;
|
|
2161
|
+
logger.log("");
|
|
2162
|
+
logger.log(
|
|
2163
|
+
boxen(
|
|
2164
|
+
theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
|
|
2165
|
+
boxStyles.error
|
|
2166
|
+
)
|
|
2167
|
+
);
|
|
2168
|
+
logger.log("");
|
|
2169
|
+
process.exit(1);
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// src/commands/sync.ts
|
|
2174
|
+
init_esm_shims();
|
|
2175
|
+
|
|
2176
|
+
// node_modules/@inquirer/core/dist/esm/lib/errors.mjs
|
|
2177
|
+
init_esm_shims();
|
|
2178
|
+
var CancelPromptError = class extends Error {
|
|
2179
|
+
name = "CancelPromptError";
|
|
2180
|
+
message = "Prompt was canceled";
|
|
2181
|
+
};
|
|
2182
|
+
|
|
2183
|
+
// src/commands/sync.ts
|
|
2184
|
+
init_api_service();
|
|
2185
|
+
init_storage_service();
|
|
2186
|
+
init_auth_service();
|
|
2187
|
+
import boxen6 from "boxen";
|
|
2188
|
+
|
|
2189
|
+
// src/utils/source-detector.ts
|
|
2190
|
+
init_esm_shims();
|
|
1880
2191
|
init_errors();
|
|
1881
2192
|
init_storage_service();
|
|
1882
2193
|
import { exec as exec2 } from "child_process";
|
|
1883
2194
|
import { promisify as promisify2 } from "util";
|
|
2195
|
+
import { select } from "@inquirer/prompts";
|
|
1884
2196
|
var execAsync2 = promisify2(exec2);
|
|
1885
2197
|
var SourceDetector = class {
|
|
1886
2198
|
/**
|
|
1887
2199
|
* Detect all possible sources from git remotes
|
|
1888
2200
|
*/
|
|
1889
|
-
async detectSources() {
|
|
2201
|
+
async detectSources(options = {}) {
|
|
1890
2202
|
const detected = [];
|
|
1891
2203
|
try {
|
|
1892
2204
|
const { stdout } = await execAsync2("git remote -v");
|
|
@@ -1905,9 +2217,53 @@ var SourceDetector = class {
|
|
|
1905
2217
|
} catch {
|
|
1906
2218
|
throw new GitError("Not a git repository");
|
|
1907
2219
|
}
|
|
1908
|
-
|
|
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
|
+
}
|
|
1909
2236
|
return { detected, recommended };
|
|
1910
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
|
+
}
|
|
1911
2267
|
/**
|
|
1912
2268
|
* Parse git remote -v output
|
|
1913
2269
|
*/
|
|
@@ -1984,6 +2340,10 @@ var SourceDetector = class {
|
|
|
1984
2340
|
return await storageService.isServiceAuthenticated("bitbucket");
|
|
1985
2341
|
} else if (type === "gitlab") {
|
|
1986
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");
|
|
1987
2347
|
}
|
|
1988
2348
|
return false;
|
|
1989
2349
|
} catch {
|
|
@@ -2003,6 +2363,10 @@ var SourceDetector = class {
|
|
|
2003
2363
|
};
|
|
2004
2364
|
var sourceDetector = new SourceDetector();
|
|
2005
2365
|
|
|
2366
|
+
// src/commands/sync.ts
|
|
2367
|
+
init_env_loader();
|
|
2368
|
+
init_config_loader();
|
|
2369
|
+
|
|
2006
2370
|
// src/sync/adapter-factory.ts
|
|
2007
2371
|
init_esm_shims();
|
|
2008
2372
|
|
|
@@ -2025,13 +2389,13 @@ init_esm_shims();
|
|
|
2025
2389
|
init_errors();
|
|
2026
2390
|
import { existsSync as existsSync2 } from "fs";
|
|
2027
2391
|
import { join as join6 } from "path";
|
|
2028
|
-
function validateGitRepository(
|
|
2029
|
-
const gitDir = join6(
|
|
2392
|
+
function validateGitRepository(path3) {
|
|
2393
|
+
const gitDir = join6(path3, ".git");
|
|
2030
2394
|
if (!existsSync2(gitDir)) {
|
|
2031
2395
|
throw new GitError(
|
|
2032
2396
|
"Not a git repository. Please run this command from within a git repository.",
|
|
2033
2397
|
{
|
|
2034
|
-
path:
|
|
2398
|
+
path: path3,
|
|
2035
2399
|
hint: 'Run "git init" to initialize a git repository, or navigate to an existing one'
|
|
2036
2400
|
}
|
|
2037
2401
|
);
|
|
@@ -3056,6 +3420,535 @@ var GitLabSyncAdapter = class {
|
|
|
3056
3420
|
};
|
|
3057
3421
|
var gitlabSyncAdapter = new GitLabSyncAdapter();
|
|
3058
3422
|
|
|
3423
|
+
// src/sync/jira-adapter.ts
|
|
3424
|
+
init_esm_shims();
|
|
3425
|
+
|
|
3426
|
+
// src/services/jira.service.ts
|
|
3427
|
+
init_esm_shims();
|
|
3428
|
+
init_errors();
|
|
3429
|
+
init_logger();
|
|
3430
|
+
init_storage_service();
|
|
3431
|
+
var JiraService = class {
|
|
3432
|
+
MAX_DESCRIPTION_LENGTH = 5e3;
|
|
3433
|
+
/**
|
|
3434
|
+
* Get stored Jira credentials
|
|
3435
|
+
*/
|
|
3436
|
+
async getCredentials() {
|
|
3437
|
+
const creds = await storageService.getServiceCredentials("jira");
|
|
3438
|
+
if (!creds || !creds.username || !creds.accessToken || !creds.instanceUrl) {
|
|
3439
|
+
throw new JiraError("Not authenticated with Jira", {
|
|
3440
|
+
hint: "Run: bragduck auth atlassian"
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
if (creds.expiresAt && creds.expiresAt < Date.now()) {
|
|
3444
|
+
throw new JiraError("API token has expired", {
|
|
3445
|
+
hint: "Run: bragduck auth atlassian"
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
return {
|
|
3449
|
+
email: creds.username,
|
|
3450
|
+
apiToken: creds.accessToken,
|
|
3451
|
+
instanceUrl: creds.instanceUrl
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
/**
|
|
3455
|
+
* Make authenticated request to Jira API
|
|
3456
|
+
*/
|
|
3457
|
+
async request(endpoint, method = "GET", body) {
|
|
3458
|
+
const { email, apiToken, instanceUrl } = await this.getCredentials();
|
|
3459
|
+
const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
3460
|
+
const baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
|
|
3461
|
+
logger.debug(`Jira API: ${method} ${endpoint}`);
|
|
3462
|
+
const options = {
|
|
3463
|
+
method,
|
|
3464
|
+
headers: {
|
|
3465
|
+
Authorization: `Basic ${auth}`,
|
|
3466
|
+
"Content-Type": "application/json",
|
|
3467
|
+
Accept: "application/json"
|
|
3468
|
+
}
|
|
3469
|
+
};
|
|
3470
|
+
if (body) {
|
|
3471
|
+
options.body = JSON.stringify(body);
|
|
3472
|
+
}
|
|
3473
|
+
const response = await fetch(`${baseUrl}${endpoint}`, options);
|
|
3474
|
+
if (!response.ok) {
|
|
3475
|
+
const statusText = response.statusText;
|
|
3476
|
+
const status = response.status;
|
|
3477
|
+
if (status === 401) {
|
|
3478
|
+
throw new JiraError("Invalid or expired API token", {
|
|
3479
|
+
hint: "Run: bragduck auth atlassian",
|
|
3480
|
+
originalError: statusText
|
|
3481
|
+
});
|
|
3482
|
+
} else if (status === 403) {
|
|
3483
|
+
throw new JiraError("Forbidden - check token permissions", {
|
|
3484
|
+
hint: "Token needs: read access to issues",
|
|
3485
|
+
originalError: statusText
|
|
3486
|
+
});
|
|
3487
|
+
} else if (status === 404) {
|
|
3488
|
+
throw new JiraError("Resource not found", {
|
|
3489
|
+
originalError: statusText
|
|
3490
|
+
});
|
|
3491
|
+
} else if (status === 429) {
|
|
3492
|
+
throw new JiraError("Rate limit exceeded", {
|
|
3493
|
+
hint: "Wait a few minutes before trying again",
|
|
3494
|
+
originalError: statusText
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
throw new JiraError(`API request failed: ${statusText}`, {
|
|
3498
|
+
originalError: statusText
|
|
3499
|
+
});
|
|
3500
|
+
}
|
|
3501
|
+
return response.json();
|
|
3502
|
+
}
|
|
3503
|
+
/**
|
|
3504
|
+
* Validate Jira instance and credentials
|
|
3505
|
+
*/
|
|
3506
|
+
async validateJiraInstance() {
|
|
3507
|
+
try {
|
|
3508
|
+
await this.request("/rest/api/2/myself");
|
|
3509
|
+
} catch (error) {
|
|
3510
|
+
if (error instanceof JiraError) {
|
|
3511
|
+
throw error;
|
|
3512
|
+
}
|
|
3513
|
+
throw new JiraError("Could not access Jira instance via API", {
|
|
3514
|
+
hint: "Check that the instance URL is correct and your credentials are valid",
|
|
3515
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
3516
|
+
});
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
/**
|
|
3520
|
+
* Get current user's email
|
|
3521
|
+
*/
|
|
3522
|
+
async getCurrentUser() {
|
|
3523
|
+
try {
|
|
3524
|
+
const user = await this.request("/rest/api/2/myself");
|
|
3525
|
+
return user.emailAddress;
|
|
3526
|
+
} catch {
|
|
3527
|
+
return null;
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
/**
|
|
3531
|
+
* Build JQL query from options
|
|
3532
|
+
*/
|
|
3533
|
+
buildJQL(options) {
|
|
3534
|
+
const queries = [];
|
|
3535
|
+
queries.push("status IN (Done, Resolved, Closed)");
|
|
3536
|
+
if (options.days) {
|
|
3537
|
+
queries.push(`updated >= -${options.days}d`);
|
|
3538
|
+
}
|
|
3539
|
+
if (options.author) {
|
|
3540
|
+
queries.push(`creator = "${options.author}"`);
|
|
3541
|
+
}
|
|
3542
|
+
if (options.jql) {
|
|
3543
|
+
queries.push(`(${options.jql})`);
|
|
3544
|
+
}
|
|
3545
|
+
return queries.join(" AND ");
|
|
3546
|
+
}
|
|
3547
|
+
/**
|
|
3548
|
+
* Estimate issue complexity for impact scoring
|
|
3549
|
+
*/
|
|
3550
|
+
estimateComplexity(issue) {
|
|
3551
|
+
const typeScores = {
|
|
3552
|
+
Epic: 500,
|
|
3553
|
+
Story: 200,
|
|
3554
|
+
Task: 100,
|
|
3555
|
+
Bug: 100,
|
|
3556
|
+
"Sub-task": 50
|
|
3557
|
+
};
|
|
3558
|
+
return typeScores[issue.fields.issuetype.name] || 100;
|
|
3559
|
+
}
|
|
3560
|
+
/**
|
|
3561
|
+
* Fetch issues with optional filtering
|
|
3562
|
+
*/
|
|
3563
|
+
async getIssues(options = {}) {
|
|
3564
|
+
const jql = this.buildJQL(options);
|
|
3565
|
+
const fields = [
|
|
3566
|
+
"summary",
|
|
3567
|
+
"description",
|
|
3568
|
+
"created",
|
|
3569
|
+
"updated",
|
|
3570
|
+
"resolutiondate",
|
|
3571
|
+
"creator",
|
|
3572
|
+
"status",
|
|
3573
|
+
"issuetype"
|
|
3574
|
+
];
|
|
3575
|
+
const allIssues = [];
|
|
3576
|
+
let startAt = 0;
|
|
3577
|
+
const maxResults = 100;
|
|
3578
|
+
while (true) {
|
|
3579
|
+
const endpoint = `/rest/api/2/search?jql=${encodeURIComponent(jql)}&startAt=${startAt}&maxResults=${maxResults}&fields=${fields.join(",")}`;
|
|
3580
|
+
const response = await this.request(endpoint);
|
|
3581
|
+
allIssues.push(...response.issues);
|
|
3582
|
+
logger.debug(
|
|
3583
|
+
`Fetched ${response.issues.length} issues (total: ${allIssues.length} of ${response.total})${startAt + maxResults < response.total ? ", fetching next page..." : ""}`
|
|
3584
|
+
);
|
|
3585
|
+
if (startAt + maxResults >= response.total) {
|
|
3586
|
+
break;
|
|
3587
|
+
}
|
|
3588
|
+
if (options.limit && allIssues.length >= options.limit) {
|
|
3589
|
+
return allIssues.slice(0, options.limit).map((issue) => this.transformIssueToCommit(issue));
|
|
3590
|
+
}
|
|
3591
|
+
startAt += maxResults;
|
|
3592
|
+
}
|
|
3593
|
+
return allIssues.map((issue) => this.transformIssueToCommit(issue));
|
|
3594
|
+
}
|
|
3595
|
+
/**
|
|
3596
|
+
* Fetch issues for the current authenticated user
|
|
3597
|
+
*/
|
|
3598
|
+
async getIssuesByCurrentUser(options = {}) {
|
|
3599
|
+
const email = await this.getCurrentUser();
|
|
3600
|
+
if (!email) {
|
|
3601
|
+
throw new JiraError("Could not get current user");
|
|
3602
|
+
}
|
|
3603
|
+
return this.getIssues({
|
|
3604
|
+
...options,
|
|
3605
|
+
author: email
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3608
|
+
/**
|
|
3609
|
+
* Transform Jira issue to GitCommit format with external fields
|
|
3610
|
+
*/
|
|
3611
|
+
transformIssueToCommit(issue, instanceUrl) {
|
|
3612
|
+
let message = issue.fields.summary;
|
|
3613
|
+
if (issue.fields.description) {
|
|
3614
|
+
const truncatedDesc = issue.fields.description.substring(0, this.MAX_DESCRIPTION_LENGTH);
|
|
3615
|
+
message = `${issue.fields.summary}
|
|
3616
|
+
|
|
3617
|
+
${truncatedDesc}`;
|
|
3618
|
+
}
|
|
3619
|
+
const date = issue.fields.resolutiondate || issue.fields.updated;
|
|
3620
|
+
let baseUrl = "https://jira.atlassian.net";
|
|
3621
|
+
if (instanceUrl) {
|
|
3622
|
+
baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
|
|
3623
|
+
} else {
|
|
3624
|
+
try {
|
|
3625
|
+
const creds = storageService.getServiceCredentials("jira");
|
|
3626
|
+
if (creds?.instanceUrl) {
|
|
3627
|
+
baseUrl = creds.instanceUrl.startsWith("http") ? creds.instanceUrl : `https://${creds.instanceUrl}`;
|
|
3628
|
+
}
|
|
3629
|
+
} catch {
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
const url = `${baseUrl}/browse/${issue.key}`;
|
|
3633
|
+
return {
|
|
3634
|
+
sha: issue.key,
|
|
3635
|
+
message,
|
|
3636
|
+
author: issue.fields.creator.displayName,
|
|
3637
|
+
authorEmail: issue.fields.creator.emailAddress,
|
|
3638
|
+
date,
|
|
3639
|
+
url,
|
|
3640
|
+
diffStats: {
|
|
3641
|
+
filesChanged: 0,
|
|
3642
|
+
insertions: this.estimateComplexity(issue),
|
|
3643
|
+
deletions: 0
|
|
3644
|
+
},
|
|
3645
|
+
externalId: issue.key,
|
|
3646
|
+
externalType: "issue",
|
|
3647
|
+
externalSource: "jira",
|
|
3648
|
+
externalUrl: url
|
|
3649
|
+
};
|
|
3650
|
+
}
|
|
3651
|
+
};
|
|
3652
|
+
var jiraService = new JiraService();
|
|
3653
|
+
|
|
3654
|
+
// src/sync/jira-adapter.ts
|
|
3655
|
+
var jiraSyncAdapter = {
|
|
3656
|
+
name: "jira",
|
|
3657
|
+
async validate() {
|
|
3658
|
+
await jiraService.validateJiraInstance();
|
|
3659
|
+
},
|
|
3660
|
+
async getRepositoryInfo() {
|
|
3661
|
+
const user = await jiraService.getCurrentUser();
|
|
3662
|
+
const creds = await jiraService.getCredentials();
|
|
3663
|
+
const userName = user || "Unknown User";
|
|
3664
|
+
const baseUrl = creds.instanceUrl.startsWith("http") ? creds.instanceUrl : `https://${creds.instanceUrl}`;
|
|
3665
|
+
return {
|
|
3666
|
+
owner: userName,
|
|
3667
|
+
name: "Jira Issues",
|
|
3668
|
+
fullName: `${userName}'s Jira Issues`,
|
|
3669
|
+
url: baseUrl
|
|
3670
|
+
};
|
|
3671
|
+
},
|
|
3672
|
+
async fetchWorkItems(options) {
|
|
3673
|
+
const author = options.author === "current" ? await this.getCurrentUser() : options.author;
|
|
3674
|
+
return jiraService.getIssues({
|
|
3675
|
+
days: options.days,
|
|
3676
|
+
limit: options.limit,
|
|
3677
|
+
author: author || void 0
|
|
3678
|
+
});
|
|
3679
|
+
},
|
|
3680
|
+
async isAuthenticated() {
|
|
3681
|
+
try {
|
|
3682
|
+
await this.validate();
|
|
3683
|
+
return true;
|
|
3684
|
+
} catch {
|
|
3685
|
+
return false;
|
|
3686
|
+
}
|
|
3687
|
+
},
|
|
3688
|
+
async getCurrentUser() {
|
|
3689
|
+
return jiraService.getCurrentUser();
|
|
3690
|
+
}
|
|
3691
|
+
};
|
|
3692
|
+
|
|
3693
|
+
// src/sync/confluence-adapter.ts
|
|
3694
|
+
init_esm_shims();
|
|
3695
|
+
|
|
3696
|
+
// src/services/confluence.service.ts
|
|
3697
|
+
init_esm_shims();
|
|
3698
|
+
init_errors();
|
|
3699
|
+
init_logger();
|
|
3700
|
+
init_storage_service();
|
|
3701
|
+
var ConfluenceService = class {
|
|
3702
|
+
/**
|
|
3703
|
+
* Get stored Confluence credentials
|
|
3704
|
+
*/
|
|
3705
|
+
async getCredentials() {
|
|
3706
|
+
const creds = await storageService.getServiceCredentials("confluence");
|
|
3707
|
+
if (!creds || !creds.username || !creds.accessToken || !creds.instanceUrl) {
|
|
3708
|
+
throw new ConfluenceError("Not authenticated with Confluence", {
|
|
3709
|
+
hint: "Run: bragduck auth atlassian"
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
if (creds.expiresAt && creds.expiresAt < Date.now()) {
|
|
3713
|
+
throw new ConfluenceError("API token has expired", {
|
|
3714
|
+
hint: "Run: bragduck auth atlassian"
|
|
3715
|
+
});
|
|
3716
|
+
}
|
|
3717
|
+
return {
|
|
3718
|
+
email: creds.username,
|
|
3719
|
+
apiToken: creds.accessToken,
|
|
3720
|
+
instanceUrl: creds.instanceUrl
|
|
3721
|
+
};
|
|
3722
|
+
}
|
|
3723
|
+
/**
|
|
3724
|
+
* Make authenticated request to Confluence API
|
|
3725
|
+
*/
|
|
3726
|
+
async request(endpoint, method = "GET", body) {
|
|
3727
|
+
const { email, apiToken, instanceUrl } = await this.getCredentials();
|
|
3728
|
+
const auth = Buffer.from(`${email}:${apiToken}`).toString("base64");
|
|
3729
|
+
const baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
|
|
3730
|
+
logger.debug(`Confluence API: ${method} ${endpoint}`);
|
|
3731
|
+
const options = {
|
|
3732
|
+
method,
|
|
3733
|
+
headers: {
|
|
3734
|
+
Authorization: `Basic ${auth}`,
|
|
3735
|
+
"Content-Type": "application/json",
|
|
3736
|
+
Accept: "application/json"
|
|
3737
|
+
}
|
|
3738
|
+
};
|
|
3739
|
+
if (body) {
|
|
3740
|
+
options.body = JSON.stringify(body);
|
|
3741
|
+
}
|
|
3742
|
+
const response = await fetch(`${baseUrl}${endpoint}`, options);
|
|
3743
|
+
if (!response.ok) {
|
|
3744
|
+
const statusText = response.statusText;
|
|
3745
|
+
const status = response.status;
|
|
3746
|
+
if (status === 401) {
|
|
3747
|
+
throw new ConfluenceError("Invalid or expired API token", {
|
|
3748
|
+
hint: "Run: bragduck auth atlassian",
|
|
3749
|
+
originalError: statusText
|
|
3750
|
+
});
|
|
3751
|
+
} else if (status === 403) {
|
|
3752
|
+
throw new ConfluenceError("Forbidden - check token permissions", {
|
|
3753
|
+
hint: "Token needs: read access to pages",
|
|
3754
|
+
originalError: statusText
|
|
3755
|
+
});
|
|
3756
|
+
} else if (status === 404) {
|
|
3757
|
+
throw new ConfluenceError("Resource not found", {
|
|
3758
|
+
originalError: statusText
|
|
3759
|
+
});
|
|
3760
|
+
} else if (status === 429) {
|
|
3761
|
+
throw new ConfluenceError("Rate limit exceeded", {
|
|
3762
|
+
hint: "Wait a few minutes before trying again",
|
|
3763
|
+
originalError: statusText
|
|
3764
|
+
});
|
|
3765
|
+
}
|
|
3766
|
+
throw new ConfluenceError(`API request failed: ${statusText}`, {
|
|
3767
|
+
originalError: statusText
|
|
3768
|
+
});
|
|
3769
|
+
}
|
|
3770
|
+
return response.json();
|
|
3771
|
+
}
|
|
3772
|
+
/**
|
|
3773
|
+
* Validate Confluence instance and credentials
|
|
3774
|
+
*/
|
|
3775
|
+
async validateConfluenceInstance() {
|
|
3776
|
+
try {
|
|
3777
|
+
await this.request("/wiki/rest/api/content?type=page&limit=1");
|
|
3778
|
+
} catch (error) {
|
|
3779
|
+
if (error instanceof ConfluenceError) {
|
|
3780
|
+
throw error;
|
|
3781
|
+
}
|
|
3782
|
+
throw new ConfluenceError("Could not access Confluence instance via API", {
|
|
3783
|
+
hint: "Check that the instance URL is correct and your credentials are valid",
|
|
3784
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
3785
|
+
});
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
/**
|
|
3789
|
+
* Get current user's email
|
|
3790
|
+
*/
|
|
3791
|
+
async getCurrentUser() {
|
|
3792
|
+
try {
|
|
3793
|
+
const creds = await this.getCredentials();
|
|
3794
|
+
return creds.email;
|
|
3795
|
+
} catch {
|
|
3796
|
+
return null;
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
/**
|
|
3800
|
+
* Build CQL query from options
|
|
3801
|
+
*/
|
|
3802
|
+
buildCQL(options) {
|
|
3803
|
+
const queries = [];
|
|
3804
|
+
queries.push("type=page");
|
|
3805
|
+
if (options.days) {
|
|
3806
|
+
queries.push(`lastModified >= now("-${options.days}d")`);
|
|
3807
|
+
}
|
|
3808
|
+
if (options.author) {
|
|
3809
|
+
queries.push(`creator = "${options.author}"`);
|
|
3810
|
+
}
|
|
3811
|
+
if (options.cql) {
|
|
3812
|
+
queries.push(`(${options.cql})`);
|
|
3813
|
+
}
|
|
3814
|
+
return queries.join(" AND ");
|
|
3815
|
+
}
|
|
3816
|
+
/**
|
|
3817
|
+
* Estimate page size for impact scoring
|
|
3818
|
+
*/
|
|
3819
|
+
estimatePageSize(page) {
|
|
3820
|
+
const content = page.body?.storage?.value || "";
|
|
3821
|
+
return Math.ceil(content.length / 80);
|
|
3822
|
+
}
|
|
3823
|
+
/**
|
|
3824
|
+
* Fetch pages with optional filtering
|
|
3825
|
+
*/
|
|
3826
|
+
async getPages(options = {}) {
|
|
3827
|
+
const cql = this.buildCQL(options);
|
|
3828
|
+
const allPages = [];
|
|
3829
|
+
let start = 0;
|
|
3830
|
+
const limit = 100;
|
|
3831
|
+
while (true) {
|
|
3832
|
+
const params = {
|
|
3833
|
+
type: "page",
|
|
3834
|
+
status: "current",
|
|
3835
|
+
start: start.toString(),
|
|
3836
|
+
limit: limit.toString(),
|
|
3837
|
+
expand: "version,body.storage"
|
|
3838
|
+
};
|
|
3839
|
+
if (cql) {
|
|
3840
|
+
params.cql = cql;
|
|
3841
|
+
}
|
|
3842
|
+
const queryString = Object.entries(params).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
|
|
3843
|
+
const endpoint = `/wiki/rest/api/content?${queryString}`;
|
|
3844
|
+
const response = await this.request(endpoint);
|
|
3845
|
+
allPages.push(...response.results);
|
|
3846
|
+
logger.debug(
|
|
3847
|
+
`Fetched ${response.results.length} pages (total: ${allPages.length})${response.results.length === limit ? ", fetching next page..." : ""}`
|
|
3848
|
+
);
|
|
3849
|
+
if (response.results.length < limit) {
|
|
3850
|
+
break;
|
|
3851
|
+
}
|
|
3852
|
+
if (options.limit && allPages.length >= options.limit) {
|
|
3853
|
+
return allPages.slice(0, options.limit).map((page) => this.transformPageToCommit(page));
|
|
3854
|
+
}
|
|
3855
|
+
start += limit;
|
|
3856
|
+
}
|
|
3857
|
+
return allPages.map((page) => this.transformPageToCommit(page));
|
|
3858
|
+
}
|
|
3859
|
+
/**
|
|
3860
|
+
* Fetch pages for the current authenticated user
|
|
3861
|
+
*/
|
|
3862
|
+
async getPagesByCurrentUser(options = {}) {
|
|
3863
|
+
const email = await this.getCurrentUser();
|
|
3864
|
+
if (!email) {
|
|
3865
|
+
throw new ConfluenceError("Could not get current user");
|
|
3866
|
+
}
|
|
3867
|
+
return this.getPages({
|
|
3868
|
+
...options,
|
|
3869
|
+
author: email
|
|
3870
|
+
});
|
|
3871
|
+
}
|
|
3872
|
+
/**
|
|
3873
|
+
* Transform Confluence page to GitCommit format with external fields
|
|
3874
|
+
*/
|
|
3875
|
+
transformPageToCommit(page, instanceUrl) {
|
|
3876
|
+
const message = `${page.title}
|
|
3877
|
+
|
|
3878
|
+
[Confluence Page v${page.version.number}]`;
|
|
3879
|
+
let baseUrl = "https://confluence.atlassian.net";
|
|
3880
|
+
if (instanceUrl) {
|
|
3881
|
+
baseUrl = instanceUrl.startsWith("http") ? instanceUrl : `https://${instanceUrl}`;
|
|
3882
|
+
} else {
|
|
3883
|
+
try {
|
|
3884
|
+
const creds = storageService.getServiceCredentials("confluence");
|
|
3885
|
+
if (creds?.instanceUrl) {
|
|
3886
|
+
baseUrl = creds.instanceUrl.startsWith("http") ? creds.instanceUrl : `https://${creds.instanceUrl}`;
|
|
3887
|
+
}
|
|
3888
|
+
} catch {
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
const url = `${baseUrl}/wiki${page._links.webui}`;
|
|
3892
|
+
return {
|
|
3893
|
+
sha: page.id,
|
|
3894
|
+
message,
|
|
3895
|
+
author: page.version.by.displayName,
|
|
3896
|
+
authorEmail: page.version.by.email,
|
|
3897
|
+
date: page.version.when,
|
|
3898
|
+
url,
|
|
3899
|
+
diffStats: {
|
|
3900
|
+
filesChanged: 1,
|
|
3901
|
+
insertions: this.estimatePageSize(page),
|
|
3902
|
+
deletions: 0
|
|
3903
|
+
},
|
|
3904
|
+
externalId: page.id,
|
|
3905
|
+
externalType: "page",
|
|
3906
|
+
externalSource: "confluence",
|
|
3907
|
+
externalUrl: url
|
|
3908
|
+
};
|
|
3909
|
+
}
|
|
3910
|
+
};
|
|
3911
|
+
var confluenceService = new ConfluenceService();
|
|
3912
|
+
|
|
3913
|
+
// src/sync/confluence-adapter.ts
|
|
3914
|
+
var confluenceSyncAdapter = {
|
|
3915
|
+
name: "confluence",
|
|
3916
|
+
async validate() {
|
|
3917
|
+
await confluenceService.validateConfluenceInstance();
|
|
3918
|
+
},
|
|
3919
|
+
async getRepositoryInfo() {
|
|
3920
|
+
const user = await confluenceService.getCurrentUser();
|
|
3921
|
+
const creds = await confluenceService.getCredentials();
|
|
3922
|
+
const userName = user || "Unknown User";
|
|
3923
|
+
const baseUrl = creds.instanceUrl.startsWith("http") ? creds.instanceUrl : `https://${creds.instanceUrl}`;
|
|
3924
|
+
return {
|
|
3925
|
+
owner: userName,
|
|
3926
|
+
name: "Confluence Pages",
|
|
3927
|
+
fullName: `${userName}'s Confluence Pages`,
|
|
3928
|
+
url: `${baseUrl}/wiki`
|
|
3929
|
+
};
|
|
3930
|
+
},
|
|
3931
|
+
async fetchWorkItems(options) {
|
|
3932
|
+
const author = options.author === "current" ? await this.getCurrentUser() : options.author;
|
|
3933
|
+
return confluenceService.getPages({
|
|
3934
|
+
days: options.days,
|
|
3935
|
+
limit: options.limit,
|
|
3936
|
+
author: author || void 0
|
|
3937
|
+
});
|
|
3938
|
+
},
|
|
3939
|
+
async isAuthenticated() {
|
|
3940
|
+
try {
|
|
3941
|
+
await this.validate();
|
|
3942
|
+
return true;
|
|
3943
|
+
} catch {
|
|
3944
|
+
return false;
|
|
3945
|
+
}
|
|
3946
|
+
},
|
|
3947
|
+
async getCurrentUser() {
|
|
3948
|
+
return confluenceService.getCurrentUser();
|
|
3949
|
+
}
|
|
3950
|
+
};
|
|
3951
|
+
|
|
3059
3952
|
// src/sync/adapter-factory.ts
|
|
3060
3953
|
var AdapterFactory = class {
|
|
3061
3954
|
/**
|
|
@@ -3071,6 +3964,10 @@ var AdapterFactory = class {
|
|
|
3071
3964
|
// Bitbucket Cloud and Server use same adapter
|
|
3072
3965
|
case "gitlab":
|
|
3073
3966
|
return gitlabSyncAdapter;
|
|
3967
|
+
case "jira":
|
|
3968
|
+
return jiraSyncAdapter;
|
|
3969
|
+
case "confluence":
|
|
3970
|
+
return confluenceSyncAdapter;
|
|
3074
3971
|
default:
|
|
3075
3972
|
throw new Error(`Unknown source type: ${source}`);
|
|
3076
3973
|
}
|
|
@@ -3079,7 +3976,7 @@ var AdapterFactory = class {
|
|
|
3079
3976
|
* Check if adapter is available for source
|
|
3080
3977
|
*/
|
|
3081
3978
|
static isSupported(source) {
|
|
3082
|
-
return source === "github" || source === "bitbucket" || source === "atlassian" || source === "gitlab";
|
|
3979
|
+
return source === "github" || source === "bitbucket" || source === "atlassian" || source === "gitlab" || source === "jira" || source === "confluence";
|
|
3083
3980
|
}
|
|
3084
3981
|
};
|
|
3085
3982
|
|
|
@@ -3219,7 +4116,7 @@ init_logger();
|
|
|
3219
4116
|
|
|
3220
4117
|
// src/ui/prompts.ts
|
|
3221
4118
|
init_esm_shims();
|
|
3222
|
-
import { checkbox, confirm, input as input2, select, editor } from "@inquirer/prompts";
|
|
4119
|
+
import { checkbox, confirm, input as input2, select as select2, editor } from "@inquirer/prompts";
|
|
3223
4120
|
import boxen4 from "boxen";
|
|
3224
4121
|
|
|
3225
4122
|
// src/ui/formatters.ts
|
|
@@ -3375,7 +4272,7 @@ async function promptDaysToScan(defaultDays = 30) {
|
|
|
3375
4272
|
{ name: "90 days", value: "90", description: "Last 3 months" },
|
|
3376
4273
|
{ name: "Custom", value: "custom", description: "Enter custom number of days" }
|
|
3377
4274
|
];
|
|
3378
|
-
const selected = await
|
|
4275
|
+
const selected = await select2({
|
|
3379
4276
|
message: "How many days back should we scan for PRs?",
|
|
3380
4277
|
choices,
|
|
3381
4278
|
default: "30"
|
|
@@ -3407,7 +4304,7 @@ async function promptSortOption() {
|
|
|
3407
4304
|
{ name: "By files (most files)", value: "files", description: "Most files changed" },
|
|
3408
4305
|
{ name: "No sorting", value: "none", description: "Keep original order" }
|
|
3409
4306
|
];
|
|
3410
|
-
return await
|
|
4307
|
+
return await select2({
|
|
3411
4308
|
message: "How would you like to sort the PRs?",
|
|
3412
4309
|
choices,
|
|
3413
4310
|
default: "date"
|
|
@@ -3421,7 +4318,7 @@ async function promptSelectOrganisation(organisations) {
|
|
|
3421
4318
|
value: org.id
|
|
3422
4319
|
}))
|
|
3423
4320
|
];
|
|
3424
|
-
const selected = await
|
|
4321
|
+
const selected = await select2({
|
|
3425
4322
|
message: "Attach brags to which company?",
|
|
3426
4323
|
choices,
|
|
3427
4324
|
default: "none"
|
|
@@ -3471,7 +4368,7 @@ ${theme.label("PR Link")} ${colors.link(prUrl)}`;
|
|
|
3471
4368
|
}
|
|
3472
4369
|
console.log(boxen4(bragDetails, boxStyles.info));
|
|
3473
4370
|
console.log("");
|
|
3474
|
-
const action = await
|
|
4371
|
+
const action = await select2({
|
|
3475
4372
|
message: `What would you like to do with this brag?`,
|
|
3476
4373
|
choices: [
|
|
3477
4374
|
{ name: "\u2713 Accept", value: "accept", description: "Add this brag as-is" },
|
|
@@ -3569,6 +4466,9 @@ async function ensureAuthenticated() {
|
|
|
3569
4466
|
}
|
|
3570
4467
|
}
|
|
3571
4468
|
|
|
4469
|
+
// src/commands/sync.ts
|
|
4470
|
+
init_errors();
|
|
4471
|
+
|
|
3572
4472
|
// src/ui/spinners.ts
|
|
3573
4473
|
init_esm_shims();
|
|
3574
4474
|
import ora2 from "ora";
|
|
@@ -3644,25 +4544,93 @@ async function syncCommand(options = {}) {
|
|
|
3644
4544
|
logger.debug(`Subscription tier "${subscriptionStatus.tier}" - proceeding with sync`);
|
|
3645
4545
|
const detectionSpinner = createStepSpinner(1, TOTAL_STEPS, "Detecting repository source");
|
|
3646
4546
|
detectionSpinner.start();
|
|
3647
|
-
|
|
3648
|
-
if (
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
logger.info("Make sure you are in a git repository with a remote URL");
|
|
3652
|
-
return;
|
|
4547
|
+
let sourceType = options.source;
|
|
4548
|
+
if (!sourceType) {
|
|
4549
|
+
const envConfig = loadEnvConfig();
|
|
4550
|
+
sourceType = envConfig.source;
|
|
3653
4551
|
}
|
|
3654
|
-
const sourceType = options.source || detectionResult.recommended;
|
|
3655
4552
|
if (!sourceType) {
|
|
3656
|
-
|
|
3657
|
-
|
|
4553
|
+
const projectConfig = await findProjectConfig();
|
|
4554
|
+
sourceType = projectConfig?.defaultSource;
|
|
3658
4555
|
}
|
|
3659
|
-
if (!
|
|
3660
|
-
|
|
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>")}`);
|
|
3661
4611
|
logger.log("");
|
|
3662
|
-
logger.info(
|
|
3663
|
-
logger.info(`Coming soon: GitLab, Atlassian, Bitbucket`);
|
|
4612
|
+
logger.info("Supported sources: github, gitlab, bitbucket, jira, confluence");
|
|
3664
4613
|
return;
|
|
3665
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`);
|
|
4622
|
+
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
|
+
);
|
|
4631
|
+
return;
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
3666
4634
|
succeedStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `Source: ${theme.value(sourceType)}`);
|
|
3667
4635
|
logger.log("");
|
|
3668
4636
|
const adapter = AdapterFactory.getAdapter(sourceType);
|
|
@@ -3824,7 +4792,12 @@ async function syncCommand(options = {}) {
|
|
|
3824
4792
|
impact_score: refined.suggested_impactLevel,
|
|
3825
4793
|
impact_description: refined.impact_description,
|
|
3826
4794
|
attachments: originalCommit?.url ? [originalCommit.url] : [],
|
|
3827
|
-
orgId: selectedOrgId || void 0
|
|
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
|
|
3828
4801
|
};
|
|
3829
4802
|
})
|
|
3830
4803
|
};
|
|
@@ -4404,7 +5377,12 @@ async function configCommand(subcommand, key, value) {
|
|
|
4404
5377
|
async function handleListConfig() {
|
|
4405
5378
|
const config2 = {
|
|
4406
5379
|
defaultCommitDays: storageService.getConfig("defaultCommitDays"),
|
|
4407
|
-
autoVersionCheck: storageService.getConfig("autoVersionCheck")
|
|
5380
|
+
autoVersionCheck: storageService.getConfig("autoVersionCheck"),
|
|
5381
|
+
defaultSource: storageService.getConfig("defaultSource"),
|
|
5382
|
+
sourcePriority: storageService.getConfig("sourcePriority"),
|
|
5383
|
+
jiraInstance: storageService.getConfig("jiraInstance"),
|
|
5384
|
+
confluenceInstance: storageService.getConfig("confluenceInstance"),
|
|
5385
|
+
gitlabInstance: storageService.getConfig("gitlabInstance")
|
|
4408
5386
|
};
|
|
4409
5387
|
const table = new Table3({
|
|
4410
5388
|
head: [chalk8.cyan("Key"), chalk8.cyan("Value"), chalk8.cyan("Default")],
|
|
@@ -4425,6 +5403,31 @@ async function handleListConfig() {
|
|
|
4425
5403
|
chalk8.yellow(String(config2.autoVersionCheck)),
|
|
4426
5404
|
chalk8.dim(String(DEFAULT_CONFIG.autoVersionCheck))
|
|
4427
5405
|
]);
|
|
5406
|
+
table.push([
|
|
5407
|
+
chalk8.white("defaultSource"),
|
|
5408
|
+
chalk8.yellow(config2.defaultSource || "not set"),
|
|
5409
|
+
chalk8.dim("not set")
|
|
5410
|
+
]);
|
|
5411
|
+
table.push([
|
|
5412
|
+
chalk8.white("sourcePriority"),
|
|
5413
|
+
chalk8.yellow(config2.sourcePriority ? config2.sourcePriority.join(", ") : "not set"),
|
|
5414
|
+
chalk8.dim("not set")
|
|
5415
|
+
]);
|
|
5416
|
+
table.push([
|
|
5417
|
+
chalk8.white("jiraInstance"),
|
|
5418
|
+
chalk8.yellow(config2.jiraInstance || "not set"),
|
|
5419
|
+
chalk8.dim("not set")
|
|
5420
|
+
]);
|
|
5421
|
+
table.push([
|
|
5422
|
+
chalk8.white("confluenceInstance"),
|
|
5423
|
+
chalk8.yellow(config2.confluenceInstance || "not set"),
|
|
5424
|
+
chalk8.dim("not set")
|
|
5425
|
+
]);
|
|
5426
|
+
table.push([
|
|
5427
|
+
chalk8.white("gitlabInstance"),
|
|
5428
|
+
chalk8.yellow(config2.gitlabInstance || "not set"),
|
|
5429
|
+
chalk8.dim("not set")
|
|
5430
|
+
]);
|
|
4428
5431
|
logger.info("Current configuration:");
|
|
4429
5432
|
logger.log("");
|
|
4430
5433
|
logger.log(table.toString());
|
|
@@ -4504,6 +5507,43 @@ Must be a number between 1 and 365`
|
|
|
4504
5507
|
Must be one of: true, false, yes, no, 1, 0`
|
|
4505
5508
|
);
|
|
4506
5509
|
}
|
|
5510
|
+
case CONFIG_KEYS.DEFAULT_SOURCE: {
|
|
5511
|
+
const validSources = ["github", "gitlab", "bitbucket", "jira", "confluence"];
|
|
5512
|
+
if (!validSources.includes(value)) {
|
|
5513
|
+
throw new ValidationError(
|
|
5514
|
+
`Invalid source: "${value}"
|
|
5515
|
+
|
|
5516
|
+
Valid sources: ${validSources.join(", ")}`
|
|
5517
|
+
);
|
|
5518
|
+
}
|
|
5519
|
+
return value;
|
|
5520
|
+
}
|
|
5521
|
+
case CONFIG_KEYS.SOURCE_PRIORITY: {
|
|
5522
|
+
const sources = value.split(",").map((s) => s.trim());
|
|
5523
|
+
const validSources = ["github", "gitlab", "bitbucket", "jira", "confluence"];
|
|
5524
|
+
for (const source of sources) {
|
|
5525
|
+
if (!validSources.includes(source)) {
|
|
5526
|
+
throw new ValidationError(
|
|
5527
|
+
`Invalid source in priority list: "${source}"
|
|
5528
|
+
|
|
5529
|
+
Valid sources: ${validSources.join(", ")}`
|
|
5530
|
+
);
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5533
|
+
return sources;
|
|
5534
|
+
}
|
|
5535
|
+
case CONFIG_KEYS.JIRA_INSTANCE:
|
|
5536
|
+
case CONFIG_KEYS.CONFLUENCE_INSTANCE:
|
|
5537
|
+
case CONFIG_KEYS.GITLAB_INSTANCE: {
|
|
5538
|
+
if (!value.match(/^[a-zA-Z0-9.-]+(:[0-9]+)?(\/.*)?$/)) {
|
|
5539
|
+
throw new ValidationError(
|
|
5540
|
+
`Invalid instance URL: "${value}"
|
|
5541
|
+
|
|
5542
|
+
Must be a valid hostname (e.g., company.atlassian.net, gitlab.company.com)`
|
|
5543
|
+
);
|
|
5544
|
+
}
|
|
5545
|
+
return value;
|
|
5546
|
+
}
|
|
4507
5547
|
default:
|
|
4508
5548
|
throw new ValidationError(`Unknown config key: "${key}"`);
|
|
4509
5549
|
}
|