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/src/cli.ts CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env npx tsx
1
+ #!/usr/bin/env node
2
2
  // ═══════════════════════════════════════════════════════════════════════
3
3
  // GitPadi v2.0 — Your AI-Powered GitHub Management Terminal
4
4
  // "Built different. Run different."
@@ -9,15 +9,20 @@ import chalk from 'chalk';
9
9
  import inquirer from 'inquirer';
10
10
  import gradient from 'gradient-string';
11
11
  import figlet from 'figlet';
12
+ import os from 'node:os';
13
+ import { execSync } from 'child_process';
14
+
12
15
  import boxen from 'boxen';
13
16
  import { createSpinner } from 'nanospinner';
14
- import { initGitHub, setRepo, getOwner, getRepo, getOctokit } from './core/github.js';
17
+ import { Octokit } from '@octokit/rest';
18
+ import { initGitHub, setRepo, getOwner, getRepo, getOctokit, loadConfig, saveConfig, getToken } from './core/github.js';
15
19
 
16
20
  import * as issues from './commands/issues.js';
17
21
  import * as prs from './commands/prs.js';
18
22
  import * as repos from './commands/repos.js';
19
23
  import * as contributors from './commands/contributors.js';
20
24
  import * as releases from './commands/releases.js';
25
+ import * as contribute from './commands/contribute.js';
21
26
 
22
27
  const VERSION = '2.0.0';
23
28
 
@@ -123,116 +128,207 @@ async function bootSequence() {
123
128
  }
124
129
 
125
130
  // ── 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('');
131
+ /**
132
+ * Ensures we have a valid GitHub token
133
+ */
134
+ async function ensureAuthenticated() {
135
+ initGitHub();
136
+ let token = getToken();
137
+
138
+ if (token) {
139
+ try {
140
+ const octokit = getOctokit();
141
+ await octokit.users.getAuthenticated();
142
+ // Silent success if token is valid
143
+ return;
144
+ } catch {
145
+ console.log(red(' ❌ Saved session invalid. Re-authenticating...'));
146
+ token = '';
147
+ }
148
+ }
149
+
150
+ console.log(neon(' ⚡ Authentication — let\'s connect you to GitHub.\n'));
151
+
152
+ const { t } = await inquirer.prompt([{
153
+ type: 'password',
154
+ name: 't',
155
+ message: magenta('🔑 GitHub Token') + dim(' (ghp_xxx):'),
156
+ mask: '•',
157
+ validate: async (v: string) => {
158
+ if (!v.startsWith('ghp_') && !v.startsWith('github_pat_')) {
159
+ return 'Token should start with ghp_ or github_pat_';
160
+ }
161
+ const spinner = createSpinner(dim('Validating token...')).start();
162
+ try {
163
+ const tempOctokit = new Octokit({ auth: v });
164
+ await tempOctokit.users.getAuthenticated();
165
+ spinner.success({ text: green('Token valid!') });
166
+ return true;
167
+ } catch {
168
+ spinner.error({ text: red('Invalid token - GitHub rejected it.') });
169
+ return 'Invalid token';
170
+ }
171
+ },
172
+ }]);
173
+
174
+ initGitHub(t);
175
+ saveConfig({ token: t, owner: getOwner() || '', repo: getRepo() || '' });
176
+ }
177
+
178
+ /**
179
+ * Ensures we have a target repository (Owner/Repo)
180
+ */
181
+ async function ensureTargetRepo() {
182
+ let owner = getOwner();
183
+ let repo = getRepo();
184
+
185
+ if (owner && repo) {
137
186
  return;
138
187
  }
139
188
 
140
- console.log(neon(' First-time setuplet\'s connect you to GitHub.\n'));
189
+ console.log(neon('\n 📦 Project Targetingwhich repo are we working on?\n'));
141
190
 
142
191
  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
192
  {
152
193
  type: 'input',
153
194
  name: 'owner',
154
195
  message: cyan('👤 GitHub Owner/Org:'),
155
- default: savedOwner || '',
196
+ default: owner || '',
156
197
  validate: (v: string) => v.length > 0 || 'Required',
157
- when: !savedOwner,
158
198
  },
159
199
  {
160
200
  type: 'input',
161
201
  name: 'repo',
162
202
  message: cyan('📦 Repository name:'),
163
- default: savedRepo || '',
203
+ default: repo || '',
164
204
  validate: (v: string) => v.length > 0 || 'Required',
165
- when: !savedRepo,
166
205
  },
167
206
  ]);
