git-push-time 1.1.2 → 1.1.4
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/.github/workflows/npm-publish.yml +59 -32
- package/README.md +110 -112
- package/bin/index.js +321 -339
- package/lib/commands/edit-time.js +141 -141
- package/lib/commands/fix.js +113 -113
- package/lib/commands/github.js +429 -445
- package/lib/commands/pull.js +90 -90
- package/lib/commands/push.js +720 -720
- package/lib/config.js +220 -220
- package/lib/fix.js +286 -286
- package/lib/git.js +525 -516
- package/lib/github.js +322 -322
- package/lib/prompt-helpers.js +19 -20
- package/lib/secrets.js +92 -92
- package/lib/ui.js +1057 -1063
- package/lib/utils.js +120 -120
- package/package.json +36 -36
package/lib/commands/github.js
CHANGED
|
@@ -1,445 +1,429 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import ora from 'ora';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import {
|
|
5
|
-
getCurrentGitHubUser,
|
|
6
|
-
verifyAndFetchUser,
|
|
7
|
-
listUserRepos,
|
|
8
|
-
createGitHubRepo,
|
|
9
|
-
initiateDeviceFlow,
|
|
10
|
-
pollDeviceToken,
|
|
11
|
-
openBrowser
|
|
12
|
-
} from '../github.js';
|
|
13
|
-
import {
|
|
14
|
-
saveGitHubToken,
|
|
15
|
-
removeGitHubToken,
|
|
16
|
-
getAllGitHubAccounts,
|
|
17
|
-
setActiveGitHubAccount
|
|
18
|
-
} from '../config.js';
|
|
19
|
-
import { selectWithBack, inputWithBack, selectBox, BACK } from '../prompt-helpers.js';
|
|
20
|
-
import {
|
|
21
|
-
printSection,
|
|
22
|
-
printGitHubProfile,
|
|
23
|
-
printDeviceCodePrompt,
|
|
24
|
-
printRepoCreatedBox,
|
|
25
|
-
printErrorBox,
|
|
26
|
-
printCancelled
|
|
27
|
-
} from '../ui.js';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
*
|
|
31
|
-
*/
|
|
32
|
-
export async function showGitHubStatus() {
|
|
33
|
-
const spinner = ora({ text: '
|
|
34
|
-
const info = await getCurrentGitHubUser();
|
|
35
|
-
spinner.stop();
|
|
36
|
-
|
|
37
|
-
if (!info.authenticated) {
|
|
38
|
-
printSection('
|
|
39
|
-
console.log(chalk.hex('#F59E0B')(' [!]
|
|
40
|
-
console.log(chalk.hex('#94A3B8')('
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
printGitHubProfile(info.user, info.source);
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
*
|
|
50
|
-
*/
|
|
51
|
-
async function loginViaDeviceFlow() {
|
|
52
|
-
const initSpinner = ora({ text: '
|
|
53
|
-
const init = await initiateDeviceFlow();
|
|
54
|
-
initSpinner.stop();
|
|
55
|
-
|
|
56
|
-
if (!init.ok || !init.data) {
|
|
57
|
-
printErrorBox('
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const { device_code, user_code, verification_uri, interval, expires_in } = init.data;
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
printDeviceCodePrompt({ userCode: user_code, verificationUri: verification_uri });
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
openBrowser(verification_uri);
|
|
68
|
-
|
|
69
|
-
const pollSpinner = ora({
|
|
70
|
-
text: chalk.hex('#38BDF8')('
|
|
71
|
-
color: 'cyan'
|
|
72
|
-
}).start();
|
|
73
|
-
|
|
74
|
-
const pollResult = await pollDeviceToken(device_code, interval, expires_in, (status) => {
|
|
75
|
-
if (status === 'slow_down') {
|
|
76
|
-
pollSpinner.text = chalk.hex('#F59E0B')('GitHub
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
if (!pollResult.ok || !pollResult.token) {
|
|
81
|
-
pollSpinner.fail(chalk.red('
|
|
82
|
-
printErrorBox('
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
pollSpinner.text = '
|
|
87
|
-
const userRes = await verifyAndFetchUser(pollResult.token);
|
|
88
|
-
if (!userRes.ok) {
|
|
89
|
-
pollSpinner.fail(chalk.red('
|
|
90
|
-
printErrorBox('
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
saveGitHubToken(pollResult.token, userRes.user);
|
|
95
|
-
pollSpinner.succeed(chalk.hex('#34D399').bold(
|
|
96
|
-
printGitHubProfile(userRes.user, 'oauth-device');
|
|
97
|
-
return userRes.user;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
*
|
|
102
|
-
*/
|
|
103
|
-
async function loginViaToken() {
|
|
104
|
-
printSection('
|
|
105
|
-
console.log(chalk.hex('#38BDF8')(' »
|
|
106
|
-
console.log(chalk.hex('#94A3B8')(' 1.
|
|
107
|
-
console.log(chalk.hex('#94A3B8')(' 2.
|
|
108
|
-
console.log(chalk.hex('#94A3B8')(' 3.
|
|
109
|
-
console.log(chalk.hex('#94A3B8')(' 4. Copy
|
|
110
|
-
|
|
111
|
-
const tokenInput = await inputWithBack({
|
|
112
|
-
message: 'GitHub
|
|
113
|
-
hint: ['
|
|
114
|
-
validate: (val) => {
|
|
115
|
-
if (!val || !val.trim()) return '
|
|
116
|
-
if (val.trim().length < 15) return '
|
|
117
|
-
return true;
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
if (tokenInput === BACK) return null;
|
|
122
|
-
|
|
123
|
-
const spinner = ora({ text: '
|
|
124
|
-
const res = await verifyAndFetchUser(tokenInput.trim());
|
|
125
|
-
if (!res.ok) {
|
|
126
|
-
spinner.fail(chalk.red('
|
|
127
|
-
printErrorBox('TOKEN
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
saveGitHubToken(tokenInput.trim(), res.user);
|
|
132
|
-
spinner.succeed(chalk.hex('#34D399')(
|
|
133
|
-
printGitHubProfile(res.user, 'config');
|
|
134
|
-
return res.user;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
*
|
|
139
|
-
*/
|
|
140
|
-
export async function promptGitHubLogin() {
|
|
141
|
-
printSection('
|
|
142
|
-
|
|
143
|
-
const method = await selectWithBack({
|
|
144
|
-
message: '
|
|
145
|
-
choices: [
|
|
146
|
-
{
|
|
147
|
-
name: '[OAuth]
|
|
148
|
-
value: 'oauth',
|
|
149
|
-
description: '
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
name: '[Token]
|
|
153
|
-
value: 'token',
|
|
154
|
-
description: '
|
|
155
|
-
}
|
|
156
|
-
]
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
if (method === BACK) return null;
|
|
160
|
-
if (method === 'oauth') return await loginViaDeviceFlow();
|
|
161
|
-
if (method === 'token') return await loginViaToken();
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
*
|
|
167
|
-
* @returns {Promise<{ useGitHub: boolean, user?: any, back?: boolean }>}
|
|
168
|
-
*/
|
|
169
|
-
export async function promptSelectGitHubAccount({ clearPromptOnDone = false } = {}) {
|
|
170
|
-
if (!clearPromptOnDone) printSection('
|
|
171
|
-
|
|
172
|
-
const accounts = getAllGitHubAccounts();
|
|
173
|
-
const currentStatus = await getCurrentGitHubUser();
|
|
174
|
-
|
|
175
|
-
if (accounts.length === 0 && !currentStatus.authenticated) {
|
|
176
|
-
//
|
|
177
|
-
const loginChoice = await selectWithBack({
|
|
178
|
-
message: '
|
|
179
|
-
clearPromptOnDone,
|
|
180
|
-
choices: [
|
|
181
|
-
{
|
|
182
|
-
name: '
|
|
183
|
-
value: 'login',
|
|
184
|
-
description: '
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
name: '
|
|
188
|
-
value: 'skip',
|
|
189
|
-
description: '
|
|
190
|
-
}
|
|
191
|
-
]
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
if (loginChoice === BACK) return { back: true };
|
|
195
|
-
if (loginChoice === 'login') {
|
|
196
|
-
const user = await promptGitHubLogin();
|
|
197
|
-
if (user) {
|
|
198
|
-
return { useGitHub: true, user };
|
|
199
|
-
}
|
|
200
|
-
//
|
|
201
|
-
return { useGitHub: false };
|
|
202
|
-
}
|
|
203
|
-
return { useGitHub: false };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
//
|
|
207
|
-
const currentLogin = currentStatus.authenticated ? currentStatus.user.login : accounts[0]?.login;
|
|
208
|
-
const choices = [
|
|
209
|
-
{
|
|
210
|
-
name: `
|
|
211
|
-
value: `use:${currentLogin}`,
|
|
212
|
-
description: `
|
|
213
|
-
}
|
|
214
|
-
];
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
for (const acc of accounts) {
|
|
218
|
-
if (acc.login !== currentLogin) {
|
|
219
|
-
choices.push({
|
|
220
|
-
name:
|
|
221
|
-
value: `switch:${acc.login}`,
|
|
222
|
-
description: `
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
choices.push(
|
|
228
|
-
{
|
|
229
|
-
name: '+
|
|
230
|
-
value: 'add_account',
|
|
231
|
-
description: '
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
name: '
|
|
235
|
-
value: 'skip',
|
|
236
|
-
description: '
|
|
237
|
-
}
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
const picked = await selectWithBack({
|
|
241
|
-
message: '
|
|
242
|
-
clearPromptOnDone,
|
|
243
|
-
choices
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
if (picked === BACK) return { back: true };
|
|
247
|
-
if (picked === 'skip') return { useGitHub: false };
|
|
248
|
-
|
|
249
|
-
if (picked === 'add_account') {
|
|
250
|
-
const newUser = await promptGitHubLogin();
|
|
251
|
-
if (newUser) return { useGitHub: true, user: newUser };
|
|
252
|
-
return { useGitHub: true, user: currentStatus.user };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (picked.startsWith('switch:')) {
|
|
256
|
-
const targetLogin = picked.replace('switch:', '');
|
|
257
|
-
setActiveGitHubAccount(targetLogin);
|
|
258
|
-
const updated = await getCurrentGitHubUser();
|
|
259
|
-
console.log(chalk.hex('#34D399')(` [OK]
|
|
260
|
-
return { useGitHub: true, user: updated.user };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return { useGitHub: true, user: currentStatus.user };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
*
|
|
268
|
-
* @param {object} options
|
|
269
|
-
* @param {string} options.targetDir
|
|
270
|
-
* @returns {Promise<{ ok: boolean, repo?: any, action?: 'push' | 'done', back?: boolean }>}
|
|
271
|
-
*/
|
|
272
|
-
export async function promptCreateRepo({ targetDir = process.cwd() } = {}) {
|
|
273
|
-
let currentUser = await getCurrentGitHubUser();
|
|
274
|
-
if (!currentUser.authenticated) {
|
|
275
|
-
console.log(chalk.hex('#F59E0B')('\n [!]
|
|
276
|
-
const loginUser = await promptGitHubLogin();
|
|
277
|
-
if (!loginUser) {
|
|
278
|
-
return { ok: false, back: true };
|
|
279
|
-
}
|
|
280
|
-
currentUser = { authenticated: true, user: loginUser };
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
printSection('
|
|
284
|
-
console.log(chalk.hex('#94A3B8')(` »
|
|
285
|
-
|
|
286
|
-
const folderName = path.basename(path.resolve(targetDir))
|
|
287
|
-
.toLowerCase()
|
|
288
|
-
.replace(/[^a-z0-9_.-]/g, '-');
|
|
289
|
-
|
|
290
|
-
const repoName = await inputWithBack({
|
|
291
|
-
message: '
|
|
292
|
-
hint: ['
|
|
293
|
-
default: folderName || 'my-awesome-repo',
|
|
294
|
-
validate: (val) => {
|
|
295
|
-
const trimmed = (val || '').trim();
|
|
296
|
-
if (!trimmed) return '
|
|
297
|
-
if (!/^[a-zA-Z0-9_.-]+$/.test(trimmed)) {
|
|
298
|
-
return '
|
|
299
|
-
}
|
|
300
|
-
return true;
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
if (repoName === BACK) return { ok: false, back: true };
|
|
305
|
-
|
|
306
|
-
const description = await inputWithBack({
|
|
307
|
-
message: '
|
|
308
|
-
hint: ['
|
|
309
|
-
default: ''
|
|
310
|
-
});
|
|
311
|
-
if (description === BACK) return { ok: false, back: true };
|
|
312
|
-
|
|
313
|
-
const visibility = await selectWithBack({
|
|
314
|
-
message: '
|
|
315
|
-
choices: [
|
|
316
|
-
{ name: 'Public (
|
|
317
|
-
{ name: 'Private (
|
|
318
|
-
]
|
|
319
|
-
});
|
|
320
|
-
if (visibility === BACK) return { ok: false, back: true };
|
|
321
|
-
|
|
322
|
-
const isPrivate = visibility === 'private';
|
|
323
|
-
|
|
324
|
-
const spinner = ora({ text:
|
|
325
|
-
const createRes = await createGitHubRepo({
|
|
326
|
-
name: repoName,
|
|
327
|
-
description: String(description || ''),
|
|
328
|
-
isPrivate,
|
|
329
|
-
autoInit: false
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
if (!createRes.ok) {
|
|
333
|
-
spinner.fail(chalk.red('
|
|
334
|
-
printErrorBox('
|
|
335
|
-
return { ok: false, error: createRes.error };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
spinner.succeed(chalk.hex('#34D399')(
|
|
339
|
-
printRepoCreatedBox(createRes.repo);
|
|
340
|
-
|
|
341
|
-
return { ok: true, repo: createRes.repo };
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
*
|
|
346
|
-
*/
|
|
347
|
-
export async function showUserRepos({ limit = 15 } = {}) {
|
|
348
|
-
const spinner = ora({ text: '
|
|
349
|
-
const res = await listUserRepos({ limit });
|
|
350
|
-
spinner.stop();
|
|
351
|
-
|
|
352
|
-
if (!res.ok) {
|
|
353
|
-
printErrorBox('
|
|
354
|
-
return [];
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (res.repos.length === 0) {
|
|
358
|
-
console.log(chalk.hex('#94A3B8')(' »
|
|
359
|
-
return [];
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
printSection(`
|
|
363
|
-
res.repos.forEach((r, idx) => {
|
|
364
|
-
const lock = r.private ? chalk.hex('#F59E0B')('[Private]') : chalk.hex('#10B981')('[Public ]');
|
|
365
|
-
const num = chalk.hex('#64748B')(String(idx + 1).padStart(2, ' '));
|
|
366
|
-
const name = chalk.hex('#F8FAFC').bold(r.name);
|
|
367
|
-
const url = chalk.hex('#38BDF8')(r.clone_url);
|
|
368
|
-
console.log(` ${num}. ${lock} ${name}`);
|
|
369
|
-
console.log(` ${chalk.hex('#64748B')('URL:')} ${url}`);
|
|
370
|
-
});
|
|
371
|
-
console.log('');
|
|
372
|
-
return res.repos;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
*
|
|
377
|
-
*/
|
|
378
|
-
export function logoutGitHub(login = null) {
|
|
379
|
-
const accounts = getAllGitHubAccounts();
|
|
380
|
-
const target = login || accounts.find((a) => a.active)?.login || accounts[0]?.login || 'GitHub';
|
|
381
|
-
removeGitHubToken(login);
|
|
382
|
-
console.log(chalk.hex('#34D399')(`\n [OK]
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
*
|
|
387
|
-
*/
|
|
388
|
-
export async function runGitHubMenu(
|
|
389
|
-
while (true) {
|
|
390
|
-
const userStatus = await getCurrentGitHubUser();
|
|
391
|
-
const isLogged = userStatus.authenticated;
|
|
392
|
-
|
|
393
|
-
printSection('
|
|
394
|
-
if (isLogged) {
|
|
395
|
-
console.log(chalk.hex('#34D399')(` [✓]
|
|
396
|
-
} else {
|
|
397
|
-
console.log(chalk.hex('#F59E0B')(' [!]
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const choices = [
|
|
401
|
-
{ name: '
|
|
402
|
-
{ name: '
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
{ name: 'Có, đẩy dự án lên repo này ngay lập tức', value: 'push', description: 'Tự động gán origin và chuyển sang màn hình push' },
|
|
431
|
-
{ name: 'Không, chỉ tạo repo thôi', value: 'skip', description: 'Hoàn tất' }
|
|
432
|
-
]
|
|
433
|
-
});
|
|
434
|
-
if (askPush === 'push') {
|
|
435
|
-
return { autoPush: true, repoUrl: res.repo.cloneUrl };
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
} else if (action === 'list') {
|
|
439
|
-
await showUserRepos();
|
|
440
|
-
} else if (action === 'logout') {
|
|
441
|
-
logoutGitHub();
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import {
|
|
5
|
+
getCurrentGitHubUser,
|
|
6
|
+
verifyAndFetchUser,
|
|
7
|
+
listUserRepos,
|
|
8
|
+
createGitHubRepo,
|
|
9
|
+
initiateDeviceFlow,
|
|
10
|
+
pollDeviceToken,
|
|
11
|
+
openBrowser
|
|
12
|
+
} from '../github.js';
|
|
13
|
+
import {
|
|
14
|
+
saveGitHubToken,
|
|
15
|
+
removeGitHubToken,
|
|
16
|
+
getAllGitHubAccounts,
|
|
17
|
+
setActiveGitHubAccount
|
|
18
|
+
} from '../config.js';
|
|
19
|
+
import { selectWithBack, inputWithBack, selectBox, BACK } from '../prompt-helpers.js';
|
|
20
|
+
import {
|
|
21
|
+
printSection,
|
|
22
|
+
printGitHubProfile,
|
|
23
|
+
printDeviceCodePrompt,
|
|
24
|
+
printRepoCreatedBox,
|
|
25
|
+
printErrorBox,
|
|
26
|
+
printCancelled
|
|
27
|
+
} from '../ui.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Display the currently connected GitHub account.
|
|
31
|
+
*/
|
|
32
|
+
export async function showGitHubStatus() {
|
|
33
|
+
const spinner = ora({ text: 'Checking the GitHub connection…', color: 'cyan' }).start();
|
|
34
|
+
const info = await getCurrentGitHubUser();
|
|
35
|
+
spinner.stop();
|
|
36
|
+
|
|
37
|
+
if (!info.authenticated) {
|
|
38
|
+
printSection('GITHUB STATUS');
|
|
39
|
+
console.log(chalk.hex('#F59E0B')(' [!] No GitHub account is connected.'));
|
|
40
|
+
console.log(chalk.hex('#94A3B8')(' Run "git-push-time github login" to connect an account in your browser.\n'));
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
printGitHubProfile(info.user, info.source);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Sign in to GitHub with OAuth Device Flow.
|
|
50
|
+
*/
|
|
51
|
+
async function loginViaDeviceFlow() {
|
|
52
|
+
const initSpinner = ora({ text: 'Connecting to GitHub OAuth…', color: 'cyan' }).start();
|
|
53
|
+
const init = await initiateDeviceFlow();
|
|
54
|
+
initSpinner.stop();
|
|
55
|
+
|
|
56
|
+
if (!init.ok || !init.data) {
|
|
57
|
+
printErrorBox('OAUTH CONNECTION FAILED', [init.error || 'GitHub did not return a verification code.']);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const { device_code, user_code, verification_uri, interval, expires_in } = init.data;
|
|
62
|
+
|
|
63
|
+
// Show the verification code to the user.
|
|
64
|
+
printDeviceCodePrompt({ userCode: user_code, verificationUri: verification_uri });
|
|
65
|
+
|
|
66
|
+
// Open the browser automatically.
|
|
67
|
+
openBrowser(verification_uri);
|
|
68
|
+
|
|
69
|
+
const pollSpinner = ora({
|
|
70
|
+
text: chalk.hex('#38BDF8')('Waiting for you to authorize access in your browser…'),
|
|
71
|
+
color: 'cyan'
|
|
72
|
+
}).start();
|
|
73
|
+
|
|
74
|
+
const pollResult = await pollDeviceToken(device_code, interval, expires_in, (status) => {
|
|
75
|
+
if (status === 'slow_down') {
|
|
76
|
+
pollSpinner.text = chalk.hex('#F59E0B')('GitHub requested a longer polling interval. Still waiting for authorization…');
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!pollResult.ok || !pollResult.token) {
|
|
81
|
+
pollSpinner.fail(chalk.red('Authorization failed.'));
|
|
82
|
+
printErrorBox('SIGN-IN FAILED', [pollResult.error]);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
pollSpinner.text = 'Loading account information…';
|
|
87
|
+
const userRes = await verifyAndFetchUser(pollResult.token);
|
|
88
|
+
if (!userRes.ok) {
|
|
89
|
+
pollSpinner.fail(chalk.red('Could not load the GitHub profile.'));
|
|
90
|
+
printErrorBox('PROFILE ERROR', [userRes.error]);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
saveGitHubToken(pollResult.token, userRes.user);
|
|
95
|
+
pollSpinner.succeed(chalk.hex('#34D399').bold(`Signed in successfully. Welcome, @${userRes.user.login}.`));
|
|
96
|
+
printGitHubProfile(userRes.user, 'oauth-device');
|
|
97
|
+
return userRes.user;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Sign in to GitHub with a Personal Access Token (PAT).
|
|
102
|
+
*/
|
|
103
|
+
async function loginViaToken() {
|
|
104
|
+
printSection('SIGN IN WITH A PERSONAL ACCESS TOKEN');
|
|
105
|
+
console.log(chalk.hex('#38BDF8')(' » To create a token:'));
|
|
106
|
+
console.log(chalk.hex('#94A3B8')(' 1. Open: https://github.com/settings/tokens'));
|
|
107
|
+
console.log(chalk.hex('#94A3B8')(' 2. Select "Generate new token (classic)"'));
|
|
108
|
+
console.log(chalk.hex('#94A3B8')(' 3. Grant the ') + chalk.hex('#34D399').bold('repo') + chalk.hex('#94A3B8')(' scope (full control of repositories)'));
|
|
109
|
+
console.log(chalk.hex('#94A3B8')(' 4. Copy the token (starts with ghp_ or github_pat_) and paste it below.\n'));
|
|
110
|
+
|
|
111
|
+
const tokenInput = await inputWithBack({
|
|
112
|
+
message: 'GitHub token (PAT)',
|
|
113
|
+
hint: ['Starts with ghp_ or github_pat_ · requires the repo scope'],
|
|
114
|
+
validate: (val) => {
|
|
115
|
+
if (!val || !val.trim()) return 'Token cannot be empty.';
|
|
116
|
+
if (val.trim().length < 15) return 'This token seems too short. Check it and try again.';
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (tokenInput === BACK) return null;
|
|
122
|
+
|
|
123
|
+
const spinner = ora({ text: 'Verifying token with GitHub…', color: 'cyan' }).start();
|
|
124
|
+
const res = await verifyAndFetchUser(tokenInput.trim());
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
spinner.fail(chalk.red('Verification failed.'));
|
|
127
|
+
printErrorBox('INVALID TOKEN', [res.error], 'Check that the token is active and has the "repo" scope.');
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
saveGitHubToken(tokenInput.trim(), res.user);
|
|
132
|
+
spinner.succeed(chalk.hex('#34D399')(`Signed in successfully. Welcome, @${res.user.login}.`));
|
|
133
|
+
printGitHubProfile(res.user, 'config');
|
|
134
|
+
return res.user;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Flexible GitHub sign-in menu (OAuth Device Flow or token).
|
|
139
|
+
*/
|
|
140
|
+
export async function promptGitHubLogin() {
|
|
141
|
+
printSection('SIGN IN TO GITHUB');
|
|
142
|
+
|
|
143
|
+
const method = await selectWithBack({
|
|
144
|
+
message: 'Choose a sign-in method:',
|
|
145
|
+
choices: [
|
|
146
|
+
{
|
|
147
|
+
name: '[OAuth] Authorize in your browser (Device Flow) — Recommended',
|
|
148
|
+
value: 'oauth',
|
|
149
|
+
description: 'Open the browser and enter a verification code; no token copy is needed'
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
name: '[Token] Paste a Personal Access Token (PAT)',
|
|
153
|
+
value: 'token',
|
|
154
|
+
description: 'Enter a token created at github.com/settings/tokens'
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
if (method === BACK) return null;
|
|
160
|
+
if (method === 'oauth') return await loginViaDeviceFlow();
|
|
161
|
+
if (method === 'token') return await loginViaToken();
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* GitHub account selection step in the push wizard.
|
|
167
|
+
* @returns {Promise<{ useGitHub: boolean, user?: any, back?: boolean }>}
|
|
168
|
+
*/
|
|
169
|
+
export async function promptSelectGitHubAccount({ clearPromptOnDone = false } = {}) {
|
|
170
|
+
if (!clearPromptOnDone) printSection('CHOOSE A GITHUB ACCOUNT');
|
|
171
|
+
|
|
172
|
+
const accounts = getAllGitHubAccounts();
|
|
173
|
+
const currentStatus = await getCurrentGitHubUser();
|
|
174
|
+
|
|
175
|
+
if (accounts.length === 0 && !currentStatus.authenticated) {
|
|
176
|
+
// No account is connected.
|
|
177
|
+
const loginChoice = await selectWithBack({
|
|
178
|
+
message: 'No GitHub account is connected. What would you like to do?',
|
|
179
|
+
clearPromptOnDone,
|
|
180
|
+
choices: [
|
|
181
|
+
{
|
|
182
|
+
name: 'Sign in to GitHub (authorize in your browser)',
|
|
183
|
+
value: 'login',
|
|
184
|
+
description: 'Connect an account to choose or create repositories'
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'Skip GitHub (enter a repository URL manually)',
|
|
188
|
+
value: 'skip',
|
|
189
|
+
description: 'Continue without a GitHub account and enter an HTTPS URL'
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
if (loginChoice === BACK) return { back: true };
|
|
195
|
+
if (loginChoice === 'login') {
|
|
196
|
+
const user = await promptGitHubLogin();
|
|
197
|
+
if (user) {
|
|
198
|
+
return { useGitHub: true, user };
|
|
199
|
+
}
|
|
200
|
+
// If sign-in is canceled or fails, allow the user to enter a URL manually.
|
|
201
|
+
return { useGitHub: false };
|
|
202
|
+
}
|
|
203
|
+
return { useGitHub: false };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// An account is already connected.
|
|
207
|
+
const currentLogin = currentStatus.authenticated ? currentStatus.user.login : accounts[0]?.login;
|
|
208
|
+
const choices = [
|
|
209
|
+
{
|
|
210
|
+
name: `Continue as @${currentLogin} (Current account)`,
|
|
211
|
+
value: `use:${currentLogin}`,
|
|
212
|
+
description: `Use @${currentLogin} to connect a repository`
|
|
213
|
+
}
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
// List the other accounts.
|
|
217
|
+
for (const acc of accounts) {
|
|
218
|
+
if (acc.login !== currentLogin) {
|
|
219
|
+
choices.push({
|
|
220
|
+
name: `Switch to @${acc.login}`,
|
|
221
|
+
value: `switch:${acc.login}`,
|
|
222
|
+
description: `Use the @${acc.login} account`
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
choices.push(
|
|
228
|
+
{
|
|
229
|
+
name: '+ Sign in to another GitHub account',
|
|
230
|
+
value: 'add_account',
|
|
231
|
+
description: 'Add an account using OAuth or a token'
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'Skip GitHub (enter a repository URL manually)',
|
|
235
|
+
value: 'skip',
|
|
236
|
+
description: 'Enter a repository URL manually'
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const picked = await selectWithBack({
|
|
241
|
+
message: 'Choose the GitHub account to use:',
|
|
242
|
+
clearPromptOnDone,
|
|
243
|
+
choices
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (picked === BACK) return { back: true };
|
|
247
|
+
if (picked === 'skip') return { useGitHub: false };
|
|
248
|
+
|
|
249
|
+
if (picked === 'add_account') {
|
|
250
|
+
const newUser = await promptGitHubLogin();
|
|
251
|
+
if (newUser) return { useGitHub: true, user: newUser };
|
|
252
|
+
return { useGitHub: true, user: currentStatus.user };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (picked.startsWith('switch:')) {
|
|
256
|
+
const targetLogin = picked.replace('switch:', '');
|
|
257
|
+
setActiveGitHubAccount(targetLogin);
|
|
258
|
+
const updated = await getCurrentGitHubUser();
|
|
259
|
+
console.log(chalk.hex('#34D399')(` [OK] Switched to @${targetLogin}.\n`));
|
|
260
|
+
return { useGitHub: true, user: updated.user };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { useGitHub: true, user: currentStatus.user };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Create a new GitHub repository.
|
|
268
|
+
* @param {object} options
|
|
269
|
+
* @param {string} options.targetDir Source folder (used to suggest a default repository name)
|
|
270
|
+
* @returns {Promise<{ ok: boolean, repo?: any, action?: 'push' | 'done', back?: boolean }>}
|
|
271
|
+
*/
|
|
272
|
+
export async function promptCreateRepo({ targetDir = process.cwd() } = {}) {
|
|
273
|
+
let currentUser = await getCurrentGitHubUser();
|
|
274
|
+
if (!currentUser.authenticated) {
|
|
275
|
+
console.log(chalk.hex('#F59E0B')('\n [!] Connect a GitHub account before creating a repository.'));
|
|
276
|
+
const loginUser = await promptGitHubLogin();
|
|
277
|
+
if (!loginUser) {
|
|
278
|
+
return { ok: false, back: true };
|
|
279
|
+
}
|
|
280
|
+
currentUser = { authenticated: true, user: loginUser };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
printSection('CREATE A GITHUB REPOSITORY');
|
|
284
|
+
console.log(chalk.hex('#94A3B8')(` » Creating under account: ${chalk.hex('#38BDF8').bold('@' + currentUser.user.login)}`));
|
|
285
|
+
|
|
286
|
+
const folderName = path.basename(path.resolve(targetDir))
|
|
287
|
+
.toLowerCase()
|
|
288
|
+
.replace(/[^a-z0-9_.-]/g, '-');
|
|
289
|
+
|
|
290
|
+
const repoName = await inputWithBack({
|
|
291
|
+
message: 'New repository name',
|
|
292
|
+
hint: ['Letters, numbers, -, _, and . only (e.g. my-awesome-repo)'],
|
|
293
|
+
default: folderName || 'my-awesome-repo',
|
|
294
|
+
validate: (val) => {
|
|
295
|
+
const trimmed = (val || '').trim();
|
|
296
|
+
if (!trimmed) return 'Repository name cannot be empty.';
|
|
297
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(trimmed)) {
|
|
298
|
+
return 'Use only letters, numbers, hyphens (-), underscores (_), or periods (.).';
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (repoName === BACK) return { ok: false, back: true };
|
|
305
|
+
|
|
306
|
+
const description = await inputWithBack({
|
|
307
|
+
message: 'Short project description',
|
|
308
|
+
hint: ['Optional; press Enter to skip'],
|
|
309
|
+
default: ''
|
|
310
|
+
});
|
|
311
|
+
if (description === BACK) return { ok: false, back: true };
|
|
312
|
+
|
|
313
|
+
const visibility = await selectWithBack({
|
|
314
|
+
message: 'Repository visibility:',
|
|
315
|
+
choices: [
|
|
316
|
+
{ name: 'Public (Anyone can view it)', value: 'public', description: 'Public repository' },
|
|
317
|
+
{ name: 'Private (Only you and collaborators can view it)', value: 'private', description: 'Private repository' }
|
|
318
|
+
]
|
|
319
|
+
});
|
|
320
|
+
if (visibility === BACK) return { ok: false, back: true };
|
|
321
|
+
|
|
322
|
+
const isPrivate = visibility === 'private';
|
|
323
|
+
|
|
324
|
+
const spinner = ora({ text: `Creating '${repoName}' on GitHub…`, color: 'cyan' }).start();
|
|
325
|
+
const createRes = await createGitHubRepo({
|
|
326
|
+
name: repoName,
|
|
327
|
+
description: String(description || ''),
|
|
328
|
+
isPrivate,
|
|
329
|
+
autoInit: false
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (!createRes.ok) {
|
|
333
|
+
spinner.fail(chalk.red('Could not create repository.'));
|
|
334
|
+
printErrorBox('REPOSITORY CREATION FAILED', [createRes.error]);
|
|
335
|
+
return { ok: false, error: createRes.error };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
spinner.succeed(chalk.hex('#34D399')(`Created @${currentUser.user.login}/${repoName} successfully.`));
|
|
339
|
+
printRepoCreatedBox(createRes.repo);
|
|
340
|
+
|
|
341
|
+
return { ok: true, repo: createRes.repo };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Display the user's most recently updated repositories.
|
|
346
|
+
*/
|
|
347
|
+
export async function showUserRepos({ limit = 15 } = {}) {
|
|
348
|
+
const spinner = ora({ text: 'Loading repositories from GitHub…', color: 'cyan' }).start();
|
|
349
|
+
const res = await listUserRepos({ limit });
|
|
350
|
+
spinner.stop();
|
|
351
|
+
|
|
352
|
+
if (!res.ok) {
|
|
353
|
+
printErrorBox('COULD NOT LOAD REPOSITORIES', [res.error]);
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (res.repos.length === 0) {
|
|
358
|
+
console.log(chalk.hex('#94A3B8')(' » This account does not have any repositories yet.\n'));
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
printSection(`REPOSITORIES (${res.repos.length} RECENT)`);
|
|
363
|
+
res.repos.forEach((r, idx) => {
|
|
364
|
+
const lock = r.private ? chalk.hex('#F59E0B')('[Private]') : chalk.hex('#10B981')('[Public ]');
|
|
365
|
+
const num = chalk.hex('#64748B')(String(idx + 1).padStart(2, ' '));
|
|
366
|
+
const name = chalk.hex('#F8FAFC').bold(r.name);
|
|
367
|
+
const url = chalk.hex('#38BDF8')(r.clone_url);
|
|
368
|
+
console.log(` ${num}. ${lock} ${name}`);
|
|
369
|
+
console.log(` ${chalk.hex('#64748B')('URL:')} ${url}`);
|
|
370
|
+
});
|
|
371
|
+
console.log('');
|
|
372
|
+
return res.repos;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Sign out of a GitHub account (defaults to the active account).
|
|
377
|
+
*/
|
|
378
|
+
export function logoutGitHub(login = null) {
|
|
379
|
+
const accounts = getAllGitHubAccounts();
|
|
380
|
+
const target = login || accounts.find((a) => a.active)?.login || accounts[0]?.login || 'GitHub';
|
|
381
|
+
removeGitHubToken(login);
|
|
382
|
+
console.log(chalk.hex('#34D399')(`\n [OK] Signed out @${target} and removed the token from this machine.\n`));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Interactive GitHub management menu.
|
|
387
|
+
*/
|
|
388
|
+
export async function runGitHubMenu() {
|
|
389
|
+
while (true) {
|
|
390
|
+
const userStatus = await getCurrentGitHubUser();
|
|
391
|
+
const isLogged = userStatus.authenticated;
|
|
392
|
+
|
|
393
|
+
printSection('GITHUB & REPOSITORIES');
|
|
394
|
+
if (isLogged) {
|
|
395
|
+
console.log(chalk.hex('#34D399')(` [✓] Signed in as @${userStatus.user.login} (${userStatus.user.name || 'Developer'})`));
|
|
396
|
+
} else {
|
|
397
|
+
console.log(chalk.hex('#F59E0B')(' [!] GitHub is not connected.'));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const choices = [
|
|
401
|
+
{ name: 'Account', value: 'status', description: 'View profile and connection status' },
|
|
402
|
+
{ name: 'Repositories', value: 'list', description: 'Browse your repositories and clone URLs' },
|
|
403
|
+
isLogged
|
|
404
|
+
? { name: 'Sign in to another account', value: 'login', description: 'Add or switch GitHub accounts' }
|
|
405
|
+
: { name: 'Sign in', value: 'login', description: 'Connect using a browser or token' },
|
|
406
|
+
...(isLogged ? [{ name: 'Sign out', value: 'logout', description: 'Disconnect the current account' }] : []),
|
|
407
|
+
{ name: 'Go back', value: BACK, description: 'Return to the dashboard' }
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
const action = await selectBox({
|
|
411
|
+
message: 'Choose a GitHub action:',
|
|
412
|
+
hint: 'View accounts · browse repositories · switch accounts',
|
|
413
|
+
choices
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
if (action === BACK) return;
|
|
417
|
+
|
|
418
|
+
if (action === 'status') {
|
|
419
|
+
await showGitHubStatus();
|
|
420
|
+
} else if (action === 'login') {
|
|
421
|
+
await promptGitHubLogin();
|
|
422
|
+
} else if (action === 'list') {
|
|
423
|
+
await showUserRepos();
|
|
424
|
+
} else if (action === 'logout') {
|
|
425
|
+
logoutGitHub();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|