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/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,27 +362,19 @@ 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):') },
302
- { type: 'confirm', name: 'dryRun', message: 'Dry run first?', default: true },
365
+ { type: 'input', name: 'file', message: yellow('📁 Path to issues file (JSON or MD)') + dim(' (q=back):') },
303
366
  { type: 'number', name: 'start', message: dim('Start index:'), default: 1 },
304
367
  { type: 'number', name: 'end', message: dim('End index:'), default: 999 },
305
368
  ]);
306
- await issues.createIssuesFromFile(a.file, { dryRun: a.dryRun, start: a.start, end: a.end });
369
+ await issues.createIssuesFromFile(a.file, { start: a.start, end: a.end });
307
370
  }
308
371
  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
372
  const spinner = createSpinner(dim('Finding issues with applicants...')).start();
315
373
  const octokit = getOctokit();
316
374
  const { data: allIssues } = await octokit.issues.listForRepo({
317
375
  owner: getOwner(), repo: getRepo(), state: 'open', per_page: 50,
318
376
  });
319
- // Filter to real issues (not PRs) with comments
320
377
  const realIssues = allIssues.filter((i) => !i.pull_request && i.comments > 0);
321
- // Check each issue for applicant comments
322
378
  const issuesWithApplicants = [];
323
379
  for (const issue of realIssues) {
324
380
  const { data: comments } = await octokit.issues.listComments({
@@ -345,7 +401,6 @@ async function issueMenu() {
345
401
  return;
346
402
  }
347
403
  console.log(`\n${bold(`🏆 Issues with Applicants`)} (${issuesWithApplicants.length})\n`);
348
- // Let user pick an issue
349
404
  const { picked } = await inquirer.prompt([{
350
405
  type: 'list', name: 'picked', message: yellow('Select an issue:'),
351
406
  choices: [
@@ -358,7 +413,6 @@ async function issueMenu() {
358
413
  }]);
359
414
  if (picked === -1)
360
415
  return;
361
- // Run the scoring
362
416
  await issues.assignBest(picked);
363
417
  }
364
418
  else if (action === 'close' || action === 'reopen' || action === 'delete') {
@@ -399,7 +453,7 @@ async function prMenu() {
399
453
  const { action } = await inquirer.prompt([{
400
454
  type: 'list', name: 'action', message: cyan('🔀 PR Operation:'),
401
455
  choices: [
402
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
456
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
403
457
  new inquirer.Separator(dim(' ─────────────────────────────')),
404
458
  { name: ` ${green('▸')} List pull requests`, value: 'list' },
405
459
  { name: ` ${green('▸')} Merge PR ${dim('(checks CI first)')}`, value: 'merge' },
@@ -451,7 +505,7 @@ async function repoMenu() {
451
505
  const { action } = await inquirer.prompt([{
452
506
  type: 'list', name: 'action', message: green('📦 Repo Operation:'),
453
507
  choices: [
454
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
508
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
455
509
  new inquirer.Separator(dim(' ─────────────────────────────')),
456
510
  { name: ` ${green('▸')} List repositories`, value: 'list' },
457
511
  { name: ` ${green('▸')} Create repo`, value: 'create' },
@@ -480,29 +534,89 @@ async function repoMenu() {
480
534
  await repos.createRepo(a.name, { org: a.org || undefined, description: a.description, private: a.isPrivate });
481
535
  }
482
536
  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() },
537
+ const { org } = await ask([
538
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
486
539
  ]);
540
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
541
+ let repoToDelete;
542
+ if (fetchedRepos.length > 0) {
543
+ const { selection } = await inquirer.prompt([{
544
+ type: 'list',
545
+ name: 'selection',
546
+ message: red('📦 Select Repo to DELETE:'),
547
+ choices: [
548
+ { name: dim('⬅ Back'), value: 'back' },
549
+ ...fetchedRepos.map((r) => ({ name: r.name, value: r.name }))
550
+ ]
551
+ }]);
552
+ if (selection === 'back')
553
+ return;
554
+ repoToDelete = selection;
555
+ }
556
+ else {
557
+ const { name } = await ask([{ type: 'input', name: 'name', message: red('📦 Repo name to DELETE:') }]);
558
+ repoToDelete = name;
559
+ }
487
560
  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' },
561
+ { type: 'input', name: 'confirm', message: red(`⚠️ Type "${repoToDelete}" to confirm deletion:`), validate: (v) => v === repoToDelete || 'Name doesn\'t match' },
489
562
  ]);
490
- if (confirm === repoName.name)
491
- await repos.deleteRepo(repoName.name, { org: repoName.org });
563
+ if (confirm === repoToDelete)
564
+ await repos.deleteRepo(repoToDelete, { org });
492
565
  }
493
566
  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() },
567
+ const { org } = await ask([
568
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
569
+ ]);
570
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
571
+ let repoToClone;
572
+ if (fetchedRepos.length > 0) {
573
+ const { selection } = await inquirer.prompt([{
574
+ type: 'list',
575
+ name: 'selection',
576
+ message: cyan('📦 Select Repo to Clone:'),
577
+ choices: [
578
+ { name: dim('⬅ Back'), value: 'back' },
579
+ ...fetchedRepos.map((r) => ({ name: r.name, value: r.name }))
580
+ ]
581
+ }]);
582
+ if (selection === 'back')
583
+ return;
584
+ repoToClone = selection;
585
+ }
586
+ else {
587
+ const { name } = await ask([{ type: 'input', name: 'name', message: cyan('📦 Repo name to Clone:') }]);
588
+ repoToClone = name;
589
+ }
590
+ const { dir } = await ask([
591
+ { type: 'input', name: 'dir', message: dim('Destination path (leave blank for default):'), default: '' },
497
592
  ]);
498
- await repos.cloneRepo(a.name, { org: a.org });
593
+ await repos.cloneRepo(repoToClone, { org, dir: dir || undefined });
499
594
  }
500
595
  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() },
596
+ const { org } = await ask([
597
+ { type: 'input', name: 'org', message: cyan('👤 Org/Owner name') + dim(' (q=back):'), default: getOwner() },
504
598
  ]);
505
- await repos.repoInfo(a.name, { org: a.org });
599
+ const fetchedRepos = await repos.listRepos({ org, silent: true });
600
+ let repoToInfo;
601
+ if (fetchedRepos.length > 0) {
602
+ const { selection } = await inquirer.prompt([{
603
+ type: 'list',
604
+ name: 'selection',
605
+ message: cyan('📦 Select Repo for Info:'),
606
+ choices: [
607
+ { name: dim('⬅ Back'), value: 'back' },
608
+ ...fetchedRepos.map((r) => ({ name: r.name, value: r.name }))
609
+ ]
610
+ }]);
611
+ if (selection === 'back')
612
+ return;
613
+ repoToInfo = selection;
614
+ }
615
+ else {
616
+ const { name } = await ask([{ type: 'input', name: 'name', message: cyan('📦 Repo name:') }]);
617
+ repoToInfo = name;
618
+ }
619
+ await repos.repoInfo(repoToInfo, { org });
506
620
  }
507
621
  else if (action === 'topics') {
508
622
  const a = await ask([
@@ -512,12 +626,12 @@ async function repoMenu() {
512
626
  await repos.setTopics(a.name, a.topics.split(/\s+/), { org: getOwner() });
513
627
  }
514
628
  }
515
- // ── Contributor Menu ───────────────────────────────────────────────────
516
- async function contributorMenu() {
629
+ // ── Contributor Scoring Menu (Maintainer Tool) ─────────────────────────
630
+ async function contributorScoringMenu() {
517
631
  const { action } = await inquirer.prompt([{
518
632
  type: 'list', name: 'action', message: yellow('🏆 Contributor Operation:'),
519
633
  choices: [
520
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
634
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
521
635
  new inquirer.Separator(dim(' ─────────────────────────────')),
522
636
  { name: ` ${yellow('▸')} Score a user`, value: 'score' },
523
637
  { name: ` ${yellow('▸')} Rank applicants for issue`, value: 'rank' },
@@ -534,7 +648,7 @@ async function contributorMenu() {
534
648
  const { n } = await ask([{ type: 'input', name: 'n', message: yellow('Issue #') + dim(' (q=back):') }]);
535
649
  await contributors.rankApplicants(parseInt(n));
536
650
  }
537
- else {
651
+ else if (action === 'list') {
538
652
  const a = await ask([{ type: 'input', name: 'limit', message: dim('Max results (q=back):'), default: '50' }]);
539
653
  await contributors.listContributors({ limit: parseInt(a.limit) });
540
654
  }
@@ -544,26 +658,27 @@ async function releaseMenu() {
544
658
  const { action } = await inquirer.prompt([{
545
659
  type: 'list', name: 'action', message: red('🚀 Release Operation:'),
546
660
  choices: [
547
- { name: ` ${dim('⬅ Back to main menu')}`, value: 'back' },
661
+ { name: ` ${dim('⬅ Back')}`, value: 'back' },
548
662
  new inquirer.Separator(dim(' ─────────────────────────────')),
549
- { name: ` ${red('▸')} Create release`, value: 'create' },
550
- { name: ` ${cyan('▸')} List releases`, value: 'list' },
663
+ { name: ` ${green('▸')} List releases`, value: 'list' },
664
+ { name: ` ${green('▸')} Create release`, value: 'create' },
551
665
  ],
552
666
  }]);
553
667
  if (action === 'back')
554
668
  return;
555
- if (action === 'create') {
669
+ if (action === 'list') {
670
+ const a = await ask([{ type: 'input', name: 'limit', message: dim('Max results:'), default: '50' }]);
671
+ await releases.listReleases({ limit: parseInt(a.limit) });
672
+ }
673
+ else if (action === 'create') {
556
674
  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: '' },
675
+ { type: 'input', name: 'tag', message: yellow('Tag (e.g. v1.0.0):') },
676
+ { type: 'input', name: 'name', message: dim('Release name (optional):'), default: '' },
677
+ { type: 'input', name: 'body', message: dim('Release body (optional):'), default: '' },
559
678
  { type: 'confirm', name: 'draft', message: 'Draft?', default: false },
560
- { type: 'confirm', name: 'prerelease', message: 'Pre-release?', default: false },
679
+ { type: 'confirm', name: 'prerelease', message: 'Prerelease?', default: false },
561
680
  ]);
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) });
681
+ await releases.createRelease(a.tag, { name: a.name, body: a.body, draft: a.draft, prerelease: a.prerelease });
567
682
  }
568
683
  }
569
684
  // ── Commander (direct commands) ────────────────────────────────────────
@@ -583,55 +698,73 @@ function setupCommander() {
583
698
  // Issues
584
699
  const i = program.command('issues').description('📋 Manage issues');
585
700
  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) }));
701
+ .action(async (o) => { await issues.listIssues({ state: o.state, labels: o.labels, limit: parseInt(o.limit) }); });
587
702
  i.command('create').requiredOption('-t, --title <t>').option('-b, --body <b>').option('-l, --labels <l>')
588
- .action((o) => issues.createIssue(o));
703
+ .action(async (o) => { await issues.createIssue(o); });
589
704
  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));
705
+ .action(async (o) => { await issues.createIssuesFromFile(o.file, { dryRun: o.dryRun, start: parseInt(o.start), end: parseInt(o.end) }); });
706
+ i.command('close <n>').action(async (n) => { await issues.closeIssue(parseInt(n)); });
707
+ i.command('reopen <n>').action(async (n) => { await issues.reopenIssue(parseInt(n)); });
708
+ i.command('delete <n>').action(async (n) => { await issues.deleteIssue(parseInt(n)); });
709
+ i.command('assign <n> <users...>').action(async (n, u) => { await issues.assignIssue(parseInt(n), u); });
710
+ i.command('assign-best <n>').action(async (n) => { await issues.assignBest(parseInt(n)); });
711
+ i.command('search <q>').action(async (q) => { await issues.searchIssues(q); });
712
+ i.command('label <n> <labels...>').action(async (n, l) => { await issues.labelIssue(parseInt(n), l); });
598
713
  // PRs
599
714
  const p = program.command('prs').description('🔀 Manage pull requests');
600
715
  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) }));
716
+ .action(async (o) => { await prs.listPRs({ state: o.state, limit: parseInt(o.limit) }); });
602
717
  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)));
