gitpadi 2.1.5 → 2.1.6

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/cli.js CHANGED
@@ -26,7 +26,7 @@ import { dripsMenu } from './commands/drips.js';
26
26
  import * as gitlabIssues from './commands/gitlab-issues.js';
27
27
  import * as gitlabMRs from './commands/gitlab-mrs.js';
28
28
  import * as gitlabPipelines from './commands/gitlab-pipelines.js';
29
- const VERSION = '2.1.5';
29
+ const VERSION = '2.1.6';
30
30
  let targetConfirmed = false;
31
31
  let gitlabProjectConfirmed = false;
32
32
  // ── Styling ────────────────────────────────────────────────────────────
@@ -456,16 +456,122 @@ async function autoApply(program, profile) {
456
456
  console.log(bold(` Done: ${green(applied + ' applied')}${skipped > 0 ? ', ' + yellow(skipped + ' skipped (org limit)') : ''}${failed > 0 ? ', ' + red(failed + ' failed') : ''}`));
457
457
  console.log(dim(`\n Track your applications: ${cyan(DRIPS_WEB + '/wave/' + program.slug)}\n`));
458
458
  }
459
+ // ── Repo/org search ───────────────────────────────────────────────────────────
460
+ /**
461
+ * Scans the wave program and returns unique repos whose org or repo name
462
+ * contains the search term (case-insensitive).
463
+ */
464
+ async function searchRepos(program, term) {
465
+ const termLower = term.toLowerCase();
466
+ const repoMap = new Map();
467
+ const MAX_SEARCH_PAGES = 20;
468
+ for (let p = 1; p <= MAX_SEARCH_PAGES; p++) {
469
+ const res = await fetchIssuePage(program.id, p, 50);
470
+ for (const issue of res.data) {
471
+ if (!issue.repo)
472
+ continue;
473
+ const fullName = issue.repo.fullName.toLowerCase();
474
+ const [org, repo] = fullName.split('/');
475
+ if (!org.includes(termLower) && !repo.includes(termLower))
476
+ continue;
477
+ const existing = repoMap.get(fullName);
478
+ if (existing) {
479
+ existing.issueCount++;
480
+ }
481
+ else {
482
+ repoMap.set(fullName, { org, repo, fullName, issueCount: 1 });
483
+ }
484
+ }
485
+ if (!res.pagination.hasNextPage)
486
+ break;
487
+ }
488
+ return Array.from(repoMap.values()).sort((a, b) => b.issueCount - a.issueCount);
489
+ }
459
490
  // ── Browse by org + track ─────────────────────────────────────────────────────
