@in-the-loop-labs/pair-review 3.2.3 → 3.3.1

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/src/config.js CHANGED
@@ -34,7 +34,7 @@ const DEFAULT_CONFIG = {
34
34
  chat: { enable_shortcuts: true, enter_to_send: true }, // Chat panel settings (enable_shortcuts: show action shortcut buttons, enter_to_send: Enter sends message instead of newline)
35
35
  providers: {}, // Custom AI analysis provider configurations (overrides built-in defaults)
36
36
  chat_providers: {}, // Custom chat provider configurations (overrides built-in defaults)
37
- monorepos: {}, // Monorepo configurations: { "owner/repo": { path: "~/path/to/clone" } }
37
+ repos: {}, // Repository configurations: { "owner/repo": { path: "~/path/to/clone" } }
38
38
  assisted_by_url: "https://github.com/in-the-loop-labs/pair-review", // URL for "Review assisted by" footer link
39
39
  hooks: {}, // Hook commands per event: { "review.started": { "my_hook": { "command": "..." } } }
40
40
  enable_graphite: false, // When true, shows Graphite links alongside GitHub links
@@ -221,6 +221,20 @@ async function loadConfig() {
221
221
  }
222
222
  }
223
223
 
224
+ // Normalize legacy monorepos key into repos (monorepos values are overridden by repos)
225
+ if (mergedConfig.monorepos) {
226
+ mergedConfig.repos = deepMerge(mergedConfig.monorepos, mergedConfig.repos);
227
+ }
228
+
229
+ // Normalize repo keys to lowercase to match the database's COLLATE NOCASE identity
230
+ if (mergedConfig.repos) {
231
+ const normalized = {};
232
+ for (const [key, value] of Object.entries(mergedConfig.repos)) {
233
+ normalized[key.toLowerCase()] = value;
234
+ }
235
+ mergedConfig.repos = normalized;
236
+ }
237
+
224
238
  // Validate port
