gitpadi 2.0.0 → 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/README.md +4 -0
- package/dist/applicant-scorer.js +256 -0
- package/dist/cli.js +789 -0
- package/dist/commands/contribute.js +291 -0
- package/dist/commands/contributors.js +101 -0
- package/dist/commands/issues.js +319 -0
- package/dist/commands/prs.js +229 -0
- package/dist/commands/releases.js +53 -0
- package/dist/commands/repos.js +128 -0
- package/dist/core/github.js +106 -0
- package/dist/core/scorer.js +95 -0
- package/dist/create-issues.js +179 -0
- package/dist/pr-review.js +117 -0
- package/package.json +6 -3
- package/src/applicant-scorer.ts +1 -1
- package/src/cli.ts +345 -170
- package/src/commands/contribute.ts +331 -0
- package/src/commands/contributors.ts +1 -1
- package/src/commands/issues.ts +76 -9
- package/src/commands/prs.ts +1 -1
- package/src/commands/releases.ts +1 -1
- package/src/commands/repos.ts +41 -26
- package/src/core/github.ts +99 -15
- package/src/create-issues.ts +1 -1
- package/src/pr-review.ts +1 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// commands/contribute.ts — Contributor workflow for GitPadi
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import boxen from 'boxen';
|
|
7
|
+
import { getOctokit, getOwner, getRepo, forkRepo, getAuthenticatedUser, getLatestCheckRuns, setRepo } from '../core/github.js';
|
|
8
|
+
const dim = chalk.dim;
|
|
9
|
+
const yellow = chalk.yellow;
|
|
10
|
+
const green = chalk.green;
|
|
11
|
+
const cyan = chalk.cyan;
|
|
12
|
+
const white = chalk.white;
|
|
13
|
+
/**
|
|
14
|
+
* Parses a GitHub Issue/Repo URL or "owner/repo" string
|
|
15
|
+
*/
|
|
16
|
+
function parseTarget(input) {
|
|
17
|
+
const urlPattern = /github\.com\/([^/]+)\/([^/]+)(\/issues\/(\d+))?/;
|
|
18
|
+
const match = input.match(urlPattern);
|
|
19
|
+
if (match) {
|
|
20
|
+
return {
|
|
21
|
+
owner: match[1],
|
|
22
|
+
repo: match[2],
|
|
23
|
+
issue: match[4] ? parseInt(match[4]) : undefined
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const parts = input.split('/');
|
|
27
|
+
if (parts.length === 2) {
|
|
28
|
+
return { owner: parts[0], repo: parts[1] };
|
|
29
|
+
}
|
|
30
|
+
throw new Error('Invalid target. Use a GitHub URL or "owner/repo" format.');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Phase 2: Fork & Clone Workflow (The "Start" Command)
|
|
34
|
+
*/
|
|
35
|
+
export async function forkAndClone(target) {
|
|
36
|
+
const { owner, repo, issue } = parseTarget(target);
|
|
37
|
+
// 1. Fork first (idempotent — GitHub returns existing fork if it exists)
|
|
38
|
+
const spinner = ora(`Forking ${cyan(`${owner}/${repo}`)}...`).start();
|
|
39
|
+
let myUser;
|
|
40
|
+
try {
|
|
41
|
+
myUser = await getAuthenticatedUser();
|
|
42
|
+
const forkFullName = await forkRepo(owner, repo);
|
|
43
|
+
spinner.succeed(`Forked to ${green(forkFullName)}`);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
spinner.fail(e.message);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// 2. Ask where to clone
|
|
50
|
+
const inquirer = (await import('inquirer')).default;
|
|
51
|
+
const path = (await import('path')).default;
|
|
52
|
+
const os = (await import('os')).default;
|
|
53
|
+
const { parentDir } = await inquirer.prompt([{
|
|
54
|
+
type: 'input',
|
|
55
|
+
name: 'parentDir',
|
|
56
|
+
message: cyan('📂 Where to clone? (parent directory):'),
|
|
57
|
+
default: '.',
|
|
58
|
+
}]);
|
|
59
|
+
// Resolve ~ and build full path: parentDir/repoName
|
|
60
|
+
const resolvedParent = parentDir.startsWith('~')
|
|
61
|
+
? parentDir.replace('~', os.homedir())
|
|
62
|
+
: parentDir;
|
|
63
|
+
let cloneDir = path.resolve(resolvedParent, repo);
|
|
64
|
+
// 3. Handle existing directory
|
|
65
|
+
if (fs.existsSync(cloneDir)) {
|
|
66
|
+
// Check if it's already a valid git clone of this repo
|
|
67
|
+
let isValidClone = false;
|
|
68
|
+
try {
|
|
69
|
+
const remoteUrl = execSync('git remote get-url origin', { cwd: cloneDir, encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
70
|
+
if (remoteUrl.includes(repo))
|
|
71
|
+
isValidClone = true;
|
|
72
|
+
}
|
|
73
|
+
catch { /* not a git repo */ }
|
|
74
|
+
if (isValidClone) {
|
|
75
|
+
console.log(yellow(`\n 📂 "${cloneDir}" already contains a clone of ${repo}.`));
|
|
76
|
+
console.log(dim(' Syncing with upstream...\n'));
|
|
77
|
+
process.chdir(cloneDir);
|
|
78
|
+
setRepo(owner, repo);
|
|
79
|
+
// Ensure upstream remote exists
|
|
80
|
+
try {
|
|
81
|
+
execSync(`git remote add upstream https://github.com/${owner}/${repo}.git`, { stdio: 'pipe' });
|
|
82
|
+
}
|
|
83
|
+
catch { /* exists */ }
|
|
84
|
+
await syncBranch();
|
|
85
|
+
// Create or switch to issue branch
|
|
86
|
+
const branchName = issue ? `fix/issue-${issue}` : null;
|
|
87
|
+
if (branchName) {
|
|
88
|
+
try {
|
|
89
|
+
execSync(`git checkout -b ${branchName}`, { stdio: 'pipe' });
|
|
90
|
+
console.log(green(` ✔ Created branch ${branchName}`));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
execSync(`git checkout ${branchName}`, { stdio: 'pipe' });
|
|
94
|
+
console.log(green(` ✔ Switched to branch ${branchName}`));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
console.log(green('\n✨ Workspace ready!'));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Directory exists but isn't a valid clone — ask for new path
|
|
101
|
+
const { newDir } = await inquirer.prompt([{
|
|
102
|
+
type: 'input',
|
|
103
|
+
name: 'newDir',
|
|
104
|
+
message: yellow(`"${cloneDir}" already exists. Enter a different folder name:`),
|
|
105
|
+
default: `${repo}-contrib`,
|
|
106
|
+
}]);
|
|
107
|
+
cloneDir = path.resolve(resolvedParent, newDir);
|
|
108
|
+
}
|
|
109
|
+
// 4. Clone
|
|
110
|
+
const cloneSpinner = ora(`Cloning your fork...`).start();
|
|
111
|
+
const cloneUrl = `https://github.com/${myUser}/${repo}.git`;
|
|
112
|
+
try {
|
|
113
|
+
execSync(`git clone ${cloneUrl} ${cloneDir}`, { stdio: 'pipe' });
|
|
114
|
+
cloneSpinner.succeed(`Cloned into ${green(cloneDir)}`);
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
cloneSpinner.fail(`Clone failed: ${e.message}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// 5. Setup Remotes & Branch
|
|
121
|
+
process.chdir(cloneDir);
|
|
122
|
+
execSync(`git remote add upstream https://github.com/${owner}/${repo}.git`, { stdio: 'pipe' });
|
|
123
|
+
const branchName = issue ? `fix/issue-${issue}` : `contrib-${Math.floor(Date.now() / 1000)}`;
|
|
124
|
+
try {
|
|
125
|
+
execSync(`git checkout -b ${branchName}`, { stdio: 'pipe' });
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
execSync(`git checkout ${branchName}`, { stdio: 'pipe' });
|
|
129
|
+
}
|
|
130
|
+
// Update local GitPadi state
|
|
131
|
+
setRepo(owner, repo);
|
|
132
|
+
console.log(`\n${green('✨ Workspace Ready!')}`);
|
|
133
|
+
console.log(`${dim('Directory:')} ${cloneDir}`);
|
|
134
|
+
console.log(`${dim('Branch:')} ${branchName}`);
|
|
135
|
+
console.log(`${dim('Upstream:')} ${owner}/${repo}`);
|
|
136
|
+
console.log(boxen(green('Next step:\n') +
|
|
137
|
+
white(`cd ${cloneDir}\n\n`) +
|
|
138
|
+
dim('Start coding! When you\'re done, run ') + yellow('gitpadi submit'), { padding: 1, borderColor: 'green', borderStyle: 'round' }));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Phase 2: Sync Fork with Upstream (full flow)
|
|
142
|
+
* 1. Sync fork on GitHub (API)
|
|
143
|
+
* 2. Pull synced changes locally
|
|
144
|
+
* 3. Merge upstream into current branch
|
|
145
|
+
*/
|
|
146
|
+
export async function syncBranch() {
|
|
147
|
+
try {
|
|
148
|
+
// Check if we are in a git repo and if upstream exists
|
|
149
|
+
const remotes = execSync('git remote', { encoding: 'utf-8' });
|
|
150
|
+
if (!remotes.includes('upstream'))
|
|
151
|
+
return;
|
|
152
|
+
const spinner = ora('Syncing fork...').start();
|
|
153
|
+
// 1. Detect upstream default branch (main or master)
|
|
154
|
+
execSync('git fetch upstream', { stdio: 'pipe' });
|
|
155
|
+
let upstreamBranch = 'main';
|
|
156
|
+
try {
|
|
157
|
+
execSync('git rev-parse upstream/main', { stdio: 'pipe' });
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
upstreamBranch = 'master';
|
|
161
|
+
}
|
|
162
|
+
// 2. Sync fork on GitHub via API
|
|
163
|
+
spinner.text = 'Syncing fork on GitHub...';
|
|
164
|
+
try {
|
|
165
|
+
const octokit = getOctokit();
|
|
166
|
+
const myUser = await getAuthenticatedUser();
|
|
167
|
+
const repo = getRepo();
|
|
168
|
+
await octokit.request('POST /repos/{owner}/{repo}/merge-upstream', {
|
|
169
|
+
owner: myUser,
|
|
170
|
+
repo: repo,
|
|
171
|
+
branch: upstreamBranch,
|
|
172
|
+
});
|
|
173
|
+
spinner.succeed(green('Fork synced on GitHub ✓'));
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
// May fail if already in sync or permissions — continue anyway
|
|
177
|
+
spinner.info(dim('GitHub sync skipped (may already be in sync)'));
|
|
178
|
+
}
|
|
179
|
+
// 3. Pull synced changes locally
|
|
180
|
+
const pullSpinner = ora('Pulling latest changes...').start();
|
|
181
|
+
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
|
|
182
|
+
// Fetch origin (our fork) with the new synced data
|
|
183
|
+
execSync('git fetch origin', { stdio: 'pipe' });
|
|
184
|
+
execSync('git fetch upstream', { stdio: 'pipe' });
|
|
185
|
+
// If we're on main/master, just pull
|
|
186
|
+
if (currentBranch === upstreamBranch) {
|
|
187
|
+
try {
|
|
188
|
+
execSync(`git pull origin ${upstreamBranch} --no-edit`, { stdio: 'pipe' });
|
|
189
|
+
pullSpinner.succeed(green(`Pulled latest ${upstreamBranch} ✓`));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
pullSpinner.warn(yellow('Pull had conflicts — resolve manually'));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// We're on a feature branch — merge upstream into it
|
|
197
|
+
pullSpinner.text = `Merging upstream/${upstreamBranch} into ${currentBranch}...`;
|
|
198
|
+
try {
|
|
199
|
+
execSync(`git merge upstream/${upstreamBranch} --no-edit`, { stdio: 'pipe' });
|
|
200
|
+
pullSpinner.succeed(green(`Merged upstream/${upstreamBranch} into ${cyan(currentBranch)} ✓`));
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
pullSpinner.warn(yellow('Merge conflict detected!'));
|
|
204
|
+
console.log(dim(' Resolve conflicts, then run:'));
|
|
205
|
+
console.log(dim(' git add . && git commit'));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
console.log(green('\n✨ Fork is synced and up to date!'));
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
// Silently fail if git commands error (likely not in a repo)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Phase 2: Submit PR
|
|
216
|
+
*/
|
|
217
|
+
export async function submitPR(opts) {
|
|
218
|
+
const spinner = ora('Preparing submission...').start();
|
|
219
|
+
try {
|
|
220
|
+
const owner = getOwner();
|
|
221
|
+
const repo = getRepo();
|
|
222
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
|
|
223
|
+
// 1. Stage and Commit
|
|
224
|
+
spinner.text = 'Staging and committing changes...';
|
|
225
|
+
const commitMsg = opts.message || opts.title || 'Automated contribution via GitPadi';
|
|
226
|
+
try {
|
|
227
|
+
execSync('git add .', { stdio: 'pipe' });
|
|
228
|
+
// Check if there are changes to commit
|
|
229
|
+
const status = execSync('git status --porcelain', { encoding: 'utf-8' });
|
|
230
|
+
if (status.trim()) {
|
|
231
|
+
execSync(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { stdio: 'pipe' });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
// If commit fails (e.g. no changes), we might still want to push if there are unpushed commits
|
|
236
|
+
dim(' (Note: No new changes to commit or commit failed)');
|
|
237
|
+
}
|
|
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
|
+
spinner.text = 'Pushing to your fork...';
|
|
246
|
+
execSync(`git push origin ${branch}`, { stdio: 'pipe' });
|
|
247
|
+
spinner.text = 'Creating Pull Request...';
|
|
248
|
+
const body = opts.body || (linkedIssue ? `Fixes #${linkedIssue}` : 'Automated PR via GitPadi');
|
|
249
|
+
const { data: pr } = await getOctokit().pulls.create({
|
|
250
|
+
owner,
|
|
251
|
+
repo,
|
|
252
|
+
title: opts.title,
|
|
253
|
+
body,
|
|
254
|
+
head: `${await getAuthenticatedUser()}:${branch}`,
|
|
255
|
+
base: 'main',
|
|
256
|
+
});
|
|
257
|
+
spinner.succeed(`PR Created: ${green(pr.html_url)}`);
|
|
258
|
+
}
|
|
259
|
+
catch (e) {
|
|
260
|
+
spinner.fail(e.message);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Phase 2: View Logs
|
|
265
|
+
*/
|
|
266
|
+
export async function viewLogs() {
|
|
267
|
+
const spinner = ora('Fetching GitHub Action logs...').start();
|
|
268
|
+
try {
|
|
269
|
+
const owner = getOwner();
|
|
270
|
+
const repo = getRepo();
|
|
271
|
+
const sha = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim();
|
|
272
|
+
const { checkRuns, combinedState } = await getLatestCheckRuns(owner, repo, sha);
|
|
273
|
+
spinner.stop();
|
|
274
|
+
if (checkRuns.length === 0) {
|
|
275
|
+
console.log(dim('\n ℹ️ No active check runs found for this commit.\n'));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
console.log(`\n${chalk.bold(`📋 GitHub Actions status:`)} ${combinedState === 'success' ? green('✅ Success') : combinedState === 'failure' ? chalk.red('❌ Failure') : yellow('⏳ Pending')}\n`);
|
|
279
|
+
checkRuns.forEach(run => {
|
|
280
|
+
const icon = run.status === 'completed' ? (run.conclusion === 'success' ? green('✅') : chalk.red('❌')) : yellow('⏳');
|
|
281
|
+
console.log(` ${icon} ${chalk.bold(run.name)}: ${dim(run.conclusion || run.status)}`);
|
|
282
|
+
if (run.conclusion === 'failure') {
|
|
283
|
+
console.log(chalk.red(` → Build failed. View details at: ${run.html_url}`));
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
console.log('');
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
spinner.fail(e.message);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// commands/contributors.ts — Contributor management commands for GitPadi
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import Table from 'cli-table3';
|
|
5
|
+
import { getOctokit, getOwner, getRepo, getFullRepo, requireRepo } from '../core/github.js';
|
|
6
|
+
import { fetchProfile, scoreApplicant, TIER_EMOJI } from '../core/scorer.js';
|
|
7
|
+
export async function scoreUser(username) {
|
|
8
|
+
requireRepo();
|
|
9
|
+
const spinner = ora(`Scoring @${username}...`).start();
|
|
10
|
+
try {
|
|
11
|
+
const profile = await fetchProfile(username);
|
|
12
|
+
const scored = scoreApplicant(profile);
|
|
13
|
+
spinner.stop();
|
|
14
|
+
console.log(`\n${chalk.bold(`🏆 Score: @${username}`)} — ${TIER_EMOJI[scored.tier]} Tier ${scored.tier} (${scored.score}/100)\n`);
|
|
15
|
+
const table = new Table({ style: { head: [], border: [] } });
|
|
16
|
+
table.push({ [chalk.cyan('🏛️ Account Maturity')]: `${scored.breakdown.accountMaturity}/15` }, { [chalk.cyan('🔧 Repo Experience')]: `${scored.breakdown.repoExperience}/30` }, { [chalk.cyan('🌐 GitHub Presence')]: `${scored.breakdown.githubPresence}/15` }, { [chalk.cyan('⚡ Activity Level')]: `${scored.breakdown.activityLevel}/15` }, { [chalk.cyan('📝 Application')]: `${scored.breakdown.applicationQuality}/15` }, { [chalk.cyan('💻 Languages')]: `${scored.breakdown.languageRelevance}/10` }, { [chalk.bold('Total')]: chalk.bold(`${scored.score}/100`) });
|
|
17
|
+
console.log(table.toString());
|
|
18
|
+
console.log(`\n ${chalk.dim('Account age:')} ${Math.round(scored.accountAge / 30)} months`);
|
|
19
|
+
console.log(` ${chalk.dim('Public repos:')} ${scored.publicRepos}`);
|
|
20
|
+
console.log(` ${chalk.dim('Followers:')} ${scored.followers}`);
|
|
21
|
+
console.log(` ${chalk.dim('Languages:')} ${scored.relevantLanguages.slice(0, 8).join(', ') || 'None detected'}`);
|
|
22
|
+
console.log(` ${chalk.dim('Repo PRs:')} ${scored.prsMerged} merged, ${scored.prsOpen} open`);
|
|
23
|
+
console.log('');
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
spinner.fail(e.message);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function rankApplicants(issueNumber) {
|
|
30
|
+
requireRepo();
|
|
31
|
+
const octokit = getOctokit();
|
|
32
|
+
const spinner = ora(`Finding applicants for #${issueNumber}...`).start();
|
|
33
|
+
try {
|
|
34
|
+
const { data: issue } = await octokit.issues.get({ owner: getOwner(), repo: getRepo(), issue_number: issueNumber });
|
|
35
|
+
const { data: comments } = await octokit.issues.listComments({ owner: getOwner(), repo: getRepo(), issue_number: issueNumber, per_page: 100 });
|
|
36
|
+
const labels = issue.labels.map((l) => typeof l === 'string' ? l : l.name || '').filter(Boolean);
|
|
37
|
+
const { isApplicationComment } = await import('../core/scorer.js');
|
|
38
|
+
const apps = comments.filter((c) => c.body && isApplicationComment(c.body) && c.user?.login !== 'github-actions[bot]');
|
|
39
|
+
const byUser = new Map();
|
|
40
|
+
apps.forEach((c) => { if (c.user?.login)
|
|
41
|
+
byUser.set(c.user.login, c.body || ''); });
|
|
42
|
+
if (byUser.size === 0) {
|
|
43
|
+
spinner.warn('No applicants found.');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
spinner.text = `Scoring ${byUser.size} applicant(s)...`;
|
|
47
|
+
const scored = [];
|
|
48
|
+
for (const [user, body] of byUser) {
|
|
49
|
+
const profile = await fetchProfile(user, body);
|
|
50
|
+
scored.push(scoreApplicant(profile, labels));
|
|
51
|
+
}
|
|
52
|
+
scored.sort((a, b) => b.score - a.score);
|
|
53
|
+
spinner.stop();
|
|
54
|
+
const table = new Table({
|
|
55
|
+
head: ['Rank', 'User', 'Tier', 'Score', 'PRs', 'Activity', 'Languages'].map((h) => chalk.cyan(h)),
|
|
56
|
+
style: { head: [], border: [] },
|
|
57
|
+
});
|
|
58
|
+
scored.forEach((s, i) => {
|
|
59
|
+
const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : `${i + 1}.`;
|
|
60
|
+
table.push([medal, `@${s.username}`, `${TIER_EMOJI[s.tier]} ${s.tier}`, `${s.score}/100`, `${s.prsMerged}m/${s.prsOpen}o`, `${s.totalContributions}`, s.relevantLanguages.slice(0, 3).join(', ') || '-']);
|
|
61
|
+
});
|
|
62
|
+
console.log(`\n${chalk.bold(`🏆 Applicant Rankings — Issue #${issueNumber}`)}\n`);
|
|
63
|
+
console.log(table.toString());
|
|
64
|
+
if (scored.length >= 2) {
|
|
65
|
+
const gap = scored[0].score - scored[1].score;
|
|
66
|
+
if (gap >= 20)
|
|
67
|
+
console.log(chalk.green(`\n → Clear winner: @${scored[0].username} (+${gap} pts)`));
|
|
68
|
+
else if (gap >= 5)
|
|
69
|
+
console.log(chalk.green(`\n → Top pick: @${scored[0].username} (+${gap} pts)`));
|
|
70
|
+
else
|
|
71
|
+
console.log(chalk.yellow(`\n → Close match — review manually`));
|
|
72
|
+
}
|
|
73
|
+
console.log('');
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
spinner.fail(e.message);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function listContributors(opts) {
|
|
80
|
+
requireRepo();
|
|
81
|
+
const spinner = ora('Fetching contributors...').start();
|
|
82
|
+
try {
|
|
83
|
+
const { data } = await getOctokit().repos.listContributors({
|
|
84
|
+
owner: getOwner(), repo: getRepo(), per_page: opts.limit || 50,
|
|
85
|
+
});
|
|
86
|
+
spinner.stop();
|
|
87
|
+
const table = new Table({
|
|
88
|
+
head: ['#', 'User', 'Contributions'].map((h) => chalk.cyan(h)),
|
|
89
|
+
style: { head: [], border: [] },
|
|
90
|
+
});
|
|
91
|
+
data.forEach((c, i) => {
|
|
92
|
+
table.push([`${i + 1}.`, `@${c.login || '?'}`, `${c.contributions}`]);
|
|
93
|
+
});
|
|
94
|
+
console.log(`\n${chalk.bold(`👥 Contributors — ${getFullRepo()}`)} (${data.length})\n`);
|
|
95
|
+
console.log(table.toString());
|
|
96
|
+
console.log('');
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
spinner.fail(e.message);
|
|
100
|
+
}
|
|
101
|
+
}
|