460
491
  async function browseByTrack(program, profile) {
461
- // Step 1: ask for org name
492
+ // Step 1: ask for org/repo name with search fallback
462
493
  const { orgInput } = await inquirer.prompt([{
463
494
  type: 'input',
464
495
  name: 'orgInput',
465
- message: bold('Enter GitHub org name (e.g. stellar, uniswap, openzeppelin):'),
496
+ message: bold('Enter org name or repo name to search (e.g. stellar, Inheritx, uniswap):'),
466
497
  validate: (v) => v.trim().length > 0 || 'Required',
467
498
  }]);
468
- const targetOrg = orgInput.trim().toLowerCase();
499
+ const term = orgInput.trim();
500
+ // Search for matching repos in the wave program
501
+ const searchSpinner = ora(dim(` Searching for "${term}" in ${program.name} wave…`)).start();
502
+ let matches;
503
+ try {
504
+ matches = await searchRepos(program, term);
505
+ }
506
+ catch (e) {
507
+ searchSpinner.fail(` Search failed: ${e.message}`);
508
+ return;
509
+ }
510
+ if (matches.length === 0) {
511
+ searchSpinner.warn(yellow(` No repos found matching "${term}" in the ${program.name} wave.`));
512
+ console.log(dim(' Try a shorter or different search term.\n'));
513
+ return browseByTrack(program, profile);
514
+ }
515
+ searchSpinner.succeed(` Found ${matches.length} repo(s) matching "${term}"`);
516
+ console.log();
517
+ // If exact single match, use it directly — otherwise let user pick
518
+ // Value is the full repo name (org/repo) so we can filter precisely
519
+ let targetFullName; // e.g. "inheritx/smart-contracts" or "stellar" (org-level)
520
+ let displayLabel;
521
+ const exactOrg = matches.find(m => m.org === term.toLowerCase());
522
+ const exactRepo = matches.find(m => m.fullName === term.toLowerCase());
523
+ if (matches.length === 1) {
524
+ targetFullName = matches[0].fullName;
525
+ displayLabel = matches[0].fullName;
526
+ console.log(dim(` Using repo: ${cyan(displayLabel)}\n`));
527
+ }
528
+ else if (exactRepo) {
529
+ targetFullName = exactRepo.fullName;
530
+ displayLabel = exactRepo.fullName;
531
+ console.log(dim(` Using repo: ${cyan(displayLabel)}\n`));
532
+ }
533
+ else if (exactOrg) {
534
+ // Exact org match — show all repos under that org
535
+ targetFullName = exactOrg.org; // org-level (prefix match)
536
+ displayLabel = exactOrg.org + '/*';
537
+ console.log(dim(` Using org: ${cyan(exactOrg.org)} (all repos)\n`));
538
+ }
539
+ else {
540
+ // Multiple partial matches — let user pick repo or entire org
541
+ const repoChoices = [];
542
+ // Group by org: if org has multiple repos, offer org-level option first
543
+ const orgs = [...new Set(matches.map(m => m.org))];
544
+ for (const org of orgs) {
545
+ const orgRepos = matches.filter(m => m.org === org);
546
+ if (orgRepos.length > 1) {
547
+ const total = orgRepos.reduce((s, r) => s + r.issueCount, 0);
548
+ repoChoices.push({
549
+ name: ` ${cyan(org + '/*')} ${dim(`all ${orgRepos.length} repos · ${total} issues`)}`,
550
+ value: org,
551
+ short: org + '/*',
552
+ });
553
+ }
554
+ for (const r of orgRepos) {
555
+ repoChoices.push({
556
+ name: ` ${dim('└')} ${cyan(r.fullName)} ${dim(r.issueCount + ' issue(s)')}`,
557
+ value: r.fullName,
558
+ short: r.fullName,
559
+ });
560
+ }
561
+ }
562
+ repoChoices.push(new inquirer.Separator(dim(' ───────────────────────────')), { name: ` ${dim('🔍 Search again')}`, value: '__search__', short: 'search' });
563
+ const { picked } = await inquirer.prompt([{
564
+ type: 'list',
565
+ name: 'picked',
566
+ message: bold('Pick the org or repo to browse:'),
567
+ choices: repoChoices,
568
+ pageSize: 18,
569
+ }]);
570
+ if (picked === '__search__')
571
+ return browseByTrack(program, profile);
572
+ targetFullName = picked;
573
+ displayLabel = picked.includes('/') && !picked.endsWith('/*') ? picked : picked + '/*';
574
+ }
469
575
  // Step 2: ask for track
470
576
  const { browseTrack } = await inquirer.prompt([{
471
577
  type: 'list',
@@ -480,17 +586,25 @@ async function browseByTrack(program, profile) {
480
586
  ],
481
587
  }]);
482
588
  const selectedTrack = browseTrack;
483
- // Step 3: scan the wave program and collect ALL unassigned issues from this org
484
- const spinner = ora(dim(` Scanning for unassigned ${selectedTrack} issues in ${bold(targetOrg)}…`)).start();
589
+ // Step 3: scan and collect match by exact repo fullName or org prefix
590
+ const isOrgLevel = !targetFullName.includes('/'); // "stellar" vs "stellar/js-stellar-sdk"
591
+ const spinner = ora(dim(` Scanning ${program.name} for unassigned ${selectedTrack} issues in ${bold(displayLabel)}…`)).start();
485
592
  const collected = [];
486
593
  let apiPage = 1;
487
- const MAX_SCAN_PAGES = 20; // scan up to 1000 issues to find the org's full list
594
+ const MAX_SCAN_PAGES = 20;
488
595
  try {
489
596
  while (apiPage <= MAX_SCAN_PAGES) {
490
597
  const res = await fetchIssuePage(program.id, apiPage, 50);
491
598
  for (const issue of res.data) {
492
- const issueOrg = extractOrg(issue);
493
- if (issueOrg !== targetOrg)
599
+ if (!issue.repo)
600
+ continue;
601
+ const issueFullName = issue.repo.fullName.toLowerCase();
602
+ const issueOrg = issueFullName.split('/')[0];
603
+ // Match: exact repo OR org-level (all repos under that org)
604
+ const matches = isOrgLevel
605
+ ? issueOrg === targetFullName
606
+ : issueFullName === targetFullName;
607
+ if (!matches)
494
608
  continue;
495
609
  if (issue.assignedApplicant !== null)
496
610
  continue;
@@ -500,7 +614,7 @@ async function browseByTrack(program, profile) {
500
614
  continue;
501
615
  collected.push(issue);
502
616
  }
503
- spinner.text = dim(` Scanning page ${apiPage}/${MAX_SCAN_PAGES} — found ${collected.length} issues in ${targetOrg} so far…`);
617
+ spinner.text = dim(` Scanning page ${apiPage}/${MAX_SCAN_PAGES} — ${collected.length} matching issue(s) so far…`);
504
618
  if (!res.pagination.hasNextPage)
505
619
  break;
506
620
  apiPage++;
@@ -513,11 +627,11 @@ async function browseByTrack(program, profile) {
513
627
  // Sort by points descending
514
628
  collected.sort((a, b) => (b.points || 0) - (a.points || 0));
515
629
  if (collected.length === 0) {
516
- spinner.warn(yellow(` No unassigned ${selectedTrack} issues found for org "${targetOrg}" in the ${program.name} wave.`));
517
- console.log(dim(' Check the org name matches exactly (e.g. "stellar" not "Stellar").\n'));
630
+ spinner.warn(yellow(` No unassigned ${selectedTrack} issues found for "${displayLabel}" in the ${program.name} wave.`));
631
+ console.log(dim(' Try a different track, lower min points, or search again.\n'));
518
632
  return;
519
633
  }
520
- spinner.succeed(` ${bold(collected.length + '')} unassigned ${selectedTrack} issue(s) from ${cyan(targetOrg)} — sorted by points`);
634
+ spinner.succeed(` ${bold(collected.length + '')} unassigned ${selectedTrack} issue(s) from ${cyan(displayLabel)} — sorted by points`);
521
635
  // Step 4: paginate through results (20 per page)
522
636
  const PAGE_SIZE = 20;
523
637
  let viewPage = 0;
@@ -553,7 +667,7 @@ async function browseByTrack(program, profile) {
553
667
  const { selected } = await inquirer.prompt([{
554
668
  type: 'list',
555
669
  name: 'selected',
556
- message: bold(`${targetOrg} — ${selectedTrack} issues (${collected.length} total, page ${viewPage + 1}/${totalPages}):`),
670
+ message: bold(`${displayLabel} — ${selectedTrack} issues (${collected.length} total, page ${viewPage + 1}/${totalPages}):`),
557
671
  choices: [...choices, ...nav],
558
672
  pageSize: 20,
559
673
  }]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitpadi",
3
- "version": "2.1.5",
3
+ "version": "2.1.6",
4
4
  "description": "GitPadi — AI-powered GitHub & GitLab management CLI. Fork repos, manage issues & PRs, score contributors, grade assignments, and automate everything. Powered by Anthropic Claude via GitLab Duo Agent Platform.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
@@ -35,7 +35,7 @@ import * as gitlabIssues from './commands/gitlab-issues.js';
35
35
  import * as gitlabMRs from './commands/gitlab-mrs.js';
36
36
  import * as gitlabPipelines from './commands/gitlab-pipelines.js';
37
37
 
38
- const VERSION = '2.1.5';
38
+ const VERSION = '2.1.6';
39
39
  let targetConfirmed = false;
40
40
  let gitlabProjectConfirmed = false;
41
41
 
@@ -554,18 +554,138 @@ async function autoApply(program: DripsProgram, profile: DripsProfile): Promise<
554
554
  console.log(dim(`\n Track your applications: ${cyan(DRIPS_WEB + '/wave/' + program.slug)}\n`));
555
555
  }
556
556
 
557
+ // ── Repo/org search ───────────────────────────────────────────────────────────
558
+
559
+ /**
560
+ * Scans the wave program and returns unique repos whose org or repo name
561
+ * contains the search term (case-insensitive).
562
+ */
563
+ async function searchRepos(
564
+ program: DripsProgram,
565
+ term: string,
566
+ ): Promise<Array<{ org: string; repo: string; fullName: string; issueCount: number }>> {
567
+ const termLower = term.toLowerCase();
568
+ const repoMap = new Map<string, { org: string; repo: string; fullName: string; issueCount: number }>();
569
+ const MAX_SEARCH_PAGES = 20;
570
+
571
+ for (let p = 1; p <= MAX_SEARCH_PAGES; p++) {
572
+ const res = await fetchIssuePage(program.id, p, 50);
573
+
574
+ for (const issue of res.data) {
575
+ if (!issue.repo) continue;
576
+ const fullName = issue.repo.fullName.toLowerCase();
577
+ const [org, repo] = fullName.split('/');
578
+ if (!org.includes(termLower) && !repo.includes(termLower)) continue;
579
+
580
+ const existing = repoMap.get(fullName);
581
+ if (existing) {
582
+ existing.issueCount++;
583
+ } else {
584
+ repoMap.set(fullName, { org, repo, fullName, issueCount: 1 });
585
+ }
586
+ }
587
+
588
+ if (!res.pagination.hasNextPage) break;
589
+ }
590
+
591
+ return Array.from(repoMap.values()).sort((a, b) => b.issueCount - a.issueCount);
592
+ }
593
+
557
594
  // ── Browse by org + track ─────────────────────────────────────────────────────
558
595
 
559
596
  async function browseByTrack(program: DripsProgram, profile: DripsProfile): Promise<void> {
560
- // Step 1: ask for org name
597
+ // Step 1: ask for org/repo name with search fallback
561
598
  const { orgInput } = await inquirer.prompt([{
562
599
  type: 'input',
563
600
  name: 'orgInput',
564
- message: bold('Enter GitHub org name (e.g. stellar, uniswap, openzeppelin):'),
601
+ message: bold('Enter org name or repo name to search (e.g. stellar, Inheritx, uniswap):'),
565
602
  validate: (v: string) => v.trim().length > 0 || 'Required',
566
603
  }]);
567
604
 
568
- const targetOrg = orgInput.trim().toLowerCase();
605
+ const term = orgInput.trim();
606
+
607
+ // Search for matching repos in the wave program
608
+ const searchSpinner = ora(dim(` Searching for "${term}" in ${program.name} wave…`)).start();
609
+ let matches: Array<{ org: string; repo: string; fullName: string; issueCount: number }>;
610
+ try {
611
+ matches = await searchRepos(program, term);
612
+ } catch (e: any) {
613
+ searchSpinner.fail(` Search failed: ${e.message}`);
614
+ return;
615
+ }
616
+
617
+ if (matches.length === 0) {
618
+ searchSpinner.warn(yellow(` No repos found matching "${term}" in the ${program.name} wave.`));
619
+ console.log(dim(' Try a shorter or different search term.\n'));
620
+ return browseByTrack(program, profile);
621
+ }
622
+
623
+ searchSpinner.succeed(` Found ${matches.length} repo(s) matching "${term}"`);
624
+ console.log();
625
+
626
+ // If exact single match, use it directly — otherwise let user pick
627
+ // Value is the full repo name (org/repo) so we can filter precisely
628
+ let targetFullName: string; // e.g. "inheritx/smart-contracts" or "stellar" (org-level)
629
+ let displayLabel: string;
630
+
631
+ const exactOrg = matches.find(m => m.org === term.toLowerCase());
632
+ const exactRepo = matches.find(m => m.fullName === term.toLowerCase());
633
+
634
+ if (matches.length === 1) {
635
+ targetFullName = matches[0].fullName;
636
+ displayLabel = matches[0].fullName;
637
+ console.log(dim(` Using repo: ${cyan(displayLabel)}\n`));
638
+ } else if (exactRepo) {
639
+ targetFullName = exactRepo.fullName;
640
+ displayLabel = exactRepo.fullName;
641
+ console.log(dim(` Using repo: ${cyan(displayLabel)}\n`));
642
+ } else if (exactOrg) {
643
+ // Exact org match — show all repos under that org
644
+ targetFullName = exactOrg.org; // org-level (prefix match)
645
+ displayLabel = exactOrg.org + '/*';
646
+ console.log(dim(` Using org: ${cyan(exactOrg.org)} (all repos)\n`));
647
+ } else {
648
+ // Multiple partial matches — let user pick repo or entire org
649
+ const repoChoices: any[] = [];
650
+
651
+ // Group by org: if org has multiple repos, offer org-level option first
652
+ const orgs = [...new Set(matches.map(m => m.org))];
653
+ for (const org of orgs) {
654
+ const orgRepos = matches.filter(m => m.org === org);
655
+ if (orgRepos.length > 1) {
656
+ const total = orgRepos.reduce((s, r) => s + r.issueCount, 0);
657
+ repoChoices.push({
658
+ name: ` ${cyan(org + '/*')} ${dim(`all ${orgRepos.length} repos · ${total} issues`)}`,
659
+ value: org,
660
+ short: org + '/*',
661
+ });
662
+ }
663
+ for (const r of orgRepos) {
664
+ repoChoices.push({
665
+ name: ` ${dim('└')} ${cyan(r.fullName)} ${dim(r.issueCount + ' issue(s)')}`,
666
+ value: r.fullName,
667
+ short: r.fullName,
668
+ });
669
+ }
670
+ }
671
+
672
+ repoChoices.push(
673
+ new inquirer.Separator(dim(' ───────────────────────────')) as any,
674
+ { name: ` ${dim('🔍 Search again')}`, value: '__search__', short: 'search' },
675
+ );
676
+
677
+ const { picked } = await inquirer.prompt([{
678
+ type: 'list',
679
+ name: 'picked',
680
+ message: bold('Pick the org or repo to browse:'),
681
+ choices: repoChoices,
682
+ pageSize: 18,
683
+ }]);
684
+
685
+ if (picked === '__search__') return browseByTrack(program, profile);
686
+ targetFullName = picked;
687
+ displayLabel = picked.includes('/') && !picked.endsWith('/*') ? picked : picked + '/*';
688
+ }
569
689
 
570
690
  // Step 2: ask for track
571
691
  const { browseTrack } = await inquirer.prompt([{
@@ -583,27 +703,36 @@ async function browseByTrack(program: DripsProgram, profile: DripsProfile): Prom
583
703
 
584
704
  const selectedTrack: Track = browseTrack;
585
705
 
586
- // Step 3: scan the wave program and collect ALL unassigned issues from this org
587
- const spinner = ora(dim(` Scanning for unassigned ${selectedTrack} issues in ${bold(targetOrg)}…`)).start();
706
+ // Step 3: scan and collect match by exact repo fullName or org prefix
707
+ const isOrgLevel = !targetFullName.includes('/'); // "stellar" vs "stellar/js-stellar-sdk"
708
+ const spinner = ora(dim(` Scanning ${program.name} for unassigned ${selectedTrack} issues in ${bold(displayLabel)}…`)).start();
588
709
 
589
710
  const collected: DripsIssue[] = [];
590
711
  let apiPage = 1;
591
- const MAX_SCAN_PAGES = 20; // scan up to 1000 issues to find the org's full list
712
+ const MAX_SCAN_PAGES = 20;
592
713
 
593
714
  try {
594
715
  while (apiPage <= MAX_SCAN_PAGES) {
595
716
  const res = await fetchIssuePage(program.id, apiPage, 50);
596
717
 
597
718
  for (const issue of res.data) {
598
- const issueOrg = extractOrg(issue);
599
- if (issueOrg !== targetOrg) continue;
719
+ if (!issue.repo) continue;
720
+ const issueFullName = issue.repo.fullName.toLowerCase();
721
+ const issueOrg = issueFullName.split('/')[0];
722
+
723
+ // Match: exact repo OR org-level (all repos under that org)
724
+ const matches = isOrgLevel
725
+ ? issueOrg === targetFullName
726
+ : issueFullName === targetFullName;
727
+ if (!matches) continue;
728
+
600
729
  if (issue.assignedApplicant !== null) continue;
601
730
  if (!matchesTrack(issue, selectedTrack)) continue;
602
731
  if (profile.minPoints > 0 && issue.points !== null && issue.points < profile.minPoints) continue;
603
732
  collected.push(issue);
604
733
  }
605
734
 
606
- spinner.text = dim(` Scanning page ${apiPage}/${MAX_SCAN_PAGES} — found ${collected.length} issues in ${targetOrg} so far…`);
735
+ spinner.text = dim(` Scanning page ${apiPage}/${MAX_SCAN_PAGES} — ${collected.length} matching issue(s) so far…`);
607
736
 
608
737
  if (!res.pagination.hasNextPage) break;
609
738
  apiPage++;
@@ -617,13 +746,13 @@ async function browseByTrack(program: DripsProgram, profile: DripsProfile): Prom
617
746
  collected.sort((a, b) => (b.points || 0) - (a.points || 0));
618
747
 
619
748
  if (collected.length === 0) {
620
- spinner.warn(yellow(` No unassigned ${selectedTrack} issues found for org "${targetOrg}" in the ${program.name} wave.`));
621
- console.log(dim(' Check the org name matches exactly (e.g. "stellar" not "Stellar").\n'));
749
+ spinner.warn(yellow(` No unassigned ${selectedTrack} issues found for "${displayLabel}" in the ${program.name} wave.`));
750
+ console.log(dim(' Try a different track, lower min points, or search again.\n'));
622
751
  return;
623
752
  }
624
753
 
625
754
  spinner.succeed(
626
- ` ${bold(collected.length + '')} unassigned ${selectedTrack} issue(s) from ${cyan(targetOrg)} — sorted by points`
755
+ ` ${bold(collected.length + '')} unassigned ${selectedTrack} issue(s) from ${cyan(displayLabel)} — sorted by points`
627
756
  );
628
757
 
629
758
  // Step 4: paginate through results (20 per page)
@@ -664,7 +793,7 @@ async function browseByTrack(program: DripsProgram, profile: DripsProfile): Prom
664
793
  const { selected } = await inquirer.prompt([{
665
794
  type: 'list',
666
795
  name: 'selected',
667
- message: bold(`${targetOrg} — ${selectedTrack} issues (${collected.length} total, page ${viewPage + 1}/${totalPages}):`),
796
+ message: bold(`${displayLabel} — ${selectedTrack} issues (${collected.length} total, page ${viewPage + 1}/${totalPages}):`),
668
797
  choices: [...choices, ...nav],
669
798
  pageSize: 20,
670
799
  }]);