gitpadi 2.0.0
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/LICENSE +21 -0
- package/README.md +266 -0
- package/action.yml +92 -0
- package/examples/applicant-scorer.yml +30 -0
- package/examples/create-issues.yml +42 -0
- package/examples/issues.json +48 -0
- package/examples/pr-review.yml +23 -0
- package/package.json +52 -0
- package/src/applicant-scorer.ts +285 -0
- package/src/cli.ts +648 -0
- package/src/commands/contributors.ts +114 -0
- package/src/commands/issues.ts +267 -0
- package/src/commands/prs.ts +243 -0
- package/src/commands/releases.ts +54 -0
- package/src/commands/repos.ts +129 -0
- package/src/core/github.ts +43 -0
- package/src/core/scorer.ts +127 -0
- package/src/create-issues.ts +203 -0
- package/src/pr-review.ts +132 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
3
|
+
// GitPadi v2.0 — Your AI-Powered GitHub Management Terminal
|
|
4
|
+
// "Built different. Run different."
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import inquirer from 'inquirer';
|
|
10
|
+
import gradient from 'gradient-string';
|
|
11
|
+
import figlet from 'figlet';
|
|
12
|
+
import boxen from 'boxen';
|
|
13
|
+
import { createSpinner } from 'nanospinner';
|
|
14
|
+
import { initGitHub, setRepo, getOwner, getRepo, getOctokit } from './core/github.js';
|
|
15
|
+
|
|
16
|
+
import * as issues from './commands/issues.js';
|
|
17
|
+
import * as prs from './commands/prs.js';
|
|
18
|
+
import * as repos from './commands/repos.js';
|
|
19
|
+
import * as contributors from './commands/contributors.js';
|
|
20
|
+
import * as releases from './commands/releases.js';
|
|
21
|
+
|
|
22
|
+
const VERSION = '2.0.0';
|
|
23
|
+
|
|
24
|
+
// ── Styling ────────────────────────────────────────────────────────────
|
|
25
|
+
const cyber = gradient(['#ff00ff', '#00ffff', '#ff00ff']);
|
|
26
|
+
const neon = gradient(['#00ff87', '#60efff']);
|
|
27
|
+
const fire = gradient(['#ff6b35', '#f7c948', '#ff6b35']);
|
|
28
|
+
const dim = chalk.dim;
|
|
29
|
+
const bold = chalk.bold;
|
|
30
|
+
const green = chalk.greenBright;
|
|
31
|
+
const cyan = chalk.cyanBright;
|
|
32
|
+
const magenta = chalk.magentaBright;
|
|
33
|
+
const yellow = chalk.yellowBright;
|
|
34
|
+
const red = chalk.redBright;
|
|
35
|
+
|
|
36
|
+
// ── Utilities ──────────────────────────────────────────────────────────
|
|
37
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
38
|
+
|
|
39
|
+
async function typewriter(text: string, speed: number = 20) {
|
|
40
|
+
for (const char of text) {
|
|
41
|
+
process.stdout.write(char);
|
|
42
|
+
await sleep(speed);
|
|
43
|
+
}
|
|
44
|
+
console.log('');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function line(char: string = '─', len: number = 60) {
|
|
48
|
+
console.log(dim(char.repeat(len)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Back Navigation ────────────────────────────────────────────────────
|
|
52
|
+
// Type 'q' at any text/number prompt to go back to the menu
|
|
53
|
+
class BackToMenu extends Error { constructor() { super('back'); } }
|
|
54
|
+
|
|
55
|
+
function checkForBack(value: any): void {
|
|
56
|
+
if (typeof value === 'string' && value.trim().toLowerCase() === 'q') {
|
|
57
|
+
throw new BackToMenu();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Wraps inquirer.prompt — runs prompts one at a time, checks for 'q' after each
|
|
62
|
+
async function ask(questions: any[]): Promise<any> {
|
|
63
|
+
const answers: any = {};
|
|
64
|
+
for (const question of questions) {
|
|
65
|
+
const ans = await inquirer.prompt([question]);
|
|
66
|
+
answers[question.name] = ans[question.name];
|
|
67
|
+
// Check for 'q' immediately after each text/number input
|
|
68
|
+
if ((question.type === 'input' || question.type === 'number' || question.type === 'password')) {
|
|
69
|
+
const val = String(answers[question.name] ?? '');
|
|
70
|
+
if (val.trim().toLowerCase() === 'q') throw new BackToMenu();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return answers;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function safeMenu(fn: () => Promise<void>) {
|
|
77
|
+
try {
|
|
78
|
+
await fn();
|
|
79
|
+
} catch (e: any) {
|
|
80
|
+
if (e instanceof BackToMenu) {
|
|
81
|
+
console.log(dim('\n ↩ Back to main menu\n'));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
throw e; // Ctrl+C and other errors bubble up to trigger shutdown
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Boot Sequence ──────────────────────────────────────────────────────
|
|
89
|
+
async function bootSequence() {
|
|
90
|
+
console.clear();
|
|
91
|
+
|
|
92
|
+
const banner = figlet.textSync('GitPadi', {
|
|
93
|
+
font: 'ANSI Shadow',
|
|
94
|
+
horizontalLayout: 'fitted',
|
|
95
|
+
});
|
|
96
|
+
console.log(cyber(banner));
|
|
97
|
+
|
|
98
|
+
console.log(boxen(
|
|
99
|
+
`${bold('v' + VERSION)} ${dim('|')} ${dim('AI-Powered GitHub Management')} ${dim('|')} ${dim('by')} ${magenta('Netwalls')}`,
|
|
100
|
+
{
|
|
101
|
+
padding: { left: 2, right: 2, top: 0, bottom: 0 },
|
|
102
|
+
borderStyle: 'double',
|
|
103
|
+
borderColor: 'magenta',
|
|
104
|
+
dimBorder: true,
|
|
105
|
+
}
|
|
106
|
+
));
|
|
107
|
+
console.log('');
|
|
108
|
+
|
|
109
|
+
const bootSteps = [
|
|
110
|
+
'▸ Initializing GitPadi engine',
|
|
111
|
+
'▸ Loading command modules',
|
|
112
|
+
'▸ Establishing GitHub connection',
|
|
113
|
+
'▸ Systems online',
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
for (const step of bootSteps) {
|
|
117
|
+
process.stdout.write(dim(step));
|
|
118
|
+
await sleep(150);
|
|
119
|
+
process.stdout.write(green(' ✓\n'));
|
|
120
|
+
await sleep(80);
|
|
121
|
+
}
|
|
122
|
+
console.log('');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Onboarding ─────────────────────────────────────────────────────────
|
|
126
|
+
async function onboarding() {
|
|
127
|
+
const savedToken = process.env.GITHUB_TOKEN || process.env.GITPADI_TOKEN;
|
|
128
|
+
const savedOwner = process.env.GITHUB_OWNER;
|
|
129
|
+
const savedRepo = process.env.GITHUB_REPO;
|
|
130
|
+
|
|
131
|
+
if (savedToken && savedOwner && savedRepo) {
|
|
132
|
+
initGitHub(savedToken, savedOwner, savedRepo);
|
|
133
|
+
const spinner = createSpinner(dim('Authenticating...')).start();
|
|
134
|
+
await sleep(600);
|
|
135
|
+
spinner.success({ text: green(`Connected to ${cyan(`${savedOwner}/${savedRepo}`)}`) });
|
|
136
|
+
console.log('');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(neon(' ⚡ First-time setup — let\'s connect you to GitHub.\n'));
|
|
141
|
+
|
|
142
|
+
const answers = await inquirer.prompt([
|
|
143
|
+
{
|
|
144
|
+
type: 'password',
|
|
145
|
+
name: 'token',
|
|
146
|
+
message: magenta('🔑 GitHub Token') + dim(' (ghp_xxx):'),
|
|
147
|
+
mask: '•',
|
|
148
|
+
validate: (v: string) => v.startsWith('ghp_') || v.startsWith('github_pat_') ? true : 'Token should start with ghp_ or github_pat_',
|
|
149
|
+
when: !savedToken,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'input',
|
|
153
|
+
name: 'owner',
|
|
154
|
+
message: cyan('👤 GitHub Owner/Org:'),
|
|
155
|
+
default: savedOwner || '',
|
|
156
|
+
validate: (v: string) => v.length > 0 || 'Required',
|
|
157
|
+
when: !savedOwner,
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
type: 'input',
|
|
161
|
+
name: 'repo',
|
|
162
|
+
message: cyan('📦 Repository name:'),
|
|
163
|
+
default: savedRepo || '',
|
|
164
|
+
validate: (v: string) => v.length > 0 || 'Required',
|
|
165
|
+
when: !savedRepo,
|
|
166
|
+
},
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const token = answers.token || savedToken;
|
|
170
|
+
const owner = answers.owner || savedOwner;
|
|
171
|
+
const repo = answers.repo || savedRepo;
|
|
172
|
+
|
|
173
|
+
initGitHub(token, owner, repo);
|
|
174
|
+
|
|
175
|
+
const spinner = createSpinner(dim('Connecting to GitHub...')).start();
|
|
176
|
+
await sleep(800);
|
|
177
|
+
spinner.success({ text: green(`Locked in → ${cyan(`${owner}/${repo}`)}`) });
|
|
178
|
+
|
|
179
|
+
console.log('');
|
|
180
|
+
console.log(boxen(
|
|
181
|
+
dim('💡 Pro tip: Set these to skip setup next time:\n\n') +
|
|
182
|
+
yellow(' export GITHUB_TOKEN=ghp_xxx\n') +
|
|
183
|
+
yellow(' export GITHUB_OWNER=' + owner + '\n') +
|
|
184
|
+
yellow(' export GITHUB_REPO=' + repo),
|
|
185
|
+
{ padding: 1, borderColor: 'yellow', dimBorder: true, borderStyle: 'round' }
|
|
186
|
+
));
|
|
187
|
+
console.log('');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Main Menu ──────────────────────────────────────────────────────────
|
|
191
|
+
async function mainMenu() {
|
|
192
|
+
while (true) {
|
|
193
|
+
line('═');
|
|
194
|
+
console.log(cyber(' ⟨ GITPADI COMMAND CENTER ⟩'));
|
|
195
|
+
console.log(dim(' Select ⬅ Back on lists • Type q on text prompts'));
|
|
196
|
+
line('═');
|
|
197
|
+
console.log('');
|
|
198
|
+
|
|
199
|
+
let category: string;
|
|
200
|
+
try {
|
|
201
|
+
const ans = await inquirer.prompt([{
|
|
202
|
+
type: 'list',
|
|
203
|
+
name: 'category',
|
|
204
|
+
message: bold('Select operation:'),
|
|
205
|
+
choices: [
|
|
206
|
+
{ name: `${magenta('📋')} ${bold('Issues')} ${dim('— create, close, delete, assign, search')}`, value: 'issues' },
|
|
207
|
+
{ name: `${cyan('🔀')} ${bold('Pull Requests')} ${dim('— merge, review, approve, diff')}`, value: 'prs' },
|
|
208
|
+
{ name: `${green('📦')} ${bold('Repositories')} ${dim('— create, delete, clone, info')}`, value: 'repos' },
|
|
209
|
+
{ name: `${yellow('🏆')} ${bold('Contributors')} ${dim('— score, rank, auto-assign best')}`, value: 'contributors' },
|
|
210
|
+
{ name: `${red('🚀')} ${bold('Releases')} ${dim('— create, list, manage')}`, value: 'releases' },
|
|
211
|
+
new inquirer.Separator(dim(' ─────────────────────────────')),
|
|
212
|
+
{ name: `${dim('⚙️')} ${dim('Switch Repo')}`, value: 'switch' },
|
|
213
|
+
{ name: `${dim('👋')} ${dim('Exit')}`, value: 'exit' },
|
|
214
|
+
],
|
|
215
|
+
loop: false,
|
|
216
|
+
}]);
|
|
217
|
+
category = ans.category;
|
|
218
|
+
} catch {
|
|
219
|
+
// Ctrl+C on main menu = exit
|
|
220
|
+
console.log('');
|
|
221
|
+
console.log(dim(' ▸ Saving session...'));
|
|
222
|
+
console.log(dim(' ▸ Disconnecting from GitHub...'));
|
|
223
|
+
console.log('');
|
|
224
|
+
console.log(fire(' 🤖 GitPadi is shutting down now. See you soon, pal :)\n'));
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (category === 'exit') {
|
|
229
|
+
console.log('');
|
|
230
|
+
console.log(dim(' ▸ Saving session...'));
|
|
231
|
+
console.log(dim(' ▸ Disconnecting from GitHub...'));
|
|
232
|
+
console.log('');
|
|
233
|
+
console.log(fire(' 🤖 GitPadi is shutting down now. See you soon, pal :)\n'));
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (category === 'switch') {
|
|
238
|
+
await safeMenu(async () => {
|
|
239
|
+
const a = await inquirer.prompt([
|
|
240
|
+
{ type: 'input', name: 'owner', message: cyan('👤 New Owner/Org:'), default: getOwner() },
|
|
241
|
+
{ type: 'input', name: 'repo', message: cyan('📦 New Repo:'), default: getRepo() },
|
|
242
|
+
]);
|
|
243
|
+
setRepo(a.owner, a.repo);
|
|
244
|
+
const s = createSpinner(dim('Switching...')).start();
|
|
245
|
+
await sleep(400);
|
|
246
|
+
s.success({ text: green(`Now targeting ${cyan(`${a.owner}/${a.repo}`)}`) });
|
|
247
|
+
});
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (category === 'issues') await safeMenu(issueMenu);
|
|
252
|
+
if (category === 'prs') await safeMenu(prMenu);
|
|
253
|
+
if (category === 'repos') await safeMenu(repoMenu);
|
|
254
|
+
if (category === 'contributors') await safeMenu(contributorMenu);
|
|
255
|
+
if (category === 'releases') await safeMenu(releaseMenu);
|
|
256
|
+
|
|
257
|
+
console.log('');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Issue Menu ─────────────────────────────────────────────────────────
|
|
262
|
+
async function issueMenu() {
|
|
263
|
+
const { action } = await inquirer.prompt([{
|
|
264
|
+
type: 'list', name: 'action', message: magenta('📋 Issue Operation:'),
|
|
265
|
+
choices: [
|
|
266
|
+
{ name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
|
|
267
|
+
new inquirer.Separator(dim(' ─────────────────────────────')),
|
|
268
|
+
{ name: ` ${green('▸')} List open issues`, value: 'list' },
|
|
269
|
+
{ name: ` ${green('▸')} Create single issue`, value: 'create' },
|
|
270
|
+
{ name: ` ${green('▸')} Bulk create from JSON`, value: 'bulk' },
|
|
271
|
+
{ name: ` ${red('▸')} Close issue`, value: 'close' },
|
|
272
|
+
{ name: ` ${green('▸')} Reopen issue`, value: 'reopen' },
|
|
273
|
+
{ name: ` ${red('▸')} Delete (close & lock)`, value: 'delete' },
|
|
274
|
+
{ name: ` ${cyan('▸')} Assign user`, value: 'assign' },
|
|
275
|
+
{ name: ` ${yellow('▸')} Auto-assign best applicant`, value: 'assign-best' },
|
|
276
|
+
{ name: ` ${cyan('▸')} Search issues`, value: 'search' },
|
|
277
|
+
{ name: ` ${cyan('▸')} Add labels`, value: 'label' },
|
|
278
|
+
],
|
|
279
|
+
}]);
|
|
280
|
+
|
|
281
|
+
if (action === 'back') return;
|
|
282
|
+
|
|
283
|
+
if (action === 'list') {
|
|
284
|
+
const opts = await ask([
|
|
285
|
+
{ type: 'list', name: 'state', message: 'State:', choices: ['open', 'closed', 'all'], default: 'open' },
|
|
286
|
+
{ type: 'input', name: 'labels', message: dim('Filter by labels (optional, q=back):'), default: '' },
|
|
287
|
+
]);
|
|
288
|
+
await issues.listIssues({ state: opts.state, labels: opts.labels || undefined });
|
|
289
|
+
} else if (action === 'create') {
|
|
290
|
+
const a = await ask([
|
|
291
|
+
{ type: 'input', name: 'title', message: yellow('Issue title') + dim(' (q=back):') },
|
|
292
|
+
{ type: 'editor', name: 'body', message: 'Issue body (opens editor):' },
|
|
293
|
+
{ type: 'input', name: 'labels', message: dim('Labels (comma-separated):'), default: '' },
|
|
294
|
+
]);
|
|
295
|
+
await issues.createIssue(a);
|
|
296
|
+
} else if (action === 'bulk') {
|
|
297
|
+
const a = await ask([
|
|
298
|
+
{ type: 'input', name: 'file', message: yellow('📁 Path to issues JSON') + dim(' (q=back):') },
|
|
299
|
+
{ type: 'confirm', name: 'dryRun', message: 'Dry run first?', default: true },
|
|
300
|
+
{ type: 'number', name: 'start', message: dim('Start index:'), default: 1 },
|
|
301
|
+
{ type: 'number', name: 'end', message: dim('End index:'), default: 999 },
|
|
302
|
+
]);
|
|
303
|
+
await issues.createIssuesFromFile(a.file, { dryRun: a.dryRun, start: a.start, end: a.end });
|
|
304
|
+
} else if (action === 'assign-best') {
|
|
305
|
+
// ── Smart Auto-Assign Flow ──
|
|
306
|
+
// 1. Fetch all open issues with comments
|
|
307
|
+
// 2. Filter to ones with applicant comments
|
|
308
|
+
// 3. Let user pick from a list
|
|
309
|
+
// 4. Show applicants + scores, then assign best
|
|
310
|
+
|
|
311
|
+
const spinner = createSpinner(dim('Finding issues with applicants...')).start();
|
|
312
|
+
const octokit = getOctokit();
|
|
313
|
+
|
|
314
|
+
const { data: allIssues } = await octokit.issues.listForRepo({
|
|
315
|
+
owner: getOwner(), repo: getRepo(), state: 'open', per_page: 50,
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Filter to real issues (not PRs) with comments
|
|
319
|
+
const realIssues = allIssues.filter((i) => !i.pull_request && i.comments > 0);
|
|
320
|
+
|
|
321
|
+
// Check each issue for applicant comments
|
|
322
|
+
const issuesWithApplicants: Array<{ number: number; title: string; applicants: string[]; labels: string[] }> = [];
|
|
323
|
+
|
|
324
|
+
for (const issue of realIssues) {
|
|
325
|
+
const { data: comments } = await octokit.issues.listComments({
|
|
326
|
+
owner: getOwner(), repo: getRepo(), issue_number: issue.number, per_page: 100,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const applicantUsers = new Set<string>();
|
|
330
|
+
comments.forEach((c) => {
|
|
331
|
+
if (c.user?.login && c.user.login !== 'github-actions[bot]') {
|
|
332
|
+
applicantUsers.add(c.user.login);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
if (applicantUsers.size > 0) {
|
|
337
|
+
issuesWithApplicants.push({
|
|
338
|
+
number: issue.number,
|
|
339
|
+
title: issue.title.substring(0, 45),
|
|
340
|
+
applicants: Array.from(applicantUsers),
|
|
341
|
+
labels: issue.labels.map((l) => typeof l === 'string' ? l : l.name || '').filter(Boolean),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
spinner.stop();
|
|
347
|
+
|
|
348
|
+
if (issuesWithApplicants.length === 0) {
|
|
349
|
+
console.log(yellow('\n ⚠️ No open issues with comments found.\n'));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
console.log(`\n${bold(`🏆 Issues with Applicants`)} (${issuesWithApplicants.length})\n`);
|
|
354
|
+
|
|
355
|
+
// Let user pick an issue
|
|
356
|
+
const { picked } = await inquirer.prompt([{
|
|
357
|
+
type: 'list', name: 'picked', message: yellow('Select an issue:'),
|
|
358
|
+
choices: [
|
|
359
|
+
{ name: ` ${dim('⬅ Back')}`, value: -1 },
|
|
360
|
+
new inquirer.Separator(dim(' ─────────────────────────────')),
|
|
361
|
+
].concat(issuesWithApplicants.map((i) => ({
|
|
362
|
+
name: ` #${i.number} ${bold(i.title)} ${dim('—')} ${cyan(`${i.applicants.length} applicant(s):`)} ${i.applicants.map((u) => `@${u}`).join(', ')}`,
|
|
363
|
+
value: i.number,
|
|
364
|
+
})) as any),
|
|
365
|
+
}]);
|
|
366
|
+
|
|
367
|
+
if (picked === -1) return;
|
|
368
|
+
|
|
369
|
+
// Run the scoring
|
|
370
|
+
await issues.assignBest(picked);
|
|
371
|
+
|
|
372
|
+
} else if (action === 'close' || action === 'reopen' || action === 'delete') {
|
|
373
|
+
const { n } = await ask([{ type: 'input', name: 'n', message: yellow('Issue #') + dim(' (q=back):') }]);
|
|
374
|
+
if (!n || isNaN(Number(n))) return;
|
|
375
|
+
if (action === 'close') await issues.closeIssue(n);
|
|
376
|
+
else if (action === 'reopen') await issues.reopenIssue(n);
|
|
377
|
+
else if (action === 'delete') {
|
|
378
|
+
const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: red('⚠️ This will close & lock the issue. Proceed?'), default: false }]);
|
|
379
|
+
if (confirm) await issues.deleteIssue(n);
|
|
380
|
+
}
|
|
381
|
+
} else if (action === 'assign') {
|
|
382
|
+
const a = await ask([
|
|
383
|
+
{ type: 'input', name: 'n', message: yellow('Issue #') + dim(' (q=back):') },
|
|
384
|
+
{ type: 'input', name: 'users', message: cyan('Username(s) (space-separated):') },
|
|
385
|
+
]);
|
|
386
|
+
await issues.assignIssue(a.n, a.users.split(/\s+/));
|
|
387
|
+
} else if (action === 'search') {
|
|
388
|
+
const { q } = await ask([{ type: 'input', name: 'q', message: cyan('🔍 Search query') + dim(' (q=back):') }]);
|
|
389
|
+
await issues.searchIssues(q);
|
|
390
|
+
} else if (action === 'label') {
|
|
391
|
+
const a = await ask([
|
|
392
|
+
{ type: 'input', name: 'n', message: yellow('Issue #') + dim(' (q=back):') },
|
|
393
|
+
{ type: 'input', name: 'labels', message: cyan('Labels (space-separated):') },
|
|
394
|
+
]);
|
|
395
|
+
await issues.labelIssue(a.n, a.labels.split(/\s+/));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ── PR Menu ────────────────────────────────────────────────────────────
|
|
400
|
+
async function prMenu() {
|
|
401
|
+
const { action } = await inquirer.prompt([{
|
|
402
|
+
type: 'list', name: 'action', message: cyan('🔀 PR Operation:'),
|
|
403
|
+
choices: [
|
|
404
|
+
{ name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
|
|
405
|
+
new inquirer.Separator(dim(' ─────────────────────────────')),
|
|
406
|
+
{ name: ` ${green('▸')} List pull requests`, value: 'list' },
|
|
407
|
+
{ name: ` ${green('▸')} Merge PR ${dim('(checks CI first)')}`, value: 'merge' },
|
|
408
|
+
{ name: ` ${yellow('▸')} Force merge ${dim('(skip CI checks)')}`, value: 'force-merge' },
|
|
409
|
+
{ name: ` ${red('▸')} Close PR`, value: 'close' },
|
|
410
|
+
{ name: ` ${yellow('▸')} Auto-review PR`, value: 'review' },
|
|
411
|
+
{ name: ` ${green('▸')} Approve PR`, value: 'approve' },
|
|
412
|
+
{ name: ` ${cyan('▸')} View diff`, value: 'diff' },
|
|
413
|
+
],
|
|
414
|
+
}]);
|
|
415
|
+
|
|
416
|
+
if (action === 'back') return;
|
|
417
|
+
|
|
418
|
+
if (action === 'list') {
|
|
419
|
+
const { state } = await inquirer.prompt([{ type: 'list', name: 'state', message: 'State:', choices: ['open', 'closed', 'all'] }]);
|
|
420
|
+
await prs.listPRs({ state });
|
|
421
|
+
} else if (action === 'merge' || action === 'force-merge') {
|
|
422
|
+
const a = await ask([
|
|
423
|
+
{ type: 'input', name: 'n', message: yellow('PR #') + dim(' (q=back):') },
|
|
424
|
+
{
|
|
425
|
+
type: 'list', name: 'method', message: 'Merge method:', choices: [
|
|
426
|
+
{ name: `${green('squash')} ${dim('— clean single commit')}`, value: 'squash' },
|
|
427
|
+
{ name: `${cyan('merge')} ${dim('— preserve all commits')}`, value: 'merge' },
|
|
428
|
+
{ name: `${yellow('rebase')} ${dim('— linear history')}`, value: 'rebase' },
|
|
429
|
+
]
|
|
430
|
+
},
|
|
431
|
+
{ type: 'confirm', name: 'confirm', message: action === 'force-merge' ? yellow('⚠️ Force merge (skip CI)?') : red('⚠️ Merge this PR?'), default: false },
|
|
432
|
+
]);
|
|
433
|
+
if (a.confirm) await prs.mergePR(a.n, { method: a.method, force: action === 'force-merge' });
|
|
434
|
+
} else {
|
|
435
|
+
const { n } = await ask([{ type: 'input', name: 'n', message: yellow('PR #') + dim(' (q=back):') }]);
|
|
436
|
+
if (action === 'close') await prs.closePR(n);
|
|
437
|
+
else if (action === 'review') await prs.reviewPR(n);
|
|
438
|
+
else if (action === 'approve') await prs.approvePR(n);
|
|
439
|
+
else await prs.diffPR(n);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ── Repo Menu ──────────────────────────────────────────────────────────
|
|
444
|
+
async function repoMenu() {
|
|
445
|
+
const { action } = await inquirer.prompt([{
|
|
446
|
+
type: 'list', name: 'action', message: green('📦 Repo Operation:'),
|
|
447
|
+
choices: [
|
|
448
|
+
{ name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
|
|
449
|
+
new inquirer.Separator(dim(' ─────────────────────────────')),
|
|
450
|
+
{ name: ` ${green('▸')} List repositories`, value: 'list' },
|
|
451
|
+
{ name: ` ${green('▸')} Create repo`, value: 'create' },
|
|
452
|
+
{ name: ` ${red('▸')} Delete repo`, value: 'delete' },
|
|
453
|
+
{ name: ` ${cyan('▸')} Clone repo`, value: 'clone' },
|
|
454
|
+
{ name: ` ${cyan('▸')} Repo info`, value: 'info' },
|
|
455
|
+
{ name: ` ${cyan('▸')} Set topics`, value: 'topics' },
|
|
456
|
+
],
|
|
457
|
+
}]);
|
|
458
|
+
|
|
459
|
+
if (action === 'back') return;
|
|
460
|
+
|
|
461
|
+
if (action === 'list') {
|
|
462
|
+
const { org } = await ask([{ type: 'input', name: 'org', message: cyan('Org (blank for yours, q=back):'), default: '' }]);
|
|
463
|
+
await repos.listRepos({ org: org || undefined });
|
|
464
|
+
} else if (action === 'create') {
|
|
465
|
+
const a = await ask([
|
|
466
|
+
{ type: 'input', name: 'name', message: yellow('📦 Repo name') + dim(' (q=back):') },
|
|
467
|
+
{ type: 'input', name: 'org', message: dim('Org (blank for personal):'), default: '' },
|
|
468
|
+
{ type: 'input', name: 'description', message: dim('Description:'), default: '' },
|
|
469
|
+
{ type: 'confirm', name: 'isPrivate', message: 'Private?', default: false },
|
|
470
|
+
]);
|
|
471
|
+
await repos.createRepo(a.name, { org: a.org || undefined, description: a.description, private: a.isPrivate });
|
|
472
|
+
} else if (action === 'delete') {
|
|
473
|
+
const repoName = await ask([
|
|
474
|
+
{ type: 'input', name: 'name', message: red('📦 Repo to DELETE') + dim(' (q=back):') },
|
|
475
|
+
{ type: 'input', name: 'org', message: 'Org:', default: getOwner() },
|
|
476
|
+
]);
|
|
477
|
+
const { confirm } = await ask([
|
|
478
|
+
{ type: 'input', name: 'confirm', message: red(`⚠️ Type "${repoName.name}" to confirm deletion:`), validate: (v: string) => v === repoName.name || 'Name doesn\'t match' },
|
|
479
|
+
]);
|
|
480
|
+
if (confirm === repoName.name) await repos.deleteRepo(repoName.name, { org: repoName.org });
|
|
481
|
+
} else if (action === 'clone') {
|
|
482
|
+
const a = await ask([
|
|
483
|
+
{ type: 'input', name: 'name', message: cyan('Repo name') + dim(' (q=back):') },
|
|
484
|
+
{ type: 'input', name: 'org', message: dim('Org:'), default: getOwner() },
|
|
485
|
+
]);
|
|
486
|
+
await repos.cloneRepo(a.name, { org: a.org });
|
|
487
|
+
} else if (action === 'info') {
|
|
488
|
+
const a = await ask([
|
|
489
|
+
{ type: 'input', name: 'name', message: cyan('Repo name:'), default: getRepo() },
|
|
490
|
+
{ type: 'input', name: 'org', message: dim('Org:'), default: getOwner() },
|
|
491
|
+
]);
|
|
492
|
+
await repos.repoInfo(a.name, { org: a.org });
|
|
493
|
+
} else if (action === 'topics') {
|
|
494
|
+
const a = await ask([
|
|
495
|
+
{ type: 'input', name: 'name', message: cyan('Repo name:'), default: getRepo() },
|
|
496
|
+
{ type: 'input', name: 'topics', message: yellow('Topics (space-separated):') },
|
|
497
|
+
]);
|
|
498
|
+
await repos.setTopics(a.name, a.topics.split(/\s+/), { org: getOwner() });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ── Contributor Menu ───────────────────────────────────────────────────
|
|
503
|
+
async function contributorMenu() {
|
|
504
|
+
const { action } = await inquirer.prompt([{
|
|
505
|
+
type: 'list', name: 'action', message: yellow('🏆 Contributor Operation:'),
|
|
506
|
+
choices: [
|
|
507
|
+
{ name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
|
|
508
|
+
new inquirer.Separator(dim(' ─────────────────────────────')),
|
|
509
|
+
{ name: ` ${yellow('▸')} Score a user`, value: 'score' },
|
|
510
|
+
{ name: ` ${yellow('▸')} Rank applicants for issue`, value: 'rank' },
|
|
511
|
+
{ name: ` ${cyan('▸')} List contributors`, value: 'list' },
|
|
512
|
+
],
|
|
513
|
+
}]);
|
|
514
|
+
|
|
515
|
+
if (action === 'back') return;
|
|
516
|
+
|
|
517
|
+
if (action === 'score') {
|
|
518
|
+
const { u } = await ask([{ type: 'input', name: 'u', message: yellow('GitHub username') + dim(' (q=back):') }]);
|
|
519
|
+
await contributors.scoreUser(u);
|
|
520
|
+
} else if (action === 'rank') {
|
|
521
|
+
const { n } = await ask([{ type: 'input', name: 'n', message: yellow('Issue #') + dim(' (q=back):') }]);
|
|
522
|
+
await contributors.rankApplicants(parseInt(n));
|
|
523
|
+
} else {
|
|
524
|
+
await contributors.listContributors({});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ── Release Menu ───────────────────────────────────────────────────────
|
|
529
|
+
async function releaseMenu() {
|
|
530
|
+
const { action } = await inquirer.prompt([{
|
|
531
|
+
type: 'list', name: 'action', message: red('🚀 Release Operation:'),
|
|
532
|
+
choices: [
|
|
533
|
+
{ name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
|
|
534
|
+
new inquirer.Separator(dim(' ─────────────────────────────')),
|
|
535
|
+
{ name: ` ${red('▸')} Create release`, value: 'create' },
|
|
536
|
+
{ name: ` ${cyan('▸')} List releases`, value: 'list' },
|
|
537
|
+
],
|
|
538
|
+
}]);
|
|
539
|
+
|
|
540
|
+
if (action === 'back') return;
|
|
541
|
+
|
|
542
|
+
if (action === 'create') {
|
|
543
|
+
const a = await ask([
|
|
544
|
+
{ type: 'input', name: 'tag', message: yellow('Tag (e.g., v1.0.0)') + dim(' (q=back):') },
|
|
545
|
+
{ type: 'input', name: 'name', message: dim('Release name:'), default: '' },
|
|
546
|
+
{ type: 'confirm', name: 'draft', message: 'Draft?', default: false },
|
|
547
|
+
{ type: 'confirm', name: 'prerelease', message: 'Pre-release?', default: false },
|
|
548
|
+
]);
|
|
549
|
+
await releases.createRelease(a.tag, { name: a.name || a.tag, draft: a.draft, prerelease: a.prerelease });
|
|
550
|
+
} else {
|
|
551
|
+
await releases.listReleases({});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ── Commander (direct commands) ────────────────────────────────────────
|
|
556
|
+
function setupCommander(): Command {
|
|
557
|
+
const program = new Command();
|
|
558
|
+
program
|
|
559
|
+
.name('gitpadi')
|
|
560
|
+
.description(cyber('🤖 GitPadi — AI-powered GitHub management CLI'))
|
|
561
|
+
.version(VERSION)
|
|
562
|
+
.option('--owner <org>', 'GitHub owner/org')
|
|
563
|
+
.option('--repo <name>', 'GitHub repo name')
|
|
564
|
+
.option('--token <token>', 'GitHub token')
|
|
565
|
+
.hook('preAction', (cmd) => {
|
|
566
|
+
const o = cmd.opts();
|
|
567
|
+
initGitHub(o.token, o.owner, o.repo);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Issues
|
|
571
|
+
const i = program.command('issues').description('📋 Manage issues');
|
|
572
|
+
i.command('list').option('-s, --state <s>', '', 'open').option('-l, --labels <l>').option('-n, --limit <n>', '', '25')
|
|
573
|
+
.action((o) => issues.listIssues({ state: o.state, labels: o.labels, limit: parseInt(o.limit) }));
|
|
574
|
+
i.command('create').requiredOption('-t, --title <t>').option('-b, --body <b>').option('-l, --labels <l>')
|
|
575
|
+
.action((o) => issues.createIssue(o));
|
|
576
|
+
i.command('bulk').requiredOption('-f, --file <f>').option('-d, --dry-run').option('--start <n>', '', '1').option('--end <n>', '', '999')
|
|
577
|
+
.action((o) => issues.createIssuesFromFile(o.file, { dryRun: o.dryRun, start: parseInt(o.start), end: parseInt(o.end) }));
|
|
578
|
+
i.command('close <n>').action((n) => issues.closeIssue(parseInt(n)));
|
|
579
|
+
i.command('reopen <n>').action((n) => issues.reopenIssue(parseInt(n)));
|
|
580
|
+
i.command('delete <n>').action((n) => issues.deleteIssue(parseInt(n)));
|
|
581
|
+
i.command('assign <n> <users...>').action((n, u) => issues.assignIssue(parseInt(n), u));
|
|
582
|
+
i.command('assign-best <n>').action((n) => issues.assignBest(parseInt(n)));
|
|
583
|
+
i.command('search <q>').action((q) => issues.searchIssues(q));
|
|
584
|
+
i.command('label <n> <labels...>').action((n, l) => issues.labelIssue(parseInt(n), l));
|
|
585
|
+
|
|
586
|
+
// PRs
|
|
587
|
+
const p = program.command('prs').description('🔀 Manage pull requests');
|
|
588
|
+
p.command('list').option('-s, --state <s>', '', 'open').option('-n, --limit <n>', '', '25')
|
|
589
|
+
.action((o) => prs.listPRs({ state: o.state, limit: parseInt(o.limit) }));
|
|
590
|
+
p.command('merge <n>').option('-m, --method <m>', '', 'squash').option('--message <msg>').option('-f, --force', 'Skip CI checks')
|
|
591
|
+
.action((n, o) => prs.mergePR(parseInt(n), o));
|
|
592
|
+
p.command('close <n>').action((n) => prs.closePR(parseInt(n)));
|
|
593
|
+
p.command('review <n>').action((n) => prs.reviewPR(parseInt(n)));
|
|
594
|
+
p.command('approve <n>').action((n) => prs.approvePR(parseInt(n)));
|
|
595
|
+
p.command('diff <n>').action((n) => prs.diffPR(parseInt(n)));
|
|
596
|
+
|
|
597
|
+
// Repos
|
|
598
|
+
const r = program.command('repo').description('📦 Manage repositories');
|
|
599
|
+
r.command('list').option('-o, --org <o>').option('-n, --limit <n>', '', '25')
|
|
600
|
+
.action((o) => repos.listRepos({ org: o.org, limit: parseInt(o.limit) }));
|
|
601
|
+
r.command('create <name>').option('-o, --org <o>').option('-p, --private').option('-d, --description <d>')
|
|
602
|
+
.action((n, o) => repos.createRepo(n, o));
|
|
603
|
+
r.command('delete <name>').option('-o, --org <o>').action((n, o) => repos.deleteRepo(n, o));
|
|
604
|
+
r.command('clone <name>').option('-o, --org <o>').option('-d, --dir <d>').action((n, o) => repos.cloneRepo(n, o));
|
|
605
|
+
r.command('info <name>').option('-o, --org <o>').action((n, o) => repos.repoInfo(n, o));
|
|
606
|
+
r.command('topics <name> <topics...>').option('-o, --org <o>').action((n, t, o) => repos.setTopics(n, t, o));
|
|
607
|
+
|
|
608
|
+
// Contributors
|
|
609
|
+
const c = program.command('contributors').description('🏆 Manage contributors');
|
|
610
|
+
c.command('score <user>').action((u) => contributors.scoreUser(u));
|
|
611
|
+
c.command('rank <issue>').action((n) => contributors.rankApplicants(parseInt(n)));
|
|
612
|
+
c.command('list').option('-n, --limit <n>', '', '25').action((o) => contributors.listContributors({ limit: parseInt(o.limit) }));
|
|
613
|
+
|
|
614
|
+
// Releases
|
|
615
|
+
const rel = program.command('release').description('🚀 Manage releases');
|
|
616
|
+
rel.command('create <tag>').option('-n, --name <n>').option('-b, --body <b>').option('-d, --draft').option('-p, --prerelease').option('--no-generate')
|
|
617
|
+
.action((t, o) => releases.createRelease(t, o));
|
|
618
|
+
rel.command('list').option('-n, --limit <n>', '', '10').action((o) => releases.listReleases({ limit: parseInt(o.limit) }));
|
|
619
|
+
|
|
620
|
+
return program;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ── Entry Point ────────────────────────────────────────────────────────
|
|
624
|
+
async function main() {
|
|
625
|
+
if (process.argv.length <= 2) {
|
|
626
|
+
await bootSequence();
|
|
627
|
+
await onboarding();
|
|
628
|
+
await mainMenu();
|
|
629
|
+
} else {
|
|
630
|
+
const program = setupCommander();
|
|
631
|
+
await program.parseAsync(process.argv);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
main().catch(async (e) => {
|
|
636
|
+
// Ctrl+C / SIGINT — show nice shutdown
|
|
637
|
+
if (e?.name === 'ExitPromptError' || e?.message?.includes('force closed') || e?.message?.includes('SIGINT')) {
|
|
638
|
+
console.log('');
|
|
639
|
+
console.log(dim(' ▸ Saving session...'));
|
|
640
|
+
console.log(dim(' ▸ Disconnecting from GitHub...'));
|
|
641
|
+
console.log('');
|
|
642
|
+
console.log(fire(' 🤖 GitPadi is shutting down now. See you soon, pal :)'));
|
|
643
|
+
console.log('');
|
|
644
|
+
process.exit(0);
|
|
645
|
+
}
|
|
646
|
+
console.error(red(`\n ❌ Fatal: ${e.message}\n`));
|
|
647
|
+
process.exit(1);
|
|
648
|
+
});
|