preguito 0.2.0 → 0.2.2

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/dist/cli.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ #!/usr/bin/env node
2
3
  import { Command } from "commander";
3
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
4
5
  import { existsSync } from "node:fs";
@@ -277,7 +278,7 @@ async function writeConfig(config) {
277
278
 
278
279
  //#endregion
279
280
  //#region src/commands/commit-parser.ts
280
- function parsePositionalArgs(args, config) {
281
+ function parsePositionalArgs(args, config, bodyFromFlag) {
281
282
  if (args.length === 0) throw new PrequitoError("No arguments provided. Usage: guito c [card_id] [shortcodes] <message...>");
282
283
  const context = {};
283
284
  let cursor = 0;
@@ -297,7 +298,8 @@ function parsePositionalArgs(args, config) {
297
298
  const message = messageParts.join(" ");
298
299
  return {
299
300
  context: mergeContext(config.defaults, context),
300
- message
301
+ message,
302
+ body: bodyFromFlag
301
303
  };
302
304
  }
303
305
  function resolveShortcodes(shortcodesStr, config) {
@@ -326,6 +328,29 @@ function resolveShortcodes(shortcodesStr, config) {
326
328
  return result;
327
329
  }
328
330
 
331
+ //#endregion
332
+ //#region src/utils/validation.ts
333
+ const GIT_HASH_PATTERN = /^[a-f0-9]{4,40}$/i;
334
+ const GIT_REF_FORBIDDEN_CHARS = /[\x00-\x1f\x7f ~^:?*[\\]/;
335
+ const GIT_REF_FORBIDDEN_SEQUENCES = /\.\.|\.lock(\/|$)|@\{|\/\//;
336
+ function validateHash(hash) {
337
+ if (!GIT_HASH_PATTERN.test(hash)) throw new PrequitoError(`Invalid git hash: "${hash}". Expected 4-40 hexadecimal characters.`);
338
+ }
339
+ function validateRefName(name, type) {
340
+ if (!name || name.trim() === "") throw new PrequitoError(`${type} name cannot be empty.`);
341
+ if (name.startsWith("-")) throw new PrequitoError(`Invalid ${type} name: "${name}". Cannot start with '-'.`);
342
+ if (name.endsWith(".")) throw new PrequitoError(`Invalid ${type} name: "${name}". Cannot end with '.'.`);
343
+ if (name.endsWith("/")) throw new PrequitoError(`Invalid ${type} name: "${name}". Cannot end with '/'.`);
344
+ if (GIT_REF_FORBIDDEN_CHARS.test(name)) throw new PrequitoError(`Invalid ${type} name: "${name}". Contains forbidden characters.`);
345
+ if (GIT_REF_FORBIDDEN_SEQUENCES.test(name)) throw new PrequitoError(`Invalid ${type} name: "${name}". Contains forbidden pattern.`);
346
+ }
347
+ function validateBranchName(branch) {
348
+ validateRefName(branch, "Branch");
349
+ }
350
+ function validateTagName(tag) {
351
+ validateRefName(tag, "Tag");
352
+ }
353
+
329
354
  //#endregion
330
355
  //#region src/git/operations.ts
331
356
  const execFileAsync = promisify(execFile);
@@ -374,11 +399,16 @@ async function stageAll() {
374
399
  await git(["add", "-A"]);
375
400
  }
376
401
  async function commit(message) {
377
- return (await git([
402
+ const parts = message.split("\n\n");
403
+ const title = parts[0];
404
+ const body = parts.slice(1).join("\n\n");
405
+ const args = [
378
406
  "commit",
379
407
  "-m",
380
- message
381
- ])).stdout;
408
+ title
409
+ ];
410
+ if (body) args.push("-m", body);
411
+ return (await git(args)).stdout;
382
412
  }
383
413
  async function commitAmend() {
384
414
  return (await git([
@@ -400,6 +430,7 @@ async function forcePushLease() {
400
430
  return result.stdout + result.stderr;
401
431
  }
402
432
  async function checkout(branch) {
433
+ validateBranchName(branch);
403
434
  await git(["checkout", branch]);
404
435
  }
405
436
  async function pull() {
@@ -407,10 +438,12 @@ async function pull() {
407
438
  return result.stdout + result.stderr;
408
439
  }
409
440
  async function rebase(branch) {
441
+ validateBranchName(branch);
410
442
  const result = await git(["rebase", branch]);
411
443
  return result.stdout + result.stderr;
412
444
  }
413
445
  async function rebaseInteractiveEdit(hash) {
446
+ validateHash(hash);
414
447
  const sedCmd = `sed -i 's/^pick ${hash.slice(0, 7)}/edit ${hash.slice(0, 7)}/'`;
415
448
  const result = await git([
416
449
  "rebase",
@@ -437,7 +470,8 @@ async function rebaseInteractive(count) {
437
470
  `HEAD~${count}`
438
471
  ]);
439
472
  }
440
- async function pushUpstream$1(branch) {
473
+ async function pushUpstream(branch) {
474
+ if (branch !== void 0) validateBranchName(branch);
441
475
  const result = await git([
442
476
  "push",
443
477
  "--set-upstream",
@@ -447,6 +481,7 @@ async function pushUpstream$1(branch) {
447
481
  return result.stdout + result.stderr;
448
482
  }
449
483
  async function commitFixup(hash) {
484
+ validateHash(hash);
450
485
  return (await git([
451
486
  "commit",
452
487
  "--fixup",
@@ -461,14 +496,17 @@ async function resetSoft(count = 1) {
461
496
  ])).stdout;
462
497
  }
463
498
  async function createBranch(branch) {
499
+ validateBranchName(branch);
464
500
  await git([
465
501
  "checkout",
466
502
  "-b",
467
503
  branch
468
504
  ]);
469
505
  }
470
- async function stash() {
471
- return (await git(["stash"])).stdout;
506
+ async function stash(message) {
507
+ const args = ["stash"];
508
+ if (message) args.push("-m", message);
509
+ return (await git(args)).stdout;
472
510
  }
473
511
  async function stashPop() {
474
512
  return (await git(["stash", "pop"])).stdout;
@@ -492,6 +530,7 @@ async function logGrep(keyword, count) {
492
530
  return (await git(args)).stdout;
493
531
  }
494
532
  async function logTag(tag) {
533
+ validateTagName(tag);
495
534
  return (await git([
496
535
  "log",
497
536
  "--oneline",
@@ -499,12 +538,44 @@ async function logTag(tag) {
499
538
  ])).stdout;
500
539
  }
501
540
  async function logTagAll(tag) {
541
+ validateTagName(tag);
502
542
  return (await git([
503
543
  "log",
504
544
  "--oneline",
505
545
  tag
506
546
  ])).stdout;
507
547
  }
548
+ async function diff(options = []) {
549
+ return (await git(["diff", ...options])).stdout;
550
+ }
551
+ async function stashList() {
552
+ return (await git(["stash", "list"])).stdout;
553
+ }
554
+
555
+ //#endregion
556
+ //#region src/utils/command.ts
557
+ async function requireGitRepo() {
558
+ if (!await isGitRepo()) throw new PrequitoError("Not inside a git repository.");
559
+ }
560
+ function withErrorHandling(fn) {
561
+ return async (...args) => {
562
+ try {
563
+ await fn(...args);
564
+ } catch (error) {
565
+ if (error instanceof PrequitoError) {
566
+ console.error(`✖ ${error.message}`);
567
+ process.exit(1);
568
+ }
569
+ throw error;
570
+ }
571
+ };
572
+ }
573
+ function parseCount(value, defaultValue = 1) {
574
+ if (value === void 0) return defaultValue;
575
+ const num = parseInt(value, 10);
576
+ if (isNaN(num) || num <= 0) throw new PrequitoError("Count must be a positive integer.");
577
+ return num;
578
+ }
508
579
 
509
580
  //#endregion
510
581
  //#region src/utils/spinner.ts
@@ -551,36 +622,23 @@ function spinner(message, options) {
551
622
  //#endregion
552
623
  //#region src/commands/commit.ts
553
624
  function registerCommitCommand(program) {
554
- program.command("c").alias("commit").description("Templated commit (e.g. guito c 42 f \"msg\" -p)").argument("[args...]", "Card ID, shortcodes, and message").option("-p, --push", "Push after committing").option("-f, --force", "Push with --force-with-lease after committing").option("-d, --dry-run", "Show the generated message without executing").option("-S, --no-stage", "Skip auto-staging (git add -A)").action(async (args, opts) => {
555
- try {
556
- await executeCommit(args, opts);
557
- } catch (error) {
558
- if (error instanceof PrequitoError) {
559
- console.error(`\u2716 ${error.message}`);
560
- process.exit(1);
561
- }
562
- throw error;
563
- }
564
- });
625
+ program.command("c").alias("commit").description("Templated commit (e.g. guito c 42 f \"msg\" -p)").argument("[args...]", "Card ID, shortcodes, and message").option("-p, --push", "Push after committing").option("-f, --force", "Push with --force-with-lease after committing").option("-d, --dry-run", "Show the generated message without executing").option("-S, --no-stage", "Skip auto-staging (git add -A)").action(withErrorHandling(async (args, opts) => {
626
+ await executeCommit(args, opts);
627
+ }));
565
628
  }
566
629
  async function executeCommit(args, opts) {
567
- if (!await isGitRepo()) {
568
- console.error("✖ Not inside a git repository.");
569
- process.exit(1);
570
- }
630
+ await requireGitRepo();
571
631
  const config = await loadConfigOrDefault();
572
- const { context, message } = parsePositionalArgs(args, config);
573
- const commitMessage = renderTemplate(config.template, context, message);
632
+ const { context, message, body } = parsePositionalArgs(args, config, typeof opts.body === "string" ? opts.body : void 0);
633
+ const commitTitle = renderTemplate(config.template, context, message);
634
+ const commitMessage = body ? `${commitTitle}\n\n${body}` : commitTitle;
574
635
  if (opts.dryRun) {
575
636
  console.log(commitMessage);
576
637
  return;
577
638
  }
578
639
  if (opts.stage !== false) await stageAll();
579
- if (!await hasStagedChanges()) {
580
- console.error("✖ No staged changes to commit.");
581
- process.exit(1);
582
- }
583
- const stopCommit = spinner(`Committing: ${commitMessage}`);
640
+ if (!await hasStagedChanges()) throw new PrequitoError("No staged changes to commit.");
641
+ const stopCommit = spinner(`Committing: ${commitTitle}`);
584
642
  await commit(commitMessage);
585
643
  stopCommit("✔ Committed.");
586
644
  if (opts.force) {
@@ -597,34 +655,11 @@ async function executeCommit(args, opts) {
597
655
  //#endregion
598
656
  //#region src/commands/amend-push.ts
599
657
  function registerAmendPushCommands(program) {
600
- program.command("ap").description("Amend last commit + force push (git push --force)").action(async () => {
601
- try {
602
- await amendAndPush(false);
603
- } catch (error) {
604
- if (error instanceof PrequitoError) {
605
- console.error(`✖ ${error.message}`);
606
- process.exit(1);
607
- }
608
- throw error;
609
- }
610
- });
611
- program.command("apl").description("Amend last commit + safe force push (--force-with-lease)").action(async () => {
612
- try {
613
- await amendAndPush(true);
614
- } catch (error) {
615
- if (error instanceof PrequitoError) {
616
- console.error(`✖ ${error.message}`);
617
- process.exit(1);
618
- }
619
- throw error;
620
- }
621
- });
658
+ program.command("ap").description("Amend last commit + force push (git push --force)").action(withErrorHandling(() => amendAndPush(false)));
659
+ program.command("apl").description("Amend last commit + safe force push (--force-with-lease)").action(withErrorHandling(() => amendAndPush(true)));
622
660
  }
623
661
  async function amendAndPush(useLease) {
624
- if (!await isGitRepo()) {
625
- console.error("✖ Not inside a git repository.");
626
- process.exit(1);
627
- }
662
+ await requireGitRepo();
628
663
  const stopStage = spinner("Staging all changes...");
629
664
  await stageAll();
630
665
  stopStage("✔ Staged.");
@@ -645,45 +680,12 @@ async function amendAndPush(useLease) {
645
680
  //#endregion
646
681
  //#region src/commands/rebase.ts
647
682
  function registerRebaseCommands(program) {
648
- program.command("r <branch>").alias("rebase").description("Rebase on <branch> (checkout, pull, rebase) (e.g. guito r main)").action(async (branch) => {
649
- try {
650
- await quickRebase(branch);
651
- } catch (error) {
652
- if (error instanceof PrequitoError) {
653
- console.error(`✖ ${error.message}`);
654
- process.exit(1);
655
- }
656
- throw error;
657
- }
658
- });
659
- program.command("ri <count>").description("Interactive rebase last <count> commits (e.g. guito ri 3)").action(async (count) => {
660
- try {
661
- await interactiveRebase(count);
662
- } catch (error) {
663
- if (error instanceof PrequitoError) {
664
- console.error(`✖ ${error.message}`);
665
- process.exit(1);
666
- }
667
- throw error;
668
- }
669
- });
670
- program.command("re <hash>").description("Edit rebase at <hash> (e.g. guito re abc123)").action(async (hash) => {
671
- try {
672
- await editRebase(hash);
673
- } catch (error) {
674
- if (error instanceof PrequitoError) {
675
- console.error(`✖ ${error.message}`);
676
- process.exit(1);
677
- }
678
- throw error;
679
- }
680
- });
683
+ program.command("r <branch>").alias("rebase").description("Rebase on <branch> (checkout, pull, rebase) (e.g. guito r main)").action(withErrorHandling(quickRebase));
684
+ program.command("ri <count>").description("Interactive rebase last <count> commits (e.g. guito ri 3)").action(withErrorHandling(interactiveRebase));
685
+ program.command("re <hash>").description("Edit rebase at <hash> (e.g. guito re abc123)").action(withErrorHandling(editRebase));
681
686
  }
682
687
  async function quickRebase(branch) {
683
- if (!await isGitRepo()) {
684
- console.error("✖ Not inside a git repository.");
685
- process.exit(1);
686
- }
688
+ await requireGitRepo();
687
689
  const currentBranch = await getCurrentBranch();
688
690
  console.log(`→ Current branch: ${currentBranch}`);
689
691
  let stop = spinner(`Checking out ${branch}...`);
@@ -700,23 +702,13 @@ async function quickRebase(branch) {
700
702
  stop("✔ Rebase complete.");
701
703
  }
702
704
  async function interactiveRebase(count) {
703
- if (!await isGitRepo()) {
704
- console.error("✖ Not inside a git repository.");
705
- process.exit(1);
706
- }
707
- const num = parseInt(count, 10);
708
- if (isNaN(num) || num <= 0) {
709
- console.error("✖ Count must be a positive integer.");
710
- process.exit(1);
711
- }
705
+ await requireGitRepo();
706
+ const num = parseCount(count);
712
707
  console.log(`→ Starting interactive rebase for the last ${num} commit(s)...`);
713
708
  await rebaseInteractive(num);
714
709
  }
715
710
  async function editRebase(hash) {
716
- if (!await isGitRepo()) {
717
- console.error("✖ Not inside a git repository.");
718
- process.exit(1);
719
- }
711
+ await requireGitRepo();
720
712
  console.log(`→ Starting edit rebase on commit ${hash}...`);
721
713
  await rebaseInteractiveEdit(hash);
722
714
  console.log("✔ Rebase paused at the target commit. Make your changes, then run:");
@@ -1037,76 +1029,32 @@ function registerConfigCommand(program) {
1037
1029
  //#endregion
1038
1030
  //#region src/commands/push.ts
1039
1031
  function registerPushCommands(program) {
1040
- program.command("p").alias("push").description("Push current branch (git push)").action(async () => {
1041
- try {
1042
- await executePush();
1043
- } catch (error) {
1044
- if (error instanceof PrequitoError) {
1045
- console.error(`✖ ${error.message}`);
1046
- process.exit(1);
1047
- }
1048
- throw error;
1049
- }
1050
- });
1051
- program.command("pu").description("Push + set upstream (git push --set-upstream origin <branch>)").action(async () => {
1052
- try {
1053
- await pushUpstream();
1054
- } catch (error) {
1055
- if (error instanceof PrequitoError) {
1056
- console.error(`✖ ${error.message}`);
1057
- process.exit(1);
1058
- }
1059
- throw error;
1060
- }
1061
- });
1062
- }
1063
- async function executePush() {
1064
- if (!await isGitRepo()) {
1065
- console.error("✖ Not inside a git repository.");
1066
- process.exit(1);
1067
- }
1068
- const stop = spinner("Pushing...");
1069
- await push();
1070
- stop("✔ Pushed.");
1071
- }
1072
- async function pushUpstream() {
1073
- if (!await isGitRepo()) {
1074
- console.error("✖ Not inside a git repository.");
1075
- process.exit(1);
1076
- }
1077
- const branch = await getCurrentBranch();
1078
- const stop = spinner(`Pushing with --set-upstream origin ${branch}...`);
1079
- await pushUpstream$1(branch);
1080
- stop("✔ Pushed.");
1032
+ program.command("p").alias("push").description("Push current branch (git push)").action(withErrorHandling(async () => {
1033
+ await requireGitRepo();
1034
+ const stop = spinner("Pushing...");
1035
+ await push();
1036
+ stop("✔ Pushed.");
1037
+ }));
1038
+ program.command("pu").description("Push + set upstream (git push --set-upstream origin <branch>)").action(withErrorHandling(async () => {
1039
+ await requireGitRepo();
1040
+ const branch = await getCurrentBranch();
1041
+ const stop = spinner(`Pushing with --set-upstream origin ${branch}...`);
1042
+ await pushUpstream(branch);
1043
+ stop("✔ Pushed.");
1044
+ }));
1081
1045
  }
1082
1046
 
1083
1047
  //#endregion
1084
1048
  //#region src/commands/fixup.ts
1085
1049
  function registerFixupCommand(program) {
1086
- program.command("cf <hash>").description("Fixup commit for <hash> (e.g. guito cf abc123 -f)").option("-p, --push", "Push after creating the fixup commit").option("-f, --force", "Push with --force-with-lease after creating").action(async (hash, opts) => {
1087
- try {
1088
- await executeFixup(hash, opts);
1089
- } catch (error) {
1090
- if (error instanceof PrequitoError) {
1091
- console.error(`✖ ${error.message}`);
1092
- process.exit(1);
1093
- }
1094
- throw error;
1095
- }
1096
- });
1050
+ program.command("cf <hash>").description("Fixup commit for <hash> (e.g. guito cf abc123 -f)").option("-p, --push", "Push after creating the fixup commit").option("-f, --force", "Push with --force-with-lease after creating").action(withErrorHandling(executeFixup));
1097
1051
  }
1098
1052
  async function executeFixup(hash, opts) {
1099
- if (!await isGitRepo()) {
1100
- console.error("✖ Not inside a git repository.");
1101
- process.exit(1);
1102
- }
1053
+ await requireGitRepo();
1103
1054
  const stopStage = spinner("Staging all changes...");
1104
1055
  await stageAll();
1105
1056
  stopStage("✔ Staged.");
1106
- if (!await hasStagedChanges()) {
1107
- console.error("✖ No staged changes to commit.");
1108
- process.exit(1);
1109
- }
1057
+ if (!await hasStagedChanges()) throw new PrequitoError("No staged changes to commit.");
1110
1058
  const stopCommit = spinner(`Creating fixup commit for ${hash}...`);
1111
1059
  await commitFixup(hash);
1112
1060
  stopCommit("✔ Fixup commit created.");
@@ -1124,221 +1072,125 @@ async function executeFixup(hash, opts) {
1124
1072
  //#endregion
1125
1073
  //#region src/commands/undo.ts
1126
1074
  function registerUndoCommand(program) {
1127
- program.command("u [count]").alias("undo").description("Undo last N commits, keep changes staged (e.g. guito u 3)").action(async (count) => {
1128
- try {
1129
- await executeUndo(count);
1130
- } catch (error) {
1131
- if (error instanceof PrequitoError) {
1132
- console.error(`✖ ${error.message}`);
1133
- process.exit(1);
1134
- }
1135
- throw error;
1136
- }
1137
- });
1138
- }
1139
- async function executeUndo(count) {
1140
- if (!await isGitRepo()) {
1141
- console.error("✖ Not inside a git repository.");
1142
- process.exit(1);
1143
- }
1144
- const num = count ? parseInt(count, 10) : 1;
1145
- if (isNaN(num) || num <= 0) {
1146
- console.error("✖ Count must be a positive integer.");
1147
- process.exit(1);
1148
- }
1149
- const stop = spinner(`Undoing last ${num} commit(s)...`);
1150
- await resetSoft(num);
1151
- stop(`✔ Undid last ${num} commit(s). Changes are staged.`);
1152
- const st = await status();
1153
- if (st) console.log(st);
1075
+ program.command("u [count]").alias("undo").description("Undo last N commits, keep changes staged (e.g. guito u 3)").action(withErrorHandling(async (count) => {
1076
+ await requireGitRepo();
1077
+ const num = parseCount(count);
1078
+ console.log(`→ Undoing last ${num} commit(s)...`);
1079
+ await resetSoft(num);
1080
+ console.log(`✔ Undid last ${num} commit(s). Changes are staged.`);
1081
+ const st = await status();
1082
+ if (st) console.log(st);
1083
+ }));
1154
1084
  }
1155
1085
 
1156
1086
  //#endregion
1157
1087
  //#region src/commands/status.ts
1158
1088
  function registerStatusCommand(program) {
1159
- program.command("s").alias("status").description("Short status (git status --short)").action(async () => {
1160
- try {
1161
- await executeStatus();
1162
- } catch (error) {
1163
- if (error instanceof PrequitoError) {
1164
- console.error(`✖ ${error.message}`);
1165
- process.exit(1);
1166
- }
1167
- throw error;
1168
- }
1169
- });
1170
- }
1171
- async function executeStatus() {
1172
- if (!await isGitRepo()) {
1173
- console.error("✖ Not inside a git repository.");
1174
- process.exit(1);
1175
- }
1176
- const output = await status();
1177
- if (output) console.log(output.trimEnd());
1178
- else console.log("✨ Nothing to commit, working tree clean.");
1089
+ program.command("s").alias("status").description("Short status (git status --short)").action(withErrorHandling(async () => {
1090
+ await requireGitRepo();
1091
+ const output = await status();
1092
+ if (output) console.log(output.trimEnd());
1093
+ else console.log("✨ Nothing to commit, working tree clean.");
1094
+ }));
1179
1095
  }
1180
1096
 
1181
1097
  //#endregion
1182
1098
  //#region src/commands/switch.ts
1183
1099
  function registerSwitchCommand(program) {
1184
- program.command("sw <branch>").alias("switch").description("Switch branch, -n to create (e.g. guito sw -n feature/x)").option("-n, --new", "Create a new branch").action(async (branch, opts) => {
1185
- try {
1186
- await executeSwitch(branch, opts);
1187
- } catch (error) {
1188
- if (error instanceof PrequitoError) {
1189
- console.error(`✖ ${error.message}`);
1190
- process.exit(1);
1191
- }
1192
- throw error;
1100
+ program.command("sw <branch>").alias("switch").description("Switch branch, -n to create (e.g. guito sw -n feature/x)").option("-n, --new", "Create a new branch").action(withErrorHandling(async (branch, opts) => {
1101
+ await requireGitRepo();
1102
+ if (opts.new) {
1103
+ console.log(`→ Creating and switching to ${branch}...`);
1104
+ await createBranch(branch);
1105
+ } else {
1106
+ console.log(`→ Switching to ${branch}...`);
1107
+ await checkout(branch);
1193
1108
  }
1194
- });
1195
- }
1196
- async function executeSwitch(branch, opts) {
1197
- if (!await isGitRepo()) {
1198
- console.error("✖ Not inside a git repository.");
1199
- process.exit(1);
1200
- }
1201
- const stop = spinner(opts.new ? `Creating and switching to ${branch}...` : `Switching to ${branch}...`);
1202
- if (opts.new) await createBranch(branch);
1203
- else await checkout(branch);
1204
- stop(`✔ On branch ${branch}.`);
1109
+ console.log(`✔ On branch ${branch}.`);
1110
+ }));
1205
1111
  }
1206
1112
 
1207
1113
  //#endregion
1208
1114
  //#region src/commands/stash.ts
1209
1115
  function registerStashCommands(program) {
1210
- program.command("st").description("Stash all changes (git stash)").action(async () => {
1211
- try {
1212
- await executeStash();
1213
- } catch (error) {
1214
- if (error instanceof PrequitoError) {
1215
- console.error(`✖ ${error.message}`);
1216
- process.exit(1);
1217
- }
1218
- throw error;
1219
- }
1220
- });
1221
- program.command("stp").description("Pop the latest stash (git stash pop)").action(async () => {
1222
- try {
1223
- await executeStashPop();
1224
- } catch (error) {
1225
- if (error instanceof PrequitoError) {
1226
- console.error(`✖ ${error.message}`);
1227
- process.exit(1);
1228
- }
1229
- throw error;
1230
- }
1231
- });
1232
- }
1233
- async function executeStash() {
1234
- if (!await isGitRepo()) {
1235
- console.error("✖ Not inside a git repository.");
1236
- process.exit(1);
1237
- }
1238
- const stop = spinner("Stashing changes...");
1239
- await stash();
1240
- stop("✔ Stashed.");
1241
- }
1242
- async function executeStashPop() {
1243
- if (!await isGitRepo()) {
1244
- console.error("✖ Not inside a git repository.");
1245
- process.exit(1);
1246
- }
1247
- const stop = spinner("Restoring stashed changes...");
1248
- await stashPop();
1249
- stop("✔ Restored.");
1116
+ program.command("st").description("Stash all changes (git stash)").option("-m, --message <message>", "Stash with a descriptive message").action(withErrorHandling(async (opts) => {
1117
+ await requireGitRepo();
1118
+ if (opts.message) console.log(`→ Stashing changes: ${opts.message}...`);
1119
+ else console.log("→ Stashing changes...");
1120
+ await stash(opts.message);
1121
+ console.log("✔ Stashed.");
1122
+ }));
1123
+ program.command("stp").description("Pop the latest stash (git stash pop)").action(withErrorHandling(async () => {
1124
+ await requireGitRepo();
1125
+ console.log("→ Restoring stashed changes...");
1126
+ await stashPop();
1127
+ console.log("✔ Restored.");
1128
+ }));
1129
+ program.command("stl").description("List all stashes (git stash list)").action(withErrorHandling(async () => {
1130
+ await requireGitRepo();
1131
+ const output = await stashList();
1132
+ if (output) console.log(output.trimEnd());
1133
+ else console.log("✨ No stashes found.");
1134
+ }));
1250
1135
  }
1251
1136
 
1252
1137
  //#endregion
1253
1138
  //#region src/commands/log.ts
1254
1139
  function registerLogCommand(program) {
1255
- program.command("l [count]").alias("log").description("Compact log, default last 10 (e.g. guito l 20)").action(async (count) => {
1256
- try {
1257
- await executeLog(count);
1258
- } catch (error) {
1259
- if (error instanceof PrequitoError) {
1260
- console.error(`✖ ${error.message}`);
1261
- process.exit(1);
1262
- }
1263
- throw error;
1264
- }
1265
- });
1266
- }
1267
- async function executeLog(count) {
1268
- if (!await isGitRepo()) {
1269
- console.error("✖ Not inside a git repository.");
1270
- process.exit(1);
1271
- }
1272
- const num = count ? parseInt(count, 10) : 10;
1273
- if (isNaN(num) || num <= 0) {
1274
- console.error("✖ Count must be a positive integer.");
1275
- process.exit(1);
1276
- }
1277
- const output = await logOneline(num);
1278
- if (output) console.log(output.trimEnd());
1279
- else console.log("✨ No commits found.");
1140
+ program.command("l [count]").alias("log").description("Compact log, default last 10 (e.g. guito l 20)").action(withErrorHandling(async (count) => {
1141
+ await requireGitRepo();
1142
+ const num = parseCount(count, 10);
1143
+ const output = await logOneline(num);
1144
+ if (output) console.log(output.trimEnd());
1145
+ else console.log("✨ No commits found.");
1146
+ }));
1280
1147
  }
1281
1148
 
1282
1149
  //#endregion
1283
1150
  //#region src/commands/find.ts
1284
1151
  function registerFindCommands(program) {
1285
- program.command("f <keyword>").alias("find").description("Search commits by message (e.g. guito f \"login\")").option("-n, --number <count>", "Limit number of results").action(async (keyword, opts) => {
1286
- try {
1287
- await executeFind(keyword, opts);
1288
- } catch (error) {
1289
- if (error instanceof PrequitoError) {
1290
- console.error(`✖ ${error.message}`);
1291
- process.exit(1);
1292
- }
1293
- throw error;
1294
- }
1295
- });
1296
- program.command("t <tag>").alias("tag").description("Commits since <tag> (e.g. guito t v1.0.0)").option("-a, --all", "Show all commits reachable from the tag").action(async (tag, opts) => {
1297
- try {
1298
- await executeTag(tag, opts);
1299
- } catch (error) {
1300
- if (error instanceof PrequitoError) {
1301
- console.error(`✖ ${error.message}`);
1302
- process.exit(1);
1303
- }
1304
- throw error;
1152
+ program.command("f <keyword>").alias("find").description("Search commits by message (e.g. guito f \"login\")").option("-n, --number <count>", "Limit number of results").action(withErrorHandling(async (keyword, opts) => {
1153
+ await requireGitRepo();
1154
+ const count = opts.number ? parseInt(opts.number, 10) : void 0;
1155
+ const output = await logGrep(keyword, count);
1156
+ if (output) console.log(output.trimEnd());
1157
+ else console.log(`✨ No commits found matching "${keyword}".`);
1158
+ }));
1159
+ program.command("t <tag>").alias("tag").description("Commits since <tag> (e.g. guito t v1.0.0)").option("-a, --all", "Show all commits reachable from the tag").action(withErrorHandling(async (tag, opts) => {
1160
+ await requireGitRepo();
1161
+ if (opts.all) {
1162
+ console.log(`🏷️ Commits reachable from ${tag}:`);
1163
+ const output = await logTagAll(tag);
1164
+ if (output) console.log(output.trimEnd());
1165
+ else console.log("✨ No commits found.");
1166
+ } else {
1167
+ console.log(`🏷️ Commits since ${tag}:`);
1168
+ const output = await logTag(tag);
1169
+ if (output) console.log(output.trimEnd());
1170
+ else console.log("✨ No commits since this tag.");
1305
1171
  }
1306
- });
1172
+ }));
1307
1173
  }
1308
- async function executeFind(keyword, opts) {
1309
- if (!await isGitRepo()) {
1310
- console.error("✖ Not inside a git repository.");
1311
- process.exit(1);
1312
- }
1313
- const count = opts.number ? parseInt(opts.number, 10) : void 0;
1314
- const stop = spinner(`Searching for "${keyword}"...`);
1315
- const output = await logGrep(keyword, count);
1316
- stop("");
1317
- if (output) console.log(output.trimEnd());
1318
- else console.log(`✨ No commits found matching "${keyword}".`);
1319
- }
1320
- async function executeTag(tag, opts) {
1321
- if (!await isGitRepo()) {
1322
- console.error("✖ Not inside a git repository.");
1323
- process.exit(1);
1324
- }
1325
- if (opts.all) {
1326
- console.log(`🏷️ Commits reachable from ${tag}:`);
1327
- const output = await logTagAll(tag);
1328
- if (output) console.log(output.trimEnd());
1329
- else console.log("✨ No commits found.");
1330
- } else {
1331
- console.log(`🏷️ Commits since ${tag}:`);
1332
- const output = await logTag(tag);
1174
+
1175
+ //#endregion
1176
+ //#region src/commands/diff.ts
1177
+ function registerDiffCommand(program) {
1178
+ program.command("d").alias("diff").description("Show changes (git diff)").option("-s, --staged", "Show staged changes only").option("--stat", "Show diffstat summary").option("-n, --name-only", "Show only names of changed files").action(withErrorHandling(async (opts) => {
1179
+ await requireGitRepo();
1180
+ const options = [];
1181
+ if (opts.staged) options.push("--staged");
1182
+ if (opts.stat) options.push("--stat");
1183
+ if (opts.nameOnly) options.push("--name-only");
1184
+ const output = await diff(options);
1333
1185
  if (output) console.log(output.trimEnd());
1334
- else console.log("✨ No commits since this tag.");
1335
- }
1186
+ else console.log("✨ No changes.");
1187
+ }));
1336
1188
  }
1337
1189
 
1338
1190
  //#endregion
1339
1191
  //#region src/cli.ts
1340
1192
  const program = new Command();
1341
- program.name("guito").description("preguito - a lazy git CLI with commit templates and shortcuts").version("0.1.0").addHelpText("before", "\n🦥 preguito v0.1.0\n A lazy git CLI with commit templates and shortcuts.\n");
1193
+ program.name("guito").description("preguito - a lazy git CLI with commit templates and shortcuts").version("0.2.2").addHelpText("before", "\n🦥 preguito v0.1.0\n A lazy git CLI with commit templates and shortcuts.\n");
1342
1194
  registerCommitCommand(program);
1343
1195
  registerAmendPushCommands(program);
1344
1196
  registerRebaseCommands(program);
@@ -1352,6 +1204,7 @@ registerSwitchCommand(program);
1352
1204
  registerStashCommands(program);
1353
1205
  registerLogCommand(program);
1354
1206
  registerFindCommands(program);
1207
+ registerDiffCommand(program);
1355
1208
  program.parse();
1356
1209
 
1357
1210
  //#endregion