168
207
 
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}`)}`) });
208
+ setRepo(answers.owner, answers.repo);
209
+ saveConfig({ token: getToken(), owner: answers.owner, repo: answers.repo });
178
210
 
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
- ));
211
+ const spinner = createSpinner(dim('Connecting...')).start();
212
+ await sleep(400);
213
+ spinner.success({ text: green(`Locked in ${cyan(`${answers.owner}/${answers.repo}`)}`) });
187
214
  console.log('');
188
215
  }
189
216
 
190
- // ── Main Menu ──────────────────────────────────────────────────────────
217
+ // ── Mode Selector ──────────────────────────────────────────────────────
191
218
  async function mainMenu() {
192
219
  while (true) {
193
220
  line('═');
194
- console.log(cyber(' ⟨ GITPADI COMMAND CENTER ⟩'));
195
- console.log(dim(' Select Back on lists • Type q on text prompts'));
221
+ console.log(cyber(' ⟨ GITPADI MODE SELECTOR ⟩'));
222
+ console.log(dim(' Select your workflow persona to continue'));
196
223
  line('═');
197
224
  console.log('');
198
225
 
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
+ const { mode } = await inquirer.prompt([{
227
+ type: 'list',
228
+ name: 'mode',
229
+ message: bold('Choose your path:'),
230
+ choices: [
231
+ { name: `${cyan('✨')} ${bold('Contributor Mode')} ${dim('— fork, clone, sync, submit PRs')}`, value: 'contributor' },
232
+ { name: `${magenta('🛠️')} ${bold('Maintainer Mode')} ${dim('— manage issues, PRs, contributors')}`, value: 'maintainer' },
233
+ new inquirer.Separator(dim(' ─────────────────────────────')),
234
+ { name: `${dim('👋')} ${dim('Exit')}`, value: 'exit' },
235
+ ],
236
+ loop: false,
237
+ }]);
238
+
239
+ if (mode === 'contributor') await safeMenu(contributorMenu);
240
+ else if (mode === 'maintainer') {
241
+ await ensureTargetRepo();
242
+ await safeMenu(maintainerMenu);
243
+ }
244
+ else break;
245
+ }
246
+ }
247
+
248
+ // ── Contributor Menu ───────────────────────────────────────────────────
249
+ async function contributorMenu() {
250
+ while (true) {
251
+ line('');
252
+ console.log(cyan(' ✨ GITPADI CONTRIBUTOR WORKSPACE'));
253
+ console.log(dim(' Automating forking, syncing, and PR delivery'));
254
+ line('═');
255
+
256
+ // Auto-check for updates if in a repo
257
+ if (getOwner() && getRepo()) {
258
+ await contribute.syncBranch();
226
259
  }
227
260
 
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;
261
+ const { action } = await inquirer.prompt([{
262
+ type: 'list',
263
+ name: 'action',
264
+ message: bold('Contributor Action:'),
265
+ choices: [
266
+ { name: `${cyan('🚀')} ${bold('Start Contribution')} ${dim('— fork, clone & branch')}`, value: 'start' },
267
+ { name: `${green('🔄')} ${bold('Sync with Upstream')} ${dim('— pull latest changes')}`, value: 'sync' },
268
+ { name: `${yellow('📋')} ${bold('View Action Logs')} ${dim('— check PR/commit status')}`, value: 'logs' },
269
+ { name: `${magenta('🚀')} ${bold('Submit PR')} ${dim('— add, commit, push & PR')}`, value: 'submit' },
270
+ new inquirer.Separator(dim(' ─────────────────────────────')),
271
+ { name: `${dim('⬅')} ${dim('Back to Mode Selector')}`, value: 'back' },
272
+ ],
273
+ loop: false,
274
+ }]);
275
+
276
+ if (action === 'back') break;
277
+
278
+ if (action === 'start') {
279
+ const { url } = await ask([{ type: 'input', name: 'url', message: 'Enter Repo or Issue URL:' }]);
280
+ await contribute.forkAndClone(url);
281
+ } else {
282
+ // These actions require a target repo
283
+ await ensureTargetRepo();
284
+
285
+ if (action === 'sync') {
286
+ await contribute.syncBranch();
287
+ } else if (action === 'logs') {
288
+ await contribute.viewLogs();
289
+ } else if (action === 'submit') {
290
+ const { title, message, issue } = await ask([
291
+ { type: 'input', name: 'title', message: 'PR Title:' },
292
+ { type: 'input', name: 'message', message: 'Commit message (optional):' },
293
+ { type: 'input', name: 'issue', message: 'Related Issue # (optional):' }
294
+ ]);
295
+ await contribute.submitPR({
296
+ title,
297
+ message: message || title,
298
+ issue: issue ? parseInt(issue) : undefined
299
+ });
300
+ }
235
301
  }
302
+ }
303
+ }
304
+
305
+ // ── Maintainer Menu ────────────────────────────────────────────────────
306
+ async function maintainerMenu() {
307
+ while (true) {
308
+ line('═');
309
+ console.log(magenta(' 🛠️ GITPADI MAINTAINER PANEL'));
310
+ console.log(dim(' Managing repository health and contributor intake'));
311
+ line('═');
312
+ console.log('');
313
+
314
+ const { category } = await inquirer.prompt([{
315
+ type: 'list',
316
+ name: 'category',
317
+ message: bold('Select operation:'),
318
+ choices: [
319
+ { name: `${magenta('📋')} ${bold('Issues')} ${dim('— create, close, delete, assign, search')}`, value: 'issues' },
320
+ { name: `${cyan('🔀')} ${bold('Pull Requests')} ${dim('— merge, review, approve, diff')}`, value: 'prs' },
321
+ { name: `${green('📦')} ${bold('Repositories')} ${dim('— create, delete, clone, info')}`, value: 'repos' },
322
+ { name: `${yellow('🏆')} ${bold('Contributors')} ${dim('— score, rank, auto-assign best')}`, value: 'contributors' },
323
+ { name: `${red('🚀')} ${bold('Releases')} ${dim('— create, list, manage')}`, value: 'releases' },
324
+ new inquirer.Separator(dim(' ─────────────────────────────')),
325
+ { name: `${dim('⚙️')} ${dim('Switch Repo')}`, value: 'switch' },
326
+ { name: `${dim('⬅')} ${dim('Back to Mode Selector')}`, value: 'back' },
327
+ ],
328
+ loop: false,
329
+ }]);
330
+
331
+ if (category === 'back') break;
236
332
 
237
333
  if (category === 'switch') {
238
334
  await safeMenu(async () => {
@@ -249,10 +345,10 @@ async function mainMenu() {
249
345
  }
250
346
 
251
347
  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);
348
+ else if (category === 'prs') await safeMenu(prMenu);
349
+ else if (category === 'repos') await safeMenu(repoMenu);
350
+ else if (category === 'contributors') await safeMenu(contributorScoringMenu);
351
+ else if (category === 'releases') await safeMenu(releaseMenu);
256
352
 
257
353
  console.log('');
258
354
  }
@@ -263,11 +359,11 @@ async function issueMenu() {
263
359
  const { action } = await inquirer.prompt([{
264
360
  type: 'list', name: 'action', message: magenta('📋 Issue Operation:'),
265
361
  choices: [
266
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
362
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
267
363
  new inquirer.Separator(dim(' ─────────────────────────────')),
268
364
  { name: ` ${green('▸')} List open issues`, value: 'list' },
269
365
  { name: ` ${green('▸')} Create single issue`, value: 'create' },
270
- { name: ` ${green('▸')} Bulk create from JSON`, value: 'bulk' },
366
+ { name: ` ${magenta('▸')} Bulk create from file (JSON/MD)`, value: 'bulk' },
271
367
  { name: ` ${red('▸')} Close issue`, value: 'close' },
272
368
  { name: ` ${green('▸')} Reopen issue`, value: 'reopen' },
273
369
  { name: ` ${red('▸')} Delete (close & lock)`, value: 'delete' },
@@ -284,30 +380,25 @@ async function issueMenu() {
284
380
  const opts = await ask([
285
381
  { type: 'list', name: 'state', message: 'State:', choices: ['open', 'closed', 'all'], default: 'open' },
286
382
  { type: 'input', name: 'labels', message: dim('Filter by labels (optional, q=back):'), default: '' },
383
+ { type: 'input', name: 'limit', message: dim('Max results:'), default: '50' },
287
384
  ]);
288
- await issues.listIssues({ state: opts.state, labels: opts.labels || undefined });
385
+ await issues.listIssues({ state: opts.state, labels: opts.labels || undefined, limit: parseInt(opts.limit) });
289
386
  } else if (action === 'create') {
290
387
  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):' },
388
+ { type: 'input', name: 'title', message: yellow('Issue title'), validate: (v: string) => v.length > 0 || 'Required' },
389
+ { type: 'input', name: 'body', message: dim('Issue body (optional):'), default: '' },
293
390
  { type: 'input', name: 'labels', message: dim('Labels (comma-separated):'), default: '' },
294
391
  ]);
295
392
  await issues.createIssue(a);
296
393
  } else if (action === 'bulk') {
297
394
  const a = await ask([
298
- { type: 'input', name: 'file', message: yellow('📁 Path to issues JSON') + dim(' (q=back):') },
395
+ { type: 'input', name: 'file', message: yellow('📁 Path to issues file (JSON or MD)') + dim(' (q=back):') },
299
396
  { type: 'confirm', name: 'dryRun', message: 'Dry run first?', default: true },
300
397
  { type: 'number', name: 'start', message: dim('Start index:'), default: 1 },
301
398
  { type: 'number', name: 'end', message: dim('End index:'), default: 999 },
302
399
  ]);
303
400
  await issues.createIssuesFromFile(a.file, { dryRun: a.dryRun, start: a.start, end: a.end });
304
401
  } 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
402
  const spinner = createSpinner(dim('Finding issues with applicants...')).start();
312
403
  const octokit = getOctokit();
313
404
 
@@ -315,10 +406,7 @@ async function issueMenu() {
315
406
  owner: getOwner(), repo: getRepo(), state: 'open', per_page: 50,
316
407
  });
317
408
 
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
409
+ const realIssues = allIssues.filter((i: any) => !i.pull_request && i.comments > 0);
322
410
  const issuesWithApplicants: Array<{ number: number; title: string; applicants: string[]; labels: string[] }> = [];
323
411
 
324
412
  for (const issue of realIssues) {
@@ -327,7 +415,7 @@ async function issueMenu() {
327
415
  });
328
416
 
329
417
  const applicantUsers = new Set<string>();
330
- comments.forEach((c) => {
418
+ comments.forEach((c: any) => {
331
419
  if (c.user?.login && c.user.login !== 'github-actions[bot]') {
332
420
  applicantUsers.add(c.user.login);
333
421
  }
@@ -338,7 +426,7 @@ async function issueMenu() {
338
426
  number: issue.number,
339
427
  title: issue.title.substring(0, 45),
340
428
  applicants: Array.from(applicantUsers),
341
- labels: issue.labels.map((l) => typeof l === 'string' ? l : l.name || '').filter(Boolean),
429
+ labels: issue.labels.map((l: any) => typeof l === 'string' ? l : l.name || '').filter(Boolean),
342
430
  });
343
431
  }
344
432
  }
@@ -352,7 +440,6 @@ async function issueMenu() {
352
440
 
353
441
  console.log(`\n${bold(`🏆 Issues with Applicants`)} (${issuesWithApplicants.length})\n`);
354
442
 
355
- // Let user pick an issue
356
443
  const { picked } = await inquirer.prompt([{
357
444
  type: 'list', name: 'picked', message: yellow('Select an issue:'),
358
445
  choices: [
@@ -365,8 +452,6 @@ async function issueMenu() {
365
452
  }]);
366
453
 
367
454
  if (picked === -1) return;
368
-
369
- // Run the scoring
370
455
  await issues.assignBest(picked);
371
456
 
372
457
  } else if (action === 'close' || action === 'reopen' || action === 'delete') {
@@ -401,7 +486,7 @@ async function prMenu() {
401
486
  const { action } = await inquirer.prompt([{
402
487
  type: 'list', name: 'action', message: cyan('🔀 PR Operation:'),
403
488
  choices: [
404
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
489
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
405
490
  new inquirer.Separator(dim(' ─────────────────────────────')),
406
491
  { name: ` ${green('▸')} List pull requests`, value: 'list' },
407
492
  { name: ` ${green('▸')} Merge PR ${dim('(checks CI first)')}`, value: 'merge' },
@@ -416,8 +501,11 @@ async function prMenu() {
416
501
  if (action === 'back') return;
417
502
 
418
503
  if (action === 'list') {
419
- const { state } = await inquirer.prompt([{ type: 'list', name: 'state', message: 'State:', choices: ['open', 'closed', 'all'] }]);
420
- await prs.listPRs({ state });
504
+ const a = await ask([
505
+ { type: 'list', name: 'state', message: 'State:', choices: ['open', 'closed', 'all'], default: 'open' },
506
+ { type: 'input', name: 'limit', message: dim('Max results:'), default: '50' },
507
+ ]);
508
+ await prs.listPRs({ state: a.state, limit: parseInt(a.limit) });
421
509
  } else if (action === 'merge' || action === 'force-merge') {
422
510
  const a = await ask([
423
511
  { type: 'input', name: 'n', message: yellow('PR #') + dim(' (q=back):') },
@@ -445,7 +533,7 @@ async function repoMenu() {
445
533
  const { action } = await inquirer.prompt([{
446
534
  type: 'list', name: 'action', message: green('📦 Repo Operation:'),
447
535
  choices: [
448
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
536
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
449
537
  new inquirer.Separator(dim(' ─────────────────────────────')),
450
538
  { name: ` ${green('▸')} List repositories`, value: 'list' },
451
539
  { name: ` ${green('▸')} Create repo`, value: 'create' },
@@ -459,8 +547,11 @@ async function repoMenu() {
459
547
  if (action === 'back') return;
460
548
 
461
549
  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 });
550
+ const a = await ask([
551
+ { type: 'input', name: 'org', message: cyan('Org (blank for yours, q=back):'), default: '' },
552
+ { type: 'input', name: 'limit', message: dim('Max results:'), default: '50' },
553
+ ]);
554
+ await repos.listRepos({ org: a.org || undefined, limit: parseInt(a.limit) });
464
555
  } else if (action === 'create') {
465
556
  const a = await ask([
466
557
  { type: 'input', name: 'name', message: yellow('📦 Repo name') + dim(' (q=back):') },
@@ -470,26 +561,88 @@ async function repoMenu() {
470
561
  ]);
471
562
  await repos.createRepo(a.name, { org: a.org || undefined, description: a.description, private: a.isPrivate });
472
563
  } 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() },
564
+ const { org } = await ask([
565
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
476
566
  ]);
567
+
568
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
569
+ let repoToDelete: string;
570
+
571
+ if (fetchedRepos.length > 0) {
572
+ const { selection } = await inquirer.prompt([{
573
+ type: 'list',
574
+ name: 'selection',
575
+ message: red('📦 Select Repo to DELETE:'),
576
+ choices: [
577
+ { name: dim('⬅ Back'), value: 'back' },
578
+ ...fetchedRepos.map((r: any) => ({ name: r.name, value: r.name }))
579
+ ]
580
+ }]);
581
+ if (selection === 'back') return;
582
+ repoToDelete = selection;
583
+ } else {
584
+ const { name } = await ask([{ type: 'input', name: 'name', message: red('📦 Repo name to DELETE:') }]);
585
+ repoToDelete = name;
586
+ }
587
+
477
588
  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' },
589
+ { type: 'input', name: 'confirm', message: red(`⚠️ Type "${repoToDelete}" to confirm deletion:`), validate: (v: string) => v === repoToDelete || 'Name doesn\'t match' },
479
590
  ]);
480
- if (confirm === repoName.name) await repos.deleteRepo(repoName.name, { org: repoName.org });
591
+ if (confirm === repoToDelete) await repos.deleteRepo(repoToDelete, { org });
481
592
  } 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() },
593
+ const { org } = await ask([
594
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
485
595
  ]);
486
- await repos.cloneRepo(a.name, { org: a.org });
596
+
597
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
598
+ let repoToClone: string;
599
+
600
+ if (fetchedRepos.length > 0) {
601
+ const { selection } = await inquirer.prompt([{
602
+ type: 'list',
603
+ name: 'selection',
604
+ message: cyan('📦 Select Repo to Clone:'),
605
+ choices: [
606
+ { name: dim('⬅ Back'), value: 'back' },
607
+ ...fetchedRepos.map((r: any) => ({ name: r.name, value: r.name }))
608
+ ]
609
+ }]);
610
+ if (selection === 'back') return;
611
+ repoToClone = selection;
612
+ } else {
613
+ const { name } = await ask([{ type: 'input', name: 'name', message: cyan('📦 Repo name to Clone:') }]);
614
+ repoToClone = name;
615
+ }
616
+
617
+ const { dir } = await ask([
618
+ { type: 'input', name: 'dir', message: dim('Destination path (leave blank for default):'), default: '' },
619
+ ]);
620
+ await repos.cloneRepo(repoToClone, { org, dir: dir || undefined });
487
621
  } 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() },
622
+ const { org } = await ask([
623
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
491
624
  ]);
492
- await repos.repoInfo(a.name, { org: a.org });
625
+
626
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
627
+ let repoToInfo: string;
628
+
629
+ if (fetchedRepos.length > 0) {
630
+ const { selection } = await inquirer.prompt([{
631
+ type: 'list',
632
+ name: 'selection',
633
+ message: cyan('📦 Select Repo for Info:'),
634
+ choices: [
635
+ { name: dim('⬅ Back'), value: 'back' },
636
+ ...fetchedRepos.map((r: any) => ({ name: r.name, value: r.name }))
637
+ ]
638
+ }]);
639
+ if (selection === 'back') return;
640
+ repoToInfo = selection;
641
+ } else {
642
+ const { name } = await ask([{ type: 'input', name: 'name', message: cyan('📦 Repo name:') }]);
643
+ repoToInfo = name;
644
+ }
645
+ await repos.repoInfo(repoToInfo, { org });
493
646
  } else if (action === 'topics') {
494
647
  const a = await ask([
495
648
  { type: 'input', name: 'name', message: cyan('Repo name:'), default: getRepo() },
@@ -499,12 +652,12 @@ async function repoMenu() {
499
652
  }
500
653
  }
501
654
 
502
- // ── Contributor Menu ───────────────────────────────────────────────────
503
- async function contributorMenu() {
655
+ // ── Contributor Scoring Menu (Maintainer Tool) ─────────────────────────
656
+ async function contributorScoringMenu() {
504
657
  const { action } = await inquirer.prompt([{
505
658
  type: 'list', name: 'action', message: yellow('🏆 Contributor Operation:'),
506
659
  choices: [
507
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
660
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
508
661
  new inquirer.Separator(dim(' ─────────────────────────────')),
509
662
  { name: ` ${yellow('▸')} Score a user`, value: 'score' },
510
663
  { name: ` ${yellow('▸')} Rank applicants for issue`, value: 'rank' },
@@ -520,8 +673,9 @@ async function contributorMenu() {
520
673
  } else if (action === 'rank') {
521
674
  const { n } = await ask([{ type: 'input', name: 'n', message: yellow('Issue #') + dim(' (q=back):') }]);
522
675
  await contributors.rankApplicants(parseInt(n));
523
- } else {
524
- await contributors.listContributors({});
676
+ } else if (action === 'list') {
677
+ const a = await ask([{ type: 'input', name: 'limit', message: dim('Max results (q=back):'), default: '50' }]);
678
+ await contributors.listContributors({ limit: parseInt(a.limit) });
525
679
  }
526
680
  }
527
681
 
@@ -530,25 +684,27 @@ async function releaseMenu() {
530
684
  const { action } = await inquirer.prompt([{
531
685
  type: 'list', name: 'action', message: red('🚀 Release Operation:'),
532
686
  choices: [
533
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
687
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
534
688
  new inquirer.Separator(dim(' ─────────────────────────────')),
535
- { name: ` ${red('▸')} Create release`, value: 'create' },
536
- { name: ` ${cyan('▸')} List releases`, value: 'list' },
689
+ { name: ` ${green('▸')} List releases`, value: 'list' },
690
+ { name: ` ${green('▸')} Create release`, value: 'create' },
537
691
  ],
538
692
  }]);
539
693
 
540
694
  if (action === 'back') return;
541
695
 
542
- if (action === 'create') {
696
+ if (action === 'list') {
697
+ const a = await ask([{ type: 'input', name: 'limit', message: dim('Max results:'), default: '50' }]);
698
+ await releases.listReleases({ limit: parseInt(a.limit) });
699
+ } else if (action === 'create') {
543
700
  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: '' },
701
+ { type: 'input', name: 'tag', message: yellow('Tag (e.g. v1.0.0):') },
702
+ { type: 'input', name: 'name', message: dim('Release name (optional):'), default: '' },
703
+ { type: 'input', name: 'body', message: dim('Release body (optional):'), default: '' },
546
704
  { type: 'confirm', name: 'draft', message: 'Draft?', default: false },
547
- { type: 'confirm', name: 'prerelease', message: 'Pre-release?', default: false },
705
+ { type: 'confirm', name: 'prerelease', message: 'Prerelease?', default: false },
548
706
  ]);
549
- await releases.createRelease(a.tag, { name: a.name || a.tag, draft: a.draft, prerelease: a.prerelease });
550
- } else {
551
- await releases.listReleases({});
707
+ await releases.createRelease(a.tag, { name: a.name, body: a.body, draft: a.draft, prerelease: a.prerelease });
552
708
  }
553
709
  }
554
710
 
@@ -569,53 +725,72 @@ function setupCommander(): Command {
569
725
 
570
726
  // Issues
571
727
  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) }));
728
+ i.command('list').option('-s, --state <s>', '', 'open').option('-l, --labels <l>').option('-n, --limit <n>', '', '50')
729
+ .action(async (o) => { await issues.listIssues({ state: o.state, labels: o.labels, limit: parseInt(o.limit) }); });
574
730
  i.command('create').requiredOption('-t, --title <t>').option('-b, --body <b>').option('-l, --labels <l>')
575
- .action((o) => issues.createIssue(o));
731
+ .action(async (o) => { await issues.createIssue(o); });
576
732
  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));
733
+ .action(async (o) => { await issues.createIssuesFromFile(o.file, { dryRun: o.dryRun, start: parseInt(o.start), end: parseInt(o.end) }); });
734
+ i.command('close <n>').action(async (n) => { await issues.closeIssue(parseInt(n)); });
735
+ i.command('reopen <n>').action(async (n) => { await issues.reopenIssue(parseInt(n)); });
736
+ i.command('delete <n>').action(async (n) => { await issues.deleteIssue(parseInt(n)); });
737
+ i.command('assign <n> <users...>').action(async (n, u) => { await issues.assignIssue(parseInt(n), u); });
738
+ i.command('assign-best <n>').action(async (n) => { await issues.assignBest(parseInt(n)); });
739
+ i.command('search <q>').action(async (q) => { await issues.searchIssues(q); });
740
+ i.command('label <n> <labels...>').action(async (n, l) => { await issues.labelIssue(parseInt(n), l); });
585
741
 
586
742
  // PRs
587
743
  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) }));
744
+ p.command('list').option('-s, --state <s>', '', 'open').option('-n, --limit <n>', '', '50')
745
+ .action(async (o) => { await prs.listPRs({ state: o.state, limit: parseInt(o.limit) }); });
590
746
  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)));
747
+ .action(async (n, o) => { await prs.mergePR(parseInt(n), o); });
748
+ p.command('close <n>').action(async (n) => { await prs.closePR(parseInt(n)); });
749
+ p.command('review <n>').action(async (n) => { await prs.reviewPR(parseInt(n)); });
750
+ p.command('approve <n>').action(async (n) => { await prs.approvePR(parseInt(n)); });
751
+ p.command('diff <n>').action(async (n) => { await prs.diffPR(parseInt(n)); });
596
752
 
597
753
  // Repos
598
754
  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) }));
755
+ r.command('list').option('-o, --org <o>').option('-n, --limit <n>', '', '50')
756
+ .action(async (o) => { await repos.listRepos({ org: o.org, limit: parseInt(o.limit) }); });
601
757
  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));
