gh-manager-cli 1.14.0 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [1.15.1](https://github.com/wiiiimm/gh-manager-cli/compare/v1.15.0...v1.15.1) (2025-09-02)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * standardise keyboard shortcut capitalisation in error messages ([44d7998](https://github.com/wiiiimm/gh-manager-cli/commit/44d7998f3bad752a18c2a54c0adef18b69f9ecca))
7
+
8
+ # [1.15.0](https://github.com/wiiiimm/gh-manager-cli/compare/v1.14.0...v1.15.0) (2025-09-02)
9
+
10
+
11
+ ### Features
12
+
13
+ * implement GitHub Device Authorization Grant flow ([#13](https://github.com/wiiiimm/gh-manager-cli/issues/13)) ([d259534](https://github.com/wiiiimm/gh-manager-cli/commit/d259534a8a0e5c21270da17df4256aec6b0b216c))
14
+
1
15
  # [1.14.0](https://github.com/wiiiimm/gh-manager-cli/compare/v1.13.2...v1.14.0) (2025-09-02)
2
16
 
3
17
 
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="docs/assets/logo-horizontal.png" alt="gh-manager-cli logo" width="400" />
3
+ </p>
4
+
1
5
  # gh-manager-cli
2
6
 
3
7
  [![npm version](https://img.shields.io/npm/v/gh-manager-cli.svg)](https://www.npmjs.com/package/gh-manager-cli)
@@ -41,12 +45,12 @@ Interactive terminal app to browse and manage your personal GitHub repositories.
41
45
  npx gh-manager-cli
42
46
  ```
43
47
 
44
- On first run, you'll be prompted for a GitHub Personal Access Token.
48
+ On first run, you'll be prompted to authenticate with GitHub (OAuth recommended).
45
49
 
46
50
  ## Features
47
51
 
48
52
  ### Core Repository Management
49
- - **Token Authentication**: Secure PAT storage with validation and persistence
53
+ - **Authentication**: GitHub OAuth (recommended) or Personal Access Token with secure storage
50
54
  - **Repository Listing**: Browse all your personal repositories with metadata (stars, forks, language, etc.)
51
55
  - **Live Pagination**: Infinite scroll with automatic page prefetching
52
56
  - **Interactive Sorting**: Modal-based sort selection (updated, pushed, name, stars) with direction toggle
@@ -142,18 +146,41 @@ pnpm link
142
146
  gh-manager-cli
143
147
  ```
144
148
 
145
- ## Token & Security
149
+ ## Authentication
150
+
151
+ The app supports two authentication methods:
152
+
153
+ ### 1. GitHub OAuth (Recommended) 🎯
154
+
155
+ The easiest and most secure way to authenticate:
146
156
 
147
- The app needs a GitHub token to read your repositories.
157
+ - **Device Flow**: No need to handle callback URLs - just enter a code on GitHub's website
158
+ - **Browser-based**: Opens GitHub's authorization page automatically
159
+ - **Secure**: No client secrets or sensitive data in the app
160
+ - **Full Permissions**: Automatically requests all necessary scopes for complete functionality
161
+ - **User-friendly**: No manual token management required
162
+
163
+ When you first run the app, select **"GitHub OAuth (Recommended)"** from the authentication options. The app will:
164
+ 1. Display a device code for you to enter on GitHub
165
+ 2. Open your browser to GitHub's device authorization page
166
+ 3. Wait for you to authorize the app
167
+ 4. Securely store the OAuth token for future use
168
+
169
+ ### 2. Personal Access Token (PAT)
170
+
171
+ Alternative method for users who prefer manual token management:
148
172
 
149
173
  - Provide via env var: `GITHUB_TOKEN` or `GH_TOKEN`, or enter when prompted on first run.
150
- - Recommended: classic PAT with `repo` scope for listing both public and private repos (read is sufficient).
174
+ - Recommended: classic PAT with `repo` scope for listing both public and private repos.
151
175
  - Validation: a minimal `viewer { login }` request verifies the token.
152
- - Storage: token is saved as JSON in your OS user config directory with POSIX perms `0600`.
176
+
177
+ ### Token Storage & Security
178
+
179
+ - Storage: tokens are saved as JSON in your OS user config directory with POSIX perms `0600`.
153
180
  - macOS: `~/Library/Preferences/gh-manager-cli/config.json`
154
181
  - Linux: `~/.config/gh-manager-cli/config.json`
155
182
  - Windows: `%APPDATA%\gh-manager-cli\config.json`
156
- - Revocation: you can revoke the PAT at any time in your GitHub settings.
183
+ - Revocation: you can revoke tokens at any time in your GitHub settings.
157
184
 
158
185
  Note: Tokens are stored in plaintext on disk with restricted permissions. Future work may add OS keychain support.
159
186
 
@@ -332,6 +359,7 @@ REPOS_PER_FETCH=5 GH_MANAGER_DEBUG=1 npx gh-manager-cli-cli
332
359
  For the up-to-date task board, see [TODOs.md](./TODOs.md).
333
360
 
334
361
  Recently implemented:
362
+ - ✅ OAuth login flow as an alternative to Personal Access Token
335
363
  - ✅ Density toggle for row spacing (compact/cozy/comfy)
336
364
  - ✅ Repo actions (archive/unarchive, delete, change visibility) with confirmations
337
365
  - ✅ Organization support and switching (press `W`) with enterprise detection
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ var require_package = __commonJS({
27
27
  "package.json"(exports, module) {
28
28
  module.exports = {
29
29
  name: "gh-manager-cli",
30
- version: "1.14.0",
30
+ version: "1.15.1",
31
31
  private: false,
32
32
  description: "Interactive CLI to manage your GitHub repos (personal) with Ink",
33
33
  license: "MIT",
@@ -62,7 +62,8 @@ var require_package = __commonJS({
62
62
  test: "vitest run",
63
63
  "test:watch": "vitest",
64
64
  "test:coverage": "vitest run --coverage",
65
- "brew:sha": "bash scripts/brew-sha.sh"
65
+ "brew:sha": "bash scripts/brew-sha.sh",
66
+ "assets:png": "bash scripts/convert-svg-to-png.sh"
66
67
  },
67
68
  engines: {
68
69
  node: ">=18"
@@ -78,6 +79,7 @@ var require_package = __commonJS({
78
79
  ink: "^6.2.3",
79
80
  "ink-spinner": "^5.0.0",
80
81
  "ink-text-input": "^6.0.0",
82
+ open: "^10.1.0",
81
83
  react: "^19.1.1"
82
84
  },
83
85
  devDependencies: {
@@ -85,13 +87,13 @@ var require_package = __commonJS({
85
87
  "@semantic-release/git": "^10.0.1",
86
88
  "@types/node": "^24.3.0",
87
89
  "@types/react": "^19.1.12",
90
+ "@vitest/coverage-v8": "^2.1.3",
88
91
  "ink-testing-library": "^3.0.0",
89
92
  pkg: "^5.8.1",
90
93
  "semantic-release": "^24.2.7",
91
94
  tsup: "^8.5.0",
92
95
  typescript: "^5.9.2",
93
- vitest: "^2.1.3",
94
- "@vitest/coverage-v8": "^2.1.3"
96
+ vitest: "^2.1.3"
95
97
  },
96
98
  repository: {
97
99
  type: "git",
@@ -150,12 +152,12 @@ var require_package = __commonJS({
150
152
 
151
153
  // src/index.tsx
152
154
  var import_package = __toESM(require_package(), 1);
153
- import { render, Box as Box15, Text as Text16 } from "ink";
155
+ import { render, Box as Box17, Text as Text18 } from "ink";
154
156
  import "dotenv/config";
155
157
 
156
158
  // src/ui/App.tsx
157
- import { useEffect as useEffect8, useMemo as useMemo2, useState as useState11 } from "react";
158
- import { Box as Box14, Text as Text15, useApp as useApp2, useStdout as useStdout2, useInput as useInput11 } from "ink";
159
+ import { useEffect as useEffect8, useMemo as useMemo2, useState as useState12 } from "react";
160
+ import { Box as Box16, Text as Text17, useApp as useApp2, useStdout as useStdout2, useInput as useInput12 } from "ink";
159
161
  import TextInput5 from "ink-text-input";
160
162
 
161
163
  // src/config.ts
@@ -192,15 +194,19 @@ function getStoredToken() {
192
194
  const cfg = readConfig();
193
195
  return cfg.token;
194
196
  }
195
- function storeToken(token) {
197
+ function storeToken(token, source = "pat") {
196
198
  const existing = readConfig();
197
- writeConfig({ ...existing, token, tokenVersion: 1 });
199
+ writeConfig({ ...existing, token, tokenVersion: 1, tokenSource: source });
198
200
  }
199
201
  function clearStoredToken() {
200
202
  const existing = readConfig();
201
- const { token, tokenVersion, ...rest } = existing;
203
+ const { token, tokenVersion, tokenSource, ...rest } = existing;
202
204
  writeConfig({ ...rest });
203
205
  }
206
+ function getTokenSource() {
207
+ const cfg = readConfig();
208
+ return cfg.tokenSource || "pat";
209
+ }
204
210
  function getUIPrefs() {
205
211
  const cfg = readConfig();
206
212
  return cfg.ui || {};
@@ -211,6 +217,191 @@ function storeUIPrefs(patch) {
211
217
  writeConfig({ ...existing, ui: mergedUI });
212
218
  }
213
219
 
220
+ // src/oauth.ts
221
+ import open from "open";
222
+
223
+ // src/constants.ts
224
+ var OAUTH_CONFIG = {
225
+ // GitHub OAuth App Client ID (public, safe to include in client)
226
+ // You'll need to register an OAuth App on GitHub and replace this with your client ID
227
+ // Note: Device flow doesn't use callback URLs, but GitHub requires one during app setup
228
+ CLIENT_ID: "Ov23li1pOAO5GZmxBF1L",
229
+ // gh-manager-cli OAuth App
230
+ // GitHub Device Authorization Grant endpoints
231
+ DEVICE_CODE_URL: "https://github.com/login/device/code",
232
+ TOKEN_URL: "https://github.com/login/oauth/access_token",
233
+ // Required OAuth scopes for the application
234
+ // Comprehensive scopes for full functionality:
235
+ // 'repo' - Full control of private repositories (includes repo:status, repo_deployment, public_repo, repo:invite)
236
+ // 'read:org' - Read organisation data including teams and membership
237
+ // 'user' - Read user profile data (includes user:email, user:follow)
238
+ // 'delete_repo' - Delete repositories
239
+ // 'workflow' - Update GitHub Actions workflow files
240
+ // 'write:packages' - Upload packages to GitHub Package Registry
241
+ // 'read:packages' - Download packages from GitHub Package Registry
242
+ SCOPES: [
243
+ "repo",
244
+ // Full repository access (private and public)
245
+ "read:org",
246
+ // Read organisation information
247
+ "user",
248
+ // Read user profile data
249
+ "delete_repo",
250
+ // Delete repositories
251
+ "workflow",
252
+ // Manage GitHub Actions workflows
253
+ "write:packages",
254
+ // Write to package registry
255
+ "read:packages"
256
+ // Read from package registry
257
+ ],
258
+ // Device flow configuration
259
+ DEVICE_FLOW_TIMEOUT_MS: 9e5,
260
+ // 15 minutes (GitHub's maximum)
261
+ POLLING_INTERVAL_MS: 5e3
262
+ // 5 seconds (GitHub's default)
263
+ };
264
+
265
+ // src/oauth.ts
266
+ async function pollForAccessToken(deviceCodeResponse) {
267
+ try {
268
+ const token = await pollForToken(deviceCodeResponse);
269
+ if (token) {
270
+ try {
271
+ const client = makeClient(token);
272
+ const login = await getViewerLogin(client);
273
+ return {
274
+ success: true,
275
+ token,
276
+ login
277
+ };
278
+ } catch (error) {
279
+ return {
280
+ success: false,
281
+ error: `Token validation failed: ${error.message}`
282
+ };
283
+ }
284
+ }
285
+ return {
286
+ success: false,
287
+ error: "Failed to obtain access token"
288
+ };
289
+ } catch (error) {
290
+ return {
291
+ success: false,
292
+ error: `Polling failed: ${error.message}`
293
+ };
294
+ }
295
+ }
296
+ async function requestDeviceCode() {
297
+ const response = await fetch(OAUTH_CONFIG.DEVICE_CODE_URL, {
298
+ method: "POST",
299
+ headers: {
300
+ "Accept": "application/json",
301
+ "Content-Type": "application/json"
302
+ },
303
+ body: JSON.stringify({
304
+ client_id: OAUTH_CONFIG.CLIENT_ID,
305
+ scope: OAUTH_CONFIG.SCOPES.join(" ")
306
+ })
307
+ });
308
+ if (!response.ok) {
309
+ throw new Error(`HTTP error ${response.status}: ${await response.text()}`);
310
+ }
311
+ const data = await response.json();
312
+ if (data.error) {
313
+ throw new Error(`${data.error}: ${data.error_description || "Unknown error"}`);
314
+ }
315
+ return data;
316
+ }
317
+ async function pollForToken(deviceCodeResponse) {
318
+ const startTime = Date.now();
319
+ const timeout = OAUTH_CONFIG.DEVICE_FLOW_TIMEOUT_MS;
320
+ const interval = Math.max(deviceCodeResponse.interval || 5, 5) * 1e3;
321
+ if (process.env.GH_MANAGER_DEBUG) {
322
+ console.log(`\u{1F504} Starting polling with interval ${interval}ms, timeout ${timeout}ms`);
323
+ }
324
+ while (Date.now() - startTime < timeout) {
325
+ await sleep(interval);
326
+ try {
327
+ if (process.env.GH_MANAGER_DEBUG) {
328
+ console.log("\u{1F504} Polling GitHub for access token...");
329
+ }
330
+ const response = await fetch(OAUTH_CONFIG.TOKEN_URL, {
331
+ method: "POST",
332
+ headers: {
333
+ "Accept": "application/json",
334
+ "Content-Type": "application/json"
335
+ },
336
+ body: JSON.stringify({
337
+ client_id: OAUTH_CONFIG.CLIENT_ID,
338
+ device_code: deviceCodeResponse.device_code,
339
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
340
+ })
341
+ });
342
+ if (!response.ok) {
343
+ const errorText = await response.text();
344
+ if (process.env.GH_MANAGER_DEBUG) {
345
+ console.log(`\u274C HTTP error ${response.status}: ${errorText}`);
346
+ }
347
+ throw new Error(`HTTP error ${response.status}: ${errorText}`);
348
+ }
349
+ const data = await response.json();
350
+ if (process.env.GH_MANAGER_DEBUG) {
351
+ console.log("\u{1F4E8} GitHub response:", JSON.stringify(data, null, 2));
352
+ }
353
+ if (data.access_token) {
354
+ if (process.env.GH_MANAGER_DEBUG) {
355
+ console.log("\u2705 Authorization successful! Token received.");
356
+ }
357
+ return data.access_token;
358
+ }
359
+ if (data.error) {
360
+ if (process.env.GH_MANAGER_DEBUG) {
361
+ console.log(`\u26A0\uFE0F GitHub error: ${data.error}`);
362
+ }
363
+ switch (data.error) {
364
+ case "authorization_pending":
365
+ continue;
366
+ case "slow_down":
367
+ if (process.env.GH_MANAGER_DEBUG) {
368
+ console.log("\u{1F40C} GitHub requested slow down, waiting extra 5 seconds");
369
+ }
370
+ await sleep(5e3);
371
+ continue;
372
+ case "expired_token":
373
+ throw new Error("The device code has expired. Please try again.");
374
+ case "access_denied":
375
+ throw new Error("Authorization was denied.");
376
+ default:
377
+ throw new Error(`${data.error}: ${data.error_description || "Unknown error"}`);
378
+ }
379
+ }
380
+ if (process.env.GH_MANAGER_DEBUG) {
381
+ console.log("\u{1F914} Unexpected response format, continuing to poll...");
382
+ }
383
+ } catch (error) {
384
+ if (process.env.GH_MANAGER_DEBUG) {
385
+ console.log("\u274C Polling error:", error.message);
386
+ }
387
+ if (error.message.includes("access_denied") || error.message.includes("expired_token")) {
388
+ throw error;
389
+ }
390
+ if (process.env.GH_MANAGER_DEBUG) {
391
+ console.log("\u{1F504} Continuing to poll despite error...");
392
+ }
393
+ }
394
+ }
395
+ throw new Error("OAuth flow timed out. Please try again.");
396
+ }
397
+ function sleep(ms) {
398
+ return new Promise((resolve) => setTimeout(resolve, ms));
399
+ }
400
+ async function openGitHubAuthorizationPage() {
401
+ const authUrl = `https://github.com/settings/connections/applications/${OAUTH_CONFIG.CLIENT_ID}`;
402
+ await open(authUrl);
403
+ }
404
+
214
405
  // src/ui/RepoList.tsx
215
406
  import React10, { useEffect as useEffect7, useMemo, useState as useState10, useRef } from "react";
216
407
  import { Box as Box13, Text as Text14, useApp, useInput as useInput10, useStdout } from "ink";
@@ -286,42 +477,59 @@ function OrgSwitcher({ token, currentContext, onSelect, onClose }) {
286
477
  const [error, setError] = useState(null);
287
478
  const [cursor, setCursor] = useState(0);
288
479
  const [enterpriseOrgs, setEnterpriseOrgs] = useState(/* @__PURE__ */ new Set());
480
+ const [showReauthorizeOption, setShowReauthorizeOption] = useState(false);
481
+ const [refreshing, setRefreshing] = useState(false);
289
482
  const isPersonalContext = currentContext === "personal";
290
- useEffect(() => {
291
- const loadOrgs = async () => {
292
- try {
293
- setLoading(true);
294
- const client = await import("./github-YDCON2PN.js").then((m) => m.makeClient(token));
295
- const orgs = await fetchViewerOrganizations(client);
296
- setOrganizations(orgs);
297
- const entOrgs = /* @__PURE__ */ new Set();
298
- for (const org of orgs) {
299
- const isEnt = await checkOrganizationIsEnterprise(client, org.login);
300
- if (isEnt) {
301
- entOrgs.add(org.login);
302
- }
483
+ const loadOrgs = async () => {
484
+ try {
485
+ setLoading(true);
486
+ setError(null);
487
+ const client = await import("./github-YDCON2PN.js").then((m) => m.makeClient(token));
488
+ const orgs = await fetchViewerOrganizations(client);
489
+ setOrganizations(orgs);
490
+ const entOrgs = /* @__PURE__ */ new Set();
491
+ for (const org of orgs) {
492
+ const isEnt = await checkOrganizationIsEnterprise(client, org.login);
493
+ if (isEnt) {
494
+ entOrgs.add(org.login);
303
495
  }
304
- setEnterpriseOrgs(entOrgs);
305
- if (!isPersonalContext) {
306
- const orgLogin2 = currentContext.login;
307
- const index = orgs.findIndex((org) => org.login === orgLogin2);
308
- if (index !== -1) {
309
- setCursor(index + 1);
310
- }
496
+ }
497
+ setEnterpriseOrgs(entOrgs);
498
+ if (!isPersonalContext) {
499
+ const orgLogin2 = currentContext.login;
500
+ const index = orgs.findIndex((org) => org.login === orgLogin2);
501
+ if (index !== -1) {
502
+ setCursor(index + 1);
311
503
  }
312
- } catch (e) {
313
- setError(e.message || "Failed to load organizations");
314
- } finally {
315
- setLoading(false);
316
504
  }
317
- };
505
+ } catch (e) {
506
+ setError(e.message || "Failed to load organisations");
507
+ } finally {
508
+ setLoading(false);
509
+ setRefreshing(false);
510
+ }
511
+ };
512
+ useEffect(() => {
318
513
  loadOrgs();
319
- }, [token, currentContext, isPersonalContext]);
514
+ }, []);
320
515
  useInput((input, key) => {
321
516
  if (key.escape) {
322
517
  onClose();
323
518
  return;
324
519
  }
520
+ if (input?.toLowerCase() === "r" && !refreshing && !loading) {
521
+ setRefreshing(true);
522
+ loadOrgs();
523
+ return;
524
+ }
525
+ if (key.ctrl && input === "w") {
526
+ process.nextTick(() => {
527
+ openGitHubAuthorizationPage().catch((err) => {
528
+ setError("Failed to open GitHub authorisation page");
529
+ });
530
+ });
531
+ return;
532
+ }
325
533
  if (key.return) {
326
534
  if (cursor === 0) {
327
535
  onSelect("personal");
@@ -343,8 +551,11 @@ function OrgSwitcher({ token, currentContext, onSelect, onClose }) {
343
551
  });
344
552
  const totalItems = organizations.length + 1;
345
553
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 2, paddingY: 1, width: 50, children: [
346
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Switch Account" }),
347
- loading ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: "Loading..." }) : error ? /* @__PURE__ */ jsx(Text, { color: "red", children: error }) : /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
554
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
555
+ "Switch Account ",
556
+ refreshing && /* @__PURE__ */ jsx(Text, { color: "yellow", children: "(Refreshing...)" })
557
+ ] }),
558
+ loading && !refreshing ? /* @__PURE__ */ jsx(Text, { color: "yellow", children: "Loading..." }) : error ? /* @__PURE__ */ jsx(Text, { color: "red", children: error }) : /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
348
559
  /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { children: cursor === 0 ? chalk.bgCyan.black(" \u2192 ") + " " + chalk.bold("Personal Account") + (isPersonalContext ? chalk.green(" \u2713") : "") : " " + chalk.gray("Personal Account") + (isPersonalContext ? chalk.green(" \u2713") : "") }) }),
349
560
  organizations.map((org, index) => {
350
561
  const isEnterprise = enterpriseOrgs.has(org.login);
@@ -352,9 +563,17 @@ function OrgSwitcher({ token, currentContext, onSelect, onClose }) {
352
563
  const isActiveContext = !isPersonalContext && currentContext.login === org.login;
353
564
  return /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { children: isCurrent ? chalk.bgCyan.black(" \u2192 ") + " " + chalk.bold(org.name || org.login) + (isEnterprise ? chalk.yellow(" (ENT)") : "") + chalk.gray(` (@${org.login})`) + (isActiveContext ? chalk.green(" \u2713") : "") : " " + chalk.gray(org.name || org.login) + (isEnterprise ? chalk.gray(" (ENT)") : "") + chalk.gray(` (@${org.login})`) + (isActiveContext ? chalk.green(" \u2713") : "") }) }, org.id);
354
565
  }),
355
- organizations.length === 0 && /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "No organizations found" })
566
+ organizations.length === 0 && /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "No organisations found" })
356
567
  ] }),
357
- /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "\u2191\u2193/Enter \u2022 Esc" }) })
568
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
569
+ /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "\u2191\u2193/Enter \u2022 R Refresh \u2022 Esc" }),
570
+ /* @__PURE__ */ jsx(Box, { height: 1, children: /* @__PURE__ */ jsx(Text, { children: " " }) }),
571
+ /* @__PURE__ */ jsxs(Text, { color: "cyan", children: [
572
+ "Not seeing your organisations? Press ",
573
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "Ctrl+W" }),
574
+ " to manage GitHub access"
575
+ ] })
576
+ ] })
358
577
  ] });