718
+ .action(async (n, o) => { await prs.mergePR(parseInt(n), o); });
719
+ p.command('close <n>').action(async (n) => { await prs.closePR(parseInt(n)); });
720
+ p.command('review <n>').action(async (n) => { await prs.reviewPR(parseInt(n)); });
721
+ p.command('approve <n>').action(async (n) => { await prs.approvePR(parseInt(n)); });
722
+ p.command('diff <n>').action(async (n) => { await prs.diffPR(parseInt(n)); });
608
723
  // Repos
609
724
  const r = program.command('repo').description('📦 Manage repositories');
610
725
  r.command('list').option('-o, --org <o>').option('-n, --limit <n>', '', '50')
611
- .action((o) => repos.listRepos({ org: o.org, limit: parseInt(o.limit) }));
726
+ .action(async (o) => { await repos.listRepos({ org: o.org, limit: parseInt(o.limit) }); });
612
727
  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));
728
+ .action(async (n, o) => { await repos.createRepo(n, o); });
729
+ r.command('delete <name>').option('-o, --org <o>').action(async (n, o) => { await repos.deleteRepo(n, o); });
730
+ r.command('clone <name>').option('-o, --org <o>').option('-d, --dir <d>').action(async (n, o) => { await repos.cloneRepo(n, o); });
731
+ r.command('info <name>').option('-o, --org <o>').action(async (n, o) => { await repos.repoInfo(n, o); });
732
+ r.command('topics <name> <topics...>').option('-o, --org <o>').action(async (n, t, o) => { await repos.setTopics(n, t, o); });
618
733
  // Contributors