758
+ .action(async (n, o) => { await repos.createRepo(n, o); });
759
+ r.command('delete <name>').option('-o, --org <o>').action(async (n, o) => { await repos.deleteRepo(n, o); });
760
+ r.command('clone <name>').option('-o, --org <o>').option('-d, --dir <d>').action(async (n, o) => { await repos.cloneRepo(n, o); });
761
+ r.command('info <name>').option('-o, --org <o>').action(async (n, o) => { await repos.repoInfo(n, o); });
762
+ r.command('topics <name> <topics...>').option('-o, --org <o>').action(async (n, t, o) => { await repos.setTopics(n, t, o); });
607
763
 
608
764
  // Contributors
609
765
  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) }));
766
+ c.command('score <user>').action(async (u) => { await contributors.scoreUser(u); });
767
+ c.command('rank <issue>').action(async (n) => { await contributors.rankApplicants(parseInt(n)); });
768
+ c.command('list').option('-n, --limit <n>', '', '50').action(async (o) => { await contributors.listContributors({ limit: parseInt(o.limit) }); });
613
769
 
614
770
  // Releases
615
771
  const rel = program.command('release').description('🚀 Manage releases');
616
772
  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) }));
773
+ .action(async (t, o) => { await releases.createRelease(t, o); });
774
+ rel.command('list').option('-n, --limit <n>', '', '50').action(async (o) => { await releases.listReleases({ limit: parseInt(o.limit) }); });
775
+
776
+ // Contributor Commands
777
+ // ILLUSTRATION: How to start?
778
+ // Option A: `gitpadi start https://github.com/owner/repo`
779
+ // Option B: Interactive Menu -> Contributor Mode -> Start Contribution
780
+ program.command('start <url>').description('🚀 Start contribution (fork, clone & branch)').action(async (u) => { await contribute.forkAndClone(u); });
781
+ program.command('sync').description('🔄 Sync with upstream').action(async () => { await contribute.syncBranch(); });
782
+ program.command('submit').description('🚀 Submit PR (add, commit, push & PR)')
783
+ .option('-t, --title <t>', 'PR title')
784
+ .option('-m, --message <m>', 'Commit message')
785
+ .option('-i, --issue <n>', 'Issue number')
786
+ .action(async (o) => {
787
+ await contribute.submitPR({
788
+ title: o.title || 'Automated PR',
789
+ message: o.message || o.title,
790
+ issue: o.issue ? parseInt(o.issue) : undefined
791
+ });
792
+ });
793
+ program.command('logs').description('📋 View Action logs').action(async () => { await contribute.viewLogs(); });
619
794
 
620
795
  return program;
621
796
  }
@@ -624,7 +799,7 @@ function setupCommander(): Command {
624
799
  async function main() {
625
800
  if (process.argv.length <= 2) {
626
801
  await bootSequence();
627
- await onboarding();
802
+ await ensureAuthenticated();
628
803
  await mainMenu();
629
804
  } else {
630
805
  const program = setupCommander();