359
578
  }
360
579
 
@@ -810,7 +1029,7 @@ var ChangeVisibilityModal = ({
810
1029
  children: /* @__PURE__ */ jsx10(Text10, { children: chalk9.bgGray.white.bold(" Cancel ") })
811
1030
  }
812
1031
  ) }),
813
- /* @__PURE__ */ jsx10(Box9, { marginTop: 1, flexDirection: "row", justifyContent: "center", children: /* @__PURE__ */ jsx10(Text10, { color: "gray", children: "Press Enter or C/Esc to Cancel" }) })
1032
+ /* @__PURE__ */ jsx10(Box9, { marginTop: 1, flexDirection: "row", justifyContent: "center", children: /* @__PURE__ */ jsx10(Text10, { color: "gray", children: "Press Enter to Cancel \u2022 C to cancel" }) })
814
1033
  ] }) : /* @__PURE__ */ jsxs9(Fragment4, { children: [
815
1034
  /* @__PURE__ */ jsx10(Text10, { color: borderColor, children: "\u26A0\uFE0F Change repository visibility?" }),
816
1035
  /* @__PURE__ */ jsx10(Box9, { height: 1, children: /* @__PURE__ */ jsx10(Text10, { children: " " }) }),
@@ -852,7 +1071,9 @@ var ChangeVisibilityModal = ({
852
1071
  ] }),
853
1072
  /* @__PURE__ */ jsx10(Box9, { marginTop: 1, flexDirection: "row", justifyContent: "center", children: /* @__PURE__ */ jsxs9(Text10, { color: "gray", children: [
854
1073
  availableOptions.length > 1 ? "\u2191\u2193 Select Option \u2022 " : "",
855
- "\u2190 \u2192 Navigate \u2022 Enter to Confirm \u2022 C/Esc to Cancel"
1074
+ "\u2190 \u2192 Navigate \u2022 Press Enter to ",
1075
+ focusedButton === "option" ? "Change" : "Cancel",
1076
+ " \u2022 Y to confirm \u2022 C to cancel"
856
1077
  ] }) })
857
1078
  ] }),
