gitpadi 2.0.1 → 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
@@ -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,13 @@ 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):') },
395
+ { type: 'input', name: 'file', message: yellow('📁 Path to issues file (JSON or MD)') + dim(' (q=back):') },
327
396
  { type: 'confirm', name: 'dryRun', message: 'Dry run first?', default: true },
328
397
  { type: 'number', name: 'start', message: dim('Start index:'), default: 1 },
329
398
  { type: 'number', name: 'end', message: dim('End index:'), default: 999 },
330
399
  ]);
331
400
  await issues.createIssuesFromFile(a.file, { dryRun: a.dryRun, start: a.start, end: a.end });
332
401
  } 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
402
  const spinner = createSpinner(dim('Finding issues with applicants...')).start();
340
403
  const octokit = getOctokit();
341
404
 
@@ -343,10 +406,7 @@ async function issueMenu() {
343
406
  owner: getOwner(), repo: getRepo(), state: 'open', per_page: 50,
344
407
  });
345
408
 
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
409
+ const realIssues = allIssues.filter((i: any) => !i.pull_request && i.comments > 0);
350
410
  const issuesWithApplicants: Array<{ number: number; title: string; applicants: string[]; labels: string[] }> = [];
351
411
 
352
412
  for (const issue of realIssues) {
@@ -355,7 +415,7 @@ async function issueMenu() {
355
415
  });
356
416
 
357
417
  const applicantUsers = new Set<string>();
358
- comments.forEach((c) => {
418
+ comments.forEach((c: any) => {
359
419
  if (c.user?.login && c.user.login !== 'github-actions[bot]') {
360
420
  applicantUsers.add(c.user.login);
361
421
  }
@@ -366,7 +426,7 @@ async function issueMenu() {
366
426
  number: issue.number,
367
427
  title: issue.title.substring(0, 45),
368
428
  applicants: Array.from(applicantUsers),
369
- 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),
370
430
  });
371
431
  }
372
432
  }
