gitpadi 2.1.4 → 2.1.5

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.4';
29
+ const VERSION = '2.1.5';
30
30
  let targetConfirmed = false;
31
31
  let gitlabProjectConfirmed = false;
32
32
  // ── Styling ────────────────────────────────────────────────────────────
@@ -456,13 +456,21 @@ 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
- // ── Browse by track ───────────────────────────────────────────────────────────
459
+ // ── Browse by org + track ─────────────────────────────────────────────────────
460
460
  async function browseByTrack(program, profile) {
461
- // Ask which track to browse if they want to override
461
+ // Step 1: ask for org name
462
+ const { orgInput } = await inquirer.prompt([{
463
+ type: 'input',
464
+ name: 'orgInput',
465
+ message: bold('Enter GitHub org name (e.g. stellar, uniswap, openzeppelin):'),
466
+ validate: (v) => v.trim().length > 0 || 'Required',
467
+ }]);
468
+ const targetOrg = orgInput.trim().toLowerCase();
469
+ // Step 2: ask for track
462
470
  const { browseTrack } = await inquirer.prompt([{
463
471
  type: 'list',
464
472
  name: 'browseTrack',
465
- message: bold('Browse which track?'),
473
+ message: bold('Filter by track:'),
466
474
  default: profile.track,
467
475
  choices: [
468
476
  { name: ` ${cyan('⚙️')} Backend`, value: 'backend' },
@@ -472,40 +480,52 @@ async function browseByTrack(program, profile) {
472
480
  ],
473
481
  }]);
474
482
  const selectedTrack = browseTrack;
475
- let page = 1;
476
- while (true) {
477
- const spinner = ora(dim(` Fetching unassigned issues (page ${page})…`)).start();
478
- let allIssues = [];
479
- let hasNextPage = false;
480
- let totalFetched = 0;
481
- // Fetch a batch and filter client-side for unassigned + track
482
- try {
483
- // Fetch 2 pages worth (100 issues) to ensure we have enough unassigned after filtering
484
- const batchStart = (page - 1) * 2 + 1;
485
- const pageA = await fetchIssuePage(program.id, batchStart, 50);
486
- const pageB = pageA.pagination.hasNextPage
487
- ? await fetchIssuePage(program.id, batchStart + 1, 50)
488
- : { data: [], pagination: { ...pageA.pagination, hasNextPage: false } };
489
- const raw = [...pageA.data, ...pageB.data];
490
- totalFetched = raw.length;
491
- // Filter: unassigned + track + min points
492
- allIssues = raw.filter(i => i.assignedApplicant === null &&
493
- matchesTrack(i, selectedTrack) &&
494
- (profile.minPoints === 0 || i.points === null || i.points >= profile.minPoints));
495
- // Sort by points descending (highest reward first)
496
- allIssues.sort((a, b) => (b.points || 0) - (a.points || 0));
497
- hasNextPage = pageB.pagination.hasNextPage || (pageA.pagination.hasNextPage && !pageB.pagination.hasNextPage);
498
- spinner.succeed(` ${allIssues.length} unassigned ${selectedTrack} issue(s) found (from ${totalFetched} scanned) — sorted by points`);
499
- }
500
- catch (e) {
501
- spinner.fail(` ${e.message}`);
502
- return;
503
- }
504
- if (allIssues.length === 0) {
505
- console.log(yellow(`\n No unassigned ${selectedTrack} issues found on this page. Try next page or change track.\n`));
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();
485
+ const collected = [];
486
+ let apiPage = 1;
487
+ const MAX_SCAN_PAGES = 20; // scan up to 1000 issues to find the org's full list
488
+ try {
489
+ while (apiPage <= MAX_SCAN_PAGES) {
490
+ const res = await fetchIssuePage(program.id, apiPage, 50);
491
+ for (const issue of res.data) {
492
+ const issueOrg = extractOrg(issue);
493
+ if (issueOrg !== targetOrg)
494
+ continue;
495
+ if (issue.assignedApplicant !== null)
496
+ continue;
497
+ if (!matchesTrack(issue, selectedTrack))
498
+ continue;
499
+ if (profile.minPoints > 0 && issue.points !== null && issue.points < profile.minPoints)
500
+ continue;
501
+ collected.push(issue);
502
+ }
503
+ spinner.text = dim(` Scanning page ${apiPage}/${MAX_SCAN_PAGES} found ${collected.length} issues in ${targetOrg} so far…`);
504
+ if (!res.pagination.hasNextPage)
505
+ break;
506
+ apiPage++;
506
507
  }
508
+ }
509
+ catch (e) {
510
+ spinner.fail(` ${e.message}`);
511
+ return;
512
+ }
513
+ // Sort by points descending
514
+ collected.sort((a, b) => (b.points || 0) - (a.points || 0));
515
+ 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'));
518
+ return;
519
+ }
520
+ spinner.succeed(` ${bold(collected.length + '')} unassigned ${selectedTrack} issue(s) from ${cyan(targetOrg)} — sorted by points`);
521
+ // Step 4: paginate through results (20 per page)
522
+ const PAGE_SIZE = 20;
523
+ let viewPage = 0;
524
+ while (true) {
525
+ const pageSlice = collected.slice(viewPage * PAGE_SIZE, (viewPage + 1) * PAGE_SIZE);
526
+ const totalPages = Math.ceil(collected.length / PAGE_SIZE);
507
527
  console.log();
508
- const choices = allIssues.slice(0, 20).map((issue) => {
528
+ const choices = pageSlice.map((issue) => {
509
529
  const age = diffDays(issue.updatedAt);
510
530
  const ageStr = age === 0 ? dim('today') : dim(`${age}d ago`);
511
531
  const pts = issue.points ? green(`+${issue.points}pts`) : dim(' —pts');
@@ -524,26 +544,29 @@ async function browseByTrack(program, profile) {
524
544
  const nav = [
525
545
  new inquirer.Separator(dim(' ─────────────────────────────────────────────────────')),
526
546
  ];
527
- if (hasNextPage)
528
- nav.push({ name: ` ${dim('→ Next page')}`, value: '__next__' });
529
- if (page > 1)
530
- nav.push({ name: ` ${dim('← Previous page')}`, value: '__prev__' });
547
+ if ((viewPage + 1) < totalPages)
548
+ nav.push({ name: ` ${dim(`→ Next page (${viewPage + 2}/${totalPages})`)}`, value: '__next__' });
549
+ if (viewPage > 0)
550
+ nav.push({ name: ` ${dim(`← Previous page (${viewPage}/${totalPages})`)}`, value: '__prev__' });
551
+ nav.push({ name: ` ${dim('🔍 Search a different org')}`, value: '__reorg__' });
531
552
  nav.push({ name: ` ${dim('⬅ Back')}`, value: '__back__' });
532
553
  const { selected } = await inquirer.prompt([{
533
554
  type: 'list',
534
555
  name: 'selected',
535
- message: bold(`${program.name} — ${selectedTrack} issues sorted by points:`),
536
- choices: allIssues.length > 0 ? [...choices, ...nav] : nav,
537
- pageSize: 18,
556
+ message: bold(`${targetOrg} — ${selectedTrack} issues (${collected.length} total, page ${viewPage + 1}/${totalPages}):`),
557
+ choices: [...choices, ...nav],
558
+ pageSize: 20,
538
559
  }]);
539
560
  if (selected === '__back__')
540
561
  return;
562
+ if (selected === '__reorg__')
563
+ return browseByTrack(program, profile);
541
564
  if (selected === '__next__') {
542
- page++;
565
+ viewPage++;
543
566
  continue;
544
567
  }
545
568
  if (selected === '__prev__') {
546
- page = Math.max(1, page - 1);
569
+ viewPage = Math.max(0, viewPage - 1);
547
570
  continue;
548
571
  }
549
572
  await applySingle(program, selected, profile);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitpadi",
3
- "version": "2.1.4",
3
+ "version": "2.1.5",
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.4';
38
+ const VERSION = '2.1.5';
39
39
  let targetConfirmed = false;
40
40
  let gitlabProjectConfirmed = false;
41
41
 
@@ -554,70 +554,89 @@ 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
- // ── Browse by track ───────────────────────────────────────────────────────────
557
+ // ── Browse by org + track ─────────────────────────────────────────────────────
558
558
 
559
559
  async function browseByTrack(program: DripsProgram, profile: DripsProfile): Promise<void> {
560
- // Ask which track to browse if they want to override
560
+ // Step 1: ask for org name
561
+ const { orgInput } = await inquirer.prompt([{
562
+ type: 'input',
563
+ name: 'orgInput',
564
+ message: bold('Enter GitHub org name (e.g. stellar, uniswap, openzeppelin):'),
565
+ validate: (v: string) => v.trim().length > 0 || 'Required',
566
+ }]);
567
+
568
+ const targetOrg = orgInput.trim().toLowerCase();
569
+
570
+ // Step 2: ask for track
561
571
  const { browseTrack } = await inquirer.prompt([{
562
572
  type: 'list',
563
573
  name: 'browseTrack',
564
- message: bold('Browse which track?'),
574
+ message: bold('Filter by track:'),
565
575
  default: profile.track,
566
576
  choices: [
567
- { name: ` ${cyan('⚙️')} Backend`, value: 'backend' },
568
- { name: ` ${magenta('🎨')} Frontend`, value: 'frontend' },
577
+ { name: ` ${cyan('⚙️')} Backend`, value: 'backend' },
578
+ { name: ` ${magenta('🎨')} Frontend`, value: 'frontend' },
569
579
  { name: ` ${yellow('📜')} Smart Contract`, value: 'contract' },
570
- { name: ` ${green('🌐')} All tracks`, value: 'all' },
580
+ { name: ` ${green('🌐')} All tracks`, value: 'all' },
571
581
  ],
572
582
  }]);
573
583
 
574
584
  const selectedTrack: Track = browseTrack;
575
- let page = 1;
576
585
 
577
- while (true) {
578
- const spinner = ora(dim(` Fetching unassigned issues (page ${page})…`)).start();
579
- let allIssues: DripsIssue[] = [];
580
- let hasNextPage = false;
581
- let totalFetched = 0;
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();
582
588
 
583
- // Fetch a batch and filter client-side for unassigned + track
584
- try {
585
- // Fetch 2 pages worth (100 issues) to ensure we have enough unassigned after filtering
586
- const batchStart = (page - 1) * 2 + 1;
587
- const pageA = await fetchIssuePage(program.id, batchStart, 50);
588
- const pageB = pageA.pagination.hasNextPage
589
- ? await fetchIssuePage(program.id, batchStart + 1, 50)
590
- : { data: [], pagination: { ...pageA.pagination, hasNextPage: false } };
591
-
592
- const raw = [...pageA.data, ...pageB.data];
593
- totalFetched = raw.length;
594
-
595
- // Filter: unassigned + track + min points
596
- allIssues = raw.filter(i =>
597
- i.assignedApplicant === null &&
598
- matchesTrack(i, selectedTrack) &&
599
- (profile.minPoints === 0 || i.points === null || i.points >= profile.minPoints)
600
- );
589
+ const collected: DripsIssue[] = [];
590
+ let apiPage = 1;
591
+ const MAX_SCAN_PAGES = 20; // scan up to 1000 issues to find the org's full list
601
592
 
602
- // Sort by points descending (highest reward first)
603
- allIssues.sort((a, b) => (b.points || 0) - (a.points || 0));
593
+ try {
594
+ while (apiPage <= MAX_SCAN_PAGES) {
595
+ const res = await fetchIssuePage(program.id, apiPage, 50);
596
+
597
+ for (const issue of res.data) {
598
+ const issueOrg = extractOrg(issue);
599
+ if (issueOrg !== targetOrg) continue;
600
+ if (issue.assignedApplicant !== null) continue;
601
+ if (!matchesTrack(issue, selectedTrack)) continue;
602
+ if (profile.minPoints > 0 && issue.points !== null && issue.points < profile.minPoints) continue;
603
+ collected.push(issue);
604
+ }
604
605
 
605
- hasNextPage = pageB.pagination.hasNextPage || (pageA.pagination.hasNextPage && !pageB.pagination.hasNextPage);
606
+ spinner.text = dim(` Scanning page ${apiPage}/${MAX_SCAN_PAGES} — found ${collected.length} issues in ${targetOrg} so far…`);
606
607
 
607
- spinner.succeed(
608
- ` ${allIssues.length} unassigned ${selectedTrack} issue(s) found (from ${totalFetched} scanned) — sorted by points`
609
- );
610
- } catch (e: any) {
611
- spinner.fail(` ${e.message}`);
612
- return;
608
+ if (!res.pagination.hasNextPage) break;
609
+ apiPage++;
613
610
  }
611
+ } catch (e: any) {
612
+ spinner.fail(` ${e.message}`);
613
+ return;
614
+ }
614
615
 
615
- if (allIssues.length === 0) {
616
- console.log(yellow(`\n No unassigned ${selectedTrack} issues found on this page. Try next page or change track.\n`));
617
- }
616
+ // Sort by points descending
617
+ collected.sort((a, b) => (b.points || 0) - (a.points || 0));
618
+
619
+ 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'));
622
+ return;
623
+ }
624
+
625
+ spinner.succeed(
626
+ ` ${bold(collected.length + '')} unassigned ${selectedTrack} issue(s) from ${cyan(targetOrg)} — sorted by points`
627
+ );
628
+
629
+ // Step 4: paginate through results (20 per page)
630
+ const PAGE_SIZE = 20;
631
+ let viewPage = 0;
632
+
633
+ while (true) {
634
+ const pageSlice = collected.slice(viewPage * PAGE_SIZE, (viewPage + 1) * PAGE_SIZE);
635
+ const totalPages = Math.ceil(collected.length / PAGE_SIZE);
618
636
 
619
637
  console.log();
620
- const choices = allIssues.slice(0, 20).map((issue) => {
638
+
639
+ const choices = pageSlice.map((issue) => {
621
640
  const age = diffDays(issue.updatedAt);
622
641
  const ageStr = age === 0 ? dim('today') : dim(`${age}d ago`);
623
642
  const pts = issue.points ? green(`+${issue.points}pts`) : dim(' —pts');
@@ -637,21 +656,23 @@ async function browseByTrack(program: DripsProgram, profile: DripsProfile): Prom
637
656
  const nav: any[] = [
638
657
  new inquirer.Separator(dim(' ─────────────────────────────────────────────────────')),
639
658
  ];
640
- if (hasNextPage) nav.push({ name: ` ${dim('→ Next page')}`, value: '__next__' });
641
- if (page > 1) nav.push({ name: ` ${dim('← Previous page')}`, value: '__prev__' });
659
+ if ((viewPage + 1) < totalPages) nav.push({ name: ` ${dim(`→ Next page (${viewPage + 2}/${totalPages})`)}`, value: '__next__' });
660
+ if (viewPage > 0) nav.push({ name: ` ${dim(`← Previous page (${viewPage}/${totalPages})`)}`, value: '__prev__' });
661
+ nav.push({ name: ` ${dim('🔍 Search a different org')}`, value: '__reorg__' });
642
662
  nav.push({ name: ` ${dim('⬅ Back')}`, value: '__back__' });
643
663
 
644
664
  const { selected } = await inquirer.prompt([{
645
665
  type: 'list',
646
666
  name: 'selected',
647
- message: bold(`${program.name} — ${selectedTrack} issues sorted by points:`),
648
- choices: allIssues.length > 0 ? [...choices, ...nav] : nav,
649
- pageSize: 18,
667
+ message: bold(`${targetOrg} — ${selectedTrack} issues (${collected.length} total, page ${viewPage + 1}/${totalPages}):`),
668
+ choices: [...choices, ...nav],
669
+ pageSize: 20,
650
670
  }]);
651
671
 
652
672
  if (selected === '__back__') return;
653
- if (selected === '__next__') { page++; continue; }
654
- if (selected === '__prev__') { page = Math.max(1, page - 1); continue; }
673
+ if (selected === '__reorg__') return browseByTrack(program, profile);
674
+ if (selected === '__next__') { viewPage++; continue; }
675
+ if (selected === '__prev__') { viewPage = Math.max(0, viewPage - 1); continue; }
655
676
 
656
677
  await applySingle(program, selected as DripsIssue, profile);
657
678
  }