gitpadi 2.0.0 → 2.0.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 ADDED
@@ -0,0 +1,789 @@
1
+ #!/usr/bin/env node
2
+ // ═══════════════════════════════════════════════════════════════════════
3
+ // GitPadi v2.0 — Your AI-Powered GitHub Management Terminal
4
+ // "Built different. Run different."
5
+ // ═══════════════════════════════════════════════════════════════════════
6
+ import { Command } from 'commander';
7
+ import chalk from 'chalk';
8
+ import inquirer from 'inquirer';
9
+ import gradient from 'gradient-string';
10
+ import figlet from 'figlet';
11
+ import boxen from 'boxen';
12
+ import { createSpinner } from 'nanospinner';
13
+ import { Octokit } from '@octokit/rest';
14
+ import { initGitHub, setRepo, getOwner, getRepo, getOctokit, saveConfig, getToken } from './core/github.js';
15
+ import * as issues from './commands/issues.js';
16
+ import * as prs from './commands/prs.js';
17
+ import * as repos from './commands/repos.js';
18
+ import * as contributors from './commands/contributors.js';
19
+ import * as releases from './commands/releases.js';
20
+ import * as contribute from './commands/contribute.js';
21
+ const VERSION = '2.0.0';
22
+ // ── Styling ────────────────────────────────────────────────────────────
23
+ const cyber = gradient(['#ff00ff', '#00ffff', '#ff00ff']);
24
+ const neon = gradient(['#00ff87', '#60efff']);
25
+ const fire = gradient(['#ff6b35', '#f7c948', '#ff6b35']);
26
+ const dim = chalk.dim;
27
+ const bold = chalk.bold;
28
+ const green = chalk.greenBright;
29
+ const cyan = chalk.cyanBright;
30
+ const magenta = chalk.magentaBright;
31
+ const yellow = chalk.yellowBright;
32
+ const red = chalk.redBright;
33
+ // ── Utilities ──────────────────────────────────────────────────────────
34
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
35
+ async function typewriter(text, speed = 20) {
36
+ for (const char of text) {
37
+ process.stdout.write(char);
38
+ await sleep(speed);
39
+ }
40
+ console.log('');
41
+ }
42
+ function line(char = '─', len = 60) {
43
+ console.log(dim(char.repeat(len)));
44
+ }
45
+ // ── Back Navigation ────────────────────────────────────────────────────
46
+ // Type 'q' at any text/number prompt to go back to the menu
47
+ class BackToMenu extends Error {
48
+ constructor() { super('back'); }
49
+ }
50
+ function checkForBack(value) {
51
+ if (typeof value === 'string' && value.trim().toLowerCase() === 'q') {
52
+ throw new BackToMenu();
53
+ }
54
+ }
55
+ // Wraps inquirer.prompt — runs prompts one at a time, checks for 'q' after each
56
+ async function ask(questions) {
57
+ const answers = {};
58
+ for (const question of questions) {
59
+ const ans = await inquirer.prompt([question]);
60
+ answers[question.name] = ans[question.name];
61
+ // Check for 'q' immediately after each text/number input
62
+ if ((question.type === 'input' || question.type === 'number' || question.type === 'password')) {
63
+ const val = String(answers[question.name] ?? '');
64
+ if (val.trim().toLowerCase() === 'q')
65
+ throw new BackToMenu();
66
+ }
67
+ }
68
+ return answers;
69
+ }
70
+ async function safeMenu(fn) {
71
+ try {
72
+ await fn();
73
+ }
74
+ catch (e) {
75
+ if (e instanceof BackToMenu) {
76
+ console.log(dim('\n ↩ Back to main menu\n'));
77
+ return;
78
+ }
79
+ throw e; // Ctrl+C and other errors bubble up to trigger shutdown
80
+ }
81
+ }
82
+ // ── Boot Sequence ──────────────────────────────────────────────────────
83
+ async function bootSequence() {
84
+ console.clear();
85
+ const banner = figlet.textSync('GitPadi', {
86
+ font: 'ANSI Shadow',
87
+ horizontalLayout: 'fitted',
88
+ });
89
+ console.log(cyber(banner));
90
+ console.log(boxen(`${bold('v' + VERSION)} ${dim('|')} ${dim('AI-Powered GitHub Management')} ${dim('|')} ${dim('by')} ${magenta('Netwalls')}`, {
91
+ padding: { left: 2, right: 2, top: 0, bottom: 0 },
92
+ borderStyle: 'double',
93
+ borderColor: 'magenta',
94
+ dimBorder: true,
95
+ }));
96
+ console.log('');
97
+ const bootSteps = [
98
+ '▸ Initializing GitPadi engine',
99
+ '▸ Loading command modules',
100
+ '▸ Establishing GitHub connection',
101
+ '▸ Systems online',
102
+ ];
103
+ for (const step of bootSteps) {
104
+ process.stdout.write(dim(step));
105
+ await sleep(150);
106
+ process.stdout.write(green(' ✓\n'));
107
+ await sleep(80);
108
+ }
109
+ console.log('');
110
+ }
111
+ // ── Onboarding ─────────────────────────────────────────────────────────
112
+ /**
113
+ * Ensures we have a valid GitHub token
114
+ */
115
+ async function ensureAuthenticated() {
116
+ initGitHub();
117
+ let token = getToken();
118
+ if (token) {
119
+ try {
120
+ const octokit = getOctokit();
121
+ await octokit.users.getAuthenticated();
122
+ // Silent success if token is valid
123
+ return;
124
+ }
125
+ catch {
126
+ console.log(red(' ❌ Saved session invalid. Re-authenticating...'));
127
+ token = '';
128
+ }
129
+ }
130
+ console.log(neon(' ⚡ Authentication — let\'s connect you to GitHub.\n'));
131
+ const { t } = await inquirer.prompt([{
132
+ type: 'password',
133
+ name: 't',
134
+ message: magenta('🔑 GitHub Token') + dim(' (ghp_xxx):'),
135
+ mask: '•',
136
+ validate: async (v) => {
137
+ if (!v.startsWith('ghp_') && !v.startsWith('github_pat_')) {
138
+ return 'Token should start with ghp_ or github_pat_';
139
+ }
140
+ const spinner = createSpinner(dim('Validating token...')).start();
141
+ try {
142
+ const tempOctokit = new Octokit({ auth: v });
143
+ await tempOctokit.users.getAuthenticated();
144
+ spinner.success({ text: green('Token valid!') });
145
+ return true;
146
+ }
147
+ catch {
148
+ spinner.error({ text: red('Invalid token - GitHub rejected it.') });
149
+ return 'Invalid token';
150
+ }
151
+ },
152
+ }]);
153
+ initGitHub(t);
154
+ saveConfig({ token: t, owner: getOwner() || '', repo: getRepo() || '' });
155
+ }
156
+ /**
157
+ * Ensures we have a target repository (Owner/Repo)
158
+ */
159
+ async function ensureTargetRepo() {
160
+ let owner = getOwner();
161
+ let repo = getRepo();
162
+ if (owner && repo) {
163
+ return;
164
+ }
165
+ console.log(neon('\n 📦 Project Targeting — which repo are we working on?\n'));
166
+ const answers = await inquirer.prompt([
167
+ {
168
+ type: 'input',
169
+ name: 'owner',
170
+ message: cyan('👤 GitHub Owner/Org:'),
171
+ default: owner || '',
172
+ validate: (v) => v.length > 0 || 'Required',
173
+ },
174
+ {
175
+ type: 'input',
176
+ name: 'repo',
177
+ message: cyan('📦 Repository name:'),
178
+ default: repo || '',
179
+ validate: (v) => v.length > 0 || 'Required',
180
+ },
181
+ ]);
182
+ setRepo(answers.owner, answers.repo);
183
+ saveConfig({ token: getToken(), owner: answers.owner, repo: answers.repo });
184
+ const spinner = createSpinner(dim('Connecting...')).start();
185
+ await sleep(400);
186
+ spinner.success({ text: green(`Locked in → ${cyan(`${answers.owner}/${answers.repo}`)}`) });
187
+ console.log('');
188
+ }
189
+ // ── Mode Selector ──────────────────────────────────────────────────────
190
+ async function mainMenu() {
191
+ while (true) {
192
+ line('═');
193
+ console.log(cyber(' ⟨ GITPADI MODE SELECTOR ⟩'));
194
+ console.log(dim(' Select your workflow persona to continue'));
195
+ line('═');
196
+ console.log('');
197
+ const { mode } = await inquirer.prompt([{
198
+ type: 'list',
199
+ name: 'mode',
200
+ message: bold('Choose your path:'),
201
+ choices: [
202
+ { name: `${cyan('✨')} ${bold('Contributor Mode')} ${dim('— fork, clone, sync, submit PRs')}`, value: 'contributor' },
203
+ { name: `${magenta('🛠️')} ${bold('Maintainer Mode')} ${dim('— manage issues, PRs, contributors')}`, value: 'maintainer' },
204
+ new inquirer.Separator(dim(' ─────────────────────────────')),
205
+ { name: `${dim('👋')} ${dim('Exit')}`, value: 'exit' },
206
+ ],
207
+ loop: false,
208
+ }]);
209
+ if (mode === 'contributor')
210
+ await safeMenu(contributorMenu);
211
+ else if (mode === 'maintainer') {
212
+ await ensureTargetRepo();
213
+ await safeMenu(maintainerMenu);
214
+ }
215
+ else
216
+ break;
217
+ }
218
+ }
219
+ // ── Contributor Menu ───────────────────────────────────────────────────
220
+ async function contributorMenu() {
221
+ while (true) {
222
+ line('═');
223
+ console.log(cyan(' ✨ GITPADI CONTRIBUTOR WORKSPACE'));
224
+ console.log(dim(' Automating forking, syncing, and PR delivery'));
225
+ line('═');
226
+ // Auto-check for updates if in a repo
227
+ if (getOwner() && getRepo()) {
228
+ await contribute.syncBranch();
229
+ }
230
+ const { action } = await inquirer.prompt([{
231
+ type: 'list',
232
+ name: 'action',
233
+ message: bold('Contributor Action:'),
234
+ choices: [
235
+ { name: `${cyan('🚀')} ${bold('Start Contribution')} ${dim('— fork, clone & branch')}`, value: 'start' },
236
+ { name: `${green('🔄')} ${bold('Sync with Upstream')} ${dim('— pull latest changes')}`, value: 'sync' },
237
+ { name: `${yellow('📋')} ${bold('View Action Logs')} ${dim('— check PR/commit status')}`, value: 'logs' },
238
+ { name: `${magenta('🚀')} ${bold('Submit PR')} ${dim('— add, commit, push & PR')}`, value: 'submit' },
239
+ new inquirer.Separator(dim(' ─────────────────────────────')),
240
+ { name: `${dim('⬅')} ${dim('Back to Mode Selector')}`, value: 'back' },
241
+ ],
242
+ loop: false,
243
+ }]);
244
+ if (action === 'back')
245
+ break;
246
+ if (action === 'start') {
247
+ const { url } = await ask([{ type: 'input', name: 'url', message: 'Enter Repo or Issue URL:' }]);
248
+ await contribute.forkAndClone(url);
249
+ }
250
+ else {
251
+ // These actions require a target repo
252
+ await ensureTargetRepo();
253
+ if (action === 'sync') {
254
+ await contribute.syncBranch();
255
+ }
256
+ else if (action === 'logs') {
257
+ await contribute.viewLogs();
258
+ }
259
+ else if (action === 'submit') {
260
+ const { title, message, issue } = await ask([
261
+ { type: 'input', name: 'title', message: 'PR Title:' },
262
+ { type: 'input', name: 'message', message: 'Commit message (optional):' },
263
+ { type: 'input', name: 'issue', message: 'Related Issue # (optional):' }
264
+ ]);
265
+ await contribute.submitPR({
266
+ title,
267
+ message: message || title,
268
+ issue: issue ? parseInt(issue) : undefined
269
+ });
270
+ }
271
+ }
272
+ }
273
+ }
274
+ // ── Maintainer Menu ────────────────────────────────────────────────────
275
+ async function maintainerMenu() {
276
+ while (true) {
277
+ line('═');
278
+ console.log(magenta(' 🛠️ GITPADI MAINTAINER PANEL'));
279
+ console.log(dim(' Managing repository health and contributor intake'));
280
+ line('═');
281
+ console.log('');
282
+ const { category } = await inquirer.prompt([{
283
+ type: 'list',
284
+ name: 'category',
285
+ message: bold('Select operation:'),
286
+ choices: [
287
+ { name: `${magenta('📋')} ${bold('Issues')} ${dim('— create, close, delete, assign, search')}`, value: 'issues' },
288
+ { name: `${cyan('🔀')} ${bold('Pull Requests')} ${dim('— merge, review, approve, diff')}`, value: 'prs' },
289
+ { name: `${green('📦')} ${bold('Repositories')} ${dim('— create, delete, clone, info')}`, value: 'repos' },
290
+ { name: `${yellow('🏆')} ${bold('Contributors')} ${dim('— score, rank, auto-assign best')}`, value: 'contributors' },
291
+ { name: `${red('🚀')} ${bold('Releases')} ${dim('— create, list, manage')}`, value: 'releases' },
292
+ new inquirer.Separator(dim(' ─────────────────────────────')),
293
+ { name: `${dim('⚙️')} ${dim('Switch Repo')}`, value: 'switch' },
294
+ { name: `${dim('⬅')} ${dim('Back to Mode Selector')}`, value: 'back' },
295
+ ],
296
+ loop: false,
297
+ }]);
298
+ if (category === 'back')
299
+ break;
300
+ if (category === 'switch') {
301
+ await safeMenu(async () => {
302
+ const a = await inquirer.prompt([
303
+ { type: 'input', name: 'owner', message: cyan('👤 New Owner/Org:'), default: getOwner() },
304
+ { type: 'input', name: 'repo', message: cyan('📦 New Repo:'), default: getRepo() },
305
+ ]);
306
+ setRepo(a.owner, a.repo);
307
+ const s = createSpinner(dim('Switching...')).start();
308
+ await sleep(400);
309
+ s.success({ text: green(`Now targeting ${cyan(`${a.owner}/${a.repo}`)}`) });
310
+ });
311
+ continue;
312
+ }
313
+ if (category === 'issues')
314
+ await safeMenu(issueMenu);
315
+ else if (category === 'prs')
316
+ await safeMenu(prMenu);
317
+ else if (category === 'repos')
318
+ await safeMenu(repoMenu);
319
+ else if (category === 'contributors')
320
+ await safeMenu(contributorScoringMenu);
321
+ else if (category === 'releases')
322
+ await safeMenu(releaseMenu);
323
+ console.log('');
324
+ }
325
+ }
326
+ // ── Issue Menu ─────────────────────────────────────────────────────────
327
+ async function issueMenu() {
328
+ const { action } = await inquirer.prompt([{
329
+ type: 'list', name: 'action', message: magenta('📋 Issue Operation:'),
330
+ choices: [
331
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
332
+ new inquirer.Separator(dim(' ─────────────────────────────')),
333
+ { name: ` ${green('▸')} List open issues`, value: 'list' },
334
+ { name: ` ${green('▸')} Create single issue`, value: 'create' },
335
+ { name: ` ${magenta('▸')} Bulk create from file (JSON/MD)`, value: 'bulk' },
336
+ { name: ` ${red('▸')} Close issue`, value: 'close' },
337
+ { name: ` ${green('▸')} Reopen issue`, value: 'reopen' },
338
+ { name: ` ${red('▸')} Delete (close & lock)`, value: 'delete' },
339
+ { name: ` ${cyan('▸')} Assign user`, value: 'assign' },
340
+ { name: ` ${yellow('▸')} Auto-assign best applicant`, value: 'assign-best' },
341
+ { name: ` ${cyan('▸')} Search issues`, value: 'search' },
342
+ { name: ` ${cyan('▸')} Add labels`, value: 'label' },
343
+ ],
344
+ }]);
345
+ if (action === 'back')
346
+ return;
347
+ if (action === 'list') {
348
+ const opts = await ask([
349
+ { type: 'list', name: 'state', message: 'State:', choices: ['open', 'closed', 'all'], default: 'open' },
350
+ { type: 'input', name: 'labels', message: dim('Filter by labels (optional, q=back):'), default: '' },
351
+ { type: 'input', name: 'limit', message: dim('Max results:'), default: '50' },
352
+ ]);
353
+ await issues.listIssues({ state: opts.state, labels: opts.labels || undefined, limit: parseInt(opts.limit) });
354
+ }
355
+ else if (action === 'create') {
356
+ const a = await ask([
357
+ { type: 'input', name: 'title', message: yellow('Issue title'), validate: (v) => v.length > 0 || 'Required' },
358
+ { type: 'input', name: 'body', message: dim('Issue body (optional):'), default: '' },
359
+ { type: 'input', name: 'labels', message: dim('Labels (comma-separated):'), default: '' },
360
+ ]);
361
+ await issues.createIssue(a);
362
+ }
363
+ else if (action === 'bulk') {
364
+ const a = await ask([
365
+ { type: 'input', name: 'file', message: yellow('📁 Path to issues file (JSON or MD)') + dim(' (q=back):') },
366
+ { type: 'confirm', name: 'dryRun', message: 'Dry run first?', default: true },
367
+ { type: 'number', name: 'start', message: dim('Start index:'), default: 1 },
368
+ { type: 'number', name: 'end', message: dim('End index:'), default: 999 },
369
+ ]);
370
+ await issues.createIssuesFromFile(a.file, { dryRun: a.dryRun, start: a.start, end: a.end });
371
+ }
372
+ else if (action === 'assign-best') {
373
+ const spinner = createSpinner(dim('Finding issues with applicants...')).start();
374
+ const octokit = getOctokit();
375
+ const { data: allIssues } = await octokit.issues.listForRepo({
376
+ owner: getOwner(), repo: getRepo(), state: 'open', per_page: 50,
377
+ });
378
+ const realIssues = allIssues.filter((i) => !i.pull_request && i.comments > 0);
379
+ const issuesWithApplicants = [];
380
+ for (const issue of realIssues) {
381
+ const { data: comments } = await octokit.issues.listComments({
382
+ owner: getOwner(), repo: getRepo(), issue_number: issue.number, per_page: 100,
383
+ });
384
+ const applicantUsers = new Set();
385
+ comments.forEach((c) => {
386
+ if (c.user?.login && c.user.login !== 'github-actions[bot]') {
387
+ applicantUsers.add(c.user.login);
388
+ }
389
+ });
390
+ if (applicantUsers.size > 0) {
391
+ issuesWithApplicants.push({
392
+ number: issue.number,
393
+ title: issue.title.substring(0, 45),
394
+ applicants: Array.from(applicantUsers),
395
+ labels: issue.labels.map((l) => typeof l === 'string' ? l : l.name || '').filter(Boolean),
396
+ });
397
+ }
398
+ }
399
+ spinner.stop();
400
+ if (issuesWithApplicants.length === 0) {
401
+ console.log(yellow('\n ⚠️ No open issues with comments found.\n'));
402
+ return;
403
+ }
404
+ console.log(`\n${bold(`🏆 Issues with Applicants`)} (${issuesWithApplicants.length})\n`);
405
+ const { picked } = await inquirer.prompt([{
406
+ type: 'list', name: 'picked', message: yellow('Select an issue:'),
407
+ choices: [
408
+ { name: ` ${dim('⬅ Back')}`, value: -1 },
409
+ new inquirer.Separator(dim(' ─────────────────────────────')),
410
+ ].concat(issuesWithApplicants.map((i) => ({
411
+ name: ` #${i.number} ${bold(i.title)} ${dim('—')} ${cyan(`${i.applicants.length} applicant(s):`)} ${i.applicants.map((u) => `@${u}`).join(', ')}`,
412
+ value: i.number,
413
+ }))),
414
+ }]);
415
+ if (picked === -1)
416
+ return;
417
+ await issues.assignBest(picked);
418
+ }
419
+ else if (action === 'close' || action === 'reopen' || action === 'delete') {
420
+ const { n } = await ask([{ type: 'input', name: 'n', message: yellow('Issue #') + dim(' (q=back):') }]);
421
+ if (!n || isNaN(Number(n)))
422
+ return;
423
+ if (action === 'close')
424
+ await issues.closeIssue(n);
425
+ else if (action === 'reopen')
426
+ await issues.reopenIssue(n);
427
+ else if (action === 'delete') {
428
+ const { confirm } = await inquirer.prompt([{ type: 'confirm', name: 'confirm', message: red('⚠️ This will close & lock the issue. Proceed?'), default: false }]);
429
+ if (confirm)
430
+ await issues.deleteIssue(n);
431
+ }
432
+ }
433
+ else if (action === 'assign') {
434
+ const a = await ask([
435
+ { type: 'input', name: 'n', message: yellow('Issue #') + dim(' (q=back):') },
436
+ { type: 'input', name: 'users', message: cyan('Username(s) (space-separated):') },
437
+ ]);
438
+ await issues.assignIssue(a.n, a.users.split(/\s+/));
439
+ }
440
+ else if (action === 'search') {
441
+ const { q } = await ask([{ type: 'input', name: 'q', message: cyan('🔍 Search query') + dim(' (q=back):') }]);
442
+ await issues.searchIssues(q);
443
+ }
444
+ else if (action === 'label') {
445
+ const a = await ask([
446
+ { type: 'input', name: 'n', message: yellow('Issue #') + dim(' (q=back):') },
447
+ { type: 'input', name: 'labels', message: cyan('Labels (space-separated):') },
448
+ ]);
449
+ await issues.labelIssue(a.n, a.labels.split(/\s+/));
450
+ }
451
+ }
452
+ // ── PR Menu ────────────────────────────────────────────────────────────
453
+ async function prMenu() {
454
+ const { action } = await inquirer.prompt([{
455
+ type: 'list', name: 'action', message: cyan('🔀 PR Operation:'),
456
+ choices: [
457
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
458
+ new inquirer.Separator(dim(' ─────────────────────────────')),
459
+ { name: ` ${green('▸')} List pull requests`, value: 'list' },
460
+ { name: ` ${green('▸')} Merge PR ${dim('(checks CI first)')}`, value: 'merge' },
461
+ { name: ` ${yellow('▸')} Force merge ${dim('(skip CI checks)')}`, value: 'force-merge' },
462
+ { name: ` ${red('▸')} Close PR`, value: 'close' },
463
+ { name: ` ${yellow('▸')} Auto-review PR`, value: 'review' },
464
+ { name: ` ${green('▸')} Approve PR`, value: 'approve' },
465
+ { name: ` ${cyan('▸')} View diff`, value: 'diff' },
466
+ ],
467
+ }]);
468
+ if (action === 'back')
469
+ return;
470
+ if (action === 'list') {
471
+ const a = await ask([
472
+ { type: 'list', name: 'state', message: 'State:', choices: ['open', 'closed', 'all'], default: 'open' },
473
+ { type: 'input', name: 'limit', message: dim('Max results:'), default: '50' },
474
+ ]);
475
+ await prs.listPRs({ state: a.state, limit: parseInt(a.limit) });
476
+ }
477
+ else if (action === 'merge' || action === 'force-merge') {
478
+ const a = await ask([
479
+ { type: 'input', name: 'n', message: yellow('PR #') + dim(' (q=back):') },
480
+ {
481
+ type: 'list', name: 'method', message: 'Merge method:', choices: [
482
+ { name: `${green('squash')} ${dim('— clean single commit')}`, value: 'squash' },
483
+ { name: `${cyan('merge')} ${dim('— preserve all commits')}`, value: 'merge' },
484
+ { name: `${yellow('rebase')} ${dim('— linear history')}`, value: 'rebase' },
485
+ ]
486
+ },
487
+ { type: 'confirm', name: 'confirm', message: action === 'force-merge' ? yellow('⚠️ Force merge (skip CI)?') : red('⚠️ Merge this PR?'), default: false },
488
+ ]);
489
+ if (a.confirm)
490
+ await prs.mergePR(a.n, { method: a.method, force: action === 'force-merge' });
491
+ }
492
+ else {
493
+ const { n } = await ask([{ type: 'input', name: 'n', message: yellow('PR #') + dim(' (q=back):') }]);
494
+ if (action === 'close')
495
+ await prs.closePR(n);
496
+ else if (action === 'review')
497
+ await prs.reviewPR(n);
498
+ else if (action === 'approve')
499
+ await prs.approvePR(n);
500
+ else
501
+ await prs.diffPR(n);
502
+ }
503
+ }
504
+ // ── Repo Menu ──────────────────────────────────────────────────────────
505
+ async function repoMenu() {
506
+ const { action } = await inquirer.prompt([{
507
+ type: 'list', name: 'action', message: green('📦 Repo Operation:'),
508
+ choices: [
509
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
510
+ new inquirer.Separator(dim(' ─────────────────────────────')),
511
+ { name: ` ${green('▸')} List repositories`, value: 'list' },
512
+ { name: ` ${green('▸')} Create repo`, value: 'create' },
513
+ { name: ` ${red('▸')} Delete repo`, value: 'delete' },
514
+ { name: ` ${cyan('▸')} Clone repo`, value: 'clone' },
515
+ { name: ` ${cyan('▸')} Repo info`, value: 'info' },
516
+ { name: ` ${cyan('▸')} Set topics`, value: 'topics' },
517
+ ],
518
+ }]);
519
+ if (action === 'back')
520
+ return;
521
+ if (action === 'list') {
522
+ const a = await ask([
523
+ { type: 'input', name: 'org', message: cyan('Org (blank for yours, q=back):'), default: '' },
524
+ { type: 'input', name: 'limit', message: dim('Max results:'), default: '50' },
525
+ ]);
526
+ await repos.listRepos({ org: a.org || undefined, limit: parseInt(a.limit) });
527
+ }
528
+ else if (action === 'create') {
529
+ const a = await ask([
530
+ { type: 'input', name: 'name', message: yellow('📦 Repo name') + dim(' (q=back):') },
531
+ { type: 'input', name: 'org', message: dim('Org (blank for personal):'), default: '' },
532
+ { type: 'input', name: 'description', message: dim('Description:'), default: '' },
533
+ { type: 'confirm', name: 'isPrivate', message: 'Private?', default: false },
534
+ ]);
535
+ await repos.createRepo(a.name, { org: a.org || undefined, description: a.description, private: a.isPrivate });
536
+ }
537
+ else if (action === 'delete') {
538
+ const { org } = await ask([
539
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
540
+ ]);
541
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
542
+ let repoToDelete;
543
+ if (fetchedRepos.length > 0) {
544
+ const { selection } = await inquirer.prompt([{
545
+ type: 'list',
546
+ name: 'selection',
547
+ message: red('📦 Select Repo to DELETE:'),
548
+ choices: [
549
+ { name: dim('⬅ Back'), value: 'back' },
550
+ ...fetchedRepos.map((r) => ({ name: r.name, value: r.name }))
551
+ ]
552
+ }]);
553
+ if (selection === 'back')
554
+ return;
555
+ repoToDelete = selection;
556
+ }
557
+ else {
558
+ const { name } = await ask([{ type: 'input', name: 'name', message: red('📦 Repo name to DELETE:') }]);
559
+ repoToDelete = name;
560
+ }
561
+ const { confirm } = await ask([
562
+ { type: 'input', name: 'confirm', message: red(`⚠️ Type "${repoToDelete}" to confirm deletion:`), validate: (v) => v === repoToDelete || 'Name doesn\'t match' },
563
+ ]);
564
+ if (confirm === repoToDelete)
565
+ await repos.deleteRepo(repoToDelete, { org });
566
+ }
567
+ else if (action === 'clone') {
568
+ const { org } = await ask([
569
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
570
+ ]);
571
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
572
+ let repoToClone;
573
+ if (fetchedRepos.length > 0) {
574
+ const { selection } = await inquirer.prompt([{
575
+ type: 'list',
576
+ name: 'selection',
577
+ message: cyan('📦 Select Repo to Clone:'),
578
+ choices: [
579
+ { name: dim('⬅ Back'), value: 'back' },
580
+ ...fetchedRepos.map((r) => ({ name: r.name, value: r.name }))
581
+ ]
582
+ }]);
583
+ if (selection === 'back')
584
+ return;
585
+ repoToClone = selection;
586
+ }
587
+ else {
588
+ const { name } = await ask([{ type: 'input', name: 'name', message: cyan('📦 Repo name to Clone:') }]);
589
+ repoToClone = name;
590
+ }
591
+ const { dir } = await ask([
592
+ { type: 'input', name: 'dir', message: dim('Destination path (leave blank for default):'), default: '' },
593
+ ]);
594
+ await repos.cloneRepo(repoToClone, { org, dir: dir || undefined });
595
+ }
596
+ else if (action === 'info') {
597
+ const { org } = await ask([
598
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
599
+ ]);
600
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
601
+ let repoToInfo;
602
+ if (fetchedRepos.length > 0) {
603
+ const { selection } = await inquirer.prompt([{
604
+ type: 'list',
605
+ name: 'selection',
606
+ message: cyan('📦 Select Repo for Info:'),
607
+ choices: [
608
+ { name: dim('⬅ Back'), value: 'back' },
609
+ ...fetchedRepos.map((r) => ({ name: r.name, value: r.name }))
610
+ ]
611
+ }]);
612
+ if (selection === 'back')
613
+ return;
614
+ repoToInfo = selection;
615
+ }
616
+ else {
617
+ const { name } = await ask([{ type: 'input', name: 'name', message: cyan('📦 Repo name:') }]);
618
+ repoToInfo = name;
619
+ }
620
+ await repos.repoInfo(repoToInfo, { org });
621
+ }
622
+ else if (action === 'topics') {
623
+ const a = await ask([
624
+ { type: 'input', name: 'name', message: cyan('Repo name:'), default: getRepo() },
625
+ { type: 'input', name: 'topics', message: yellow('Topics (space-separated):') },
626
+ ]);
627
+ await repos.setTopics(a.name, a.topics.split(/\s+/), { org: getOwner() });
628
+ }
629
+ }
630
+ // ── Contributor Scoring Menu (Maintainer Tool) ─────────────────────────
631
+ async function contributorScoringMenu() {
632
+ const { action } = await inquirer.prompt([{
633
+ type: 'list', name: 'action', message: yellow('🏆 Contributor Operation:'),
634
+ choices: [
635
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
636
+ new inquirer.Separator(dim(' ─────────────────────────────')),
637
+ { name: ` ${yellow('▸')} Score a user`, value: 'score' },
638
+ { name: ` ${yellow('▸')} Rank applicants for issue`, value: 'rank' },
639
+ { name: ` ${cyan('▸')} List contributors`, value: 'list' },
640
+ ],
641
+ }]);
642
+ if (action === 'back')
643
+ return;
644
+ if (action === 'score') {
645
+ const { u } = await ask([{ type: 'input', name: 'u', message: yellow('GitHub username') + dim(' (q=back):') }]);
646
+ await contributors.scoreUser(u);
647
+ }
648
+ else if (action === 'rank') {
649
+ const { n } = await ask([{ type: 'input', name: 'n', message: yellow('Issue #') + dim(' (q=back):') }]);
650
+ await contributors.rankApplicants(parseInt(n));
651
+ }
652
+ else if (action === 'list') {
653
+ const a = await ask([{ type: 'input', name: 'limit', message: dim('Max results (q=back):'), default: '50' }]);
654
+ await contributors.listContributors({ limit: parseInt(a.limit) });
655
+ }
656
+ }
657
+ // ── Release Menu ───────────────────────────────────────────────────────
658
+ async function releaseMenu() {
659
+ const { action } = await inquirer.prompt([{
660
+ type: 'list', name: 'action', message: red('🚀 Release Operation:'),
661
+ choices: [
662
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
663
+ new inquirer.Separator(dim(' ─────────────────────────────')),
664
+ { name: ` ${green('▸')} List releases`, value: 'list' },
665
+ { name: ` ${green('▸')} Create release`, value: 'create' },
666
+ ],
667
+ }]);
668
+ if (action === 'back')
669
+ return;
670
+ if (action === 'list') {
671
+ const a = await ask([{ type: 'input', name: 'limit', message: dim('Max results:'), default: '50' }]);
672
+ await releases.listReleases({ limit: parseInt(a.limit) });
673
+ }
674
+ else if (action === 'create') {
675
+ const a = await ask([
676
+ { type: 'input', name: 'tag', message: yellow('Tag (e.g. v1.0.0):') },
677
+ { type: 'input', name: 'name', message: dim('Release name (optional):'), default: '' },
678
+ { type: 'input', name: 'body', message: dim('Release body (optional):'), default: '' },
679
+ { type: 'confirm', name: 'draft', message: 'Draft?', default: false },
680
+ { type: 'confirm', name: 'prerelease', message: 'Prerelease?', default: false },
681
+ ]);
682
+ await releases.createRelease(a.tag, { name: a.name, body: a.body, draft: a.draft, prerelease: a.prerelease });
683
+ }
684
+ }
685
+ // ── Commander (direct commands) ────────────────────────────────────────
686
+ function setupCommander() {
687
+ const program = new Command();
688
+ program
689
+ .name('gitpadi')
690
+ .description(cyber('🤖 GitPadi — AI-powered GitHub management CLI'))
691
+ .version(VERSION)
692
+ .option('--owner <org>', 'GitHub owner/org')
693
+ .option('--repo <name>', 'GitHub repo name')
694
+ .option('--token <token>', 'GitHub token')
695
+ .hook('preAction', (cmd) => {
696
+ const o = cmd.opts();
697
+ initGitHub(o.token, o.owner, o.repo);
698
+ });
699
+ // Issues
700
+ const i = program.command('issues').description('📋 Manage issues');
701
+ i.command('list').option('-s, --state <s>', '', 'open').option('-l, --labels <l>').option('-n, --limit <n>', '', '50')
702
+ .action(async (o) => { await issues.listIssues({ state: o.state, labels: o.labels, limit: parseInt(o.limit) }); });
703
+ i.command('create').requiredOption('-t, --title <t>').option('-b, --body <b>').option('-l, --labels <l>')
704
+ .action(async (o) => { await issues.createIssue(o); });
705
+ i.command('bulk').requiredOption('-f, --file <f>').option('-d, --dry-run').option('--start <n>', '', '1').option('--end <n>', '', '999')
706
+ .action(async (o) => { await issues.createIssuesFromFile(o.file, { dryRun: o.dryRun, start: parseInt(o.start), end: parseInt(o.end) }); });
707
+ i.command('close <n>').action(async (n) => { await issues.closeIssue(parseInt(n)); });
708
+ i.command('reopen <n>').action(async (n) => { await issues.reopenIssue(parseInt(n)); });
709
+ i.command('delete <n>').action(async (n) => { await issues.deleteIssue(parseInt(n)); });
710
+ i.command('assign <n> <users...>').action(async (n, u) => { await issues.assignIssue(parseInt(n), u); });
711
+ i.command('assign-best <n>').action(async (n) => { await issues.assignBest(parseInt(n)); });
712
+ i.command('search <q>').action(async (q) => { await issues.searchIssues(q); });
713
+ i.command('label <n> <labels...>').action(async (n, l) => { await issues.labelIssue(parseInt(n), l); });
714
+ // PRs
715
+ const p = program.command('prs').description('🔀 Manage pull requests');
716
+ p.command('list').option('-s, --state <s>', '', 'open').option('-n, --limit <n>', '', '50')
717
+ .action(async (o) => { await prs.listPRs({ state: o.state, limit: parseInt(o.limit) }); });
718
+ p.command('merge <n>').option('-m, --method <m>', '', 'squash').option('--message <msg>').option('-f, --force', 'Skip CI checks')
719
+ .action(async (n, o) => { await prs.mergePR(parseInt(n), o); });
720
+ p.command('close <n>').action(async (n) => { await prs.closePR(parseInt(n)); });
721
+ p.command('review <n>').action(async (n) => { await prs.reviewPR(parseInt(n)); });
722
+ p.command('approve <n>').action(async (n) => { await prs.approvePR(parseInt(n)); });
723
+ p.command('diff <n>').action(async (n) => { await prs.diffPR(parseInt(n)); });
724
+ // Repos
725
+ const r = program.command('repo').description('📦 Manage repositories');
726
+ r.command('list').option('-o, --org <o>').option('-n, --limit <n>', '', '50')
727
+ .action(async (o) => { await repos.listRepos({ org: o.org, limit: parseInt(o.limit) }); });
728
+ r.command('create <name>').option('-o, --org <o>').option('-p, --private').option('-d, --description <d>')
729
+ .action(async (n, o) => { await repos.createRepo(n, o); });
730
+ r.command('delete <name>').option('-o, --org <o>').action(async (n, o) => { await repos.deleteRepo(n, o); });
731
+ r.command('clone <name>').option('-o, --org <o>').option('-d, --dir <d>').action(async (n, o) => { await repos.cloneRepo(n, o); });
732
+ r.command('info <name>').option('-o, --org <o>').action(async (n, o) => { await repos.repoInfo(n, o); });
733
+ r.command('topics <name> <topics...>').option('-o, --org <o>').action(async (n, t, o) => { await repos.setTopics(n, t, o); });
734
+ // Contributors
735
+ const c = program.command('contributors').description('🏆 Manage contributors');
736
+ c.command('score <user>').action(async (u) => { await contributors.scoreUser(u); });
737
+ c.command('rank <issue>').action(async (n) => { await contributors.rankApplicants(parseInt(n)); });
738
+ c.command('list').option('-n, --limit <n>', '', '50').action(async (o) => { await contributors.listContributors({ limit: parseInt(o.limit) }); });
739
+ // Releases
740
+ const rel = program.command('release').description('🚀 Manage releases');
741
+ rel.command('create <tag>').option('-n, --name <n>').option('-b, --body <b>').option('-d, --draft').option('-p, --prerelease').option('--no-generate')
742
+ .action(async (t, o) => { await releases.createRelease(t, o); });
743
+ rel.command('list').option('-n, --limit <n>', '', '50').action(async (o) => { await releases.listReleases({ limit: parseInt(o.limit) }); });
744
+ // Contributor Commands
745
+ // ILLUSTRATION: How to start?
746
+ // Option A: `gitpadi start https://github.com/owner/repo`
747
+ // Option B: Interactive Menu -> Contributor Mode -> Start Contribution
748
+ program.command('start <url>').description('🚀 Start contribution (fork, clone & branch)').action(async (u) => { await contribute.forkAndClone(u); });
749
+ program.command('sync').description('🔄 Sync with upstream').action(async () => { await contribute.syncBranch(); });
750
+ program.command('submit').description('🚀 Submit PR (add, commit, push & PR)')
751
+ .option('-t, --title <t>', 'PR title')
752
+ .option('-m, --message <m>', 'Commit message')
753
+ .option('-i, --issue <n>', 'Issue number')
754
+ .action(async (o) => {
755
+ await contribute.submitPR({
756
+ title: o.title || 'Automated PR',
757
+ message: o.message || o.title,
758
+ issue: o.issue ? parseInt(o.issue) : undefined
759
+ });
760
+ });
761
+ program.command('logs').description('📋 View Action logs').action(async () => { await contribute.viewLogs(); });
762
+ return program;
763
+ }
764
+ // ── Entry Point ────────────────────────────────────────────────────────
765
+ async function main() {
766
+ if (process.argv.length <= 2) {
767
+ await bootSequence();
768
+ await ensureAuthenticated();
769
+ await mainMenu();
770
+ }
771
+ else {
772
+ const program = setupCommander();
773
+ await program.parseAsync(process.argv);
774
+ }
775
+ }
776
+ main().catch(async (e) => {
777
+ // Ctrl+C / SIGINT — show nice shutdown
778
+ if (e?.name === 'ExitPromptError' || e?.message?.includes('force closed') || e?.message?.includes('SIGINT')) {
779
+ console.log('');
780
+ console.log(dim(' ▸ Saving session...'));
781
+ console.log(dim(' ▸ Disconnecting from GitHub...'));
782
+ console.log('');
783
+ console.log(fire(' 🤖 GitPadi is shutting down now. See you soon, pal :)'));
784
+ console.log('');
785
+ process.exit(0);
786
+ }
787
+ console.error(red(`\n ❌ Fatal: ${e.message}\n`));
788
+ process.exit(1);
789
+ });