gitpadi 2.1.2 → 2.1.3
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 +426 -108
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/commands/drips.ts +493 -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,39 @@ 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
|
+
/** Detect which track an issue belongs to based on its labels and title */
|
|
100
|
+
function detectTrack(issue) {
|
|
101
|
+
const haystack = [...issueLabels(issue), issue.title.toLowerCase()].join(' ');
|
|
102
|
+
for (const [track, keywords] of Object.entries(TRACK_KEYWORDS)) {
|
|
103
|
+
if (keywords.some(kw => haystack.includes(kw)))
|
|
104
|
+
return track;
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
/** Score an issue against the user's tech stack. Returns 0 if no match. */
|
|
109
|
+
function scoreIssue(issue, techStack) {
|
|
110
|
+
const haystack = [...issueLabels(issue), issue.title.toLowerCase()].join(' ');
|
|
111
|
+
const matchReasons = [];
|
|
112
|
+
for (const tech of techStack) {
|
|
113
|
+
if (haystack.includes(tech.toLowerCase())) {
|
|
114
|
+
matchReasons.push(tech);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { score: matchReasons.length, matchReasons };
|
|
118
|
+
}
|
|
119
|
+
/** Filter issues matching the user's track preference */
|
|
120
|
+
function matchesTrack(issue, track) {
|
|
121
|
+
if (track === 'all')
|
|
122
|
+
return true;
|
|
123
|
+
const detected = detectTrack(issue);
|
|
124
|
+
if (detected === track)
|
|
125
|
+
return true;
|
|
126
|
+
// Also accept issues with no detected track when browsing 'all'
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
64
129
|
// ── API ───────────────────────────────────────────────────────────────────────
|
|
65
130
|
async function dripsGet(endpoint, token) {
|
|
66
131
|
const headers = { Accept: 'application/json' };
|
|
@@ -97,36 +162,41 @@ async function dripsPost(endpoint, body, token) {
|
|
|
97
162
|
return {};
|
|
98
163
|
}
|
|
99
164
|
}
|
|
165
|
+
async function fetchIssuePage(programId, page, limit = 50) {
|
|
166
|
+
return dripsGet(`/api/issues?waveProgramId=${programId}&state=open&page=${page}&limit=${limit}&sortBy=updatedAt`);
|
|
167
|
+
}
|
|
168
|
+
async function getWaveProgram(slug) {
|
|
169
|
+
const data = await dripsGet(`/api/wave-programs?slug=${encodeURIComponent(slug)}`);
|
|
170
|
+
if (data?.id)
|
|
171
|
+
return data;
|
|
172
|
+
if (Array.isArray(data?.data) && data.data.length > 0)
|
|
173
|
+
return data.data[0];
|
|
174
|
+
throw new Error(`Wave program "${slug}" not found. Check the slug or URL.`);
|
|
175
|
+
}
|
|
100
176
|
// ── Auth ──────────────────────────────────────────────────────────────────────
|
|
101
177
|
export async function ensureDripsAuth() {
|
|
102
178
|
const config = loadConfig();
|
|
103
179
|
if (config.dripsToken) {
|
|
104
|
-
// Validate by hitting a quota endpoint that requires auth
|
|
105
180
|
try {
|
|
106
181
|
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
182
|
if (res.status !== 401)
|
|
108
183
|
return config.dripsToken;
|
|
109
184
|
}
|
|
110
|
-
catch { /* network issue
|
|
185
|
+
catch { /* network issue */ }
|
|
111
186
|
}
|
|
112
|
-
// Show how-to instructions
|
|
113
187
|
console.log();
|
|
114
188
|
console.log(dim(' ┌─ Connect your Drips Network account ────────────────────────┐'));
|
|
115
189
|
console.log(dim(' │'));
|
|
116
|
-
console.log(dim(' │ Drips uses GitHub OAuth. Here\'s how to get your token:'));
|
|
117
|
-
console.log(dim(' │'));
|
|
118
190
|
console.log(dim(' │ 1. Log in at: ') + cyan(DRIPS_WEB + '/wave/login'));
|
|
119
|
-
console.log(dim(' │ (GitPadi will open it in your browser
|
|
191
|
+
console.log(dim(' │ (GitPadi will open it in your browser)'));
|
|
120
192
|
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'));
|
|
193
|
+
console.log(dim(' │ 2. After login, open DevTools:'));
|
|
194
|
+
console.log(dim(' │ ') + bold('F12') + dim(' → ') + bold('Application') + dim(' → ') + bold('Cookies') + dim(' → ') + cyan('www.drips.network'));
|
|
125
195
|
console.log(dim(' │'));
|
|
126
|
-
console.log(dim(' │ 3. Find the cookie
|
|
127
|
-
console.log(dim(' │ Copy
|
|
196
|
+
console.log(dim(' │ 3. Find the cookie: ') + yellow('wave_access_token'));
|
|
197
|
+
console.log(dim(' │ Copy its full value (starts with ') + dim('eyJ...') + dim(')'));
|
|
128
198
|
console.log(dim(' │'));
|
|
129
|
-
console.log(dim(' │
|
|
199
|
+
console.log(dim(' │ This is a one-time step — GitPadi saves it for future use.'));
|
|
130
200
|
console.log(dim(' │'));
|
|
131
201
|
console.log(dim(' └─────────────────────────────────────────────────────────────┘'));
|
|
132
202
|
console.log();
|
|
@@ -152,79 +222,279 @@ export async function ensureDripsAuth() {
|
|
|
152
222
|
if (!v || v.trim().length < 20)
|
|
153
223
|
return 'Token seems too short — copy the full value from DevTools';
|
|
154
224
|
if (!v.trim().startsWith('eyJ'))
|
|
155
|
-
return 'Expected a JWT starting with eyJ —
|
|
225
|
+
return 'Expected a JWT starting with eyJ — check you copied wave_access_token';
|
|
156
226
|
return true;
|
|
157
227
|
},
|
|
158
228
|
}]);
|
|
159
229
|
const t = token.trim();
|
|
160
230
|
saveDripsToken(t);
|
|
161
|
-
console.log(green('\n ✅ Drips session saved
|
|
231
|
+
console.log(green('\n ✅ Drips session saved.\n'));
|
|
162
232
|
return t;
|
|
163
233
|
}
|
|
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() {
|
|
234
|
+
// ── Profile setup ─────────────────────────────────────────────────────────────
|
|
235
|
+
async function setupProfile(existing) {
|
|
179
236
|
console.log();
|
|
180
|
-
console.log(bold(' Drips
|
|
181
|
-
console.log(dim(' Browse issues from any Wave program and apply in seconds.'));
|
|
237
|
+
console.log(bold(' Set up your Drips profile — GitPadi uses this to find the right issues for you.'));
|
|
182
238
|
console.log();
|
|
183
|
-
const {
|
|
239
|
+
const { programInput } = await inquirer.prompt([{
|
|
184
240
|
type: 'input',
|
|
185
|
-
name: '
|
|
186
|
-
message: bold('
|
|
187
|
-
default: 'stellar',
|
|
188
|
-
validate: (v) => v.trim().length > 0 || 'Required',
|
|
241
|
+
name: 'programInput',
|
|
242
|
+
message: bold('Drips Wave program (slug or URL):'),
|
|
243
|
+
default: existing?.programSlug || 'stellar',
|
|
189
244
|
}]);
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
245
|
+
const { track } = await inquirer.prompt([{
|
|
246
|
+
type: 'list',
|
|
247
|
+
name: 'track',
|
|
248
|
+
message: bold('What is your track?'),
|
|
249
|
+
default: existing?.track || 'backend',
|
|
250
|
+
choices: [
|
|
251
|
+
{ name: ` ${cyan('⚙️')} Backend ${dim('— APIs, servers, databases, Node.js, Rust, Go...')}`, value: 'backend' },
|
|
252
|
+
{ name: ` ${magenta('🎨')} Frontend ${dim('— React, Svelte, Vue, CSS, mobile...')}`, value: 'frontend' },
|
|
253
|
+
{ name: ` ${yellow('📜')} Smart Contract ${dim('— Solidity, EVM, Anchor, DeFi, Web3...')}`, value: 'contract' },
|
|
254
|
+
{ name: ` ${green('🌐')} All tracks ${dim('— show everything, filter by tech stack only')}`, value: 'all' },
|
|
255
|
+
],
|
|
256
|
+
}]);
|
|
257
|
+
const { stackInput } = await inquirer.prompt([{
|
|
258
|
+
type: 'input',
|
|
259
|
+
name: 'stackInput',
|
|
260
|
+
message: bold('Your tech stack (comma-separated):'),
|
|
261
|
+
default: existing?.techStack.join(', ') || 'TypeScript, Node.js',
|
|
262
|
+
validate: (v) => v.trim().length > 0 || 'Enter at least one technology',
|
|
263
|
+
}]);
|
|
264
|
+
const { minPoints } = await inquirer.prompt([{
|
|
265
|
+
type: 'list',
|
|
266
|
+
name: 'minPoints',
|
|
267
|
+
message: bold('Minimum points to apply for:'),
|
|
268
|
+
default: existing?.minPoints || 0,
|
|
269
|
+
choices: [
|
|
270
|
+
{ name: ` Any points (including unspecified)`, value: 0 },
|
|
271
|
+
{ name: ` 50+ pts`, value: 50 },
|
|
272
|
+
{ name: ` 100+ pts`, value: 100 },
|
|
273
|
+
{ name: ` 200+ pts`, value: 200 },
|
|
274
|
+
{ name: ` 500+ pts`, value: 500 },
|
|
275
|
+
],
|
|
276
|
+
}]);
|
|
277
|
+
const techStack = stackInput.split(',').map((s) => s.trim()).filter(Boolean);
|
|
278
|
+
const programSlug = parseSlug(programInput.trim());
|
|
279
|
+
const profile = { track, techStack, minPoints, programSlug };
|
|
280
|
+
saveProfile(profile);
|
|
281
|
+
console.log(green(`\n ✅ Profile saved — track: ${bold(track)}, stack: ${techStack.join(', ')}, min: ${minPoints}pts\n`));
|
|
282
|
+
return profile;
|
|
283
|
+
}
|
|
284
|
+
// ── Auto-apply ────────────────────────────────────────────────────────────────
|
|
285
|
+
async function autoApply(program, profile) {
|
|
286
|
+
console.log();
|
|
287
|
+
console.log(bold(` Scanning ${program.name} issues for matches…`));
|
|
288
|
+
console.log(dim(` Track: ${profile.track} | Stack: ${profile.techStack.join(', ')} | Min: ${profile.minPoints}pts`));
|
|
289
|
+
console.log();
|
|
290
|
+
const spinner = ora(dim(' Fetching open issues…')).start();
|
|
291
|
+
const scored = [];
|
|
292
|
+
let page = 1;
|
|
293
|
+
const MAX_PAGES = 10; // scan up to 500 issues
|
|
294
|
+
while (page <= MAX_PAGES) {
|
|
295
|
+
let data;
|
|
296
|
+
let pagination;
|
|
297
|
+
try {
|
|
298
|
+
const res = await fetchIssuePage(program.id, page, 50);
|
|
299
|
+
data = res.data;
|
|
300
|
+
pagination = res.pagination;
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
spinner.fail(` Fetch error: ${e.message}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
for (const issue of data) {
|
|
307
|
+
// Skip assigned issues
|
|
308
|
+
if (issue.assignedApplicant !== null)
|
|
309
|
+
continue;
|
|
310
|
+
// Skip below min points (only if points are set)
|
|
311
|
+
if (profile.minPoints > 0 && issue.points !== null && issue.points < profile.minPoints)
|
|
312
|
+
continue;
|
|
313
|
+
// Skip if track doesn't match
|
|
314
|
+
if (!matchesTrack(issue, profile.track))
|
|
315
|
+
continue;
|
|
316
|
+
// Score against tech stack
|
|
317
|
+
const { score, matchReasons } = scoreIssue(issue, profile.techStack);
|
|
318
|
+
// For 'all' track, accept any score >= 0. For specific tracks, require at least a label match
|
|
319
|
+
if (profile.track !== 'all' && score === 0) {
|
|
320
|
+
// Still include it if it clearly matches the track (track filter already handled above)
|
|
321
|
+
// but boost score so track-matching issues without stack matches still show up
|
|
322
|
+
scored.push({ issue, score: 0.5, matchReasons: [profile.track] });
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
scored.push({ issue, score, matchReasons: score > 0 ? matchReasons : [profile.track] });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
spinner.text = dim(` Scanned ${page * 50} issues, found ${scored.length} matches so far…`);
|
|
329
|
+
if (!pagination.hasNextPage || scored.length >= 50)
|
|
330
|
+
break;
|
|
331
|
+
page++;
|
|
332
|
+
}
|
|
333
|
+
// Sort: by score desc, then by points desc
|
|
334
|
+
scored.sort((a, b) => {
|
|
335
|
+
if (b.score !== a.score)
|
|
336
|
+
return b.score - a.score;
|
|
337
|
+
return (b.issue.points || 0) - (a.issue.points || 0);
|
|
338
|
+
});
|
|
339
|
+
const top = scored.slice(0, 30);
|
|
340
|
+
spinner.succeed(` Found ${scored.length} matching issue(s) — showing top ${top.length}`);
|
|
341
|
+
if (top.length === 0) {
|
|
342
|
+
console.log(yellow('\n No matching unassigned issues found. Try broadening your tech stack or lowering min points.\n'));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
console.log();
|
|
346
|
+
// Checkbox selection
|
|
347
|
+
const checkboxChoices = top.map(({ issue, score, matchReasons }) => {
|
|
348
|
+
const pts = issue.points ? green(`+${issue.points}pts`) : dim('—pts');
|
|
349
|
+
const applicants = issue.pendingApplicationsCount > 0 ? yellow(`${issue.pendingApplicationsCount}▲`) : dim('0▲');
|
|
350
|
+
const match = matchReasons.length > 0 ? cyan(`[${matchReasons.slice(0, 3).join(', ')}]`) : '';
|
|
351
|
+
const title = truncate(issue.title, 44);
|
|
352
|
+
return {
|
|
353
|
+
name: ` ${pts} ${bold(title)} ${match} ${applicants}`,
|
|
354
|
+
value: issue,
|
|
355
|
+
checked: score >= 1, // pre-select strong matches
|
|
356
|
+
};
|
|
357
|
+
});
|
|
358
|
+
const { selectedIssues } = await inquirer.prompt([{
|
|
359
|
+
type: 'checkbox',
|
|
360
|
+
name: 'selectedIssues',
|
|
361
|
+
message: bold('Select issues to apply for (Space to toggle, Enter to confirm):'),
|
|
362
|
+
choices: checkboxChoices,
|
|
363
|
+
pageSize: 20,
|
|
364
|
+
validate: (selected) => selected.length > 0 || 'Select at least one issue',
|
|
365
|
+
}]);
|
|
366
|
+
if (!selectedIssues || selectedIssues.length === 0) {
|
|
367
|
+
console.log(dim('\n No issues selected.\n'));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
console.log();
|
|
371
|
+
console.log(bold(` ${selectedIssues.length} issue(s) selected. Composing application message…`));
|
|
372
|
+
console.log();
|
|
373
|
+
// Single application message for all
|
|
374
|
+
const { style } = await inquirer.prompt([{
|
|
375
|
+
type: 'list',
|
|
376
|
+
name: 'style',
|
|
377
|
+
message: bold('Application message for all selected issues:'),
|
|
378
|
+
choices: [
|
|
379
|
+
{ name: ` ${green('⚡')} Quick — standard intro message`, value: 'quick' },
|
|
380
|
+
{ name: ` ${cyan('📝')} Custom — write one message for all`, value: 'custom' },
|
|
381
|
+
],
|
|
382
|
+
}]);
|
|
383
|
+
let baseMessage;
|
|
384
|
+
if (style === 'quick') {
|
|
385
|
+
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)*`;
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
const { msg } = await inquirer.prompt([{
|
|
389
|
+
type: 'input',
|
|
390
|
+
name: 'msg',
|
|
391
|
+
message: bold('Your message (applies to all selected issues):'),
|
|
392
|
+
validate: (v) => v.trim().length >= 10 || 'Write at least 10 characters',
|
|
393
|
+
}]);
|
|
394
|
+
baseMessage = `${msg.trim()}\n\n*Applied via [GitPadi](https://github.com/Netwalls/contributor-agent)*`;
|
|
395
|
+
}
|
|
396
|
+
const { go } = await inquirer.prompt([{
|
|
397
|
+
type: 'confirm',
|
|
398
|
+
name: 'go',
|
|
399
|
+
message: bold(`Apply to ${selectedIssues.length} issue(s) now?`),
|
|
400
|
+
default: true,
|
|
401
|
+
}]);
|
|
402
|
+
if (!go)
|
|
403
|
+
return;
|
|
404
|
+
// Auth
|
|
405
|
+
let token;
|
|
193
406
|
try {
|
|
194
|
-
|
|
195
|
-
progSpinner.succeed(` ${bold(program.name)} · ${cyan(program.issueCount.toLocaleString() + ' open issues')} · ${green(program.presetBudgetUSD + '/mo')} · ${dim(program.approvedRepoCount + ' repos')}`);
|
|
407
|
+
token = await ensureDripsAuth();
|
|
196
408
|
}
|
|
197
|
-
catch
|
|
198
|
-
|
|
409
|
+
catch {
|
|
410
|
+
console.log(red('\n Authentication cancelled.\n'));
|
|
199
411
|
return;
|
|
200
412
|
}
|
|
413
|
+
console.log();
|
|
414
|
+
let applied = 0;
|
|
415
|
+
let failed = 0;
|
|
416
|
+
for (const issue of selectedIssues) {
|
|
417
|
+
const spinner = ora(` Applying to: ${truncate(issue.title, 50)}…`).start();
|
|
418
|
+
try {
|
|
419
|
+
await dripsPost(`/api/wave-programs/${program.id}/issues/${issue.id}/applications`, { applicationText: baseMessage }, token);
|
|
420
|
+
spinner.succeed(green(` ✅ ${truncate(issue.title, 55)}`) + (issue.points ? dim(` (+${issue.points}pts)`) : ''));
|
|
421
|
+
applied++;
|
|
422
|
+
}
|
|
423
|
+
catch (e) {
|
|
424
|
+
spinner.fail(red(` ✗ ${truncate(issue.title, 55)} — ${e.message}`));
|
|
425
|
+
failed++;
|
|
426
|
+
if (e.message.startsWith('401')) {
|
|
427
|
+
saveDripsToken('');
|
|
428
|
+
console.log(yellow('\n Session expired — run again to re-authenticate.\n'));
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
console.log();
|
|
434
|
+
console.log(bold(` Done: ${green(applied + ' applied')}${failed > 0 ? ', ' + red(failed + ' failed') : ''}`));
|
|
435
|
+
console.log(dim(`\n Track your applications: ${cyan(DRIPS_WEB + '/wave/' + program.slug)}\n`));
|
|
436
|
+
}
|
|
437
|
+
// ── Browse by track ───────────────────────────────────────────────────────────
|
|
438
|
+
async function browseByTrack(program, profile) {
|
|
439
|
+
// Ask which track to browse if they want to override
|
|
440
|
+
const { browseTrack } = await inquirer.prompt([{
|
|
441
|
+
type: 'list',
|
|
442
|
+
name: 'browseTrack',
|
|
443
|
+
message: bold('Browse which track?'),
|
|
444
|
+
default: profile.track,
|
|
445
|
+
choices: [
|
|
446
|
+
{ name: ` ${cyan('⚙️')} Backend`, value: 'backend' },
|
|
447
|
+
{ name: ` ${magenta('🎨')} Frontend`, value: 'frontend' },
|
|
448
|
+
{ name: ` ${yellow('📜')} Smart Contract`, value: 'contract' },
|
|
449
|
+
{ name: ` ${green('🌐')} All tracks`, value: 'all' },
|
|
450
|
+
],
|
|
451
|
+
}]);
|
|
452
|
+
const selectedTrack = browseTrack;
|
|
201
453
|
let page = 1;
|
|
202
454
|
while (true) {
|
|
203
|
-
const
|
|
204
|
-
let
|
|
205
|
-
let
|
|
455
|
+
const spinner = ora(dim(` Fetching unassigned issues (page ${page})…`)).start();
|
|
456
|
+
let allIssues = [];
|
|
457
|
+
let hasNextPage = false;
|
|
458
|
+
let totalFetched = 0;
|
|
459
|
+
// Fetch a batch and filter client-side for unassigned + track
|
|
206
460
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
461
|
+
// Fetch 2 pages worth (100 issues) to ensure we have enough unassigned after filtering
|
|
462
|
+
const batchStart = (page - 1) * 2 + 1;
|
|
463
|
+
const pageA = await fetchIssuePage(program.id, batchStart, 50);
|
|
464
|
+
const pageB = pageA.pagination.hasNextPage
|
|
465
|
+
? await fetchIssuePage(program.id, batchStart + 1, 50)
|
|
466
|
+
: { data: [], pagination: { ...pageA.pagination, hasNextPage: false } };
|
|
467
|
+
const raw = [...pageA.data, ...pageB.data];
|
|
468
|
+
totalFetched = raw.length;
|
|
469
|
+
// Filter: unassigned + track + min points
|
|
470
|
+
allIssues = raw.filter(i => i.assignedApplicant === null &&
|
|
471
|
+
matchesTrack(i, selectedTrack) &&
|
|
472
|
+
(profile.minPoints === 0 || i.points === null || i.points >= profile.minPoints));
|
|
473
|
+
// Sort by points descending (highest reward first)
|
|
474
|
+
allIssues.sort((a, b) => (b.points || 0) - (a.points || 0));
|
|
475
|
+
hasNextPage = pageB.pagination.hasNextPage || (pageA.pagination.hasNextPage && !pageB.pagination.hasNextPage);
|
|
476
|
+
spinner.succeed(` ${allIssues.length} unassigned ${selectedTrack} issue(s) found (from ${totalFetched} scanned) — sorted by points`);
|
|
211
477
|
}
|
|
212
478
|
catch (e) {
|
|
213
|
-
|
|
479
|
+
spinner.fail(` ${e.message}`);
|
|
214
480
|
return;
|
|
215
481
|
}
|
|
482
|
+
if (allIssues.length === 0) {
|
|
483
|
+
console.log(yellow(`\n No unassigned ${selectedTrack} issues found on this page. Try next page or change track.\n`));
|
|
484
|
+
}
|
|
216
485
|
console.log();
|
|
217
|
-
const choices =
|
|
486
|
+
const choices = allIssues.slice(0, 20).map((issue) => {
|
|
218
487
|
const age = diffDays(issue.updatedAt);
|
|
219
488
|
const ageStr = age === 0 ? dim('today') : dim(`${age}d ago`);
|
|
220
|
-
const pts = issue.points ? green(`+${issue.points}pts`) : dim('
|
|
489
|
+
const pts = issue.points ? green(`+${issue.points}pts`) : dim(' —pts');
|
|
221
490
|
const applicants = issue.pendingApplicationsCount > 0
|
|
222
|
-
? yellow(`${issue.pendingApplicationsCount}
|
|
223
|
-
: dim('0
|
|
224
|
-
const
|
|
225
|
-
const
|
|
491
|
+
? yellow(`${issue.pendingApplicationsCount}▲`)
|
|
492
|
+
: dim('0▲');
|
|
493
|
+
const detected = detectTrack(issue);
|
|
494
|
+
const trackTag = detected ? dim(`[${detected}]`) : '';
|
|
495
|
+
const title = truncate(issue.title, 46);
|
|
226
496
|
return {
|
|
227
|
-
name: ` ${pts} ${bold(title)} ${
|
|
497
|
+
name: ` ${pts} ${bold(title)} ${trackTag} ${applicants} ${ageStr}`,
|
|
228
498
|
value: issue,
|
|
229
499
|
short: issue.title,
|
|
230
500
|
};
|
|
@@ -232,7 +502,7 @@ export async function dripsMenu() {
|
|
|
232
502
|
const nav = [
|
|
233
503
|
new inquirer.Separator(dim(' ─────────────────────────────────────────────────────')),
|
|
234
504
|
];
|
|
235
|
-
if (
|
|
505
|
+
if (hasNextPage)
|
|
236
506
|
nav.push({ name: ` ${dim('→ Next page')}`, value: '__next__' });
|
|
237
507
|
if (page > 1)
|
|
238
508
|
nav.push({ name: ` ${dim('← Previous page')}`, value: '__prev__' });
|
|
@@ -240,8 +510,8 @@ export async function dripsMenu() {
|
|
|
240
510
|
const { selected } = await inquirer.prompt([{
|
|
241
511
|
type: 'list',
|
|
242
512
|
name: 'selected',
|
|
243
|
-
message: bold(`${program.name}
|
|
244
|
-
choices: [...choices, ...nav],
|
|
513
|
+
message: bold(`${program.name} — ${selectedTrack} issues sorted by points:`),
|
|
514
|
+
choices: allIssues.length > 0 ? [...choices, ...nav] : nav,
|
|
245
515
|
pageSize: 18,
|
|
246
516
|
}]);
|
|
247
517
|
if (selected === '__back__')
|
|
@@ -254,32 +524,28 @@ export async function dripsMenu() {
|
|
|
254
524
|
page = Math.max(1, page - 1);
|
|
255
525
|
continue;
|
|
256
526
|
}
|
|
257
|
-
await
|
|
527
|
+
await applySingle(program, selected, profile);
|
|
258
528
|
}
|
|
259
529
|
}
|
|
260
|
-
// ── Apply
|
|
261
|
-
async function
|
|
530
|
+
// ── Apply single ──────────────────────────────────────────────────────────────
|
|
531
|
+
async function applySingle(program, issue, profile) {
|
|
262
532
|
console.log();
|
|
263
533
|
console.log(` ${bold(cyan(truncate(issue.title, 72)))}`);
|
|
264
534
|
if (issue.repo)
|
|
265
535
|
console.log(` ${dim('Repo: ')} ${issue.repo.fullName}`);
|
|
266
536
|
console.log(` ${dim('Points: ')} ${issue.points ? green('+' + issue.points + ' pts') : dim('—')}`);
|
|
267
537
|
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) {
|
|
538
|
+
if (issue.gitHubIssueUrl)
|
|
272
539
|
console.log(` ${dim('GitHub: ')} ${dim(issue.gitHubIssueUrl)}`);
|
|
273
|
-
}
|
|
274
540
|
console.log();
|
|
275
541
|
const { style } = await inquirer.prompt([{
|
|
276
542
|
type: 'list',
|
|
277
543
|
name: 'style',
|
|
278
|
-
message: bold('
|
|
544
|
+
message: bold('Application style:'),
|
|
279
545
|
choices: [
|
|
280
|
-
{ name: ` ${green('⚡')} Quick
|
|
281
|
-
{ name: ` ${cyan('📝')} Custom
|
|
282
|
-
new inquirer.Separator(dim('
|
|
546
|
+
{ name: ` ${green('⚡')} Quick — standard intro`, value: 'quick' },
|
|
547
|
+
{ name: ` ${cyan('📝')} Custom — write your own`, value: 'custom' },
|
|
548
|
+
new inquirer.Separator(dim(' ────────────────')),
|
|
283
549
|
{ name: ` ${dim('⬅ Cancel')}`, value: 'cancel' },
|
|
284
550
|
],
|
|
285
551
|
}]);
|
|
@@ -287,45 +553,30 @@ async function applyToDripsIssue(program, issue) {
|
|
|
287
553
|
return;
|
|
288
554
|
let applicationText;
|
|
289
555
|
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');
|
|
556
|
+
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
557
|
}
|
|
298
558
|
else {
|
|
299
559
|
const { msg } = await inquirer.prompt([{
|
|
300
560
|
type: 'input',
|
|
301
561
|
name: 'msg',
|
|
302
|
-
message: bold('Your
|
|
303
|
-
validate: (v) => v.trim().length >= 10 || '
|
|
562
|
+
message: bold('Your message:'),
|
|
563
|
+
validate: (v) => v.trim().length >= 10 || 'Write at least 10 characters',
|
|
304
564
|
}]);
|
|
305
|
-
applicationText = [
|
|
306
|
-
msg.trim(),
|
|
307
|
-
``,
|
|
308
|
-
`*Applied via [GitPadi](https://github.com/Netwalls/contributor-agent)*`,
|
|
309
|
-
].join('\n');
|
|
565
|
+
applicationText = `${msg.trim()}\n\n*Applied via [GitPadi](https://github.com/Netwalls/contributor-agent)*`;
|
|
310
566
|
}
|
|
311
|
-
// Preview
|
|
312
567
|
console.log();
|
|
313
|
-
console.log(dim(' ── Preview
|
|
568
|
+
console.log(dim(' ── Preview ────────────────────────────────────────────────────'));
|
|
314
569
|
applicationText.split('\n').forEach(l => console.log(` ${dim(l)}`));
|
|
315
|
-
console.log(dim('
|
|
570
|
+
console.log(dim(' ───────────────────────────────────────────────────────────────'));
|
|
316
571
|
console.log();
|
|
317
572
|
const { confirm } = await inquirer.prompt([{
|
|
318
|
-
type: '
|
|
573
|
+
type: 'confirm',
|
|
319
574
|
name: 'confirm',
|
|
320
|
-
message: bold('Submit
|
|
321
|
-
|
|
322
|
-
{ name: ` ${green('✅ Yes, apply')}`, value: 'yes' },
|
|
323
|
-
{ name: ` ${dim('❌ Cancel')}`, value: 'no' },
|
|
324
|
-
],
|
|
575
|
+
message: bold('Submit application?'),
|
|
576
|
+
default: true,
|
|
325
577
|
}]);
|
|
326
|
-
if (confirm
|
|
578
|
+
if (!confirm)
|
|
327
579
|
return;
|
|
328
|
-
// Ensure auth only when the user actually commits to applying
|
|
329
580
|
let token;
|
|
330
581
|
try {
|
|
331
582
|
token = await ensureDripsAuth();
|
|
@@ -334,12 +585,11 @@ async function applyToDripsIssue(program, issue) {
|
|
|
334
585
|
console.log(red('\n Authentication cancelled.\n'));
|
|
335
586
|
return;
|
|
336
587
|
}
|
|
337
|
-
const spinner = ora(' Submitting
|
|
588
|
+
const spinner = ora(' Submitting…').start();
|
|
338
589
|
try {
|
|
339
590
|
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`));
|
|
591
|
+
spinner.succeed(green(` ✅ Applied! ${bold(issue.title)}`));
|
|
592
|
+
console.log(dim(`\n Track: ${cyan(DRIPS_WEB + '/wave/' + program.slug)}\n`));
|
|
343
593
|
}
|
|
344
594
|
catch (e) {
|
|
345
595
|
spinner.fail(red(` Failed: ${e.message}`));
|
|
@@ -349,3 +599,71 @@ async function applyToDripsIssue(program, issue) {
|
|
|
349
599
|
}
|
|
350
600
|
}
|
|
351
601
|
}
|
|
602
|
+
// ── Main menu ─────────────────────────────────────────────────────────────────
|
|
603
|
+
export async function dripsMenu() {
|
|
604
|
+
// Load or set up profile first
|
|
605
|
+
let profile = loadProfile();
|
|
606
|
+
if (!profile) {
|
|
607
|
+
console.log();
|
|
608
|
+
console.log(bold(' Welcome to Drips Network! Let\'s set up your profile first.'));
|
|
609
|
+
profile = await setupProfile();
|
|
610
|
+
}
|
|
611
|
+
// Load the wave program
|
|
612
|
+
const progSpinner = ora(dim(` Loading "${profile.programSlug}" wave program…`)).start();
|
|
613
|
+
let program;
|
|
614
|
+
try {
|
|
615
|
+
program = await getWaveProgram(profile.programSlug);
|
|
616
|
+
progSpinner.succeed(` ${bold(program.name)} · ${cyan(program.issueCount.toLocaleString() + ' issues')} · ${green(program.presetBudgetUSD + '/mo')} · ${dim(program.approvedRepoCount + ' repos')}`);
|
|
617
|
+
}
|
|
618
|
+
catch (e) {
|
|
619
|
+
progSpinner.fail(` ${e.message}`);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
while (true) {
|
|
623
|
+
console.log();
|
|
624
|
+
const { action } = await inquirer.prompt([{
|
|
625
|
+
type: 'list',
|
|
626
|
+
name: 'action',
|
|
627
|
+
message: bold(`Drips Network — ${program.name} Wave`),
|
|
628
|
+
choices: [
|
|
629
|
+
{
|
|
630
|
+
name: ` ${green('🤖')} ${bold('Auto-Apply for Me')} ${dim('— scan issues, match my stack, batch-apply')}`,
|
|
631
|
+
value: 'auto',
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: ` ${cyan('🔍')} ${bold('Browse by Track')} ${dim('— unassigned only, sorted by points, filter by track')}`,
|
|
635
|
+
value: 'browse',
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
name: ` ${dim('⚙️')} ${dim('Update my Profile')} ${dim('— change track, tech stack, min points')}`,
|
|
639
|
+
value: 'profile',
|
|
640
|
+
},
|
|
641
|
+
new inquirer.Separator(dim(' ─────────────────────────────────────────────')),
|
|
642
|
+
{ name: ` ${dim('⬅ Back')}`, value: 'back' },
|
|
643
|
+
],
|
|
644
|
+
loop: false,
|
|
645
|
+
}]);
|
|
646
|
+
if (action === 'back')
|
|
647
|
+
return;
|
|
648
|
+
if (action === 'auto') {
|
|
649
|
+
await autoApply(program, profile);
|
|
650
|
+
}
|
|
651
|
+
else if (action === 'browse') {
|
|
652
|
+
await browseByTrack(program, profile);
|
|
653
|
+
}
|
|
654
|
+
else if (action === 'profile') {
|
|
655
|
+
profile = await setupProfile(profile);
|
|
656
|
+
// Reload program if slug changed
|
|
657
|
+
if (profile.programSlug !== program.slug) {
|
|
658
|
+
const s = ora(dim(` Switching to "${profile.programSlug}"…`)).start();
|
|
659
|
+
try {
|
|
660
|
+
program = await getWaveProgram(profile.programSlug);
|
|
661
|
+
s.succeed(` ${bold(program.name)} loaded`);
|
|
662
|
+
}
|
|
663
|
+
catch (e) {
|
|
664
|
+
s.fail(` ${e.message}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|