gitpadi 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.ts CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env npx tsx
1
+ #!/usr/bin/env node
2
2
  // ═══════════════════════════════════════════════════════════════════════
3
3
  // GitPadi v2.0 — Your AI-Powered GitHub Management Terminal
4
4
  // "Built different. Run different."
@@ -11,7 +11,8 @@ import gradient from 'gradient-string';
11
11
  import figlet from 'figlet';
12
12
  import boxen from 'boxen';
13
13
  import { createSpinner } from 'nanospinner';
14
- import { initGitHub, setRepo, getOwner, getRepo, getOctokit } from './core/github.js';
14
+ import { Octokit } from '@octokit/rest';
15
+ import { initGitHub, setRepo, getOwner, getRepo, getOctokit, loadConfig, saveConfig, getToken } from './core/github.js';
15
16
 
16
17
  import * as issues from './commands/issues.js';
17
18
  import * as prs from './commands/prs.js';
@@ -124,17 +125,27 @@ async function bootSequence() {
124
125
 
125
126
  // ── Onboarding ─────────────────────────────────────────────────────────
126
127
  async function onboarding() {
127
- const savedToken = process.env.GITHUB_TOKEN || process.env.GITPADI_TOKEN;
128
- const savedOwner = process.env.GITHUB_OWNER;
129
- const savedRepo = process.env.GITHUB_REPO;
128
+ // Force a fresh load from config/env
129
+ initGitHub();
130
130
 
131
- if (savedToken && savedOwner && savedRepo) {
132
- initGitHub(savedToken, savedOwner, savedRepo);
131
+ let token = getToken();
132
+ let owner = getOwner();
133
+ let repo = getRepo();
134
+
135
+ // If everything is already set (likely via env or previously saved config), skip
136
+ if (token && owner && repo) {
133
137
  const spinner = createSpinner(dim('Authenticating...')).start();
134
- await sleep(600);
135
- spinner.success({ text: green(`Connected to ${cyan(`${savedOwner}/${savedRepo}`)}`) });
136
- console.log('');
137
- return;
138
+ try {
139
+ const octokit = getOctokit();
140
+ await octokit.users.getAuthenticated();
141
+ spinner.success({ text: green(`Authenticated & targeting ${cyan(`${owner}/${repo}`)}`) });
142
+ console.log('');
143
+ return;
144
+ } catch {
145
+ spinner.error({ text: red('Saved session invalid. Re-authenticating...') });
146
+ // Clear and continue to prompt
147
+ token = '';
148
+ }
138
149
  }
139
150
 
140
151
  console.log(neon(' ⚡ First-time setup — let\'s connect you to GitHub.\n'));
@@ -145,43 +156,59 @@ async function onboarding() {
145
156
  name: 'token',
146
157
  message: magenta('🔑 GitHub Token') + dim(' (ghp_xxx):'),
147
158
  mask: '•',
148
- validate: (v: string) => v.startsWith('ghp_') || v.startsWith('github_pat_') ? true : 'Token should start with ghp_ or github_pat_',
149
- when: !savedToken,
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,
150
175
  },
151
176
  {
152
177
  type: 'input',
153
178
  name: 'owner',
154
179
  message: cyan('👤 GitHub Owner/Org:'),
155
- default: savedOwner || '',
180
+ default: owner || '',
156
181
  validate: (v: string) => v.length > 0 || 'Required',
157
- when: !savedOwner,
182
+ when: !owner,
158
183
  },
159
184
  {
160
185
  type: 'input',
161
186
  name: 'repo',
162
187
  message: cyan('📦 Repository name:'),
163
- default: savedRepo || '',
188
+ default: repo || '',
164
189
  validate: (v: string) => v.length > 0 || 'Required',
165
- when: !savedRepo,
190
+ when: !repo,
166
191
  },
167
192
  ]);
168
193
 
169
- const token = answers.token || savedToken;
170
- const owner = answers.owner || savedOwner;
171
- const repo = answers.repo || savedRepo;
194
+ const finalToken = answers.token || token;
195
+ const finalOwner = answers.owner || owner;
196
+ const finalRepo = answers.repo || repo;
172
197
 
173
- initGitHub(token, owner, repo);
198
+ initGitHub(finalToken, finalOwner, finalRepo);
199
+ saveConfig({ token: finalToken, owner: finalOwner, repo: finalRepo });
174
200
 
175
201
  const spinner = createSpinner(dim('Connecting to GitHub...')).start();
176
- await sleep(800);
177
- spinner.success({ text: green(`Locked in → ${cyan(`${owner}/${repo}`)}`) });
202
+ await sleep(400);
203
+ spinner.success({ text: green(`Locked in → ${cyan(`${finalOwner}/${finalRepo}`)}`) });
178
204
 
179
205
  console.log('');
180
206
  console.log(boxen(
181
- dim('💡 Pro tip: Set these to skip setup next time:\n\n') +
207
+ dim('💡 Session saved to ~/.gitpadi/config.json\n\n') +
208
+ dim('Set environment variables to override:\n') +
182
209
  yellow(' export GITHUB_TOKEN=ghp_xxx\n') +
183
- yellow(' export GITHUB_OWNER=' + owner + '\n') +
184
- yellow(' export GITHUB_REPO=' + repo),
210
+ yellow(' export GITHUB_OWNER=' + finalOwner + '\n') +
211
+ yellow(' export GITHUB_REPO=' + finalRepo),
185
212
  { padding: 1, borderColor: 'yellow', dimBorder: true, borderStyle: 'round' }
186
213
  ));
187
214
  console.log('');
@@ -267,7 +294,7 @@ async function issueMenu() {
267
294
  new inquirer.Separator(dim(' ─────────────────────────────')),
268
295
  { name: ` ${green('▸')} List open issues`, value: 'list' },
269
296
  { name: ` ${green('▸')} Create single issue`, value: 'create' },
270
- { name: ` ${green('▸')} Bulk create from JSON`, value: 'bulk' },
297
+ { name: ` ${magenta('▸')} Bulk create from JSON file`, value: 'bulk' },
271
298
  { name: ` ${red('▸')} Close issue`, value: 'close' },
272
299
  { name: ` ${green('▸')} Reopen issue`, value: 'reopen' },
273
300
  { name: ` ${red('▸')} Delete (close & lock)`, value: 'delete' },
@@ -284,18 +311,19 @@ async function issueMenu() {
284
311
  const opts = await ask([
285
312
  { type: 'list', name: 'state', message: 'State:', choices: ['open', 'closed', 'all'], default: 'open' },
286
313
  { type: 'input', name: 'labels', message: dim('Filter by labels (optional, q=back):'), default: '' },
314
+ { type: 'input', name: 'limit', message: dim('Max results:'), default: '50' },
287
315
  ]);
288
- await issues.listIssues({ state: opts.state, labels: opts.labels || undefined });
316
+ await issues.listIssues({ state: opts.state, labels: opts.labels || undefined, limit: parseInt(opts.limit) });
289
317
  } else if (action === 'create') {
290
318
  const a = await ask([
291
- { type: 'input', name: 'title', message: yellow('Issue title') + dim(' (q=back):') },
292
- { type: 'editor', name: 'body', message: 'Issue body (opens editor):' },
319
+ { type: 'input', name: 'title', message: yellow('Issue title'), validate: (v: string) => v.length > 0 || 'Required' },
320
+ { type: 'input', name: 'body', message: dim('Issue body (optional):'), default: '' },
293
321
  { type: 'input', name: 'labels', message: dim('Labels (comma-separated):'), default: '' },
294
322
  ]);
295
323
  await issues.createIssue(a);
296
324
  } else if (action === 'bulk') {
297
325
  const a = await ask([
298
- { type: 'input', name: 'file', message: yellow('📁 Path to issues JSON') + dim(' (q=back):') },
326
+ { type: 'input', name: 'file', message: yellow('📁 Path to issues JSON file') + dim(' (q=back):') },
299
327
  { type: 'confirm', name: 'dryRun', message: 'Dry run first?', default: true },
300
328
  { type: 'number', name: 'start', message: dim('Start index:'), default: 1 },
301
329
  { type: 'number', name: 'end', message: dim('End index:'), default: 999 },
@@ -416,8 +444,11 @@ async function prMenu() {
416
444
  if (action === 'back') return;
417
445
 
418
446
  if (action === 'list') {
419
- const { state } = await inquirer.prompt([{ type: 'list', name: 'state', message: 'State:', choices: ['open', 'closed', 'all'] }]);
420
- await prs.listPRs({ state });
447
+ const a = await ask([
448
+ { type: 'list', name: 'state', message: 'State:', choices: ['open', 'closed', 'all'], default: 'open' },
449
+ { type: 'input', name: 'limit', message: dim('Max results:'), default: '50' },
450
+ ]);
451
+ await prs.listPRs({ state: a.state, limit: parseInt(a.limit) });
421
452
  } else if (action === 'merge' || action === 'force-merge') {
422
453
  const a = await ask([
423
454
  { type: 'input', name: 'n', message: yellow('PR #') + dim(' (q=back):') },
@@ -459,8 +490,11 @@ async function repoMenu() {
459
490
  if (action === 'back') return;
460
491
 
461
492
  if (action === 'list') {
462
- const { org } = await ask([{ type: 'input', name: 'org', message: cyan('Org (blank for yours, q=back):'), default: '' }]);
463
- await repos.listRepos({ org: org || undefined });
493
+ const a = await ask([
494
+ { type: 'input', name: 'org', message: cyan('Org (blank for yours, q=back):'), default: '' },
495
+ { type: 'input', name: 'limit', message: dim('Max results:'), default: '50' },
496
+ ]);
497
+ await repos.listRepos({ org: a.org || undefined, limit: parseInt(a.limit) });
464
498
  } else if (action === 'create') {
465
499
  const a = await ask([
466
500
  { type: 'input', name: 'name', message: yellow('📦 Repo name') + dim(' (q=back):') },
@@ -521,7 +555,8 @@ async function contributorMenu() {
521
555
  const { n } = await ask([{ type: 'input', name: 'n', message: yellow('Issue #') + dim(' (q=back):') }]);
522
556
  await contributors.rankApplicants(parseInt(n));
523
557
  } else {
524
- await contributors.listContributors({});
558
+ const a = await ask([{ type: 'input', name: 'limit', message: dim('Max results (q=back):'), default: '50' }]);
559
+ await contributors.listContributors({ limit: parseInt(a.limit) });
525
560
  }
526
561
  }
527
562
 
@@ -548,7 +583,8 @@ async function releaseMenu() {
548
583
  ]);
549
584
  await releases.createRelease(a.tag, { name: a.name || a.tag, draft: a.draft, prerelease: a.prerelease });
550
585
  } else {
551
- await releases.listReleases({});
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) });
552
588
  }
553
589
  }
554
590
 
@@ -569,7 +605,7 @@ function setupCommander(): Command {
569
605
 
570
606
  // Issues
571
607
  const i = program.command('issues').description('📋 Manage issues');
572
- i.command('list').option('-s, --state <s>', '', 'open').option('-l, --labels <l>').option('-n, --limit <n>', '', '25')
608
+ i.command('list').option('-s, --state <s>', '', 'open').option('-l, --labels <l>').option('-n, --limit <n>', '', '50')
573
609
  .action((o) => issues.listIssues({ state: o.state, labels: o.labels, limit: parseInt(o.limit) }));
574
610
  i.command('create').requiredOption('-t, --title <t>').option('-b, --body <b>').option('-l, --labels <l>')
575
611
  .action((o) => issues.createIssue(o));
@@ -585,7 +621,7 @@ function setupCommander(): Command {
585
621
 
586
622
  // PRs
587
623
  const p = program.command('prs').description('🔀 Manage pull requests');
588
- p.command('list').option('-s, --state <s>', '', 'open').option('-n, --limit <n>', '', '25')
624
+ p.command('list').option('-s, --state <s>', '', 'open').option('-n, --limit <n>', '', '50')
589
625
  .action((o) => prs.listPRs({ state: o.state, limit: parseInt(o.limit) }));
590
626
  p.command('merge <n>').option('-m, --method <m>', '', 'squash').option('--message <msg>').option('-f, --force', 'Skip CI checks')
591
627
  .action((n, o) => prs.mergePR(parseInt(n), o));
@@ -596,7 +632,7 @@ function setupCommander(): Command {
596
632
 
597
633
  // Repos
598
634
  const r = program.command('repo').description('📦 Manage repositories');
599
- r.command('list').option('-o, --org <o>').option('-n, --limit <n>', '', '25')
635
+ r.command('list').option('-o, --org <o>').option('-n, --limit <n>', '', '50')
600
636
  .action((o) => repos.listRepos({ org: o.org, limit: parseInt(o.limit) }));
601
637
  r.command('create <name>').option('-o, --org <o>').option('-p, --private').option('-d, --description <d>')
602
638
  .action((n, o) => repos.createRepo(n, o));
@@ -609,13 +645,13 @@ function setupCommander(): Command {
609
645
  const c = program.command('contributors').description('🏆 Manage contributors');
610
646
  c.command('score <user>').action((u) => contributors.scoreUser(u));
611
647
  c.command('rank <issue>').action((n) => contributors.rankApplicants(parseInt(n)));
612
- c.command('list').option('-n, --limit <n>', '', '25').action((o) => contributors.listContributors({ limit: parseInt(o.limit) }));
648
+ c.command('list').option('-n, --limit <n>', '', '50').action((o) => contributors.listContributors({ limit: parseInt(o.limit) }));
613
649
 
614
650
  // Releases
615
651
  const rel = program.command('release').description('🚀 Manage releases');
616
652
  rel.command('create <tag>').option('-n, --name <n>').option('-b, --body <b>').option('-d, --draft').option('-p, --prerelease').option('--no-generate')
617
653
  .action((t, o) => releases.createRelease(t, o));
618
- rel.command('list').option('-n, --limit <n>', '', '10').action((o) => releases.listReleases({ limit: parseInt(o.limit) }));
654
+ rel.command('list').option('-n, --limit <n>', '', '50').action((o) => releases.listReleases({ limit: parseInt(o.limit) }));
619
655
 
620
656
  return program;
621
657
  }
@@ -94,7 +94,7 @@ export async function listContributors(opts: { limit?: number }) {
94
94
 
95
95
  try {
96
96
  const { data } = await getOctokit().repos.listContributors({
97
- owner: getOwner(), repo: getRepo(), per_page: opts.limit || 25,
97
+ owner: getOwner(), repo: getRepo(), per_page: opts.limit || 50,
98
98
  });
99
99
  spinner.stop();
100
100
 
@@ -18,7 +18,7 @@ export async function listIssues(opts: { state?: string; labels?: string; limit?
18
18
  owner: getOwner(), repo: getRepo(),
19
19
  state: (opts.state as 'open' | 'closed' | 'all') || 'open',
20
20
  labels: opts.labels || undefined,
21
- per_page: opts.limit || 25,
21
+ per_page: opts.limit || 50,
22
22
  });
23
23
 
24
24
  // Filter out PRs (GitHub API returns PRs in issues endpoint)
@@ -13,7 +13,7 @@ export async function listPRs(opts: { state?: string; limit?: number }) {
13
13
  const { data: prs } = await getOctokit().pulls.list({
14
14
  owner: getOwner(), repo: getRepo(),
15
15
  state: (opts.state as 'open' | 'closed' | 'all') || 'open',
16
- per_page: opts.limit || 25,
16
+ per_page: opts.limit || 50,
17
17
  });
18
18
 
19
19
  spinner.stop();
@@ -31,7 +31,7 @@ export async function listReleases(opts: { limit?: number }) {
31
31
 
32
32
  try {
33
33
  const { data } = await getOctokit().repos.listReleases({
34
- owner: getOwner(), repo: getRepo(), per_page: opts.limit || 10,
34
+ owner: getOwner(), repo: getRepo(), per_page: opts.limit || 50,
35
35
  });
36
36
  spinner.stop();
37
37
 
@@ -100,9 +100,9 @@ export async function listRepos(opts: { org?: string; limit?: number }) {
100
100
  try {
101
101
  let repos: any[];
102
102
  if (opts.org) {
103
- ({ data: repos } = await octokit.repos.listForOrg({ org: opts.org, per_page: opts.limit || 25, sort: 'updated' }));
103
+ ({ data: repos } = await octokit.repos.listForOrg({ org: opts.org, per_page: opts.limit || 50, sort: 'updated' }));
104
104
  } else {
105
- ({ data: repos } = await octokit.repos.listForAuthenticatedUser({ per_page: opts.limit || 25, sort: 'updated' }));
105
+ ({ data: repos } = await octokit.repos.listForAuthenticatedUser({ per_page: opts.limit || 50, sort: 'updated' }));
106
106
  }
107
107
 
108
108
  spinner.stop();
@@ -1,36 +1,68 @@
1
- // core/github.ts — Shared Octokit client + auth for GitPadi
2
-
3
1
  import { Octokit } from '@octokit/rest';
4
2
  import chalk from 'chalk';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import os from 'node:os';
6
+
7
+
8
+ const CONFIG_DIR = path.join(os.homedir(), '.gitpadi');
9
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
5
10
 
6
11
  let _octokit: Octokit | null = null;
7
12
  let _owner: string = '';
8
13
  let _repo: string = '';
14
+ let _token: string = '';
9
15
 
10
- export function initGitHub(token?: string, owner?: string, repo?: string): void {
11
- const t = token || process.env.GITHUB_TOKEN || process.env.GITPADI_TOKEN || '';
12
- if (!t) {
13
- console.error(chalk.red('\n❌ GitHub token required.'));
14
- console.error(chalk.dim(' Set it: export GITHUB_TOKEN=ghp_xxx'));
15
- console.error(chalk.dim(' Or: export GITPADI_TOKEN=ghp_xxx\n'));
16
- process.exit(1);
16
+ export interface GitPadiConfig {
17
+ token: string;
18
+ owner: string;
19
+ repo: string;
20
+ }
21
+
22
+ export function loadConfig(): GitPadiConfig | null {
23
+ if (!fs.existsSync(CONFIG_FILE)) return null;
24
+ try {
25
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
26
+ } catch {
27
+ return null;
17
28
  }
29
+ }
18
30
 
19
- _octokit = new Octokit({ auth: t });
20
- _owner = owner || process.env.GITHUB_OWNER || process.env.GITHUB_REPOSITORY?.split('/')[0] || '';
21
- _repo = repo || process.env.GITHUB_REPO || process.env.GITHUB_REPOSITORY?.split('/')[1] || '';
31
+ export function saveConfig(config: GitPadiConfig): void {
32
+ if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
33
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
34
+ }
35
+
36
+ export function initGitHub(token?: string, owner?: string, repo?: string): void {
37
+ const config = loadConfig();
38
+
39
+ _token = token || process.env.GITHUB_TOKEN || process.env.GITPADI_TOKEN || config?.token || '';
40
+ _owner = owner || process.env.GITHUB_OWNER || config?.owner || process.env.GITHUB_REPOSITORY?.split('/')[0] || '';
41
+ _repo = repo || process.env.GITHUB_REPO || config?.repo || process.env.GITHUB_REPOSITORY?.split('/')[1] || '';
42
+
43
+ if (_token) {
44
+ _octokit = new Octokit({ auth: _token });
45
+ }
22
46
  }
23
47
 
24
48
  export function getOctokit(): Octokit {
25
49
  if (!_octokit) {
26
- initGitHub();
50
+ throw new Error('GitHub client not initialized. Call initGitHub() first.');
27
51
  }
28
- return _octokit!;
52
+ return _octokit;
29
53
  }
30
54
 
31
55
  export function getOwner(): string { return _owner; }
32
56
  export function getRepo(): string { return _repo; }
33
- export function setRepo(owner: string, repo: string): void { _owner = owner; _repo = repo; }
57
+ export function getToken(): string { return _token; }
58
+ export function setRepo(owner: string, repo: string): void {
59
+ _owner = owner;
60
+ _repo = repo;
61
+ // If we have a token, PERSIST the new repo selection
62
+ if (_token) {
63
+ saveConfig({ token: _token, owner, repo });
64
+ }
65
+ }
34
66
  export function getFullRepo(): string { return `${_owner}/${_repo}`; }
35
67
 
36
68
  export function requireRepo(): void {
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env tsx
1
+ #!/usr/bin/env node
2
2
  // create-issues.ts — Generic issue creator from JSON data file
3
3
  //
4
4
  // Usage via GitHub Action:
package/src/pr-review.ts CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env tsx
1
+ #!/usr/bin/env node
2
2
  // pr-review.ts — Generic PR review agent for any repository
3
3
  //
4
4
  // Checks: linked issues, PR size, file scope, test coverage, commit messages, sensitive files