gitpadi 2.0.2 → 2.0.4

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/README.md CHANGED
@@ -1,270 +1,102 @@
1
- <p align="center">
2
- <h1 align="center">šŸ¤– GitPadi</h1>
3
- <p align="center"><strong>Your AI-powered GitHub management agent.</strong></p>
4
- <p align="center">
5
- Auto-score contributors, review PRs, manage issues & repos — from your terminal or as a GitHub Action.
6
- </p>
7
- <p align="center">
8
- <a href="#-use-the-cli">CLI</a> •
9
- <a href="#-use-as-github-action">GitHub Action</a> •
10
- <a href="#-scoring-algorithm">Scoring Algorithm</a>
11
- </p>
12
- </p>
1
+ # šŸ¤– GitPadi
13
2
 
14
- ---
15
-
16
- ## ⚔ Two Ways to Use GitPadi
3
+ GitPadi is a GitHub management CLI and Action built to automate the repetitive parts of maintaining open-source projects.
17
4
 
18
- | Method | Best For | Setup |
19
- |--------|----------|-------|
20
- | **CLI** (`npx gitpadi`) | Hands-on management from your terminal | One command |
21
- | **GitHub Action** | Automated scoring & reviews on every event | Copy one workflow file |
5
+ Whether you're a maintainer managing hundreds of issues or a contributor making your first PR, GitPadi turns multi-step GitHub workflows into single commands.
22
6
 
23
7
  ---
24
8
 
25
- ## šŸ–„ļø Use the CLI
9
+ ## šŸš€ For Contributors
26
10
 
27
- **No installation needed.** Run directly with `npx`:
11
+ Stop worrying about forking, upstream remotes, and branch naming. GitPadi handles the plumbing so you can focus on the code.
28
12
 
13
+ ### Start a new contribution
29
14
  ```bash
30
- export GITHUB_TOKEN=ghp_your_token
31
- export GITHUB_OWNER=your-org
32
- export GITHUB_REPO=your-repo
33
-
34
- npx gitpadi
35
- ```
36
-
37
- That launches the **interactive terminal**:
38
-
39
- ```
40
- ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
41
- │ šŸ¤– GitPadi v2.0 │
42
- │ Your GitHub Management Companion │
43
- ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
44
-
45
- ? What would you like to do?
46
- šŸ“‹ Manage Issues
47
- šŸ”€ Manage Pull Requests
48
- šŸ“¦ Manage Repositories
49
- šŸ† Score Contributors
50
- šŸš€ Create Release
51
- āš™ļø Switch Repo
52
- šŸ‘‹ Exit
15
+ npx gitpadi start <issue-url>
53
16
  ```
17
+ This single command:
18
+ 1. **Forks** the repository to your account.
19
+ 2. **Clones** it to your machine.
20
+ 3. Sets up the **upstream remote**.
21
+ 4. Creates a **fresh feature branch** linked to the issue.
54
22
 
55
- Or use **direct commands**:
56
-
23
+ ### Submit your work
57
24
  ```bash
58
- npx gitpadi issues list
59
- npx gitpadi prs merge 5 --method squash
60
- npx gitpadi contributors score octocat
61
- npx gitpadi repo create my-app --org MyOrg
25
+ npx gitpadi submit
62
26
  ```
27
+ When you're done, this command:
28
+ 1. **Stages** all your changes.
29
+ 2. Creates a **commit** with a clean message.
30
+ 3. **Pushes** to your fork.
31
+ 4. Opens a **Pull Request** on the original repo, auto-linked to the issue.
32
+
33
+ ---
63
34
 
64
- ### All CLI Commands
35
+ ## šŸ› ļø For Maintainers
65
36
 
66
- <details>
67
- <summary><b>šŸ“‹ Issues</b></summary>
37
+ Manage your community and project health directly from your terminal.
68
38
 
39
+ ### šŸ“‹ Bulk Issue Creation
40
+ Create dozens of issues in seconds from a simple Markdown or JSON file.
69
41
  ```bash
70
- npx gitpadi issues list # List open issues
71
- npx gitpadi issues create -t "Bug fix" -l bug # Create issue
72
- npx gitpadi issues bulk -f issues.json # Bulk create from JSON
73
- npx gitpadi issues close 42 # Close issue
74
- npx gitpadi issues reopen 42 # Reopen issue
75
- npx gitpadi issues delete 42 # Close & lock
76
- npx gitpadi issues assign 42 alice bob # Assign users
77
- npx gitpadi issues assign-best 42 # šŸ† Auto-assign top scorer
78
- npx gitpadi issues search "login bug" # Search issues
79
- npx gitpadi issues label 42 bug priority-high # Add labels
42
+ npx gitpadi issues bulk -f roadmap.md
80
43
  ```
81
- </details>
82
-
83
- <details>
84
- <summary><b>šŸ”€ Pull Requests</b></summary>
44
+ *(Supports `## Heading` titles and `**Labels:**` tags).*
85
45
 