619
734
  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) }));
735
+ c.command('score <user>').action(async (u) => { await contributors.scoreUser(u); });
736
+ c.command('rank <issue>').action(async (n) => { await contributors.rankApplicants(parseInt(n)); });
737
+ c.command('list').option('-n, --limit <n>', '', '50').action(async (o) => { await contributors.listContributors({ limit: parseInt(o.limit) }); });
623
738
  // Releases
624
739
  const rel = program.command('release').description('🚀 Manage releases');
625
740
  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) }));
741
+ .action(async (t, o) => { await releases.createRelease(t, o); });
742
+ rel.command('list').option('-n, --limit <n>', '', '50').action(async (o) => { await releases.listReleases({ limit: parseInt(o.limit) }); });
743
+ // Contributor Commands
744
+ // ILLUSTRATION: How to start?
745
+ // Option A: `gitpadi start https://github.com/owner/repo`
746
+ // Option B: Interactive Menu -> Contributor Mode -> Start Contribution
747
+ program.command('start <url>').description('🚀 Start contribution (fork, clone & branch)').action(async (u) => { await contribute.forkAndClone(u); });
748
+ program.command('sync').description('🔄 Sync with upstream').action(async () => { await contribute.syncBranch(); });
749
+ program.command('submit').description('🚀 Submit PR (add, commit, push & PR)')
750
+ .option('-t, --title <t>', 'PR title')
751
+ .option('-m, --message <m>', 'Commit message')
752
+ .option('-i, --issue <n>', 'Issue number')
753
+ .action(async (o) => {
754
+ await contribute.submitPR({
755
+ title: o.title || 'Automated PR',
756
+ message: o.message || o.title,
757
+ issue: o.issue ? parseInt(o.issue) : undefined
758
+ });
759
+ });
760
+ program.command('logs').description('📋 View Action logs').action(async () => { await contribute.viewLogs(); });
628
761
  return program;
629
762
  }
630
763
  // ── Entry Point ────────────────────────────────────────────────────────
631
764
  async function main() {
632
765
  if (process.argv.length <= 2) {
633
766
  await bootSequence();
634
- await onboarding();
767
+ await ensureAuthenticated();
635
768
  await mainMenu();
636
769
  }
637
770
  else {