858
1079
  /* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(
@@ -951,7 +1172,10 @@ function RepoListHeader({
951
1172
  visibilityFilter = "all",
952
1173
  isEnterprise = false
953
1174
  }) {
1175
+ const contextLabel = ownerContext === "personal" ? "Personal Account" : ownerContext?.type === "organization" ? `Organization: ${ownerContext.name ?? ownerContext.login}` : "";
1176
+ const visibilityLabel = visibilityFilter === "public" ? "Public" : visibilityFilter === "private" ? isEnterprise ? "Private/Internal" : "Private" : visibilityFilter === "internal" ? "Internal" : "";
954
1177
  return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "row", gap: 2, marginBottom: 1, children: [
1178
+ contextLabel && /* @__PURE__ */ jsx13(Text13, { children: contextLabel }),
955
1179
  /* @__PURE__ */ jsxs12(Text13, { color: "gray", dimColor: true, children: [
956
1180
  "Sort: ",
957
1181
  sortKey,
@@ -962,9 +1186,9 @@ function RepoListHeader({
962
1186
  "Fork Status - Commits Behind: ",
963
1187
  forkTracking ? "ON" : "OFF"
964
1188
  ] }),
965
- visibilityFilter !== "all" && /* @__PURE__ */ jsxs12(Text13, { color: "yellow", children: [
1189
+ !!visibilityLabel && /* @__PURE__ */ jsxs12(Text13, { color: "yellow", children: [
966
1190
  "Visibility: ",
967
- visibilityFilter === "public" ? "Public" : visibilityFilter === "private" ? isEnterprise ? "Private/Internal" : "Private" : ""
1191
+ visibilityLabel
968
1192
  ] }),
969
1193
  filter && !searchActive && /* @__PURE__ */ jsxs12(Text13, { color: "cyan", children: [
970
1194
  'Filter: "',
@@ -1955,9 +2179,9 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
1955
2179
  ] }) }),
1956
2180
  /* @__PURE__ */ jsx14(Box13, { borderStyle: "single", borderColor: "red", paddingX: 1, paddingY: 1, marginX: 1, height: contentHeight + containerPadding + 2, flexDirection: "column", children: /* @__PURE__ */ jsx14(Box13, { height: contentHeight, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", alignItems: "center", children: [
1957
2181
  /* @__PURE__ */ jsx14(Text14, { color: "red", children: error }),
1958
- /* @__PURE__ */ jsx14(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text14, { color: "gray", dimColor: true, children: "Press 'r' to retry or 'q' to quit" }) })
2182
+ /* @__PURE__ */ jsx14(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx14(Text14, { color: "gray", dimColor: true, children: "Press R to retry or Q to quit" }) })
1959
2183
  ] }) }) }),
