gitpadi 2.1.1 → 2.1.2
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 +6 -1
- package/dist/commands/drips.js +351 -0
- package/package.json +1 -1
- package/src/cli.ts +5 -1
- package/src/commands/drips.ts +408 -0
package/dist/cli.js
CHANGED
|
@@ -22,10 +22,11 @@ import * as releases from './commands/releases.js';
|
|
|
22
22
|
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
|
+
import { dripsMenu } from './commands/drips.js';
|
|
25
26
|
import * as gitlabIssues from './commands/gitlab-issues.js';
|
|
26
27
|
import * as gitlabMRs from './commands/gitlab-mrs.js';
|
|
27
28
|
import * as gitlabPipelines from './commands/gitlab-pipelines.js';
|
|
28
|
-
const VERSION = '2.1.
|
|
29
|
+
const VERSION = '2.1.2';
|
|
29
30
|
let targetConfirmed = false;
|
|
30
31
|
let gitlabProjectConfirmed = false;
|
|
31
32
|
// ── Styling ────────────────────────────────────────────────────────────
|
|
@@ -602,6 +603,7 @@ async function mainMenu() {
|
|
|
602
603
|
{ name: `${cyan('✨')} ${bold('Contributor Mode')} ${dim('— fork, clone, sync, submit PRs')}`, value: 'contributor' },
|
|
603
604
|
{ name: `${magenta('🛠️')} ${bold('Maintainer Mode')} ${dim('— manage issues, PRs, contributors')}`, value: 'maintainer' },
|
|
604
605
|
{ name: `${yellow('🏫')} ${bold('Organization/School')} ${dim('— assignments, grading, leaderboard')}`, value: 'org' },
|
|
606
|
+
{ name: `${cyan('🌊')} ${bold('Drips Network')} ${dim('— apply for bounty issues on drips.network')}`, value: 'drips' },
|
|
605
607
|
new inquirer.Separator(dim(' ─────────────────────────────')),
|
|
606
608
|
{ name: `${dim('🔄')} ${dim('Switch Platform')}`, value: 'switch' },
|
|
607
609
|
{ name: `${dim('👋')} ${dim('Exit')}`, value: 'exit' },
|
|
@@ -624,6 +626,9 @@ async function mainMenu() {
|
|
|
624
626
|
await ensureTargetRepo();
|
|
625
627
|
await safeMenu(orgMenu);
|
|
626
628
|
}
|
|
629
|
+
else if (mode === 'drips') {
|
|
630
|
+
await safeMenu(dripsMenu);
|
|
631
|
+
}
|
|
627
632
|
}
|
|
628
633
|
else {
|
|
629
634
|
// GitLab mode
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// commands/drips.ts — Apply for bounty issues on Drips Network from your terminal
|
|
2
|
+
//
|
|
3
|
+
// API reverse-engineered from https://github.com/drips-network/app
|
|
4
|
+
// Base: https://wave-api.drips.network
|
|
5
|
+
// Auth: GitHub OAuth → JWT cookie (wave_access_token)
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
const DRIPS_API = 'https://wave-api.drips.network';
|
|
14
|
+
const DRIPS_WEB = 'https://www.drips.network';
|
|
15
|
+
const CONFIG_DIR = path.join(os.homedir(), '.gitpadi');
|
|
16
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
17
|
+
const dim = chalk.dim;
|
|
18
|
+
const cyan = chalk.cyanBright;
|
|
19
|
+
const yellow = chalk.yellowBright;
|
|
20
|
+
const green = chalk.greenBright;
|
|
21
|
+
const bold = chalk.bold;
|
|
22
|
+
const red = chalk.redBright;
|
|
23
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
24
|
+
function loadConfig() {
|
|
25
|
+
if (!fs.existsSync(CONFIG_FILE))
|
|
26
|
+
return {};
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function saveDripsToken(token) {
|
|
35
|
+
const current = loadConfig();
|
|
36
|
+
if (!fs.existsSync(CONFIG_DIR))
|
|
37
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
38
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, dripsToken: token }, null, 2));
|
|
39
|
+
}
|
|
40
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
41
|
+
function truncate(text, max) {
|
|
42
|
+
return text.length <= max ? text : text.slice(0, max - 1) + '…';
|
|
43
|
+
}
|
|
44
|
+
function diffDays(dateStr) {
|
|
45
|
+
return Math.floor((Date.now() - new Date(dateStr).getTime()) / 86_400_000);
|
|
46
|
+
}
|
|
47
|
+
function openBrowser(url) {
|
|
48
|
+
try {
|
|
49
|
+
if (process.platform === 'darwin')
|
|
50
|
+
execSync(`open "${url}"`, { stdio: 'ignore' });
|
|
51
|
+
else if (process.platform === 'win32')
|
|
52
|
+
execSync(`start "" "${url}"`, { stdio: 'ignore' });
|
|
53
|
+
else
|
|
54
|
+
execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
|
|
55
|
+
}
|
|
56
|
+
catch { /* ignore */ }
|
|
57
|
+
}
|
|
58
|
+
function parseSlug(input) {
|
|
59
|
+
const m = input.match(/drips\.network\/wave\/([^/?#\s]+)/);
|
|
60
|
+
if (m)
|
|
61
|
+
return m[1];
|
|
62
|
+
return input.replace(/^\/+|\/+$/g, '').split('/').pop() || input;
|
|
63
|
+
}
|
|
64
|
+
// ── API ───────────────────────────────────────────────────────────────────────
|
|
65
|
+
async function dripsGet(endpoint, token) {
|
|
66
|
+
const headers = { Accept: 'application/json' };
|
|
67
|
+
if (token)
|
|
68
|
+
headers['Cookie'] = `wave_access_token=${token}`;
|
|
69
|
+
const res = await fetch(`${DRIPS_API}${endpoint}`, { headers });
|
|
70
|
+
if (!res.ok)
|
|
71
|
+
throw new Error(`Drips API ${res.status} on ${endpoint}`);
|
|
72
|
+
return res.json();
|
|
73
|
+
}
|
|
74
|
+
async function dripsPost(endpoint, body, token) {
|
|
75
|
+
const res = await fetch(`${DRIPS_API}${endpoint}`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
Accept: 'application/json',
|
|
80
|
+
Cookie: `wave_access_token=${token}`,
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify(body),
|
|
83
|
+
});
|
|
84
|
+
const text = await res.text();
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
let msg = text;
|
|
87
|
+
try {
|
|
88
|
+
msg = JSON.parse(text)?.message || text;
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
throw new Error(`${res.status}: ${msg}`);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(text);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ── Auth ──────────────────────────────────────────────────────────────────────
|
|
101
|
+
export async function ensureDripsAuth() {
|
|
102
|
+
const config = loadConfig();
|
|
103
|
+
if (config.dripsToken) {
|
|
104
|
+
// Validate by hitting a quota endpoint that requires auth
|
|
105
|
+
try {
|
|
106
|
+
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
|
+
if (res.status !== 401)
|
|
108
|
+
return config.dripsToken;
|
|
109
|
+
}
|
|
110
|
+
catch { /* network issue, let user try anyway */ }
|
|
111
|
+
}
|
|
112
|
+
// Show how-to instructions
|
|
113
|
+
console.log();
|
|
114
|
+
console.log(dim(' ┌─ Connect your Drips Network account ────────────────────────┐'));
|
|
115
|
+
console.log(dim(' │'));
|
|
116
|
+
console.log(dim(' │ Drips uses GitHub OAuth. Here\'s how to get your token:'));
|
|
117
|
+
console.log(dim(' │'));
|
|
118
|
+
console.log(dim(' │ 1. Log in at: ') + cyan(DRIPS_WEB + '/wave/login'));
|
|
119
|
+
console.log(dim(' │ (GitPadi will open it in your browser now)'));
|
|
120
|
+
console.log(dim(' │'));
|
|
121
|
+
console.log(dim(' │ 2. After logging in with GitHub, open DevTools:'));
|
|
122
|
+
console.log(dim(' │ ') + bold('F12') + dim(' (or Cmd+Option+I on Mac)'));
|
|
123
|
+
console.log(dim(' │ → ') + bold('Application') + dim(' tab'));
|
|
124
|
+
console.log(dim(' │ → ') + bold('Cookies') + dim(' → ') + cyan('www.drips.network'));
|
|
125
|
+
console.log(dim(' │'));
|
|
126
|
+
console.log(dim(' │ 3. Find the cookie named ') + yellow('wave_access_token'));
|
|
127
|
+
console.log(dim(' │ Copy the full value (it starts with ') + dim('eyJ...') + dim(')'));
|
|
128
|
+
console.log(dim(' │'));
|
|
129
|
+
console.log(dim(' │ GitPadi saves it once — you won\'t need to do this again.'));
|
|
130
|
+
console.log(dim(' │'));
|
|
131
|
+
console.log(dim(' └─────────────────────────────────────────────────────────────┘'));
|
|
132
|
+
console.log();
|
|
133
|
+
const { launch } = await inquirer.prompt([{
|
|
134
|
+
type: 'confirm',
|
|
135
|
+
name: 'launch',
|
|
136
|
+
message: 'Open drips.network login in your browser?',
|
|
137
|
+
default: true,
|
|
138
|
+
}]);
|
|
139
|
+
if (launch) {
|
|
140
|
+
openBrowser(`${DRIPS_WEB}/wave/login?skipWelcome=false`);
|
|
141
|
+
console.log(dim('\n Browser opened. Log in with GitHub, then come back here.\n'));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
console.log(dim(`\n Open manually: ${cyan(DRIPS_WEB + '/wave/login')}\n`));
|
|
145
|
+
}
|
|
146
|
+
const { token } = await inquirer.prompt([{
|
|
147
|
+
type: 'password',
|
|
148
|
+
name: 'token',
|
|
149
|
+
message: cyan('Paste your wave_access_token cookie value:'),
|
|
150
|
+
mask: '•',
|
|
151
|
+
validate: (v) => {
|
|
152
|
+
if (!v || v.trim().length < 20)
|
|
153
|
+
return 'Token seems too short — copy the full value from DevTools';
|
|
154
|
+
if (!v.trim().startsWith('eyJ'))
|
|
155
|
+
return 'Expected a JWT starting with eyJ — make sure you copied wave_access_token, not another cookie';
|
|
156
|
+
return true;
|
|
157
|
+
},
|
|
158
|
+
}]);
|
|
159
|
+
const t = token.trim();
|
|
160
|
+
saveDripsToken(t);
|
|
161
|
+
console.log(green('\n ✅ Drips session saved — you\'re all set!\n'));
|
|
162
|
+
return t;
|
|
163
|
+
}
|
|
164
|
+
// ── Program lookup ────────────────────────────────────────────────────────────
|
|
165
|
+
async function getWaveProgram(slug) {
|
|
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() {
|
|
179
|
+
console.log();
|
|
180
|
+
console.log(bold(' Drips Network — Apply for bounty issues from your terminal'));
|
|
181
|
+
console.log(dim(' Browse issues from any Wave program and apply in seconds.'));
|
|
182
|
+
console.log();
|
|
183
|
+
const { input } = await inquirer.prompt([{
|
|
184
|
+
type: 'input',
|
|
185
|
+
name: 'input',
|
|
186
|
+
message: bold('Enter Drips Wave URL or program slug:'),
|
|
187
|
+
default: 'stellar',
|
|
188
|
+
validate: (v) => v.trim().length > 0 || 'Required',
|
|
189
|
+
}]);
|
|
190
|
+
const slug = parseSlug(input.trim());
|
|
191
|
+
const progSpinner = ora(dim(` Loading "${slug}" wave program…`)).start();
|
|
192
|
+
let program;
|
|
193
|
+
try {
|
|
194
|
+
program = await getWaveProgram(slug);
|
|
195
|
+
progSpinner.succeed(` ${bold(program.name)} · ${cyan(program.issueCount.toLocaleString() + ' open issues')} · ${green(program.presetBudgetUSD + '/mo')} · ${dim(program.approvedRepoCount + ' repos')}`);
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
progSpinner.fail(` ${e.message}`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
let page = 1;
|
|
202
|
+
while (true) {
|
|
203
|
+
const issSpinner = ora(dim(` Fetching issues (page ${page})…`)).start();
|
|
204
|
+
let issues;
|
|
205
|
+
let pagination;
|
|
206
|
+
try {
|
|
207
|
+
const res = await getIssues(program.id, page);
|
|
208
|
+
issues = res.data;
|
|
209
|
+
pagination = res.pagination;
|
|
210
|
+
issSpinner.succeed(` Showing ${((page - 1) * 20) + 1}–${Math.min(page * 20, pagination.total)} of ${pagination.total.toLocaleString()} open issues`);
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
issSpinner.fail(` ${e.message}`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
console.log();
|
|
217
|
+
const choices = issues.map((issue) => {
|
|
218
|
+
const age = diffDays(issue.updatedAt);
|
|
219
|
+
const ageStr = age === 0 ? dim('today') : dim(`${age}d ago`);
|
|
220
|
+
const pts = issue.points ? green(`+${issue.points}pts`) : dim(' — pts');
|
|
221
|
+
const applicants = issue.pendingApplicationsCount > 0
|
|
222
|
+
? yellow(`${issue.pendingApplicationsCount} applied`)
|
|
223
|
+
: dim('0 applied');
|
|
224
|
+
const taken = issue.assignedApplicant ? red(' [assigned]') : '';
|
|
225
|
+
const title = truncate(issue.title, 48);
|
|
226
|
+
return {
|
|
227
|
+
name: ` ${pts} ${bold(title)} ${applicants}${taken} ${ageStr}`,
|
|
228
|
+
value: issue,
|
|
229
|
+
short: issue.title,
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
const nav = [
|
|
233
|
+
new inquirer.Separator(dim(' ─────────────────────────────────────────────────────')),
|
|
234
|
+
];
|
|
235
|
+
if (pagination.hasNextPage)
|
|
236
|
+
nav.push({ name: ` ${dim('→ Next page')}`, value: '__next__' });
|
|
237
|
+
if (page > 1)
|
|
238
|
+
nav.push({ name: ` ${dim('← Previous page')}`, value: '__prev__' });
|
|
239
|
+
nav.push({ name: ` ${dim('⬅ Back')}`, value: '__back__' });
|
|
240
|
+
const { selected } = await inquirer.prompt([{
|
|
241
|
+
type: 'list',
|
|
242
|
+
name: 'selected',
|
|
243
|
+
message: bold(`${program.name} Wave — choose an issue to apply for:`),
|
|
244
|
+
choices: [...choices, ...nav],
|
|
245
|
+
pageSize: 18,
|
|
246
|
+
}]);
|
|
247
|
+
if (selected === '__back__')
|
|
248
|
+
return;
|
|
249
|
+
if (selected === '__next__') {
|
|
250
|
+
page++;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (selected === '__prev__') {
|
|
254
|
+
page = Math.max(1, page - 1);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
await applyToDripsIssue(program, selected);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// ── Apply ─────────────────────────────────────────────────────────────────────
|
|
261
|
+
async function applyToDripsIssue(program, issue) {
|
|
262
|
+
console.log();
|
|
263
|
+
console.log(` ${bold(cyan(truncate(issue.title, 72)))}`);
|
|
264
|
+
if (issue.repo)
|
|
265
|
+
console.log(` ${dim('Repo: ')} ${issue.repo.fullName}`);
|
|
266
|
+
console.log(` ${dim('Points: ')} ${issue.points ? green('+' + issue.points + ' pts') : dim('—')}`);
|
|
267
|
+
console.log(` ${dim('Applied: ')} ${issue.pendingApplicationsCount} applicant(s)`);
|
|
268
|
+
if (issue.assignedApplicant) {
|
|
269
|
+
console.log(` ${yellow('⚠ Already assigned — you can still apply as a backup')}`);
|
|
270
|
+
}
|
|
271
|
+
if (issue.gitHubIssueUrl) {
|
|
272
|
+
console.log(` ${dim('GitHub: ')} ${dim(issue.gitHubIssueUrl)}`);
|
|
273
|
+
}
|
|
274
|
+
console.log();
|
|
275
|
+
const { style } = await inquirer.prompt([{
|
|
276
|
+
type: 'list',
|
|
277
|
+
name: 'style',
|
|
278
|
+
message: bold('How would you like to apply?'),
|
|
279
|
+
choices: [
|
|
280
|
+
{ name: ` ${green('⚡')} Quick apply — standard intro message`, value: 'quick' },
|
|
281
|
+
{ name: ` ${cyan('📝')} Custom message — write your own`, value: 'custom' },
|
|
282
|
+
new inquirer.Separator(dim(' ─────────────────────────────────')),
|
|
283
|
+
{ name: ` ${dim('⬅ Cancel')}`, value: 'cancel' },
|
|
284
|
+
],
|
|
285
|
+
}]);
|
|
286
|
+
if (style === 'cancel')
|
|
287
|
+
return;
|
|
288
|
+
let applicationText;
|
|
289
|
+
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');
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const { msg } = await inquirer.prompt([{
|
|
300
|
+
type: 'input',
|
|
301
|
+
name: 'msg',
|
|
302
|
+
message: bold('Your application message (min 10 chars):'),
|
|
303
|
+
validate: (v) => v.trim().length >= 10 || 'Please write at least 10 characters',
|
|
304
|
+
}]);
|
|
305
|
+
applicationText = [
|
|
306
|
+
msg.trim(),
|
|
307
|
+
``,
|
|
308
|
+
`*Applied via [GitPadi](https://github.com/Netwalls/contributor-agent)*`,
|
|
309
|
+
].join('\n');
|
|
310
|
+
}
|
|
311
|
+
// Preview
|
|
312
|
+
console.log();
|
|
313
|
+
console.log(dim(' ── Preview ───────────────────────────────────────────────────'));
|
|
314
|
+
applicationText.split('\n').forEach(l => console.log(` ${dim(l)}`));
|
|
315
|
+
console.log(dim(' ──────────────────────────────────────────────────────────────'));
|
|
316
|
+
console.log();
|
|
317
|
+
const { confirm } = await inquirer.prompt([{
|
|
318
|
+
type: 'list',
|
|
319
|
+
name: 'confirm',
|
|
320
|
+
message: bold('Submit this application?'),
|
|
321
|
+
choices: [
|
|
322
|
+
{ name: ` ${green('✅ Yes, apply')}`, value: 'yes' },
|
|
323
|
+
{ name: ` ${dim('❌ Cancel')}`, value: 'no' },
|
|
324
|
+
],
|
|
325
|
+
}]);
|
|
326
|
+
if (confirm === 'no')
|
|
327
|
+
return;
|
|
328
|
+
// Ensure auth only when the user actually commits to applying
|
|
329
|
+
let token;
|
|
330
|
+
try {
|
|
331
|
+
token = await ensureDripsAuth();
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
console.log(red('\n Authentication cancelled.\n'));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const spinner = ora(' Submitting application…').start();
|
|
338
|
+
try {
|
|
339
|
+
await dripsPost(`/api/wave-programs/${program.id}/issues/${issue.id}/applications`, { applicationText }, token);
|
|
340
|
+
spinner.succeed(green(` Applied for: ${bold(issue.title)}`));
|
|
341
|
+
console.log(dim(`\n Track your applications:`));
|
|
342
|
+
console.log(dim(` ${cyan(DRIPS_WEB + '/wave/' + program.slug)}\n`));
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
spinner.fail(red(` Failed: ${e.message}`));
|
|
346
|
+
if (e.message.startsWith('401')) {
|
|
347
|
+
saveDripsToken('');
|
|
348
|
+
console.log(yellow('\n Session expired — run again to re-authenticate.\n'));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitpadi",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
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
|
@@ -30,11 +30,12 @@ import * as releases from './commands/releases.js';
|
|
|
30
30
|
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
|
+
import { dripsMenu } from './commands/drips.js';
|
|
33
34
|
import * as gitlabIssues from './commands/gitlab-issues.js';
|
|
34
35
|
import * as gitlabMRs from './commands/gitlab-mrs.js';
|
|
35
36
|
import * as gitlabPipelines from './commands/gitlab-pipelines.js';
|
|
36
37
|
|
|
37
|
-
const VERSION = '2.1.
|
|
38
|
+
const VERSION = '2.1.2';
|
|
38
39
|
let targetConfirmed = false;
|
|
39
40
|
let gitlabProjectConfirmed = false;
|
|
40
41
|
|
|
@@ -642,6 +643,7 @@ async function mainMenu() {
|
|
|
642
643
|
{ name: `${cyan('✨')} ${bold('Contributor Mode')} ${dim('— fork, clone, sync, submit PRs')}`, value: 'contributor' },
|
|
643
644
|
{ name: `${magenta('🛠️')} ${bold('Maintainer Mode')} ${dim('— manage issues, PRs, contributors')}`, value: 'maintainer' },
|
|
644
645
|
{ name: `${yellow('🏫')} ${bold('Organization/School')} ${dim('— assignments, grading, leaderboard')}`, value: 'org' },
|
|
646
|
+
{ name: `${cyan('🌊')} ${bold('Drips Network')} ${dim('— apply for bounty issues on drips.network')}`, value: 'drips' },
|
|
645
647
|
new inquirer.Separator(dim(' ─────────────────────────────')),
|
|
646
648
|
{ name: `${dim('🔄')} ${dim('Switch Platform')}`, value: 'switch' },
|
|
647
649
|
{ name: `${dim('👋')} ${dim('Exit')}`, value: 'exit' },
|
|
@@ -658,6 +660,8 @@ async function mainMenu() {
|
|
|
658
660
|
} else if (mode === 'org') {
|
|
659
661
|
await ensureTargetRepo();
|
|
660
662
|
await safeMenu(orgMenu);
|
|
663
|
+
} else if (mode === 'drips') {
|
|
664
|
+
await safeMenu(dripsMenu);
|
|
661
665
|
}
|
|
662
666
|
} else {
|
|
663
667
|
// GitLab mode
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
// commands/drips.ts — Apply for bounty issues on Drips Network from your terminal
|
|
2
|
+
//
|
|
3
|
+
// API reverse-engineered from https://github.com/drips-network/app
|
|
4
|
+
// Base: https://wave-api.drips.network
|
|
5
|
+
// Auth: GitHub OAuth → JWT cookie (wave_access_token)
|
|
6
|
+
|
|
7
|
+
import inquirer from 'inquirer';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
|
|
15
|
+
const DRIPS_API = 'https://wave-api.drips.network';
|
|
16
|
+
const DRIPS_WEB = 'https://www.drips.network';
|
|
17
|
+
const CONFIG_DIR = path.join(os.homedir(), '.gitpadi');
|
|
18
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
19
|
+
|
|
20
|
+
const dim = chalk.dim;
|
|
21
|
+
const cyan = chalk.cyanBright;
|
|
22
|
+
const yellow = chalk.yellowBright;
|
|
23
|
+
const green = chalk.greenBright;
|
|
24
|
+
const bold = chalk.bold;
|
|
25
|
+
const red = chalk.redBright;
|
|
26
|
+
|
|
27
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
interface DripsIssue {
|
|
30
|
+
id: string;
|
|
31
|
+
title: string;
|
|
32
|
+
state: string;
|
|
33
|
+
points: number | null;
|
|
34
|
+
complexity: string | null;
|
|
35
|
+
labels: Array<{ id: string; name: string }>;
|
|
36
|
+
repo: { name: string; fullName: string; url: string } | null;
|
|
37
|
+
assignedApplicant: any | null;
|
|
38
|
+
pendingApplicationsCount: number;
|
|
39
|
+
waveProgramId: string | null;
|
|
40
|
+
gitHubIssueUrl: string | null;
|
|
41
|
+
gitHubIssueNumber: number | null;
|
|
42
|
+
updatedAt: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface DripsPagination {
|
|
46
|
+
page: number;
|
|
47
|
+
limit: number;
|
|
48
|
+
total: number;
|
|
49
|
+
totalPages: number;
|
|
50
|
+
hasNextPage: boolean;
|
|
51
|
+
hasPreviousPage: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface DripsProgram {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
slug: string;
|
|
58
|
+
description: string;
|
|
59
|
+
issueCount: number;
|
|
60
|
+
approvedRepoCount: number;
|
|
61
|
+
presetBudgetUSD: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Config ────────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function loadConfig(): Record<string, any> {
|
|
67
|
+
if (!fs.existsSync(CONFIG_FILE)) return {};
|
|
68
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')); } catch { return {}; }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function saveDripsToken(token: string): void {
|
|
72
|
+
const current = loadConfig();
|
|
73
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
74
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, dripsToken: token }, null, 2));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
function truncate(text: string, max: number): string {
|
|
80
|
+
return text.length <= max ? text : text.slice(0, max - 1) + '…';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function diffDays(dateStr: string): number {
|
|
84
|
+
return Math.floor((Date.now() - new Date(dateStr).getTime()) / 86_400_000);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function openBrowser(url: string): void {
|
|
88
|
+
try {
|
|
89
|
+
if (process.platform === 'darwin') execSync(`open "${url}"`, { stdio: 'ignore' });
|
|
90
|
+
else if (process.platform === 'win32') execSync(`start "" "${url}"`, { stdio: 'ignore' });
|
|
91
|
+
else execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
|
|
92
|
+
} catch { /* ignore */ }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseSlug(input: string): string {
|
|
96
|
+
const m = input.match(/drips\.network\/wave\/([^/?#\s]+)/);
|
|
97
|
+
if (m) return m[1];
|
|
98
|
+
return input.replace(/^\/+|\/+$/g, '').split('/').pop() || input;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── API ───────────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
async function dripsGet(endpoint: string, token?: string): Promise<any> {
|
|
104
|
+
const headers: Record<string, string> = { Accept: 'application/json' };
|
|
105
|
+
if (token) headers['Cookie'] = `wave_access_token=${token}`;
|
|
106
|
+
const res = await fetch(`${DRIPS_API}${endpoint}`, { headers });
|
|
107
|
+
if (!res.ok) throw new Error(`Drips API ${res.status} on ${endpoint}`);
|
|
108
|
+
return res.json();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function dripsPost(endpoint: string, body: any, token: string): Promise<any> {
|
|
112
|
+
const res = await fetch(`${DRIPS_API}${endpoint}`, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: {
|
|
115
|
+
'Content-Type': 'application/json',
|
|
116
|
+
Accept: 'application/json',
|
|
117
|
+
Cookie: `wave_access_token=${token}`,
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify(body),
|
|
120
|
+
});
|
|
121
|
+
const text = await res.text();
|
|
122
|
+
if (!res.ok) {
|
|
123
|
+
let msg = text;
|
|
124
|
+
try { msg = JSON.parse(text)?.message || text; } catch { }
|
|
125
|
+
throw new Error(`${res.status}: ${msg}`);
|
|
126
|
+
}
|
|
127
|
+
try { return JSON.parse(text); } catch { return {}; }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Auth ──────────────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
export async function ensureDripsAuth(): Promise<string> {
|
|
133
|
+
const config = loadConfig();
|
|
134
|
+
|
|
135
|
+
if (config.dripsToken) {
|
|
136
|
+
// Validate by hitting a quota endpoint that requires auth
|
|
137
|
+
try {
|
|
138
|
+
const res = await fetch(
|
|
139
|
+
`${DRIPS_API}/api/wave-programs/fdc01c95-806f-4b6a-998b-a6ed37e0d81b/quotas/applications`,
|
|
140
|
+
{ headers: { Cookie: `wave_access_token=${config.dripsToken}`, Accept: 'application/json' } }
|
|
141
|
+
);
|
|
142
|
+
if (res.status !== 401) return config.dripsToken as string;
|
|
143
|
+
} catch { /* network issue, let user try anyway */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Show how-to instructions
|
|
147
|
+
console.log();
|
|
148
|
+
console.log(dim(' ┌─ Connect your Drips Network account ────────────────────────┐'));
|
|
149
|
+
console.log(dim(' │'));
|
|
150
|
+
console.log(dim(' │ Drips uses GitHub OAuth. Here\'s how to get your token:'));
|
|
151
|
+
console.log(dim(' │'));
|
|
152
|
+
console.log(dim(' │ 1. Log in at: ') + cyan(DRIPS_WEB + '/wave/login'));
|
|
153
|
+
console.log(dim(' │ (GitPadi will open it in your browser now)'));
|
|
154
|
+
console.log(dim(' │'));
|
|
155
|
+
console.log(dim(' │ 2. After logging in with GitHub, open DevTools:'));
|
|
156
|
+
console.log(dim(' │ ') + bold('F12') + dim(' (or Cmd+Option+I on Mac)'));
|
|
157
|
+
console.log(dim(' │ → ') + bold('Application') + dim(' tab'));
|
|
158
|
+
console.log(dim(' │ → ') + bold('Cookies') + dim(' → ') + cyan('www.drips.network'));
|
|
159
|
+
console.log(dim(' │'));
|
|
160
|
+
console.log(dim(' │ 3. Find the cookie named ') + yellow('wave_access_token'));
|
|
161
|
+
console.log(dim(' │ Copy the full value (it starts with ') + dim('eyJ...') + dim(')'));
|
|
162
|
+
console.log(dim(' │'));
|
|
163
|
+
console.log(dim(' │ GitPadi saves it once — you won\'t need to do this again.'));
|
|
164
|
+
console.log(dim(' │'));
|
|
165
|
+
console.log(dim(' └─────────────────────────────────────────────────────────────┘'));
|
|
166
|
+
console.log();
|
|
167
|
+
|
|
168
|
+
const { launch } = await inquirer.prompt([{
|
|
169
|
+
type: 'confirm',
|
|
170
|
+
name: 'launch',
|
|
171
|
+
message: 'Open drips.network login in your browser?',
|
|
172
|
+
default: true,
|
|
173
|
+
}]);
|
|
174
|
+
|
|
175
|
+
if (launch) {
|
|
176
|
+
openBrowser(`${DRIPS_WEB}/wave/login?skipWelcome=false`);
|
|
177
|
+
console.log(dim('\n Browser opened. Log in with GitHub, then come back here.\n'));
|
|
178
|
+
} else {
|
|
179
|
+
console.log(dim(`\n Open manually: ${cyan(DRIPS_WEB + '/wave/login')}\n`));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { token } = await inquirer.prompt([{
|
|
183
|
+
type: 'password',
|
|
184
|
+
name: 'token',
|
|
185
|
+
message: cyan('Paste your wave_access_token cookie value:'),
|
|
186
|
+
mask: '•',
|
|
187
|
+
validate: (v: string) => {
|
|
188
|
+
if (!v || v.trim().length < 20) return 'Token seems too short — copy the full value from DevTools';
|
|
189
|
+
if (!v.trim().startsWith('eyJ')) return 'Expected a JWT starting with eyJ — make sure you copied wave_access_token, not another cookie';
|
|
190
|
+
return true;
|
|
191
|
+
},
|
|
192
|
+
}]);
|
|
193
|
+
|
|
194
|
+
const t = token.trim();
|
|
195
|
+
saveDripsToken(t);
|
|
196
|
+
console.log(green('\n ✅ Drips session saved — you\'re all set!\n'));
|
|
197
|
+
return t;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Program lookup ────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
async function getWaveProgram(slug: string): Promise<DripsProgram> {
|
|
203
|
+
const data = await dripsGet(`/api/wave-programs?slug=${encodeURIComponent(slug)}`);
|
|
204
|
+
if (data?.id) return data as DripsProgram;
|
|
205
|
+
if (Array.isArray(data?.data) && data.data.length > 0) return data.data[0] as DripsProgram;
|
|
206
|
+
throw new Error(`Wave program "${slug}" not found. Check the slug or URL.`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Issues ────────────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
async function getIssues(
|
|
212
|
+
programId: string,
|
|
213
|
+
page: number,
|
|
214
|
+
): Promise<{ data: DripsIssue[]; pagination: DripsPagination }> {
|
|
215
|
+
return dripsGet(
|
|
216
|
+
`/api/issues?waveProgramId=${programId}&state=open&page=${page}&limit=20&sortBy=updatedAt`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── Main menu ─────────────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
export async function dripsMenu(): Promise<void> {
|
|
223
|
+
console.log();
|
|
224
|
+
console.log(bold(' Drips Network — Apply for bounty issues from your terminal'));
|
|
225
|
+
console.log(dim(' Browse issues from any Wave program and apply in seconds.'));
|
|
226
|
+
console.log();
|
|
227
|
+
|
|
228
|
+
const { input } = await inquirer.prompt([{
|
|
229
|
+
type: 'input',
|
|
230
|
+
name: 'input',
|
|
231
|
+
message: bold('Enter Drips Wave URL or program slug:'),
|
|
232
|
+
default: 'stellar',
|
|
233
|
+
validate: (v: string) => v.trim().length > 0 || 'Required',
|
|
234
|
+
}]);
|
|
235
|
+
|
|
236
|
+
const slug = parseSlug(input.trim());
|
|
237
|
+
|
|
238
|
+
const progSpinner = ora(dim(` Loading "${slug}" wave program…`)).start();
|
|
239
|
+
let program: DripsProgram;
|
|
240
|
+
try {
|
|
241
|
+
program = await getWaveProgram(slug);
|
|
242
|
+
progSpinner.succeed(
|
|
243
|
+
` ${bold(program.name)} · ${cyan(program.issueCount.toLocaleString() + ' open issues')} · ${green(program.presetBudgetUSD + '/mo')} · ${dim(program.approvedRepoCount + ' repos')}`
|
|
244
|
+
);
|
|
245
|
+
} catch (e: any) {
|
|
246
|
+
progSpinner.fail(` ${e.message}`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let page = 1;
|
|
251
|
+
|
|
252
|
+
while (true) {
|
|
253
|
+
const issSpinner = ora(dim(` Fetching issues (page ${page})…`)).start();
|
|
254
|
+
let issues: DripsIssue[];
|
|
255
|
+
let pagination: DripsPagination;
|
|
256
|
+
try {
|
|
257
|
+
const res = await getIssues(program.id, page);
|
|
258
|
+
issues = res.data;
|
|
259
|
+
pagination = res.pagination;
|
|
260
|
+
issSpinner.succeed(
|
|
261
|
+
` Showing ${((page - 1) * 20) + 1}–${Math.min(page * 20, pagination.total)} of ${pagination.total.toLocaleString()} open issues`
|
|
262
|
+
);
|
|
263
|
+
} catch (e: any) {
|
|
264
|
+
issSpinner.fail(` ${e.message}`);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log();
|
|
269
|
+
|
|
270
|
+
const choices = issues.map((issue) => {
|
|
271
|
+
const age = diffDays(issue.updatedAt);
|
|
272
|
+
const ageStr = age === 0 ? dim('today') : dim(`${age}d ago`);
|
|
273
|
+
const pts = issue.points ? green(`+${issue.points}pts`) : dim(' — pts');
|
|
274
|
+
const applicants = issue.pendingApplicationsCount > 0
|
|
275
|
+
? yellow(`${issue.pendingApplicationsCount} applied`)
|
|
276
|
+
: dim('0 applied');
|
|
277
|
+
const taken = issue.assignedApplicant ? red(' [assigned]') : '';
|
|
278
|
+
const title = truncate(issue.title, 48);
|
|
279
|
+
return {
|
|
280
|
+
name: ` ${pts} ${bold(title)} ${applicants}${taken} ${ageStr}`,
|
|
281
|
+
value: issue,
|
|
282
|
+
short: issue.title,
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const nav: any[] = [
|
|
287
|
+
new inquirer.Separator(dim(' ─────────────────────────────────────────────────────')),
|
|
288
|
+
];
|
|
289
|
+
if (pagination.hasNextPage) nav.push({ name: ` ${dim('→ Next page')}`, value: '__next__' });
|
|
290
|
+
if (page > 1) nav.push({ name: ` ${dim('← Previous page')}`, value: '__prev__' });
|
|
291
|
+
nav.push({ name: ` ${dim('⬅ Back')}`, value: '__back__' });
|
|
292
|
+
|
|
293
|
+
const { selected } = await inquirer.prompt([{
|
|
294
|
+
type: 'list',
|
|
295
|
+
name: 'selected',
|
|
296
|
+
message: bold(`${program.name} Wave — choose an issue to apply for:`),
|
|
297
|
+
choices: [...choices, ...nav],
|
|
298
|
+
pageSize: 18,
|
|
299
|
+
}]);
|
|
300
|
+
|
|
301
|
+
if (selected === '__back__') return;
|
|
302
|
+
if (selected === '__next__') { page++; continue; }
|
|
303
|
+
if (selected === '__prev__') { page = Math.max(1, page - 1); continue; }
|
|
304
|
+
|
|
305
|
+
await applyToDripsIssue(program, selected as DripsIssue);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Apply ─────────────────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
async function applyToDripsIssue(program: DripsProgram, issue: DripsIssue): Promise<void> {
|
|
312
|
+
console.log();
|
|
313
|
+
console.log(` ${bold(cyan(truncate(issue.title, 72)))}`);
|
|
314
|
+
if (issue.repo) console.log(` ${dim('Repo: ')} ${issue.repo.fullName}`);
|
|
315
|
+
console.log(` ${dim('Points: ')} ${issue.points ? green('+' + issue.points + ' pts') : dim('—')}`);
|
|
316
|
+
console.log(` ${dim('Applied: ')} ${issue.pendingApplicationsCount} applicant(s)`);
|
|
317
|
+
if (issue.assignedApplicant) {
|
|
318
|
+
console.log(` ${yellow('⚠ Already assigned — you can still apply as a backup')}`);
|
|
319
|
+
}
|
|
320
|
+
if (issue.gitHubIssueUrl) {
|
|
321
|
+
console.log(` ${dim('GitHub: ')} ${dim(issue.gitHubIssueUrl)}`);
|
|
322
|
+
}
|
|
323
|
+
console.log();
|
|
324
|
+
|
|
325
|
+
const { style } = await inquirer.prompt([{
|
|
326
|
+
type: 'list',
|
|
327
|
+
name: 'style',
|
|
328
|
+
message: bold('How would you like to apply?'),
|
|
329
|
+
choices: [
|
|
330
|
+
{ name: ` ${green('⚡')} Quick apply — standard intro message`, value: 'quick' },
|
|
331
|
+
{ name: ` ${cyan('📝')} Custom message — write your own`, value: 'custom' },
|
|
332
|
+
new inquirer.Separator(dim(' ─────────────────────────────────')),
|
|
333
|
+
{ name: ` ${dim('⬅ Cancel')}`, value: 'cancel' },
|
|
334
|
+
],
|
|
335
|
+
}]);
|
|
336
|
+
|
|
337
|
+
if (style === 'cancel') return;
|
|
338
|
+
|
|
339
|
+
let applicationText: string;
|
|
340
|
+
|
|
341
|
+
if (style === 'quick') {
|
|
342
|
+
applicationText = [
|
|
343
|
+
`Hi! I'd like to work on this issue.`,
|
|
344
|
+
``,
|
|
345
|
+
`I'm available to start right away and will keep you updated on progress. Please consider assigning this to me.`,
|
|
346
|
+
``,
|
|
347
|
+
`*Applied via [GitPadi](https://github.com/Netwalls/contributor-agent)*`,
|
|
348
|
+
].join('\n');
|
|
349
|
+
} else {
|
|
350
|
+
const { msg } = await inquirer.prompt([{
|
|
351
|
+
type: 'input',
|
|
352
|
+
name: 'msg',
|
|
353
|
+
message: bold('Your application message (min 10 chars):'),
|
|
354
|
+
validate: (v: string) => v.trim().length >= 10 || 'Please write at least 10 characters',
|
|
355
|
+
}]);
|
|
356
|
+
applicationText = [
|
|
357
|
+
msg.trim(),
|
|
358
|
+
``,
|
|
359
|
+
`*Applied via [GitPadi](https://github.com/Netwalls/contributor-agent)*`,
|
|
360
|
+
].join('\n');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Preview
|
|
364
|
+
console.log();
|
|
365
|
+
console.log(dim(' ── Preview ───────────────────────────────────────────────────'));
|
|
366
|
+
applicationText.split('\n').forEach(l => console.log(` ${dim(l)}`));
|
|
367
|
+
console.log(dim(' ──────────────────────────────────────────────────────────────'));
|
|
368
|
+
console.log();
|
|
369
|
+
|
|
370
|
+
const { confirm } = await inquirer.prompt([{
|
|
371
|
+
type: 'list',
|
|
372
|
+
name: 'confirm',
|
|
373
|
+
message: bold('Submit this application?'),
|
|
374
|
+
choices: [
|
|
375
|
+
{ name: ` ${green('✅ Yes, apply')}`, value: 'yes' },
|
|
376
|
+
{ name: ` ${dim('❌ Cancel')}`, value: 'no' },
|
|
377
|
+
],
|
|
378
|
+
}]);
|
|
379
|
+
|
|
380
|
+
if (confirm === 'no') return;
|
|
381
|
+
|
|
382
|
+
// Ensure auth only when the user actually commits to applying
|
|
383
|
+
let token: string;
|
|
384
|
+
try {
|
|
385
|
+
token = await ensureDripsAuth();
|
|
386
|
+
} catch {
|
|
387
|
+
console.log(red('\n Authentication cancelled.\n'));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const spinner = ora(' Submitting application…').start();
|
|
392
|
+
try {
|
|
393
|
+
await dripsPost(
|
|
394
|
+
`/api/wave-programs/${program.id}/issues/${issue.id}/applications`,
|
|
395
|
+
{ applicationText },
|
|
396
|
+
token,
|
|
397
|
+
);
|
|
398
|
+
spinner.succeed(green(` Applied for: ${bold(issue.title)}`));
|
|
399
|
+
console.log(dim(`\n Track your applications:`));
|
|
400
|
+
console.log(dim(` ${cyan(DRIPS_WEB + '/wave/' + program.slug)}\n`));
|
|
401
|
+
} catch (e: any) {
|
|
402
|
+
spinner.fail(red(` Failed: ${e.message}`));
|
|
403
|
+
if (e.message.startsWith('401')) {
|
|
404
|
+
saveDripsToken('');
|
|
405
|
+
console.log(yellow('\n Session expired — run again to re-authenticate.\n'));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|