@@ -380,7 +440,6 @@ async function issueMenu() {
380
440
 
381
441
  console.log(`\n${bold(`🏆 Issues with Applicants`)} (${issuesWithApplicants.length})\n`);
382
442
 
383
- // Let user pick an issue
384
443
  const { picked } = await inquirer.prompt([{
385
444
  type: 'list', name: 'picked', message: yellow('Select an issue:'),
386
445
  choices: [
@@ -393,8 +452,6 @@ async function issueMenu() {
393
452
  }]);
394
453
 
395
454
  if (picked === -1) return;
396
-
397
- // Run the scoring
398
455
  await issues.assignBest(picked);
399
456
 
400
457
  } else if (action === 'close' || action === 'reopen' || action === 'delete') {
@@ -429,7 +486,7 @@ async function prMenu() {
429
486
  const { action } = await inquirer.prompt([{
430
487
  type: 'list', name: 'action', message: cyan('🔀 PR Operation:'),
431
488
  choices: [
432
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
489
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
433
490
  new inquirer.Separator(dim(' ─────────────────────────────')),
434
491
  { name: ` ${green('▸')} List pull requests`, value: 'list' },
435
492
  { name: ` ${green('▸')} Merge PR ${dim('(checks CI first)')}`, value: 'merge' },
@@ -476,7 +533,7 @@ async function repoMenu() {
476
533
  const { action } = await inquirer.prompt([{
477
534
  type: 'list', name: 'action', message: green('📦 Repo Operation:'),
478
535
  choices: [
479
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
536
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
480
537
  new inquirer.Separator(dim(' ─────────────────────────────')),
481
538
  { name: ` ${green('▸')} List repositories`, value: 'list' },
482
539
  { name: ` ${green('▸')} Create repo`, value: 'create' },
@@ -504,26 +561,88 @@ async function repoMenu() {
504
561
  ]);
505
562
  await repos.createRepo(a.name, { org: a.org || undefined, description: a.description, private: a.isPrivate });
506
563
  } 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() },
564
+ const { org } = await ask([
565
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
510
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
+
511
588
  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' },
589
+ { type: 'input', name: 'confirm', message: red(`⚠️ Type "${repoToDelete}" to confirm deletion:`), validate: (v: string) => v === repoToDelete || 'Name doesn\'t match' },
513
590
  ]);
514
- if (confirm === repoName.name) await repos.deleteRepo(repoName.name, { org: repoName.org });
591
+ if (confirm === repoToDelete) await repos.deleteRepo(repoToDelete, { org });
515
592
  } 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() },
593
+ const { org } = await ask([
594
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
519
595
  ]);
520
- 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 });
521
621
  } 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() },
622
+ const { org } = await ask([
623
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
525
624
  ]);
526
- 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 });
527
646
  } else if (action === 'topics') {
528
647
  const a = await ask([
529
648
  { type: 'input', name: 'name', message: cyan('Repo name:'), default: getRepo() },
@@ -533,12 +652,12 @@ async function repoMenu() {
533
652
  }
534
653
  }
535
654
 
536
- // ── Contributor Menu ───────────────────────────────────────────────────
537
- async function contributorMenu() {
655
+ // ── Contributor Scoring Menu (Maintainer Tool) ─────────────────────────
656
+ async function contributorScoringMenu() {
538
657
  const { action } = await inquirer.prompt([{
539
658
  type: 'list', name: 'action', message: yellow('🏆 Contributor Operation:'),
540
659
  choices: [
541
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
660
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
542
661
  new inquirer.Separator(dim(' ─────────────────────────────')),
543
662
  { name: ` ${yellow('▸')} Score a user`, value: 'score' },
544
663
  { name: ` ${yellow('▸')} Rank applicants for issue`, value: 'rank' },
@@ -554,7 +673,7 @@ async function contributorMenu() {
554
673
  } else if (action === 'rank') {
555
674
  const { n } = await ask([{ type: 'input', name: 'n', message: yellow('Issue #') + dim(' (q=back):') }]);
556
675
  await contributors.rankApplicants(parseInt(n));
557
- } else {
676
+ } else if (action === 'list') {
558
677
  const a = await ask([{ type: 'input', name: 'limit', message: dim('Max results (q=back):'), default: '50' }]);
559
678
  await contributors.listContributors({ limit: parseInt(a.limit) });
560
679
  }
@@ -565,26 +684,27 @@ async function releaseMenu() {
565
684
  const { action } = await inquirer.prompt([{
566
685
  type: 'list', name: 'action', message: red('🚀 Release Operation:'),
567
686
  choices: [
568
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
687
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
569
688
  new inquirer.Separator(dim(' ─────────────────────────────')),
570
- { name: ` ${red('▸')} Create release`, value: 'create' },
571
- { name: ` ${cyan('▸')} List releases`, value: 'list' },
689
+ { name: ` ${green('▸')} List releases`, value: 'list' },
690
+ { name: ` ${green('▸')} Create release`, value: 'create' },
572
691
  ],
573
692
  }]);
574
693
 
575
694
  if (action === 'back') return;
576
695
 
577
- 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') {
578
700
  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: '' },
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: '' },
581
704
  { type: 'confirm', name: 'draft', message: 'Draft?', default: false },
582
- { type: 'confirm', name: 'prerelease', message: 'Pre-release?', default: false },
705
+ { type: 'confirm', name: 'prerelease', message: 'Prerelease?', default: false },
583
706
  ]);
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) });
707
+ await releases.createRelease(a.tag, { name: a.name, body: a.body, draft: a.draft, prerelease: a.prerelease });
588
708
  }
589
709
  }
590
710
 
@@ -606,52 +726,71 @@ function setupCommander(): Command {
606
726
  // Issues
607
727
  const i = program.command('issues').description('📋 Manage issues');
608
728
  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) }));
729
+ .action(async (o) => { await issues.listIssues({ state: o.state, labels: o.labels, limit: parseInt(o.limit) }); });
610
730
  i.command('create').requiredOption('-t, --title <t>').option('-b, --body <b>').option('-l, --labels <l>')
611
- .action((o) => issues.createIssue(o));
731
+ .action(async (o) => { await issues.createIssue(o); });
612
732
  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));
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); });
621
741
 
622
742
  // PRs
623
743
  const p = program.command('prs').description('🔀 Manage pull requests');
624
744
  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) }));
745
+ .action(async (o) => { await prs.listPRs({ state: o.state, limit: parseInt(o.limit) }); });
626
746
  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)));
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)); });
632
752
 
633
753
  // Repos
634
754
  const r = program.command('repo').description('📦 Manage repositories');
635
755
  r.command('list').option('-o, --org <o>').option('-n, --limit <n>', '', '50')
636
- .action((o) => repos.listRepos({ org: o.org, limit: parseInt(o.limit) }));
756
+ .action(async (o) => { await repos.listRepos({ org: o.org, limit: parseInt(o.limit) }); });
637
757
  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));
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); });
643
763
 
644
764
  // Contributors
645
765
  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) }));
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) }); });
649
769
 
650
770
  // Releases
651
771
  const rel = program.command('release').description('🚀 Manage releases');
652
772
  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) }));
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(); });
655
794
 
656
795
  return program;
657
796
  }
@@ -660,7 +799,7 @@ function setupCommander(): Command {
660
799
  async function main() {
661
800
  if (process.argv.length <= 2) {
662
801
  await bootSequence();
663
- await onboarding();
802
+ await ensureAuthenticated();
664
803
  await mainMenu();
665
804
  } else {
666
805
  const program = setupCommander();