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