225
239
  if (!validatePort(mergedConfig.port)) {
226
240
  console.error(`Invalid port number ${mergedConfig.port}`);
@@ -393,83 +407,248 @@ function expandPath(p) {
393
407
  }
394
408
 
395
409
  /**
396
- * Gets the configured monorepo path for a repository
410
+ * Get repository configuration, checking `repos` key first, falling back to `monorepos`.
411
+ * @param {object} config
412
+ * @param {string} repository - owner/repo
413
+ * @returns {object|null}
414
+ */
415
+ function getRepoConfig(config, repository) {
416
+ const reposSection = config.repos || {};
417
+ const entry = reposSection[repository];
418
+ if (entry) return entry;
419
+
420
+ const legacySection = config.monorepos || {};
421
+ return legacySection[repository] || null;
422
+ }
423
+
424
+ /**
425
+ * Gets the configured repository path
397
426
  * @param {Object} config - Configuration object from loadConfig()
398
427
  * @param {string} repository - Repository in "owner/repo" format
399
428
  * @returns {string|null} - Expanded path or null if not configured
400
429
  */
401
- function getMonorepoPath(config, repository) {
402
- const monorepoConfig = config.monorepos?.[repository];
403
- if (monorepoConfig?.path) {
404
- return expandPath(monorepoConfig.path);
430
+ function getRepoPath(config, repository) {
431
+ const repoConfig = getRepoConfig(config, repository);
432
+ if (repoConfig?.path) {
433
+ return expandPath(repoConfig.path);
405
434
  }
406
435
  return null;
407
436
  }
408
437
 
409
438
  /**
410
- * Gets the configured checkout script for a monorepo repository
439
+ * Gets the configured checkout script for a repository
411
440
  * @param {Object} config - Configuration object from loadConfig()
412
441
  * @param {string} repository - Repository in "owner/repo" format
413
442
  * @returns {string|null} - Checkout script path or null if not configured
414
443
  */
415
- function getMonorepoCheckoutScript(config, repository) {
416
- const monorepoConfig = config.monorepos?.[repository];
417
- return monorepoConfig?.checkout_script || null;
444
+ function getRepoCheckoutScript(config, repository) {
445
+ const repoConfig = getRepoConfig(config, repository);
446
+ return repoConfig?.checkout_script || null;
418
447
  }
419
448
 
420
449
  /**
421
- * Gets the configured worktree directory for a monorepo repository
450
+ * Gets the configured worktree directory for a repository
422
451
  * @param {Object} config - Configuration object from loadConfig()
423
452
  * @param {string} repository - Repository in "owner/repo" format
424
453
  * @returns {string|null} - Expanded worktree directory path or null if not configured
425
454
  */
426
- function getMonorepoWorktreeDirectory(config, repository) {
427
- const monorepoConfig = config.monorepos?.[repository];
428
- if (monorepoConfig?.worktree_directory) {
429
- return expandPath(monorepoConfig.worktree_directory);
455
+ function getRepoWorktreeDirectory(config, repository) {
456
+ const repoConfig = getRepoConfig(config, repository);
457
+ if (repoConfig?.worktree_directory) {
458
+ return expandPath(repoConfig.worktree_directory);
430
459
  }
431
460
  return null;
432
461
  }
433
462
 
434
463
  /**
435
- * Gets the configured worktree name template for a monorepo repository
464
+ * Gets the configured worktree name template for a repository
436
465
  * @param {Object} config - Configuration object from loadConfig()
437
466
  * @param {string} repository - Repository in "owner/repo" format
438
467
  * @returns {string|null} - Template string or null if not configured
439
468
  */
440
- function getMonorepoWorktreeNameTemplate(config, repository) {
441
- const monorepoConfig = config.monorepos?.[repository];
442
- return monorepoConfig?.worktree_name_template || null;
469
+ function getRepoWorktreeNameTemplate(config, repository) {
470
+ const repoConfig = getRepoConfig(config, repository);
471
+ return repoConfig?.worktree_name_template || null;
443
472
  }
444
473
 
445
474
  /**
446
- * Gets the configured checkout script timeout for a monorepo repository
475
+ * Computes the display name for a worktree path by deriving the relative
476
+ * path from the configured (or default) worktree base directory.
477
+ * Falls back to the basename when the path lies outside the base directory.
478
+ *
479
+ * @param {string} worktreePath - Absolute path to the worktree
480
+ * @param {Object} config - Configuration object from loadConfig()
481
+ * @param {string} repository - Repository in "owner/repo" format
482
+ * @returns {string|null} - Relative display name (e.g. "abc123/src") or basename fallback
483
+ */
484
+ function getWorktreeDisplayName(worktreePath, config, repository) {
485
+ if (!worktreePath) return null;
486
+ const worktreeBaseDir = getRepoWorktreeDirectory(config, repository)
487
+ || path.join(getConfigDir(), 'worktrees');
488
+ const relativePath = path.relative(worktreeBaseDir, worktreePath);
489
+ if (relativePath.startsWith('..')) {
490
+ return path.basename(worktreePath);
491
+ }
492
+ return relativePath;
493
+ }
494
+
495
+ /**
496
+ * Gets the configured checkout script timeout for a repository
447
497
  * @param {Object} config - Configuration object from loadConfig()
448
498
  * @param {string} repository - Repository in "owner/repo" format
449
499
  * @returns {number} - Timeout in milliseconds (default: 300000 = 5 minutes)
450
500
  */
451
- function getMonorepoCheckoutTimeout(config, repository) {
452
- const monorepoConfig = config.monorepos?.[repository];
453
- if (monorepoConfig?.checkout_timeout_seconds > 0) {
454
- return monorepoConfig.checkout_timeout_seconds * 1000;
501
+ function getRepoCheckoutTimeout(config, repository) {
502
+ const repoConfig = getRepoConfig(config, repository);
503
+ if (repoConfig?.checkout_timeout_seconds > 0) {
504
+ return repoConfig.checkout_timeout_seconds * 1000;
455
505
  }
456
506
  return DEFAULT_CHECKOUT_TIMEOUT_MS; // 5 minutes default
457
507
  }
458
508
 
459
509
  /**
460
- * Resolves all monorepo worktree options for a repository into a single object.
510
+ * Gets the configured reset script for a repository
511
+ * @param {Object} config - Configuration object from loadConfig()
512
+ * @param {string} repository - Repository in "owner/repo" format
513
+ * @returns {string|null} - Reset script path or null if not configured
514
+ */
515
+ function getRepoResetScript(config, repository) {
516
+ const repoConfig = getRepoConfig(config, repository);
517
+ return repoConfig?.reset_script || null;
518
+ }
519
+
520
+ /**
521
+ * Gets the configured pool size for a repository from file config only.
522
+ * Prefer resolvePoolConfig() when DB repo_settings are available.
523
+ * @param {Object} config - Configuration object from loadConfig()
524
+ * @param {string} repository - Repository in "owner/repo" format
525
+ * @returns {number} - Pool size (0 if not configured or invalid)
526
+ */
527
+ function getRepoPoolSize(config, repository) {
528
+ const repoConfig = getRepoConfig(config, repository);
529
+ const size = repoConfig?.pool_size;
530
+ return (typeof size === 'number' && size > 0) ? size : 0;
531
+ }
532
+
533
+ /**
534
+ * Gets the configured pool fetch interval for a repository from file config only.
535
+ * Prefer resolvePoolConfig() when DB repo_settings are available.
536
+ * @param {Object} config - Configuration object from loadConfig()
537
+ * @param {string} repository - Repository in "owner/repo" format
538
+ * @returns {number|null} - Interval in minutes or null if not configured
539
+ */
540
+ function getRepoPoolFetchInterval(config, repository) {
541
+ const repoConfig = getRepoConfig(config, repository);
542
+ const minutes = repoConfig?.pool_fetch_interval_minutes;
543
+ return (typeof minutes === 'number' && minutes > 0) ? minutes : null;
544
+ }
545
+
546
+ /**
547
+ * Gets the configured load_skills setting for a repository from file config.
548
+ * @param {Object} config - Configuration object from loadConfig()
549
+ * @param {string} repository - Repository in "owner/repo" format
550
+ * @returns {boolean|null} - true/false if set, null if not configured
551
+ */
552
+ function getRepoLoadSkills(config, repository) {
553
+ const repoConfig = getRepoConfig(config, repository);
554
+ const val = repoConfig?.load_skills;
555
+ return typeof val === 'boolean' ? val : null;
556
+ }
557
+
558
+ /**
559
+ * Resolves the load_skills setting for a repository, checking DB repo_settings first,
560
+ * then repo JSON config, then provider config. Returns a boolean suitable for passing
561
+ * directly to provider constructors (which check `!== false`).
562
+ *
563
+ * @param {Object} config - Configuration object from loadConfig()
564
+ * @param {string} repository - Repository in "owner/repo" format
565
+ * @param {Object|null} repoSettings - DB repo_settings row (from RepoSettingsRepository.getRepoSettings)
566
+ * @param {boolean} [providerLoadSkills] - Provider-level load_skills from config.providers
567
+ * @returns {boolean} - Resolved load_skills value
568
+ */
569
+ function resolveLoadSkills(config, repository, repoSettings, providerLoadSkills) {
570
+ // Tier 1: DB repo settings (1 = true, 0 = false, null = not set)
571
+ const dbVal = repoSettings?.load_skills;
572
+ if (typeof dbVal === 'number' && (dbVal === 0 || dbVal === 1)) {
573
+ return dbVal === 1;
574
+ }
575
+
576
+ // Tier 2: Repo JSON config (config.repos["owner/repo"].load_skills)
577
+ const repoVal = getRepoLoadSkills(config, repository);
578
+ if (repoVal !== null) {
579
+ return repoVal;
580
+ }
581
+
582
+ // Tier 3: Provider-level config
583
+ if (typeof providerLoadSkills === 'boolean') {
584
+ return providerLoadSkills;
585
+ }
586
+
587
+ // Tier 4: Default
588
+ return true;
589
+ }
590
+
591
+ /**
592
+ * Builds council-mode provider overrides: a shared (tier 1+2) base and a per-provider
593
+ * map that includes tier 3 resolution for each configured provider.
594
+ *
595
+ * @param {Object} config - Configuration object from loadConfig()
596
+ * @param {string} repository - Repository in "owner/repo" format
597
+ * @param {Object|null} repoSettings - DB repo_settings row
598
+ * @returns {{ providerOverrides: Object, providerOverridesMap: Object }}
599
+ */
600
+ function buildCouncilProviderOverrides(config, repository, repoSettings) {
601
+ const baseLoadSkills = resolveLoadSkills(config, repository, repoSettings);
602
+ const providerOverrides = { load_skills: baseLoadSkills };
603
+ const providerOverridesMap = {};
604
+ if (config.providers) {
605
+ for (const [pid, pconf] of Object.entries(config.providers)) {
606
+ providerOverridesMap[pid] = {
607
+ load_skills: resolveLoadSkills(config, repository, repoSettings, pconf?.load_skills)
608
+ };
609
+ }
610
+ }
611
+ return { providerOverrides, providerOverridesMap };
612
+ }
613
+
614
+ /**
615
+ * Resolves pool configuration for a repository, checking DB repo_settings first,
616
+ * then falling back to file config. DB values take precedence when set (non-null).
617
+ * @param {Object} config - Configuration object from loadConfig()
618
+ * @param {string} repository - Repository in "owner/repo" format
619
+ * @param {Object|null} repoSettings - DB repo_settings row (from RepoSettingsRepository.getRepoSettings)
620
+ * @returns {{ poolSize: number, poolFetchIntervalMinutes: number|null }}
621
+ */
622
+ function resolvePoolConfig(config, repository, repoSettings) {
623
+ const dbPoolSize = repoSettings?.pool_size;
624
+ const dbFetchInterval = repoSettings?.pool_fetch_interval_minutes;
625
+
626
+ const poolSize = (typeof dbPoolSize === 'number' && dbPoolSize >= 0)
627
+ ? dbPoolSize
628
+ : getRepoPoolSize(config, repository);
629
+
630
+ const poolFetchIntervalMinutes = (typeof dbFetchInterval === 'number' && dbFetchInterval >= 0)
631
+ ? (dbFetchInterval > 0 ? dbFetchInterval : null)
632
+ : getRepoPoolFetchInterval(config, repository);
633
+
634
+ return { poolSize, poolFetchIntervalMinutes };
635
+ }
636
+
637
+ /**
638
+ * Resolves all repository worktree options into a single object.
461
639
  * Composite helper that combines the individual getters into the shape expected
462
640
  * by GitWorktreeManager and createWorktreeForPR.
463
641
  *
464
642
  * @param {Object} config - Configuration object from loadConfig()
465
643
  * @param {string} repository - Repository in "owner/repo" format
466
- * @returns {{ checkoutScript: string|null, checkoutTimeout: number, worktreeConfig: Object|null }}
644
+ * @param {Object|null} [repoSettings=null] - DB repo_settings row (from RepoSettingsRepository.getRepoSettings)
645
+ * @returns {{ checkoutScript: string|null, checkoutTimeout: number, worktreeConfig: Object|null, resetScript: string|null, poolSize: number, poolFetchIntervalMinutes: number|null }}
467
646
  */
468
- function resolveMonorepoOptions(config, repository) {
469
- const checkoutScript = getMonorepoCheckoutScript(config, repository);
470
- const checkoutTimeout = getMonorepoCheckoutTimeout(config, repository);
471
- const worktreeDirectory = getMonorepoWorktreeDirectory(config, repository);
472
- const nameTemplate = getMonorepoWorktreeNameTemplate(config, repository);
647
+ function resolveRepoOptions(config, repository, repoSettings = null) {
648
+ const checkoutScript = getRepoCheckoutScript(config, repository);
649
+ const checkoutTimeout = getRepoCheckoutTimeout(config, repository);
650
+ const worktreeDirectory = getRepoWorktreeDirectory(config, repository);
651
+ const nameTemplate = getRepoWorktreeNameTemplate(config, repository);
473
652
 
474
653
  let worktreeConfig = null;
475
654
  if (worktreeDirectory || nameTemplate) {
@@ -478,7 +657,10 @@ function resolveMonorepoOptions(config, repository) {
478
657
  if (nameTemplate) worktreeConfig.nameTemplate = nameTemplate;
479
658
  }
480
659
 
481
- return { checkoutScript, checkoutTimeout, worktreeConfig };
660
+ const resetScript = getRepoResetScript(config, repository);
661
+ const { poolSize, poolFetchIntervalMinutes } = resolvePoolConfig(config, repository, repoSettings);
662
+
663
+ return { checkoutScript, checkoutTimeout, worktreeConfig, resetScript, poolSize, poolFetchIntervalMinutes };
482
664
  }
483
665
 
484
666
  /**
@@ -558,12 +740,22 @@ module.exports = {
558
740
  isRunningViaNpx,
559
741
  showWelcomeMessage,
560
742
  expandPath,
561
- getMonorepoPath,
562
- getMonorepoCheckoutScript,
563
- getMonorepoWorktreeDirectory,
564
- getMonorepoWorktreeNameTemplate,
565
- getMonorepoCheckoutTimeout,
566
- resolveMonorepoOptions,
743
+ // New repo-prefixed names
744
+ getRepoConfig,
745
+ getRepoPath,
746
+ getRepoCheckoutScript,
747
+ getRepoWorktreeDirectory,
748
+ getRepoWorktreeNameTemplate,
749
+ getWorktreeDisplayName,
750
+ getRepoCheckoutTimeout,
751
+ resolveRepoOptions,
752
+ getRepoResetScript,
753
+ getRepoPoolSize,
754
+ getRepoPoolFetchInterval,
755
+ getRepoLoadSkills,
756
+ resolvePoolConfig,
757
+ resolveLoadSkills,
758
+ buildCouncilProviderOverrides,
567
759
  resolveDbName,
568
760
  warnIfDevModeWithoutDbName,
569
761
  shouldSkipUpdateNotifier,