gitpadi 2.1.9 → 2.2.0
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 +18 -0
- package/dist/commands/prs.js +107 -0
- package/package.json +1 -1
- package/src/cli.ts +16 -0
- package/src/commands/prs.ts +95 -0
package/dist/cli.js
CHANGED
|
@@ -1551,6 +1551,8 @@ async function prMenu() {
|
|
|
1551
1551
|
{ name: ` ${green('▸')} List pull requests`, value: 'list' },
|
|
1552
1552
|
{ name: ` ${green('▸')} Merge PR ${dim('(checks CI first)')}`, value: 'merge' },
|
|
1553
1553
|
{ name: ` ${yellow('▸')} Force merge ${dim('(skip CI checks)')}`, value: 'force-merge' },
|
|
1554
|
+
{ name: ` ${green('▸')} Merge ALL open PRs ${dim('(checks CI per PR)')}`, value: 'merge-all' },
|
|
1555
|
+
{ name: ` ${yellow('▸')} Force merge ALL open PRs ${dim('(skip CI)')}`, value: 'force-merge-all' },
|
|
1554
1556
|
{ name: ` ${red('▸')} Close PR`, value: 'close' },
|
|
1555
1557
|
{ name: ` ${yellow('▸')} Auto-review PR`, value: 'review' },
|
|
1556
1558
|
{ name: ` ${green('▸')} Approve PR`, value: 'approve' },
|
|
@@ -1566,6 +1568,20 @@ async function prMenu() {
|
|
|
1566
1568
|
]);
|
|
1567
1569
|
await prs.listPRs({ state: a.state, limit: parseInt(a.limit) });
|
|
1568
1570
|
}
|
|
1571
|
+
else if (action === 'merge-all' || action === 'force-merge-all') {
|
|
1572
|
+
const a = await ask([
|
|
1573
|
+
{
|
|
1574
|
+
type: 'list', name: 'method', message: 'Merge method:', choices: [
|
|
1575
|
+
{ name: `${green('squash')} ${dim('— clean single commit')}`, value: 'squash' },
|
|
1576
|
+
{ name: `${cyan('merge')} ${dim('— preserve all commits')}`, value: 'merge' },
|
|
1577
|
+
{ name: `${yellow('rebase')} ${dim('— linear history')}`, value: 'rebase' },
|
|
1578
|
+
]
|
|
1579
|
+
},
|
|
1580
|
+
{ type: 'confirm', name: 'confirm', message: action === 'force-merge-all' ? yellow('⚠️ Force merge ALL open PRs (skip CI)?') : red('⚠️ Merge ALL open PRs?'), default: false },
|
|
1581
|
+
]);
|
|
1582
|
+
if (a.confirm)
|
|
1583
|
+
await prs.mergeAllPRs({ method: a.method, force: action === 'force-merge-all' });
|
|
1584
|
+
}
|
|
1569
1585
|
else if (action === 'merge' || action === 'force-merge') {
|
|
1570
1586
|
const a = await ask([
|
|
1571
1587
|
{ type: 'input', name: 'n', message: yellow('PR #') + dim(' (q=back):') },
|
|
@@ -1809,6 +1825,8 @@ function setupCommander() {
|
|
|
1809
1825
|
.action(async (o) => { await prs.listPRs({ state: o.state, limit: parseInt(o.limit) }); });
|
|
1810
1826
|
p.command('merge <n>').option('-m, --method <m>', '', 'squash').option('--message <msg>').option('-f, --force', 'Skip CI checks')
|
|
1811
1827
|
.action(async (n, o) => { await prs.mergePR(parseInt(n), o); });
|
|
1828
|
+
p.command('merge-all').option('-m, --method <m>', '', 'squash').option('-f, --force', 'Skip CI checks')
|
|
1829
|
+
.action(async (o) => { await prs.mergeAllPRs(o); });
|
|
1812
1830
|
p.command('close <n>').action(async (n) => { await prs.closePR(parseInt(n)); });
|
|
1813
1831
|
p.command('review <n>').action(async (n) => { await prs.reviewPR(parseInt(n)); });
|
|
1814
1832
|
p.command('approve <n>').action(async (n) => { await prs.approvePR(parseInt(n)); });
|
package/dist/commands/prs.js
CHANGED
|
@@ -142,6 +142,113 @@ export async function mergePR(number, opts) {
|
|
|
142
142
|
console.error(chalk.red(` ❌ ${e.message}`));
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
export async function mergeAllPRs(opts) {
|
|
146
|
+
requireRepo();
|
|
147
|
+
const octokit = getOctokit();
|
|
148
|
+
const method = (opts.method || 'squash');
|
|
149
|
+
const spinner = ora(`Fetching open PRs from ${chalk.cyan(getFullRepo())}...`).start();
|
|
150
|
+
let openPRs = [];
|
|
151
|
+
try {
|
|
152
|
+
const { data } = await octokit.pulls.list({ owner: getOwner(), repo: getRepo(), state: 'open', per_page: 100 });
|
|
153
|
+
openPRs = data;
|
|
154
|
+
spinner.stop();
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
spinner.fail(e.message);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (openPRs.length === 0) {
|
|
161
|
+
console.log(chalk.yellow('\n No open PRs found.\n'));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
console.log(`\n${chalk.bold(`🔀 Merge All Open PRs — ${getFullRepo()}`)} (${openPRs.length} PRs)\n`);
|
|
165
|
+
const results = [];
|
|
166
|
+
for (const pr of openPRs) {
|
|
167
|
+
console.log(chalk.dim(` ─── PR #${pr.number}: ${pr.title.substring(0, 60)} ───`));
|
|
168
|
+
try {
|
|
169
|
+
if (opts.force) {
|
|
170
|
+
const s = ora(` Force merging #${pr.number}...`).start();
|
|
171
|
+
const { data } = await octokit.pulls.merge({
|
|
172
|
+
owner: getOwner(), repo: getRepo(), pull_number: pr.number,
|
|
173
|
+
merge_method: method,
|
|
174
|
+
});
|
|
175
|
+
if (data.merged) {
|
|
176
|
+
s.succeed(` Merged #${pr.number} ${chalk.yellow('(CI skipped)')}`);
|
|
177
|
+
results.push({ number: pr.number, title: pr.title, status: 'merged' });
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
s.fail(` Could not merge #${pr.number}: ${data.message}`);
|
|
181
|
+
results.push({ number: pr.number, title: pr.title, status: 'failed', reason: data.message });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
// Check CI
|
|
186
|
+
const s = ora(` Checking CI for #${pr.number}...`).start();
|
|
187
|
+
const sha = pr.head.sha;
|
|
188
|
+
let attempts = 0;
|
|
189
|
+
let allComplete = false;
|
|
190
|
+
let ciChecks = [];
|
|
191
|
+
while (!allComplete && attempts < 90) {
|
|
192
|
+
const { data: checkRuns } = await octokit.checks.listForRef({ owner: getOwner(), repo: getRepo(), ref: sha, per_page: 100 });
|
|
193
|
+
const { data: statusData } = await octokit.repos.getCombinedStatusForRef({ owner: getOwner(), repo: getRepo(), ref: sha });
|
|
194
|
+
ciChecks = checkRuns.check_runs.map((c) => ({ name: c.name, status: c.status, conclusion: c.conclusion }));
|
|
195
|
+
statusData.statuses.forEach((st) => {
|
|
196
|
+
if (!ciChecks.find((c) => c.name === st.context))
|
|
197
|
+
ciChecks.push({ name: st.context, status: st.state === 'pending' ? 'in_progress' : 'completed', conclusion: st.state === 'pending' ? null : st.state });
|
|
198
|
+
});
|
|
199
|
+
if (ciChecks.length === 0)
|
|
200
|
+
break;
|
|
201
|
+
allComplete = ciChecks.every((c) => c.status === 'completed');
|
|
202
|
+
if (!allComplete) {
|
|
203
|
+
s.text = chalk.dim(` Waiting for CI on #${pr.number}... (${attempts * 10}s)`);
|
|
204
|
+
await new Promise((r) => setTimeout(r, 10000));
|
|
205
|
+
attempts++;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
s.stop();
|
|
209
|
+
if (!allComplete) {
|
|
210
|
+
console.log(chalk.yellow(` ⚠️ CI still running for #${pr.number} after 15 min — skipping.\n`));
|
|
211
|
+
results.push({ number: pr.number, title: pr.title, status: 'skipped', reason: 'CI timeout' });
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const allPassed = ciChecks.length === 0 || ciChecks.every((c) => c.conclusion === 'success' || c.conclusion === 'neutral' || c.conclusion === 'skipped');
|
|
215
|
+
if (!allPassed) {
|
|
216
|
+
console.log(chalk.red(` ❌ CI failed for #${pr.number} — skipping.\n`));
|
|
217
|
+
results.push({ number: pr.number, title: pr.title, status: 'skipped', reason: 'CI failed' });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const ms = ora(` Merging #${pr.number}...`).start();
|
|
221
|
+
const { data } = await octokit.pulls.merge({ owner: getOwner(), repo: getRepo(), pull_number: pr.number, merge_method: method });
|
|
222
|
+
if (data.merged) {
|
|
223
|
+
ms.succeed(` Merged #${pr.number}`);
|
|
224
|
+
results.push({ number: pr.number, title: pr.title, status: 'merged' });
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
ms.fail(` Could not merge #${pr.number}: ${data.message}`);
|
|
228
|
+
results.push({ number: pr.number, title: pr.title, status: 'failed', reason: data.message });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
console.error(chalk.red(` ❌ #${pr.number}: ${e.message}`));
|
|
234
|
+
results.push({ number: pr.number, title: pr.title, status: 'failed', reason: e.message });
|
|
235
|
+
}
|
|
236
|
+
console.log('');
|
|
237
|
+
}
|
|
238
|
+
// Summary table
|
|
239
|
+
const table = new Table({
|
|
240
|
+
head: ['#', 'Title', 'Result'].map((h) => chalk.cyan(h)),
|
|
241
|
+
style: { head: [], border: [] },
|
|
242
|
+
});
|
|
243
|
+
results.forEach((r) => {
|
|
244
|
+
const icon = r.status === 'merged' ? chalk.green('✅ merged') : r.status === 'skipped' ? chalk.yellow(`⏭️ skipped${r.reason ? ` (${r.reason})` : ''}`) : chalk.red(`❌ failed${r.reason ? ` (${r.reason})` : ''}`);
|
|
245
|
+
table.push([`#${r.number}`, r.title.substring(0, 50), icon]);
|
|
246
|
+
});
|
|
247
|
+
const merged = results.filter((r) => r.status === 'merged').length;
|
|
248
|
+
console.log(`${chalk.bold('📊 Summary')}\n`);
|
|
249
|
+
console.log(table.toString());
|
|
250
|
+
console.log(`\n ${chalk.green(`${merged} merged`)}, ${chalk.yellow(`${results.filter((r) => r.status === 'skipped').length} skipped`)}, ${chalk.red(`${results.filter((r) => r.status === 'failed').length} failed`)}\n`);
|
|
251
|
+
}
|
|
145
252
|
export async function closePR(number) {
|
|
146
253
|
requireRepo();
|
|
147
254
|
const spinner = ora(`Closing PR #${number}...`).start();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitpadi",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "GitPadi — AI-powered GitHub & GitLab management CLI. Fork repos, manage issues & PRs, score contributors, grade assignments, and automate everything. Powered by Anthropic Claude via GitLab Duo Agent Platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cli.ts
CHANGED
|
@@ -1618,6 +1618,8 @@ async function prMenu() {
|
|
|
1618
1618
|
{ name: ` ${green('▸')} List pull requests`, value: 'list' },
|
|
1619
1619
|
{ name: ` ${green('▸')} Merge PR ${dim('(checks CI first)')}`, value: 'merge' },
|
|
1620
1620
|
{ name: ` ${yellow('▸')} Force merge ${dim('(skip CI checks)')}`, value: 'force-merge' },
|
|
1621
|
+
{ name: ` ${green('▸')} Merge ALL open PRs ${dim('(checks CI per PR)')}`, value: 'merge-all' },
|
|
1622
|
+
{ name: ` ${yellow('▸')} Force merge ALL open PRs ${dim('(skip CI)')}`, value: 'force-merge-all' },
|
|
1621
1623
|
{ name: ` ${red('▸')} Close PR`, value: 'close' },
|
|
1622
1624
|
{ name: ` ${yellow('▸')} Auto-review PR`, value: 'review' },
|
|
1623
1625
|
{ name: ` ${green('▸')} Approve PR`, value: 'approve' },
|
|
@@ -1633,6 +1635,18 @@ async function prMenu() {
|
|
|
1633
1635
|
{ type: 'input', name: 'limit', message: dim('Max results:'), default: '50' },
|
|
1634
1636
|
]);
|
|
1635
1637
|
await prs.listPRs({ state: a.state, limit: parseInt(a.limit) });
|
|
1638
|
+
} else if (action === 'merge-all' || action === 'force-merge-all') {
|
|
1639
|
+
const a = await ask([
|
|
1640
|
+
{
|
|
1641
|
+
type: 'list', name: 'method', message: 'Merge method:', choices: [
|
|
1642
|
+
{ name: `${green('squash')} ${dim('— clean single commit')}`, value: 'squash' },
|
|
1643
|
+
{ name: `${cyan('merge')} ${dim('— preserve all commits')}`, value: 'merge' },
|
|
1644
|
+
{ name: `${yellow('rebase')} ${dim('— linear history')}`, value: 'rebase' },
|
|
1645
|
+
]
|
|
1646
|
+
},
|
|
1647
|
+
{ type: 'confirm', name: 'confirm', message: action === 'force-merge-all' ? yellow('⚠️ Force merge ALL open PRs (skip CI)?') : red('⚠️ Merge ALL open PRs?'), default: false },
|
|
1648
|
+
]);
|
|
1649
|
+
if (a.confirm) await prs.mergeAllPRs({ method: a.method, force: action === 'force-merge-all' });
|
|
1636
1650
|
} else if (action === 'merge' || action === 'force-merge') {
|
|
1637
1651
|
const a = await ask([
|
|
1638
1652
|
{ type: 'input', name: 'n', message: yellow('PR #') + dim(' (q=back):') },
|
|
@@ -1872,6 +1886,8 @@ function setupCommander(): Command {
|
|
|
1872
1886
|
.action(async (o) => { await prs.listPRs({ state: o.state, limit: parseInt(o.limit) }); });
|
|
1873
1887
|
p.command('merge <n>').option('-m, --method <m>', '', 'squash').option('--message <msg>').option('-f, --force', 'Skip CI checks')
|
|
1874
1888
|
.action(async (n, o) => { await prs.mergePR(parseInt(n), o); });
|
|
1889
|
+
p.command('merge-all').option('-m, --method <m>', '', 'squash').option('-f, --force', 'Skip CI checks')
|
|
1890
|
+
.action(async (o) => { await prs.mergeAllPRs(o); });
|
|
1875
1891
|
p.command('close <n>').action(async (n) => { await prs.closePR(parseInt(n)); });
|
|
1876
1892
|
p.command('review <n>').action(async (n) => { await prs.reviewPR(parseInt(n)); });
|
|
1877
1893
|
p.command('approve <n>').action(async (n) => { await prs.approvePR(parseInt(n)); });
|
package/src/commands/prs.ts
CHANGED
|
@@ -152,6 +152,101 @@ export async function mergePR(number: number, opts: { method?: string; message?:
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
|
|
155
|
+
export async function mergeAllPRs(opts: { method?: string; force?: boolean }) {
|
|
156
|
+
requireRepo();
|
|
157
|
+
const octokit = getOctokit();
|
|
158
|
+
const method = (opts.method || 'squash') as 'merge' | 'squash' | 'rebase';
|
|
159
|
+
|
|
160
|
+
const spinner = ora(`Fetching open PRs from ${chalk.cyan(getFullRepo())}...`).start();
|
|
161
|
+
let openPRs: any[] = [];
|
|
162
|
+
try {
|
|
163
|
+
const { data } = await octokit.pulls.list({ owner: getOwner(), repo: getRepo(), state: 'open', per_page: 100 });
|
|
164
|
+
openPRs = data;
|
|
165
|
+
spinner.stop();
|
|
166
|
+
} catch (e: any) { spinner.fail(e.message); return; }
|
|
167
|
+
|
|
168
|
+
if (openPRs.length === 0) { console.log(chalk.yellow('\n No open PRs found.\n')); return; }
|
|
169
|
+
|
|
170
|
+
console.log(`\n${chalk.bold(`🔀 Merge All Open PRs — ${getFullRepo()}`)} (${openPRs.length} PRs)\n`);
|
|
171
|
+
|
|
172
|
+
const results: Array<{ number: number; title: string; status: 'merged' | 'failed' | 'skipped'; reason?: string }> = [];
|
|
173
|
+
|
|
174
|
+
for (const pr of openPRs) {
|
|
175
|
+
console.log(chalk.dim(` ─── PR #${pr.number}: ${pr.title.substring(0, 60)} ───`));
|
|
176
|
+
try {
|
|
177
|
+
if (opts.force) {
|
|
178
|
+
const s = ora(` Force merging #${pr.number}...`).start();
|
|
179
|
+
const { data } = await octokit.pulls.merge({
|
|
180
|
+
owner: getOwner(), repo: getRepo(), pull_number: pr.number,
|
|
181
|
+
merge_method: method,
|
|
182
|
+
});
|
|
183
|
+
if (data.merged) { s.succeed(` Merged #${pr.number} ${chalk.yellow('(CI skipped)')}`); results.push({ number: pr.number, title: pr.title, status: 'merged' }); }
|
|
184
|
+
else { s.fail(` Could not merge #${pr.number}: ${data.message}`); results.push({ number: pr.number, title: pr.title, status: 'failed', reason: data.message }); }
|
|
185
|
+
} else {
|
|
186
|
+
// Check CI
|
|
187
|
+
const s = ora(` Checking CI for #${pr.number}...`).start();
|
|
188
|
+
const sha = pr.head.sha;
|
|
189
|
+
let attempts = 0;
|
|
190
|
+
let allComplete = false;
|
|
191
|
+
let ciChecks: Array<{ name: string; status: string; conclusion: string | null }> = [];
|
|
192
|
+
|
|
193
|
+
while (!allComplete && attempts < 90) {
|
|
194
|
+
const { data: checkRuns } = await octokit.checks.listForRef({ owner: getOwner(), repo: getRepo(), ref: sha, per_page: 100 });
|
|
195
|
+
const { data: statusData } = await octokit.repos.getCombinedStatusForRef({ owner: getOwner(), repo: getRepo(), ref: sha });
|
|
196
|
+
ciChecks = checkRuns.check_runs.map((c) => ({ name: c.name, status: c.status, conclusion: c.conclusion }));
|
|
197
|
+
statusData.statuses.forEach((st) => {
|
|
198
|
+
if (!ciChecks.find((c) => c.name === st.context))
|
|
199
|
+
ciChecks.push({ name: st.context, status: st.state === 'pending' ? 'in_progress' : 'completed', conclusion: st.state === 'pending' ? null : st.state });
|
|
200
|
+
});
|
|
201
|
+
if (ciChecks.length === 0) break;
|
|
202
|
+
allComplete = ciChecks.every((c) => c.status === 'completed');
|
|
203
|
+
if (!allComplete) { s.text = chalk.dim(` Waiting for CI on #${pr.number}... (${attempts * 10}s)`); await new Promise((r) => setTimeout(r, 10000)); attempts++; }
|
|
204
|
+
}
|
|
205
|
+
s.stop();
|
|
206
|
+
|
|
207
|
+
if (!allComplete) {
|
|
208
|
+
console.log(chalk.yellow(` ⚠️ CI still running for #${pr.number} after 15 min — skipping.\n`));
|
|
209
|
+
results.push({ number: pr.number, title: pr.title, status: 'skipped', reason: 'CI timeout' });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const allPassed = ciChecks.length === 0 || ciChecks.every((c) =>
|
|
214
|
+
c.conclusion === 'success' || c.conclusion === 'neutral' || c.conclusion === 'skipped');
|
|
215
|
+
|
|
216
|
+
if (!allPassed) {
|
|
217
|
+
console.log(chalk.red(` ❌ CI failed for #${pr.number} — skipping.\n`));
|
|
218
|
+
results.push({ number: pr.number, title: pr.title, status: 'skipped', reason: 'CI failed' });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const ms = ora(` Merging #${pr.number}...`).start();
|
|
223
|
+
const { data } = await octokit.pulls.merge({ owner: getOwner(), repo: getRepo(), pull_number: pr.number, merge_method: method });
|
|
224
|
+
if (data.merged) { ms.succeed(` Merged #${pr.number}`); results.push({ number: pr.number, title: pr.title, status: 'merged' }); }
|
|
225
|
+
else { ms.fail(` Could not merge #${pr.number}: ${data.message}`); results.push({ number: pr.number, title: pr.title, status: 'failed', reason: data.message }); }
|
|
226
|
+
}
|
|
227
|
+
} catch (e: any) {
|
|
228
|
+
console.error(chalk.red(` ❌ #${pr.number}: ${e.message}`));
|
|
229
|
+
results.push({ number: pr.number, title: pr.title, status: 'failed', reason: e.message });
|
|
230
|
+
}
|
|
231
|
+
console.log('');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Summary table
|
|
235
|
+
const table = new Table({
|
|
236
|
+
head: ['#', 'Title', 'Result'].map((h) => chalk.cyan(h)),
|
|
237
|
+
style: { head: [], border: [] },
|
|
238
|
+
});
|
|
239
|
+
results.forEach((r) => {
|
|
240
|
+
const icon = r.status === 'merged' ? chalk.green('✅ merged') : r.status === 'skipped' ? chalk.yellow(`⏭️ skipped${r.reason ? ` (${r.reason})` : ''}`) : chalk.red(`❌ failed${r.reason ? ` (${r.reason})` : ''}`);
|
|
241
|
+
table.push([`#${r.number}`, r.title.substring(0, 50), icon]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const merged = results.filter((r) => r.status === 'merged').length;
|
|
245
|
+
console.log(`${chalk.bold('📊 Summary')}\n`);
|
|
246
|
+
console.log(table.toString());
|
|
247
|
+
console.log(`\n ${chalk.green(`${merged} merged`)}, ${chalk.yellow(`${results.filter((r) => r.status === 'skipped').length} skipped`)}, ${chalk.red(`${results.filter((r) => r.status === 'failed').length} failed`)}\n`);
|
|
248
|
+
}
|
|
249
|
+
|
|
155
250
|
export async function closePR(number: number) {
|
|
156
251
|
requireRepo();
|
|
157
252
|
const spinner = ora(`Closing PR #${number}...`).start();
|