46
+ ### šŸ† Smart Contributor Scoring
47
+ Auto-score applicants based on their GitHub history, account age, and relevance to the issue.
86
48
  ```bash
87
- npx gitpadi prs list # List open PRs
88
- npx gitpadi prs merge 5 --method squash # Merge PR (squash/merge/rebase)
89
- npx gitpadi prs close 5 # Close PR
90
- npx gitpadi prs review 5 # Auto-review (size, tests, security)
91
- npx gitpadi prs approve 5 # Approve PR
92
- npx gitpadi prs diff 5 # Show changed files
49
+ npx gitpadi contributors score octocat
93
50
  ```
94
- </details>
95
-
96
- <details>
97
- <summary><b>šŸ“¦ Repositories</b></summary>
98
-
51
+ Or let GitPadi find the best applicant for an issue:
99
52
  ```bash
100
- npx gitpadi repo list --org MyOrg # List repos
101
- npx gitpadi repo create my-app --org MyOrg # Create repo
102
- npx gitpadi repo delete my-app --org MyOrg # Delete repo
103
- npx gitpadi repo clone my-app # Clone repo
104
- npx gitpadi repo info my-app # Show repo details
105
- npx gitpadi repo topics my-app rust blockchain # Set topics
53
+ npx gitpadi issues assign-best 42
106
54
  ```
107
- </details>
108
55
 
109
- <details>
110
- <summary><b>šŸ† Contributors</b></summary>
56
+ ### šŸ”€ Advanced PR Management
57
+ - **Review PRs**: Get an automated review (size analysis, security checks, test coverage).
58
+ - **Fast Merge**: Squash and merge PRs directly from the CLI.
59
+ - **CI Status**: View GitHub Action logs for a PR without opening a browser.
111
60
 
112
- ```bash
113
- npx gitpadi contributors score octocat # Score a user (0-100)
114
- npx gitpadi contributors rank 42 # Rank all applicants for issue #42
115
- npx gitpadi contributors list # List repo contributors
116
- ```
117
- </details>
61
+ ---
118
62
 
119
- <details>
120
- <summary><b>šŸš€ Releases</b></summary>
63
+ ## ⚔ Quick Start
64
+
65
+ No installation required. Just set your token and go:
121
66
 
122
67
  ```bash
123
- npx gitpadi release create v1.0.0 # Create release (auto-notes)
124
- npx gitpadi release create v1.0.0 --draft # Create as draft
125
- npx gitpadi release list # List releases
68
+ export GITHUB_TOKEN=ghp_your_token
69
+ npx gitpadi
126
70
  ```
127
- </details>
128
-
129
- ### Or install globally:
130
71
 
72
+ ### Direct Commands
131
73
  ```bash
132
- npm install -g gitpadi
133
- gitpadi issues list
74
+ npx gitpadi issues list --limit 100
75
+ npx gitpadi prs review 5
76
+ npx gitpadi release create v1.0.0
134
77
  ```
135
78
 
136
79
  ---
137
80
 
138
- ## šŸ”§ Use as GitHub Action
139
-
140
- Just add a workflow file — no setup, no tokens to manage.
81
+ ## šŸ”§ Use as a GitHub Action
141
82
 
142
- ### šŸ† Auto-Score Contributors
83
+ GitPadi can run on every event in your repo. Just add a workflow file.
143
84
 
85
+ **Auto-Score Applicants:**
144
86
  ```yaml
145
- # .github/workflows/applicant-scorer.yml
146
- name: Score Applicants
147
- on:
148
- issue_comment:
149
- types: [created]
150
- permissions:
151
- contents: read
152
- issues: write
153
- pull-requests: read
154
- jobs:
155
- score:
156
- if: "!github.event.issue.pull_request"
157
- runs-on: ubuntu-latest
158
- steps:
159
- - uses: actions/checkout@v4
160
- - uses: Netwalls/contributor-agent@v2
161
- with:
162
- action: score-applicant
163
- notify-user: 'your-username' # ← you get @mentioned
87
+ - uses: Netwalls/contributor-agent@v2
88
+ with:
89
+ action: score-applicant
164
90
  ```
165
91
 
