gitpadi 2.1.4 → 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 +1 -1
- package/dist/commands/drips.js +181 -44
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/drips.ts +200 -50
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.
|
|
29
|
+
const VERSION = '2.1.6';
|
|
30
30
|
let targetConfirmed = false;
|
|
31
31
|
let gitlabProjectConfirmed = false;
|
|
32
32
|
// ── Styling ────────────────────────────────────────────────────────────
|
package/dist/commands/drips.js
CHANGED
|
@@ -456,13 +456,127 @@ 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
|
-
// ──
|
|
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
|
+
}
|
|
490
|
+
// ── Browse by org + track ─────────────────────────────────────────────────────
|
|
460
491
|
async function browseByTrack(program, profile) {
|
|
461
|
-
//
|
|
492
|
+
// Step 1: ask for org/repo name with search fallback
|
|
493
|
+
const { orgInput } = await inquirer.prompt([{
|
|
494
|
+
type: 'input',
|
|
495
|
+
name: 'orgInput',
|
|
496
|
+
message: bold('Enter org name or repo name to search (e.g. stellar, Inheritx, uniswap):'),
|
|
497
|
+
validate: (v) => v.trim().length > 0 || 'Required',
|
|
498
|
+
}]);
|
|
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
|
+
}
|
|
575
|
+
// Step 2: ask for track
|
|
462
576
|
const { browseTrack } = await inquirer.prompt([{
|
|
463
577
|
type: 'list',
|
|
464
578
|
name: 'browseTrack',
|
|
465
|
-
message: bold('
|
|
579
|
+
message: bold('Filter by track:'),
|
|
466
580
|
default: profile.track,
|
|
467
581
|
choices: [
|
|
468
582
|
{ name: ` ${cyan('⚙️')} Backend`, value: 'backend' },
|
|
@@ -472,40 +586,60 @@ async function browseByTrack(program, profile) {
|
|
|
472
586
|
],
|
|
473
587
|
}]);
|
|
474
588
|
const selectedTrack = browseTrack;
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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();
|
|
592
|
+
const collected = [];
|
|
593
|
+
let apiPage = 1;
|
|
594
|
+
const MAX_SCAN_PAGES = 20;
|
|
595
|
+
try {
|
|
596
|
+
while (apiPage <= MAX_SCAN_PAGES) {
|
|
597
|
+
const res = await fetchIssuePage(program.id, apiPage, 50);
|
|
598
|
+
for (const issue of res.data) {
|
|
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)
|
|
608
|
+
continue;
|
|
609
|
+
if (issue.assignedApplicant !== null)
|
|
610
|
+
continue;
|
|
611
|
+
if (!matchesTrack(issue, selectedTrack))
|
|
612
|
+
continue;
|
|
613
|
+
if (profile.minPoints > 0 && issue.points !== null && issue.points < profile.minPoints)
|
|
614
|
+
continue;
|
|
615
|
+
collected.push(issue);
|
|
616
|
+
}
|
|
617
|
+
spinner.text = dim(` Scanning page ${apiPage}/${MAX_SCAN_PAGES} — ${collected.length} matching issue(s) so far…`);
|
|
618
|
+
if (!res.pagination.hasNextPage)
|
|
619
|
+
break;
|
|
620
|
+
apiPage++;
|
|
506
621
|
}
|
|
622
|
+
}
|
|
623
|
+
catch (e) {
|
|
624
|
+
spinner.fail(` ${e.message}`);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
// Sort by points descending
|
|
628
|
+
collected.sort((a, b) => (b.points || 0) - (a.points || 0));
|
|
629
|
+
if (collected.length === 0) {
|
|
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'));
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
spinner.succeed(` ${bold(collected.length + '')} unassigned ${selectedTrack} issue(s) from ${cyan(displayLabel)} — sorted by points`);
|
|
635
|
+
// Step 4: paginate through results (20 per page)
|
|
636
|
+
const PAGE_SIZE = 20;
|
|
637
|
+
let viewPage = 0;
|
|
638
|
+
while (true) {
|
|
639
|
+
const pageSlice = collected.slice(viewPage * PAGE_SIZE, (viewPage + 1) * PAGE_SIZE);
|
|
640
|
+
const totalPages = Math.ceil(collected.length / PAGE_SIZE);
|
|
507
641
|
console.log();
|
|
508
|
-
const choices =
|
|
642
|
+
const choices = pageSlice.map((issue) => {
|
|
509
643
|
const age = diffDays(issue.updatedAt);
|
|
510
644
|
const ageStr = age === 0 ? dim('today') : dim(`${age}d ago`);
|
|
511
645
|
const pts = issue.points ? green(`+${issue.points}pts`) : dim(' —pts');
|
|
@@ -524,26 +658,29 @@ async function browseByTrack(program, profile) {
|
|
|
524
658
|
const nav = [
|
|
525
659
|
new inquirer.Separator(dim(' ─────────────────────────────────────────────────────')),
|
|
526
660
|
];
|
|
527
|
-
if (
|
|
528
|
-
nav.push({ name: ` ${dim(
|
|
529
|
-
if (
|
|
530
|
-
nav.push({ name: ` ${dim(
|
|
661
|
+
if ((viewPage + 1) < totalPages)
|
|
662
|
+
nav.push({ name: ` ${dim(`→ Next page (${viewPage + 2}/${totalPages})`)}`, value: '__next__' });
|
|
663
|
+
if (viewPage > 0)
|
|
664
|
+
nav.push({ name: ` ${dim(`← Previous page (${viewPage}/${totalPages})`)}`, value: '__prev__' });
|
|
665
|
+
nav.push({ name: ` ${dim('🔍 Search a different org')}`, value: '__reorg__' });
|
|
531
666
|
nav.push({ name: ` ${dim('⬅ Back')}`, value: '__back__' });
|
|
532
667
|
const { selected } = await inquirer.prompt([{
|
|
533
668
|
type: 'list',
|
|
534
669
|
name: 'selected',
|
|
535
|
-
message: bold(`${
|
|
536
|
-
choices:
|
|
537
|
-
pageSize:
|
|
670
|
+
message: bold(`${displayLabel} — ${selectedTrack} issues (${collected.length} total, page ${viewPage + 1}/${totalPages}):`),
|
|
671
|
+
choices: [...choices, ...nav],
|
|
672
|
+
pageSize: 20,
|
|
538
673
|
}]);
|
|
539
674
|
if (selected === '__back__')
|
|
540
675
|
return;
|
|
676
|
+
if (selected === '__reorg__')
|
|
677
|
+
return browseByTrack(program, profile);
|
|
541
678
|
if (selected === '__next__') {
|
|
542
|
-
|
|
679
|
+
viewPage++;
|
|
543
680
|
continue;
|
|
544
681
|
}
|
|
545
682
|
if (selected === '__prev__') {
|
|
546
|
-
|
|
683
|
+
viewPage = Math.max(0, viewPage - 1);
|
|
547
684
|
continue;
|
|
548
685
|
}
|
|
549
686
|
await applySingle(program, selected, profile);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitpadi",
|
|
3
|
-
"version": "2.1.
|
|
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.
|
|
38
|
+
const VERSION = '2.1.6';
|
|
39
39
|
let targetConfirmed = false;
|
|
40
40
|
let gitlabProjectConfirmed = false;
|
|
41
41
|
|
package/src/commands/drips.ts
CHANGED
|
@@ -554,70 +554,218 @@ 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
|
-
// ──
|
|
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
|
+
|
|
594
|
+
// ── Browse by org + track ─────────────────────────────────────────────────────
|
|
558
595
|
|
|
559
596
|
async function browseByTrack(program: DripsProgram, profile: DripsProfile): Promise<void> {
|
|
560
|
-
//
|
|
597
|
+
// Step 1: ask for org/repo name with search fallback
|
|
598
|
+
const { orgInput } = await inquirer.prompt([{
|
|
599
|
+
type: 'input',
|
|
600
|
+
name: 'orgInput',
|
|
601
|
+
message: bold('Enter org name or repo name to search (e.g. stellar, Inheritx, uniswap):'),
|
|
602
|
+
validate: (v: string) => v.trim().length > 0 || 'Required',
|
|
603
|
+
}]);
|
|
604
|
+
|
|
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
|
+
}
|
|
689
|
+
|
|
690
|
+
// Step 2: ask for track
|
|
561
691
|
const { browseTrack } = await inquirer.prompt([{
|
|
562
692
|
type: 'list',
|
|
563
693
|
name: 'browseTrack',
|
|
564
|
-
message: bold('
|
|
694
|
+
message: bold('Filter by track:'),
|
|
565
695
|
default: profile.track,
|
|
566
696
|
choices: [
|
|
567
|
-
{ name: ` ${cyan('⚙️')} Backend`,
|
|
568
|
-
{ name: ` ${magenta('🎨')} Frontend`,
|
|
697
|
+
{ name: ` ${cyan('⚙️')} Backend`, value: 'backend' },
|
|
698
|
+
{ name: ` ${magenta('🎨')} Frontend`, value: 'frontend' },
|
|
569
699
|
{ name: ` ${yellow('📜')} Smart Contract`, value: 'contract' },
|
|
570
|
-
{ name: ` ${green('🌐')} All tracks`,
|
|
700
|
+
{ name: ` ${green('🌐')} All tracks`, value: 'all' },
|
|
571
701
|
],
|
|
572
702
|
}]);
|
|
573
703
|
|
|
574
704
|
const selectedTrack: Track = browseTrack;
|
|
575
|
-
let page = 1;
|
|
576
705
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
let hasNextPage = false;
|
|
581
|
-
let totalFetched = 0;
|
|
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();
|
|
582
709
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
);
|
|
710
|
+
const collected: DripsIssue[] = [];
|
|
711
|
+
let apiPage = 1;
|
|
712
|
+
const MAX_SCAN_PAGES = 20;
|
|
601
713
|
|
|
602
|
-
|
|
603
|
-
|
|
714
|
+
try {
|
|
715
|
+
while (apiPage <= MAX_SCAN_PAGES) {
|
|
716
|
+
const res = await fetchIssuePage(program.id, apiPage, 50);
|
|
717
|
+
|
|
718
|
+
for (const issue of res.data) {
|
|
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
|
+
|
|
729
|
+
if (issue.assignedApplicant !== null) continue;
|
|
730
|
+
if (!matchesTrack(issue, selectedTrack)) continue;
|
|
731
|
+
if (profile.minPoints > 0 && issue.points !== null && issue.points < profile.minPoints) continue;
|
|
732
|
+
collected.push(issue);
|
|
733
|
+
}
|
|
604
734
|
|
|
605
|
-
|
|
735
|
+
spinner.text = dim(` Scanning page ${apiPage}/${MAX_SCAN_PAGES} — ${collected.length} matching issue(s) so far…`);
|
|
606
736
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
);
|
|
610
|
-
} catch (e: any) {
|
|
611
|
-
spinner.fail(` ${e.message}`);
|
|
612
|
-
return;
|
|
737
|
+
if (!res.pagination.hasNextPage) break;
|
|
738
|
+
apiPage++;
|
|
613
739
|
}
|
|
740
|
+
} catch (e: any) {
|
|
741
|
+
spinner.fail(` ${e.message}`);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
614
744
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
745
|
+
// Sort by points descending
|
|
746
|
+
collected.sort((a, b) => (b.points || 0) - (a.points || 0));
|
|
747
|
+
|
|
748
|
+
if (collected.length === 0) {
|
|
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'));
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
spinner.succeed(
|
|
755
|
+
` ${bold(collected.length + '')} unassigned ${selectedTrack} issue(s) from ${cyan(displayLabel)} — sorted by points`
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
// Step 4: paginate through results (20 per page)
|
|
759
|
+
const PAGE_SIZE = 20;
|
|
760
|
+
let viewPage = 0;
|
|
761
|
+
|
|
762
|
+
while (true) {
|
|
763
|
+
const pageSlice = collected.slice(viewPage * PAGE_SIZE, (viewPage + 1) * PAGE_SIZE);
|
|
764
|
+
const totalPages = Math.ceil(collected.length / PAGE_SIZE);
|
|
618
765
|
|
|
619
766
|
console.log();
|
|
620
|
-
|
|
767
|
+
|
|
768
|
+
const choices = pageSlice.map((issue) => {
|
|
621
769
|
const age = diffDays(issue.updatedAt);
|
|
622
770
|
const ageStr = age === 0 ? dim('today') : dim(`${age}d ago`);
|
|
623
771
|
const pts = issue.points ? green(`+${issue.points}pts`) : dim(' —pts');
|
|
@@ -637,21 +785,23 @@ async function browseByTrack(program: DripsProgram, profile: DripsProfile): Prom
|
|
|
637
785
|
const nav: any[] = [
|
|
638
786
|
new inquirer.Separator(dim(' ─────────────────────────────────────────────────────')),
|
|
639
787
|
];
|
|
640
|
-
if (
|
|
641
|
-
if (
|
|
788
|
+
if ((viewPage + 1) < totalPages) nav.push({ name: ` ${dim(`→ Next page (${viewPage + 2}/${totalPages})`)}`, value: '__next__' });
|
|
789
|
+
if (viewPage > 0) nav.push({ name: ` ${dim(`← Previous page (${viewPage}/${totalPages})`)}`, value: '__prev__' });
|
|
790
|
+
nav.push({ name: ` ${dim('🔍 Search a different org')}`, value: '__reorg__' });
|
|
642
791
|
nav.push({ name: ` ${dim('⬅ Back')}`, value: '__back__' });
|
|
643
792
|
|
|
644
793
|
const { selected } = await inquirer.prompt([{
|
|
645
794
|
type: 'list',
|
|
646
795
|
name: 'selected',
|
|
647
|
-
message: bold(`${
|
|
648
|
-
choices:
|
|
649
|
-
pageSize:
|
|
796
|
+
message: bold(`${displayLabel} — ${selectedTrack} issues (${collected.length} total, page ${viewPage + 1}/${totalPages}):`),
|
|
797
|
+
choices: [...choices, ...nav],
|
|
798
|
+
pageSize: 20,
|
|
650
799
|
}]);
|
|
651
800
|
|
|
652
801
|
if (selected === '__back__') return;
|
|
653
|
-
if (selected === '
|
|
654
|
-
if (selected === '
|
|
802
|
+
if (selected === '__reorg__') return browseByTrack(program, profile);
|
|
803
|
+
if (selected === '__next__') { viewPage++; continue; }
|
|
804
|
+
if (selected === '__prev__') { viewPage = Math.max(0, viewPage - 1); continue; }
|
|
655
805
|
|
|
656
806
|
await applySingle(program, selected as DripsIssue, profile);
|
|
657
807
|
}
|