@alejandrochaves/devflow-cli 1.3.2 → 1.4.0

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.
Files changed (59) hide show
  1. package/dist/commands/amend.d.ts.map +1 -1
  2. package/dist/commands/amend.js +139 -68
  3. package/dist/commands/amend.js.map +1 -1
  4. package/dist/commands/branch.d.ts.map +1 -1
  5. package/dist/commands/branch.js +201 -80
  6. package/dist/commands/branch.js.map +1 -1
  7. package/dist/commands/changelog.d.ts.map +1 -1
  8. package/dist/commands/changelog.js +62 -32
  9. package/dist/commands/changelog.js.map +1 -1
  10. package/dist/commands/cleanup.d.ts.map +1 -1
  11. package/dist/commands/cleanup.js +10 -3
  12. package/dist/commands/cleanup.js.map +1 -1
  13. package/dist/commands/commit.d.ts.map +1 -1
  14. package/dist/commands/commit.js +343 -175
  15. package/dist/commands/commit.js.map +1 -1
  16. package/dist/commands/fixup.d.ts.map +1 -1
  17. package/dist/commands/fixup.js +129 -81
  18. package/dist/commands/fixup.js.map +1 -1
  19. package/dist/commands/init.d.ts.map +1 -1
  20. package/dist/commands/init.js +223 -110
  21. package/dist/commands/init.js.map +1 -1
  22. package/dist/commands/issue.d.ts.map +1 -1
  23. package/dist/commands/issue.js +341 -83
  24. package/dist/commands/issue.js.map +1 -1
  25. package/dist/commands/issues.d.ts.map +1 -1
  26. package/dist/commands/issues.js +166 -77
  27. package/dist/commands/issues.js.map +1 -1
  28. package/dist/commands/log.d.ts.map +1 -1
  29. package/dist/commands/log.js +81 -46
  30. package/dist/commands/log.js.map +1 -1
  31. package/dist/commands/merge.d.ts.map +1 -1
  32. package/dist/commands/merge.js +78 -38
  33. package/dist/commands/merge.js.map +1 -1
  34. package/dist/commands/pr.d.ts.map +1 -1
  35. package/dist/commands/pr.js +110 -42
  36. package/dist/commands/pr.js.map +1 -1
  37. package/dist/commands/release.d.ts.map +1 -1
  38. package/dist/commands/release.js +70 -37
  39. package/dist/commands/release.js.map +1 -1
  40. package/dist/commands/review.d.ts.map +1 -1
  41. package/dist/commands/review.js +144 -84
  42. package/dist/commands/review.js.map +1 -1
  43. package/dist/commands/stash.d.ts.map +1 -1
  44. package/dist/commands/stash.js +164 -94
  45. package/dist/commands/stash.js.map +1 -1
  46. package/dist/commands/test-plan.d.ts.map +1 -1
  47. package/dist/commands/test-plan.js +86 -42
  48. package/dist/commands/test-plan.js.map +1 -1
  49. package/dist/commands/update.d.ts.map +1 -1
  50. package/dist/commands/update.js +9 -3
  51. package/dist/commands/update.js.map +1 -1
  52. package/dist/commands/worktree.d.ts.map +1 -1
  53. package/dist/commands/worktree.js +168 -96
  54. package/dist/commands/worktree.js.map +1 -1
  55. package/dist/prompts.d.ts +62 -0
  56. package/dist/prompts.d.ts.map +1 -0
  57. package/dist/prompts.js +130 -0
  58. package/dist/prompts.js.map +1 -0
  59. package/package.json +3 -2
@@ -1,8 +1,8 @@
1
- import { select, search, confirm, input, checkbox } from "@inquirer/prompts";
2
1
  import { execSync } from "child_process";
3
2
  import { loadConfig } from "../config.js";
4
3
  import { inferTicket, inferScope, getBranch, isProtectedBranch } from "../git.js";
5
4
  import { bold, cyan, dim, green, yellow } from "../colors.js";