1960
- /* @__PURE__ */ jsx14(Box13, { marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsx14(Text14, { color: "gray", children: "Press 'r' to retry \u2022 'q' to quit" }) })
2184
+ /* @__PURE__ */ jsx14(Box13, { marginTop: 1, paddingX: 1, children: /* @__PURE__ */ jsx14(Text14, { color: "gray", children: "Press R to retry \u2022 Q to quit" }) })
1961
2185
  ] });
1962
2186
  }
1963
2187
  if (loading && items.length === 0 || sortingLoading) {
@@ -2416,21 +2640,165 @@ function RepoList({ token, maxVisibleRows, onLogout, viewerLogin, onOrgContextCh
2416
2640
  ] });
2417
2641
  }
2418
2642
 
2419
- // src/ui/App.tsx
2643
+ // src/ui/components/auth/AuthMethodSelector.tsx
2644
+ import { useState as useState11 } from "react";
2645
+ import { Box as Box14, Text as Text15, useInput as useInput11 } from "ink";
2646
+ import chalk12 from "chalk";
2420
2647
  import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
2648
+ function AuthMethodSelector({ onSelect, onQuit }) {
2649
+ const [selectedIndex, setSelectedIndex] = useState11(0);
2650
+ const methods = [
2651
+ {
2652
+ key: "oauth",
2653
+ label: "GitHub OAuth",
2654
+ description: "Login via GitHub in your browser (recommended)"
2655
+ },
2656
+ {
2657
+ key: "pat",
2658
+ label: "Personal Access Token",
2659
+ description: "Manually enter a GitHub Personal Access Token"
2660
+ }
2661
+ ];
2662
+ useInput11((input, key) => {
2663
+ if (key.escape || input?.toLowerCase() === "q") {
2664
+ if (onQuit) {
2665
+ onQuit();
2666
+ } else {
2667
+ process.exit(0);
2668
+ }
2669
+ } else if (key.upArrow) {
2670
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
2671
+ } else if (key.downArrow) {
2672
+ setSelectedIndex((prev) => Math.min(methods.length - 1, prev + 1));
2673
+ } else if (key.return) {
2674
+ onSelect(methods[selectedIndex].key);
2675
+ } else if (input === "1") {
2676
+ onSelect("oauth");
2677
+ } else if (input === "2") {
2678
+ onSelect("pat");
2679
+ }
2680
+ });
2681
+ return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 2, paddingY: 1, children: [
2682
+ /* @__PURE__ */ jsx15(Text15, { bold: true, marginBottom: 1, children: "Choose Authentication Method" }),
2683
+ /* @__PURE__ */ jsx15(Box14, { flexDirection: "column", marginY: 1, children: methods.map((method, index) => {
2684
+ const isSelected = index === selectedIndex;
2685
+ const prefix = isSelected ? chalk12.cyan("\u203A") : " ";
2686
+ const numberPrefix = `${index + 1}.`;
2687
+ return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", marginBottom: 1, children: [
2688
+ /* @__PURE__ */ jsx15(Text15, { children: /* @__PURE__ */ jsxs14(Text15, { color: isSelected ? "cyan" : void 0, bold: isSelected, children: [
2689
+ prefix,
2690
+ " ",
2691
+ numberPrefix,
2692
+ " ",
2693
+ method.label
2694
+ ] }) }),
2695
+ /* @__PURE__ */ jsxs14(Text15, { color: "gray", dimColor: true, children: [
2696
+ " ",
2697
+ method.description
2698
+ ] })
2699
+ ] }, method.key);
2700
+ }) }),
2701
+ /* @__PURE__ */ jsx15(Text15, { color: "gray", dimColor: true, marginTop: 1, children: "Use arrow keys to navigate, Enter to select, or press 1/2 \u2022 Q/Esc to quit" })
2702
+ ] });
2703
+ }
2704
+
2705
+ // src/ui/components/auth/OAuthProgress.tsx
2706
+ import { Box as Box15, Text as Text16 } from "ink";
2707
+ import Spinner from "ink-spinner";
2708
+ import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
2709
+ function OAuthProgress({ status, error, deviceCode }) {
2710
+ const statusMessages = {
2711
+ initializing: {
2712
+ message: "Initializing GitHub Device Flow...",
2713
+ showSpinner: true
2714
+ },
2715
+ device_code_requested: {
2716
+ message: "Requesting device authorization code...",
2717
+ showSpinner: true
2718
+ },
2719
+ browser_opening: {
2720
+ message: "Opening browser for GitHub authentication...",
2721
+ showSpinner: true
2722
+ },
2723
+ waiting_for_authorization: {
2724
+ message: "Waiting for you to authorize in your browser...",
2725
+ showSpinner: true
2726
+ },
2727
+ polling_for_token: {
2728
+ message: "Polling GitHub for access token...",
2729
+ showSpinner: true
2730
+ },
2731
+ validating_token: {
2732
+ message: "Validating token with GitHub...",
2733
+ showSpinner: true
2734
+ },
2735
+ success: {
2736
+ message: "Authentication successful!",
2737
+ showSpinner: false
2738
+ },
2739
+ error: {
2740
+ message: "Authentication failed",
2741
+ showSpinner: false
2742
+ }
2743
+ };
2744
+ const { message, showSpinner } = statusMessages[status];
2745
+ return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", borderStyle: "single", borderColor: status === "error" ? "red" : "cyan", paddingX: 2, paddingY: 1, children: [
2746
+ /* @__PURE__ */ jsx16(Text16, { bold: true, marginBottom: 1, children: "GitHub OAuth Authentication" }),
2747
+ /* @__PURE__ */ jsx16(Box15, { marginY: 1, children: showSpinner ? /* @__PURE__ */ jsxs15(Box15, { children: [
2748
+ /* @__PURE__ */ jsx16(Text16, { color: "green", children: /* @__PURE__ */ jsx16(Spinner, { type: "dots" }) }),
2749
+ /* @__PURE__ */ jsxs15(Text16, { children: [
2750
+ " ",
2751
+ message
2752
+ ] })
2753
+ ] }) : /* @__PURE__ */ jsxs15(Text16, { color: status === "error" ? "red" : "green", children: [
2754
+ status === "error" ? "\u2717" : "\u2713",
2755
+ " ",
2756
+ message
2757
+ ] }) }),
2758
+ (status === "waiting_for_authorization" || status === "polling_for_token") && deviceCode && /* @__PURE__ */ jsxs15(Box15, { marginY: 1, flexDirection: "column", children: [
2759
+ /* @__PURE__ */ jsx16(Text16, { bold: true, color: "cyan", marginBottom: 1, children: "\u{1F4CB} Please complete these steps:" }),
2760
+ /* @__PURE__ */ jsxs15(Box15, { marginBottom: 1, children: [
2761
+ /* @__PURE__ */ jsx16(Text16, { children: "1. Visit: " }),
2762
+ /* @__PURE__ */ jsx16(Text16, { bold: true, color: "blue", children: deviceCode.verification_uri })
2763
+ ] }),
2764
+ /* @__PURE__ */ jsxs15(Box15, { marginBottom: 1, flexDirection: "column", children: [
2765
+ /* @__PURE__ */ jsx16(Text16, { children: "2. Enter this code:" }),
2766
+ /* @__PURE__ */ jsx16(Box15, { borderStyle: "single", borderColor: "yellow", paddingX: 2, paddingY: 1, marginTop: 1, children: /* @__PURE__ */ jsx16(Text16, { bold: true, color: "yellow", children: deviceCode.user_code }) })
2767
+ ] }),
2768
+ status === "waiting_for_authorization" && /* @__PURE__ */ jsx16(Text16, { color: "gray", marginTop: 1, children: "Your browser should open automatically." }),
2769
+ status === "polling_for_token" && /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", marginTop: 1, children: [
2770
+ /* @__PURE__ */ jsx16(Text16, { color: "gray", children: "Waiting for you to complete authorization in your browser..." }),
2771
+ /* @__PURE__ */ jsx16(Text16, { color: "gray", dimColor: true, marginTop: 1, children: "This will timeout in 15 minutes. Press Esc to cancel." })
2772
+ ] })
2773
+ ] }),
2774
+ status === "error" && error && /* @__PURE__ */ jsxs15(Box15, { marginY: 1, flexDirection: "column", children: [
2775
+ /* @__PURE__ */ jsx16(Text16, { color: "red", children: error }),
2776
+ /* @__PURE__ */ jsx16(Text16, { color: "gray", marginTop: 1, children: "Press Esc to go back and try again." })
2777
+ ] }),
2778
+ status === "success" && /* @__PURE__ */ jsx16(Text16, { color: "gray", marginTop: 1, children: "Returning to application..." })
2779
+ ] });
2780
+ }
2781
+
2782
+ // src/ui/App.tsx
2783
+ import { jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
2421
2784
  var packageJson = require_package();
2422
2785
  function App() {
2423
2786
  const { exit } = useApp2();
2424
2787
  const { stdout } = useStdout2();
2425
- const [mode, setMode] = useState11("checking");
2426
- const [token, setToken] = useState11(null);
2427
- const [input, setInput] = useState11("");
2428
- const [error, setError] = useState11(null);
2429
- const [viewer, setViewer] = useState11(null);
2430
- const [rateLimitReset, setRateLimitReset] = useState11(null);
2431
- const [wasRateLimited, setWasRateLimited] = useState11(false);
2432
- const [orgContext, setOrgContext] = useState11("personal");
2433
- const [dims, setDims] = useState11(() => {
2788
+ const [mode, setMode] = useState12("checking");
2789
+ const [token, setToken] = useState12(null);
2790
+ const [input, setInput] = useState12("");
2791
+ const [error, setError] = useState12(null);
2792
+ const [viewer, setViewer] = useState12(null);
2793
+ const [rateLimitReset, setRateLimitReset] = useState12(null);
2794
+ const [wasRateLimited, setWasRateLimited] = useState12(false);
2795
+ const [orgContext, setOrgContext] = useState12("personal");
2796
+ const [authMethod, setAuthMethod] = useState12("pat");
2797
+ const [oauthStatus, setOAuthStatus] = useState12("initializing");
2798
+ const [tokenSource, setTokenSource] = useState12("pat");
2799
+ const [deviceCodeResponse, setDeviceCodeResponse] = useState12(null);
2800
+ const [oauthDeviceCode, setOauthDeviceCode] = useState12(null);
2801
+ const [dims, setDims] = useState12(() => {
2434
2802
  const cols = stdout?.columns ?? 100;
2435
2803
  const rows = stdout?.rows ?? 30;
2436
2804
  return { cols, rows };
@@ -2450,6 +2818,8 @@ function App() {
2450
2818
  useEffect8(() => {
2451
2819
  const env = getTokenFromEnv();
2452
2820
  const stored = getStoredToken();
2821
+ const source = getTokenSource();
2822
+ setTokenSource(source);
2453
2823
  if (env) {
2454
2824
  setToken(env);
2455
2825
  setMode("validating");
@@ -2457,15 +2827,66 @@ function App() {
2457
2827
  setToken(stored);
2458
2828
  setMode("validating");
2459
2829
  } else {
2460
- setMode("prompt");
2830
+ setMode("auth_method_selection");
2461
2831
  }
2462
2832
  }, []);
2833
+ useEffect8(() => {
2834
+ if (mode !== "oauth_flow") return;
2835
+ (async () => {
2836
+ try {
2837
+ setOAuthStatus("initializing");
2838
+ await new Promise((resolve) => setTimeout(resolve, 500));
2839
+ setOAuthStatus("device_code_requested");
2840
+ const deviceCodeResp = await requestDeviceCode();
2841
+ setDeviceCodeResponse(deviceCodeResp);
2842
+ setOauthDeviceCode({
2843
+ user_code: deviceCodeResp.user_code,
2844
+ verification_uri: deviceCodeResp.verification_uri
2845
+ });
2846
+ await new Promise((resolve) => setTimeout(resolve, 500));
2847
+ setOAuthStatus("browser_opening");
2848
+ const open2 = (await import("open")).default;
2849
+ await open2(deviceCodeResp.verification_uri);
2850
+ setOAuthStatus("waiting_for_authorization");
2851
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
2852
+ setOAuthStatus("polling_for_token");
2853
+ const tokenResult = await pollForAccessToken(deviceCodeResp);
2854
+ if (tokenResult.success && tokenResult.token) {
2855
+ setOAuthStatus("validating_token");
2856
+ storeToken(tokenResult.token, "oauth");
2857
+ setToken(tokenResult.token);
2858
+ setTokenSource("oauth");
2859
+ if (tokenResult.login) {
2860
+ setViewer(tokenResult.login);
2861
+ setOAuthStatus("success");
2862
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
2863
+ setMode("ready");
2864
+ } else {
2865
+ throw new Error("Failed to get user login from token");
2866
+ }
2867
+ } else {
2868
+ throw new Error(tokenResult.error || "Failed to obtain access token");
2869
+ }
2870
+ } catch (error2) {
2871
+ setOAuthStatus("error");
2872
+ setError(error2.message);
2873
+ }
2874
+ })();
2875
+ }, [mode]);
2876
+ const handleAuthMethodSelect = (method) => {
2877
+ setAuthMethod(method);
2878
+ if (method === "pat") {
2879
+ setMode("prompt");
2880
+ } else if (method === "oauth") {
2881
+ setMode("oauth_flow");
2882
+ }
2883
+ };
2463
2884
  useEffect8(() => {
2464
2885
  (async () => {
2465
2886
  if (mode !== "validating" || !token) return;
2466
2887
  const timeoutId = setTimeout(() => {
2467
2888
  setError("Token validation timed out. Please check your network connection.");
2468
- setMode("prompt");
2889
+ setMode("auth_method_selection");
2469
2890
  setToken(null);
2470
2891
  }, 15e3);
2471
2892
  try {
@@ -2529,8 +2950,9 @@ function App() {
2529
2950
  } else {
2530
2951
  setError(errorMessage);
2531
2952
  setInput("");
2532
- setMode("prompt");
2533
2953
  setToken(null);
2954
+ clearStoredToken();
2955
+ setMode("auth_method_selection");
2534
2956
  }
2535
2957
  }
2536
2958
  })();
@@ -2538,6 +2960,7 @@ function App() {
2538
2960
  const onSubmitToken = async () => {
2539
2961
  if (!input.trim()) return;
2540
2962
  setToken(input.trim());
2963
+ setTokenSource("pat");
2541
2964
  setError(null);
2542
2965
  setMode("validating");
2543
2966
  };
@@ -2550,12 +2973,20 @@ function App() {
2550
2973
  setToken(null);
2551
2974
  setViewer(null);
2552
2975
  setInput("");
2553
- setMode("prompt");
2976
+ setTokenSource("pat");
2977
+ setMode("auth_method_selection");
2554
2978
  };
2555
- useInput11((input2, key) => {
2556
- if (mode === "prompt" && key.escape) {
2979
+ useInput12((input2, key) => {
2980
+ if ((mode === "prompt" || mode === "auth_method_selection") && key.escape) {
2557
2981
  exit();
2558
2982
  }
2983
+ if (mode === "oauth_flow" && key.escape) {
2984
+ setMode("auth_method_selection");
2985
+ setError(null);
2986
+ setOAuthStatus("initializing");
2987
+ setOauthDeviceCode(null);
2988
+ setDeviceCodeResponse(null);
2989
+ }
2559
2990
  if (mode === "rate_limited") {
2560
2991
  const ch = (input2 || "").toLowerCase();
2561
2992
  if (key.escape || ch === "q") {
@@ -2567,29 +2998,29 @@ function App() {
2567
2998
  }
2568
2999
  }
2569
3000
  if (mode === "validating" && key.escape) {
2570
- if (wasRateLimited) {
3001
+ if (wasRateLimited || rateLimitReset) {
2571
3002
  setMode("rate_limited");
2572
3003
  } else {
2573
- setMode("prompt");
2574
- setToken(null);
2575
- setInput("");
3004
+ setMode("auth_method_selection");
2576
3005
  }
3006
+ setToken(null);
3007
+ setInput("");
2577
3008
  }
2578
3009
  });
2579
3010
  const verticalPadding = Math.floor(dims.rows * 0.15);
2580
- const header = useMemo2(() => /* @__PURE__ */ jsxs14(Box14, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [
2581
- /* @__PURE__ */ jsxs14(Box14, { flexDirection: "row", gap: 1, children: [
2582
- /* @__PURE__ */ jsxs14(Text15, { bold: true, color: "cyan", children: [
3011
+ const header = useMemo2(() => /* @__PURE__ */ jsxs16(Box16, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [
3012
+ /* @__PURE__ */ jsxs16(Box16, { flexDirection: "row", gap: 1, children: [
3013
+ /* @__PURE__ */ jsxs16(Text17, { bold: true, color: "cyan", children: [
2583
3014
  " ",
2584
3015
  "GitHub Repository Manager"
2585
3016
  ] }),
2586
- /* @__PURE__ */ jsxs14(Text15, { color: "gray", dimColor: true, children: [
3017
+ /* @__PURE__ */ jsxs16(Text17, { color: "gray", dimColor: true, children: [
2587
3018
  "v",
2588
3019
  packageJson.version
2589
3020
  ] }),
2590
- process.env.GH_MANAGER_DEBUG === "1" && /* @__PURE__ */ jsx15(Text15, { backgroundColor: "blue", color: "white", children: " debug mode " })
3021
+ process.env.GH_MANAGER_DEBUG === "1" && /* @__PURE__ */ jsx17(Text17, { backgroundColor: "blue", color: "white", children: " debug mode " })
2591
3022
  ] }),
2592
- viewer && /* @__PURE__ */ jsx15(Text15, { color: "gray", children: orgContext !== "personal" && orgContext.login ? `${orgContext.login}/@${viewer} ` : `@${viewer} ` })
3023
+ viewer && /* @__PURE__ */ jsx17(Text17, { color: "gray", children: orgContext !== "personal" && orgContext.login ? `${orgContext.login}/@${viewer} ` : `@${viewer} ` })
2593
3024
  ] }), [viewer, orgContext]);
2594
3025
  if (mode === "rate_limited") {
2595
3026
  const formatResetTime = (resetTime) => {
@@ -2612,55 +3043,70 @@ function App() {
2612
3043
  return "Unknown";
2613
3044
  }
2614
3045
  };
2615
- return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
3046
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
2616
3047
  header,
2617
- /* @__PURE__ */ jsx15(Box14, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsxs14(Box14, { borderStyle: "single", borderColor: "yellow", paddingX: 3, paddingY: 2, flexDirection: "column", width: Math.min(dims.cols - 8, 80), children: [
2618
- /* @__PURE__ */ jsx15(Text15, { bold: true, color: "yellow", marginBottom: 1, children: "\u26A0\uFE0F Rate Limit Exceeded" }),
2619
- /* @__PURE__ */ jsx15(Text15, { color: "gray", marginBottom: 1, children: "You've hit GitHub's API rate limit for your token." }),
2620
- /* @__PURE__ */ jsx15(Text15, { color: "gray", marginBottom: 1, children: "This happens when you make too many requests in a short time." }),
2621
- rateLimitReset && /* @__PURE__ */ jsxs14(Box14, { marginTop: 1, marginBottom: 1, children: [
2622
- /* @__PURE__ */ jsxs14(Text15, { children: [
2623
- /* @__PURE__ */ jsx15(Text15, { color: "cyan", children: "Reset in:" }),
3048
+ /* @__PURE__ */ jsx17(Box16, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsxs16(Box16, { borderStyle: "single", borderColor: "yellow", paddingX: 3, paddingY: 2, flexDirection: "column", width: Math.min(dims.cols - 8, 80), children: [
3049
+ /* @__PURE__ */ jsx17(Text17, { bold: true, color: "yellow", marginBottom: 1, children: "\u26A0\uFE0F Rate Limit Exceeded" }),
3050
+ /* @__PURE__ */ jsx17(Text17, { color: "gray", marginBottom: 1, children: "You've hit GitHub's API rate limit for your token." }),
3051
+ /* @__PURE__ */ jsx17(Text17, { color: "gray", marginBottom: 1, children: "This happens when you make too many requests in a short time." }),
3052
+ rateLimitReset && /* @__PURE__ */ jsxs16(Box16, { marginTop: 1, marginBottom: 1, children: [
3053
+ /* @__PURE__ */ jsxs16(Text17, { children: [
3054
+ /* @__PURE__ */ jsx17(Text17, { color: "cyan", children: "Reset in:" }),
2624
3055
  " ",
2625
- /* @__PURE__ */ jsx15(Text15, { bold: true, children: formatResetTime(rateLimitReset) })
3056
+ /* @__PURE__ */ jsx17(Text17, { bold: true, children: formatResetTime(rateLimitReset) })
2626
3057
  ] }),
2627
- /* @__PURE__ */ jsxs14(Text15, { color: "gray", dimColor: true, children: [
3058
+ /* @__PURE__ */ jsxs16(Text17, { color: "gray", dimColor: true, children: [
2628
3059
  "(",
2629
3060
  new Date(rateLimitReset).toLocaleTimeString(),
2630
3061
  ")"
2631
3062
  ] })
2632
3063
  ] }),
2633
- /* @__PURE__ */ jsxs14(Box14, { marginTop: 2, flexDirection: "column", gap: 1, children: [
2634
- /* @__PURE__ */ jsx15(Text15, { bold: true, children: "What would you like to do?" }),
2635
- /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", paddingLeft: 2, children: [
2636
- /* @__PURE__ */ jsxs14(Text15, { children: [
2637
- /* @__PURE__ */ jsx15(Text15, { color: "cyan", bold: true, children: "R" }),
3064
+ /* @__PURE__ */ jsxs16(Box16, { marginTop: 2, flexDirection: "column", gap: 1, children: [
3065
+ /* @__PURE__ */ jsx17(Text17, { bold: true, children: "What would you like to do?" }),
3066
+ /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", paddingLeft: 2, children: [
3067
+ /* @__PURE__ */ jsxs16(Text17, { children: [
3068
+ /* @__PURE__ */ jsx17(Text17, { color: "cyan", bold: true, children: "R" }),
2638
3069
  " - Retry now ",
2639
3070
  rateLimitReset && formatResetTime(rateLimitReset) !== "Now (should be reset)" ? "(likely to fail until reset)" : "(should work now)"
2640
3071
  ] }),
2641
- /* @__PURE__ */ jsxs14(Text15, { children: [
2642
- /* @__PURE__ */ jsx15(Text15, { color: "cyan", bold: true, children: "L" }),
2643
- " - Logout and use a different token"
3072
+ /* @__PURE__ */ jsxs16(Text17, { children: [
3073
+ /* @__PURE__ */ jsx17(Text17, { color: "cyan", bold: true, children: "L" }),
3074
+ " - Logout and choose authentication method"
2644
3075
  ] }),
2645
- /* @__PURE__ */ jsxs14(Text15, { children: [
2646
- /* @__PURE__ */ jsx15(Text15, { color: "gray", bold: true, children: "Q/Esc" }),
3076
+ /* @__PURE__ */ jsxs16(Text17, { children: [
3077
+ /* @__PURE__ */ jsx17(Text17, { color: "gray", bold: true, children: "Q/Esc" }),
2647
3078
  " - Quit application"
2648
3079
  ] })
2649
3080
  ] })
2650
3081
  ] }),
2651
- /* @__PURE__ */ jsx15(Text15, { color: "gray", dimColor: true, marginTop: 2, children: "Tip: Using multiple tokens or waiting between requests can help avoid rate limits." })
3082
+ /* @__PURE__ */ jsx17(Text17, { color: "gray", dimColor: true, marginTop: 2, children: "Tip: Using multiple tokens or waiting between requests can help avoid rate limits." })
2652
3083
  ] }) })
2653
3084
  ] });
2654
3085
  }
3086
+ if (mode === "auth_method_selection") {
3087
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
3088
+ header,
3089
+ /* @__PURE__ */ jsx17(Box16, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", alignItems: "center", children: [
3090
+ /* @__PURE__ */ jsx17(AuthMethodSelector, { onSelect: handleAuthMethodSelect }),
3091
+ error && /* @__PURE__ */ jsx17(Text17, { color: "red", marginTop: 1, children: error })
3092
+ ] }) })
3093
+ ] });
3094
+ }
3095
+ if (mode === "oauth_flow") {
3096
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
3097
+ header,
3098
+ /* @__PURE__ */ jsx17(Box16, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx17(OAuthProgress, { status: oauthStatus, error: error || void 0, deviceCode: oauthDeviceCode || void 0 }) })
3099
+ ] });
3100
+ }
2655
3101
  if (mode === "prompt") {
2656
- return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
3102
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
2657
3103
  header,
2658
- /* @__PURE__ */ jsx15(Box14, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsxs14(Box14, { borderStyle: "single", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", children: [
2659
- /* @__PURE__ */ jsx15(Text15, { bold: true, marginBottom: 1, children: "Authentication Required" }),
2660
- /* @__PURE__ */ jsx15(Text15, { color: "gray", marginBottom: 1, children: "Enter your GitHub Personal Access Token" }),
2661
- /* @__PURE__ */ jsxs14(Box14, { children: [
2662
- /* @__PURE__ */ jsx15(Text15, { children: "Token: " }),
2663
- /* @__PURE__ */ jsx15(
3104
+ /* @__PURE__ */ jsx17(Box16, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsxs16(Box16, { borderStyle: "single", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", children: [
3105
+ /* @__PURE__ */ jsx17(Text17, { bold: true, marginBottom: 1, children: "Authentication Required" }),
3106
+ /* @__PURE__ */ jsx17(Text17, { color: "gray", marginBottom: 1, children: "Enter your GitHub Personal Access Token" }),
3107
+ /* @__PURE__ */ jsxs16(Box16, { children: [
3108
+ /* @__PURE__ */ jsx17(Text17, { children: "Token: " }),
3109
+ /* @__PURE__ */ jsx17(
2664
3110
  TextInput5,
2665
3111
  {
2666
3112
  value: input,
@@ -2670,30 +3116,30 @@ function App() {
2670
3116
  }
2671
3117
  )
2672
3118
  ] }),
2673
- error && /* @__PURE__ */ jsx15(Text15, { color: "red", marginTop: 1, children: error }),
2674
- /* @__PURE__ */ jsx15(Text15, { color: "gray", dimColor: true, marginTop: 1, children: "The token will be stored securely in your local config" }),
2675
- /* @__PURE__ */ jsx15(Text15, { color: "gray", dimColor: true, marginTop: 1, children: "Press Esc to quit" })
3119
+ error && /* @__PURE__ */ jsx17(Text17, { color: "red", marginTop: 1, children: error }),
3120
+ /* @__PURE__ */ jsx17(Text17, { color: "gray", dimColor: true, marginTop: 1, children: "The token will be stored securely in your local config" }),
3121
+ /* @__PURE__ */ jsx17(Text17, { color: "gray", dimColor: true, marginTop: 1, children: "Press Esc to go back" })
2676
3122
  ] }) })
2677
3123
  ] });
2678
3124
  }
2679
3125
  if (mode === "validating" || mode === "checking") {
2680
- return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
3126
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
2681
3127
  header,
2682
- /* @__PURE__ */ jsx15(Box14, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", alignItems: "center", children: [
2683
- /* @__PURE__ */ jsx15(Text15, { color: "yellow", children: "Validating token..." }),
2684
- mode === "validating" && /* @__PURE__ */ jsx15(Text15, { color: "gray", dimColor: true, marginTop: 1, children: "Press Esc to cancel" })
3128
+ /* @__PURE__ */ jsx17(Box16, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", alignItems: "center", children: [
3129
+ /* @__PURE__ */ jsx17(Text17, { color: "yellow", children: "Validating token..." }),
3130
+ mode === "validating" && /* @__PURE__ */ jsx17(Text17, { color: "gray", dimColor: true, marginTop: 1, children: "Press Esc to cancel" })
2685
3131
  ] }) })
2686
3132
  ] });
2687
3133
  }
2688
3134
  if (mode === "error") {
2689
- return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
3135
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
2690
3136
  header,
2691
- /* @__PURE__ */ jsx15(Box14, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx15(Text15, { color: "red", children: error ?? "Unexpected error" }) })
3137
+ /* @__PURE__ */ jsx17(Box16, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx17(Text17, { color: "red", children: error ?? "Unexpected error" }) })
2692
3138
  ] });
2693
3139
  }
2694
- return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
3140
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
2695
3141
  header,
2696
- /* @__PURE__ */ jsx15(
3142
+ /* @__PURE__ */ jsx17(
2697
3143
  RepoList,
2698
3144
  {
2699
3145
  token,
@@ -2707,7 +3153,7 @@ function App() {
2707
3153
  }
2708
3154
 
2709
3155
  // src/index.tsx
2710
- import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
3156
+ import { jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
2711
3157
  var argv = process.argv.slice(2);
2712
3158
  if (argv.includes("--version") || argv.includes("-v")) {
2713
3159
  const version = import_package.default?.version || "0.0.0";
@@ -2742,8 +3188,8 @@ process.on("unhandledRejection", (reason) => {
2742
3188
  process.exit(1);
2743
3189
  });
2744
3190
  render(
2745
- /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
2746
- /* @__PURE__ */ jsx16(App, {}),
2747
- /* @__PURE__ */ jsx16(Text16, { color: "gray" })
3191
+ /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", children: [
3192
+ /* @__PURE__ */ jsx18(App, {}),
3193
+ /* @__PURE__ */ jsx18(Text18, { color: "gray" })
2748
3194
  ] })
2749
3195
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gh-manager-cli",
3
- "version": "1.14.0",
3
+ "version": "1.15.1",
4
4
  "private": false,
5
5
  "description": "Interactive CLI to manage your GitHub repos (personal) with Ink",
6
6
  "license": "MIT",
@@ -35,7 +35,8 @@
35
35
  "test": "vitest run",
36
36
  "test:watch": "vitest",
37
37
  "test:coverage": "vitest run --coverage",
38
- "brew:sha": "bash scripts/brew-sha.sh"
38
+ "brew:sha": "bash scripts/brew-sha.sh",
39
+ "assets:png": "bash scripts/convert-svg-to-png.sh"
39
40
  },
40
41
  "engines": {
41
42
  "node": ">=18"
@@ -51,6 +52,7 @@
51
52
  "ink": "^6.2.3",
52
53
  "ink-spinner": "^5.0.0",
53
54
  "ink-text-input": "^6.0.0",
55
+ "open": "^10.1.0",
54
56
  "react": "^19.1.1"
55
57
  },
56
58
  "devDependencies": {
@@ -58,13 +60,13 @@
58
60
  "@semantic-release/git": "^10.0.1",
59
61
  "@types/node": "^24.3.0",
60
62
  "@types/react": "^19.1.12",
63
+ "@vitest/coverage-v8": "^2.1.3",
61
64
  "ink-testing-library": "^3.0.0",
62
65
  "pkg": "^5.8.1",
63
66
  "semantic-release": "^24.2.7",
64
67
  "tsup": "^8.5.0",
65
68
  "typescript": "^5.9.2",
66
- "vitest": "^2.1.3",
67
- "@vitest/coverage-v8": "^2.1.3"
69
+ "vitest": "^2.1.3"
68
70
  },
69
71
  "repository": {
70
72
  "type": "git",