166
- ### šŸ” Auto-Review PRs
167
-
92
+ **Auto-Review PRs:**
168
93
  ```yaml
169
- # .github/workflows/pr-review.yml
170
- name: PR Review
171
- on:
172
- pull_request:
173
- types: [opened, synchronize, reopened]
174
- permissions:
175
- contents: read
176
- pull-requests: write
177
- jobs:
178
- review:
179
- runs-on: ubuntu-latest
180
- steps:
181
- - uses: actions/checkout@v4
182
- - uses: Netwalls/contributor-agent@v2
183
- with:
184
- action: review-pr
185
- ```
186
-
187
- ### šŸ“‹ Bulk Create Issues
188
-
189
- ```yaml
190
- # .github/workflows/create-issues.yml
191
- name: Create Issues
192
- on:
193
- workflow_dispatch:
194
- inputs:
195
- issues-file:
196
- description: 'Path to issues JSON'
197
- default: 'issues.json'
198
- dry-run:
199
- description: 'Preview only'
200
- default: 'true'
201
- permissions:
202
- issues: write
203
- jobs:
204
- create:
205
- runs-on: ubuntu-latest
206
- steps:
207
- - uses: actions/checkout@v4
208
- - uses: Netwalls/contributor-agent@v2
209
- with:
210
- action: create-issues
211
- issues-file: ${{ github.event.inputs.issues-file }}
212
- dry-run: ${{ github.event.inputs.dry-run }}
213
- ```
214
-
215
- ---
216
-
217
- ## šŸ† Scoring Algorithm
218
-
219
- GitPadi scores contributors on **6 dimensions** (100 points total):
220
-
221
- | Category | Max | What It Measures |
222
- |----------|-----|-----------------|
223
- | šŸ”§ **Repo Experience** | **30** | Merged PRs, open PRs, issues in YOUR specific repo |
224
- | šŸ›ļø Account Maturity | 15 | Account age |
225
- | 🌐 GitHub Presence | 15 | Public repos, followers, profile README |
226
- | ⚔ Activity Level | 15 | Recent public GitHub activity |
227
- | šŸ“ Application Quality | 15 | Comment depth — mentions approach, experience, timeline |
228
- | šŸ’» Language Relevance | 10 | User's languages match issue label languages |
229
-
230
- ### Tiers
231
-
232
- | Tier | Score | Recommendation |
233
- |------|-------|---------------|
234
- | šŸ† **S** | 75+ | Strong — assign immediately |
235
- | 🟢 **A** | 55-74 | Good — assign with confidence |
236
- | 🟔 **B** | 40-54 | Decent — review profile first |
237
- | 🟠 **C** | 25-39 | Below average — wait for better |
238
- | šŸ”“ **D** | 0-24 | Low — manual review needed |
239
-
240
- ### Detected Application Phrases
241
-
242
- > *"I'd like to work on this"* Ā· *"Can I take this?"* Ā· *"Assign me"* Ā· *"I'm interested"* Ā· *"I want to contribute"* Ā· *"Let me handle this"* Ā· *"Picking this up"* Ā· *"Claiming this"*
243
-
244
- ---
245
-
246
- ## āš™ļø Configuration
247
-
248
- ### Environment Variables
249
-
250
- | Variable | Required | Description |
251
- |----------|----------|-------------|
252
- | `GITHUB_TOKEN` | āœ… | GitHub PAT with `repo` scope |
253
- | `GITHUB_OWNER` | For CLI | Org or username |
254
- | `GITHUB_REPO` | For CLI | Repository name |
255
-
256
- ### CLI Flags
257
-
258
- ```bash
259
- npx gitpadi --owner MyOrg --repo my-project --token ghp_xxx issues list
94
+ - uses: Netwalls/contributor-agent@v2
95
+ with:
96
+ action: review-pr
260
97
  ```
261
98
 
262
99
  ---
263
100
 
264
101
  ## šŸ“„ License
265
-
266
- MIT — Built by [Netwalls](https://github.com/Netwalls)
267
-
268
-
269
-
270
- i noticed that a users have more issues but it is just bring out the top 10 issues that are open how do we bring out more issues
102
+ MIT — Built by [Netwalls](https://github.com/Netwalls)
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import chalk from 'chalk';
8
8
  import inquirer from 'inquirer';
9
9
  import gradient from 'gradient-string';
10
10
  import figlet from 'figlet';
11
+ import { execSync } from 'child_process';
11
12
  import boxen from 'boxen';
12
13
  import { createSpinner } from 'nanospinner';
13
14
  import { Octokit } from '@octokit/rest';
@@ -257,16 +258,50 @@ async function contributorMenu() {
257
258
  await contribute.viewLogs();
258
259
  }
259
260
  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):' }
261
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
262
+ const match = branch.match(/issue-(\d+)/);
263
+ const detectedIssue = match ? parseInt(match[1]) : undefined;
264
+ let detectedTitle = '';
265
+ if (detectedIssue) {
266
+ process.stdout.write(dim(` šŸ” Detected Issue #${detectedIssue} from branch... `));
267
+ try {
268
+ const { data: issueData } = await getOctokit().issues.get({
269
+ owner: getOwner(),
270
+ repo: getRepo(),
271
+ issue_number: detectedIssue,
272
+ });
273
+ detectedTitle = issueData.title;
274
+ console.log(cyan(`${detectedTitle}`));
275
+ }
276
+ catch {
277
+ console.log(dim(' (could not fetch title)'));
278
+ }
279
+ }
280
+ const { confirm } = await ask([
281
+ {
282
+ type: 'list',
283
+ name: 'confirm',
284
+ message: yellow('šŸš€ Ready to submit?'),
285
+ choices: [
286
+ { name: `āœ… Yes, use automated metadata`, value: 'yes' },
287
+ { name: `āœļø Edit title/message manually`, value: 'edit' },
288
+ { name: `⬅ Back`, value: 'back' }
289
+ ]
290
+ }
264
291
  ]);
265
- await contribute.submitPR({
266
- title,
267
- message: message || title,
268
- issue: issue ? parseInt(issue) : undefined
269
- });
292
+ if (confirm === 'back')
293
+ return;
294
+ let submissionOpts = {};
295
+ if (confirm === 'edit') {
296
+ submissionOpts = await ask([
297
+ { type: 'input', name: 'title', message: 'PR Title:', default: detectedTitle ? `fix: ${detectedTitle}` : '' },
298
+ { type: 'input', name: 'message', message: 'Commit Message:', default: detectedTitle ? `fix: ${detectedTitle} (#${detectedIssue})` : '' },
299
+ { type: 'input', name: 'issue', message: 'Issue #:', default: detectedIssue ? detectedIssue.toString() : '' }
300
+ ]);
301
+ if (submissionOpts.issue)
302
+ submissionOpts.issue = parseInt(submissionOpts.issue);
303
+ }
304
+ await contribute.submitPR(submissionOpts);
270
305
  }