5
+ import { selectWithBack, inputWithBack, confirmWithBack, searchWithBack, checkboxWithBack, BACK_VALUE, } from "../prompts.js";
6
6
  export function inferScopeFromPaths(stagedFiles, scopes) {
7
7
  const scopesWithPaths = scopes.filter((s) => s.paths && s.paths.length > 0);
8
8
  if (scopesWithPaths.length === 0)
@@ -53,16 +53,17 @@ export async function commitCommand(options = {}) {
53
53
  if (isProtectedBranch() && !options.yes) {
54
54
  const branch = getBranch();
55
55
  console.log(yellow(`⚠ You are on ${bold(branch)}. Committing directly to protected branches is not recommended.`));
56
- const proceed = await confirm({
56
+ const proceed = await confirmWithBack({
57
57
  message: `Continue committing to ${branch}?`,
58
58
  default: false,
59
+ showBack: false,
59
60
  });
60
- if (!proceed) {
61
+ if (proceed !== true) {
61
62
  console.log("Use: devflow branch");
62
63
  process.exit(0);
63
64
  }
64
65
  }
65
- // Stage files if --all or --files provided
66
+ // Stage files if --all or --files provided (non-interactive)
66
67
  if (options.all) {
67
68
  if (!options.dryRun) {
68
69
  execSync("git add -A");
@@ -76,198 +77,354 @@ export async function commitCommand(options = {}) {
76
77
  }
77
78
  }
78
79
  }
79
- // Check for staged files
80
- const staged = execSync("git diff --cached --name-only", { encoding: "utf-8" }).trim();
81
- const unstaged = execSync("git diff --name-only", { encoding: "utf-8" }).trim();
82
- const untracked = execSync("git ls-files --others --exclude-standard", { encoding: "utf-8" }).trim();
83
- const allChanges = [
84
- ...unstaged.split("\n").filter(Boolean).map((f) => ({ file: f, label: `M ${f}` })),
85
- ...untracked.split("\n").filter(Boolean).map((f) => ({ file: f, label: `? ${f}` })),
86
- ];
87
- if (!staged && allChanges.length === 0) {
88
- console.log("Nothing to commit — working tree clean.");
89
- process.exit(0);
80
+ // Initialize state
81
+ const state = {
82
+ stagedFiles: [],
83
+ type: options.type || "",
84
+ scope: options.scope ?? "",
85
+ message: options.message || "",
86
+ isBreaking: options.breaking ?? false,
87
+ body: options.body || "",
88
+ breakingDesc: options.breakingDesc || "",
89
+ };
90
+ let currentStep = "stageFiles";
91
+ const stepOrder = ["stageFiles", "type", "scope", "message", "breaking", "body", "breakingDesc"];
92
+ // Determine starting step based on flags
93
+ if (options.all || options.files) {
94
+ // Files already staged via flags, skip to type
95
+ const staged = execSync("git diff --cached --name-only", { encoding: "utf-8" }).trim();
96
+ state.stagedFiles = staged ? staged.split("\n") : [];
97
+ currentStep = "type";
90
98
  }
91
- let stagedFiles = staged ? staged.split("\n") : [];
92
- // Interactive file staging (only if no files staged and no --all/--files flags)
93
- if (!staged && !options.all && !options.files) {
94
- if (allChanges.length === 1) {
95
- if (options.yes) {
96
- // Auto-stage single file when --yes
97
- if (!options.dryRun) {
98
- execSync(`git add ${JSON.stringify(allChanges[0].file)}`);
99
+ const goBack = () => {
100
+ const currentIndex = stepOrder.indexOf(currentStep);
101
+ if (currentIndex > 0) {
102
+ currentStep = stepOrder[currentIndex - 1];
103
+ // Skip breakingDesc if not breaking
104
+ if (currentStep === "breakingDesc" && !state.isBreaking) {
105
+ const prevIndex = stepOrder.indexOf(currentStep) - 1;
106
+ if (prevIndex >= 0)
107
+ currentStep = stepOrder[prevIndex];
108
+ }
109
+ // Skip stageFiles if files were staged via flags
110
+ if (currentStep === "stageFiles" && (options.all || options.files)) {
111
+ // Can't go back further
112
+ currentStep = "type";
113
+ }
114
+ }
115
+ };
116
+ const goNext = () => {
117
+ const currentIndex = stepOrder.indexOf(currentStep);
118
+ if (currentIndex < stepOrder.length - 1) {
119
+ currentStep = stepOrder[currentIndex + 1];
120
+ // Skip breakingDesc if not breaking
121
+ if (currentStep === "breakingDesc" && !state.isBreaking) {
122
+ currentStep = stepOrder[stepOrder.indexOf(currentStep) + 1] || currentStep;
123
+ }
124
+ }
125
+ };
126
+ while (currentStep !== undefined) {
127
+ const canGoBackToStageFiles = !options.all && !options.files && !options.yes;
128
+ const isFirstStep = (currentStep === "stageFiles") || (currentStep === "type" && !canGoBackToStageFiles);
129
+ switch (currentStep) {
130
+ case "stageFiles": {
131
+ // Check for staged files
132
+ const staged = execSync("git diff --cached --name-only", { encoding: "utf-8" }).trim();
133
+ const unstaged = execSync("git diff --name-only", { encoding: "utf-8" }).trim();
134
+ const untracked = execSync("git ls-files --others --exclude-standard", { encoding: "utf-8" }).trim();
135
+ const allChanges = [
136
+ ...unstaged.split("\n").filter(Boolean).map((f) => ({ file: f, label: `M ${f}` })),
137
+ ...untracked.split("\n").filter(Boolean).map((f) => ({ file: f, label: `? ${f}` })),
138
+ ];
139
+ // If already have staged changes, skip to type
140
+ if (staged) {
141
+ state.stagedFiles = staged.split("\n");
142
+ console.log(dim("Staged files:"));
143
+ staged.split("\n").forEach((f) => console.log(dim(` ${f}`)));
144
+ console.log("");
145
+ goNext();
146
+ break;
147
+ }
148
+ if (allChanges.length === 0) {
149
+ console.log("Nothing to commit — working tree clean.");
150
+ process.exit(0);
151
+ }
152
+ if (options.yes) {
153
+ // Auto-stage all files when --yes
154
+ if (!options.dryRun) {
155
+ execSync("git add -A");
156
+ }
157
+ state.stagedFiles = allChanges.map((c) => c.file);
158
+ goNext();
159
+ break;
160
+ }
161
+ if (allChanges.length === 1) {
162
+ const result = await confirmWithBack({
163
+ message: `Stage ${allChanges[0].file}?`,
164
+ default: true,
165
+ showBack: false, // First step
166
+ });
167
+ if (result !== true) {
168
+ console.log("No files staged. Aborting.");
169
+ process.exit(0);
170
+ }
171
+ if (!options.dryRun) {
172
+ execSync(`git add ${JSON.stringify(allChanges[0].file)}`);
173
+ }
174
+ state.stagedFiles = [allChanges[0].file];
175
+ goNext();
176
+ }
177
+ else {
178
+ // Ask: stage all or select specific files?
179
+ const stageChoice = await selectWithBack({
180
+ message: "Stage files:",
181
+ choices: [
182
+ { value: "all", name: `Stage all (${allChanges.length} files)` },
183
+ { value: "select", name: "Select specific files" },
184
+ ],
185
+ default: "all",
186
+ showBack: false, // First step
187
+ });
188
+ if (stageChoice === BACK_VALUE) {
189
+ // Can't go back from first step
190
+ break;
191
+ }
192
+ if (stageChoice === "all") {
193
+ if (!options.dryRun) {
194
+ execSync("git add -A");
195
+ }
196
+ state.stagedFiles = allChanges.map((c) => c.file);
197
+ goNext();
198
+ }
199
+ else {
200
+ const filesToStage = await checkboxWithBack({
201
+ message: "Select files to stage:",
202
+ choices: allChanges.map((c) => ({ value: c.file, name: c.label })),
203
+ required: true,
204
+ showBack: true,
205
+ });
206
+ if (filesToStage === BACK_VALUE) {
207
+ // Stay on this step to re-show the all/select choice
208
+ break;
209
+ }
210
+ if (filesToStage.length === 0) {
211
+ console.log("No files selected. Please select at least one file.");
212
+ break;
213
+ }
214
+ if (!options.dryRun) {
215
+ for (const file of filesToStage) {
216
+ execSync(`git add ${JSON.stringify(file)}`);
217
+ }
218
+ }
219
+ state.stagedFiles = filesToStage;
220
+ goNext();
221
+ }
99
222
  }
100
- stagedFiles = [allChanges[0].file];
223
+ break;
101
224
  }
102
- else {
103
- const stageIt = await confirm({
104
- message: `Stage ${allChanges[0].file}?`,
105
- default: true,
225
+ case "type": {
226
+ if (options.type) {
227
+ goNext();
228
+ break;
229
+ }
230
+ const result = await selectWithBack({
231
+ message: "Select commit type:",
232
+ choices: config.commitTypes.map((t) => ({ value: t.value, name: t.label })),
233
+ showBack: canGoBackToStageFiles,
106
234
  });
107
- if (!stageIt) {
108
- console.log("No files staged. Aborting.");
109
- process.exit(0);
235
+ if (result === BACK_VALUE) {
236
+ // Unstage files before going back
237
+ execSync("git reset HEAD", { stdio: "ignore" });
238
+ goBack();
110
239
  }
111
- if (!options.dryRun) {
112
- execSync(`git add ${JSON.stringify(allChanges[0].file)}`);
240
+ else {
241
+ state.type = result;
242
+ goNext();
113
243
  }
114
- stagedFiles = [allChanges[0].file];
244
+ break;
115
245
  }
116
- }
117
- else {
118
- if (options.yes) {
119
- // Auto-stage all files when --yes
120
- if (!options.dryRun) {
121
- execSync("git add -A");
246
+ case "scope": {
247
+ if (options.scope !== undefined) {
248
+ goNext();
249
+ break;
250
+ }
251
+ if (config.scopes.length > 0) {
252
+ const inferredFromPaths = inferScopeFromPaths(state.stagedFiles, config.scopes);
253
+ const inferredFromLog = inferScope();
254
+ const inferred = inferredFromPaths || inferredFromLog;
255
+ const result = await searchWithBack({
256
+ message: inferred
257
+ ? `Select scope (suggested: ${cyan(inferred)}):`
258
+ : "Select scope (type to filter):",
259
+ source: (term) => {
260
+ const filtered = config.scopes.filter((s) => !term ||
261
+ s.value.includes(term.toLowerCase()) ||
262
+ s.description.toLowerCase().includes(term.toLowerCase()));
263
+ if (inferred) {
264
+ filtered.sort((a, b) => a.value === inferred ? -1 : b.value === inferred ? 1 : 0);
265
+ }
266
+ return filtered.map((s) => ({
267
+ value: s.value,
268
+ name: `${s.value} — ${s.description}`,
269
+ }));
270
+ },
271
+ showBack: true,
272
+ });
273
+ if (result === BACK_VALUE) {
274
+ goBack();
275
+ }
276
+ else {
277
+ state.scope = result;
278
+ goNext();
279
+ }
280
+ }
281
+ else {
282
+ const inferredFromLog = inferScope();
283
+ const result = await inputWithBack({
284
+ message: inferredFromLog
285
+ ? `Enter scope (default: ${inferredFromLog}):`
286
+ : "Enter scope (optional):",
287
+ default: inferredFromLog,
288
+ showBack: true,
289
+ });
290
+ if (result === BACK_VALUE) {
291
+ goBack();
292
+ }
293
+ else {
294
+ state.scope = result;
295
+ goNext();
296
+ }
122
297
  }
123
- stagedFiles = allChanges.map((c) => c.file);
298
+ break;
124
299
  }
125
- else {
126
- const filesToStage = await checkbox({
127
- message: "Select files to stage:",
128
- choices: [
129
- { value: "__ALL__", name: "Stage all" },
130
- ...allChanges.map((c) => ({ value: c.file, name: c.label })),
131
- ],
132
- required: true,
300
+ case "message": {
301
+ if (options.message) {
302
+ goNext();
303
+ break;
304
+ }
305
+ const result = await inputWithBack({
306
+ message: "Enter commit message:",
307
+ default: state.message || undefined,
308
+ validate: (val) => val.trim().length > 0 || "Commit message is required",
309
+ showBack: true,
133
310
  });
134
- if (!options.dryRun) {
135
- if (filesToStage.includes("__ALL__")) {
136
- execSync("git add -A");
137
- stagedFiles = allChanges.map((c) => c.file);
311
+ if (result === BACK_VALUE) {
312
+ goBack();
313
+ }
314
+ else {
315
+ state.message = result;
316
+ goNext();
317
+ }
318
+ break;
319
+ }
320
+ case "breaking": {
321
+ if (options.breaking !== undefined || options.yes) {
322
+ state.isBreaking = options.breaking ?? false;
323
+ goNext();
324
+ break;
325
+ }
326
+ const result = await confirmWithBack({
327
+ message: "Is this a breaking change?",
328
+ default: state.isBreaking,
329
+ showBack: true,
330
+ });
331
+ if (result === BACK_VALUE) {
332
+ goBack();
333
+ }
334
+ else {
335
+ state.isBreaking = result;
336
+ goNext();
337
+ }
338
+ break;
339
+ }
340
+ case "body": {
341
+ if (options.body || options.yes) {
342
+ goNext();
343
+ break;
344
+ }
345
+ const result = await confirmWithBack({
346
+ message: "Add a longer description (body)?",
347
+ default: false,
348
+ showBack: true,
349
+ });
350
+ if (result === BACK_VALUE) {
351
+ goBack();
352
+ }
353
+ else if (result) {
354
+ const bodyResult = await inputWithBack({
355
+ message: "Body (longer explanation):",
356
+ default: state.body || undefined,
357
+ showBack: true,
358
+ });
359
+ if (bodyResult === BACK_VALUE) {
360
+ // Stay on this step
138
361
  }
139
362
  else {
140
- for (const file of filesToStage) {
141
- execSync(`git add ${JSON.stringify(file)}`);
142
- }
143
- stagedFiles = filesToStage;
363
+ state.body = bodyResult;
364
+ goNext();
144
365
  }
145
366
  }
146
367
  else {
147
- stagedFiles = filesToStage.includes("__ALL__")
148
- ? allChanges.map((c) => c.file)
149
- : filesToStage;
368
+ state.body = "";
369
+ goNext();
150
370
  }
371
+ break;
151
372
  }
152
- }
153
- }
154
- else if (staged) {
155
- console.log(dim("Staged files:"));
156
- staged.split("\n").forEach((f) => console.log(dim(` ${f}`)));
157
- console.log("");
158
- }
159
- // Get commit type (from flag or prompt)
160
- const type = options.type || await select({
161
- message: "Select commit type:",
162
- choices: config.commitTypes.map((t) => ({ value: t.value, name: t.label })),
163
- });
164
- // Get scope (from flag or prompt)
165
- let finalScope;
166
- if (options.scope !== undefined) {
167
- finalScope = options.scope;
168
- }
169
- else if (config.scopes.length > 0) {
170
- const inferredFromPaths = inferScopeFromPaths(stagedFiles, config.scopes);
171
- const inferredFromLog = inferScope();
172
- const inferred = inferredFromPaths || inferredFromLog;
173
- finalScope = await search({
174
- message: inferred
175
- ? `Select scope (suggested: ${cyan(inferred)}):`
176
- : "Select scope (type to filter):",
177
- source: (term) => {
178
- const filtered = config.scopes.filter((s) => !term ||
179
- s.value.includes(term.toLowerCase()) ||
180
- s.description.toLowerCase().includes(term.toLowerCase()));
181
- if (inferred) {
182
- filtered.sort((a, b) => a.value === inferred ? -1 : b.value === inferred ? 1 : 0);
373
+ case "breakingDesc": {
374
+ if (!state.isBreaking) {
375
+ // Exit the loop - we're done collecting input
376
+ currentStep = undefined;
377
+ break;
183
378
  }
184
- return filtered.map((s) => ({
185
- value: s.value,
186
- name: `${s.value} — ${s.description}`,
187
- }));
188
- },
189
- });
190
- }
191
- else {
192
- const inferredFromLog = inferScope();
193
- finalScope = await input({
194
- message: inferredFromLog
195
- ? `Enter scope (default: ${inferredFromLog}):`
196
- : "Enter scope (optional):",
197
- default: inferredFromLog,
198
- });
199
- }
200
- // Get message (from flag or prompt)
201
- let message;
202
- if (options.message) {
203
- message = options.message;
204
- }
205
- else {
206
- message = await input({
207
- message: "Enter commit message:",
208
- validate: (val) => val.trim().length > 0 || "Commit message is required",
209
- });
210
- }
211
- // Get breaking change status (from flag or prompt)
212
- let isBreaking;
213
- if (options.breaking !== undefined) {
214
- isBreaking = options.breaking;
215
- }
216
- else if (options.yes) {
217
- isBreaking = false;
218
- }
219
- else {
220
- isBreaking = await confirm({
221
- message: "Is this a breaking change?",
222
- default: false,
223
- });
379
+ if (options.breakingDesc) {
380
+ currentStep = undefined;
381
+ break;
382
+ }
383
+ if (options.yes) {
384
+ console.log(yellow("Warning: Breaking change without description. Use --breaking-desc to provide one."));
385
+ currentStep = undefined;
386
+ break;
387
+ }
388
+ const result = await inputWithBack({
389
+ message: "Describe the breaking change:",
390
+ default: state.breakingDesc || undefined,
391
+ validate: (val) => val.trim().length > 0 || "Breaking change description is required",
392
+ showBack: true,
393
+ });
394
+ if (result === BACK_VALUE) {
395
+ goBack();
396
+ }
397
+ else {
398
+ state.breakingDesc = result;
399
+ currentStep = undefined;
400
+ }
401
+ break;
402
+ }
403
+ default:
404
+ currentStep = undefined;
405
+ }
406
+ // Check if we've passed the last step
407
+ if (currentStep && stepOrder.indexOf(currentStep) >= stepOrder.length) {
408
+ break;
409
+ }
224
410
  }
225
411
  const ticket = inferTicket();
226
- const breaking = isBreaking ? "!" : "";
227
- const scope = finalScope || "";
412
+ const breaking = state.isBreaking ? "!" : "";
413
+ const scope = state.scope || "";
228
414
  const subject = formatCommitMessage(config.commitFormat, {
229
- type,
415
+ type: state.type,
230
416
  ticket,
231
417
  breaking,
232
418
  scope,
233
- message: message.trim(),
419
+ message: state.message.trim(),
234
420
  });
235
- // Get body (from flag or prompt)
236
- let body = options.body || "";
237
- if (!options.body && !options.yes) {
238
- const addBody = await confirm({
239
- message: "Add a longer description (body)?",
240
- default: false,
241
- });
242
- if (addBody) {
243
- body = await input({
244
- message: "Body (longer explanation):",
245
- });
246
- }
247
- }
248
- // Get breaking change description (from flag or prompt)
249
- let breakingFooter = "";
250
- if (isBreaking) {
251
- if (options.breakingDesc) {
252
- breakingFooter = options.breakingDesc;
253
- }
254
- else if (options.yes) {
255
- console.log(yellow("Warning: Breaking change without description. Use --breaking-desc to provide one."));
256
- }
257
- else {
258
- breakingFooter = await input({
259
- message: "Describe the breaking change:",
260
- validate: (val) => val.trim().length > 0 || "Breaking change description is required",
261
- });
262
- }
263
- }
264
421
  // Build full message
265
422
  const parts = [subject];
266
- if (body.trim())
267
- parts.push(body.trim());
423
+ if (state.body.trim())
424
+ parts.push(state.body.trim());
268
425
  const footers = [];
269
- if (breakingFooter.trim())
270
- footers.push(`BREAKING CHANGE: ${breakingFooter.trim()}`);
426
+ if (state.breakingDesc.trim())
427
+ footers.push(`BREAKING CHANGE: ${state.breakingDesc.trim()}`);
271
428
  if (ticket && ticket !== "UNTRACKED")
272
429
  footers.push(`Refs: ${ticket}`);
273
430
  if (footers.length > 0)
@@ -275,8 +432,8 @@ export async function commitCommand(options = {}) {
275
432
  const fullMessage = parts.join("\n\n");
276
433
  console.log(`\n${dim("───")} ${bold("Commit Preview")} ${dim("───")}`);
277
434
  console.log(green(subject));
278
- if (body.trim())
279
- console.log(`\n${body.trim()}`);
435
+ if (state.body.trim())
436
+ console.log(`\n${state.body.trim()}`);
280
437
  if (footers.length > 0)
281
438
  console.log(`\n${dim(footers.join("\n"))}`);
282
439
  console.log(`${dim("───────────────────")}\n`);
@@ -285,15 +442,26 @@ export async function commitCommand(options = {}) {
285
442
  return;
286
443
  }
287
444
  // Confirm (skip if --yes)
445
+ let confirmed = true;
288
446
  if (!options.yes) {
289
- const confirmed = await confirm({
447
+ const confirmResult = await selectWithBack({
290
448
  message: "Create this commit?",
291
- default: true,
449
+ choices: [
450
+ { value: "yes", name: "Yes, create commit" },
451
+ { value: "no", name: "No, abort" },
452
+ ],
453
+ default: "yes",
454
+ showBack: true,
292
455
  });
293
- if (!confirmed) {
294
- console.log("Commit aborted.");
295
- process.exit(0);
456
+ if (confirmResult === BACK_VALUE) {
457
+ // Go back to breaking desc or body step
458
+ return commitCommand(options); // Restart for simplicity (state is lost)
296
459
  }
460
+ confirmed = confirmResult === "yes";
461
+ }
462
+ if (!confirmed) {
463
+ console.log("Commit aborted.");
464
+ process.exit(0);
297
465
  }
298
466
  execSync(`git commit -m ${JSON.stringify(fullMessage)}`, {
299
467
  stdio: "inherit",