@bragduck/cli 2.7.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.
@@ -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
- this.config.set(key, value);
680
+ if (value !== void 0) {
681
+ this.config.set(key, value);
682
+ }
372
683
  });
373
684
  }
374
685
  /**
@@ -391,101 +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, JiraError, ConfluenceError;
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
- JiraError = class extends BragduckError {
475
- constructor(message, details) {
476
- super(message, "JIRA_ERROR", details);
477
- this.name = "JiraError";
478
- }
479
- };
480
- ConfluenceError = class extends BragduckError {
481
- constructor(message, details) {
482
- super(message, "CONFLUENCE_ERROR", details);
483
- this.name = "ConfluenceError";
484
- }
485
- };
486
- }
487
- });
488
-
489
705
  // src/utils/logger.ts
490
706
  import chalk from "chalk";
491
707
  var logger;
@@ -1976,12 +2192,13 @@ init_errors();
1976
2192
  init_storage_service();
1977
2193
  import { exec as exec2 } from "child_process";
1978
2194
  import { promisify as promisify2 } from "util";
2195
+ import { select } from "@inquirer/prompts";
1979
2196
  var execAsync2 = promisify2(exec2);
1980
2197
  var SourceDetector = class {
1981
2198
  /**
1982
2199
  * Detect all possible sources from git remotes
1983
2200
  */
