gitpadi 2.0.5 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import { execSync } from 'child_process';
12
12
  import boxen from 'boxen';
13
13
  import { createSpinner } from 'nanospinner';
14
14
  import { Octokit } from '@octokit/rest';
15
- import { initGitHub, setRepo, getOwner, getRepo, getOctokit, saveConfig, getToken } from './core/github.js';
15
+ import { initGitHub, setRepo, getOwner, getRepo, getOctokit, saveConfig, getToken, getAuthenticatedUser, getRepoPermissions } from './core/github.js';
16
16
  import * as issues from './commands/issues.js';
17
17
  import * as prs from './commands/prs.js';
18
18
  import * as repos from './commands/repos.js';
@@ -20,6 +20,7 @@ import * as contributors from './commands/contributors.js';
20
20
  import * as releases from './commands/releases.js';
21
21
  import * as contribute from './commands/contribute.js';
22
22
  const VERSION = '2.0.0';
23
+ let targetConfirmed = false;
23
24
  // ── Styling ────────────────────────────────────────────────────────────
24
25
  const cyber = gradient(['#ff00ff', '#00ffff', '#ff00ff']);
25
26
  const neon = gradient(['#00ff87', '#60efff']);
@@ -157,35 +158,102 @@ async function ensureAuthenticated() {
157
158
  /**
158
159
  * Ensures we have a target repository (Owner/Repo)
159
160
  */
160
- async function ensureTargetRepo() {
161
+ async function ensureTargetRepo(force = false) {
161
162
  let owner = getOwner();
162
163
  let repo = getRepo();
163
- if (owner && repo) {
164
- return;
164
+ if (!force && owner && repo) {
165
+ if (targetConfirmed)
166
+ return;
167
+ const { confirm } = await inquirer.prompt([{
168
+ type: 'confirm',
169
+ name: 'confirm',
170
+ message: cyan('🎯 Targeting ') + bold(`${owner}/${repo}`) + cyan('. Correct?'),
171
+ default: true
172
+ }]);
173
+ if (confirm) {
174
+ targetConfirmed = true;
175
+ return;
176
+ }
165
177
  }
166
178
  console.log(neon('\n 📦 Project Targeting — which repo are we working on?\n'));
167
- const answers = await inquirer.prompt([
168
- {
179
+ // 1. Get the Owner/Org
180
+ const { targetOwner } = await inquirer.prompt([{
169
181
  type: 'input',
170
- name: 'owner',
182
+ name: 'targetOwner',
171
183
  message: cyan('👤 GitHub Owner/Org:'),
172
- default: owner || '',
173
- validate: (v) => v.length > 0 || 'Required',
174
- },
175
- {
176
- type: 'input',
177
- name: 'repo',
178
- message: cyan('📦 Repository name:'),
179
- default: repo || '',
184
+ default: owner || await getAuthenticatedUser(),
180
185
  validate: (v) => v.length > 0 || 'Required',
181
- },
182
- ]);
183
- setRepo(answers.owner, answers.repo);
184
- saveConfig({ token: getToken(), owner: answers.owner, repo: answers.repo });
185
- const spinner = createSpinner(dim('Connecting...')).start();
186
- await sleep(400);
187
- spinner.success({ text: green(`Locked in ${cyan(`${answers.owner}/${answers.repo}`)}`) });
188
- console.log('');
186
+ }]);
187
+ // 2. Fetch Repos for that Owner
188
+ const spin = createSpinner(dim(`Fetching repositories for ${targetOwner}...`)).start();
189
+ const fetchedRepos = await repos.listRepos({ owner: targetOwner, silent: true });
190
+ if (fetchedRepos.length > 0) {
191
+ spin.success({ text: green(`Found ${fetchedRepos.length} repositories for ${targetOwner}`) });
192
+ const { selectedRepo } = await inquirer.prompt([{
193
+ type: 'list',
194
+ name: 'selectedRepo',
195
+ message: cyan('📦 Select Repository:'),
196
+ choices: [
197
+ ...fetchedRepos.map((r) => ({ name: r.name, value: r.name })),
198
+ new inquirer.Separator(),
199
+ { name: '✍️ Enter manually...', value: '__manual__' }
200
+ ],
201
+ loop: false
202
+ }]);
203
+ if (selectedRepo !== '__manual__') {
204
+ repo = selectedRepo;
205
+ }
206
+ else {
207
+ const { manualRepo } = await inquirer.prompt([{
208
+ type: 'input',
209
+ name: 'manualRepo',
210
+ message: cyan('📦 Repository name:'),
211
+ validate: (v) => v.length > 0 || 'Required',
212
+ }]);
213
+ repo = manualRepo;
214
+ }
215
+ }
216
+ else {
217
+ spin.warn({ text: yellow(`No public repositories found for ${targetOwner}.`) });
218
+ const { manualRepo } = await inquirer.prompt([{
219
+ type: 'input',
220
+ name: 'manualRepo',
221
+ message: cyan('📦 Enter Repository name manually:'),
222
+ validate: (v) => v.length > 0 || 'Required',
223
+ }]);
224
+ repo = manualRepo;
225
+ }
226
+ setRepo(targetOwner, repo);
227
+ saveConfig({ token: getToken(), owner: targetOwner, repo: repo });
228
+ targetConfirmed = true;
229
+ // 4. Permission Check (for Maintainer safety)
230
+ const checkSpin = createSpinner(dim('Checking maintainer permissions...')).start();
231
+ try {
232
+ const perms = await getRepoPermissions(targetOwner, repo);
233
+ if (!perms.push && !perms.admin) {
234
+ checkSpin.warn({ text: yellow(`Limited Access → ${cyan(`${targetOwner}/${repo}`)}`) });
235
+ console.log(boxen(chalk.yellow(`🛡️ Maintainer Mode Warning\n\n`) +
236
+ chalk.dim(`You do not have push/admin permissions for this repository.\n`) +
237
+ chalk.dim(`Most maintainer actions (bulk issues, merging PRs) will fail.`), { padding: 1, borderColor: 'yellow', borderStyle: 'round' }));
238
+ const { proceed } = await inquirer.prompt([{
239
+ type: 'confirm',
240
+ name: 'proceed',
241
+ message: 'Continue anyway?',
242
+ default: false
243
+ }]);
244
+ if (!proceed) {
245
+ targetConfirmed = false;
246
+ throw new BackToMenu();
247
+ }
248
+ }
249
+ else {
250
+ checkSpin.success({ text: green(`Permissions verified for ${cyan(`${targetOwner}/${repo}`)}`) });
251
+ }
252
+ }
253
+ catch {
254
+ checkSpin.warn({ text: yellow('Could not verify permissions (API limit or private)') });
255
+ }
256
+ console.log(green(`\n ✅ Locked in → ${cyan(`${targetOwner}/${repo}`)}\n`));
189
257
  }
190
258
  // ── Mode Selector ──────────────────────────────────────────────────────
191
259
  async function mainMenu() {
@@ -333,16 +401,7 @@ async function maintainerMenu() {
333
401
  if (category === 'back')
334
402
  break;
335
403
  if (category === 'switch') {
336
- await safeMenu(async () => {
337
- const a = await inquirer.prompt([
338
- { type: 'input', name: 'owner', message: cyan('👤 New Owner/Org:'), default: getOwner() },
339
- { type: 'input', name: 'repo', message: cyan('📦 New Repo:'), default: getRepo() },
340
- ]);
341
- setRepo(a.owner, a.repo);
342
- const s = createSpinner(dim('Switching...')).start();
343
- await sleep(400);
344
- s.success({ text: green(`Now targeting ${cyan(`${a.owner}/${a.repo}`)}`) });
345
- });
404
+ await ensureTargetRepo(true);
346
405
  continue;
347
406
  }
348
407
  if (category === 'issues')
@@ -91,9 +91,23 @@ export async function listRepos(opts) {
91
91
  const spinner = !opts.silent ? ora('Fetching repos...').start() : null;
92
92
  const octokit = getOctokit();
93
93
  try {
94
- let repos;
95
- if (opts.org) {
96
- ({ data: repos } = await octokit.repos.listForOrg({ org: opts.org, per_page: opts.limit || 100, sort: 'updated' }));
94
+ let repos = [];
95
+ const owner = opts.org || opts.owner;
96
+ if (owner) {
97
+ // Check if it's an org or user
98
+ try {
99
+ const { data: entity } = await octokit.users.getByUsername({ username: owner });
100
+ if (entity.type === 'Organization') {
101
+ ({ data: repos } = await octokit.repos.listForOrg({ org: owner, per_page: opts.limit || 100, sort: 'updated' }));
102
+ }
103
+ else {
104
+ ({ data: repos } = await octokit.repos.listForUser({ username: owner, per_page: opts.limit || 100, sort: 'updated' }));
105
+ }
106
+ }
107
+ catch {
108
+ // Fallback to org if lookup fails (might be a private org only visible to token)
109
+ ({ data: repos } = await octokit.repos.listForOrg({ org: owner, per_page: opts.limit || 100, sort: 'updated' }));
110
+ }
97
111
  }
98
112
  else {
99
113
  ({ data: repos } = await octokit.repos.listForAuthenticatedUser({ per_page: opts.limit || 100, sort: 'updated' }));
@@ -94,6 +94,10 @@ export async function getRepoDetails(owner, repo) {
94
94
  const { data } = await getOctokit().repos.get({ owner, repo });
95
95
  return data;
96
96
  }
97
+ export async function getRepoPermissions(owner, repo) {
98
+ const data = await getRepoDetails(owner, repo);
99
+ return data.permissions || { admin: false, push: false, pull: true };
100
+ }
97
101
  export async function getLatestCheckRuns(owner, repo, ref) {
98
102
  const octokit = getOctokit();
99
103
  const { data: checks } = await octokit.checks.listForRef({ owner, repo, ref });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitpadi",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
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": {
package/src/cli.ts CHANGED
@@ -15,7 +15,7 @@ import { execSync } from 'child_process';
15
15
  import boxen from 'boxen';
16
16
  import { createSpinner } from 'nanospinner';
17
17
  import { Octokit } from '@octokit/rest';
18
- import { initGitHub, setRepo, getOwner, getRepo, getOctokit, loadConfig, saveConfig, getToken } from './core/github.js';
18
+ import { initGitHub, setRepo, getOwner, getRepo, getOctokit, loadConfig, saveConfig, getToken, getAuthenticatedUser, getRepoPermissions } from './core/github.js';
19
19
 
20
20
  import * as issues from './commands/issues.js';
21
21
  import * as prs from './commands/prs.js';
@@ -25,6 +25,7 @@ import * as releases from './commands/releases.js';
25
25
  import * as contribute from './commands/contribute.js';
26
26
 
27
27
  const VERSION = '2.0.0';
28
+ let targetConfirmed = false;
28
29
 
29
30
  // ── Styling ────────────────────────────────────────────────────────────
30
31
  const cyber = gradient(['#ff00ff', '#00ffff', '#ff00ff']);
@@ -178,40 +179,113 @@ async function ensureAuthenticated() {
178
179
  /**
179
180
  * Ensures we have a target repository (Owner/Repo)
180
181
  */
181
- async function ensureTargetRepo() {
182
+ async function ensureTargetRepo(force = false) {
182
183
  let owner = getOwner();
183
184
  let repo = getRepo();
184
185
 
185
- if (owner && repo) {
186
- return;
186
+ if (!force && owner && repo) {
187
+ if (targetConfirmed) return;
188
+
189
+ const { confirm } = await inquirer.prompt([{
190
+ type: 'confirm',
191
+ name: 'confirm',
192
+ message: cyan('🎯 Targeting ') + bold(`${owner}/${repo}`) + cyan('. Correct?'),
193
+ default: true
194
+ }]);
195
+ if (confirm) {
196
+ targetConfirmed = true;
197
+ return;
198
+ }
187
199
  }
188
200
 
189
201
  console.log(neon('\n 📦 Project Targeting — which repo are we working on?\n'));
190
202
 
191
- const answers = await inquirer.prompt([
192
- {
193
- type: 'input',
194
- name: 'owner',
195
- message: cyan('👤 GitHub Owner/Org:'),
196
- default: owner || '',
197
- validate: (v: string) => v.length > 0 || 'Required',
198
- },
199
- {
203
+ // 1. Get the Owner/Org
204
+ const { targetOwner } = await inquirer.prompt([{
205
+ type: 'input',
206
+ name: 'targetOwner',
207
+ message: cyan('👤 GitHub Owner/Org:'),
208
+ default: owner || await getAuthenticatedUser(),
209
+ validate: (v: string) => v.length > 0 || 'Required',
210
+ }]);
211
+
212
+ // 2. Fetch Repos for that Owner
213
+ const spin = createSpinner(dim(`Fetching repositories for ${targetOwner}...`)).start();
214
+ const fetchedRepos = await repos.listRepos({ owner: targetOwner, silent: true });
215
+
216
+ if (fetchedRepos.length > 0) {
217
+ spin.success({ text: green(`Found ${fetchedRepos.length} repositories for ${targetOwner}`) });
218
+
219
+ const { selectedRepo } = await inquirer.prompt([{
220
+ type: 'list',
221
+ name: 'selectedRepo',
222
+ message: cyan('📦 Select Repository:'),
223
+ choices: [
224
+ ...fetchedRepos.map((r: any) => ({ name: r.name, value: r.name })),
225
+ new inquirer.Separator(),
226
+ { name: '✍️ Enter manually...', value: '__manual__' }
227
+ ],
228
+ loop: false
229
+ }]);
230
+
231
+ if (selectedRepo !== '__manual__') {
232
+ repo = selectedRepo;
233
+ } else {
234
+ const { manualRepo } = await inquirer.prompt([{
235
+ type: 'input',
236
+ name: 'manualRepo',
237
+ message: cyan('📦 Repository name:'),
238
+ validate: (v: string) => v.length > 0 || 'Required',
239
+ }]);
240
+ repo = manualRepo;
241
+ }
242
+ } else {
243
+ spin.warn({ text: yellow(`No public repositories found for ${targetOwner}.`) });
244
+ const { manualRepo } = await inquirer.prompt([{
200
245
  type: 'input',
201
- name: 'repo',
202
- message: cyan('📦 Repository name:'),
203
- default: repo || '',
246
+ name: 'manualRepo',
247
+ message: cyan('📦 Enter Repository name manually:'),
204
248
  validate: (v: string) => v.length > 0 || 'Required',
205
- },
206
- ]);
249
+ }]);
250
+ repo = manualRepo;
251
+ }
207
252
 
208
- setRepo(answers.owner, answers.repo);
209
- saveConfig({ token: getToken(), owner: answers.owner, repo: answers.repo });
253
+ setRepo(targetOwner, repo as string);
254
+ saveConfig({ token: getToken(), owner: targetOwner, repo: repo as string });
255
+ targetConfirmed = true;
210
256
 
211
- const spinner = createSpinner(dim('Connecting...')).start();
212
- await sleep(400);
213
- spinner.success({ text: green(`Locked in → ${cyan(`${answers.owner}/${answers.repo}`)}`) });
214
- console.log('');
257
+ // 4. Permission Check (for Maintainer safety)
258
+ const checkSpin = createSpinner(dim('Checking maintainer permissions...')).start();
259
+ try {
260
+ const perms = await getRepoPermissions(targetOwner, repo as string);
261
+ if (!perms.push && !perms.admin) {
262
+ checkSpin.warn({ text: yellow(`Limited Access → ${cyan(`${targetOwner}/${repo}`)}`) });
263
+ console.log(boxen(
264
+ chalk.yellow(`🛡️ Maintainer Mode Warning\n\n`) +
265
+ chalk.dim(`You do not have push/admin permissions for this repository.\n`) +
266
+ chalk.dim(`Most maintainer actions (bulk issues, merging PRs) will fail.`),
267
+ { padding: 1, borderColor: 'yellow', borderStyle: 'round' }
268
+ ));
269
+
270
+ const { proceed } = await inquirer.prompt([{
271
+ type: 'confirm',
272
+ name: 'proceed',
273
+ message: 'Continue anyway?',
274
+ default: false
275
+ }]);
276
+
277
+ if (!proceed) {
278
+ targetConfirmed = false;
279
+ throw new BackToMenu();
280
+ }
281
+ } else {
282
+ checkSpin.success({ text: green(`Permissions verified for ${cyan(`${targetOwner}/${repo}`)}`) });
283
+ }
284
+ } catch {
285
+ checkSpin.warn({ text: yellow('Could not verify permissions (API limit or private)') });
286
+ }
287
+
288
+ console.log(green(`\n ✅ Locked in → ${cyan(`${targetOwner}/${repo}`)}\n`));
215
289
  }
216
290
 
217
291
  // ── Mode Selector ──────────────────────────────────────────────────────
@@ -365,16 +439,7 @@ async function maintainerMenu() {
365
439
  if (category === 'back') break;
366
440
 
367
441
  if (category === 'switch') {
368
- await safeMenu(async () => {
369
- const a = await inquirer.prompt([
370
- { type: 'input', name: 'owner', message: cyan('👤 New Owner/Org:'), default: getOwner() },
371
- { type: 'input', name: 'repo', message: cyan('📦 New Repo:'), default: getRepo() },
372
- ]);
373
- setRepo(a.owner, a.repo);
374
- const s = createSpinner(dim('Switching...')).start();
375
- await sleep(400);
376
- s.success({ text: green(`Now targeting ${cyan(`${a.owner}/${a.repo}`)}`) });
377
- });
442
+ await ensureTargetRepo(true);
378
443
  continue;
379
444
  }
380
445
 
@@ -101,14 +101,27 @@ export async function setTopics(name: string, topics: string[], opts: { org?: st
101
101
  } catch (e: any) { spinner.fail(e.message); }
102
102
  }
103
103
 
104
- export async function listRepos(opts: { org?: string; limit?: number; silent?: boolean }) {
104
+ export async function listRepos(opts: { org?: string; owner?: string; limit?: number; silent?: boolean }) {
105
105
  const spinner = !opts.silent ? ora('Fetching repos...').start() : null;
106
106
  const octokit = getOctokit();
107
107
 
108
108
  try {
109
- let repos: any[];
110
- if (opts.org) {
111
- ({ data: repos } = await octokit.repos.listForOrg({ org: opts.org, per_page: opts.limit || 100, sort: 'updated' }));
109
+ let repos: any[] = [];
110
+ const owner = opts.org || opts.owner;
111
+
112
+ if (owner) {
113
+ // Check if it's an org or user
114
+ try {
115
+ const { data: entity } = await octokit.users.getByUsername({ username: owner });
116
+ if (entity.type === 'Organization') {
117
+ ({ data: repos } = await octokit.repos.listForOrg({ org: owner, per_page: opts.limit || 100, sort: 'updated' }));
118
+ } else {
119
+ ({ data: repos } = await octokit.repos.listForUser({ username: owner, per_page: opts.limit || 100, sort: 'updated' }));
120
+ }
121
+ } catch {
122
+ // Fallback to org if lookup fails (might be a private org only visible to token)
123
+ ({ data: repos } = await octokit.repos.listForOrg({ org: owner, per_page: opts.limit || 100, sort: 'updated' }));
124
+ }
112
125
  } else {
113
126
  ({ data: repos } = await octokit.repos.listForAuthenticatedUser({ per_page: opts.limit || 100, sort: 'updated' }));
114
127
  }
@@ -114,6 +114,11 @@ export async function getRepoDetails(owner: string, repo: string) {
114
114
  return data;
115
115
  }
116
116
 
117
+ export async function getRepoPermissions(owner: string, repo: string) {
118
+ const data = await getRepoDetails(owner, repo);
119
+ return data.permissions || { admin: false, push: false, pull: true };
120
+ }
121
+
117
122
  export async function getLatestCheckRuns(owner: string, repo: string, ref: string) {
118
123
  const octokit = getOctokit();
119
124
  const { data: checks } = await octokit.checks.listForRef({ owner, repo, ref });