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/dist/cli.js CHANGED
@@ -17,6 +17,7 @@ import * as prs from './commands/prs.js';
17
17
  import * as repos from './commands/repos.js';
18
18
  import * as contributors from './commands/contributors.js';
19
19
  import * as releases from './commands/releases.js';
20
+ import * as contribute from './commands/contribute.js';
20
21
  const VERSION = '2.0.0';
21
22
  // ── Styling ────────────────────────────────────────────────────────────
22
23
  const cyber = gradient(['#ff00ff', '#00ffff', '#ff00ff']);
@@ -108,33 +109,28 @@ async function bootSequence() {
108
109
  console.log('');
109
110
  }
110
111
  // ── Onboarding ─────────────────────────────────────────────────────────
111
- async function onboarding() {
112
- // Force a fresh load from config/env
112
+ /**
113
+ * Ensures we have a valid GitHub token
114
+ */
115
+ async function ensureAuthenticated() {
113
116
  initGitHub();
114
117
  let token = getToken();
115
- let owner = getOwner();
116
- let repo = getRepo();
117
- // If everything is already set (likely via env or previously saved config), skip
118
- if (token && owner && repo) {
119
- const spinner = createSpinner(dim('Authenticating...')).start();
118
+ if (token) {
120
119
  try {
121
120
  const octokit = getOctokit();
122
121
  await octokit.users.getAuthenticated();
123
- spinner.success({ text: green(`Authenticated & targeting ${cyan(`${owner}/${repo}`)}`) });
124
- console.log('');
122
+ // Silent success if token is valid
125
123
  return;
126
124
  }
127
125
  catch {
128
- spinner.error({ text: red('Saved session invalid. Re-authenticating...') });
129
- // Clear and continue to prompt
126
+ console.log(red('Saved session invalid. Re-authenticating...'));
130
127
  token = '';
131
128
  }
132
129
  }
133
- console.log(neon(' ⚡ First-time setup — let\'s connect you to GitHub.\n'));
134
- const answers = await inquirer.prompt([
135
- {
130
+ console.log(neon(' ⚡ Authentication — let\'s connect you to GitHub.\n'));
131
+ const { t } = await inquirer.prompt([{
136
132
  type: 'password',
137
- name: 'token',
133
+ name: 't',
138
134
  message: magenta('🔑 GitHub Token') + dim(' (ghp_xxx):'),
139
135
  mask: '•',
140
136
  validate: async (v) => {
@@ -153,15 +149,27 @@ async function onboarding() {
153
149
  return 'Invalid token';
154
150
  }
155
151
  },
156
- when: !token,
157
- },
152
+ }]);
153
+ initGitHub(t);
154
+ saveConfig({ token: t, owner: getOwner() || '', repo: getRepo() || '' });
155
+ }
156
+ /**
157
+ * Ensures we have a target repository (Owner/Repo)
158
+ */
159
+ async function ensureTargetRepo() {
160
+ let owner = getOwner();
161
+ let repo = getRepo();
162
+ if (owner && repo) {
163
+ return;
164
+ }
165
+ console.log(neon('\n 📦 Project Targeting — which repo are we working on?\n'));
166
+ const answers = await inquirer.prompt([
158
167
  {
159
168
  type: 'input',
160
169
  name: 'owner',
161
170
  message: cyan('👤 GitHub Owner/Org:'),
162
171
  default: owner || '',
163
172
  validate: (v) => v.length > 0 || 'Required',
164
- when: !owner,
165
173
  },
166
174
  {
167
175
  type: 'input',
@@ -169,70 +177,126 @@ async function onboarding() {
169
177
  message: cyan('📦 Repository name:'),
170
178
  default: repo || '',
171
179
  validate: (v) => v.length > 0 || 'Required',
172
- when: !repo,
173
180
  },
174
181
  ]);
175
- const finalToken = answers.token || token;
176
- const finalOwner = answers.owner || owner;
177
- const finalRepo = answers.repo || repo;
178
- initGitHub(finalToken, finalOwner, finalRepo);
179
- saveConfig({ token: finalToken, owner: finalOwner, repo: finalRepo });
180
- const spinner = createSpinner(dim('Connecting to GitHub...')).start();
182
+ setRepo(answers.owner, answers.repo);
183
+ saveConfig({ token: getToken(), owner: answers.owner, repo: answers.repo });
184
+ const spinner = createSpinner(dim('Connecting...')).start();
181
185
  await sleep(400);
182
- spinner.success({ text: green(`Locked in → ${cyan(`${finalOwner}/${finalRepo}`)}`) });
183
- console.log('');
184
- console.log(boxen(dim('💡 Session saved to ~/.gitpadi/config.json\n\n') +
185
- dim('Set environment variables to override:\n') +
186
- yellow(' export GITHUB_TOKEN=ghp_xxx\n') +
187
- yellow(' export GITHUB_OWNER=' + finalOwner + '\n') +
188
- yellow(' export GITHUB_REPO=' + finalRepo), { padding: 1, borderColor: 'yellow', dimBorder: true, borderStyle: 'round' }));
186
+ spinner.success({ text: green(`Locked in → ${cyan(`${answers.owner}/${answers.repo}`)}`) });
189
187
  console.log('');
190
188
  }
191
- // ── Main Menu ──────────────────────────────────────────────────────────
189
+ // ── Mode Selector ──────────────────────────────────────────────────────
192
190
  async function mainMenu() {
193
191
  while (true) {
194
192
  line('═');
195
- console.log(cyber(' ⟨ GITPADI COMMAND CENTER ⟩'));
196
- console.log(dim(' Select Back on lists • Type q on text prompts'));
193
+ console.log(cyber(' ⟨ GITPADI MODE SELECTOR ⟩'));
194
+ console.log(dim(' Select your workflow persona to continue'));
197
195
  line('═');
198
196
  console.log('');
199
- let category;
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;
197
+ const { mode } = await inquirer.prompt([{
198
+ type: 'list',
199
+ name: 'mode',
200
+ message: bold('Choose your path:'),
201
+ choices: [
202
+ { name: `${cyan('✨')} ${bold('Contributor Mode')} ${dim('— fork, clone, sync, submit PRs')}`, value: 'contributor' },
203
+ { name: `${magenta('🛠️')} ${bold('Maintainer Mode')} ${dim('— manage issues, PRs, contributors')}`, value: 'maintainer' },
204
+ new inquirer.Separator(dim(' ─────────────────────────────')),
205
+ { name: `${dim('👋')} ${dim('Exit')}`, value: 'exit' },
206
+ ],
207
+ loop: false,
208
+ }]);
209
+ if (mode === 'contributor')
210
+ await safeMenu(contributorMenu);
211
+ else if (mode === 'maintainer') {
212
+ await ensureTargetRepo();
213
+ await safeMenu(maintainerMenu);
218
214
  }
219
- catch {
220
- // Ctrl+C on main menu = exit
221
- console.log('');
222
- console.log(dim(' ▸ Saving session...'));
223
- console.log(dim(' ▸ Disconnecting from GitHub...'));
224
- console.log('');
225
- console.log(fire(' 🤖 GitPadi is shutting down now. See you soon, pal :)\n'));
215
+ else
226
216
  break;
217
+ }
218
+ }
219
+ // ── Contributor Menu ───────────────────────────────────────────────────
220
+ async function contributorMenu() {
221
+ while (true) {
222
+ line('═');
223
+ console.log(cyan(' ✨ GITPADI CONTRIBUTOR WORKSPACE'));
224
+ console.log(dim(' Automating forking, syncing, and PR delivery'));
225
+ line('═');
226
+ // Auto-check for updates if in a repo
227
+ if (getOwner() && getRepo()) {
228
+ await contribute.syncBranch();
227
229
  }
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'));
230
+ const { action } = await inquirer.prompt([{
231
+ type: 'list',
232
+ name: 'action',
233
+ message: bold('Contributor Action:'),
234
+ choices: [
235
+ { name: `${cyan('🚀')} ${bold('Start Contribution')} ${dim('— fork, clone & branch')}`, value: 'start' },
236
+ { name: `${green('🔄')} ${bold('Sync with Upstream')} ${dim('— pull latest changes')}`, value: 'sync' },
237
+ { name: `${yellow('📋')} ${bold('View Action Logs')} ${dim('— check PR/commit status')}`, value: 'logs' },
238
+ { name: `${magenta('🚀')} ${bold('Submit PR')} ${dim('— add, commit, push & PR')}`, value: 'submit' },
239
+ new inquirer.Separator(dim(' ─────────────────────────────')),
240
+ { name: `${dim('⬅')} ${dim('Back to Mode Selector')}`, value: 'back' },
241
+ ],
242
+ loop: false,
243
+ }]);
244
+ if (action === 'back')
234
245
  break;
246
+ if (action === 'start') {
247
+ const { url } = await ask([{ type: 'input', name: 'url', message: 'Enter Repo or Issue URL:' }]);
248
+ await contribute.forkAndClone(url);
249
+ }
250
+ else {
251
+ // These actions require a target repo
252
+ await ensureTargetRepo();
253
+ if (action === 'sync') {
254
+ await contribute.syncBranch();
255
+ }
256
+ else if (action === 'logs') {
257
+ await contribute.viewLogs();
258
+ }
259
+ else if (action === 'submit') {
260
+ const { title, message, issue } = await ask([
261
+ { type: 'input', name: 'title', message: 'PR Title:' },
262
+ { type: 'input', name: 'message', message: 'Commit message (optional):' },
263
+ { type: 'input', name: 'issue', message: 'Related Issue # (optional):' }
264
+ ]);
265
+ await contribute.submitPR({
266
+ title,
267
+ message: message || title,
268
+ issue: issue ? parseInt(issue) : undefined
269
+ });
270
+ }
235
271
  }
272
+ }
273
+ }
274
+ // ── Maintainer Menu ────────────────────────────────────────────────────
275
+ async function maintainerMenu() {
276
+ while (true) {
277
+ line('═');
278
+ console.log(magenta(' 🛠️ GITPADI MAINTAINER PANEL'));
279
+ console.log(dim(' Managing repository health and contributor intake'));
280
+ line('═');
281
+ console.log('');
282
+ const { category } = await inquirer.prompt([{
283
+ type: 'list',
284
+ name: 'category',
285
+ message: bold('Select operation:'),
286
+ choices: [
287
+ { name: `${magenta('📋')} ${bold('Issues')} ${dim('— create, close, delete, assign, search')}`, value: 'issues' },
288
+ { name: `${cyan('🔀')} ${bold('Pull Requests')} ${dim('— merge, review, approve, diff')}`, value: 'prs' },
289
+ { name: `${green('📦')} ${bold('Repositories')} ${dim('— create, delete, clone, info')}`, value: 'repos' },
290
+ { name: `${yellow('🏆')} ${bold('Contributors')} ${dim('— score, rank, auto-assign best')}`, value: 'contributors' },
291
+ { name: `${red('🚀')} ${bold('Releases')} ${dim('— create, list, manage')}`, value: 'releases' },
292
+ new inquirer.Separator(dim(' ─────────────────────────────')),
293
+ { name: `${dim('⚙️')} ${dim('Switch Repo')}`, value: 'switch' },
294
+ { name: `${dim('⬅')} ${dim('Back to Mode Selector')}`, value: 'back' },
295
+ ],
296
+ loop: false,
297
+ }]);
298
+ if (category === 'back')
299
+ break;
236
300
  if (category === 'switch') {
237
301
  await safeMenu(async () => {
238
302
  const a = await inquirer.prompt([
@@ -248,13 +312,13 @@ async function mainMenu() {
248
312
  }
249
313
  if (category === 'issues')
250
314
  await safeMenu(issueMenu);
251
- if (category === 'prs')
315
+ else if (category === 'prs')
252
316
  await safeMenu(prMenu);
253
- if (category === 'repos')
317
+ else if (category === 'repos')
254
318
  await safeMenu(repoMenu);
255
- if (category === 'contributors')
256
- await safeMenu(contributorMenu);
257
- if (category === 'releases')
319
+ else if (category === 'contributors')
320
+ await safeMenu(contributorScoringMenu);
321
+ else if (category === 'releases')
258
322
  await safeMenu(releaseMenu);
259
323
  console.log('');
260
324
  }
@@ -264,11 +328,11 @@ async function issueMenu() {
264
328
  const { action } = await inquirer.prompt([{
265
329
  type: 'list', name: 'action', message: magenta('📋 Issue Operation:'),
266
330
  choices: [
267
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
331
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
268
332
  new inquirer.Separator(dim(' ─────────────────────────────')),
269
333
  { name: ` ${green('▸')} List open issues`, value: 'list' },
270
334
  { name: ` ${green('▸')} Create single issue`, value: 'create' },
271
- { name: ` ${magenta('▸')} Bulk create from JSON file`, value: 'bulk' },
335
+ { name: ` ${magenta('▸')} Bulk create from file (JSON/MD)`, value: 'bulk' },
272
336
  { name: ` ${red('▸')} Close issue`, value: 'close' },
273
337
  { name: ` ${green('▸')} Reopen issue`, value: 'reopen' },
274
338
  { name: ` ${red('▸')} Delete (close & lock)`, value: 'delete' },
@@ -298,7 +362,7 @@ async function issueMenu() {
298
362
  }
299
363
  else if (action === 'bulk') {
300
364
  const a = await ask([
301
- { type: 'input', name: 'file', message: yellow('📁 Path to issues JSON file') + dim(' (q=back):') },
365
+ { type: 'input', name: 'file', message: yellow('📁 Path to issues file (JSON or MD)') + dim(' (q=back):') },
302
366
  { type: 'confirm', name: 'dryRun', message: 'Dry run first?', default: true },
303
367
  { type: 'number', name: 'start', message: dim('Start index:'), default: 1 },
304
368
  { type: 'number', name: 'end', message: dim('End index:'), default: 999 },
@@ -306,19 +370,12 @@ async function issueMenu() {
306
370
  await issues.createIssuesFromFile(a.file, { dryRun: a.dryRun, start: a.start, end: a.end });
307
371
  }
308
372
  else if (action === 'assign-best') {
309
- // ── Smart Auto-Assign Flow ──
310
- // 1. Fetch all open issues with comments
311
- // 2. Filter to ones with applicant comments
312
- // 3. Let user pick from a list
313
- // 4. Show applicants + scores, then assign best
314
373
  const spinner = createSpinner(dim('Finding issues with applicants...')).start();
315
374
  const octokit = getOctokit();
316
375
  const { data: allIssues } = await octokit.issues.listForRepo({
317
376
  owner: getOwner(), repo: getRepo(), state: 'open', per_page: 50,
318
377
  });
319
- // Filter to real issues (not PRs) with comments
320
378
  const realIssues = allIssues.filter((i) => !i.pull_request && i.comments > 0);
321
- // Check each issue for applicant comments
322
379
  const issuesWithApplicants = [];
323
380
  for (const issue of realIssues) {
324
381
  const { data: comments } = await octokit.issues.listComments({
@@ -345,7 +402,6 @@ async function issueMenu() {
345
402
  return;
346
403
  }
347
404
  console.log(`\n${bold(`🏆 Issues with Applicants`)} (${issuesWithApplicants.length})\n`);
348
- // Let user pick an issue
349
405
  const { picked } = await inquirer.prompt([{
350
406
  type: 'list', name: 'picked', message: yellow('Select an issue:'),
351
407
  choices: [
@@ -358,7 +414,6 @@ async function issueMenu() {
358
414
  }]);
359
415
  if (picked === -1)
360
416
  return;
361
- // Run the scoring
362
417
  await issues.assignBest(picked);
363
418
  }
364
419
  else if (action === 'close' || action === 'reopen' || action === 'delete') {
@@ -399,7 +454,7 @@ async function prMenu() {
399
454
  const { action } = await inquirer.prompt([{
400
455
  type: 'list', name: 'action', message: cyan('🔀 PR Operation:'),
401
456
  choices: [
402
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
457
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
403
458
  new inquirer.Separator(dim(' ─────────────────────────────')),
404
459
  { name: ` ${green('▸')} List pull requests`, value: 'list' },
405
460
  { name: ` ${green('▸')} Merge PR ${dim('(checks CI first)')}`, value: 'merge' },
@@ -451,7 +506,7 @@ async function repoMenu() {
451
506
  const { action } = await inquirer.prompt([{
452
507
  type: 'list', name: 'action', message: green('📦 Repo Operation:'),
453
508
  choices: [
454
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
509
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
455
510
  new inquirer.Separator(dim(' ─────────────────────────────')),
456
511
  { name: ` ${green('▸')} List repositories`, value: 'list' },
457
512
  { name: ` ${green('▸')} Create repo`, value: 'create' },
@@ -480,29 +535,89 @@ async function repoMenu() {
480
535
  await repos.createRepo(a.name, { org: a.org || undefined, description: a.description, private: a.isPrivate });
481
536
  }
482
537
  else if (action === 'delete') {
483
- const repoName = await ask([
484
- { type: 'input', name: 'name', message: red('📦 Repo to DELETE') + dim(' (q=back):') },
485
- { type: 'input', name: 'org', message: 'Org:', default: getOwner() },
538
+ const { org } = await ask([
539
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
486
540
  ]);
541
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
542
+ let repoToDelete;
543
+ if (fetchedRepos.length > 0) {
544
+ const { selection } = await inquirer.prompt([{
545
+ type: 'list',
546
+ name: 'selection',
547
+ message: red('📦 Select Repo to DELETE:'),
548
+ choices: [
549
+ { name: dim('⬅ Back'), value: 'back' },
550
+ ...fetchedRepos.map((r) => ({ name: r.name, value: r.name }))
551
+ ]
552
+ }]);
553
+ if (selection === 'back')
554
+ return;
555
+ repoToDelete = selection;
556
+ }
557
+ else {
558
+ const { name } = await ask([{ type: 'input', name: 'name', message: red('📦 Repo name to DELETE:') }]);
559
+ repoToDelete = name;
560
+ }
487
561
  const { confirm } = await ask([
488
- { type: 'input', name: 'confirm', message: red(`⚠️ Type "${repoName.name}" to confirm deletion:`), validate: (v) => v === repoName.name || 'Name doesn\'t match' },
562
+ { type: 'input', name: 'confirm', message: red(`⚠️ Type "${repoToDelete}" to confirm deletion:`), validate: (v) => v === repoToDelete || 'Name doesn\'t match' },
489
563
  ]);
490
- if (confirm === repoName.name)
491
- await repos.deleteRepo(repoName.name, { org: repoName.org });
564
+ if (confirm === repoToDelete)
565
+ await repos.deleteRepo(repoToDelete, { org });
492
566
  }
493
567
  else if (action === 'clone') {
494
- const a = await ask([
495
- { type: 'input', name: 'name', message: cyan('Repo name') + dim(' (q=back):') },
496
- { type: 'input', name: 'org', message: dim('Org:'), default: getOwner() },
568
+ const { org } = await ask([
569
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
570
+ ]);
571
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
572
+ let repoToClone;
573
+ if (fetchedRepos.length > 0) {
574
+ const { selection } = await inquirer.prompt([{
575
+ type: 'list',
576
+ name: 'selection',
577
+ message: cyan('📦 Select Repo to Clone:'),
578
+ choices: [
579
+ { name: dim('⬅ Back'), value: 'back' },
580
+ ...fetchedRepos.map((r) => ({ name: r.name, value: r.name }))
581
+ ]
582
+ }]);
583
+ if (selection === 'back')
584
+ return;
585
+ repoToClone = selection;
586
+ }
587
+ else {
588
+ const { name } = await ask([{ type: 'input', name: 'name', message: cyan('📦 Repo name to Clone:') }]);
589
+ repoToClone = name;
590
+ }
591
+ const { dir } = await ask([
592
+ { type: 'input', name: 'dir', message: dim('Destination path (leave blank for default):'), default: '' },
497
593
  ]);
498
- await repos.cloneRepo(a.name, { org: a.org });
594
+ await repos.cloneRepo(repoToClone, { org, dir: dir || undefined });
499
595
  }
500
596
  else if (action === 'info') {
501
- const a = await ask([
502
- { type: 'input', name: 'name', message: cyan('Repo name:'), default: getRepo() },
503
- { type: 'input', name: 'org', message: dim('Org:'), default: getOwner() },
597
+ const { org } = await ask([
598
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
504
599
  ]);
505
- await repos.repoInfo(a.name, { org: a.org });
600
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
601
+ let repoToInfo;
602
+ if (fetchedRepos.length > 0) {
603
+ const { selection } = await inquirer.prompt([{
604
+ type: 'list',
605
+ name: 'selection',
606
+ message: cyan('📦 Select Repo for Info:'),
607
+ choices: [
608
+ { name: dim('⬅ Back'), value: 'back' },
609
+ ...fetchedRepos.map((r) => ({ name: r.name, value: r.name }))
610
+ ]
611
+ }]);
612
+ if (selection === 'back')
613
+ return;
614
+ repoToInfo = selection;
615
+ }
616
+ else {
617
+ const { name } = await ask([{ type: 'input', name: 'name', message: cyan('📦 Repo name:') }]);
618
+ repoToInfo = name;
619
+ }
620
+ await repos.repoInfo(repoToInfo, { org });
506
621
  }
507
622
  else if (action === 'topics') {
508
623
  const a = await ask([
@@ -512,12 +627,12 @@ async function repoMenu() {
512
627
  await repos.setTopics(a.name, a.topics.split(/\s+/), { org: getOwner() });
513
628
  }
514
629
  }
515
- // ── Contributor Menu ───────────────────────────────────────────────────
516
- async function contributorMenu() {
630
+ // ── Contributor Scoring Menu (Maintainer Tool) ─────────────────────────
631
+ async function contributorScoringMenu() {
517
632
  const { action } = await inquirer.prompt([{
518
633
  type: 'list', name: 'action', message: yellow('🏆 Contributor Operation:'),
519
634
  choices: [
520
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
635
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
521
636
  new inquirer.Separator(dim(' ─────────────────────────────')),
522
637
  { name: ` ${yellow('▸')} Score a user`, value: 'score' },
523
638
  { name: ` ${yellow('▸')} Rank applicants for issue`, value: 'rank' },
@@ -534,7 +649,7 @@ async function contributorMenu() {
534
649
  const { n } = await ask([{ type: 'input', name: 'n', message: yellow('Issue #') + dim(' (q=back):') }]);
535
650
  await contributors.rankApplicants(parseInt(n));
536
651
  }
537
- else {
652
+ else if (action === 'list') {
538
653
  const a = await ask([{ type: 'input', name: 'limit', message: dim('Max results (q=back):'), default: '50' }]);
539
654
  await contributors.listContributors({ limit: parseInt(a.limit) });
540
655
  }
@@ -544,26 +659,27 @@ async function releaseMenu() {
544
659
  const { action } = await inquirer.prompt([{
545
660
  type: 'list', name: 'action', message: red('🚀 Release Operation:'),
546
661
  choices: [
547
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
662
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
548
663
  new inquirer.Separator(dim(' ─────────────────────────────')),
549
- { name: ` ${red('▸')} Create release`, value: 'create' },
550
- { name: ` ${cyan('▸')} List releases`, value: 'list' },
664
+ { name: ` ${green('▸')} List releases`, value: 'list' },
665
+ { name: ` ${green('▸')} Create release`, value: 'create' },
551
666
  ],
552
667
  }]);
553
668
  if (action === 'back')
554
669
  return;
555
- if (action === 'create') {
670
+ if (action === 'list') {
671
+ const a = await ask([{ type: 'input', name: 'limit', message: dim('Max results:'), default: '50' }]);
672
+ await releases.listReleases({ limit: parseInt(a.limit) });
673
+ }
674
+ else if (action === 'create') {
556
675
  const a = await ask([
557
- { type: 'input', name: 'tag', message: yellow('Tag (e.g., v1.0.0)') + dim(' (q=back):') },
558
- { type: 'input', name: 'name', message: dim('Release name:'), default: '' },
676
+ { type: 'input', name: 'tag', message: yellow('Tag (e.g. v1.0.0):') },
677
+ { type: 'input', name: 'name', message: dim('Release name (optional):'), default: '' },
678
+ { type: 'input', name: 'body', message: dim('Release body (optional):'), default: '' },
559
679
  { type: 'confirm', name: 'draft', message: 'Draft?', default: false },
560
- { type: 'confirm', name: 'prerelease', message: 'Pre-release?', default: false },
680
+ { type: 'confirm', name: 'prerelease', message: 'Prerelease?', default: false },
561
681
  ]);
562
- await releases.createRelease(a.tag, { name: a.name || a.tag, draft: a.draft, prerelease: a.prerelease });
563
- }
564
- else {
565
- const a = await ask([{ type: 'input', name: 'limit', message: dim('Max results (q=back):'), default: '50' }]);
566
- await releases.listReleases({ limit: parseInt(a.limit) });
682
+ await releases.createRelease(a.tag, { name: a.name, body: a.body, draft: a.draft, prerelease: a.prerelease });
567
683
  }
568
684
  }
569
685
  // ── Commander (direct commands) ────────────────────────────────────────
@@ -583,55 +699,73 @@ function setupCommander() {
583
699
  // Issues
584
700
  const i = program.command('issues').description('📋 Manage issues');
585
701
  i.command('list').option('-s, --state <s>', '', 'open').option('-l, --labels <l>').option('-n, --limit <n>', '', '50')
586
- .action((o) => issues.listIssues({ state: o.state, labels: o.labels, limit: parseInt(o.limit) }));
702
+ .action(async (o) => { await issues.listIssues({ state: o.state, labels: o.labels, limit: parseInt(o.limit) }); });
587
703
  i.command('create').requiredOption('-t, --title <t>').option('-b, --body <b>').option('-l, --labels <l>')
588
- .action((o) => issues.createIssue(o));
704
+ .action(async (o) => { await issues.createIssue(o); });
589
705
  i.command('bulk').requiredOption('-f, --file <f>').option('-d, --dry-run').option('--start <n>', '', '1').option('--end <n>', '', '999')
590
- .action((o) => issues.createIssuesFromFile(o.file, { dryRun: o.dryRun, start: parseInt(o.start), end: parseInt(o.end) }));
591
- i.command('close <n>').action((n) => issues.closeIssue(parseInt(n)));
592
- i.command('reopen <n>').action((n) => issues.reopenIssue(parseInt(n)));
593
- i.command('delete <n>').action((n) => issues.deleteIssue(parseInt(n)));
594
- i.command('assign <n> <users...>').action((n, u) => issues.assignIssue(parseInt(n), u));
595
- i.command('assign-best <n>').action((n) => issues.assignBest(parseInt(n)));
596
- i.command('search <q>').action((q) => issues.searchIssues(q));
597
- i.command('label <n> <labels...>').action((n, l) => issues.labelIssue(parseInt(n), l));
706
+ .action(async (o) => { await issues.createIssuesFromFile(o.file, { dryRun: o.dryRun, start: parseInt(o.start), end: parseInt(o.end) }); });
707
+ i.command('close <n>').action(async (n) => { await issues.closeIssue(parseInt(n)); });
708
+ i.command('reopen <n>').action(async (n) => { await issues.reopenIssue(parseInt(n)); });
709
+ i.command('delete <n>').action(async (n) => { await issues.deleteIssue(parseInt(n)); });
710
+ i.command('assign <n> <users...>').action(async (n, u) => { await issues.assignIssue(parseInt(n), u); });
711
+ i.command('assign-best <n>').action(async (n) => { await issues.assignBest(parseInt(n)); });
712
+ i.command('search <q>').action(async (q) => { await issues.searchIssues(q); });
713
+ i.command('label <n> <labels...>').action(async (n, l) => { await issues.labelIssue(parseInt(n), l); });
598
714
  // PRs
599
715
  const p = program.command('prs').description('🔀 Manage pull requests');
600
716
  p.command('list').option('-s, --state <s>', '', 'open').option('-n, --limit <n>', '', '50')
601
- .action((o) => prs.listPRs({ state: o.state, limit: parseInt(o.limit) }));
717
+ .action(async (o) => { await prs.listPRs({ state: o.state, limit: parseInt(o.limit) }); });
602
718
  p.command('merge <n>').option('-m, --method <m>', '', 'squash').option('--message <msg>').option('-f, --force', 'Skip CI checks')
603
- .action((n, o) => prs.mergePR(parseInt(n), o));
604
- p.command('close <n>').action((n) => prs.closePR(parseInt(n)));
605
- p.command('review <n>').action((n) => prs.reviewPR(parseInt(n)));
606
- p.command('approve <n>').action((n) => prs.approvePR(parseInt(n)));
607
- p.command('diff <n>').action((n) => prs.diffPR(parseInt(n)));
719
+ .action(async (n, o) => { await prs.mergePR(parseInt(n), o); });
720
+ p.command('close <n>').action(async (n) => { await prs.closePR(parseInt(n)); });
721
+ p.command('review <n>').action(async (n) => { await prs.reviewPR(parseInt(n)); });
722
+ p.command('approve <n>').action(async (n) => { await prs.approvePR(parseInt(n)); });
723
+ p.command('diff <n>').action(async (n) => { await prs.diffPR(parseInt(n)); });
608
724
  // Repos
609
725
  const r = program.command('repo').description('📦 Manage repositories');
610
726
  r.command('list').option('-o, --org <o>').option('-n, --limit <n>', '', '50')
611
- .action((o) => repos.listRepos({ org: o.org, limit: parseInt(o.limit) }));
727
+ .action(async (o) => { await repos.listRepos({ org: o.org, limit: parseInt(o.limit) }); });
612
728
  r.command('create <name>').option('-o, --org <o>').option('-p, --private').option('-d, --description <d>')
613
- .action((n, o) => repos.createRepo(n, o));
614
- r.command('delete <name>').option('-o, --org <o>').action((n, o) => repos.deleteRepo(n, o));
615
- r.command('clone <name>').option('-o, --org <o>').option('-d, --dir <d>').action((n, o) => repos.cloneRepo(n, o));
616
- r.command('info <name>').option('-o, --org <o>').action((n, o) => repos.repoInfo(n, o));
617
- r.command('topics <name> <topics...>').option('-o, --org <o>').action((n, t, o) => repos.setTopics(n, t, o));
729
+ .action(async (n, o) => { await repos.createRepo(n, o); });
730
+ r.command('delete <name>').option('-o, --org <o>').action(async (n, o) => { await repos.deleteRepo(n, o); });
731
+ r.command('clone <name>').option('-o, --org <o>').option('-d, --dir <d>').action(async (n, o) => { await repos.cloneRepo(n, o); });
732
+ r.command('info <name>').option('-o, --org <o>').action(async (n, o) => { await repos.repoInfo(n, o); });
733
+ r.command('topics <name> <topics...>').option('-o, --org <o>').action(async (n, t, o) => { await repos.setTopics(n, t, o); });
618
734
  // Contributors
619
735
  const c = program.command('contributors').description('🏆 Manage contributors');
620
- c.command('score <user>').action((u) => contributors.scoreUser(u));
621
- c.command('rank <issue>').action((n) => contributors.rankApplicants(parseInt(n)));
622
- c.command('list').option('-n, --limit <n>', '', '50').action((o) => contributors.listContributors({ limit: parseInt(o.limit) }));
736
+ c.command('score <user>').action(async (u) => { await contributors.scoreUser(u); });
737
+ c.command('rank <issue>').action(async (n) => { await contributors.rankApplicants(parseInt(n)); });
738
+ c.command('list').option('-n, --limit <n>', '', '50').action(async (o) => { await contributors.listContributors({ limit: parseInt(o.limit) }); });
623
739
  // Releases
624
740
  const rel = program.command('release').description('🚀 Manage releases');
625
741
  rel.command('create <tag>').option('-n, --name <n>').option('-b, --body <b>').option('-d, --draft').option('-p, --prerelease').option('--no-generate')
626
- .action((t, o) => releases.createRelease(t, o));
627
- rel.command('list').option('-n, --limit <n>', '', '50').action((o) => releases.listReleases({ limit: parseInt(o.limit) }));
742
+ .action(async (t, o) => { await releases.createRelease(t, o); });
743
+ rel.command('list').option('-n, --limit <n>', '', '50').action(async (o) => { await releases.listReleases({ limit: parseInt(o.limit) }); });
744
+ // Contributor Commands
745
+ // ILLUSTRATION: How to start?
746
+ // Option A: `gitpadi start https://github.com/owner/repo`
747
+ // Option B: Interactive Menu -> Contributor Mode -> Start Contribution
748
+ program.command('start <url>').description('🚀 Start contribution (fork, clone & branch)').action(async (u) => { await contribute.forkAndClone(u); });
749
+ program.command('sync').description('🔄 Sync with upstream').action(async () => { await contribute.syncBranch(); });
750
+ program.command('submit').description('🚀 Submit PR (add, commit, push & PR)')
751
+ .option('-t, --title <t>', 'PR title')
752
+ .option('-m, --message <m>', 'Commit message')
753
+ .option('-i, --issue <n>', 'Issue number')
754
+ .action(async (o) => {
755
+ await contribute.submitPR({
756
+ title: o.title || 'Automated PR',
757
+ message: o.message || o.title,
758
+ issue: o.issue ? parseInt(o.issue) : undefined
759
+ });
760
+ });
761
+ program.command('logs').description('📋 View Action logs').action(async () => { await contribute.viewLogs(); });
628
762
  return program;
629
763
  }
630
764
  // ── Entry Point ────────────────────────────────────────────────────────
631
765
  async function main() {
632
766
  if (process.argv.length <= 2) {
633
767
  await bootSequence();
634
- await onboarding();
768
+ await ensureAuthenticated();
635
769
  await mainMenu();
636
770
  }
637
771
  else {