271
306
  }
272
307
  }
@@ -363,11 +398,10 @@ async function issueMenu() {
363
398
  else if (action === 'bulk') {
364
399
  const a = await ask([
365
400
  { type: 'input', name: 'file', message: yellow('šŸ“ Path to issues file (JSON or MD)') + dim(' (q=back):') },
366
- { type: 'confirm', name: 'dryRun', message: 'Dry run first?', default: true },
367
401
  { type: 'number', name: 'start', message: dim('Start index:'), default: 1 },
368
402
  { type: 'number', name: 'end', message: dim('End index:'), default: 999 },
369
403
  ]);
370
- await issues.createIssuesFromFile(a.file, { dryRun: a.dryRun, start: a.start, end: a.end });
404
+ await issues.createIssuesFromFile(a.file, { start: a.start, end: a.end });
371
405
  }
372
406
  else if (action === 'assign-best') {
373
407
  const spinner = createSpinner(dim('Finding issues with applicants...')).start();
@@ -753,8 +787,8 @@ function setupCommander() {
753
787
  .option('-i, --issue <n>', 'Issue number')
754
788
  .action(async (o) => {
755
789
  await contribute.submitPR({
756
- title: o.title || 'Automated PR',
757
- message: o.message || o.title,
790
+ title: o.title,
791
+ message: o.message,
758
792
  issue: o.issue ? parseInt(o.issue) : undefined
759
793
  });
760
794
  });
@@ -81,8 +81,23 @@ export async function forkAndClone(target) {
81
81
  execSync(`git remote add upstream https://github.com/${owner}/${repo}.git`, { stdio: 'pipe' });
82
82
  }
83
83
  catch { /* exists */ }
84
+ // 1. Detect default branch
85
+ let defaultBranch = 'main';
86
+ try {
87
+ execSync('git rev-parse upstream/main', { stdio: 'pipe' });
88
+ }
89
+ catch {
90
+ defaultBranch = 'master';
91
+ }
92
+ // 2. Checkout default branch before syncing
93
+ try {
94
+ execSync(`git checkout ${defaultBranch}`, { stdio: 'pipe' });
95
+ }
96
+ catch {
97
+ // If it fails, maybe it's not even a valid default branch locally, but we'll try to sync anyway
98
+ }
84
99
  await syncBranch();
85
- // Create or switch to issue branch
100
+ // 3. Create or switch to issue branch
86
101
  const branchName = issue ? `fix/issue-${issue}` : null;
87
102
  if (branchName) {
88
103
  try {
@@ -220,9 +235,34 @@ export async function submitPR(opts) {
220
235
  const owner = getOwner();
221
236
  const repo = getRepo();
222
237
  const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
238
+ // Auto-infer issue from branch name (e.g. fix/issue-303)
239
+ let linkedIssue = opts.issue;
240
+ if (!linkedIssue) {
241
+ const match = branch.match(/issue-(\d+)/);
242
+ if (match)
243
+ linkedIssue = parseInt(match[1]);
244
+ }
245
+ const octokit = getOctokit();
246
+ // 2. Fetch Issue Details for better PR Metadata
247
+ let issueTitle = '';
248
+ if (linkedIssue) {
249
+ spinner.text = `Fetching details for issue #${linkedIssue}...`;
250
+ try {
251
+ const { data: issueData } = await octokit.issues.get({
252
+ owner,
253
+ repo,
254
+ issue_number: linkedIssue,
255
+ });
256
+ issueTitle = issueData.title;
257
+ }
258
+ catch (e) {
259
+ // Silently fallback if issue doesn't exist or no access
260
+ }
261
+ }
262
+ const prTitle = opts.title || (issueTitle ? `fix: ${issueTitle} (#${linkedIssue})` : 'Automated contribution via GitPadi');
263
+ const commitMsg = opts.message || prTitle;
223
264
  // 1. Stage and Commit
224
265
  spinner.text = 'Staging and committing changes...';
225
- const commitMsg = opts.message || opts.title || 'Automated contribution via GitPadi';
226
266
  try {
227
267
  execSync('git add .', { stdio: 'pipe' });
228
268
  // Check if there are changes to commit
@@ -235,24 +275,25 @@ export async function submitPR(opts) {
235
275
  // If commit fails (e.g. no changes), we might still want to push if there are unpushed commits
236
276
  dim(' (Note: No new changes to commit or commit failed)');
237
277
  }
238
- // Auto-infer issue from branch name (e.g. fix/issue-303)
239
- let linkedIssue = opts.issue;
240
- if (!linkedIssue) {
241
- const match = branch.match(/issue-(\d+)/);
242
- if (match)
243
- linkedIssue = parseInt(match[1]);
244
- }
245
278
  spinner.text = 'Pushing to your fork...';
246
279
  execSync(`git push origin ${branch}`, { stdio: 'pipe' });
247
280
  spinner.text = 'Creating Pull Request...';
248
281
  const body = opts.body || (linkedIssue ? `Fixes #${linkedIssue}` : 'Automated PR via GitPadi');
249
- const { data: pr } = await getOctokit().pulls.create({
282
+ // Detect base branch
283
+ let baseBranch = 'main';
284
+ try {
285
+ execSync('git rev-parse origin/main', { stdio: 'pipe' });
286
+ }
287
+ catch {
288
+ baseBranch = 'master';
289
+ }
290
+ const { data: pr } = await octokit.pulls.create({
250
291
  owner,
251
292
  repo,
252
- title: opts.title,
293
+ title: prTitle,
253
294
  body,
254
295
  head: `${await getAuthenticatedUser()}:${branch}`,
255
- base: 'main',
296
+ base: baseBranch,
256
297
  });
257
298
  spinner.succeed(`PR Created: ${green(pr.html_url)}`);
258
299
  }
@@ -11,16 +11,29 @@ export async function listIssues(opts) {
11
11
  const octokit = getOctokit();
12
12
  const spinner = ora(`Fetching issues from ${chalk.cyan(getFullRepo())}...`).start();
13
13
  try {
14
- const { data: issues } = await octokit.issues.listForRepo({
15
- owner: getOwner(), repo: getRepo(),
16
- state: opts.state || 'open',
17
- labels: opts.labels || undefined,
18
- per_page: opts.limit || 50,
19
- });
20
- // Filter out PRs (GitHub API returns PRs in issues endpoint)
21
- const realIssues = issues.filter((i) => !i.pull_request);
14
+ const requestedLimit = opts.limit || 50;
15
+ const realIssues = [];
16
+ let page = 1;
17
+ // Fetch until we have enough real issues or run out of pages
18
+ while (realIssues.length < requestedLimit) {
19
+ const { data: issues } = await octokit.issues.listForRepo({
20
+ owner: getOwner(), repo: getRepo(),
21
+ state: opts.state || 'open',
22
+ labels: opts.labels || undefined,
23
+ per_page: 100, // Fetch max per page to be efficient
24
+ page: page++,
25
+ });
26
+ if (issues.length === 0)
27
+ break;
28
+ const batch = issues.filter((i) => !i.pull_request);
29
+ realIssues.push(...batch);
30
+ if (issues.length < 100)
31
+ break; // Last page
32
+ }
33
+ // Clip to requested limit
34
+ const finalIssues = realIssues.slice(0, requestedLimit);
22
35
  spinner.stop();
23
- if (realIssues.length === 0) {
36
+ if (finalIssues.length === 0) {
24
37
  console.log(chalk.yellow('\n No issues found.\n'));
25
38
  return;
26
39
  }
@@ -28,13 +41,13 @@ export async function listIssues(opts) {
28
41
  head: ['#', 'Title', 'Labels', 'Assignee', 'State'].map((h) => chalk.cyan(h)),
29
42
  style: { head: [], border: [] },
30
43
  });
31
- realIssues.forEach((i) => {
44
+ finalIssues.forEach((i) => {
32
45
  const labels = i.labels.map((l) => typeof l === 'string' ? l : l.name || '').join(', ');
33
46
  const assignee = i.assignee?.login || chalk.dim('unassigned');
34
47
  const state = i.state === 'open' ? chalk.green('open') : chalk.red('closed');
35
48
  table.push([`#${i.number}`, i.title.substring(0, 60), labels.substring(0, 30), assignee, state]);
36
49
  });
37
- console.log(`\n${chalk.bold(`šŸ“‹ Issues — ${getFullRepo()}`)} (${realIssues.length})\n`);
50
+ console.log(`\n${chalk.bold(`šŸ“‹ Issues — ${getFullRepo()}`)} (${finalIssues.length})\n`);
38
51
  console.log(table.toString());
39
52
  console.log('');
40
53
  }
@@ -137,16 +150,22 @@ export async function createIssuesFromFile(filePath, opts) {
137
150
  console.log(`\n${chalk.bold('šŸ“‹ GitPadi Issue Creator')}`);
138
151
  console.log(chalk.dim(` Repo: ${getFullRepo()}`));
139
152
  console.log(chalk.dim(` File: ${filePath} (${detectedFormat})`));
140
- console.log(chalk.dim(` Range: #${start}-#${end} (${filtered.length} issues)`));
141
- console.log(chalk.dim(` Mode: ${opts.dryRun ? 'DRY RUN' : 'LIVE'}\n`));
142
- if (opts.dryRun) {
143
- filtered.forEach((i) => {
144
- console.log(` ${chalk.dim(`#${String(i.number).padStart(2, '0')}`)} ${i.title}`);
145
- console.log(chalk.dim(` [${(i.labels || []).join(', ')}]`));
146
- if (i.body)
147
- console.log(chalk.dim(` ${i.body.substring(0, 80)}...`));
148
- });
149
- console.log(chalk.green(`\nāœ… Dry run: ${filtered.length} issues would be created.\n`));
153
+ console.log(chalk.dim(` Found: ${filtered.length} issues\n`));
154
+ // Always preview first
155
+ filtered.forEach((i) => {
156
+ console.log(` ${chalk.dim(`#${String(i.number).padStart(2, '0')}`)} ${i.title}`);
157
+ console.log(chalk.dim(` [${(i.labels || []).join(', ')}]`));
158
+ });
159
+ // Ask for confirmation
160
+ const inquirer = (await import('inquirer')).default;
161
+ const { proceed } = await inquirer.prompt([{
162
+ type: 'confirm',
163
+ name: 'proceed',
164
+ message: chalk.yellow(`Create these ${filtered.length} issues on GitHub?`),
165
+ default: true,
166
+ }]);
167
+ if (!proceed) {
168
+ console.log(chalk.dim('\n Cancelled.\n'));
150
169
  return;
151
170
  }
152
171
  const octokit = getOctokit();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gitpadi",
3
- "version": "2.0.2",
4
- "description": "šŸ¤– GitPadi — Your AI-powered GitHub management CLI. Create repos, manage issues & PRs, score contributors, and automate everything.",
3
+ "version": "2.0.4",
4
+ "description": "GitPadi — Your AI-powered GitHub management CLI. Create repos, manage issues & PRs, score contributors, and automate everything.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "gitpadi": "./dist/cli.js"
package/src/cli.ts CHANGED
@@ -287,16 +287,50 @@ async function contributorMenu() {
287
287
  } else if (action === 'logs') {
288
288
  await contribute.viewLogs();
289
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):' }
290
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
291
+ const match = branch.match(/issue-(\d+)/);
292
+ const detectedIssue = match ? parseInt(match[1]) : undefined;
293
+
294
+ let detectedTitle = '';
295
+ if (detectedIssue) {
296
+ process.stdout.write(dim(` šŸ” Detected Issue #${detectedIssue} from branch... `));
297
+ try {
298
+ const { data: issueData } = await getOctokit().issues.get({
299
+ owner: getOwner(),
300
+ repo: getRepo(),
301
+ issue_number: detectedIssue,
302
+ });
303
+ detectedTitle = issueData.title;
304
+ console.log(cyan(`${detectedTitle}`));
305
+ } catch { console.log(dim(' (could not fetch title)')); }
306
+ }
307
+
308
+ const { confirm } = await ask([
309
+ {
310
+ type: 'list',
311
+ name: 'confirm',
312
+ message: yellow('šŸš€ Ready to submit?'),
313
+ choices: [
314
+ { name: `āœ… Yes, use automated metadata`, value: 'yes' },
315
+ { name: `āœļø Edit title/message manually`, value: 'edit' },
316
+ { name: `⬅ Back`, value: 'back' }
317
+ ]
318
+ }
294
319
  ]);
295
- await contribute.submitPR({
296
- title,
297
- message: message || title,
298
- issue: issue ? parseInt(issue) : undefined
299
- });
320
+
321
+ if (confirm === 'back') return;
322
+
323
+ let submissionOpts: any = {};
324
+ if (confirm === 'edit') {
325
+ submissionOpts = await ask([
326
+ { type: 'input', name: 'title', message: 'PR Title:', default: detectedTitle ? `fix: ${detectedTitle}` : '' },
327
+ { type: 'input', name: 'message', message: 'Commit Message:', default: detectedTitle ? `fix: ${detectedTitle} (#${detectedIssue})` : '' },
328
+ { type: 'input', name: 'issue', message: 'Issue #:', default: detectedIssue ? detectedIssue.toString() : '' }
329
+ ]);
330
+ if (submissionOpts.issue) submissionOpts.issue = parseInt(submissionOpts.issue);
331
+ }
332
+
333
+ await contribute.submitPR(submissionOpts);
300
334
  }
301
335
  }
302
336
  }
@@ -393,11 +427,10 @@ async function issueMenu() {
393
427
  } else if (action === 'bulk') {
394
428
  const a = await ask([
395
429
  { type: 'input', name: 'file', message: yellow('šŸ“ Path to issues file (JSON or MD)') + dim(' (q=back):') },
396
- { type: 'confirm', name: 'dryRun', message: 'Dry run first?', default: true },
397
430
  { type: 'number', name: 'start', message: dim('Start index:'), default: 1 },
398
431
  { type: 'number', name: 'end', message: dim('End index:'), default: 999 },
399
432
  ]);
400
- await issues.createIssuesFromFile(a.file, { dryRun: a.dryRun, start: a.start, end: a.end });
433
+ await issues.createIssuesFromFile(a.file, { start: a.start, end: a.end });
401
434
  } else if (action === 'assign-best') {
402
435
  const spinner = createSpinner(dim('Finding issues with applicants...')).start();
403
436
  const octokit = getOctokit();
@@ -785,8 +818,8 @@ function setupCommander(): Command {
785
818
  .option('-i, --issue <n>', 'Issue number')
786
819
  .action(async (o) => {
787
820
  await contribute.submitPR({
788
- title: o.title || 'Automated PR',
789
- message: o.message || o.title,
821
+ title: o.title,
822
+ message: o.message,
790
823
  issue: o.issue ? parseInt(o.issue) : undefined
791
824
  });
792
825
  });
@@ -100,9 +100,24 @@ export async function forkAndClone(target: string) {
100
100
  // Ensure upstream remote exists
101
101
  try { execSync(`git remote add upstream https://github.com/${owner}/${repo}.git`, { stdio: 'pipe' }); } catch { /* exists */ }
102
102
 
103
+ // 1. Detect default branch
104
+ let defaultBranch = 'main';
105
+ try {
106
+ execSync('git rev-parse upstream/main', { stdio: 'pipe' });
107
+ } catch {
108
+ defaultBranch = 'master';
109
+ }
110
+
111
+ // 2. Checkout default branch before syncing
112
+ try {
113
+ execSync(`git checkout ${defaultBranch}`, { stdio: 'pipe' });
114
+ } catch {
115
+ // If it fails, maybe it's not even a valid default branch locally, but we'll try to sync anyway
116
+ }
117
+
103
118
  await syncBranch();
104
119
 
105
- // Create or switch to issue branch
120
+ // 3. Create or switch to issue branch
106
121
  const branchName = issue ? `fix/issue-${issue}` : null;
107
122
  if (branchName) {
108
123
  try {
@@ -253,9 +268,36 @@ export async function submitPR(opts: { title: string, body?: string, issue?: num
253
268
  const repo = getRepo();
254
269
  const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
255
270
 
271
+ // Auto-infer issue from branch name (e.g. fix/issue-303)
272
+ let linkedIssue = opts.issue;
273
+ if (!linkedIssue) {
274
+ const match = branch.match(/issue-(\d+)/);
275
+ if (match) linkedIssue = parseInt(match[1]);
276
+ }
277
+
278
+ const octokit = getOctokit();
279
+
280
+ // 2. Fetch Issue Details for better PR Metadata
281
+ let issueTitle = '';
282
+ if (linkedIssue) {
283
+ spinner.text = `Fetching details for issue #${linkedIssue}...`;
284
+ try {
285
+ const { data: issueData } = await octokit.issues.get({
286
+ owner,
287
+ repo,
288
+ issue_number: linkedIssue,
289
+ });
290
+ issueTitle = issueData.title;
291
+ } catch (e) {
292
+ // Silently fallback if issue doesn't exist or no access
293
+ }
294
+ }
295
+
296
+ const prTitle = opts.title || (issueTitle ? `fix: ${issueTitle} (#${linkedIssue})` : 'Automated contribution via GitPadi');
297
+ const commitMsg = opts.message || prTitle;
298
+
256
299
  // 1. Stage and Commit
257
300
  spinner.text = 'Staging and committing changes...';
258
- const commitMsg = opts.message || opts.title || 'Automated contribution via GitPadi';
259
301
 
260
302
  try {
261
303
  execSync('git add .', { stdio: 'pipe' });
@@ -269,26 +311,27 @@ export async function submitPR(opts: { title: string, body?: string, issue?: num
269
311
  dim(' (Note: No new changes to commit or commit failed)');
270
312
  }
271
313
 
272
- // Auto-infer issue from branch name (e.g. fix/issue-303)
273
- let linkedIssue = opts.issue;
274
- if (!linkedIssue) {
275
- const match = branch.match(/issue-(\d+)/);
276
- if (match) linkedIssue = parseInt(match[1]);
277
- }
278
-
279
314
  spinner.text = 'Pushing to your fork...';
280
315
  execSync(`git push origin ${branch}`, { stdio: 'pipe' });
281
316
 
282
317
  spinner.text = 'Creating Pull Request...';
283
318
  const body = opts.body || (linkedIssue ? `Fixes #${linkedIssue}` : 'Automated PR via GitPadi');
284
319
 
285
- const { data: pr } = await getOctokit().pulls.create({
320
+ // Detect base branch
321
+ let baseBranch = 'main';
322
+ try {
323
+ execSync('git rev-parse origin/main', { stdio: 'pipe' });
324
+ } catch {
325
+ baseBranch = 'master';
326
+ }
327
+
328
+ const { data: pr } = await octokit.pulls.create({
286
329
  owner,
287
330
  repo,
288
- title: opts.title,
331
+ title: prTitle,
289
332
  body,
290
333
  head: `${await getAuthenticatedUser()}:${branch}`,
291
- base: 'main',
334
+ base: baseBranch,
292
335
  });
293
336
 
294
337
  spinner.succeed(`PR Created: ${green(pr.html_url)}`);
@@ -14,18 +14,34 @@ export async function listIssues(opts: { state?: string; labels?: string; limit?
14
14
  const spinner = ora(`Fetching issues from ${chalk.cyan(getFullRepo())}...`).start();
15
15
 
16
16
  try {
17
- const { data: issues } = await octokit.issues.listForRepo({
18
- owner: getOwner(), repo: getRepo(),
19
- state: (opts.state as 'open' | 'closed' | 'all') || 'open',
20
- labels: opts.labels || undefined,
21
- per_page: opts.limit || 50,
22
- });
17
+ const requestedLimit = opts.limit || 50;
18
+ const realIssues: any[] = [];
19
+ let page = 1;
20
+
21
+ // Fetch until we have enough real issues or run out of pages
22
+ while (realIssues.length < requestedLimit) {
23
+ const { data: issues } = await octokit.issues.listForRepo({
24
+ owner: getOwner(), repo: getRepo(),
25
+ state: (opts.state as 'open' | 'closed' | 'all') || 'open',
26
+ labels: opts.labels || undefined,
27
+ per_page: 100, // Fetch max per page to be efficient
28
+ page: page++,
29
+ });
30
+
31
+ if (issues.length === 0) break;
32
+
33
+ const batch = issues.filter((i) => !i.pull_request);
34
+ realIssues.push(...batch);
35
+
36
+ if (issues.length < 100) break; // Last page
37
+ }
38
+
39
+ // Clip to requested limit
40
+ const finalIssues = realIssues.slice(0, requestedLimit);
23
41
 
24
- // Filter out PRs (GitHub API returns PRs in issues endpoint)
25
- const realIssues = issues.filter((i) => !i.pull_request);
26
42
  spinner.stop();
27
43
 
28
- if (realIssues.length === 0) {
44
+ if (finalIssues.length === 0) {
29
45
  console.log(chalk.yellow('\n No issues found.\n'));
30
46
  return;
31
47
  }
@@ -35,14 +51,14 @@ export async function listIssues(opts: { state?: string; labels?: string; limit?
35
51
  style: { head: [], border: [] },
36
52
  });
37
53
 
38
- realIssues.forEach((i) => {
39
- const labels = i.labels.map((l) => typeof l === 'string' ? l : l.name || '').join(', ');
54
+ finalIssues.forEach((i) => {
55
+ const labels = i.labels.map((l: any) => typeof l === 'string' ? l : l.name || '').join(', ');
40
56
  const assignee = i.assignee?.login || chalk.dim('unassigned');
41
57
  const state = i.state === 'open' ? chalk.green('open') : chalk.red('closed');
42
58
  table.push([`#${i.number}`, i.title.substring(0, 60), labels.substring(0, 30), assignee, state]);
43
59
  });
44
60
 
45
- console.log(`\n${chalk.bold(`šŸ“‹ Issues — ${getFullRepo()}`)} (${realIssues.length})\n`);
61
+ console.log(`\n${chalk.bold(`šŸ“‹ Issues — ${getFullRepo()}`)} (${finalIssues.length})\n`);
46
62
  console.log(table.toString());
47
63
  console.log('');
48
64
  } catch (e: any) {
@@ -153,16 +169,25 @@ export async function createIssuesFromFile(filePath: string, opts: { dryRun?: bo
153
169
  console.log(`\n${chalk.bold('šŸ“‹ GitPadi Issue Creator')}`);
154
170
  console.log(chalk.dim(` Repo: ${getFullRepo()}`));
155
171
  console.log(chalk.dim(` File: ${filePath} (${detectedFormat})`));
156
- console.log(chalk.dim(` Range: #${start}-#${end} (${filtered.length} issues)`));
157
- console.log(chalk.dim(` Mode: ${opts.dryRun ? 'DRY RUN' : 'LIVE'}\n`));
158
-
159
- if (opts.dryRun) {
160
- filtered.forEach((i: any) => {
161
- console.log(` ${chalk.dim(`#${String(i.number).padStart(2, '0')}`)} ${i.title}`);
162
- console.log(chalk.dim(` [${(i.labels || []).join(', ')}]`));
163
- if (i.body) console.log(chalk.dim(` ${i.body.substring(0, 80)}...`));
164
- });
165
- console.log(chalk.green(`\nāœ… Dry run: ${filtered.length} issues would be created.\n`));
172
+ console.log(chalk.dim(` Found: ${filtered.length} issues\n`));
173
+
174
+ // Always preview first
175
+ filtered.forEach((i: any) => {
176
+ console.log(` ${chalk.dim(`#${String(i.number).padStart(2, '0')}`)} ${i.title}`);
177
+ console.log(chalk.dim(` [${(i.labels || []).join(', ')}]`));
178
+ });
179
+
180
+ // Ask for confirmation
181
+ const inquirer = (await import('inquirer')).default;
182
+ const { proceed } = await inquirer.prompt([{
183
+ type: 'confirm',
184
+ name: 'proceed',
185
+ message: chalk.yellow(`Create these ${filtered.length} issues on GitHub?`),
186
+ default: true,
187
+ }]);
188
+
189
+ if (!proceed) {
190
+ console.log(chalk.dim('\n Cancelled.\n'));
166
191
  return;
167
192
  }
168
193