gitpadi 2.1.5 → 2.1.8
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 +5 -1
- package/dist/commands/drips.js +122 -16
- package/dist/commands/issues.js +1 -1
- package/dist/remind-contributors.js +5 -2
- package/package.json +1 -1
- package/src/cli.ts +4 -1
- package/src/commands/drips.ts +154 -15
- package/src/commands/issues.ts +1 -1
- package/src/remind-contributors.ts +5 -2
package/dist/cli.js
CHANGED
|
@@ -23,10 +23,11 @@ import * as contribute from './commands/contribute.js';
|
|
|
23
23
|
import * as applyForIssue from './commands/apply-for-issue.js';
|
|
24
24
|
import { runBountyHunter } from './commands/bounty-hunter.js';
|
|
25
25
|
import { dripsMenu } from './commands/drips.js';
|
|
26
|
+
import { remindContributors } from './remind-contributors.js';
|
|
26
27
|
import * as gitlabIssues from './commands/gitlab-issues.js';
|
|
27
28
|
import * as gitlabMRs from './commands/gitlab-mrs.js';
|
|
28
29
|
import * as gitlabPipelines from './commands/gitlab-pipelines.js';
|
|
29
|
-
const VERSION = '2.1.
|
|
30
|
+
const VERSION = '2.1.6';
|
|
30
31
|
let targetConfirmed = false;
|
|
31
32
|
let gitlabProjectConfirmed = false;
|
|
32
33
|
// ── Styling ────────────────────────────────────────────────────────────
|
|
@@ -1075,6 +1076,7 @@ async function maintainerMenu() {
|
|
|
1075
1076
|
{ name: `${green('📦')} ${bold('Repositories')} ${dim('— create, delete, clone, topics, info')}`, value: 'repos' },
|
|
1076
1077
|
{ name: `${red('🚀')} ${bold('Releases')} ${dim('— create, list, tag releases')}`, value: 'releases' },
|
|
1077
1078
|
{ name: `${green('🎯')} ${bold('Bounty Hunter')} ${dim('— auto-apply to Drips Wave & GrantFox')}`, value: 'hunt' },
|
|
1079
|
+
{ name: `${cyan('🔔')} ${bold('Remind Contributors')} ${dim('— warn assignees with no PR (12h)')}`, value: 'remind' },
|
|
1078
1080
|
new inquirer.Separator(dim(' ─────────────────────────────────────────')),
|
|
1079
1081
|
{ name: `${dim('⚙️')} ${dim('Switch Repo')}`, value: 'switch' },
|
|
1080
1082
|
{ name: `${dim('⬅')} ${dim('Back to Mode Selector')}`, value: 'back' },
|
|
@@ -1100,6 +1102,8 @@ async function maintainerMenu() {
|
|
|
1100
1102
|
await safeMenu(releaseMenu);
|
|
1101
1103
|
else if (category === 'hunt')
|
|
1102
1104
|
await runBountyHunter({ platform: 'all', maxApplications: 3, dryRun: false });
|
|
1105
|
+
else if (category === 'remind')
|
|
1106
|
+
await safeMenu(remindContributors);
|
|
1103
1107
|
else if (category === 'review-merge') {
|
|
1104
1108
|
await ensureTargetRepo();
|
|
1105
1109
|
const { prNum } = await ask([{ type: 'input', name: 'prNum', message: cyan('PR number to review:') }]);
|
package/dist/commands/drips.js
CHANGED
|
@@ -218,6 +218,19 @@ export async function ensureDripsAuth() {
|
|
|
218
218
|
else {
|
|
219
219
|
console.log(dim(`\n Open manually: ${cyan(DRIPS_WEB + '/wave/login')}\n`));
|
|
220
220
|
}
|
|
221
|
+
console.log(dim(' After logging in, get your token:'));
|
|
222
|
+
console.log();
|
|
223
|
+
console.log(dim(' 1. Press ') + bold('F12') + dim(' → click the ') + bold('Console') + dim(' tab'));
|
|
224
|
+
console.log(dim(' 2. Paste this command and press ') + bold('Enter') + dim(':'));
|
|
225
|
+
console.log();
|
|
226
|
+
console.log(' ' + cyan('document.cookie.match(/wave_access_token=([^;]+)/)?.[1]'));
|
|
227
|
+
console.log();
|
|
228
|
+
console.log(dim(' 3. Copy the value shown (starts with ') + yellow('eyJ...') + dim(')'));
|
|
229
|
+
console.log();
|
|
230
|
+
console.log(dim(' ── If Console shows "undefined", use this instead: ──────────'));
|
|
231
|
+
console.log(dim(' F12 → Application tab → Cookies → www.drips.network'));
|
|
232
|
+
console.log(dim(' Find ') + yellow('wave_access_token') + dim(' → copy the Value column'));
|
|
233
|
+
console.log();
|
|
221
234
|
const { token } = await inquirer.prompt([{
|
|
222
235
|
type: 'password',
|
|
223
236
|
name: 'token',
|
|
@@ -456,16 +469,113 @@ async function autoApply(program, profile) {
|
|
|
456
469
|
console.log(bold(` Done: ${green(applied + ' applied')}${skipped > 0 ? ', ' + yellow(skipped + ' skipped (org limit)') : ''}${failed > 0 ? ', ' + red(failed + ' failed') : ''}`));
|
|
457
470
|
console.log(dim(`\n Track your applications: ${cyan(DRIPS_WEB + '/wave/' + program.slug)}\n`));
|
|
458
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* Uses the Drips repos API to search by org/repo name — fast, covers ALL repos.
|
|
474
|
+
*/
|
|
475
|
+
async function searchRepos(program, term) {
|
|
476
|
+
const data = await dripsGet(`/api/wave-programs/${program.id}/repos?search=${encodeURIComponent(term)}&limit=20&status=approved`);
|
|
477
|
+
const items = Array.isArray(data?.data) ? data.data : (data ? [data] : []);
|
|
478
|
+
return items.map((item) => ({
|
|
479
|
+
orgId: item.org?.id || '',
|
|
480
|
+
orgLogin: item.org?.gitHubOrgLogin || '',
|
|
481
|
+
repoFullName: item.repo?.gitHubRepoFullName || '',
|
|
482
|
+
repoName: item.repo?.gitHubRepoName || '',
|
|
483
|
+
issueCount: item.issueCount || 0,
|
|
484
|
+
description: item.description || '',
|
|
485
|
+
})).filter(r => r.orgId && r.repoFullName);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Fetch issues filtered by Drips orgId — returns all issues for that org directly.
|
|
489
|
+
*/
|
|
490
|
+
async function getIssuesByOrg(programId, orgId, page, limit = 50) {
|
|
491
|
+
return dripsGet(`/api/issues?waveProgramId=${programId}&orgId=${orgId}&state=open&page=${page}&limit=${limit}&sortBy=points`);
|
|
492
|
+
}
|
|
459
493
|
// ── Browse by org + track ─────────────────────────────────────────────────────
|
|
460
494
|
async function browseByTrack(program, profile) {
|
|
461
|
-
// Step 1: ask for org name
|
|
495
|
+
// Step 1: ask for org/repo name with search fallback
|
|
462
496
|
const { orgInput } = await inquirer.prompt([{
|
|
463
497
|
type: 'input',
|
|
464
498
|
name: 'orgInput',
|
|
465
|
-
message: bold('Enter
|
|
499
|
+
message: bold('Enter org name or repo name to search (e.g. stellar, Inheritx, uniswap):'),
|
|
466
500
|
validate: (v) => v.trim().length > 0 || 'Required',
|
|
467
501
|
}]);
|
|
468
|
-
const
|
|
502
|
+
const term = orgInput.trim();
|
|
503
|
+
// Search for matching repos in the wave program
|
|
504
|
+
const searchSpinner = ora(dim(` Searching for "${term}" in ${program.name} wave…`)).start();
|
|
505
|
+
let matches;
|
|
506
|
+
try {
|
|
507
|
+
matches = await searchRepos(program, term);
|
|
508
|
+
}
|
|
509
|
+
catch (e) {
|
|
510
|
+
searchSpinner.fail(` Search failed: ${e.message}`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (matches.length === 0) {
|
|
514
|
+
searchSpinner.warn(yellow(` No repos found matching "${term}" in the ${program.name} wave.`));
|
|
515
|
+
console.log(dim(' Try a shorter or different search term.\n'));
|
|
516
|
+
return browseByTrack(program, profile);
|
|
517
|
+
}
|
|
518
|
+
searchSpinner.succeed(` Found ${matches.length} repo(s) matching "${term}"`);
|
|
519
|
+
console.log();
|
|
520
|
+
let targetOrgId;
|
|
521
|
+
let displayLabel;
|
|
522
|
+
const exactOrg = matches.find(m => m.orgLogin.toLowerCase() === term.toLowerCase());
|
|
523
|
+
const exactRepo = matches.find(m => m.repoFullName.toLowerCase() === term.toLowerCase());
|
|
524
|
+
if (matches.length === 1) {
|
|
525
|
+
targetOrgId = matches[0].orgId;
|
|
526
|
+
displayLabel = matches[0].repoFullName;
|
|
527
|
+
console.log(dim(` Using repo: ${cyan(displayLabel)}\n`));
|
|
528
|
+
}
|
|
529
|
+
else if (exactRepo) {
|
|
530
|
+
targetOrgId = exactRepo.orgId;
|
|
531
|
+
displayLabel = exactRepo.repoFullName;
|
|
532
|
+
console.log(dim(` Using repo: ${cyan(displayLabel)}\n`));
|
|
533
|
+
}
|
|
534
|
+
else if (exactOrg) {
|
|
535
|
+
targetOrgId = exactOrg.orgId;
|
|
536
|
+
displayLabel = exactOrg.orgLogin + '/*';
|
|
537
|
+
console.log(dim(` Using org: ${cyan(exactOrg.orgLogin)} (all repos)\n`));
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
// Multiple partial matches — let user pick
|
|
541
|
+
const repoChoices = [];
|
|
542
|
+
const orgGroups = new Map();
|
|
543
|
+
for (const r of matches) {
|
|
544
|
+
const list = orgGroups.get(r.orgLogin) || [];
|
|
545
|
+
list.push(r);
|
|
546
|
+
orgGroups.set(r.orgLogin, list);
|
|
547
|
+
}
|
|
548
|
+
for (const [org, orgRepos] of orgGroups) {
|
|
549
|
+
if (orgRepos.length > 1) {
|
|
550
|
+
const total = orgRepos.reduce((s, r) => s + r.issueCount, 0);
|
|
551
|
+
repoChoices.push({
|
|
552
|
+
name: ` ${cyan(org + '/*')} ${dim(`all ${orgRepos.length} repos · ${total} issues`)}`,
|
|
553
|
+
value: orgRepos[0].orgId,
|
|
554
|
+
short: org + '/*',
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
for (const r of orgRepos) {
|
|
558
|
+
repoChoices.push({
|
|
559
|
+
name: ` ${dim('└')} ${cyan(r.repoFullName)} ${dim(r.issueCount + ' issue(s)')}`,
|
|
560
|
+
value: r.orgId,
|
|
561
|
+
short: r.repoFullName,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
repoChoices.push(new inquirer.Separator(dim(' ───────────────────────────')), { name: ` ${dim('🔍 Search again')}`, value: '__search__', short: 'search' });
|
|
566
|
+
const { picked } = await inquirer.prompt([{
|
|
567
|
+
type: 'list',
|
|
568
|
+
name: 'picked',
|
|
569
|
+
message: bold('Pick the org or repo to browse:'),
|
|
570
|
+
choices: repoChoices,
|
|
571
|
+
pageSize: 18,
|
|
572
|
+
}]);
|
|
573
|
+
if (picked === '__search__')
|
|
574
|
+
return browseByTrack(program, profile);
|
|
575
|
+
targetOrgId = picked;
|
|
576
|
+
const pickedRepo = matches.find(r => r.orgId === picked);
|
|
577
|
+
displayLabel = pickedRepo ? pickedRepo.repoFullName : picked;
|
|
578
|
+
}
|
|
469
579
|
// Step 2: ask for track
|
|
470
580
|
const { browseTrack } = await inquirer.prompt([{
|
|
471
581
|
type: 'list',
|
|
@@ -480,18 +590,14 @@ async function browseByTrack(program, profile) {
|
|
|
480
590
|
],
|
|
481
591
|
}]);
|
|
482
592
|
const selectedTrack = browseTrack;
|
|
483
|
-
// Step 3:
|
|
484
|
-
const spinner = ora(dim(`
|
|
593
|
+
// Step 3: fetch issues for the selected org via orgId filter
|
|
594
|
+
const spinner = ora(dim(` Fetching unassigned ${selectedTrack} issues for ${bold(displayLabel)}…`)).start();
|
|
485
595
|
const collected = [];
|
|
486
596
|
let apiPage = 1;
|
|
487
|
-
const MAX_SCAN_PAGES = 20; // scan up to 1000 issues to find the org's full list
|
|
488
597
|
try {
|
|
489
|
-
while (
|
|
490
|
-
const res = await
|
|
598
|
+
while (true) {
|
|
599
|
+
const res = await getIssuesByOrg(program.id, targetOrgId, apiPage, 50);
|
|
491
600
|
for (const issue of res.data) {
|
|
492
|
-
const issueOrg = extractOrg(issue);
|
|
493
|
-
if (issueOrg !== targetOrg)
|
|
494
|
-
continue;
|
|
495
601
|
if (issue.assignedApplicant !== null)
|
|
496
602
|
continue;
|
|
497
603
|
if (!matchesTrack(issue, selectedTrack))
|
|
@@ -500,7 +606,7 @@ async function browseByTrack(program, profile) {
|
|
|
500
606
|
continue;
|
|
501
607
|
collected.push(issue);
|
|
502
608
|
}
|
|
503
|
-
spinner.text = dim(`
|
|
609
|
+
spinner.text = dim(` Fetched page ${apiPage}/${res.pagination.totalPages} — ${collected.length} matching issue(s) so far…`);
|
|
504
610
|
if (!res.pagination.hasNextPage)
|
|
505
611
|
break;
|
|
506
612
|
apiPage++;
|
|
@@ -513,11 +619,11 @@ async function browseByTrack(program, profile) {
|
|
|
513
619
|
// Sort by points descending
|
|
514
620
|
collected.sort((a, b) => (b.points || 0) - (a.points || 0));
|
|
515
621
|
if (collected.length === 0) {
|
|
516
|
-
spinner.warn(yellow(` No unassigned ${selectedTrack} issues found for
|
|
517
|
-
console.log(dim('
|
|
622
|
+
spinner.warn(yellow(` No unassigned ${selectedTrack} issues found for "${displayLabel}" in the ${program.name} wave.`));
|
|
623
|
+
console.log(dim(' Try a different track, lower min points, or search again.\n'));
|
|
518
624
|
return;
|
|
519
625
|
}
|
|
520
|
-
spinner.succeed(` ${bold(collected.length + '')} unassigned ${selectedTrack} issue(s) from ${cyan(
|
|
626
|
+
spinner.succeed(` ${bold(collected.length + '')} unassigned ${selectedTrack} issue(s) from ${cyan(displayLabel)} — sorted by points`);
|
|
521
627
|
// Step 4: paginate through results (20 per page)
|
|
522
628
|
const PAGE_SIZE = 20;
|
|
523
629
|
let viewPage = 0;
|
|
@@ -553,7 +659,7 @@ async function browseByTrack(program, profile) {
|
|
|
553
659
|
const { selected } = await inquirer.prompt([{
|
|
554
660
|
type: 'list',
|
|
555
661
|
name: 'selected',
|
|
556
|
-
message: bold(`${
|
|
662
|
+
message: bold(`${displayLabel} — ${selectedTrack} issues (${collected.length} total, page ${viewPage + 1}/${totalPages}):`),
|
|
557
663
|
choices: [...choices, ...nav],
|
|
558
664
|
pageSize: 20,
|
|
559
665
|
}]);
|
package/dist/commands/issues.js
CHANGED
|
@@ -114,7 +114,7 @@ function parseMarkdownIssues(content) {
|
|
|
114
114
|
}
|
|
115
115
|
export async function createIssuesFromFile(filePath, opts) {
|
|
116
116
|
requireRepo();
|
|
117
|
-
const resolved = path.resolve(filePath);
|
|
117
|
+
const resolved = path.resolve(filePath.trim());
|
|
118
118
|
if (!fs.existsSync(resolved)) {
|
|
119
119
|
console.error(chalk.red(`\n❌ File not found: ${resolved}\n`));
|
|
120
120
|
return;
|
|
@@ -8,7 +8,7 @@ const TIER_3_HOURS = 72; // Auto-unassign
|
|
|
8
8
|
const SIG_TIER_1 = '<!-- gitpadi-reminder-24h -->';
|
|
9
9
|
const SIG_TIER_2 = '<!-- gitpadi-reminder-48h -->';
|
|
10
10
|
const SIG_TIER_3 = '<!-- gitpadi-unassigned -->';
|
|
11
|
-
async function
|
|
11
|
+
export async function remindContributors() {
|
|
12
12
|
console.log(chalk.bold('\n🚀 GitPadi Escalating Reminder Engine\n'));
|
|
13
13
|
try {
|
|
14
14
|
initGitHub();
|
|
@@ -124,4 +124,7 @@ ${SIG_TIER_1}`;
|
|
|
124
124
|
process.exit(1);
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
|
|
127
|
+
// Standalone entry point (npx tsx src/remind-contributors.ts)
|
|
128
|
+
if (process.argv[1]?.endsWith('remind-contributors.ts') || process.argv[1]?.endsWith('remind-contributors.js')) {
|
|
129
|
+
remindContributors();
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitpadi",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.8",
|
|
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
|
@@ -31,11 +31,12 @@ import * as contribute from './commands/contribute.js';
|
|
|
31
31
|
import * as applyForIssue from './commands/apply-for-issue.js';
|
|
32
32
|
import { runBountyHunter } from './commands/bounty-hunter.js';
|
|
33
33
|
import { dripsMenu } from './commands/drips.js';
|
|
34
|
+
import { remindContributors } from './remind-contributors.js';
|
|
34
35
|
import * as gitlabIssues from './commands/gitlab-issues.js';
|
|
35
36
|
import * as gitlabMRs from './commands/gitlab-mrs.js';
|
|
36
37
|
import * as gitlabPipelines from './commands/gitlab-pipelines.js';
|
|
37
38
|
|
|
38
|
-
const VERSION = '2.1.
|
|
39
|
+
const VERSION = '2.1.6';
|
|
39
40
|
let targetConfirmed = false;
|
|
40
41
|
let gitlabProjectConfirmed = false;
|
|
41
42
|
|
|
@@ -1120,6 +1121,7 @@ async function maintainerMenu() {
|
|
|
1120
1121
|
{ name: `${green('📦')} ${bold('Repositories')} ${dim('— create, delete, clone, topics, info')}`, value: 'repos' },
|
|
1121
1122
|
{ name: `${red('🚀')} ${bold('Releases')} ${dim('— create, list, tag releases')}`, value: 'releases' },
|
|
1122
1123
|
{ name: `${green('🎯')} ${bold('Bounty Hunter')} ${dim('— auto-apply to Drips Wave & GrantFox')}`, value: 'hunt' },
|
|
1124
|
+
{ name: `${cyan('🔔')} ${bold('Remind Contributors')} ${dim('— warn assignees with no PR (12h)')}`, value: 'remind' },
|
|
1123
1125
|
new inquirer.Separator(dim(' ─────────────────────────────────────────')),
|
|
1124
1126
|
{ name: `${dim('⚙️')} ${dim('Switch Repo')}`, value: 'switch' },
|
|
1125
1127
|
{ name: `${dim('⬅')} ${dim('Back to Mode Selector')}`, value: 'back' },
|
|
@@ -1141,6 +1143,7 @@ async function maintainerMenu() {
|
|
|
1141
1143
|
else if (category === 'contributors') await safeMenu(contributorScoringMenu);
|
|
1142
1144
|
else if (category === 'releases') await safeMenu(releaseMenu);
|
|
1143
1145
|
else if (category === 'hunt') await runBountyHunter({ platform: 'all', maxApplications: 3, dryRun: false });
|
|
1146
|
+
else if (category === 'remind') await safeMenu(remindContributors);
|
|
1144
1147
|
else if (category === 'review-merge') {
|
|
1145
1148
|
await ensureTargetRepo();
|
|
1146
1149
|
const { prNum } = await ask([{ type: 'input', name: 'prNum', message: cyan('PR number to review:') }]);
|
package/src/commands/drips.ts
CHANGED
|
@@ -285,6 +285,20 @@ export async function ensureDripsAuth(): Promise<string> {
|
|
|
285
285
|
console.log(dim(`\n Open manually: ${cyan(DRIPS_WEB + '/wave/login')}\n`));
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
console.log(dim(' After logging in, get your token:'));
|
|
289
|
+
console.log();
|
|
290
|
+
console.log(dim(' 1. Press ') + bold('F12') + dim(' → click the ') + bold('Console') + dim(' tab'));
|
|
291
|
+
console.log(dim(' 2. Paste this command and press ') + bold('Enter') + dim(':'));
|
|
292
|
+
console.log();
|
|
293
|
+
console.log(' ' + cyan('document.cookie.match(/wave_access_token=([^;]+)/)?.[1]'));
|
|
294
|
+
console.log();
|
|
295
|
+
console.log(dim(' 3. Copy the value shown (starts with ') + yellow('eyJ...') + dim(')'));
|
|
296
|
+
console.log();
|
|
297
|
+
console.log(dim(' ── If Console shows "undefined", use this instead: ──────────'));
|
|
298
|
+
console.log(dim(' F12 → Application tab → Cookies → www.drips.network'));
|
|
299
|
+
console.log(dim(' Find ') + yellow('wave_access_token') + dim(' → copy the Value column'));
|
|
300
|
+
console.log();
|
|
301
|
+
|
|
288
302
|
const { token } = await inquirer.prompt([{
|
|
289
303
|
type: 'password',
|
|
290
304
|
name: 'token',
|
|
@@ -554,18 +568,146 @@ async function autoApply(program: DripsProgram, profile: DripsProfile): Promise<
|
|
|
554
568
|
console.log(dim(`\n Track your applications: ${cyan(DRIPS_WEB + '/wave/' + program.slug)}\n`));
|
|
555
569
|
}
|
|
556
570
|
|
|
571
|
+
// ── Repo/org search ───────────────────────────────────────────────────────────
|
|
572
|
+
|
|
573
|
+
interface DripsRepo {
|
|
574
|
+
orgId: string;
|
|
575
|
+
orgLogin: string;
|
|
576
|
+
repoFullName: string;
|
|
577
|
+
repoName: string;
|
|
578
|
+
issueCount: number;
|
|
579
|
+
description: string;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Uses the Drips repos API to search by org/repo name — fast, covers ALL repos.
|
|
584
|
+
*/
|
|
585
|
+
async function searchRepos(program: DripsProgram, term: string): Promise<DripsRepo[]> {
|
|
586
|
+
const data = await dripsGet(
|
|
587
|
+
`/api/wave-programs/${program.id}/repos?search=${encodeURIComponent(term)}&limit=20&status=approved`,
|
|
588
|
+
);
|
|
589
|
+
const items: any[] = Array.isArray(data?.data) ? data.data : (data ? [data] : []);
|
|
590
|
+
return items.map((item: any) => ({
|
|
591
|
+
orgId: item.org?.id || '',
|
|
592
|
+
orgLogin: item.org?.gitHubOrgLogin || '',
|
|
593
|
+
repoFullName: item.repo?.gitHubRepoFullName || '',
|
|
594
|
+
repoName: item.repo?.gitHubRepoName || '',
|
|
595
|
+
issueCount: item.issueCount || 0,
|
|
596
|
+
description: item.description || '',
|
|
597
|
+
})).filter(r => r.orgId && r.repoFullName);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Fetch issues filtered by Drips orgId — returns all issues for that org directly.
|
|
602
|
+
*/
|
|
603
|
+
async function getIssuesByOrg(
|
|
604
|
+
programId: string,
|
|
605
|
+
orgId: string,
|
|
606
|
+
page: number,
|
|
607
|
+
limit: number = 50,
|
|
608
|
+
): Promise<{ data: DripsIssue[]; pagination: DripsPagination }> {
|
|
609
|
+
return dripsGet(
|
|
610
|
+
`/api/issues?waveProgramId=${programId}&orgId=${orgId}&state=open&page=${page}&limit=${limit}&sortBy=points`,
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
557
614
|
// ── Browse by org + track ─────────────────────────────────────────────────────
|
|
558
615
|
|
|
559
616
|
async function browseByTrack(program: DripsProgram, profile: DripsProfile): Promise<void> {
|
|
560
|
-
// Step 1: ask for org name
|
|
617
|
+
// Step 1: ask for org/repo name with search fallback
|
|
561
618
|
const { orgInput } = await inquirer.prompt([{
|
|
562
619
|
type: 'input',
|
|
563
620
|
name: 'orgInput',
|
|
564
|
-
message: bold('Enter
|
|
621
|
+
message: bold('Enter org name or repo name to search (e.g. stellar, Inheritx, uniswap):'),
|
|
565
622
|
validate: (v: string) => v.trim().length > 0 || 'Required',
|
|
566
623
|
}]);
|
|
567
624
|
|
|
568
|
-
const
|
|
625
|
+
const term = orgInput.trim();
|
|
626
|
+
|
|
627
|
+
// Search for matching repos in the wave program
|
|
628
|
+
const searchSpinner = ora(dim(` Searching for "${term}" in ${program.name} wave…`)).start();
|
|
629
|
+
let matches: DripsRepo[];
|
|
630
|
+
try {
|
|
631
|
+
matches = await searchRepos(program, term);
|
|
632
|
+
} catch (e: any) {
|
|
633
|
+
searchSpinner.fail(` Search failed: ${e.message}`);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (matches.length === 0) {
|
|
638
|
+
searchSpinner.warn(yellow(` No repos found matching "${term}" in the ${program.name} wave.`));
|
|
639
|
+
console.log(dim(' Try a shorter or different search term.\n'));
|
|
640
|
+
return browseByTrack(program, profile);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
searchSpinner.succeed(` Found ${matches.length} repo(s) matching "${term}"`);
|
|
644
|
+
console.log();
|
|
645
|
+
|
|
646
|
+
let targetOrgId: string;
|
|
647
|
+
let displayLabel: string;
|
|
648
|
+
|
|
649
|
+
const exactOrg = matches.find(m => m.orgLogin.toLowerCase() === term.toLowerCase());
|
|
650
|
+
const exactRepo = matches.find(m => m.repoFullName.toLowerCase() === term.toLowerCase());
|
|
651
|
+
|
|
652
|
+
if (matches.length === 1) {
|
|
653
|
+
targetOrgId = matches[0].orgId;
|
|
654
|
+
displayLabel = matches[0].repoFullName;
|
|
655
|
+
console.log(dim(` Using repo: ${cyan(displayLabel)}\n`));
|
|
656
|
+
} else if (exactRepo) {
|
|
657
|
+
targetOrgId = exactRepo.orgId;
|
|
658
|
+
displayLabel = exactRepo.repoFullName;
|
|
659
|
+
console.log(dim(` Using repo: ${cyan(displayLabel)}\n`));
|
|
660
|
+
} else if (exactOrg) {
|
|
661
|
+
targetOrgId = exactOrg.orgId;
|
|
662
|
+
displayLabel = exactOrg.orgLogin + '/*';
|
|
663
|
+
console.log(dim(` Using org: ${cyan(exactOrg.orgLogin)} (all repos)\n`));
|
|
664
|
+
} else {
|
|
665
|
+
// Multiple partial matches — let user pick
|
|
666
|
+
const repoChoices: any[] = [];
|
|
667
|
+
|
|
668
|
+
const orgGroups = new Map<string, DripsRepo[]>();
|
|
669
|
+
for (const r of matches) {
|
|
670
|
+
const list = orgGroups.get(r.orgLogin) || [];
|
|
671
|
+
list.push(r);
|
|
672
|
+
orgGroups.set(r.orgLogin, list);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
for (const [org, orgRepos] of orgGroups) {
|
|
676
|
+
if (orgRepos.length > 1) {
|
|
677
|
+
const total = orgRepos.reduce((s, r) => s + r.issueCount, 0);
|
|
678
|
+
repoChoices.push({
|
|
679
|
+
name: ` ${cyan(org + '/*')} ${dim(`all ${orgRepos.length} repos · ${total} issues`)}`,
|
|
680
|
+
value: orgRepos[0].orgId,
|
|
681
|
+
short: org + '/*',
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
for (const r of orgRepos) {
|
|
685
|
+
repoChoices.push({
|
|
686
|
+
name: ` ${dim('└')} ${cyan(r.repoFullName)} ${dim(r.issueCount + ' issue(s)')}`,
|
|
687
|
+
value: r.orgId,
|
|
688
|
+
short: r.repoFullName,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
repoChoices.push(
|
|
694
|
+
new inquirer.Separator(dim(' ───────────────────────────')) as any,
|
|
695
|
+
{ name: ` ${dim('🔍 Search again')}`, value: '__search__', short: 'search' },
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
const { picked } = await inquirer.prompt([{
|
|
699
|
+
type: 'list',
|
|
700
|
+
name: 'picked',
|
|
701
|
+
message: bold('Pick the org or repo to browse:'),
|
|
702
|
+
choices: repoChoices,
|
|
703
|
+
pageSize: 18,
|
|
704
|
+
}]);
|
|
705
|
+
|
|
706
|
+
if (picked === '__search__') return browseByTrack(program, profile);
|
|
707
|
+
targetOrgId = picked;
|
|
708
|
+
const pickedRepo = matches.find(r => r.orgId === picked);
|
|
709
|
+
displayLabel = pickedRepo ? pickedRepo.repoFullName : picked;
|
|
710
|
+
}
|
|
569
711
|
|
|
570
712
|
// Step 2: ask for track
|
|
571
713
|
const { browseTrack } = await inquirer.prompt([{
|
|
@@ -583,27 +725,24 @@ async function browseByTrack(program: DripsProgram, profile: DripsProfile): Prom
|
|
|
583
725
|
|
|
584
726
|
const selectedTrack: Track = browseTrack;
|
|
585
727
|
|
|
586
|
-
// Step 3:
|
|
587
|
-
const spinner = ora(dim(`
|
|
728
|
+
// Step 3: fetch issues for the selected org via orgId filter
|
|
729
|
+
const spinner = ora(dim(` Fetching unassigned ${selectedTrack} issues for ${bold(displayLabel)}…`)).start();
|
|
588
730
|
|
|
589
731
|
const collected: DripsIssue[] = [];
|
|
590
732
|
let apiPage = 1;
|
|
591
|
-
const MAX_SCAN_PAGES = 20; // scan up to 1000 issues to find the org's full list
|
|
592
733
|
|
|
593
734
|
try {
|
|
594
|
-
while (
|
|
595
|
-
const res = await
|
|
735
|
+
while (true) {
|
|
736
|
+
const res = await getIssuesByOrg(program.id, targetOrgId, apiPage, 50);
|
|
596
737
|
|
|
597
738
|
for (const issue of res.data) {
|
|
598
|
-
const issueOrg = extractOrg(issue);
|
|
599
|
-
if (issueOrg !== targetOrg) continue;
|
|
600
739
|
if (issue.assignedApplicant !== null) continue;
|
|
601
740
|
if (!matchesTrack(issue, selectedTrack)) continue;
|
|
602
741
|
if (profile.minPoints > 0 && issue.points !== null && issue.points < profile.minPoints) continue;
|
|
603
742
|
collected.push(issue);
|
|
604
743
|
}
|
|
605
744
|
|
|
606
|
-
spinner.text = dim(`
|
|
745
|
+
spinner.text = dim(` Fetched page ${apiPage}/${res.pagination.totalPages} — ${collected.length} matching issue(s) so far…`);
|
|
607
746
|
|
|
608
747
|
if (!res.pagination.hasNextPage) break;
|
|
609
748
|
apiPage++;
|
|
@@ -617,13 +756,13 @@ async function browseByTrack(program: DripsProgram, profile: DripsProfile): Prom
|
|
|
617
756
|
collected.sort((a, b) => (b.points || 0) - (a.points || 0));
|
|
618
757
|
|
|
619
758
|
if (collected.length === 0) {
|
|
620
|
-
spinner.warn(yellow(` No unassigned ${selectedTrack} issues found for
|
|
621
|
-
console.log(dim('
|
|
759
|
+
spinner.warn(yellow(` No unassigned ${selectedTrack} issues found for "${displayLabel}" in the ${program.name} wave.`));
|
|
760
|
+
console.log(dim(' Try a different track, lower min points, or search again.\n'));
|
|
622
761
|
return;
|
|
623
762
|
}
|
|
624
763
|
|
|
625
764
|
spinner.succeed(
|
|
626
|
-
` ${bold(collected.length + '')} unassigned ${selectedTrack} issue(s) from ${cyan(
|
|
765
|
+
` ${bold(collected.length + '')} unassigned ${selectedTrack} issue(s) from ${cyan(displayLabel)} — sorted by points`
|
|
627
766
|
);
|
|
628
767
|
|
|
629
768
|
// Step 4: paginate through results (20 per page)
|
|
@@ -664,7 +803,7 @@ async function browseByTrack(program: DripsProgram, profile: DripsProfile): Prom
|
|
|
664
803
|
const { selected } = await inquirer.prompt([{
|
|
665
804
|
type: 'list',
|
|
666
805
|
name: 'selected',
|
|
667
|
-
message: bold(`${
|
|
806
|
+
message: bold(`${displayLabel} — ${selectedTrack} issues (${collected.length} total, page ${viewPage + 1}/${totalPages}):`),
|
|
668
807
|
choices: [...choices, ...nav],
|
|
669
808
|
pageSize: 20,
|
|
670
809
|
}]);
|
package/src/commands/issues.ts
CHANGED
|
@@ -131,7 +131,7 @@ function parseMarkdownIssues(content: string): { issues: any[]; labels: Record<s
|
|
|
131
131
|
|
|
132
132
|
export async function createIssuesFromFile(filePath: string, opts: { dryRun?: boolean; start?: number; end?: number }) {
|
|
133
133
|
requireRepo();
|
|
134
|
-
const resolved = path.resolve(filePath);
|
|
134
|
+
const resolved = path.resolve(filePath.trim());
|
|
135
135
|
if (!fs.existsSync(resolved)) {
|
|
136
136
|
console.error(chalk.red(`\n❌ File not found: ${resolved}\n`));
|
|
137
137
|
return;
|
|
@@ -11,7 +11,7 @@ const SIG_TIER_1 = '<!-- gitpadi-reminder-24h -->';
|
|
|
11
11
|
const SIG_TIER_2 = '<!-- gitpadi-reminder-48h -->';
|
|
12
12
|
const SIG_TIER_3 = '<!-- gitpadi-unassigned -->';
|
|
13
13
|
|
|
14
|
-
async function
|
|
14
|
+
export async function remindContributors() {
|
|
15
15
|
console.log(chalk.bold('\n🚀 GitPadi Escalating Reminder Engine\n'));
|
|
16
16
|
|
|
17
17
|
try {
|
|
@@ -156,4 +156,7 @@ ${SIG_TIER_1}`;
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
// Standalone entry point (npx tsx src/remind-contributors.ts)
|
|
160
|
+
if (process.argv[1]?.endsWith('remind-contributors.ts') || process.argv[1]?.endsWith('remind-contributors.js')) {
|
|
161
|
+
remindContributors();
|
|
162
|
+
}
|