gitpadi 2.1.2 → 2.1.4
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 +448 -108
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/drips.ts +518 -117
package/dist/commands/drips.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
// commands/drips.ts —
|
|
1
|
+
// commands/drips.ts — Smart Drips Network issue applicant
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// Features:
|
|
4
|
+
// 1. Auto-Apply — scan issues, score against your tech stack, batch-apply
|
|
5
|
+
// 2. Browse by Track — unassigned only, sorted by points, filter backend/frontend/contract
|
|
6
|
+
//
|
|
7
|
+
// API: https://wave-api.drips.network
|
|
5
8
|
// Auth: GitHub OAuth → JWT cookie (wave_access_token)
|
|
6
9
|
import inquirer from 'inquirer';
|
|
7
10
|
import chalk from 'chalk';
|
|
@@ -20,6 +23,26 @@ const yellow = chalk.yellowBright;
|
|
|
20
23
|
const green = chalk.greenBright;
|
|
21
24
|
const bold = chalk.bold;
|
|
22
25
|
const red = chalk.redBright;
|
|
26
|
+
const magenta = chalk.magentaBright;
|
|
27
|
+
// ── Track keyword maps ────────────────────────────────────────────────────────
|
|
28
|
+
const TRACK_KEYWORDS = {
|
|
29
|
+
contract: [
|
|
30
|
+
'solidity', 'smart contract', 'contract', 'blockchain', 'web3',
|
|
31
|
+
'evm', 'anchor', 'defi', 'nft', 'token', 'wallet', 'on-chain',
|
|
32
|
+
'hardhat', 'foundry', 'wagmi', 'ethers',
|
|
33
|
+
],
|
|
34
|
+
backend: [
|
|
35
|
+
'backend', 'api', 'server', 'database', 'db', 'node', 'python',
|
|
36
|
+
'golang', 'go', 'java', 'rust', 'graphql', 'rest', 'grpc',
|
|
37
|
+
'postgresql', 'postgres', 'mysql', 'redis', 'kafka', 'docker',
|
|
38
|
+
'microservice', 'auth', 'authentication', 'authorization',
|
|
39
|
+
],
|
|
40
|
+
frontend: [
|
|
41
|
+
'frontend', 'ui', 'ux', 'react', 'svelte', 'vue', 'angular',
|
|
42
|
+
'css', 'html', 'tailwind', 'mobile', 'ios', 'android', 'flutter',
|
|
43
|
+
'next', 'nuxt', 'design', 'component', 'responsive', 'a11y',
|
|
44
|
+
],
|
|
45
|
+
};
|
|
23
46
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
24
47
|
function loadConfig() {
|
|
25
48
|
if (!fs.existsSync(CONFIG_FILE))
|
|
@@ -31,11 +54,20 @@ function loadConfig() {
|
|
|
31
54
|
return {};
|
|
32
55
|
}
|
|
33
56
|
}
|
|
34
|
-
function
|
|
57
|
+
function saveConfig(patch) {
|
|
35
58
|
const current = loadConfig();
|
|
36
59
|
if (!fs.existsSync(CONFIG_DIR))
|
|
37
60
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
38
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current,
|
|
61
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...patch }, null, 2));
|
|
62
|
+
}
|
|
63
|
+
function loadProfile() {
|
|
64
|
+
return loadConfig().dripsProfile ?? null;
|
|
65
|
+
}
|
|
66
|
+
function saveProfile(profile) {
|
|
67
|
+
saveConfig({ dripsProfile: profile });
|
|
68
|
+
}
|
|
69
|
+
function saveDripsToken(token) {
|
|
70
|
+
saveConfig({ dripsToken: token });
|
|
39
71
|
}
|
|
40
72
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
41
73
|
function truncate(text, max) {
|
|
@@ -61,6 +93,44 @@ function parseSlug(input) {
|
|
|
61
93
|
return m[1];
|
|
62
94
|
return input.replace(/^\/+|\/+$/g, '').split('/').pop() || input;
|
|
63
95
|
}
|
|
96
|
+
function issueLabels(issue) {
|
|
97
|
+
return issue.labels.map(l => l.name.toLowerCase());
|
|
98
|
+
}
|
|
99
|
+
const ORG_APPLICATION_LIMIT = 4;
|
|
100
|
+
/** Extract the org/owner from a repo fullName like "myorg/myrepo" */
|
|
101
|
+
function extractOrg(issue) {
|
|
102
|
+
return issue.repo?.fullName?.split('/')[0]?.toLowerCase() || '__unknown__';
|
|
103
|
+
}
|
|
104
|
+
/** Detect which track an issue belongs to based on its labels and title */
|
|
105
|
+
function detectTrack(issue) {
|
|
106
|
+
const haystack = [...issueLabels(issue), issue.title.toLowerCase()].join(' ');
|
|
107
|
+
for (const [track, keywords] of Object.entries(TRACK_KEYWORDS)) {
|
|
108
|
+
if (keywords.some(kw => haystack.includes(kw)))
|
|
109
|
+
return track;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
/** Score an issue against the user's tech stack. Returns 0 if no match. */
|
|
114
|
+
function scoreIssue(issue, techStack) {
|
|
115
|
+
const haystack = [...issueLabels(issue), issue.title.toLowerCase()].join(' ');
|
|
116
|
+
const matchReasons = [];
|
|
117
|
+
for (const tech of techStack) {
|
|
118
|
+
if (haystack.includes(tech.toLowerCase())) {
|
|
119
|
+
matchReasons.push(tech);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { score: matchReasons.length, matchReasons };
|
|
123
|
+
}
|
|
124
|
+
/** Filter issues matching the user's track preference */
|
|
125
|
+
function matchesTrack(issue, track) {
|
|
126
|
+
if (track === 'all')
|
|
127
|
+
return true;
|
|
128
|
+
const detected = detectTrack(issue);
|
|
129
|
+
if (detected === track)
|
|
130
|
+
return true;
|
|
131
|
+
// Also accept issues with no detected track when browsing 'all'
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
64
134
|
// ── API ───────────────────────────────────────────────────────────────────────
|
|
65
135
|
async function dripsGet(endpoint, token) {
|
|
66
136
|
const headers = { Accept: 'application/json' };
|
|
@@ -97,36 +167,41 @@ async function dripsPost(endpoint, body, token) {
|
|
|
97
167
|
return {};
|
|
98
168
|
}
|
|
99
169
|
}
|
|
170
|
+
async function fetchIssuePage(programId, page, limit = 50) {
|
|
171
|
+
return dripsGet(`/api/issues?waveProgramId=${programId}&state=open&page=${page}&limit=${limit}&sortBy=updatedAt`);
|
|
172
|
+
}
|
|
173
|
+
async function getWaveProgram(slug) {
|
|
174
|
+
const data = await dripsGet(`/api/wave-programs?slug=${encodeURIComponent(slug)}`);
|
|
175
|
+
if (data?.id)
|
|
176
|
+
return data;
|
|
177
|
+
if (Array.isArray(data?.data) && data.data.length > 0)
|
|
178
|
+
return data.data[0];
|
|
179
|
+
throw new Error(`Wave program "${slug}" not found. Check the slug or URL.`);
|
|
180
|
+
}
|
|
100
181
|
// ── Auth ──────────────────────────────────────────────────────────────────────
|
|
101
182
|
export async function ensureDripsAuth() {
|
|
102
183
|
const config = loadConfig();
|
|
103
184
|
if (config.dripsToken) {
|
|
104
|
-
// Validate by hitting a quota endpoint that requires auth
|
|
105
185
|
try {
|
|
106
186
|
const res = await fetch(`${DRIPS_API}/api/wave-programs/fdc01c95-806f-4b6a-998b-a6ed37e0d81b/quotas/applications`, { headers: { Cookie: `wave_access_token=${config.dripsToken}`, Accept: 'application/json' } });
|
|
107
187
|
if (res.status !== 401)
|
|
108
188
|
return config.dripsToken;
|
|
109
189
|
}
|
|
110
|
-
catch { /* network issue
|
|
190
|
+
catch { /* network issue */ }
|
|
111
191
|
}
|
|
112
|
-
// Show how-to instructions
|
|
113
192
|
console.log();
|
|
114
193
|
console.log(dim(' ┌─ Connect your Drips Network account ────────────────────────┐'));
|
|
115
194
|
console.log(dim(' │'));
|
|
116
|
-
console.log(dim(' │ Drips uses GitHub OAuth. Here\'s how to get your token:'));
|
|
117
|
-
console.log(dim(' │'));
|
|
118
195
|
console.log(dim(' │ 1. Log in at: ') + cyan(DRIPS_WEB + '/wave/login'));
|
|
119
|
-
console.log(dim(' │ (GitPadi will open it in your browser
|
|
196
|
+
console.log(dim(' │ (GitPadi will open it in your browser)'));
|
|
120
197
|
console.log(dim(' │'));
|
|
121
|
-
console.log(dim(' │ 2. After
|
|
122
|
-
console.log(dim(' │ ') + bold('F12') + dim(' (
|
|
123
|
-
console.log(dim(' │ → ') + bold('Application') + dim(' tab'));
|
|
124
|
-
console.log(dim(' │ → ') + bold('Cookies') + dim(' → ') + cyan('www.drips.network'));
|
|
198
|
+
console.log(dim(' │ 2. After login, open DevTools:'));
|
|
199
|
+
console.log(dim(' │ ') + bold('F12') + dim(' → ') + bold('Application') + dim(' → ') + bold('Cookies') + dim(' → ') + cyan('www.drips.network'));
|
|
125
200
|
console.log(dim(' │'));
|
|
126
|
-
console.log(dim(' │ 3. Find the cookie
|
|
127
|
-
console.log(dim(' │ Copy
|
|
201
|
+
console.log(dim(' │ 3. Find the cookie: ') + yellow('wave_access_token'));
|
|
202
|
+
console.log(dim(' │ Copy its full value (starts with ') + dim('eyJ...') + dim(')'));
|
|
128
203
|
console.log(dim(' │'));
|
|
129
|
-
console.log(dim(' │
|
|
204
|
+
console.log(dim(' │ This is a one-time step — GitPadi saves it for future use.'));
|
|
130
205
|
console.log(dim(' │'));
|
|
131
206
|
console.log(dim(' └─────────────────────────────────────────────────────────────┘'));
|
|
132
207
|
console.log();
|
|
@@ -152,79 +227,296 @@ export async function ensureDripsAuth() {
|
|
|
152
227
|
if (!v || v.trim().length < 20)
|
|
153
228
|
return 'Token seems too short — copy the full value from DevTools';
|
|
154
229
|
if (!v.trim().startsWith('eyJ'))
|
|
155
|
-
return 'Expected a JWT starting with eyJ —
|
|
230
|
+
return 'Expected a JWT starting with eyJ — check you copied wave_access_token';
|
|
156
231
|
return true;
|
|
157
232
|
},
|
|
158
233
|
}]);
|
|
159
234
|
const t = token.trim();
|
|
160
235
|
saveDripsToken(t);
|
|
161
|
-
console.log(green('\n ✅ Drips session saved
|
|
236
|
+
console.log(green('\n ✅ Drips session saved.\n'));
|
|
162
237
|
return t;
|
|
163
238
|
}
|
|
164
|
-
// ──
|
|
165
|
-
async function
|
|
166
|
-
const data = await dripsGet(`/api/wave-programs?slug=${encodeURIComponent(slug)}`);
|
|
167
|
-
if (data?.id)
|
|
168
|
-
return data;
|
|
169
|
-
if (Array.isArray(data?.data) && data.data.length > 0)
|
|
170
|
-
return data.data[0];
|
|
171
|
-
throw new Error(`Wave program "${slug}" not found. Check the slug or URL.`);
|
|
172
|
-
}
|
|
173
|
-
// ── Issues ────────────────────────────────────────────────────────────────────
|
|
174
|
-
async function getIssues(programId, page) {
|
|
175
|
-
return dripsGet(`/api/issues?waveProgramId=${programId}&state=open&page=${page}&limit=20&sortBy=updatedAt`);
|
|
176
|
-
}
|
|
177
|
-
// ── Main menu ─────────────────────────────────────────────────────────────────
|
|
178
|
-
export async function dripsMenu() {
|
|
239
|
+
// ── Profile setup ─────────────────────────────────────────────────────────────
|
|
240
|
+
async function setupProfile(existing) {
|
|
179
241
|
console.log();
|
|
180
|
-
console.log(bold(' Drips
|
|
181
|
-
console.log(dim(' Browse issues from any Wave program and apply in seconds.'));
|
|
242
|
+
console.log(bold(' Set up your Drips profile — GitPadi uses this to find the right issues for you.'));
|
|
182
243
|
console.log();
|
|
183
|
-
const {
|
|
244
|
+
const { programInput } = await inquirer.prompt([{
|
|
184
245
|
type: 'input',
|
|
185
|
-
name: '
|
|
186
|
-
message: bold('
|
|
187
|
-
default: 'stellar',
|
|
188
|
-
validate: (v) => v.trim().length > 0 || 'Required',
|
|
246
|
+
name: 'programInput',
|
|
247
|
+
message: bold('Drips Wave program (slug or URL):'),
|
|
248
|
+
default: existing?.programSlug || 'stellar',
|
|
189
249
|
}]);
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
250
|
+
const { track } = await inquirer.prompt([{
|
|
251
|
+
type: 'list',
|
|
252
|
+
name: 'track',
|
|
253
|
+
message: bold('What is your track?'),
|
|
254
|
+
default: existing?.track || 'backend',
|
|
255
|
+
choices: [
|
|
256
|
+
{ name: ` ${cyan('⚙️')} Backend ${dim('— APIs, servers, databases, Node.js, Rust, Go...')}`, value: 'backend' },
|
|
257
|
+
{ name: ` ${magenta('🎨')} Frontend ${dim('— React, Svelte, Vue, CSS, mobile...')}`, value: 'frontend' },
|
|
258
|
+
{ name: ` ${yellow('📜')} Smart Contract ${dim('— Solidity, EVM, Anchor, DeFi, Web3...')}`, value: 'contract' },
|
|
259
|
+
{ name: ` ${green('🌐')} All tracks ${dim('— show everything, filter by tech stack only')}`, value: 'all' },
|
|
260
|
+
],
|
|
261
|
+
}]);
|
|
262
|
+
const { stackInput } = await inquirer.prompt([{
|
|
263
|
+
type: 'input',
|
|
264
|
+
name: 'stackInput',
|
|
265
|
+
message: bold('Your tech stack (comma-separated):'),
|
|
266
|
+
default: existing?.techStack.join(', ') || 'TypeScript, Node.js',
|
|
267
|
+
validate: (v) => v.trim().length > 0 || 'Enter at least one technology',
|
|
268
|
+
}]);
|
|
269
|
+
const { minPoints } = await inquirer.prompt([{
|
|
270
|
+
type: 'list',
|
|
271
|
+
name: 'minPoints',
|
|
272
|
+
message: bold('Minimum points to apply for:'),
|
|
273
|
+
default: existing?.minPoints ?? 100,
|
|
274
|
+
choices: [
|
|
275
|
+
{ name: ` Any (including unspecified)`, value: 0 },
|
|
276
|
+
{ name: ` 100 pts`, value: 100 },
|
|
277
|
+
{ name: ` 150 pts`, value: 150 },
|
|
278
|
+
{ name: ` 200 pts`, value: 200 },
|
|
279
|
+
],
|
|
280
|
+
}]);
|
|
281
|
+
const techStack = stackInput.split(',').map((s) => s.trim()).filter(Boolean);
|
|
282
|
+
const programSlug = parseSlug(programInput.trim());
|
|
283
|
+
const profile = { track, techStack, minPoints, programSlug };
|
|
284
|
+
saveProfile(profile);
|
|
285
|
+
console.log(green(`\n ✅ Profile saved — track: ${bold(track)}, stack: ${techStack.join(', ')}, min: ${minPoints}pts\n`));
|
|
286
|
+
return profile;
|
|
287
|
+
}
|
|
288
|
+
// ── Auto-apply ────────────────────────────────────────────────────────────────
|
|
289
|
+
async function autoApply(program, profile) {
|
|
290
|
+
console.log();
|
|
291
|
+
console.log(bold(` Scanning ${program.name} issues for matches…`));
|
|
292
|
+
console.log(dim(` Track: ${profile.track} | Stack: ${profile.techStack.join(', ')} | Min: ${profile.minPoints}pts`));
|
|
293
|
+
console.log();
|
|
294
|
+
const spinner = ora(dim(' Fetching open issues…')).start();
|
|
295
|
+
const scored = [];
|
|
296
|
+
let page = 1;
|
|
297
|
+
const MAX_PAGES = 10; // scan up to 500 issues
|
|
298
|
+
while (page <= MAX_PAGES) {
|
|
299
|
+
let data;
|
|
300
|
+
let pagination;
|
|
301
|
+
try {
|
|
302
|
+
const res = await fetchIssuePage(program.id, page, 50);
|
|
303
|
+
data = res.data;
|
|
304
|
+
pagination = res.pagination;
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
spinner.fail(` Fetch error: ${e.message}`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
for (const issue of data) {
|
|
311
|
+
// Skip assigned issues
|
|
312
|
+
if (issue.assignedApplicant !== null)
|
|
313
|
+
continue;
|
|
314
|
+
// Skip below min points (only if points are set)
|
|
315
|
+
if (profile.minPoints > 0 && issue.points !== null && issue.points < profile.minPoints)
|
|
316
|
+
continue;
|
|
317
|
+
// Skip if track doesn't match
|
|
318
|
+
if (!matchesTrack(issue, profile.track))
|
|
319
|
+
continue;
|
|
320
|
+
// Score against tech stack
|
|
321
|
+
const { score, matchReasons } = scoreIssue(issue, profile.techStack);
|
|
322
|
+
// For 'all' track, accept any score >= 0. For specific tracks, require at least a label match
|
|
323
|
+
if (profile.track !== 'all' && score === 0) {
|
|
324
|
+
// Still include it if it clearly matches the track (track filter already handled above)
|
|
325
|
+
// but boost score so track-matching issues without stack matches still show up
|
|
326
|
+
scored.push({ issue, score: 0.5, matchReasons: [profile.track] });
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
scored.push({ issue, score, matchReasons: score > 0 ? matchReasons : [profile.track] });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
spinner.text = dim(` Scanned ${page * 50} issues, found ${scored.length} matches so far…`);
|
|
333
|
+
if (!pagination.hasNextPage || scored.length >= 50)
|
|
334
|
+
break;
|
|
335
|
+
page++;
|
|
336
|
+
}
|
|
337
|
+
// Sort: by score desc, then by points desc
|
|
338
|
+
scored.sort((a, b) => {
|
|
339
|
+
if (b.score !== a.score)
|
|
340
|
+
return b.score - a.score;
|
|
341
|
+
return (b.issue.points || 0) - (a.issue.points || 0);
|
|
342
|
+
});
|
|
343
|
+
const top = scored.slice(0, 30);
|
|
344
|
+
spinner.succeed(` Found ${scored.length} matching issue(s) — showing top ${top.length}`);
|
|
345
|
+
if (top.length === 0) {
|
|
346
|
+
console.log(yellow('\n No matching unassigned issues found. Try broadening your tech stack or lowering min points.\n'));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
console.log();
|
|
350
|
+
// Checkbox selection
|
|
351
|
+
// Pre-compute org counts to show how many slots remain per org
|
|
352
|
+
const previewOrgCounts = new Map();
|
|
353
|
+
const checkboxChoices = top.map(({ issue, score, matchReasons }) => {
|
|
354
|
+
const pts = issue.points ? green(`+${issue.points}pts`) : dim('—pts');
|
|
355
|
+
const applicants = issue.pendingApplicationsCount > 0 ? yellow(`${issue.pendingApplicationsCount}▲`) : dim('0▲');
|
|
356
|
+
const match = matchReasons.length > 0 ? cyan(`[${matchReasons.slice(0, 3).join(', ')}]`) : '';
|
|
357
|
+
const org = extractOrg(issue);
|
|
358
|
+
const orgSlots = ORG_APPLICATION_LIMIT - (previewOrgCounts.get(org) || 0);
|
|
359
|
+
const orgTag = dim(`(${org} ${orgSlots}/${ORG_APPLICATION_LIMIT} slots)`);
|
|
360
|
+
const title = truncate(issue.title, 40);
|
|
361
|
+
const autoCheck = score >= 1 && orgSlots > 0;
|
|
362
|
+
if (autoCheck)
|
|
363
|
+
previewOrgCounts.set(org, (previewOrgCounts.get(org) || 0) + 1);
|
|
364
|
+
return {
|
|
365
|
+
name: ` ${pts} ${bold(title)} ${match} ${applicants} ${orgTag}`,
|
|
366
|
+
value: issue,
|
|
367
|
+
checked: autoCheck,
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
const { selectedIssues } = await inquirer.prompt([{
|
|
371
|
+
type: 'checkbox',
|
|
372
|
+
name: 'selectedIssues',
|
|
373
|
+
message: bold('Select issues to apply for (Space to toggle, Enter to confirm):'),
|
|
374
|
+
choices: checkboxChoices,
|
|
375
|
+
pageSize: 20,
|
|
376
|
+
validate: (selected) => selected.length > 0 || 'Select at least one issue',
|
|
377
|
+
}]);
|
|
378
|
+
if (!selectedIssues || selectedIssues.length === 0) {
|
|
379
|
+
console.log(dim('\n No issues selected.\n'));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
console.log();
|
|
383
|
+
console.log(bold(` ${selectedIssues.length} issue(s) selected. Composing application message…`));
|
|
384
|
+
console.log();
|
|
385
|
+
// Single application message for all
|
|
386
|
+
const { style } = await inquirer.prompt([{
|
|
387
|
+
type: 'list',
|
|
388
|
+
name: 'style',
|
|
389
|
+
message: bold('Application message for all selected issues:'),
|
|
390
|
+
choices: [
|
|
391
|
+
{ name: ` ${green('⚡')} Quick — standard intro message`, value: 'quick' },
|
|
392
|
+
{ name: ` ${cyan('📝')} Custom — write one message for all`, value: 'custom' },
|
|
393
|
+
],
|
|
394
|
+
}]);
|
|
395
|
+
let baseMessage;
|
|
396
|
+
if (style === 'quick') {
|
|
397
|
+
baseMessage = `Hi! I'd like to work on this issue.\n\nI'm available to start right away and will keep you updated on my progress. My stack includes ${profile.techStack.join(', ')}. Please consider assigning this to me.\n\n*Applied via [GitPadi](https://github.com/Netwalls/contributor-agent)*`;
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
const { msg } = await inquirer.prompt([{
|
|
401
|
+
type: 'input',
|
|
402
|
+
name: 'msg',
|
|
403
|
+
message: bold('Your message (applies to all selected issues):'),
|
|
404
|
+
validate: (v) => v.trim().length >= 10 || 'Write at least 10 characters',
|
|
405
|
+
}]);
|
|
406
|
+
baseMessage = `${msg.trim()}\n\n*Applied via [GitPadi](https://github.com/Netwalls/contributor-agent)*`;
|
|
407
|
+
}
|
|
408
|
+
const { go } = await inquirer.prompt([{
|
|
409
|
+
type: 'confirm',
|
|
410
|
+
name: 'go',
|
|
411
|
+
message: bold(`Apply to ${selectedIssues.length} issue(s) now?`),
|
|
412
|
+
default: true,
|
|
413
|
+
}]);
|
|
414
|
+
if (!go)
|
|
415
|
+
return;
|
|
416
|
+
// Auth
|
|
417
|
+
let token;
|
|
193
418
|
try {
|
|
194
|
-
|
|
195
|
-
progSpinner.succeed(` ${bold(program.name)} · ${cyan(program.issueCount.toLocaleString() + ' open issues')} · ${green(program.presetBudgetUSD + '/mo')} · ${dim(program.approvedRepoCount + ' repos')}`);
|
|
419
|
+
token = await ensureDripsAuth();
|
|
196
420
|
}
|
|
197
|
-
catch
|
|
198
|
-
|
|
421
|
+
catch {
|
|
422
|
+
console.log(red('\n Authentication cancelled.\n'));
|
|
199
423
|
return;
|
|
200
424
|
}
|
|
425
|
+
console.log();
|
|
426
|
+
let applied = 0;
|
|
427
|
+
let failed = 0;
|
|
428
|
+
let skipped = 0;
|
|
429
|
+
const orgCounts = new Map();
|
|
430
|
+
for (const issue of selectedIssues) {
|
|
431
|
+
const org = extractOrg(issue);
|
|
432
|
+
const orgCount = orgCounts.get(org) || 0;
|
|
433
|
+
if (orgCount >= ORG_APPLICATION_LIMIT) {
|
|
434
|
+
console.log(yellow(` ⚠ Skipped: ${truncate(issue.title, 50)} — already applied to ${ORG_APPLICATION_LIMIT} issues from ${org}`));
|
|
435
|
+
skipped++;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
const spinner = ora(` Applying to: ${truncate(issue.title, 50)}…`).start();
|
|
439
|
+
try {
|
|
440
|
+
await dripsPost(`/api/wave-programs/${program.id}/issues/${issue.id}/applications`, { applicationText: baseMessage }, token);
|
|
441
|
+
orgCounts.set(org, orgCount + 1);
|
|
442
|
+
spinner.succeed(green(` ✅ ${truncate(issue.title, 55)}`) + (issue.points ? dim(` (+${issue.points}pts)`) : '') + dim(` [${org}: ${orgCount + 1}/${ORG_APPLICATION_LIMIT}]`));
|
|
443
|
+
applied++;
|
|
444
|
+
}
|
|
445
|
+
catch (e) {
|
|
446
|
+
spinner.fail(red(` ✗ ${truncate(issue.title, 55)} — ${e.message}`));
|
|
447
|
+
failed++;
|
|
448
|
+
if (e.message.startsWith('401')) {
|
|
449
|
+
saveDripsToken('');
|
|
450
|
+
console.log(yellow('\n Session expired — run again to re-authenticate.\n'));
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
console.log();
|
|
456
|
+
console.log(bold(` Done: ${green(applied + ' applied')}${skipped > 0 ? ', ' + yellow(skipped + ' skipped (org limit)') : ''}${failed > 0 ? ', ' + red(failed + ' failed') : ''}`));
|
|
457
|
+
console.log(dim(`\n Track your applications: ${cyan(DRIPS_WEB + '/wave/' + program.slug)}\n`));
|
|
458
|
+
}
|
|
459
|
+
// ── Browse by track ───────────────────────────────────────────────────────────
|
|
460
|
+
async function browseByTrack(program, profile) {
|
|
461
|
+
// Ask which track to browse if they want to override
|
|
462
|
+
const { browseTrack } = await inquirer.prompt([{
|
|
463
|
+
type: 'list',
|
|
464
|
+
name: 'browseTrack',
|
|
465
|
+
message: bold('Browse which track?'),
|
|
466
|
+
default: profile.track,
|
|
467
|
+
choices: [
|
|
468
|
+
{ name: ` ${cyan('⚙️')} Backend`, value: 'backend' },
|
|
469
|
+
{ name: ` ${magenta('🎨')} Frontend`, value: 'frontend' },
|
|
470
|
+
{ name: ` ${yellow('📜')} Smart Contract`, value: 'contract' },
|
|
471
|
+
{ name: ` ${green('🌐')} All tracks`, value: 'all' },
|
|
472
|
+
],
|
|
473
|
+
}]);
|
|
474
|
+
const selectedTrack = browseTrack;
|
|
201
475
|
let page = 1;
|
|
202
476
|
while (true) {
|
|
203
|
-
const
|
|
204
|
-
let
|
|
205
|
-
let
|
|
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
|
|
206
482
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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`);
|
|
211
499
|
}
|
|
212
500
|
catch (e) {
|
|
213
|
-
|
|
501
|
+
spinner.fail(` ${e.message}`);
|
|
214
502
|
return;
|
|
215
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`));
|
|
506
|
+
}
|
|
216
507
|
console.log();
|
|
217
|
-
const choices =
|
|
508
|
+
const choices = allIssues.slice(0, 20).map((issue) => {
|
|
218
509
|
const age = diffDays(issue.updatedAt);
|
|
219
510
|
const ageStr = age === 0 ? dim('today') : dim(`${age}d ago`);
|
|
220
|
-
const pts = issue.points ? green(`+${issue.points}pts`) : dim('
|
|
511
|
+
const pts = issue.points ? green(`+${issue.points}pts`) : dim(' —pts');
|
|
221
512
|
const applicants = issue.pendingApplicationsCount > 0
|
|
222
|
-
? yellow(`${issue.pendingApplicationsCount}
|
|
223
|
-
: dim('0
|
|
224
|
-
const
|
|
225
|
-
const
|
|
513
|
+
? yellow(`${issue.pendingApplicationsCount}▲`)
|
|
514
|
+
: dim('0▲');
|
|
515
|
+
const detected = detectTrack(issue);
|
|
516
|
+
const trackTag = detected ? dim(`[${detected}]`) : '';
|
|
517
|
+
const title = truncate(issue.title, 46);
|
|
226
518
|
return {
|
|
227
|
-
name: ` ${pts} ${bold(title)} ${
|
|
519
|
+
name: ` ${pts} ${bold(title)} ${trackTag} ${applicants} ${ageStr}`,
|
|
228
520
|
value: issue,
|
|
229
521
|
short: issue.title,
|
|
230
522
|
};
|
|
@@ -232,7 +524,7 @@ export async function dripsMenu() {
|
|
|
232
524
|
const nav = [
|
|
233
525
|
new inquirer.Separator(dim(' ─────────────────────────────────────────────────────')),
|
|
234
526
|
];
|
|
235
|
-
if (
|
|
527
|
+
if (hasNextPage)
|
|
236
528
|
nav.push({ name: ` ${dim('→ Next page')}`, value: '__next__' });
|
|
237
529
|
if (page > 1)
|
|
238
530
|
nav.push({ name: ` ${dim('← Previous page')}`, value: '__prev__' });
|
|
@@ -240,8 +532,8 @@ export async function dripsMenu() {
|
|
|
240
532
|
const { selected } = await inquirer.prompt([{
|
|
241
533
|
type: 'list',
|
|
242
534
|
name: 'selected',
|
|
243
|
-
message: bold(`${program.name}
|
|
244
|
-
choices: [...choices, ...nav],
|
|
535
|
+
message: bold(`${program.name} — ${selectedTrack} issues sorted by points:`),
|
|
536
|
+
choices: allIssues.length > 0 ? [...choices, ...nav] : nav,
|
|
245
537
|
pageSize: 18,
|
|
246
538
|
}]);
|
|
247
539
|
if (selected === '__back__')
|
|
@@ -254,32 +546,28 @@ export async function dripsMenu() {
|
|
|
254
546
|
page = Math.max(1, page - 1);
|
|
255
547
|
continue;
|
|
256
548
|
}
|
|
257
|
-
await
|
|
549
|
+
await applySingle(program, selected, profile);
|
|
258
550
|
}
|
|
259
551
|
}
|
|
260
|
-
// ── Apply
|
|
261
|
-
async function
|
|
552
|
+
// ── Apply single ──────────────────────────────────────────────────────────────
|
|
553
|
+
async function applySingle(program, issue, profile) {
|
|
262
554
|
console.log();
|
|
263
555
|
console.log(` ${bold(cyan(truncate(issue.title, 72)))}`);
|
|
264
556
|
if (issue.repo)
|
|
265
557
|
console.log(` ${dim('Repo: ')} ${issue.repo.fullName}`);
|
|
266
558
|
console.log(` ${dim('Points: ')} ${issue.points ? green('+' + issue.points + ' pts') : dim('—')}`);
|
|
267
559
|
console.log(` ${dim('Applied: ')} ${issue.pendingApplicationsCount} applicant(s)`);
|
|
268
|
-
if (issue.
|
|
269
|
-
console.log(` ${yellow('⚠ Already assigned — you can still apply as a backup')}`);
|
|
270
|
-
}
|
|
271
|
-
if (issue.gitHubIssueUrl) {
|
|
560
|
+
if (issue.gitHubIssueUrl)
|
|
272
561
|
console.log(` ${dim('GitHub: ')} ${dim(issue.gitHubIssueUrl)}`);
|
|
273
|
-
}
|
|
274
562
|
console.log();
|
|
275
563
|
const { style } = await inquirer.prompt([{
|
|
276
564
|
type: 'list',
|
|
277
565
|
name: 'style',
|
|
278
|
-
message: bold('
|
|
566
|
+
message: bold('Application style:'),
|
|
279
567
|
choices: [
|
|
280
|
-
{ name: ` ${green('⚡')} Quick
|
|
281
|
-
{ name: ` ${cyan('📝')} Custom
|
|
282
|
-
new inquirer.Separator(dim('
|
|
568
|
+
{ name: ` ${green('⚡')} Quick — standard intro`, value: 'quick' },
|
|
569
|
+
{ name: ` ${cyan('📝')} Custom — write your own`, value: 'custom' },
|
|
570
|
+
new inquirer.Separator(dim(' ────────────────')),
|
|
283
571
|
{ name: ` ${dim('⬅ Cancel')}`, value: 'cancel' },
|
|
284
572
|
],
|
|
285
573
|
}]);
|
|
@@ -287,45 +575,30 @@ async function applyToDripsIssue(program, issue) {
|
|
|
287
575
|
return;
|
|
288
576
|
let applicationText;
|
|
289
577
|
if (style === 'quick') {
|
|
290
|
-
applicationText = [
|
|
291
|
-
`Hi! I'd like to work on this issue.`,
|
|
292
|
-
``,
|
|
293
|
-
`I'm available to start right away and will keep you updated on progress. Please consider assigning this to me.`,
|
|
294
|
-
``,
|
|
295
|
-
`*Applied via [GitPadi](https://github.com/Netwalls/contributor-agent)*`,
|
|
296
|
-
].join('\n');
|
|
578
|
+
applicationText = `Hi! I'd like to work on this issue.\n\nI'm available to start right away. My stack includes ${profile.techStack.join(', ')}. Please consider assigning this to me.\n\n*Applied via [GitPadi](https://github.com/Netwalls/contributor-agent)*`;
|
|
297
579
|
}
|
|
298
580
|
else {
|
|
299
581
|
const { msg } = await inquirer.prompt([{
|
|
300
582
|
type: 'input',
|
|
301
583
|
name: 'msg',
|
|
302
|
-
message: bold('Your
|
|
303
|
-
validate: (v) => v.trim().length >= 10 || '
|
|
584
|
+
message: bold('Your message:'),
|
|
585
|
+
validate: (v) => v.trim().length >= 10 || 'Write at least 10 characters',
|
|
304
586
|
}]);
|
|
305
|
-
applicationText = [
|
|
306
|
-
msg.trim(),
|
|
307
|
-
``,
|
|
308
|
-
`*Applied via [GitPadi](https://github.com/Netwalls/contributor-agent)*`,
|
|
309
|
-
].join('\n');
|
|
587
|
+
applicationText = `${msg.trim()}\n\n*Applied via [GitPadi](https://github.com/Netwalls/contributor-agent)*`;
|
|
310
588
|
}
|
|
311
|
-
// Preview
|
|
312
589
|
console.log();
|
|
313
|
-
console.log(dim(' ── Preview
|
|
590
|
+
console.log(dim(' ── Preview ────────────────────────────────────────────────────'));
|
|
314
591
|
applicationText.split('\n').forEach(l => console.log(` ${dim(l)}`));
|
|
315
|
-
console.log(dim('
|
|
592
|
+
console.log(dim(' ───────────────────────────────────────────────────────────────'));
|
|
316
593
|
console.log();
|
|
317
594
|
const { confirm } = await inquirer.prompt([{
|
|
318
|
-
type: '
|
|
595
|
+
type: 'confirm',
|
|
319
596
|
name: 'confirm',
|
|
320
|
-
message: bold('Submit
|
|
321
|
-
|
|
322
|
-
{ name: ` ${green('✅ Yes, apply')}`, value: 'yes' },
|
|
323
|
-
{ name: ` ${dim('❌ Cancel')}`, value: 'no' },
|
|
324
|
-
],
|
|
597
|
+
message: bold('Submit application?'),
|
|
598
|
+
default: true,
|
|
325
599
|
}]);
|
|
326
|
-
if (confirm
|
|
600
|
+
if (!confirm)
|
|
327
601
|
return;
|
|
328
|
-
// Ensure auth only when the user actually commits to applying
|
|
329
602
|
let token;
|
|
330
603
|
try {
|
|
331
604
|
token = await ensureDripsAuth();
|
|
@@ -334,12 +607,11 @@ async function applyToDripsIssue(program, issue) {
|
|
|
334
607
|
console.log(red('\n Authentication cancelled.\n'));
|
|
335
608
|
return;
|
|
336
609
|
}
|
|
337
|
-
const spinner = ora(' Submitting
|
|
610
|
+
const spinner = ora(' Submitting…').start();
|
|
338
611
|
try {
|
|
339
612
|
await dripsPost(`/api/wave-programs/${program.id}/issues/${issue.id}/applications`, { applicationText }, token);
|
|
340
|
-
spinner.succeed(green(` Applied
|
|
341
|
-
console.log(dim(`\n Track
|
|
342
|
-
console.log(dim(` ${cyan(DRIPS_WEB + '/wave/' + program.slug)}\n`));
|
|
613
|
+
spinner.succeed(green(` ✅ Applied! ${bold(issue.title)}`));
|
|
614
|
+
console.log(dim(`\n Track: ${cyan(DRIPS_WEB + '/wave/' + program.slug)}\n`));
|
|
343
615
|
}
|
|
344
616
|
catch (e) {
|
|
345
617
|
spinner.fail(red(` Failed: ${e.message}`));
|
|
@@ -349,3 +621,71 @@ async function applyToDripsIssue(program, issue) {
|
|
|
349
621
|
}
|
|
350
622
|
}
|
|
351
623
|
}
|
|
624
|
+
// ── Main menu ─────────────────────────────────────────────────────────────────
|
|
625
|
+
export async function dripsMenu() {
|
|
626
|
+
// Load or set up profile first
|
|
627
|
+
let profile = loadProfile();
|
|
628
|
+
if (!profile) {
|
|
629
|
+
console.log();
|
|
630
|
+
console.log(bold(' Welcome to Drips Network! Let\'s set up your profile first.'));
|
|
631
|
+
profile = await setupProfile();
|
|
632
|
+
}
|
|
633
|
+
// Load the wave program
|
|
634
|
+
const progSpinner = ora(dim(` Loading "${profile.programSlug}" wave program…`)).start();
|
|
635
|
+
let program;
|
|
636
|
+
try {
|
|
637
|
+
program = await getWaveProgram(profile.programSlug);
|
|
638
|
+
progSpinner.succeed(` ${bold(program.name)} · ${cyan(program.issueCount.toLocaleString() + ' issues')} · ${green(program.presetBudgetUSD + '/mo')} · ${dim(program.approvedRepoCount + ' repos')}`);
|
|
639
|
+
}
|
|
640
|
+
catch (e) {
|
|
641
|
+
progSpinner.fail(` ${e.message}`);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
while (true) {
|
|
645
|
+
console.log();
|
|
646
|
+
const { action } = await inquirer.prompt([{
|
|
647
|
+
type: 'list',
|
|
648
|
+
name: 'action',
|
|
649
|
+
message: bold(`Drips Network — ${program.name} Wave`),
|
|
650
|
+
choices: [
|
|
651
|
+
{
|
|
652
|
+
name: ` ${green('🤖')} ${bold('Auto-Apply for Me')} ${dim('— scan issues, match my stack, batch-apply')}`,
|
|
653
|
+
value: 'auto',
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
name: ` ${cyan('🔍')} ${bold('Browse by Track')} ${dim('— unassigned only, sorted by points, filter by track')}`,
|
|
657
|
+
value: 'browse',
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
name: ` ${dim('⚙️')} ${dim('Update my Profile')} ${dim('— change track, tech stack, min points')}`,
|
|
661
|
+
value: 'profile',
|
|
662
|
+
},
|
|
663
|
+
new inquirer.Separator(dim(' ─────────────────────────────────────────────')),
|
|
664
|
+
{ name: ` ${dim('⬅ Back')}`, value: 'back' },
|
|
665
|
+
],
|
|
666
|
+
loop: false,
|
|
667
|
+
}]);
|
|
668
|
+
if (action === 'back')
|
|
669
|
+
return;
|
|
670
|
+
if (action === 'auto') {
|
|
671
|
+
await autoApply(program, profile);
|
|
672
|
+
}
|
|
673
|
+
else if (action === 'browse') {
|
|
674
|
+
await browseByTrack(program, profile);
|
|
675
|
+
}
|
|
676
|
+
else if (action === 'profile') {
|
|
677
|
+
profile = await setupProfile(profile);
|
|
678
|
+
// Reload program if slug changed
|
|
679
|
+
if (profile.programSlug !== program.slug) {
|
|
680
|
+
const s = ora(dim(` Switching to "${profile.programSlug}"…`)).start();
|
|
681
|
+
try {
|
|
682
|
+
program = await getWaveProgram(profile.programSlug);
|
|
683
|
+
s.succeed(` ${bold(program.name)} loaded`);
|
|
684
|
+
}
|
|
685
|
+
catch (e) {
|
|
686
|
+
s.fail(` ${e.message}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|