1984
- async detectSources() {
2201
+ async detectSources(options = {}) {
1985
2202
  const detected = [];
1986
2203
  try {
1987
2204
  const { stdout } = await execAsync2("git remote -v");
@@ -2000,9 +2217,53 @@ var SourceDetector = class {
2000
2217
  } catch {
2001
2218
  throw new GitError("Not a git repository");
2002
2219
  }
2003
- const recommended = this.selectRecommendedSource(detected);
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
+ }
2004
2236
  return { detected, recommended };
2005
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
+ }
2006
2267
  /**
2007
2268
  * Parse git remote -v output
2008
2269
  */
@@ -2102,6 +2363,10 @@ var SourceDetector = class {
2102
2363
  };
2103
2364
  var sourceDetector = new SourceDetector();
2104
2365
 
2366
+ // src/commands/sync.ts
2367
+ init_env_loader();
2368
+ init_config_loader();
2369
+
2105
2370
  // src/sync/adapter-factory.ts
2106
2371
  init_esm_shims();
2107
2372
 
@@ -2124,13 +2389,13 @@ init_esm_shims();
2124
2389
  init_errors();
2125
2390
  import { existsSync as existsSync2 } from "fs";
2126
2391
  import { join as join6 } from "path";
2127
- function validateGitRepository(path2) {
2128
- const gitDir = join6(path2, ".git");
2392
+ function validateGitRepository(path3) {
2393
+ const gitDir = join6(path3, ".git");
2129
2394
  if (!existsSync2(gitDir)) {
2130
2395
  throw new GitError(
2131
2396
  "Not a git repository. Please run this command from within a git repository.",
2132
2397
  {
2133
- path: path2,
2398
+ path: path3,
2134
2399
  hint: 'Run "git init" to initialize a git repository, or navigate to an existing one'
2135
2400
  }
2136
2401
  );
@@ -3851,7 +4116,7 @@ init_logger();
3851
4116
 
3852
4117
  // src/ui/prompts.ts
3853
4118
  init_esm_shims();
3854
- 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";
3855
4120
  import boxen4 from "boxen";
3856
4121
 
3857
4122
  // src/ui/formatters.ts
@@ -4007,7 +4272,7 @@ async function promptDaysToScan(defaultDays = 30) {
4007
4272
  { name: "90 days", value: "90", description: "Last 3 months" },
4008
4273
  { name: "Custom", value: "custom", description: "Enter custom number of days" }
4009
4274
  ];
4010
- const selected = await select({
4275
+ const selected = await select2({
4011
4276
  message: "How many days back should we scan for PRs?",
4012
4277
  choices,
4013
4278
  default: "30"
@@ -4039,7 +4304,7 @@ async function promptSortOption() {
4039
4304
  { name: "By files (most files)", value: "files", description: "Most files changed" },
4040
4305
  { name: "No sorting", value: "none", description: "Keep original order" }
4041
4306
  ];
4042
- return await select({
4307
+ return await select2({
4043
4308
  message: "How would you like to sort the PRs?",
4044
4309
  choices,
4045
4310
  default: "date"
@@ -4053,7 +4318,7 @@ async function promptSelectOrganisation(organisations) {
4053
4318
  value: org.id
4054
4319
  }))
4055
4320
  ];
4056
- const selected = await select({
4321
+ const selected = await select2({
4057
4322
  message: "Attach brags to which company?",
4058
4323
  choices,
4059
4324
  default: "none"
@@ -4103,7 +4368,7 @@ ${theme.label("PR Link")} ${colors.link(prUrl)}`;
4103
4368
  }
4104
4369
  console.log(boxen4(bragDetails, boxStyles.info));
4105
4370
  console.log("");
4106
- const action = await select({
4371
+ const action = await select2({
4107
4372
  message: `What would you like to do with this brag?`,
4108
4373
  choices: [
4109
4374
  { name: "\u2713 Accept", value: "accept", description: "Add this brag as-is" },
@@ -4201,6 +4466,9 @@ async function ensureAuthenticated() {
4201
4466
  }
4202
4467
  }
4203
4468
 
4469
+ // src/commands/sync.ts
4470
+ init_errors();
4471
+
4204
4472
  // src/ui/spinners.ts
4205
4473
  init_esm_shims();
4206
4474
  import ora2 from "ora";
@@ -4277,42 +4545,89 @@ async function syncCommand(options = {}) {
4277
4545
  const detectionSpinner = createStepSpinner(1, TOTAL_STEPS, "Detecting repository source");
4278
4546
  detectionSpinner.start();
4279
4547
  let sourceType = options.source;
4548
+ if (!sourceType) {
4549
+ const envConfig = loadEnvConfig();
4550
+ sourceType = envConfig.source;
4551
+ }
4552
+ if (!sourceType) {
4553
+ const projectConfig = await findProjectConfig();
4554
+ sourceType = projectConfig?.defaultSource;
4555
+ }
4280
4556
  if (!sourceType) {
4281
4557
  try {
4282
- const detectionResult = await sourceDetector.detectSources();
4558
+ const detectionResult = await sourceDetector.detectSources({
4559
+ allowInteractive: true,
4560
+ respectPriority: true,
4561
+ showAuthStatus: true
4562
+ });
4283
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
+ }
4284
4569
  if (!sourceType && detectionResult.detected.length === 0) {
4285
4570
  failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "No supported sources detected");
4286
4571
  logger.log("");
4287
4572
  logger.info("Make sure you are in a git repository with a remote URL");
4288
- logger.info("Or use --source flag for non-git sources: --source jira|confluence");
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")}`);
4289
4576
  return;
4290
4577
  }
4291
- } catch {
4292
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, "Not a git repository");
4293
- logger.log("");
4294
- logger.info("Use --source flag to specify source: --source jira|confluence");
4295
- return;
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;
4296
4591
  }
4297
4592
  }
4298
- if (!sourceType) {
4593
+ if (!sourceType || !AdapterFactory.isSupported(sourceType)) {
4299
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>")}`);
4300
4611
  logger.log("");
4301
- logger.info("Use --source flag: --source github|gitlab|bitbucket|jira|confluence");
4302
- return;
4303
- }
4304
- if (!AdapterFactory.isSupported(sourceType)) {
4305
- failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `Source ${sourceType} not yet supported`);
4306
- logger.log("");
4307
- logger.info(`Currently supported: GitHub, GitLab, Bitbucket, Jira, Confluence`);
4612
+ logger.info("Supported sources: github, gitlab, bitbucket, jira, confluence");
4308
4613
  return;
4309
4614
  }
4310
4615
  if (sourceType === "jira" || sourceType === "confluence") {
4311
4616
  const creds = await storageService.getServiceCredentials(sourceType);
4312
- if (!creds || !creds.instanceUrl) {
4617
+ const envInstance = loadEnvConfig()[`${sourceType}Instance`];
4618
+ const projectConfig = await findProjectConfig();
4619
+ const configInstance = projectConfig?.[`${sourceType}Instance`];
4620
+ if (!creds?.instanceUrl && !envInstance && !configInstance) {
4313
4621
  failStepSpinner(detectionSpinner, 1, TOTAL_STEPS, `No ${sourceType} instance configured`);
4314
4622
  logger.log("");
4315
- logger.info(`Run: ${theme.command("bragduck auth atlassian")}`);
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
+ );
4316
4631
  return;
4317
4632
  }
4318
4633
  }
@@ -5062,7 +5377,12 @@ async function configCommand(subcommand, key, value) {
5062
5377
  async function handleListConfig() {
5063
5378
  const config2 = {
5064
5379
  defaultCommitDays: storageService.getConfig("defaultCommitDays"),
5065
- 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")
5066
5386
  };
5067
5387
  const table = new Table3({
5068
5388
  head: [chalk8.cyan("Key"), chalk8.cyan("Value"), chalk8.cyan("Default")],
@@ -5083,6 +5403,31 @@ async function handleListConfig() {
5083
5403
  chalk8.yellow(String(config2.autoVersionCheck)),
5084
5404
  chalk8.dim(String(DEFAULT_CONFIG.autoVersionCheck))
5085
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
+ ]);
5086
5431
  logger.info("Current configuration:");
5087
5432
  logger.log("");
5088
5433
  logger.log(table.toString());
@@ -5162,6 +5507,43 @@ Must be a number between 1 and 365`
5162
5507
  Must be one of: true, false, yes, no, 1, 0`
5163
5508
  );
5164
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
+ }
5165
5547
  default:
5166
5548
  throw new ValidationError(`Unknown config key: "${key}"`);
5167
5549
  }