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/push.js
CHANGED
|
@@ -1,720 +1,720 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import ora from 'ora';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import { spawnSync } from 'child_process';
|
|
6
|
-
import { isValidGitUrl, parseCustomDate } from '../utils.js';
|
|
7
|
-
import {
|
|
8
|
-
isGitRepo,
|
|
9
|
-
initGitRepo,
|
|
10
|
-
getGitTopLevel,
|
|
11
|
-
listSubdirectories,
|
|
12
|
-
resolveTargetDir,
|
|
13
|
-
setGitRemote,
|
|
14
|
-
stageAllFiles,
|
|
15
|
-
hasChangesToCommit,
|
|
16
|
-
commitWithCustomDate,
|
|
17
|
-
setBranchName,
|
|
18
|
-
isValidBranchName,
|
|
19
|
-
pushToRemote,
|
|
20
|
-
getCurrentBranch,
|
|
21
|
-
findRiskyStagedFiles,
|
|
22
|
-
pullFromRemote
|
|
23
|
-
} from '../git.js';
|
|
24
|
-
import { scanStagedContentForSecrets } from '../secrets.js';
|
|
25
|
-
import {
|
|
26
|
-
autoFixStagedSecrets,
|
|
27
|
-
hasStagedChanges,
|
|
28
|
-
getUnpushedCommits,
|
|
29
|
-
squashUnpushedCommits
|
|
30
|
-
} from '../fix.js';
|
|
31
|
-
import {
|
|
32
|
-
getCurrentGitHubUser,
|
|
33
|
-
listUserRepos
|
|
34
|
-
} from '../github.js';
|
|
35
|
-
import { promptCreateRepo, promptSelectGitHubAccount } from './github.js';
|
|
36
|
-
import { selectWithBack, inputWithBack, promptContinueOrBack, selectBox, BACK } from '../prompt-helpers.js';
|
|
37
|
-
import {
|
|
38
|
-
printSummary,
|
|
39
|
-
printSection,
|
|
40
|
-
printPushProgress,
|
|
41
|
-
stepLabel,
|
|
42
|
-
printNestedRepoWarning,
|
|
43
|
-
printPrePushSecretWarning,
|
|
44
|
-
printSecretBlockedBox,
|
|
45
|
-
printSuccessBox,
|
|
46
|
-
printCancelled,
|
|
47
|
-
printErrorBox,
|
|
48
|
-
printAuthFailedBox,
|
|
49
|
-
printNonFastForwardBox,
|
|
50
|
-
printFixResult,
|
|
51
|
-
printFooter
|
|
52
|
-
} from '../ui.js';
|
|
53
|
-
|
|
54
|
-
const TOTAL_STEPS = 6;
|
|
55
|
-
const WIZARD_STEPS = 7;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
*/
|
|
60
|
-
export async function promptForTargetDir(startDir, clearPromptOnDone = false) {
|
|
61
|
-
const subs = listSubdirectories(startDir);
|
|
62
|
-
if (subs.length === 0) return startDir;
|
|
63
|
-
|
|
64
|
-
const picked = await selectWithBack({
|
|
65
|
-
message: '
|
|
66
|
-
clearPromptOnDone,
|
|
67
|
-
pageSize: Math.min(12, subs.length + 3),
|
|
68
|
-
choices: [
|
|
69
|
-
{ name: '
|
|
70
|
-
...subs.map((s) => ({
|
|
71
|
-
name: `${s.name}`,
|
|
72
|
-
value: s.fullPath,
|
|
73
|
-
description: s.fullPath
|
|
74
|
-
})),
|
|
75
|
-
{ name: '
|
|
76
|
-
]
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
if (picked === BACK) return BACK;
|
|
80
|
-
if (picked !== '__custom__') return picked;
|
|
81
|
-
|
|
82
|
-
const customPath = await inputWithBack({
|
|
83
|
-
message: '
|
|
84
|
-
hint: ['
|
|
85
|
-
clearPromptOnDone,
|
|
86
|
-
validate: (value) => {
|
|
87
|
-
const r = resolveTargetDir(value, startDir);
|
|
88
|
-
return r.ok || r.error;
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
if (customPath === BACK) return BACK;
|
|
92
|
-
return resolveTargetDir(customPath, startDir).fullPath;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
*
|
|
97
|
-
*/
|
|
98
|
-
export async function handleNestedRepo(targetDir, isInteractive, options) {
|
|
99
|
-
const topLevel = getGitTopLevel(targetDir);
|
|
100
|
-
if (!topLevel) return;
|
|
101
|
-
if (path.resolve(topLevel) === path.resolve(targetDir)) return;
|
|
102
|
-
if (fs.existsSync(path.join(targetDir, '.git'))) return;
|
|
103
|
-
|
|
104
|
-
printNestedRepoWarning({ targetDir, topLevel });
|
|
105
|
-
|
|
106
|
-
let choice = 'separate';
|
|
107
|
-
if (isInteractive && !options.yes) {
|
|
108
|
-
choice = await selectWithBack({
|
|
109
|
-
message: '
|
|
110
|
-
choices: [
|
|
111
|
-
{ name: '
|
|
112
|
-
{ name: '
|
|
113
|
-
{ name: '
|
|
114
|
-
]
|
|
115
|
-
});
|
|
116
|
-
if (choice === BACK) return BACK;
|
|
117
|
-
} else if (options.dir) {
|
|
118
|
-
choice = 'separate';
|
|
119
|
-
} else {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (choice === 'abort') {
|
|
124
|
-
printCancelled();
|
|
125
|
-
process.exit(0);
|
|
126
|
-
}
|
|
127
|
-
if (choice === 'separate') {
|
|
128
|
-
initGitRepo(targetDir);
|
|
129
|
-
console.log(chalk.hex('#34D399')(' [OK]
|
|
130
|
-
} else {
|
|
131
|
-
console.log(chalk.hex('#64748B')(' »
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Prompt
|
|
137
|
-
*/
|
|
138
|
-
async function promptForRepoSelection(targetDir, gitHubUser = null, clearPromptOnDone = false) {
|
|
139
|
-
const choices = [];
|
|
140
|
-
|
|
141
|
-
if (gitHubUser) {
|
|
142
|
-
choices.push(
|
|
143
|
-
{
|
|
144
|
-
name: `
|
|
145
|
-
value: 'pick_existing',
|
|
146
|
-
description: '
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
name: '
|
|
150
|
-
value: 'create_repo',
|
|
151
|
-
description: `
|
|
152
|
-
}
|
|
153
|
-
);
|
|
154
|
-
} else {
|
|
155
|
-
choices.push(
|
|
156
|
-
{
|
|
157
|
-
name: '
|
|
158
|
-
value: 'create_repo',
|
|
159
|
-
description: '
|
|
160
|
-
}
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
choices.push({
|
|
165
|
-
name: '
|
|
166
|
-
value: 'manual_url',
|
|
167
|
-
description: '
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const method = await selectWithBack({
|
|
171
|
-
message: '
|
|
172
|
-
clearPromptOnDone,
|
|
173
|
-
choices
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
if (method === BACK) return BACK;
|
|
177
|
-
|
|
178
|
-
if (method === 'create_repo') {
|
|
179
|
-
const res = await promptCreateRepo({ targetDir });
|
|
180
|
-
if (!res.ok || !res.repo) return BACK;
|
|
181
|
-
return res.repo.cloneUrl;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (method === 'pick_existing') {
|
|
185
|
-
const spinner = ora({ text: '
|
|
186
|
-
const res = await listUserRepos({ limit: 25 });
|
|
187
|
-
spinner.stop();
|
|
188
|
-
|
|
189
|
-
if (!res.ok || !res.repos || res.repos.length === 0) {
|
|
190
|
-
console.log(chalk.hex('#F59E0B')(' [!]
|
|
191
|
-
} else {
|
|
192
|
-
const repoChoices = res.repos.map((r) => ({
|
|
193
|
-
name: `${r.private ? chalk.hex('#F59E0B')('[Private]') : chalk.hex('#10B981')('[Public ]')} ${chalk.hex('#F8FAFC').bold(r.name)}`,
|
|
194
|
-
value: r.clone_url,
|
|
195
|
-
description: r.clone_url
|
|
196
|
-
}));
|
|
197
|
-
|
|
198
|
-
const picked = await selectWithBack({
|
|
199
|
-
message: '
|
|
200
|
-
clearPromptOnDone,
|
|
201
|
-
pageSize: Math.min(12, repoChoices.length + 2),
|
|
202
|
-
choices: repoChoices
|
|
203
|
-
});
|
|
204
|
-
if (picked !== BACK) return picked;
|
|
205
|
-
return BACK;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
const val = await inputWithBack({
|
|
211
|
-
message: '
|
|
212
|
-
hint: ['HTTPS
|
|
213
|
-
clearPromptOnDone,
|
|
214
|
-
validate: (value) => {
|
|
215
|
-
if (!value || !value.trim()) return '
|
|
216
|
-
if (!isValidGitUrl(value)) return 'URL
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
return val;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
*
|
|
226
|
-
*/
|
|
227
|
-
export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
228
|
-
const isInteractive = !options.yes;
|
|
229
|
-
const fromMenu = typeof onBackToMenu === 'function';
|
|
230
|
-
const startDir = process.cwd();
|
|
231
|
-
// Copy flags
|
|
232
|
-
//
|
|
233
|
-
const eff = { ...options };
|
|
234
|
-
|
|
235
|
-
let targetDir = startDir;
|
|
236
|
-
let selectedGitHubUser = null;
|
|
237
|
-
let repoUrl = eff.repo;
|
|
238
|
-
let branchName = eff.branch ? String(eff.branch).trim() : undefined;
|
|
239
|
-
let commitMsg = eff.message ? String(eff.message).trim() : undefined;
|
|
240
|
-
let dateInput = eff.date;
|
|
241
|
-
let parsedDate = null;
|
|
242
|
-
|
|
243
|
-
if (eff.dir) {
|
|
244
|
-
const r = resolveTargetDir(eff.dir, startDir);
|
|
245
|
-
if (!r.ok) {
|
|
246
|
-
printErrorBox('
|
|
247
|
-
process.exit(1);
|
|
248
|
-
}
|
|
249
|
-
targetDir = r.fullPath;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (!isInteractive) {
|
|
253
|
-
if (!commitMsg) commitMsg = 'Initial commit';
|
|
254
|
-
if (!dateInput) dateInput = 'now';
|
|
255
|
-
if (!branchName) branchName = 'main';
|
|
256
|
-
if (!repoUrl || !isValidGitUrl(repoUrl)) {
|
|
257
|
-
printErrorBox('
|
|
258
|
-
process.exit(1);
|
|
259
|
-
}
|
|
260
|
-
parsedDate = parseCustomDate(dateInput);
|
|
261
|
-
if (!parsedDate.valid) {
|
|
262
|
-
printErrorBox('
|
|
263
|
-
process.exit(1);
|
|
264
|
-
}
|
|
265
|
-
printSummary({ repoUrl, commitMsg, dateDisplay: parsedDate.display, branch: branchName, force: options.force, sourceDir: targetDir });
|
|
266
|
-
} else {
|
|
267
|
-
//
|
|
268
|
-
// 0: 📁
|
|
269
|
-
// 1: 👤
|
|
270
|
-
// 2: 📦
|
|
271
|
-
// 3: 🌿
|
|
272
|
-
// 4: 💬
|
|
273
|
-
// 5: ⏰
|
|
274
|
-
// 6: 🚀
|
|
275
|
-
let step = eff.dir ? 1 : 0;
|
|
276
|
-
|
|
277
|
-
//
|
|
278
|
-
if (eff.repo) {
|
|
279
|
-
repoUrl = eff.repo;
|
|
280
|
-
delete eff.repo;
|
|
281
|
-
step = 3;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
while (true) {
|
|
285
|
-
const progressTitles = ['Folder', 'Account', 'Repository', 'Branch', 'Message', 'Time', '
|
|
286
|
-
const completed = [];
|
|
287
|
-
if (step > 0) completed.push({ label: 'Folder', value: targetDir });
|
|
288
|
-
if (step > 1) completed.push({ label: 'Account', value: selectedGitHubUser?.login || 'URL
|
|
289
|
-
if (step > 2) {
|
|
290
|
-
const shortRepo = String(repoUrl || '').replace(/\.git$/i, '').match(/(?:github\.com[:/])([^/]+\/[^/]+)$/i)?.[1] || repoUrl;
|
|
291
|
-
completed.push({ label: 'Repository', value: shortRepo || '
|
|
292
|
-
}
|
|
293
|
-
if (step > 3) completed.push({ label: 'Branch', value: branchName || 'main' });
|
|
294
|
-
if (step > 4) completed.push({ label: 'Message', value: commitMsg || 'Initial commit' });
|
|
295
|
-
if (step > 5) completed.push({ label: 'Time', value: parsedDate?.display || dateInput || 'now' });
|
|
296
|
-
printPushProgress({ current: step + 1, total: WIZARD_STEPS, title: progressTitles[step], completed });
|
|
297
|
-
|
|
298
|
-
//
|
|
299
|
-
if (step === 0) {
|
|
300
|
-
const picked = await promptForTargetDir(startDir, true);
|
|
301
|
-
if (picked === BACK) {
|
|
302
|
-
if (onBackToMenu) return await onBackToMenu();
|
|
303
|
-
printCancelled();
|
|
304
|
-
process.exit(0);
|
|
305
|
-
}
|
|
306
|
-
targetDir = picked;
|
|
307
|
-
delete eff.dir;
|
|
308
|
-
const nested = await handleNestedRepo(targetDir, isInteractive, eff);
|
|
309
|
-
if (nested === BACK) continue;
|
|
310
|
-
step = 1;
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
//
|
|
315
|
-
if (step === 1) {
|
|
316
|
-
const accRes = await promptSelectGitHubAccount({ clearPromptOnDone: true });
|
|
317
|
-
if (accRes.back) {
|
|
318
|
-
//
|
|
319
|
-
delete eff.dir;
|
|
320
|
-
step = 0;
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
selectedGitHubUser = accRes.useGitHub ? accRes.user : null;
|
|
324
|
-
step = 2;
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
//
|
|
329
|
-
if (step === 2) {
|
|
330
|
-
if (eff.repo) {
|
|
331
|
-
repoUrl = eff.repo;
|
|
332
|
-
delete eff.repo;
|
|
333
|
-
step = 3;
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
const pickedUrl = await promptForRepoSelection(targetDir, selectedGitHubUser, true);
|
|
337
|
-
if (pickedUrl === BACK) {
|
|
338
|
-
step = 1;
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
repoUrl = pickedUrl;
|
|
342
|
-
if (!repoUrl || !isValidGitUrl(repoUrl)) {
|
|
343
|
-
printErrorBox('
|
|
344
|
-
|
|
345
|
-
if (retry === 'exit') { printCancelled(); process.exit(0); }
|
|
346
|
-
if (retry === 'back') step = 1;
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
step = 3;
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
//
|
|
354
|
-
if (step === 3) {
|
|
355
|
-
if (eff.branch) {
|
|
356
|
-
branchName = String(eff.branch).trim();
|
|
357
|
-
delete eff.branch;
|
|
358
|
-
step = 4;
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
const currentB = getCurrentBranch(targetDir);
|
|
362
|
-
const val = await inputWithBack({
|
|
363
|
-
message: '
|
|
364
|
-
promptLabel: '
|
|
365
|
-
hint: [
|
|
366
|
-
'
|
|
367
|
-
'
|
|
368
|
-
],
|
|
369
|
-
default: currentB || 'main',
|
|
370
|
-
clearPromptOnDone: true,
|
|
371
|
-
validate: (v) => {
|
|
372
|
-
if (!v || !v.trim()) return true;
|
|
373
|
-
return isValidBranchName(v.trim()) || '
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
if (val === BACK) {
|
|
377
|
-
step = 2;
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
branchName = String(val || currentB || 'main').trim();
|
|
381
|
-
if (!isValidBranchName(branchName)) {
|
|
382
|
-
printErrorBox('
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
step = 4;
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
//
|
|
390
|
-
if (step === 4) {
|
|
391
|
-
if (eff.message) {
|
|
392
|
-
commitMsg = String(eff.message).trim();
|
|
393
|
-
delete eff.message;
|
|
394
|
-
step = 5;
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
const val = await inputWithBack({
|
|
398
|
-
message: '
|
|
399
|
-
promptLabel: 'Commit',
|
|
400
|
-
hint: [
|
|
401
|
-
'
|
|
402
|
-
'
|
|
403
|
-
],
|
|
404
|
-
default: 'Initial commit',
|
|
405
|
-
clearPromptOnDone: true
|
|
406
|
-
});
|
|
407
|
-
if (val === BACK) {
|
|
408
|
-
step = 3;
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
commitMsg = String(val || '').trim() || 'Initial commit';
|
|
412
|
-
step = 5;
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
//
|
|
417
|
-
if (step === 5) {
|
|
418
|
-
if (eff.date) {
|
|
419
|
-
dateInput = eff.date;
|
|
420
|
-
delete eff.date;
|
|
421
|
-
step = 6;
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
const dateOption = await selectWithBack({
|
|
425
|
-
message: '
|
|
426
|
-
hint: '
|
|
427
|
-
clearPromptOnDone: true,
|
|
428
|
-
choices: [
|
|
429
|
-
{ name: '
|
|
430
|
-
{ name: '
|
|
431
|
-
{ name: '
|
|
432
|
-
]
|
|
433
|
-
});
|
|
434
|
-
if (dateOption === BACK) {
|
|
435
|
-
step = 4;
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (dateOption === 'now') {
|
|
440
|
-
dateInput = 'now';
|
|
441
|
-
} else if (dateOption === 'relative') {
|
|
442
|
-
const val = await inputWithBack({
|
|
443
|
-
message: '
|
|
444
|
-
hint: ['-2d = 2
|
|
445
|
-
default: '-1d',
|
|
446
|
-
clearPromptOnDone: true,
|
|
447
|
-
validate: (v) => { const p = parseCustomDate(v); return p.valid || p.error || '
|
|
448
|
-
});
|
|
449
|
-
if (val === BACK) continue;
|
|
450
|
-
dateInput = val;
|
|
451
|
-
} else if (dateOption === 'custom') {
|
|
452
|
-
const val = await inputWithBack({
|
|
453
|
-
message: '
|
|
454
|
-
hint: ['
|
|
455
|
-
default: '2024-01-15 10:00:00',
|
|
456
|
-
clearPromptOnDone: true,
|
|
457
|
-
validate: (v) => { const p = parseCustomDate(v); return p.valid || p.error || '
|
|
458
|
-
});
|
|
459
|
-
if (val === BACK) continue;
|
|
460
|
-
dateInput = val;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
parsedDate = parseCustomDate(dateInput || 'now');
|
|
464
|
-
if (!parsedDate.valid) {
|
|
465
|
-
printErrorBox('
|
|
466
|
-
const retry = await promptContinueOrBack('
|
|
467
|
-
if (retry === 'exit') { printCancelled(); process.exit(0); }
|
|
468
|
-
dateInput = undefined;
|
|
469
|
-
delete eff.date;
|
|
470
|
-
if (retry === 'back') step = 4;
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
step = 6;
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
//
|
|
478
|
-
if (step === 6) {
|
|
479
|
-
if (!commitMsg) commitMsg = 'Initial commit';
|
|
480
|
-
if (!dateInput) dateInput = 'now';
|
|
481
|
-
if (!branchName) branchName = 'main';
|
|
482
|
-
parsedDate = parseCustomDate(dateInput);
|
|
483
|
-
|
|
484
|
-
printSummary({
|
|
485
|
-
repoUrl,
|
|
486
|
-
commitMsg,
|
|
487
|
-
dateDisplay: parsedDate.display,
|
|
488
|
-
branch: branchName,
|
|
489
|
-
force: options.force,
|
|
490
|
-
sourceDir: targetDir
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
const decision = await selectBox({
|
|
494
|
-
message: '
|
|
495
|
-
hint: '
|
|
496
|
-
choices: [
|
|
497
|
-
{ name: '
|
|
498
|
-
{ name: '
|
|
499
|
-
{ name: '
|
|
500
|
-
{ name: '
|
|
501
|
-
{ name: '
|
|
502
|
-
{ name: '
|
|
503
|
-
{ name: '
|
|
504
|
-
{ name: '
|
|
505
|
-
{ name: '
|
|
506
|
-
]
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
if (decision === 'exit') { printCancelled(); process.exit(0); }
|
|
510
|
-
if (decision === 'continue') break;
|
|
511
|
-
if (decision === 'back-date') { dateInput = undefined; delete eff.date; parsedDate = null; step = 5; continue; }
|
|
512
|
-
if (decision === 'back-msg') { commitMsg = undefined; delete eff.message; step = 4; continue; }
|
|
513
|
-
if (decision === 'back-branch') { branchName = undefined; delete eff.branch; step = 3; continue; }
|
|
514
|
-
if (decision === 'back-repo') { repoUrl = undefined; delete eff.repo; step = 2; continue; }
|
|
515
|
-
if (decision === 'back-account') { step = 1; continue; }
|
|
516
|
-
if (decision === 'back-folder') { delete eff.dir; step = 0; continue; }
|
|
517
|
-
if (decision === 'back-menu') {
|
|
518
|
-
if (onBackToMenu) return await onBackToMenu();
|
|
519
|
-
step = options.dir ? 1 : 0;
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// 🚀
|
|
527
|
-
const spinner = ora({ color: 'cyan' });
|
|
528
|
-
let secretStatus = '✓
|
|
529
|
-
let pushDurationSeconds = null;
|
|
530
|
-
printSection(`🚀
|
|
531
|
-
|
|
532
|
-
try {
|
|
533
|
-
// 1. Git init
|
|
534
|
-
if (!isGitRepo(targetDir)) {
|
|
535
|
-
spinner.start(stepLabel(1, TOTAL_STEPS, '
|
|
536
|
-
initGitRepo(targetDir);
|
|
537
|
-
spinner.succeed(stepLabel(1, TOTAL_STEPS, chalk.hex('#34D399')('
|
|
538
|
-
} else {
|
|
539
|
-
console.log(' ' + stepLabel(1, TOTAL_STEPS, chalk.hex('#64748B')('
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// 2. Remote
|
|
543
|
-
spinner.start(stepLabel(2, TOTAL_STEPS, '
|
|
544
|
-
setGitRemote(repoUrl, targetDir);
|
|
545
|
-
spinner.succeed(stepLabel(2, TOTAL_STEPS, chalk.hex('#34D399')('
|
|
546
|
-
|
|
547
|
-
// 3. Stage
|
|
548
|
-
spinner.start(stepLabel(3, TOTAL_STEPS, 'Stage files (git add .)...'));
|
|
549
|
-
stageAllFiles(targetDir);
|
|
550
|
-
spinner.succeed(stepLabel(3, TOTAL_STEPS, chalk.hex('#34D399')('
|
|
551
|
-
|
|
552
|
-
// 3.5.
|
|
553
|
-
const risky = findRiskyStagedFiles(targetDir);
|
|
554
|
-
const findings = scanStagedContentForSecrets(targetDir);
|
|
555
|
-
if (risky.length > 0 || findings.length > 0) secretStatus = `!
|
|
556
|
-
if (risky.length > 0 || findings.length > 0) {
|
|
557
|
-
spinner.stop();
|
|
558
|
-
printPrePushSecretWarning({ riskyFiles: risky, findings });
|
|
559
|
-
const allFiles = [...new Set([...risky, ...findings.map((f) => f.file)])];
|
|
560
|
-
|
|
561
|
-
let wantFix = options.fix === true;
|
|
562
|
-
if (!wantFix && isInteractive) {
|
|
563
|
-
const fixChoice = await selectWithBack({
|
|
564
|
-
message: '
|
|
565
|
-
choices: [
|
|
566
|
-
{ name: '
|
|
567
|
-
{ name: '
|
|
568
|
-
]
|
|
569
|
-
});
|
|
570
|
-
if (fixChoice === BACK) {
|
|
571
|
-
printCancelled();
|
|
572
|
-
process.exit(0);
|
|
573
|
-
}
|
|
574
|
-
wantFix = fixChoice === 'yes';
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
if (wantFix) {
|
|
578
|
-
spinner.start('
|
|
579
|
-
const res = autoFixStagedSecrets(allFiles, targetDir);
|
|
580
|
-
spinner.succeed(chalk.hex('#34D399')(`[OK]
|
|
581
|
-
printFixResult(res);
|
|
582
|
-
|
|
583
|
-
const remainingRisky = findRiskyStagedFiles(targetDir);
|
|
584
|
-
const remainingFindings = scanStagedContentForSecrets(targetDir);
|
|
585
|
-
secretStatus = remainingRisky.length || remainingFindings.length
|
|
586
|
-
? `!
|
|
587
|
-
: '✓
|
|
588
|
-
|
|
589
|
-
spawnSync('git', ['add', '.gitignore'], { cwd: targetDir, stdio: 'ignore' });
|
|
590
|
-
|
|
591
|
-
if (!hasStagedChanges(targetDir)) {
|
|
592
|
-
printErrorBox('
|
|
593
|
-
'
|
|
594
|
-
'
|
|
595
|
-
]);
|
|
596
|
-
if (fromMenu) return 'back';
|
|
597
|
-
process.exit(0);
|
|
598
|
-
}
|
|
599
|
-
console.log(chalk.hex('#34D399')(' »
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// 4. Commit
|
|
604
|
-
spinner.start(stepLabel(4, TOTAL_STEPS, `
|
|
605
|
-
if (!hasChangesToCommit(targetDir)) {
|
|
606
|
-
spinner.info(stepLabel(4, TOTAL_STEPS, chalk.hex('#94A3B8')('
|
|
607
|
-
} else {
|
|
608
|
-
commitWithCustomDate(commitMsg, parsedDate.formatted, targetDir);
|
|
609
|
-
spinner.succeed(stepLabel(4, TOTAL_STEPS, chalk.hex('#34D399')(`Commit
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// 5. Branch
|
|
613
|
-
spinner.start(stepLabel(5, TOTAL_STEPS,
|
|
614
|
-
setBranchName(branchName, targetDir);
|
|
615
|
-
spinner.succeed(stepLabel(5, TOTAL_STEPS, chalk.hex('#34D399')(`Branch: ${chalk.hex('#5EEAD4').bold(branchName)}`)));
|
|
616
|
-
|
|
617
|
-
// 6. Push
|
|
618
|
-
spinner.start(stepLabel(6, TOTAL_STEPS, `Push
|
|
619
|
-
const pushStartedAt = Date.now();
|
|
620
|
-
pushToRemote(branchName, options.force, targetDir);
|
|
621
|
-
pushDurationSeconds = (Date.now() - pushStartedAt) / 1000;
|
|
622
|
-
spinner.succeed(stepLabel(6, TOTAL_STEPS, chalk.bold.hex('#34D399')('
|
|
623
|
-
|
|
624
|
-
printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
|
|
625
|
-
printFooter();
|
|
626
|
-
} catch (error) {
|
|
627
|
-
spinner.stop();
|
|
628
|
-
if (error.code === 'SECRET_BLOCKED') {
|
|
629
|
-
printSecretBlockedBox(error.message, error.details || {});
|
|
630
|
-
|
|
631
|
-
const unpushed = getUnpushedCommits(branchName, targetDir);
|
|
632
|
-
if (unpushed.length > 0) {
|
|
633
|
-
console.log(chalk.cyanBright(`\n »
|
|
634
|
-
let doSquash = options.fix === true;
|
|
635
|
-
if (!doSquash && isInteractive) {
|
|
636
|
-
const sqChoice = await selectWithBack({
|
|
637
|
-
message: '
|
|
638
|
-
choices: [
|
|
639
|
-
{ name: '
|
|
640
|
-
{ name: '
|
|
641
|
-
]
|
|
642
|
-
});
|
|
643
|
-
doSquash = sqChoice === 'yes';
|
|
644
|
-
}
|
|
645
|
-
if (doSquash) {
|
|
646
|
-
spinner.start('
|
|
647
|
-
const sqRes = squashUnpushedCommits(branchName, commitMsg, parsedDate.formatted, targetDir);
|
|
648
|
-
if (sqRes.ok) {
|
|
649
|
-
spinner.succeed(chalk.green('[OK]
|
|
650
|
-
try {
|
|
651
|
-
spinner.start(stepLabel(6, TOTAL_STEPS, `
|
|
652
|
-
const retryStartedAt = Date.now();
|
|
653
|
-
pushToRemote(branchName, options.force, targetDir);
|
|
654
|
-
pushDurationSeconds = (Date.now() - retryStartedAt) / 1000;
|
|
655
|
-
spinner.succeed(chalk.bold.green('[OK] Push
|
|
656
|
-
printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
|
|
657
|
-
printFooter();
|
|
658
|
-
return;
|
|
659
|
-
} catch (retryErr) {
|
|
660
|
-
spinner.fail(chalk.red(`
|
|
661
|
-
}
|
|
662
|
-
} else {
|
|
663
|
-
spinner.fail(chalk.red(`
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
} else if (error.code === 'AUTH_FAILED') {
|
|
668
|
-
printAuthFailedBox(error.message);
|
|
669
|
-
} else if (error.code === 'NON_FAST_FORWARD') {
|
|
670
|
-
printNonFastForwardBox(error.message, branchName);
|
|
671
|
-
if (isInteractive) {
|
|
672
|
-
const ffChoice = await selectBox({
|
|
673
|
-
message: '
|
|
674
|
-
hint: `git pull --rebase origin ${branchName}
|
|
675
|
-
choices: [
|
|
676
|
-
{ name: 'Pull
|
|
677
|
-
{ name: '
|
|
678
|
-
{ name: '
|
|
679
|
-
{ name: '
|
|
680
|
-
]
|
|
681
|
-
});
|
|
682
|
-
if (ffChoice === 'pull-push' || ffChoice === 'pull-only') {
|
|
683
|
-
try {
|
|
684
|
-
spinner.start(
|
|
685
|
-
const pullOut = pullFromRemote('origin', branchName, targetDir, { rebase: true });
|
|
686
|
-
spinner.succeed(chalk.hex('#34D399')(
|
|
687
|
-
if (pullOut) console.log(chalk.hex('#64748B')(` ${String(pullOut).split('\n')[0].slice(0, 120)}`));
|
|
688
|
-
if (ffChoice === 'pull-push') {
|
|
689
|
-
spinner.start(stepLabel(6, TOTAL_STEPS, `
|
|
690
|
-
const retryStartedAt = Date.now();
|
|
691
|
-
pushToRemote(branchName, options.force, targetDir);
|
|
692
|
-
pushDurationSeconds = (Date.now() - retryStartedAt) / 1000;
|
|
693
|
-
spinner.succeed(chalk.bold.hex('#34D399')('Push
|
|
694
|
-
printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
|
|
695
|
-
printFooter();
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
console.log(chalk.hex('#34D399')('\n [OK]
|
|
699
|
-
if (fromMenu) return 'back';
|
|
700
|
-
return;
|
|
701
|
-
} catch (pullErr) {
|
|
702
|
-
spinner.fail(chalk.red(`Pull --rebase
|
|
703
|
-
printErrorBox('PULL --REBASE
|
|
704
|
-
if (fromMenu) return 'back';
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
} else if (ffChoice === 'back' && onBackToMenu) {
|
|
708
|
-
return await onBackToMenu();
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
} else if (error.code === 'REMOTE_OR_NETWORK') {
|
|
712
|
-
printErrorBox('
|
|
713
|
-
} else {
|
|
714
|
-
printErrorBox('
|
|
715
|
-
}
|
|
716
|
-
//
|
|
717
|
-
if (fromMenu) return 'back';
|
|
718
|
-
process.exit(1);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { spawnSync } from 'child_process';
|
|
6
|
+
import { isValidGitUrl, parseCustomDate } from '../utils.js';
|
|
7
|
+
import {
|
|
8
|
+
isGitRepo,
|
|
9
|
+
initGitRepo,
|
|
10
|
+
getGitTopLevel,
|
|
11
|
+
listSubdirectories,
|
|
12
|
+
resolveTargetDir,
|
|
13
|
+
setGitRemote,
|
|
14
|
+
stageAllFiles,
|
|
15
|
+
hasChangesToCommit,
|
|
16
|
+
commitWithCustomDate,
|
|
17
|
+
setBranchName,
|
|
18
|
+
isValidBranchName,
|
|
19
|
+
pushToRemote,
|
|
20
|
+
getCurrentBranch,
|
|
21
|
+
findRiskyStagedFiles,
|
|
22
|
+
pullFromRemote
|
|
23
|
+
} from '../git.js';
|
|
24
|
+
import { scanStagedContentForSecrets } from '../secrets.js';
|
|
25
|
+
import {
|
|
26
|
+
autoFixStagedSecrets,
|
|
27
|
+
hasStagedChanges,
|
|
28
|
+
getUnpushedCommits,
|
|
29
|
+
squashUnpushedCommits
|
|
30
|
+
} from '../fix.js';
|
|
31
|
+
import {
|
|
32
|
+
getCurrentGitHubUser,
|
|
33
|
+
listUserRepos
|
|
34
|
+
} from '../github.js';
|
|
35
|
+
import { promptCreateRepo, promptSelectGitHubAccount } from './github.js';
|
|
36
|
+
import { selectWithBack, inputWithBack, promptContinueOrBack, selectBox, BACK } from '../prompt-helpers.js';
|
|
37
|
+
import {
|
|
38
|
+
printSummary,
|
|
39
|
+
printSection,
|
|
40
|
+
printPushProgress,
|
|
41
|
+
stepLabel,
|
|
42
|
+
printNestedRepoWarning,
|
|
43
|
+
printPrePushSecretWarning,
|
|
44
|
+
printSecretBlockedBox,
|
|
45
|
+
printSuccessBox,
|
|
46
|
+
printCancelled,
|
|
47
|
+
printErrorBox,
|
|
48
|
+
printAuthFailedBox,
|
|
49
|
+
printNonFastForwardBox,
|
|
50
|
+
printFixResult,
|
|
51
|
+
printFooter
|
|
52
|
+
} from '../ui.js';
|
|
53
|
+
|
|
54
|
+
const TOTAL_STEPS = 6;
|
|
55
|
+
const WIZARD_STEPS = 7;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Let the user choose the source folder to push.
|
|
59
|
+
*/
|
|
60
|
+
export async function promptForTargetDir(startDir, clearPromptOnDone = false) {
|
|
61
|
+
const subs = listSubdirectories(startDir);
|
|
62
|
+
if (subs.length === 0) return startDir;
|
|
63
|
+
|
|
64
|
+
const picked = await selectWithBack({
|
|
65
|
+
message: 'Which project folder should be pushed to Git?',
|
|
66
|
+
clearPromptOnDone,
|
|
67
|
+
pageSize: Math.min(12, subs.length + 3),
|
|
68
|
+
choices: [
|
|
69
|
+
{ name: 'Current folder (.)', value: startDir, description: 'Push the entire current folder' },
|
|
70
|
+
...subs.map((s) => ({
|
|
71
|
+
name: `${s.name}`,
|
|
72
|
+
value: s.fullPath,
|
|
73
|
+
description: s.fullPath
|
|
74
|
+
})),
|
|
75
|
+
{ name: 'Enter another path...', value: '__custom__', description: 'Type an absolute or relative path' }
|
|
76
|
+
]
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (picked === BACK) return BACK;
|
|
80
|
+
if (picked !== '__custom__') return picked;
|
|
81
|
+
|
|
82
|
+
const customPath = await inputWithBack({
|
|
83
|
+
message: 'Project folder path',
|
|
84
|
+
hint: ['Enter an absolute or relative path (e.g. ./my-app, E:/projects/demo)'],
|
|
85
|
+
clearPromptOnDone,
|
|
86
|
+
validate: (value) => {
|
|
87
|
+
const r = resolveTargetDir(value, startDir);
|
|
88
|
+
return r.ok || r.error;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (customPath === BACK) return BACK;
|
|
92
|
+
return resolveTargetDir(customPath, startDir).fullPath;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handle a folder nested inside a parent repository.
|
|
97
|
+
*/
|
|
98
|
+
export async function handleNestedRepo(targetDir, isInteractive, options) {
|
|
99
|
+
const topLevel = getGitTopLevel(targetDir);
|
|
100
|
+
if (!topLevel) return;
|
|
101
|
+
if (path.resolve(topLevel) === path.resolve(targetDir)) return;
|
|
102
|
+
if (fs.existsSync(path.join(targetDir, '.git'))) return;
|
|
103
|
+
|
|
104
|
+
printNestedRepoWarning({ targetDir, topLevel });
|
|
105
|
+
|
|
106
|
+
let choice = 'separate';
|
|
107
|
+
if (isInteractive && !options.yes) {
|
|
108
|
+
choice = await selectWithBack({
|
|
109
|
+
message: 'How would you like to push this folder?',
|
|
110
|
+
choices: [
|
|
111
|
+
{ name: 'Create a SEPARATE repository for this folder (recommended)', value: 'separate', description: 'Run git init here and push only this folder' },
|
|
112
|
+
{ name: 'Use the parent repository', value: 'shared', description: 'Keep the parent repository and push from it' },
|
|
113
|
+
{ name: 'Cancel and review', value: 'abort', description: 'Exit without making changes' }
|
|
114
|
+
]
|
|
115
|
+
});
|
|
116
|
+
if (choice === BACK) return BACK;
|
|
117
|
+
} else if (options.dir) {
|
|
118
|
+
choice = 'separate';
|
|
119
|
+
} else {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (choice === 'abort') {
|
|
124
|
+
printCancelled();
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
if (choice === 'separate') {
|
|
128
|
+
initGitRepo(targetDir);
|
|
129
|
+
console.log(chalk.hex('#34D399')(' [OK] Created a separate Git repository for this folder.\n'));
|
|
130
|
+
} else {
|
|
131
|
+
console.log(chalk.hex('#64748B')(' » Using the parent repository (only files in this folder will be staged).\n'));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Prompt the user to select or create a repository URL.
|
|
137
|
+
*/
|
|
138
|
+
async function promptForRepoSelection(targetDir, gitHubUser = null, clearPromptOnDone = false) {
|
|
139
|
+
const choices = [];
|
|
140
|
+
|
|
141
|
+
if (gitHubUser) {
|
|
142
|
+
choices.push(
|
|
143
|
+
{
|
|
144
|
+
name: `Choose from @${gitHubUser.login}'s repositories`,
|
|
145
|
+
value: 'pick_existing',
|
|
146
|
+
description: 'Quickly choose one of your recently updated GitHub repositories'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'Create a NEW GitHub repository',
|
|
150
|
+
value: 'create_repo',
|
|
151
|
+
description: `Create a repository under @${gitHubUser.login}`
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
} else {
|
|
155
|
+
choices.push(
|
|
156
|
+
{
|
|
157
|
+
name: 'Create a NEW GitHub repository',
|
|
158
|
+
value: 'create_repo',
|
|
159
|
+
description: 'Sign in and create a repository'
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
choices.push({
|
|
165
|
+
name: 'Enter a Git repository URL manually',
|
|
166
|
+
value: 'manual_url',
|
|
167
|
+
description: 'Enter an HTTPS or SSH URL (GitHub, GitLab, Bitbucket...)'
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const method = await selectWithBack({
|
|
171
|
+
message: 'How would you like to choose a repository?',
|
|
172
|
+
clearPromptOnDone,
|
|
173
|
+
choices
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (method === BACK) return BACK;
|
|
177
|
+
|
|
178
|
+
if (method === 'create_repo') {
|
|
179
|
+
const res = await promptCreateRepo({ targetDir });
|
|
180
|
+
if (!res.ok || !res.repo) return BACK;
|
|
181
|
+
return res.repo.cloneUrl;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (method === 'pick_existing') {
|
|
185
|
+
const spinner = ora({ text: 'Loading repositories from GitHub…', color: 'cyan' }).start();
|
|
186
|
+
const res = await listUserRepos({ limit: 25 });
|
|
187
|
+
spinner.stop();
|
|
188
|
+
|
|
189
|
+
if (!res.ok || !res.repos || res.repos.length === 0) {
|
|
190
|
+
console.log(chalk.hex('#F59E0B')(' [!] No repositories found for this account.'));
|
|
191
|
+
} else {
|
|
192
|
+
const repoChoices = res.repos.map((r) => ({
|
|
193
|
+
name: `${r.private ? chalk.hex('#F59E0B')('[Private]') : chalk.hex('#10B981')('[Public ]')} ${chalk.hex('#F8FAFC').bold(r.name)}`,
|
|
194
|
+
value: r.clone_url,
|
|
195
|
+
description: r.clone_url
|
|
196
|
+
}));
|
|
197
|
+
|
|
198
|
+
const picked = await selectWithBack({
|
|
199
|
+
message: 'Choose the repository to push to:',
|
|
200
|
+
clearPromptOnDone,
|
|
201
|
+
pageSize: Math.min(12, repoChoices.length + 2),
|
|
202
|
+
choices: repoChoices
|
|
203
|
+
});
|
|
204
|
+
if (picked !== BACK) return picked;
|
|
205
|
+
return BACK;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Enter a URL manually.
|
|
210
|
+
const val = await inputWithBack({
|
|
211
|
+
message: 'Git repository URL',
|
|
212
|
+
hint: ['HTTPS or SSH (e.g. https://github.com/user/repo.git)'],
|
|
213
|
+
clearPromptOnDone,
|
|
214
|
+
validate: (value) => {
|
|
215
|
+
if (!value || !value.trim()) return 'Enter a repository URL.';
|
|
216
|
+
if (!isValidGitUrl(value)) return 'Invalid URL (e.g. https://github.com/user/repo.git).';
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return val;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Run the standard seven-step push flow.
|
|
226
|
+
*/
|
|
227
|
+
export async function runPushWizard(options = {}, onBackToMenu = null) {
|
|
228
|
+
const isInteractive = !options.yes;
|
|
229
|
+
const fromMenu = typeof onBackToMenu === 'function';
|
|
230
|
+
const startDir = process.cwd();
|
|
231
|
+
// Copy flags into a mutable object so back navigation can clear consumed flags
|
|
232
|
+
// and avoid looping when --repo/--branch/--message/--date remain truthy.
|
|
233
|
+
const eff = { ...options };
|
|
234
|
+
|
|
235
|
+
let targetDir = startDir;
|
|
236
|
+
let selectedGitHubUser = null;
|
|
237
|
+
let repoUrl = eff.repo;
|
|
238
|
+
let branchName = eff.branch ? String(eff.branch).trim() : undefined;
|
|
239
|
+
let commitMsg = eff.message ? String(eff.message).trim() : undefined;
|
|
240
|
+
let dateInput = eff.date;
|
|
241
|
+
let parsedDate = null;
|
|
242
|
+
|
|
243
|
+
if (eff.dir) {
|
|
244
|
+
const r = resolveTargetDir(eff.dir, startDir);
|
|
245
|
+
if (!r.ok) {
|
|
246
|
+
printErrorBox('INVALID SOURCE DIRECTORY', [r.error], 'Example: --dir ./my-app or --dir "E:/projects/my-app"');
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
targetDir = r.fullPath;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!isInteractive) {
|
|
253
|
+
if (!commitMsg) commitMsg = 'Initial commit';
|
|
254
|
+
if (!dateInput) dateInput = 'now';
|
|
255
|
+
if (!branchName) branchName = 'main';
|
|
256
|
+
if (!repoUrl || !isValidGitUrl(repoUrl)) {
|
|
257
|
+
printErrorBox('INVALID REPOSITORY URL', [`Received URL: '${repoUrl || '(empty)'}'`], 'Example: https://github.com/username/repository.git');
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
parsedDate = parseCustomDate(dateInput);
|
|
261
|
+
if (!parsedDate.valid) {
|
|
262
|
+
printErrorBox('INVALID DATE OR TIME', [parsedDate.error], 'Examples: now, -2d, -5h, 2024-01-15 14:30:00');
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
printSummary({ repoUrl, commitMsg, dateDisplay: parsedDate.display, branch: branchName, force: options.force, sourceDir: targetDir });
|
|
266
|
+
} else {
|
|
267
|
+
// Standard seven-step interactive flow:
|
|
268
|
+
// 0: 📁 Choose folder
|
|
269
|
+
// 1: 👤 Choose GitHub account
|
|
270
|
+
// 2: 📦 Choose repository
|
|
271
|
+
// 3: 🌿 Choose branch
|
|
272
|
+
// 4: 💬 Enter commit message
|
|
273
|
+
// 5: ⏰ Choose commit time
|
|
274
|
+
// 6: 🚀 Review and push
|
|
275
|
+
let step = eff.dir ? 1 : 0;
|
|
276
|
+
|
|
277
|
+
// Skip repository selection once when a URL was supplied as a flag.
|
|
278
|
+
if (eff.repo) {
|
|
279
|
+
repoUrl = eff.repo;
|
|
280
|
+
delete eff.repo;
|
|
281
|
+
step = 3;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
while (true) {
|
|
285
|
+
const progressTitles = ['Folder', 'Account', 'Repository', 'Branch', 'Message', 'Time', 'Confirm'];
|
|
286
|
+
const completed = [];
|
|
287
|
+
if (step > 0) completed.push({ label: 'Folder', value: targetDir });
|
|
288
|
+
if (step > 1) completed.push({ label: 'Account', value: selectedGitHubUser?.login || 'Manual URL' });
|
|
289
|
+
if (step > 2) {
|
|
290
|
+
const shortRepo = String(repoUrl || '').replace(/\.git$/i, '').match(/(?:github\.com[:/])([^/]+\/[^/]+)$/i)?.[1] || repoUrl;
|
|
291
|
+
completed.push({ label: 'Repository', value: shortRepo || 'Selected' });
|
|
292
|
+
}
|
|
293
|
+
if (step > 3) completed.push({ label: 'Branch', value: branchName || 'main' });
|
|
294
|
+
if (step > 4) completed.push({ label: 'Message', value: commitMsg || 'Initial commit' });
|
|
295
|
+
if (step > 5) completed.push({ label: 'Time', value: parsedDate?.display || dateInput || 'now' });
|
|
296
|
+
printPushProgress({ current: step + 1, total: WIZARD_STEPS, title: progressTitles[step], completed });
|
|
297
|
+
|
|
298
|
+
// STEP 0: 📁 Choose project folder.
|
|
299
|
+
if (step === 0) {
|
|
300
|
+
const picked = await promptForTargetDir(startDir, true);
|
|
301
|
+
if (picked === BACK) {
|
|
302
|
+
if (onBackToMenu) return await onBackToMenu();
|
|
303
|
+
printCancelled();
|
|
304
|
+
process.exit(0);
|
|
305
|
+
}
|
|
306
|
+
targetDir = picked;
|
|
307
|
+
delete eff.dir;
|
|
308
|
+
const nested = await handleNestedRepo(targetDir, isInteractive, eff);
|
|
309
|
+
if (nested === BACK) continue;
|
|
310
|
+
step = 1;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// STEP 1: 👤 Choose GitHub account.
|
|
315
|
+
if (step === 1) {
|
|
316
|
+
const accRes = await promptSelectGitHubAccount({ clearPromptOnDone: true });
|
|
317
|
+
if (accRes.back) {
|
|
318
|
+
// Allow returning to folder selection even when --dir was provided.
|
|
319
|
+
delete eff.dir;
|
|
320
|
+
step = 0;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
selectedGitHubUser = accRes.useGitHub ? accRes.user : null;
|
|
324
|
+
step = 2;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// STEP 2: 📦 Choose repository.
|
|
329
|
+
if (step === 2) {
|
|
330
|
+
if (eff.repo) {
|
|
331
|
+
repoUrl = eff.repo;
|
|
332
|
+
delete eff.repo;
|
|
333
|
+
step = 3;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const pickedUrl = await promptForRepoSelection(targetDir, selectedGitHubUser, true);
|
|
337
|
+
if (pickedUrl === BACK) {
|
|
338
|
+
step = 1;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
repoUrl = pickedUrl;
|
|
342
|
+
if (!repoUrl || !isValidGitUrl(repoUrl)) {
|
|
343
|
+
printErrorBox('INVALID REPOSITORY URL', [`URL: '${repoUrl || '(empty)'}'`]);
|
|
344
|
+
const retry = await promptContinueOrBack('Invalid URL. Would you like to try again or go back?');
|
|
345
|
+
if (retry === 'exit') { printCancelled(); process.exit(0); }
|
|
346
|
+
if (retry === 'back') step = 1;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
step = 3;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// STEP 3: 🌿 Choose branch.
|
|
354
|
+
if (step === 3) {
|
|
355
|
+
if (eff.branch) {
|
|
356
|
+
branchName = String(eff.branch).trim();
|
|
357
|
+
delete eff.branch;
|
|
358
|
+
step = 4;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const currentB = getCurrentBranch(targetDir);
|
|
362
|
+
const val = await inputWithBack({
|
|
363
|
+
message: 'Choose a Git branch name',
|
|
364
|
+
promptLabel: 'Branch name',
|
|
365
|
+
hint: [
|
|
366
|
+
'The local branch will be renamed before it is pushed to origin.',
|
|
367
|
+
'Examples: main, dev, feature/login.',
|
|
368
|
+
],
|
|
369
|
+
default: currentB || 'main',
|
|
370
|
+
clearPromptOnDone: true,
|
|
371
|
+
validate: (v) => {
|
|
372
|
+
if (!v || !v.trim()) return true;
|
|
373
|
+
return isValidBranchName(v.trim()) || 'Invalid branch name. Avoid spaces, .., ~ ^ : ? * [ \\ and @{.';
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
if (val === BACK) {
|
|
377
|
+
step = 2;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
branchName = String(val || currentB || 'main').trim();
|
|
381
|
+
if (!isValidBranchName(branchName)) {
|
|
382
|
+
printErrorBox('INVALID BRANCH NAME', [`Branch: '${branchName}'`]);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
step = 4;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// STEP 4: 💬 Enter commit message.
|
|
390
|
+
if (step === 4) {
|
|
391
|
+
if (eff.message) {
|
|
392
|
+
commitMsg = String(eff.message).trim();
|
|
393
|
+
delete eff.message;
|
|
394
|
+
step = 5;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const val = await inputWithBack({
|
|
398
|
+
message: 'Write a commit message',
|
|
399
|
+
promptLabel: 'Commit message',
|
|
400
|
+
hint: [
|
|
401
|
+
'Briefly describe the changes you are about to push.',
|
|
402
|
+
'Examples: feat: add sign-in · fix: resolve push error.',
|
|
403
|
+
],
|
|
404
|
+
default: 'Initial commit',
|
|
405
|
+
clearPromptOnDone: true
|
|
406
|
+
});
|
|
407
|
+
if (val === BACK) {
|
|
408
|
+
step = 3;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
commitMsg = String(val || '').trim() || 'Initial commit';
|
|
412
|
+
step = 5;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// STEP 5: ⏰ Choose commit time.
|
|
417
|
+
if (step === 5) {
|
|
418
|
+
if (eff.date) {
|
|
419
|
+
dateInput = eff.date;
|
|
420
|
+
delete eff.date;
|
|
421
|
+
step = 6;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const dateOption = await selectWithBack({
|
|
425
|
+
message: 'Step 6/7 · Commit time',
|
|
426
|
+
hint: 'Choose the timestamp to record in the commit',
|
|
427
|
+
clearPromptOnDone: true,
|
|
428
|
+
choices: [
|
|
429
|
+
{ name: 'Now', value: 'now', description: 'Use the current time' },
|
|
430
|
+
{ name: 'Relative time (backdate)', value: 'relative', description: 'e.g. -2d (2 days ago), -5h, -30m, -1w' },
|
|
431
|
+
{ name: 'Specific date and time', value: 'custom', description: 'e.g. 2024-01-15 14:30:00' }
|
|
432
|
+
]
|
|
433
|
+
});
|
|
434
|
+
if (dateOption === BACK) {
|
|
435
|
+
step = 4;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (dateOption === 'now') {
|
|
440
|
+
dateInput = 'now';
|
|
441
|
+
} else if (dateOption === 'relative') {
|
|
442
|
+
const val = await inputWithBack({
|
|
443
|
+
message: 'Backdate the commit',
|
|
444
|
+
hint: ['-2d = 2 days ago · -5h = 5 hours ago · -30m = 30 minutes ago · -1w = 1 week ago'],
|
|
445
|
+
default: '-1d',
|
|
446
|
+
clearPromptOnDone: true,
|
|
447
|
+
validate: (v) => { const p = parseCustomDate(v); return p.valid || p.error || 'Invalid time'; }
|
|
448
|
+
});
|
|
449
|
+
if (val === BACK) continue;
|
|
450
|
+
dateInput = val;
|
|
451
|
+
} else if (dateOption === 'custom') {
|
|
452
|
+
const val = await inputWithBack({
|
|
453
|
+
message: 'Set a specific commit date and time',
|
|
454
|
+
hint: ['Format: YYYY-MM-DD HH:mm:ss (e.g. 2024-01-15 10:00:00)'],
|
|
455
|
+
default: '2024-01-15 10:00:00',
|
|
456
|
+
clearPromptOnDone: true,
|
|
457
|
+
validate: (v) => { const p = parseCustomDate(v); return p.valid || p.error || 'Invalid date and time'; }
|
|
458
|
+
});
|
|
459
|
+
if (val === BACK) continue;
|
|
460
|
+
dateInput = val;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
parsedDate = parseCustomDate(dateInput || 'now');
|
|
464
|
+
if (!parsedDate.valid) {
|
|
465
|
+
printErrorBox('INVALID DATE OR TIME', [parsedDate.error]);
|
|
466
|
+
const retry = await promptContinueOrBack('Invalid time. Would you like to try again or go back?');
|
|
467
|
+
if (retry === 'exit') { printCancelled(); process.exit(0); }
|
|
468
|
+
dateInput = undefined;
|
|
469
|
+
delete eff.date;
|
|
470
|
+
if (retry === 'back') step = 4;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
step = 6;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// STEP 6: 🚀 Review and push.
|
|
478
|
+
if (step === 6) {
|
|
479
|
+
if (!commitMsg) commitMsg = 'Initial commit';
|
|
480
|
+
if (!dateInput) dateInput = 'now';
|
|
481
|
+
if (!branchName) branchName = 'main';
|
|
482
|
+
parsedDate = parseCustomDate(dateInput);
|
|
483
|
+
|
|
484
|
+
printSummary({
|
|
485
|
+
repoUrl,
|
|
486
|
+
commitMsg,
|
|
487
|
+
dateDisplay: parsedDate.display,
|
|
488
|
+
branch: branchName,
|
|
489
|
+
force: options.force,
|
|
490
|
+
sourceDir: targetDir
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const decision = await selectBox({
|
|
494
|
+
message: 'Ready to push this project to Git?',
|
|
495
|
+
hint: 'Review the summary above before pushing',
|
|
496
|
+
choices: [
|
|
497
|
+
{ name: 'Push project', value: 'continue', description: 'Run git init/add/commit/push' },
|
|
498
|
+
{ name: 'Change commit time', value: 'back-date', description: 'Return to the time step' },
|
|
499
|
+
{ name: 'Edit commit message', value: 'back-msg', description: 'Return to the message step' },
|
|
500
|
+
{ name: 'Change branch', value: 'back-branch', description: 'Return to the branch step' },
|
|
501
|
+
{ name: 'Change repository', value: 'back-repo', description: 'Return to the repository step' },
|
|
502
|
+
{ name: 'Change GitHub account', value: 'back-account', description: 'Return to the account step' },
|
|
503
|
+
{ name: 'Choose another project folder', value: 'back-folder', description: 'Return to the folder step' },
|
|
504
|
+
{ name: 'Back to dashboard', value: 'back-menu', description: 'Return to the main menu' },
|
|
505
|
+
{ name: 'Cancel', value: 'exit', description: 'Exit without continuing' }
|
|
506
|
+
]
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (decision === 'exit') { printCancelled(); process.exit(0); }
|
|
510
|
+
if (decision === 'continue') break;
|
|
511
|
+
if (decision === 'back-date') { dateInput = undefined; delete eff.date; parsedDate = null; step = 5; continue; }
|
|
512
|
+
if (decision === 'back-msg') { commitMsg = undefined; delete eff.message; step = 4; continue; }
|
|
513
|
+
if (decision === 'back-branch') { branchName = undefined; delete eff.branch; step = 3; continue; }
|
|
514
|
+
if (decision === 'back-repo') { repoUrl = undefined; delete eff.repo; step = 2; continue; }
|
|
515
|
+
if (decision === 'back-account') { step = 1; continue; }
|
|
516
|
+
if (decision === 'back-folder') { delete eff.dir; step = 0; continue; }
|
|
517
|
+
if (decision === 'back-menu') {
|
|
518
|
+
if (onBackToMenu) return await onBackToMenu();
|
|
519
|
+
step = options.dir ? 1 : 0;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 🚀 STEP 7: PERFORM THE PUSH.
|
|
527
|
+
const spinner = ora({ color: 'cyan' });
|
|
528
|
+
let secretStatus = '✓ None found';
|
|
529
|
+
let pushDurationSeconds = null;
|
|
530
|
+
printSection(`🚀 PUSHING TO GIT (${TOTAL_STEPS} STEPS)`);
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
// 1. Git init
|
|
534
|
+
if (!isGitRepo(targetDir)) {
|
|
535
|
+
spinner.start(stepLabel(1, TOTAL_STEPS, 'Initialize Git repository (git init)...'));
|
|
536
|
+
initGitRepo(targetDir);
|
|
537
|
+
spinner.succeed(stepLabel(1, TOTAL_STEPS, chalk.hex('#34D399')('Git repository initialized')));
|
|
538
|
+
} else {
|
|
539
|
+
console.log(' ' + stepLabel(1, TOTAL_STEPS, chalk.hex('#64748B')('Git repository already exists — skipping init')));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// 2. Remote
|
|
543
|
+
spinner.start(stepLabel(2, TOTAL_STEPS, 'Configure origin remote...'));
|
|
544
|
+
setGitRemote(repoUrl, targetDir);
|
|
545
|
+
spinner.succeed(stepLabel(2, TOTAL_STEPS, chalk.hex('#34D399')('Origin remote configured')));
|
|
546
|
+
|
|
547
|
+
// 3. Stage
|
|
548
|
+
spinner.start(stepLabel(3, TOTAL_STEPS, 'Stage files (git add .)...'));
|
|
549
|
+
stageAllFiles(targetDir);
|
|
550
|
+
spinner.succeed(stepLabel(3, TOTAL_STEPS, chalk.hex('#34D399')('All files staged')));
|
|
551
|
+
|
|
552
|
+
// 3.5. Scan for secrets.
|
|
553
|
+
const risky = findRiskyStagedFiles(targetDir);
|
|
554
|
+
const findings = scanStagedContentForSecrets(targetDir);
|
|
555
|
+
if (risky.length > 0 || findings.length > 0) secretStatus = `! ${risky.length + findings.length} warning(s)`;
|
|
556
|
+
if (risky.length > 0 || findings.length > 0) {
|
|
557
|
+
spinner.stop();
|
|
558
|
+
printPrePushSecretWarning({ riskyFiles: risky, findings });
|
|
559
|
+
const allFiles = [...new Set([...risky, ...findings.map((f) => f.file)])];
|
|
560
|
+
|
|
561
|
+
let wantFix = options.fix === true;
|
|
562
|
+
if (!wantFix && isInteractive) {
|
|
563
|
+
const fixChoice = await selectWithBack({
|
|
564
|
+
message: 'Safely remove the secret files? (keeps local files, removes them from the commit)',
|
|
565
|
+
choices: [
|
|
566
|
+
{ name: 'Yes, remove sensitive files', value: 'yes', description: 'Unstage secret files and add them to .gitignore' },
|
|
567
|
+
{ name: 'No, keep them', value: 'no', description: 'Continue without removing them' }
|
|
568
|
+
]
|
|
569
|
+
});
|
|
570
|
+
if (fixChoice === BACK) {
|
|
571
|
+
printCancelled();
|
|
572
|
+
process.exit(0);
|
|
573
|
+
}
|
|
574
|
+
wantFix = fixChoice === 'yes';
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (wantFix) {
|
|
578
|
+
spinner.start('Removing secret files from the staged changes...');
|
|
579
|
+
const res = autoFixStagedSecrets(allFiles, targetDir);
|
|
580
|
+
spinner.succeed(chalk.hex('#34D399')(`[OK] Removed ${res.fixed.length} file(s) from the commit`));
|
|
581
|
+
printFixResult(res);
|
|
582
|
+
|
|
583
|
+
const remainingRisky = findRiskyStagedFiles(targetDir);
|
|
584
|
+
const remainingFindings = scanStagedContentForSecrets(targetDir);
|
|
585
|
+
secretStatus = remainingRisky.length || remainingFindings.length
|
|
586
|
+
? `! ${remainingRisky.length + remainingFindings.length} warning(s) remain`
|
|
587
|
+
: '✓ Warnings removed';
|
|
588
|
+
|
|
589
|
+
spawnSync('git', ['add', '.gitignore'], { cwd: targetDir, stdio: 'ignore' });
|
|
590
|
+
|
|
591
|
+
if (!hasStagedChanges(targetDir)) {
|
|
592
|
+
printErrorBox('NOTHING LEFT TO COMMIT', [
|
|
593
|
+
'No files remain to commit after removing the secrets.',
|
|
594
|
+
'Your local files are still intact.'
|
|
595
|
+
]);
|
|
596
|
+
if (fromMenu) return 'back';
|
|
597
|
+
process.exit(0);
|
|
598
|
+
}
|
|
599
|
+
console.log(chalk.hex('#34D399')(' » Continuing with the safe changes.\n'));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// 4. Commit
|
|
604
|
+
spinner.start(stepLabel(4, TOTAL_STEPS, `Create commit [${parsedDate.display}]...`));
|
|
605
|
+
if (!hasChangesToCommit(targetDir)) {
|
|
606
|
+
spinner.info(stepLabel(4, TOTAL_STEPS, chalk.hex('#94A3B8')('No new changes — using the existing commit')));
|
|
607
|
+
} else {
|
|
608
|
+
commitWithCustomDate(commitMsg, parsedDate.formatted, targetDir);
|
|
609
|
+
spinner.succeed(stepLabel(4, TOTAL_STEPS, chalk.hex('#34D399')(`Commit created • ${chalk.hex('#C084FC').bold(parsedDate.display)}`)));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// 5. Branch
|
|
613
|
+
spinner.start(stepLabel(5, TOTAL_STEPS, `Switch branch -> '${branchName}'...`));
|
|
614
|
+
setBranchName(branchName, targetDir);
|
|
615
|
+
spinner.succeed(stepLabel(5, TOTAL_STEPS, chalk.hex('#34D399')(`Branch: ${chalk.hex('#5EEAD4').bold(branchName)}`)));
|
|
616
|
+
|
|
617
|
+
// 6. Push
|
|
618
|
+
spinner.start(stepLabel(6, TOTAL_STEPS, `Push to origin/${branchName}...`));
|
|
619
|
+
const pushStartedAt = Date.now();
|
|
620
|
+
pushToRemote(branchName, options.force, targetDir);
|
|
621
|
+
pushDurationSeconds = (Date.now() - pushStartedAt) / 1000;
|
|
622
|
+
spinner.succeed(stepLabel(6, TOTAL_STEPS, chalk.bold.hex('#34D399')('Successfully pushed to remote')));
|
|
623
|
+
|
|
624
|
+
printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
|
|
625
|
+
printFooter();
|
|
626
|
+
} catch (error) {
|
|
627
|
+
spinner.stop();
|
|
628
|
+
if (error.code === 'SECRET_BLOCKED') {
|
|
629
|
+
printSecretBlockedBox(error.message, error.details || {});
|
|
630
|
+
|
|
631
|
+
const unpushed = getUnpushedCommits(branchName, targetDir);
|
|
632
|
+
if (unpushed.length > 0) {
|
|
633
|
+
console.log(chalk.cyanBright(`\n » Found ${unpushed.length} unpushed commit(s) that may contain traces of an old secret.`));
|
|
634
|
+
let doSquash = options.fix === true;
|
|
635
|
+
if (!doSquash && isInteractive) {
|
|
636
|
+
const sqChoice = await selectWithBack({
|
|
637
|
+
message: 'Clean the history (squash commits into one clean commit) and try pushing again?',
|
|
638
|
+
choices: [
|
|
639
|
+
{ name: 'Yes, squash and retry the push', value: 'yes', description: 'Clean the commit history' },
|
|
640
|
+
{ name: 'No, I will handle it', value: 'no', description: 'Stop here' }
|
|
641
|
+
]
|
|
642
|
+
});
|
|
643
|
+
doSquash = sqChoice === 'yes';
|
|
644
|
+
}
|
|
645
|
+
if (doSquash) {
|
|
646
|
+
spinner.start('Squashing and cleaning unpushed commits...');
|
|
647
|
+
const sqRes = squashUnpushedCommits(branchName, commitMsg, parsedDate.formatted, targetDir);
|
|
648
|
+
if (sqRes.ok) {
|
|
649
|
+
spinner.succeed(chalk.green('[OK] Commit history cleaned. Retrying push...'));
|
|
650
|
+
try {
|
|
651
|
+
spinner.start(stepLabel(6, TOTAL_STEPS, `Retry push to origin/${branchName}...`));
|
|
652
|
+
const retryStartedAt = Date.now();
|
|
653
|
+
pushToRemote(branchName, options.force, targetDir);
|
|
654
|
+
pushDurationSeconds = (Date.now() - retryStartedAt) / 1000;
|
|
655
|
+
spinner.succeed(chalk.bold.green('[OK] Push succeeded'));
|
|
656
|
+
printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
|
|
657
|
+
printFooter();
|
|
658
|
+
return;
|
|
659
|
+
} catch (retryErr) {
|
|
660
|
+
spinner.fail(chalk.red(`Retry push failed: ${retryErr.message}`));
|
|
661
|
+
}
|
|
662
|
+
} else {
|
|
663
|
+
spinner.fail(chalk.red(`Unable to squash commits: ${sqRes.error}`));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
} else if (error.code === 'AUTH_FAILED') {
|
|
668
|
+
printAuthFailedBox(error.message);
|
|
669
|
+
} else if (error.code === 'NON_FAST_FORWARD') {
|
|
670
|
+
printNonFastForwardBox(error.message, branchName);
|
|
671
|
+
if (isInteractive) {
|
|
672
|
+
const ffChoice = await selectBox({
|
|
673
|
+
message: 'The remote has newer commits. What would you like to do?',
|
|
674
|
+
hint: `git pull --rebase origin ${branchName}, then push again`,
|
|
675
|
+
choices: [
|
|
676
|
+
{ name: 'Pull with rebase, then push — Recommended', value: 'pull-push', description: `git pull --rebase origin ${branchName}, then push again` },
|
|
677
|
+
{ name: 'Pull only', value: 'pull-only', description: 'Sync the latest changes without pushing' },
|
|
678
|
+
{ name: 'Back to dashboard', value: 'back', description: 'Return to the main menu' },
|
|
679
|
+
{ name: 'Exit', value: 'exit', description: 'Stop here' }
|
|
680
|
+
]
|
|
681
|
+
});
|
|
682
|
+
if (ffChoice === 'pull-push' || ffChoice === 'pull-only') {
|
|
683
|
+
try {
|
|
684
|
+
spinner.start(`Pulling --rebase from origin/${branchName}...`);
|
|
685
|
+
const pullOut = pullFromRemote('origin', branchName, targetDir, { rebase: true });
|
|
686
|
+
spinner.succeed(chalk.hex('#34D399')(`Pulled --rebase from origin/${branchName}`));
|
|
687
|
+
if (pullOut) console.log(chalk.hex('#64748B')(` ${String(pullOut).split('\n')[0].slice(0, 120)}`));
|
|
688
|
+
if (ffChoice === 'pull-push') {
|
|
689
|
+
spinner.start(stepLabel(6, TOTAL_STEPS, `Retry push to origin/${branchName}...`));
|
|
690
|
+
const retryStartedAt = Date.now();
|
|
691
|
+
pushToRemote(branchName, options.force, targetDir);
|
|
692
|
+
pushDurationSeconds = (Date.now() - retryStartedAt) / 1000;
|
|
693
|
+
spinner.succeed(chalk.bold.hex('#34D399')('Push succeeded after pulling'));
|
|
694
|
+
printSuccessBox({ branch: branchName, repoUrl, durationSeconds: pushDurationSeconds, secretStatus });
|
|
695
|
+
printFooter();
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
console.log(chalk.hex('#34D399')('\n [OK] Pulled the latest changes.\n'));
|
|
699
|
+
if (fromMenu) return 'back';
|
|
700
|
+
return;
|
|
701
|
+
} catch (pullErr) {
|
|
702
|
+
spinner.fail(chalk.red(`Pull --rebase failed: ${String(pullErr.message).split('\n')[0]}`));
|
|
703
|
+
printErrorBox('PULL --REBASE FAILED', [String(pullErr.message).split('\n')[0]]);
|
|
704
|
+
if (fromMenu) return 'back';
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
} else if (ffChoice === 'back' && onBackToMenu) {
|
|
708
|
+
return await onBackToMenu();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
} else if (error.code === 'REMOTE_OR_NETWORK') {
|
|
712
|
+
printErrorBox('REMOTE CONNECTION ERROR', [String(error.message).split('\n')[0]], 'Check the repository URL, network connection, and access permissions.');
|
|
713
|
+
} else {
|
|
714
|
+
printErrorBox('GIT PUSH FAILED', [error.message]);
|
|
715
|
+
}
|
|
716
|
+
// When running from the interactive menu, return to it instead of exiting.
|
|
717
|
+
if (fromMenu) return 'back';
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
}
|