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