gitpadi 2.0.7 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitlab/duo/chat-rules.md +40 -0
- package/.gitlab/duo/mr-review-instructions.md +44 -0
- package/.gitlab-ci.yml +136 -0
- package/README.md +585 -57
- package/action.yml +21 -2
- package/dist/applicant-scorer.js +27 -105
- package/dist/cli.js +1040 -34
- package/dist/commands/apply-for-issue.js +396 -0
- package/dist/commands/bounty-hunter.js +441 -0
- package/dist/commands/contribute.js +245 -51
- package/dist/commands/gitlab-issues.js +87 -0
- package/dist/commands/gitlab-mrs.js +163 -0
- package/dist/commands/gitlab-pipelines.js +95 -0
- package/dist/commands/prs.js +3 -3
- package/dist/core/github.js +24 -0
- package/dist/core/gitlab.js +233 -0
- package/dist/gitlab-agents/ci-recovery-agent.js +173 -0
- package/dist/gitlab-agents/contributor-scoring-agent.js +159 -0
- package/dist/gitlab-agents/grade-assignment-agent.js +252 -0
- package/dist/gitlab-agents/mr-review-agent.js +200 -0
- package/dist/gitlab-agents/reminder-agent.js +164 -0
- package/dist/grade-assignment.js +262 -0
- package/dist/remind-contributors.js +127 -0
- package/dist/review-and-merge.js +125 -0
- package/examples/gitpadi.yml +152 -0
- package/package.json +20 -4
- package/src/applicant-scorer.ts +33 -141
- package/src/cli.ts +1073 -33
- package/src/commands/apply-for-issue.ts +452 -0
- package/src/commands/bounty-hunter.ts +529 -0
- package/src/commands/contribute.ts +264 -50
- package/src/commands/gitlab-issues.ts +87 -0
- package/src/commands/gitlab-mrs.ts +185 -0
- package/src/commands/gitlab-pipelines.ts +104 -0
- package/src/commands/prs.ts +3 -3
- package/src/core/github.ts +24 -0
- package/src/core/gitlab.ts +397 -0
- package/src/gitlab-agents/ci-recovery-agent.ts +201 -0
- package/src/gitlab-agents/contributor-scoring-agent.ts +196 -0
- package/src/gitlab-agents/grade-assignment-agent.ts +275 -0
- package/src/gitlab-agents/mr-review-agent.ts +231 -0
- package/src/gitlab-agents/reminder-agent.ts +203 -0
- package/src/grade-assignment.ts +283 -0
- package/src/remind-contributors.ts +159 -0
- package/src/review-and-merge.ts +143 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
// commands/bounty-hunter.ts — Autonomous Bounty Hunter
|
|
2
|
+
//
|
|
3
|
+
// Searches Drips Wave and GrantFox for open issues, scores them against your
|
|
4
|
+
// GitHub profile, and automatically posts applications — no human input needed.
|
|
5
|
+
//
|
|
6
|
+
// How it works:
|
|
7
|
+
// Drips Wave → GitHub issues labeled "Stellar Wave" across 196+ orgs
|
|
8
|
+
// GrantFox → GitHub issues in org:GrantChain and known partner repos
|
|
9
|
+
//
|
|
10
|
+
// Both platforms sync from GitHub, so posting a matching application comment
|
|
11
|
+
// IS the application. Their bots pick it up automatically.
|
|
12
|
+
//
|
|
13
|
+
// Usage:
|
|
14
|
+
// gitpadi hunt
|
|
15
|
+
// gitpadi hunt --platform drips --max 5 --dry-run
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
import ora from 'ora';
|
|
18
|
+
import { getOctokit, getAuthenticatedUser } from '../core/github.js';
|
|
19
|
+
const dim = chalk.dim;
|
|
20
|
+
const cyan = chalk.cyan;
|
|
21
|
+
const green = chalk.green;
|
|
22
|
+
const yellow = chalk.yellow;
|
|
23
|
+
const bold = chalk.bold;
|
|
24
|
+
const magenta = chalk.magenta;
|
|
25
|
+
const red = chalk.red;
|
|
26
|
+
// ── Point values for Drips Wave complexity labels ────────────────────────────
|
|
27
|
+
const DRIPS_COMPLEXITY = {
|
|
28
|
+
'small': 100, 'trivial': 100,
|
|
29
|
+
'medium': 150,
|
|
30
|
+
'large': 200, 'high': 200, 'complex': 200,
|
|
31
|
+
};
|
|
32
|
+
// Drips 3× multiplier repos (highest reward)
|
|
33
|
+
const DRIPS_3X_REPOS = new Set([
|
|
34
|
+
'akkuea/akkuea',
|
|
35
|
+
'tupui/soroban-versioning',
|
|
36
|
+
'OFFER-HUB/offer-hub-monorepo',
|
|
37
|
+
'safetrustcr/frontend-SafeTrust',
|
|
38
|
+
'kindfi-org/kindfi',
|
|
39
|
+
'Stellopay/stellopay-frontend',
|
|
40
|
+
'Stellar-Rent/stellar-rent',
|
|
41
|
+
'Stellopay/stellopay-core',
|
|
42
|
+
'boundlessfi/bounties',
|
|
43
|
+
'BuenDia-Builders/be-energy',
|
|
44
|
+
'Trustless-Work/clonable-backoffice',
|
|
45
|
+
'Trustless-Work/tokenization-private-credit-oss',
|
|
46
|
+
'safetrustcr/backend-SafeTrust',
|
|
47
|
+
]);
|
|
48
|
+
// GrantFox partner orgs on GitHub
|
|
49
|
+
const GRANTFOX_ORGS = ['GrantChain', 'Trustless-Work'];
|
|
50
|
+
// Labels that signal open contribution opportunity
|
|
51
|
+
const CONTRIBUTOR_LABELS = [
|
|
52
|
+
'good first issue', 'help wanted', 'bounty', 'hacktoberfest',
|
|
53
|
+
'up for grabs', 'stellar wave', 'grantfox',
|
|
54
|
+
'contributions welcome', 'open to pr',
|
|
55
|
+
];
|
|
56
|
+
const DEFAULT_OPTIONS = {
|
|
57
|
+
platform: 'all',
|
|
58
|
+
maxApplications: 3,
|
|
59
|
+
dryRun: false,
|
|
60
|
+
skills: [],
|
|
61
|
+
minPoints: 0,
|
|
62
|
+
skipAssigned: true,
|
|
63
|
+
verbose: false,
|
|
64
|
+
};
|
|
65
|
+
// ── Search ───────────────────────────────────────────────────────────────────
|
|
66
|
+
async function searchDripsIssues(octokit, skills) {
|
|
67
|
+
const issues = [];
|
|
68
|
+
// Primary: label "Stellar Wave" — this is the official Drips Wave label
|
|
69
|
+
const queries = [
|
|
70
|
+
'label:"Stellar Wave" state:open type:issue',
|
|
71
|
+
'label:"stellar-wave" state:open type:issue',
|
|
72
|
+
];
|
|
73
|
+
for (const q of queries) {
|
|
74
|
+
try {
|
|
75
|
+
const { data } = await octokit.search.issuesAndPullRequests({
|
|
76
|
+
q, per_page: 50, sort: 'updated', order: 'desc',
|
|
77
|
+
});
|
|
78
|
+
for (const item of data.items) {
|
|
79
|
+
if (item.pull_request)
|
|
80
|
+
continue;
|
|
81
|
+
const [owner, repo] = (item.repository_url?.replace('https://api.github.com/repos/', '') || '/').split('/');
|
|
82
|
+
const fullRepo = `${owner}/${repo}`;
|
|
83
|
+
const labels = item.labels.map((l) => (l.name || '').toLowerCase());
|
|
84
|
+
// Parse complexity from labels
|
|
85
|
+
let complexity = 'medium';
|
|
86
|
+
let points = 150;
|
|
87
|
+
for (const [lbl, pts] of Object.entries(DRIPS_COMPLEXITY)) {
|
|
88
|
+
if (labels.some((l) => l.includes(lbl))) {
|
|
89
|
+
complexity = lbl;
|
|
90
|
+
points = pts;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const multiplier = DRIPS_3X_REPOS.has(fullRepo) ? 3 : 1;
|
|
95
|
+
issues.push({
|
|
96
|
+
platform: 'drips',
|
|
97
|
+
owner, repo,
|
|
98
|
+
number: item.number,
|
|
99
|
+
title: item.title,
|
|
100
|
+
body: item.body || '',
|
|
101
|
+
url: item.html_url,
|
|
102
|
+
labels,
|
|
103
|
+
points,
|
|
104
|
+
multiplier,
|
|
105
|
+
effectivePoints: points * multiplier,
|
|
106
|
+
complexity,
|
|
107
|
+
assignees: item.assignees?.map((a) => a.login) || [],
|
|
108
|
+
hasPr: false,
|
|
109
|
+
skillMatch: calcSkillMatch(item.title + ' ' + (item.body || '') + ' ' + labels.join(' '), skills),
|
|
110
|
+
age: Math.floor((Date.now() - new Date(item.updated_at).getTime()) / 86_400_000),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch { /* rate limit or no results */ }
|
|
115
|
+
}
|
|
116
|
+
return dedupeIssues(issues);
|
|
117
|
+
}
|
|
118
|
+
async function searchGrantFoxIssues(octokit, skills) {
|
|
119
|
+
const issues = [];
|
|
120
|
+
const queries = [
|
|
121
|
+
// GrantFox own repositories
|
|
122
|
+
...GRANTFOX_ORGS.map(org => `org:${org} state:open type:issue`),
|
|
123
|
+
// Issues tagged with contributor-friendly labels in known partner orgs
|
|
124
|
+
'label:"good first issue" org:Trustless-Work state:open type:issue',
|
|
125
|
+
'label:"help wanted" org:Trustless-Work state:open type:issue',
|
|
126
|
+
'label:bounty state:open type:issue org:GrantChain',
|
|
127
|
+
];
|
|
128
|
+
for (const q of queries) {
|
|
129
|
+
try {
|
|
130
|
+
const { data } = await octokit.search.issuesAndPullRequests({
|
|
131
|
+
q, per_page: 30, sort: 'updated', order: 'desc',
|
|
132
|
+
});
|
|
133
|
+
for (const item of data.items) {
|
|
134
|
+
if (item.pull_request)
|
|
135
|
+
continue;
|
|
136
|
+
const [owner, repo] = (item.repository_url?.replace('https://api.github.com/repos/', '') || '/').split('/');
|
|
137
|
+
const labels = item.labels.map((l) => (l.name || '').toLowerCase());
|
|
138
|
+
issues.push({
|
|
139
|
+
platform: 'grantfox',
|
|
140
|
+
owner, repo,
|
|
141
|
+
number: item.number,
|
|
142
|
+
title: item.title,
|
|
143
|
+
body: item.body || '',
|
|
144
|
+
url: item.html_url,
|
|
145
|
+
labels,
|
|
146
|
+
points: 0,
|
|
147
|
+
multiplier: 1,
|
|
148
|
+
effectivePoints: 0,
|
|
149
|
+
complexity: 'unknown',
|
|
150
|
+
assignees: item.assignees?.map((a) => a.login) || [],
|
|
151
|
+
hasPr: false,
|
|
152
|
+
skillMatch: calcSkillMatch(item.title + ' ' + (item.body || '') + ' ' + labels.join(' '), skills),
|
|
153
|
+
age: Math.floor((Date.now() - new Date(item.updated_at).getTime()) / 86_400_000),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch { /* rate limit or no results */ }
|
|
158
|
+
}
|
|
159
|
+
return dedupeIssues(issues);
|
|
160
|
+
}
|
|
161
|
+
// ── Scoring helpers ──────────────────────────────────────────────────────────
|
|
162
|
+
function calcSkillMatch(text, skills) {
|
|
163
|
+
if (skills.length === 0)
|
|
164
|
+
return 50; // neutral
|
|
165
|
+
const lower = text.toLowerCase();
|
|
166
|
+
const hits = skills.filter(s => lower.includes(s.toLowerCase()));
|
|
167
|
+
return Math.round((hits.length / skills.length) * 100);
|
|
168
|
+
}
|
|
169
|
+
function scoreIssue(issue) {
|
|
170
|
+
let score = 0;
|
|
171
|
+
score += issue.skillMatch * 0.4; // 40% skill match
|
|
172
|
+
score += Math.min(issue.effectivePoints / 6, 33); // up to 33 pts for reward
|
|
173
|
+
score += issue.assignees.length === 0 ? 20 : 0; // unassigned bonus
|
|
174
|
+
score -= Math.min(issue.age / 10, 10); // penalise stale issues
|
|
175
|
+
return Math.round(Math.max(0, score));
|
|
176
|
+
}
|
|
177
|
+
function dedupeIssues(issues) {
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
return issues.filter(i => {
|
|
180
|
+
const key = `${i.owner}/${i.repo}#${i.number}`;
|
|
181
|
+
if (seen.has(key))
|
|
182
|
+
return false;
|
|
183
|
+
seen.add(key);
|
|
184
|
+
return true;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// ── Application comment builders ─────────────────────────────────────────────
|
|
188
|
+
function buildDripsApplication(issue, me) {
|
|
189
|
+
const pts = issue.effectivePoints > 0
|
|
190
|
+
? `\n\n> **Reward:** ${issue.points} pts${issue.multiplier > 1 ? ` × ${issue.multiplier}× = **${issue.effectivePoints} pts**` : ''} (${issue.complexity})`
|
|
191
|
+
: '';
|
|
192
|
+
return `Hey! I'd like to work on this issue.
|
|
193
|
+
|
|
194
|
+
I'm available to start right away, will open a draft PR early to show progress, and will keep you updated.${pts}
|
|
195
|
+
|
|
196
|
+
Please assign this to me if you think I'm a good fit — happy to answer any questions about my approach first.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
*Applied via [GitPadi](https://github.com/Netwalls/contributor-agent) Bounty Hunter 🤖*`;
|
|
200
|
+
}
|
|
201
|
+
function buildGrantFoxApplication(issue, me) {
|
|
202
|
+
return `Hi! I'd like to work on this issue.
|
|
203
|
+
|
|
204
|
+
I found this through GrantFox and I'm interested in contributing. I'll:
|
|
205
|
+
- Open a draft PR early so you can track my progress
|
|
206
|
+
- Ask questions here if anything needs clarification
|
|
207
|
+
- Aim to complete within 3–5 days
|
|
208
|
+
|
|
209
|
+
Please assign this to me if it's still open — I'm ready to start.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
*Applied via [GitPadi](https://github.com/Netwalls/contributor-agent) Bounty Hunter 🤖*`;
|
|
213
|
+
}
|
|
214
|
+
// ── Check if already applied ─────────────────────────────────────────────────
|
|
215
|
+
async function alreadyApplied(octokit, issue, me) {
|
|
216
|
+
try {
|
|
217
|
+
const { data: comments } = await octokit.issues.listComments({
|
|
218
|
+
owner: issue.owner,
|
|
219
|
+
repo: issue.repo,
|
|
220
|
+
issue_number: issue.number,
|
|
221
|
+
per_page: 100,
|
|
222
|
+
});
|
|
223
|
+
return comments.some((c) => c.user?.login === me &&
|
|
224
|
+
/i('d| would| want| will)|assign.*me|let me|i can|i'm interested|claiming|GitPadi Bounty Hunter/i.test(c.body || ''));
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// ── Print helpers ────────────────────────────────────────────────────────────
|
|
231
|
+
function platformBadge(platform) {
|
|
232
|
+
return platform === 'drips'
|
|
233
|
+
? cyan('[Drips Wave]')
|
|
234
|
+
: magenta('[GrantFox] ');
|
|
235
|
+
}
|
|
236
|
+
function pointsBadge(issue) {
|
|
237
|
+
if (issue.effectivePoints === 0)
|
|
238
|
+
return dim(' ');
|
|
239
|
+
if (issue.multiplier > 1)
|
|
240
|
+
return yellow(`${issue.effectivePoints}pts★`);
|
|
241
|
+
return dim(`${issue.effectivePoints}pts `);
|
|
242
|
+
}
|
|
243
|
+
function printIssueRow(issue, idx, status) {
|
|
244
|
+
const num = dim(`${idx + 1}.`);
|
|
245
|
+
const badge = platformBadge(issue.platform);
|
|
246
|
+
const pts = pointsBadge(issue);
|
|
247
|
+
const assigned = issue.assignees.length > 0 ? yellow(' [assigned]') : '';
|
|
248
|
+
const title = issue.title.length > 55 ? issue.title.slice(0, 54) + '…' : issue.title;
|
|
249
|
+
console.log(` ${num} ${badge} ${pts} ${bold(title)}${assigned}`);
|
|
250
|
+
console.log(` ${dim(`${issue.owner}/${issue.repo}#${issue.number}`)} ${dim('→')} ${dim(issue.url)}`);
|
|
251
|
+
console.log(` ${status}`);
|
|
252
|
+
console.log();
|
|
253
|
+
}
|
|
254
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
255
|
+
export async function runBountyHunter(opts = {}) {
|
|
256
|
+
const options = { ...DEFAULT_OPTIONS, ...opts };
|
|
257
|
+
const octokit = getOctokit();
|
|
258
|
+
console.log();
|
|
259
|
+
console.log(bold(cyan(' 🎯 GitPadi Bounty Hunter')));
|
|
260
|
+
console.log(dim(` Platform: ${options.platform} | Max applications: ${options.maxApplications} | Dry-run: ${options.dryRun}`));
|
|
261
|
+
console.log();
|
|
262
|
+
// Get authenticated user + their languages
|
|
263
|
+
const spinner = ora(' Loading your GitHub profile…').start();
|
|
264
|
+
let me = '';
|
|
265
|
+
let myLanguages = options.skills;
|
|
266
|
+
try {
|
|
267
|
+
me = await getAuthenticatedUser();
|
|
268
|
+
if (myLanguages.length === 0) {
|
|
269
|
+
// Auto-detect user's top languages from their repos
|
|
270
|
+
const { data: myRepos } = await octokit.repos.listForAuthenticatedUser({
|
|
271
|
+
per_page: 50, sort: 'updated',
|
|
272
|
+
});
|
|
273
|
+
const langCounts = {};
|
|
274
|
+
myRepos.forEach((r) => {
|
|
275
|
+
if (r.language)
|
|
276
|
+
langCounts[r.language] = (langCounts[r.language] || 0) + 1;
|
|
277
|
+
});
|
|
278
|
+
myLanguages = Object.entries(langCounts)
|
|
279
|
+
.sort((a, b) => b[1] - a[1])
|
|
280
|
+
.slice(0, 8)
|
|
281
|
+
.map(([l]) => l.toLowerCase());
|
|
282
|
+
}
|
|
283
|
+
spinner.succeed(` Logged in as @${me} | Skills: ${myLanguages.join(', ') || 'all'}`);
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
spinner.fail(` Could not load profile: ${e.message}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// Search issues
|
|
290
|
+
const searches = [];
|
|
291
|
+
if (options.platform === 'drips' || options.platform === 'all') {
|
|
292
|
+
const s = ora(' Searching Drips Wave issues…').start();
|
|
293
|
+
searches.push(searchDripsIssues(octokit, myLanguages)
|
|
294
|
+
.then(r => { s.succeed(` Drips Wave: ${r.length} issues found`); return r; })
|
|
295
|
+
.catch(e => { s.fail(` Drips search failed: ${e.message}`); return []; }));
|
|
296
|
+
}
|
|
297
|
+
if (options.platform === 'grantfox' || options.platform === 'all') {
|
|
298
|
+
const s = ora(' Searching GrantFox issues…').start();
|
|
299
|
+
searches.push(searchGrantFoxIssues(octokit, myLanguages)
|
|
300
|
+
.then(r => { s.succeed(` GrantFox: ${r.length} issues found`); return r; })
|
|
301
|
+
.catch(e => { s.fail(` GrantFox search failed: ${e.message}`); return []; }));
|
|
302
|
+
}
|
|
303
|
+
const results = await Promise.all(searches);
|
|
304
|
+
let allIssues = dedupeIssues(results.flat());
|
|
305
|
+
// Filter
|
|
306
|
+
if (options.skipAssigned) {
|
|
307
|
+
allIssues = allIssues.filter(i => i.assignees.length === 0);
|
|
308
|
+
}
|
|
309
|
+
if (options.minPoints > 0) {
|
|
310
|
+
allIssues = allIssues.filter(i => i.platform === 'grantfox' || i.effectivePoints >= options.minPoints);
|
|
311
|
+
}
|
|
312
|
+
if (options.skills.length > 0) {
|
|
313
|
+
allIssues = allIssues.filter(i => i.skillMatch > 0);
|
|
314
|
+
}
|
|
315
|
+
// Rank
|
|
316
|
+
allIssues.sort((a, b) => scoreIssue(b) - scoreIssue(a));
|
|
317
|
+
if (allIssues.length === 0) {
|
|
318
|
+
console.log(yellow('\n No matching issues found. Try --platform all or remove skill filters.'));
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
console.log();
|
|
322
|
+
console.log(bold(` Found ${allIssues.length} matching issue(s) — top picks:\n`));
|
|
323
|
+
// Show top issues
|
|
324
|
+
const TOP_DISPLAY = Math.min(allIssues.length, 10);
|
|
325
|
+
for (let i = 0; i < TOP_DISPLAY; i++) {
|
|
326
|
+
const issue = allIssues[i];
|
|
327
|
+
const rank = scoreIssue(issue);
|
|
328
|
+
const rankStr = rank >= 70 ? green(`match ${rank}%`) : rank >= 40 ? yellow(`match ${rank}%`) : dim(`match ${rank}%`);
|
|
329
|
+
printIssueRow(issue, i, rankStr);
|
|
330
|
+
}
|
|
331
|
+
if (allIssues.length > TOP_DISPLAY) {
|
|
332
|
+
console.log(dim(` … and ${allIssues.length - TOP_DISPLAY} more\n`));
|
|
333
|
+
}
|
|
334
|
+
// Select top N for application
|
|
335
|
+
const candidates = allIssues.slice(0, options.maxApplications);
|
|
336
|
+
if (options.dryRun) {
|
|
337
|
+
console.log(yellow(` [dry-run] Would apply to ${candidates.length} issue(s):`));
|
|
338
|
+
candidates.forEach((issue, i) => {
|
|
339
|
+
console.log(` ${i + 1}. ${issue.owner}/${issue.repo}#${issue.number} — ${issue.title}`);
|
|
340
|
+
});
|
|
341
|
+
console.log();
|
|
342
|
+
console.log(dim(' Remove --dry-run to actually post applications.'));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Apply
|
|
346
|
+
console.log(bold(`\n Applying to top ${candidates.length} issue(s)…\n`));
|
|
347
|
+
const results_ = [];
|
|
348
|
+
for (const issue of candidates) {
|
|
349
|
+
const label = ` ${platformBadge(issue.platform)} ${issue.owner}/${issue.repo}#${issue.number}`;
|
|
350
|
+
const applySpinner = ora(`${label}`).start();
|
|
351
|
+
// Check for existing application
|
|
352
|
+
const already = await alreadyApplied(octokit, issue, me);
|
|
353
|
+
if (already) {
|
|
354
|
+
applySpinner.warn(`${label} — already applied, skipping`);
|
|
355
|
+
results_.push({ issue, status: 'skipped' });
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const body = issue.platform === 'drips'
|
|
359
|
+
? buildDripsApplication(issue, me)
|
|
360
|
+
: buildGrantFoxApplication(issue, me);
|
|
361
|
+
try {
|
|
362
|
+
const { data: comment } = await octokit.issues.createComment({
|
|
363
|
+
owner: issue.owner,
|
|
364
|
+
repo: issue.repo,
|
|
365
|
+
issue_number: issue.number,
|
|
366
|
+
body,
|
|
367
|
+
});
|
|
368
|
+
applySpinner.succeed(`${label} — ${green('applied')} → ${dim(comment.html_url)}`);
|
|
369
|
+
results_.push({ issue, status: 'applied', url: comment.html_url });
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
applySpinner.fail(`${label} — ${red('failed:')} ${e.message}`);
|
|
373
|
+
results_.push({ issue, status: 'failed' });
|
|
374
|
+
}
|
|
375
|
+
// Small delay to avoid rate limiting
|
|
376
|
+
await new Promise(r => setTimeout(r, 800));
|
|
377
|
+
}
|
|
378
|
+
// Summary
|
|
379
|
+
const applied = results_.filter(r => r.status === 'applied');
|
|
380
|
+
const skipped = results_.filter(r => r.status === 'skipped');
|
|
381
|
+
const failed = results_.filter(r => r.status === 'failed');
|
|
382
|
+
console.log();
|
|
383
|
+
console.log(bold(' ── Summary ───────────────────────────────────────────'));
|
|
384
|
+
console.log(` ${green(`✅ Applied: ${applied.length}`)}`);
|
|
385
|
+
if (skipped.length)
|
|
386
|
+
console.log(` ${yellow(`⏭ Skipped: ${skipped.length} (already applied)`)}`);
|
|
387
|
+
if (failed.length)
|
|
388
|
+
console.log(` ${red(`❌ Failed: ${failed.length}`)}`);
|
|
389
|
+
console.log();
|
|
390
|
+
if (applied.length > 0) {
|
|
391
|
+
console.log(dim(' What happens next:'));
|
|
392
|
+
const hasDrips = applied.some(r => r.issue.platform === 'drips');
|
|
393
|
+
const hasGF = applied.some(r => r.issue.platform === 'grantfox');
|
|
394
|
+
if (hasDrips) {
|
|
395
|
+
console.log(dim(' • Drips Wave: submit a PR when your fix is ready. Points are awarded on merge.'));
|
|
396
|
+
}
|
|
397
|
+
if (hasGF) {
|
|
398
|
+
console.log(dim(' • GrantFox: maintainer will review your profile and assign if you\'re a good fit.'));
|
|
399
|
+
}
|
|
400
|
+
console.log(dim(' • Run `gitpadi` → Contributor → Sync with Upstream to stay current.'));
|
|
401
|
+
console.log();
|
|
402
|
+
}
|
|
403
|
+
// Offer to fork + branch for applied issues
|
|
404
|
+
if (applied.length > 0) {
|
|
405
|
+
console.log(bold(' Fork & branch for applied issues?'));
|
|
406
|
+
for (const r of applied) {
|
|
407
|
+
const { execFileSync } = await import('child_process');
|
|
408
|
+
const { forkRepo } = await import('../core/github.js');
|
|
409
|
+
const { existsSync } = await import('fs');
|
|
410
|
+
console.log(`\n ${cyan(`${r.issue.owner}/${r.issue.repo}#${r.issue.number}`)}`);
|
|
411
|
+
try {
|
|
412
|
+
const forkSpinner = ora(` Forking ${r.issue.owner}/${r.issue.repo}…`).start();
|
|
413
|
+
await forkRepo(r.issue.owner, r.issue.repo);
|
|
414
|
+
forkSpinner.succeed(` Forked → ${me}/${r.issue.repo}`);
|
|
415
|
+
const dir = `./${r.issue.repo}-${r.issue.number}`;
|
|
416
|
+
if (!existsSync(dir)) {
|
|
417
|
+
const cloneSpinner = ora(` Cloning into ${dir}…`).start();
|
|
418
|
+
execFileSync('git', ['clone', `https://github.com/${me}/${r.issue.repo}.git`, dir], { stdio: 'pipe' });
|
|
419
|
+
cloneSpinner.succeed();
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
execFileSync('git', ['remote', 'add', 'upstream', `https://github.com/${r.issue.owner}/${r.issue.repo}.git`], { cwd: dir, stdio: 'pipe' });
|
|
423
|
+
}
|
|
424
|
+
catch { /* already exists */ }
|
|
425
|
+
const branch = `fix/issue-${r.issue.number}`;
|
|
426
|
+
execFileSync('git', ['checkout', '-b', branch], { cwd: dir, stdio: 'pipe' });
|
|
427
|
+
console.log(green(` ✅ Branch ready: ${cyan(branch)}`));
|
|
428
|
+
console.log(dim(` cd ${dir} && npx gitpadi`));
|
|
429
|
+
}
|
|
430
|
+
catch (e) {
|
|
431
|
+
if (/already exists/i.test(e.message)) {
|
|
432
|
+
console.log(yellow(` Fork already exists.`));
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
console.log(yellow(` Could not fork/clone: ${e.message}`));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
console.log();
|
|
440
|
+
}
|
|
441
|
+
}
|