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 +14 -0
- package/README.md +35 -7
- package/dist/index.js +561 -115
- package/package.json +6 -4
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
|
[](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
|
|
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
|
-
- **
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
158
|
-
import { Box as
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
}, [
|
|
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__ */
|
|
347
|
-
|
|
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
|
|
566
|
+
organizations.length === 0 && /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: "No organisations found" })
|
|
356
567
|
] }),
|
|
357
|
-
/* @__PURE__ */
|
|
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
|
|
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
|
|
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
|
-
|
|
1189
|
+
!!visibilityLabel && /* @__PURE__ */ jsxs12(Text13, { color: "yellow", children: [
|
|
966
1190
|
"Visibility: ",
|
|
967
|
-
|
|
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
|
|
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
|
|
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/
|
|
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] =
|
|
2426
|
-
const [token, setToken] =
|
|
2427
|
-
const [input, setInput] =
|
|
2428
|
-
const [error, setError] =
|
|
2429
|
-
const [viewer, setViewer] =
|
|
2430
|
-
const [rateLimitReset, setRateLimitReset] =
|
|
2431
|
-
const [wasRateLimited, setWasRateLimited] =
|
|
2432
|
-
const [orgContext, setOrgContext] =
|
|
2433
|
-
const [
|
|
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("
|
|
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("
|
|
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
|
-
|
|
2976
|
+
setTokenSource("pat");
|
|
2977
|
+
setMode("auth_method_selection");
|
|
2554
2978
|
};
|
|
2555
|
-
|
|
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("
|
|
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__ */
|
|
2581
|
-
/* @__PURE__ */
|
|
2582
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
3021
|
+
process.env.GH_MANAGER_DEBUG === "1" && /* @__PURE__ */ jsx17(Text17, { backgroundColor: "blue", color: "white", children: " debug mode " })
|
|
2591
3022
|
] }),
|
|
2592
|
-
viewer && /* @__PURE__ */
|
|
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__ */
|
|
3046
|
+
return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
|
|
2616
3047
|
header,
|
|
2617
|
-
/* @__PURE__ */
|
|
2618
|
-
/* @__PURE__ */
|
|
2619
|
-
/* @__PURE__ */
|
|
2620
|
-
/* @__PURE__ */
|
|
2621
|
-
rateLimitReset && /* @__PURE__ */
|
|
2622
|
-
/* @__PURE__ */
|
|
2623
|
-
/* @__PURE__ */
|
|
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__ */
|
|
3056
|
+
/* @__PURE__ */ jsx17(Text17, { bold: true, children: formatResetTime(rateLimitReset) })
|
|
2626
3057
|
] }),
|
|
2627
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2634
|
-
/* @__PURE__ */
|
|
2635
|
-
/* @__PURE__ */
|
|
2636
|
-
/* @__PURE__ */
|
|
2637
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2642
|
-
/* @__PURE__ */
|
|
2643
|
-
" - Logout and
|
|
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__ */
|
|
2646
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
3102
|
+
return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
|
|
2657
3103
|
header,
|
|
2658
|
-
/* @__PURE__ */
|
|
2659
|
-
/* @__PURE__ */
|
|
2660
|
-
/* @__PURE__ */
|
|
2661
|
-
/* @__PURE__ */
|
|
2662
|
-
/* @__PURE__ */
|
|
2663
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2674
|
-
/* @__PURE__ */
|
|
2675
|
-
/* @__PURE__ */
|
|
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__ */
|
|
3126
|
+
return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
|
|
2681
3127
|
header,
|
|
2682
|
-
/* @__PURE__ */
|
|
2683
|
-
/* @__PURE__ */
|
|
2684
|
-
mode === "validating" && /* @__PURE__ */
|
|
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__ */
|
|
3135
|
+
return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
|
|
2690
3136
|
header,
|
|
2691
|
-
/* @__PURE__ */
|
|
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__ */
|
|
3140
|
+
return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", height: dims.rows, paddingX: 2, paddingTop: verticalPadding, paddingBottom: verticalPadding, children: [
|
|
2695
3141
|
header,
|
|
2696
|
-
/* @__PURE__ */
|
|
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
|
|
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__ */
|
|
2746
|
-
/* @__PURE__ */
|
|
2747
|
-
/* @__PURE__ */
|
|
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.
|
|
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",
|