gitpadi 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts CHANGED
@@ -9,6 +9,9 @@ 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
17
  import { Octokit } from '@octokit/rest';
@@ -19,6 +22,7 @@ import * as prs from './commands/prs.js';
19
22
  import * as repos from './commands/repos.js';
20
23
  import * as contributors from './commands/contributors.js';
21
24
  import * as releases from './commands/releases.js';
25
+ import * as contribute from './commands/contribute.js';
22
26
 
23
27
  const VERSION = '2.0.0';
24
28
 
@@ -124,62 +128,73 @@ async function bootSequence() {
124
128
  }
125
129
 
126
130
  // ── Onboarding ─────────────────────────────────────────────────────────
127
- async function onboarding() {
128
- // Force a fresh load from config/env
131
+ /**
132
+ * Ensures we have a valid GitHub token
133
+ */
134
+ async function ensureAuthenticated() {
129
135
  initGitHub();
130
-
131
136
  let token = getToken();
132
- let owner = getOwner();
133
- let repo = getRepo();
134
137
 
135
- // If everything is already set (likely via env or previously saved config), skip
136
- if (token && owner && repo) {
137
- const spinner = createSpinner(dim('Authenticating...')).start();
138
+ if (token) {
138
139
  try {
139
140
  const octokit = getOctokit();
140
141
  await octokit.users.getAuthenticated();
141
- spinner.success({ text: green(`Authenticated & targeting ${cyan(`${owner}/${repo}`)}`) });
142
- console.log('');
142
+ // Silent success if token is valid
143
143
  return;
144
144
  } catch {
145
- spinner.error({ text: red('Saved session invalid. Re-authenticating...') });
146
- // Clear and continue to prompt
145
+ console.log(red('Saved session invalid. Re-authenticating...'));
147
146
  token = '';
148
147
  }
149
148
  }
150
149
 
151
- console.log(neon(' ⚡ First-time setup — let\'s connect you to GitHub.\n'));
150
+ console.log(neon(' ⚡ Authentication — let\'s connect you to GitHub.\n'));
152
151
 
153
- const answers = await inquirer.prompt([
154
- {
155
- type: 'password',
156
- name: 'token',
157
- message: magenta('🔑 GitHub Token') + dim(' (ghp_xxx):'),
158
- mask: '•',
159
- validate: async (v: string) => {
160
- if (!v.startsWith('ghp_') && !v.startsWith('github_pat_')) {
161
- return 'Token should start with ghp_ or github_pat_';
162
- }
163
- const spinner = createSpinner(dim('Validating token...')).start();
164
- try {
165
- const tempOctokit = new Octokit({ auth: v });
166
- await tempOctokit.users.getAuthenticated();
167
- spinner.success({ text: green('Token valid!') });
168
- return true;
169
- } catch {
170
- spinner.error({ text: red('Invalid token - GitHub rejected it.') });
171
- return 'Invalid token';
172
- }
173
- },
174
- when: !token,
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
+ }
175
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) {
186
+ return;
187
+ }
188
+
189
+ console.log(neon('\n 📦 Project Targeting — which repo are we working on?\n'));
190
+
191
+ const answers = await inquirer.prompt([
176
192
  {
177
193
  type: 'input',
178
194
  name: 'owner',
179
195
  message: cyan('👤 GitHub Owner/Org:'),
180
196
  default: owner || '',
181
197
  validate: (v: string) => v.length > 0 || 'Required',
182
- when: !owner,
183
198
  },
184
199
  {
185
200
  type: 'input',
@@ -187,79 +202,133 @@ async function onboarding() {
187
202
  message: cyan('📦 Repository name:'),
188
203
  default: repo || '',
189
204
  validate: (v: string) => v.length > 0 || 'Required',
190
- when: !repo,
191
205
  },
192
206
  ]);
193
207
 
194
- const finalToken = answers.token || token;
195
- const finalOwner = answers.owner || owner;
196
- const finalRepo = answers.repo || repo;
197
-
198
- initGitHub(finalToken, finalOwner, finalRepo);
199
- saveConfig({ token: finalToken, owner: finalOwner, repo: finalRepo });
208
+ setRepo(answers.owner, answers.repo);
209
+ saveConfig({ token: getToken(), owner: answers.owner, repo: answers.repo });
200
210
 
201
- const spinner = createSpinner(dim('Connecting to GitHub...')).start();
211
+ const spinner = createSpinner(dim('Connecting...')).start();
202
212
  await sleep(400);
203
- spinner.success({ text: green(`Locked in → ${cyan(`${finalOwner}/${finalRepo}`)}`) });
204
-
205
- console.log('');
206
- console.log(boxen(
207
- dim('💡 Session saved to ~/.gitpadi/config.json\n\n') +
208
- dim('Set environment variables to override:\n') +
209
- yellow(' export GITHUB_TOKEN=ghp_xxx\n') +
210
- yellow(' export GITHUB_OWNER=' + finalOwner + '\n') +
211
- yellow(' export GITHUB_REPO=' + finalRepo),
212
- { padding: 1, borderColor: 'yellow', dimBorder: true, borderStyle: 'round' }
213
- ));
213
+ spinner.success({ text: green(`Locked in → ${cyan(`${answers.owner}/${answers.repo}`)}`) });
214
214
  console.log('');
215
215
  }
216
216
 
217
- // ── Main Menu ──────────────────────────────────────────────────────────
217
+ // ── Mode Selector ──────────────────────────────────────────────────────
218
218
  async function mainMenu() {
219
219
  while (true) {
220
220
  line('═');
221
- console.log(cyber(' ⟨ GITPADI COMMAND CENTER ⟩'));
222
- 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'));
223
223
  line('═');
224
224
  console.log('');
225
225
 
226
- let category: string;
227
- try {
228
- const ans = await inquirer.prompt([{
229
- type: 'list',
230
- name: 'category',
231
- message: bold('Select operation:'),
232
- choices: [
233
- { name: `${magenta('📋')} ${bold('Issues')} ${dim('— create, close, delete, assign, search')}`, value: 'issues' },
234
- { name: `${cyan('🔀')} ${bold('Pull Requests')} ${dim('— merge, review, approve, diff')}`, value: 'prs' },
235
- { name: `${green('📦')} ${bold('Repositories')} ${dim('— create, delete, clone, info')}`, value: 'repos' },
236
- { name: `${yellow('🏆')} ${bold('Contributors')} ${dim('— score, rank, auto-assign best')}`, value: 'contributors' },
237
- { name: `${red('🚀')} ${bold('Releases')} ${dim('— create, list, manage')}`, value: 'releases' },
238
- new inquirer.Separator(dim(' ─────────────────────────────')),
239
- { name: `${dim('⚙️')} ${dim('Switch Repo')}`, value: 'switch' },
240
- { name: `${dim('👋')} ${dim('Exit')}`, value: 'exit' },
241
- ],
242
- loop: false,
243
- }]);
244
- category = ans.category;
245
- } catch {
246
- // Ctrl+C on main menu = exit
247
- console.log('');
248
- console.log(dim(' ▸ Saving session...'));
249
- console.log(dim(' ▸ Disconnecting from GitHub...'));
250
- console.log('');
251
- console.log(fire(' 🤖 GitPadi is shutting down now. See you soon, pal :)\n'));
252
- 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();
253
259
  }
254
260
 
255
- if (category === 'exit') {
256
- console.log('');
257
- console.log(dim(' ▸ Saving session...'));
258
- console.log(dim(' Disconnecting from GitHub...'));
259
- console.log('');
260
- console.log(fire(' 🤖 GitPadi is shutting down now. See you soon, pal :)\n'));
261
- 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
+ }
262
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;
263
332
 
264
333
  if (category === 'switch') {
265
334
  await safeMenu(async () => {
@@ -276,10 +345,10 @@ async function mainMenu() {
276
345
  }
277
346
 
278
347
  if (category === 'issues') await safeMenu(issueMenu);
279
- if (category === 'prs') await safeMenu(prMenu);
280
- if (category === 'repos') await safeMenu(repoMenu);
281
- if (category === 'contributors') await safeMenu(contributorMenu);
282
- 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);
283
352
 
284
353
  console.log('');
285
354
  }
@@ -290,11 +359,11 @@ async function issueMenu() {
290
359
  const { action } = await inquirer.prompt([{
291
360
  type: 'list', name: 'action', message: magenta('📋 Issue Operation:'),
292
361
  choices: [
293
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
362
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
294
363
  new inquirer.Separator(dim(' ─────────────────────────────')),
295
364
  { name: ` ${green('▸')} List open issues`, value: 'list' },
296
365
  { name: ` ${green('▸')} Create single issue`, value: 'create' },
297
- { name: ` ${magenta('▸')} Bulk create from JSON file`, value: 'bulk' },
366
+ { name: ` ${magenta('▸')} Bulk create from file (JSON/MD)`, value: 'bulk' },
298
367
  { name: ` ${red('▸')} Close issue`, value: 'close' },
299
368
  { name: ` ${green('▸')} Reopen issue`, value: 'reopen' },
300
369
  { name: ` ${red('▸')} Delete (close & lock)`, value: 'delete' },
@@ -323,19 +392,12 @@ async function issueMenu() {
323
392
  await issues.createIssue(a);
324
393
  } else if (action === 'bulk') {
325
394
  const a = await ask([
326
- { type: 'input', name: 'file', message: yellow('📁 Path to issues JSON file') + dim(' (q=back):') },
327
- { type: 'confirm', name: 'dryRun', message: 'Dry run first?', default: true },
395
+ { type: 'input', name: 'file', message: yellow('📁 Path to issues file (JSON or MD)') + dim(' (q=back):') },
328
396
  { type: 'number', name: 'start', message: dim('Start index:'), default: 1 },
329
397
  { type: 'number', name: 'end', message: dim('End index:'), default: 999 },
330
398
  ]);
331
- await issues.createIssuesFromFile(a.file, { dryRun: a.dryRun, start: a.start, end: a.end });
399
+ await issues.createIssuesFromFile(a.file, { start: a.start, end: a.end });
332
400
  } else if (action === 'assign-best') {
333
- // ── Smart Auto-Assign Flow ──
334
- // 1. Fetch all open issues with comments
335
- // 2. Filter to ones with applicant comments
336
- // 3. Let user pick from a list
337
- // 4. Show applicants + scores, then assign best
338
-
339
401
  const spinner = createSpinner(dim('Finding issues with applicants...')).start();
340
402
  const octokit = getOctokit();
341
403
 
@@ -343,10 +405,7 @@ async function issueMenu() {
343
405
  owner: getOwner(), repo: getRepo(), state: 'open', per_page: 50,
344
406
  });
345
407
 
346
- // Filter to real issues (not PRs) with comments
347
- const realIssues = allIssues.filter((i) => !i.pull_request && i.comments > 0);
348
-
349
- // Check each issue for applicant comments
408
+ const realIssues = allIssues.filter((i: any) => !i.pull_request && i.comments > 0);
350
409
  const issuesWithApplicants: Array<{ number: number; title: string; applicants: string[]; labels: string[] }> = [];
351
410
 
352
411
  for (const issue of realIssues) {
@@ -355,7 +414,7 @@ async function issueMenu() {
355
414
  });
356
415
 
357
416
  const applicantUsers = new Set<string>();
358
- comments.forEach((c) => {
417
+ comments.forEach((c: any) => {
359
418
  if (c.user?.login && c.user.login !== 'github-actions[bot]') {
360
419
  applicantUsers.add(c.user.login);
361
420
  }
@@ -366,7 +425,7 @@ async function issueMenu() {
366
425
  number: issue.number,
367
426
  title: issue.title.substring(0, 45),
368
427
  applicants: Array.from(applicantUsers),
369
- labels: issue.labels.map((l) => typeof l === 'string' ? l : l.name || '').filter(Boolean),
428
+ labels: issue.labels.map((l: any) => typeof l === 'string' ? l : l.name || '').filter(Boolean),
370
429
  });
371
430
  }
372
431
  }
@@ -380,7 +439,6 @@ async function issueMenu() {
380
439
 
381
440
  console.log(`\n${bold(`🏆 Issues with Applicants`)} (${issuesWithApplicants.length})\n`);
382
441
 
383
- // Let user pick an issue
384
442
  const { picked } = await inquirer.prompt([{
385
443
  type: 'list', name: 'picked', message: yellow('Select an issue:'),
386
444
  choices: [
@@ -393,8 +451,6 @@ async function issueMenu() {
393
451
  }]);
394
452
 
395
453
  if (picked === -1) return;
396
-
397
- // Run the scoring
398
454
  await issues.assignBest(picked);
399
455
 
400
456
  } else if (action === 'close' || action === 'reopen' || action === 'delete') {
@@ -429,7 +485,7 @@ async function prMenu() {
429
485
  const { action } = await inquirer.prompt([{
430
486
  type: 'list', name: 'action', message: cyan('🔀 PR Operation:'),
431
487
  choices: [
432
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
488
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
433
489
  new inquirer.Separator(dim(' ─────────────────────────────')),
434
490
  { name: ` ${green('▸')} List pull requests`, value: 'list' },
435
491
  { name: ` ${green('▸')} Merge PR ${dim('(checks CI first)')}`, value: 'merge' },
@@ -476,7 +532,7 @@ async function repoMenu() {
476
532
  const { action } = await inquirer.prompt([{
477
533
  type: 'list', name: 'action', message: green('📦 Repo Operation:'),
478
534
  choices: [
479
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
535
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
480
536
  new inquirer.Separator(dim(' ─────────────────────────────')),
481
537
  { name: ` ${green('▸')} List repositories`, value: 'list' },
482
538
  { name: ` ${green('▸')} Create repo`, value: 'create' },
@@ -504,26 +560,88 @@ async function repoMenu() {
504
560
  ]);
505
561
  await repos.createRepo(a.name, { org: a.org || undefined, description: a.description, private: a.isPrivate });
506
562
  } else if (action === 'delete') {
507
- const repoName = await ask([
508
- { type: 'input', name: 'name', message: red('📦 Repo to DELETE') + dim(' (q=back):') },
509
- { type: 'input', name: 'org', message: 'Org:', default: getOwner() },
563
+ const { org } = await ask([
564
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
510
565
  ]);
566
+
567
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
568
+ let repoToDelete: string;
569
+
570
+ if (fetchedRepos.length > 0) {
571
+ const { selection } = await inquirer.prompt([{
572
+ type: 'list',
573
+ name: 'selection',
574
+ message: red('📦 Select Repo to DELETE:'),
575
+ choices: [
576
+ { name: dim('⬅ Back'), value: 'back' },
577
+ ...fetchedRepos.map((r: any) => ({ name: r.name, value: r.name }))
578
+ ]
579
+ }]);
580
+ if (selection === 'back') return;
581
+ repoToDelete = selection;
582
+ } else {
583
+ const { name } = await ask([{ type: 'input', name: 'name', message: red('📦 Repo name to DELETE:') }]);
584
+ repoToDelete = name;
585
+ }
586
+
511
587
  const { confirm } = await ask([
512
- { type: 'input', name: 'confirm', message: red(`⚠️ Type "${repoName.name}" to confirm deletion:`), validate: (v: string) => v === repoName.name || 'Name doesn\'t match' },
588
+ { type: 'input', name: 'confirm', message: red(`⚠️ Type "${repoToDelete}" to confirm deletion:`), validate: (v: string) => v === repoToDelete || 'Name doesn\'t match' },
513
589
  ]);
514
- if (confirm === repoName.name) await repos.deleteRepo(repoName.name, { org: repoName.org });
590
+ if (confirm === repoToDelete) await repos.deleteRepo(repoToDelete, { org });
515
591
  } else if (action === 'clone') {
516
- const a = await ask([
517
- { type: 'input', name: 'name', message: cyan('Repo name') + dim(' (q=back):') },
518
- { type: 'input', name: 'org', message: dim('Org:'), default: getOwner() },
592
+ const { org } = await ask([
593
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
519
594
  ]);
520
- await repos.cloneRepo(a.name, { org: a.org });
595
+
596
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
597
+ let repoToClone: string;
598
+
599
+ if (fetchedRepos.length > 0) {
600
+ const { selection } = await inquirer.prompt([{
601
+ type: 'list',
602
+ name: 'selection',
603
+ message: cyan('📦 Select Repo to Clone:'),
604
+ choices: [
605
+ { name: dim('⬅ Back'), value: 'back' },
606
+ ...fetchedRepos.map((r: any) => ({ name: r.name, value: r.name }))
607
+ ]
608
+ }]);
609
+ if (selection === 'back') return;
610
+ repoToClone = selection;
611
+ } else {
612
+ const { name } = await ask([{ type: 'input', name: 'name', message: cyan('📦 Repo name to Clone:') }]);
613
+ repoToClone = name;
614
+ }
615
+
616
+ const { dir } = await ask([
617
+ { type: 'input', name: 'dir', message: dim('Destination path (leave blank for default):'), default: '' },
618
+ ]);
619
+ await repos.cloneRepo(repoToClone, { org, dir: dir || undefined });
521
620
  } else if (action === 'info') {
522
- const a = await ask([
523
- { type: 'input', name: 'name', message: cyan('Repo name:'), default: getRepo() },
524
- { type: 'input', name: 'org', message: dim('Org:'), default: getOwner() },
621
+ const { org } = await ask([
622
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
525
623
  ]);
526
- await repos.repoInfo(a.name, { org: a.org });
624
+
625
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
626
+ let repoToInfo: string;
627
+
628
+ if (fetchedRepos.length > 0) {
629
+ const { selection } = await inquirer.prompt([{
630
+ type: 'list',
631
+ name: 'selection',
632
+ message: cyan('📦 Select Repo for Info:'),
633
+ choices: [
634
+ { name: dim('⬅ Back'), value: 'back' },
635
+ ...fetchedRepos.map((r: any) => ({ name: r.name, value: r.name }))
636
+ ]
637
+ }]);
638
+ if (selection === 'back') return;
639
+ repoToInfo = selection;
640
+ } else {
641
+ const { name } = await ask([{ type: 'input', name: 'name', message: cyan('📦 Repo name:') }]);
642
+ repoToInfo = name;
643
+ }
644
+ await repos.repoInfo(repoToInfo, { org });
527
645
  } else if (action === 'topics') {
528
646
  const a = await ask([
529
647
  { type: 'input', name: 'name', message: cyan('Repo name:'), default: getRepo() },
@@ -533,12 +651,12 @@ async function repoMenu() {
533
651
  }
534
652
  }
535
653
 
536
- // ── Contributor Menu ───────────────────────────────────────────────────
537
- async function contributorMenu() {
654
+ // ── Contributor Scoring Menu (Maintainer Tool) ─────────────────────────
655
+ async function contributorScoringMenu() {
538
656
  const { action } = await inquirer.prompt([{
539
657
  type: 'list', name: 'action', message: yellow('🏆 Contributor Operation:'),
540
658
  choices: [
541
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
659
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
542
660
  new inquirer.Separator(dim(' ─────────────────────────────')),
543
661
  { name: ` ${yellow('▸')} Score a user`, value: 'score' },
544
662
  { name: ` ${yellow('▸')} Rank applicants for issue`, value: 'rank' },
@@ -554,7 +672,7 @@ async function contributorMenu() {
554
672
  } else if (action === 'rank') {
555
673
  const { n } = await ask([{ type: 'input', name: 'n', message: yellow('Issue #') + dim(' (q=back):') }]);
556
674
  await contributors.rankApplicants(parseInt(n));
557
- } else {
675
+ } else if (action === 'list') {
558
676
  const a = await ask([{ type: 'input', name: 'limit', message: dim('Max results (q=back):'), default: '50' }]);
559
677
  await contributors.listContributors({ limit: parseInt(a.limit) });
560
678
  }
@@ -565,26 +683,27 @@ async function releaseMenu() {
565
683
  const { action } = await inquirer.prompt([{
566
684
  type: 'list', name: 'action', message: red('🚀 Release Operation:'),
567
685
  choices: [
568
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
686
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
569
687
  new inquirer.Separator(dim(' ─────────────────────────────')),
570
- { name: ` ${red('▸')} Create release`, value: 'create' },
571
- { name: ` ${cyan('▸')} List releases`, value: 'list' },
688
+ { name: ` ${green('▸')} List releases`, value: 'list' },
689
+ { name: ` ${green('▸')} Create release`, value: 'create' },
572
690
  ],
573
691
  }]);
574
692
 
575
693
  if (action === 'back') return;
576
694
 
577
- if (action === 'create') {
695
+ if (action === 'list') {
696
+ const a = await ask([{ type: 'input', name: 'limit', message: dim('Max results:'), default: '50' }]);
697
+ await releases.listReleases({ limit: parseInt(a.limit) });
698
+ } else if (action === 'create') {
578
699
  const a = await ask([
579
- { type: 'input', name: 'tag', message: yellow('Tag (e.g., v1.0.0)') + dim(' (q=back):') },
580
- { type: 'input', name: 'name', message: dim('Release name:'), default: '' },
700
+ { type: 'input', name: 'tag', message: yellow('Tag (e.g. v1.0.0):') },
701
+ { type: 'input', name: 'name', message: dim('Release name (optional):'), default: '' },
702
+ { type: 'input', name: 'body', message: dim('Release body (optional):'), default: '' },
581
703
  { type: 'confirm', name: 'draft', message: 'Draft?', default: false },
582
- { type: 'confirm', name: 'prerelease', message: 'Pre-release?', default: false },
704
+ { type: 'confirm', name: 'prerelease', message: 'Prerelease?', default: false },
583
705
  ]);
584
- await releases.createRelease(a.tag, { name: a.name || a.tag, draft: a.draft, prerelease: a.prerelease });
585
- } else {
586
- const a = await ask([{ type: 'input', name: 'limit', message: dim('Max results (q=back):'), default: '50' }]);
587
- await releases.listReleases({ limit: parseInt(a.limit) });
706
+ await releases.createRelease(a.tag, { name: a.name, body: a.body, draft: a.draft, prerelease: a.prerelease });
588
707
  }
589
708
  }
590
709
 
@@ -606,52 +725,71 @@ function setupCommander(): Command {
606
725
  // Issues
607
726
  const i = program.command('issues').description('📋 Manage issues');
608
727
  i.command('list').option('-s, --state <s>', '', 'open').option('-l, --labels <l>').option('-n, --limit <n>', '', '50')
609
- .action((o) => issues.listIssues({ state: o.state, labels: o.labels, limit: parseInt(o.limit) }));
728
+ .action(async (o) => { await issues.listIssues({ state: o.state, labels: o.labels, limit: parseInt(o.limit) }); });
610
729
  i.command('create').requiredOption('-t, --title <t>').option('-b, --body <b>').option('-l, --labels <l>')
611
- .action((o) => issues.createIssue(o));
730
+ .action(async (o) => { await issues.createIssue(o); });
612
731
  i.command('bulk').requiredOption('-f, --file <f>').option('-d, --dry-run').option('--start <n>', '', '1').option('--end <n>', '', '999')
613
- .action((o) => issues.createIssuesFromFile(o.file, { dryRun: o.dryRun, start: parseInt(o.start), end: parseInt(o.end) }));
614
- i.command('close <n>').action((n) => issues.closeIssue(parseInt(n)));
615
- i.command('reopen <n>').action((n) => issues.reopenIssue(parseInt(n)));
616
- i.command('delete <n>').action((n) => issues.deleteIssue(parseInt(n)));
617
- i.command('assign <n> <users...>').action((n, u) => issues.assignIssue(parseInt(n), u));
618
- i.command('assign-best <n>').action((n) => issues.assignBest(parseInt(n)));
619
- i.command('search <q>').action((q) => issues.searchIssues(q));
620
- i.command('label <n> <labels...>').action((n, l) => issues.labelIssue(parseInt(n), l));
732
+ .action(async (o) => { await issues.createIssuesFromFile(o.file, { dryRun: o.dryRun, start: parseInt(o.start), end: parseInt(o.end) }); });
733
+ i.command('close <n>').action(async (n) => { await issues.closeIssue(parseInt(n)); });
734
+ i.command('reopen <n>').action(async (n) => { await issues.reopenIssue(parseInt(n)); });
735
+ i.command('delete <n>').action(async (n) => { await issues.deleteIssue(parseInt(n)); });
736
+ i.command('assign <n> <users...>').action(async (n, u) => { await issues.assignIssue(parseInt(n), u); });
737
+ i.command('assign-best <n>').action(async (n) => { await issues.assignBest(parseInt(n)); });
738
+ i.command('search <q>').action(async (q) => { await issues.searchIssues(q); });
739
+ i.command('label <n> <labels...>').action(async (n, l) => { await issues.labelIssue(parseInt(n), l); });
621
740
 
622
741
  // PRs
623
742
  const p = program.command('prs').description('🔀 Manage pull requests');
624
743
  p.command('list').option('-s, --state <s>', '', 'open').option('-n, --limit <n>', '', '50')
625
- .action((o) => prs.listPRs({ state: o.state, limit: parseInt(o.limit) }));
744
+ .action(async (o) => { await prs.listPRs({ state: o.state, limit: parseInt(o.limit) }); });
626
745
  p.command('merge <n>').option('-m, --method <m>', '', 'squash').option('--message <msg>').option('-f, --force', 'Skip CI checks')
627
- .action((n, o) => prs.mergePR(parseInt(n), o));
628
- p.command('close <n>').action((n) => prs.closePR(parseInt(n)));
629
- p.command('review <n>').action((n) => prs.reviewPR(parseInt(n)));
630
- p.command('approve <n>').action((n) => prs.approvePR(parseInt(n)));
631
- p.command('diff <n>').action((n) => prs.diffPR(parseInt(n)));
746
+ .action(async (n, o) => { await prs.mergePR(parseInt(n), o); });
747
+ p.command('close <n>').action(async (n) => { await prs.closePR(parseInt(n)); });
748
+ p.command('review <n>').action(async (n) => { await prs.reviewPR(parseInt(n)); });
749
+ p.command('approve <n>').action(async (n) => { await prs.approvePR(parseInt(n)); });
750
+ p.command('diff <n>').action(async (n) => { await prs.diffPR(parseInt(n)); });
632
751
 
633
752
  // Repos
634
753
  const r = program.command('repo').description('📦 Manage repositories');
635
754
  r.command('list').option('-o, --org <o>').option('-n, --limit <n>', '', '50')
636
- .action((o) => repos.listRepos({ org: o.org, limit: parseInt(o.limit) }));
755
+ .action(async (o) => { await repos.listRepos({ org: o.org, limit: parseInt(o.limit) }); });
637
756
  r.command('create <name>').option('-o, --org <o>').option('-p, --private').option('-d, --description <d>')
638
- .action((n, o) => repos.createRepo(n, o));
639
- r.command('delete <name>').option('-o, --org <o>').action((n, o) => repos.deleteRepo(n, o));
640
- r.command('clone <name>').option('-o, --org <o>').option('-d, --dir <d>').action((n, o) => repos.cloneRepo(n, o));
641
- r.command('info <name>').option('-o, --org <o>').action((n, o) => repos.repoInfo(n, o));
642
- r.command('topics <name> <topics...>').option('-o, --org <o>').action((n, t, o) => repos.setTopics(n, t, o));
757
+ .action(async (n, o) => { await repos.createRepo(n, o); });
758
+ r.command('delete <name>').option('-o, --org <o>').action(async (n, o) => { await repos.deleteRepo(n, o); });
759
+ r.command('clone <name>').option('-o, --org <o>').option('-d, --dir <d>').action(async (n, o) => { await repos.cloneRepo(n, o); });
760
+ r.command('info <name>').option('-o, --org <o>').action(async (n, o) => { await repos.repoInfo(n, o); });
761
+ r.command('topics <name> <topics...>').option('-o, --org <o>').action(async (n, t, o) => { await repos.setTopics(n, t, o); });
643
762
 
644
763
  // Contributors
645
764
  const c = program.command('contributors').description('🏆 Manage contributors');
646
- c.command('score <user>').action((u) => contributors.scoreUser(u));
647
- c.command('rank <issue>').action((n) => contributors.rankApplicants(parseInt(n)));
648
- c.command('list').option('-n, --limit <n>', '', '50').action((o) => contributors.listContributors({ limit: parseInt(o.limit) }));
765
+ c.command('score <user>').action(async (u) => { await contributors.scoreUser(u); });
766
+ c.command('rank <issue>').action(async (n) => { await contributors.rankApplicants(parseInt(n)); });
767
+ c.command('list').option('-n, --limit <n>', '', '50').action(async (o) => { await contributors.listContributors({ limit: parseInt(o.limit) }); });
649
768
 
650
769
  // Releases
651
770
  const rel = program.command('release').description('🚀 Manage releases');
652
771
  rel.command('create <tag>').option('-n, --name <n>').option('-b, --body <b>').option('-d, --draft').option('-p, --prerelease').option('--no-generate')
653
- .action((t, o) => releases.createRelease(t, o));
654
- rel.command('list').option('-n, --limit <n>', '', '50').action((o) => releases.listReleases({ limit: parseInt(o.limit) }));
772
+ .action(async (t, o) => { await releases.createRelease(t, o); });
773
+ rel.command('list').option('-n, --limit <n>', '', '50').action(async (o) => { await releases.listReleases({ limit: parseInt(o.limit) }); });
774
+
775
+ // Contributor Commands
776
+ // ILLUSTRATION: How to start?
777
+ // Option A: `gitpadi start https://github.com/owner/repo`
778
+ // Option B: Interactive Menu -> Contributor Mode -> Start Contribution
779
+ program.command('start <url>').description('🚀 Start contribution (fork, clone & branch)').action(async (u) => { await contribute.forkAndClone(u); });
780
+ program.command('sync').description('🔄 Sync with upstream').action(async () => { await contribute.syncBranch(); });
781
+ program.command('submit').description('🚀 Submit PR (add, commit, push & PR)')
782
+ .option('-t, --title <t>', 'PR title')
783
+ .option('-m, --message <m>', 'Commit message')
784
+ .option('-i, --issue <n>', 'Issue number')
785
+ .action(async (o) => {
786
+ await contribute.submitPR({
787
+ title: o.title || 'Automated PR',
788
+ message: o.message || o.title,
789
+ issue: o.issue ? parseInt(o.issue) : undefined
790
+ });
791
+ });
792
+ program.command('logs').description('📋 View Action logs').action(async () => { await contribute.viewLogs(); });
655
793
 
656
794
  return program;
657
795
  }
@@ -660,7 +798,7 @@ function setupCommander(): Command {
660
798
  async function main() {
661
799
  if (process.argv.length <= 2) {
662
800
  await bootSequence();
663
- await onboarding();
801
+ await ensureAuthenticated();
664
802
  await mainMenu();
665
803
  } else {
666
804
  const program = setupCommander();