preguito 0.2.1 → 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";
@@ -327,6 +328,29 @@ function resolveShortcodes(shortcodesStr, config) {
327
328
  return result;
328
329
  }
329
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
+
330
354
  //#endregion
331
355
  //#region src/git/operations.ts
332
356
  const execFileAsync = promisify(execFile);
@@ -406,6 +430,7 @@ async function forcePushLease() {
406
430
  return result.stdout + result.stderr;
407
431
  }
408
432
  async function checkout(branch) {
433
+ validateBranchName(branch);
409
434
  await git(["checkout", branch]);
410
435
  }
411
436
  async function pull() {
@@ -413,10 +438,12 @@ async function pull() {
413
438
  return result.stdout + result.stderr;
414
439
  }
415
440
  async function rebase(branch) {
441
+ validateBranchName(branch);
416
442
  const result = await git(["rebase", branch]);
417
443
  return result.stdout + result.stderr;
418
444
  }
419
445
  async function rebaseInteractiveEdit(hash) {
446
+ validateHash(hash);
420
447
  const sedCmd = `sed -i 's/^pick ${hash.slice(0, 7)}/edit ${hash.slice(0, 7)}/'`;
421
448
  const result = await git([
422
449
  "rebase",
@@ -443,7 +470,8 @@ async function rebaseInteractive(count) {
443
470
  `HEAD~${count}`
444
471
  ]);
445
472
  }
446
- async function pushUpstream$1(branch) {
473
+ async function pushUpstream(branch) {
474
+ if (branch !== void 0) validateBranchName(branch);
447
475
  const result = await git([
448
476
  "push",
449
477
  "--set-upstream",
@@ -453,6 +481,7 @@ async function pushUpstream$1(branch) {
453
481
  return result.stdout + result.stderr;
454
482
  }
455
483
  async function commitFixup(hash) {
484
+ validateHash(hash);
456
485
  return (await git([
457
486
  "commit",
458
487
  "--fixup",
@@ -467,14 +496,17 @@ async function resetSoft(count = 1) {
467
496
  ])).stdout;
468
497
  }
469
498
  async function createBranch(branch) {
499
+ validateBranchName(branch);
470
500
  await git([
471
501
  "checkout",
472
502
  "-b",
473
503
  branch
474
504
  ]);
475
505
  }
476
- async function stash() {
477
- 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;
478
510
  }
479
511
  async function stashPop() {
480
512
  return (await git(["stash", "pop"])).stdout;
@@ -498,6 +530,7 @@ async function logGrep(keyword, count) {
498
530
  return (await git(args)).stdout;
499
531
  }
500
532
  async function logTag(tag) {
533
+ validateTagName(tag);
501
534
  return (await git([
502
535
  "log",
503
536
  "--oneline",
@@ -505,12 +538,44 @@ async function logTag(tag) {
505
538
  ])).stdout;
506
539
  }
507
540
  async function logTagAll(tag) {
541
+ validateTagName(tag);
508
542
  return (await git([
509
543
  "log",
510
544
  "--oneline",
511
545
  tag
512
546
  ])).stdout;
513
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
+ }
514
579
 
515
580
  //#endregion
516
581
  //#region src/utils/spinner.ts
@@ -557,23 +622,12 @@ function spinner(message, options) {
557
622
  //#endregion
558
623
  //#region src/commands/commit.ts
559
624
  function registerCommitCommand(program) {
560
- 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)").option("-b, --body <text>", "Commit body (optional, multi-line description)").action(async (args, opts) => {
561
- try {
562
- await executeCommit(args, opts);
563
- } catch (error) {
564
- if (error instanceof PrequitoError) {
565
- console.error(`\u2716 ${error.message}`);
566
- process.exit(1);
567
- }
568
- throw error;
569
- }
570
- });
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
+ }));
571
628
  }
572
629
  async function executeCommit(args, opts) {
573
- if (!await isGitRepo()) {
574
- console.error("✖ Not inside a git repository.");
575
- process.exit(1);
576
- }
630
+ await requireGitRepo();
577
631
  const config = await loadConfigOrDefault();
578
632
  const { context, message, body } = parsePositionalArgs(args, config, typeof opts.body === "string" ? opts.body : void 0);
579
633
  const commitTitle = renderTemplate(config.template, context, message);
@@ -583,10 +637,7 @@ async function executeCommit(args, opts) {
583
637
  return;
584
638
  }
585
639
  if (opts.stage !== false) await stageAll();
586
- if (!await hasStagedChanges()) {
587
- console.error("✖ No staged changes to commit.");
588
- process.exit(1);
589
- }
640
+ if (!await hasStagedChanges()) throw new PrequitoError("No staged changes to commit.");
590
641
  const stopCommit = spinner(`Committing: ${commitTitle}`);
591
642
  await commit(commitMessage);
592
643
  stopCommit("✔ Committed.");
@@ -604,34 +655,11 @@ async function executeCommit(args, opts) {
604
655
  //#endregion
605
656
  //#region src/commands/amend-push.ts
606
657
  function registerAmendPushCommands(program) {
607
- program.command("ap").description("Amend last commit + force push (git push --force)").action(async () => {
608
- try {
609
- await amendAndPush(false);
610
- } catch (error) {
611
- if (error instanceof PrequitoError) {
612
- console.error(`✖ ${error.message}`);
613
- process.exit(1);
614
- }
615
- throw error;
616
- }
617
- });
618
- program.command("apl").description("Amend last commit + safe force push (--force-with-lease)").action(async () => {
619
- try {
620
- await amendAndPush(true);
621
- } catch (error) {
622
- if (error instanceof PrequitoError) {
623
- console.error(`✖ ${error.message}`);
624
- process.exit(1);
625
- }
626
- throw error;
627
- }
628
- });
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)));
629
660
  }
630
661
  async function amendAndPush(useLease) {
631
- if (!await isGitRepo()) {
632
- console.error("✖ Not inside a git repository.");
633
- process.exit(1);
634
- }
662
+ await requireGitRepo();
635
663
  const stopStage = spinner("Staging all changes...");
636
664
  await stageAll();
637
665
  stopStage("✔ Staged.");
@@ -652,45 +680,12 @@ async function amendAndPush(useLease) {
652
680
  //#endregion
653
681
  //#region src/commands/rebase.ts
654
682
  function registerRebaseCommands(program) {
655
- program.command("r <branch>").alias("rebase").description("Rebase on <branch> (checkout, pull, rebase) (e.g. guito r main)").action(async (branch) => {
656
- try {
657
- await quickRebase(branch);
658
- } catch (error) {
659
- if (error instanceof PrequitoError) {
660
- console.error(`✖ ${error.message}`);
661
- process.exit(1);
662
- }
663
- throw error;
664
- }
665
- });
666
- program.command("ri <count>").description("Interactive rebase last <count> commits (e.g. guito ri 3)").action(async (count) => {
667
- try {
668
- await interactiveRebase(count);
669
- } catch (error) {
670
- if (error instanceof PrequitoError) {
671
- console.error(`✖ ${error.message}`);
672
- process.exit(1);
673
- }
674
- throw error;
675
- }
676
- });
677
- program.command("re <hash>").description("Edit rebase at <hash> (e.g. guito re abc123)").action(async (hash) => {
678
- try {
679
- await editRebase(hash);
680
- } catch (error) {
681
- if (error instanceof PrequitoError) {
682
- console.error(`✖ ${error.message}`);
683
- process.exit(1);
684
- }
685
- throw error;
686
- }
687
- });
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));
688
686
  }
689
687
  async function quickRebase(branch) {
690
- if (!await isGitRepo()) {
691
- console.error("✖ Not inside a git repository.");
692
- process.exit(1);
693
- }
688
+ await requireGitRepo();
694
689
  const currentBranch = await getCurrentBranch();
695
690
  console.log(`→ Current branch: ${currentBranch}`);
696
691
  let stop = spinner(`Checking out ${branch}...`);
@@ -707,23 +702,13 @@ async function quickRebase(branch) {
707
702
  stop("✔ Rebase complete.");
708
703
  }
709
704
  async function interactiveRebase(count) {
710
- if (!await isGitRepo()) {
711
- console.error("✖ Not inside a git repository.");
712
- process.exit(1);
713
- }
714
- const num = parseInt(count, 10);
715
- if (isNaN(num) || num <= 0) {
716
- console.error("✖ Count must be a positive integer.");
717
- process.exit(1);
718
- }
705
+ await requireGitRepo();
706
+ const num = parseCount(count);
719
707
  console.log(`→ Starting interactive rebase for the last ${num} commit(s)...`);
720
708
  await rebaseInteractive(num);
721
709
  }
722
710
  async function editRebase(hash) {
723
- if (!await isGitRepo()) {
724
- console.error("✖ Not inside a git repository.");
725
- process.exit(1);
726
- }
711
+ await requireGitRepo();
727
712
  console.log(`→ Starting edit rebase on commit ${hash}...`);
728
713
  await rebaseInteractiveEdit(hash);
729
714
  console.log("✔ Rebase paused at the target commit. Make your changes, then run:");
@@ -1044,76 +1029,32 @@ function registerConfigCommand(program) {
1044
1029
  //#endregion
1045
1030
  //#region src/commands/push.ts
1046
1031
  function registerPushCommands(program) {
1047
- program.command("p").alias("push").description("Push current branch (git push)").action(async () => {
1048
- try {
1049
- await executePush();
1050
- } catch (error) {
1051
- if (error instanceof PrequitoError) {
1052
- console.error(`✖ ${error.message}`);
1053
- process.exit(1);
1054
- }
1055
- throw error;
1056
- }
1057
- });
1058
- program.command("pu").description("Push + set upstream (git push --set-upstream origin <branch>)").action(async () => {
1059
- try {
1060
- await pushUpstream();
1061
- } catch (error) {
1062
- if (error instanceof PrequitoError) {
1063
- console.error(`✖ ${error.message}`);
1064
- process.exit(1);
1065
- }
1066
- throw error;
1067
- }
1068
- });
1069
- }
1070
- async function executePush() {
1071
- if (!await isGitRepo()) {
1072
- console.error("✖ Not inside a git repository.");
1073
- process.exit(1);
1074
- }
1075
- const stop = spinner("Pushing...");
1076
- await push();
1077
- stop("✔ Pushed.");
1078
- }
1079
- async function pushUpstream() {
1080
- if (!await isGitRepo()) {
1081
- console.error("✖ Not inside a git repository.");
1082
- process.exit(1);
1083
- }
1084
- const branch = await getCurrentBranch();
1085
- const stop = spinner(`Pushing with --set-upstream origin ${branch}...`);
1086
- await pushUpstream$1(branch);
1087
- 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
+ }));
1088
1045
  }
1089
1046
 
1090
1047
  //#endregion
1091
1048
  //#region src/commands/fixup.ts
1092
1049
  function registerFixupCommand(program) {
1093
- 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) => {
1094
- try {
1095
- await executeFixup(hash, opts);
1096
- } catch (error) {
1097
- if (error instanceof PrequitoError) {
1098
- console.error(`✖ ${error.message}`);
1099
- process.exit(1);
1100
- }
1101
- throw error;
1102
- }
1103
- });
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));
1104
1051
  }
1105
1052
  async function executeFixup(hash, opts) {
1106
- if (!await isGitRepo()) {
1107
- console.error("✖ Not inside a git repository.");
1108
- process.exit(1);
1109
- }
1053
+ await requireGitRepo();
1110
1054
  const stopStage = spinner("Staging all changes...");
1111
1055
  await stageAll();
1112
1056
  stopStage("✔ Staged.");
1113
- if (!await hasStagedChanges()) {
1114
- console.error("✖ No staged changes to commit.");
1115
- process.exit(1);
1116
- }
1057
+ if (!await hasStagedChanges()) throw new PrequitoError("No staged changes to commit.");
1117
1058
  const stopCommit = spinner(`Creating fixup commit for ${hash}...`);
1118
1059
  await commitFixup(hash);
1119
1060
  stopCommit("✔ Fixup commit created.");
@@ -1131,221 +1072,125 @@ async function executeFixup(hash, opts) {
1131
1072
  //#endregion
1132
1073
  //#region src/commands/undo.ts
1133
1074
  function registerUndoCommand(program) {
1134
- program.command("u [count]").alias("undo").description("Undo last N commits, keep changes staged (e.g. guito u 3)").action(async (count) => {
1135
- try {
1136
- await executeUndo(count);
1137
- } catch (error) {
1138
- if (error instanceof PrequitoError) {
1139
- console.error(`✖ ${error.message}`);
1140
- process.exit(1);
1141
- }
1142
- throw error;
1143
- }
1144
- });
1145
- }
1146
- async function executeUndo(count) {
1147
- if (!await isGitRepo()) {
1148
- console.error("✖ Not inside a git repository.");
1149
- process.exit(1);
1150
- }
1151
- const num = count ? parseInt(count, 10) : 1;
1152
- if (isNaN(num) || num <= 0) {
1153
- console.error("✖ Count must be a positive integer.");
1154
- process.exit(1);
1155
- }
1156
- const stop = spinner(`Undoing last ${num} commit(s)...`);
1157
- await resetSoft(num);
1158
- stop(`✔ Undid last ${num} commit(s). Changes are staged.`);
1159
- const st = await status();
1160
- 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
+ }));
1161
1084
  }
1162
1085
 
1163
1086
  //#endregion
1164
1087
  //#region src/commands/status.ts
1165
1088
  function registerStatusCommand(program) {
1166
- program.command("s").alias("status").description("Short status (git status --short)").action(async () => {
1167
- try {
1168
- await executeStatus();
1169
- } catch (error) {
1170
- if (error instanceof PrequitoError) {
1171
- console.error(`✖ ${error.message}`);
1172
- process.exit(1);
1173
- }
1174
- throw error;
1175
- }
1176
- });
1177
- }
1178
- async function executeStatus() {
1179
- if (!await isGitRepo()) {
1180
- console.error("✖ Not inside a git repository.");
1181
- process.exit(1);
1182
- }
1183
- const output = await status();
1184
- if (output) console.log(output.trimEnd());
1185
- 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
+ }));
1186
1095
  }
1187
1096
 
1188
1097
  //#endregion
1189
1098
  //#region src/commands/switch.ts
1190
1099
  function registerSwitchCommand(program) {
1191
- 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) => {
1192
- try {
1193
- await executeSwitch(branch, opts);
1194
- } catch (error) {
1195
- if (error instanceof PrequitoError) {
1196
- console.error(`✖ ${error.message}`);
1197
- process.exit(1);
1198
- }
1199
- 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);
1200
1108
  }
1201
- });
1202
- }
1203
- async function executeSwitch(branch, opts) {
1204
- if (!await isGitRepo()) {
1205
- console.error("✖ Not inside a git repository.");
1206
- process.exit(1);
1207
- }
1208
- const stop = spinner(opts.new ? `Creating and switching to ${branch}...` : `Switching to ${branch}...`);
1209
- if (opts.new) await createBranch(branch);
1210
- else await checkout(branch);
1211
- stop(`✔ On branch ${branch}.`);
1109
+ console.log(`✔ On branch ${branch}.`);
1110
+ }));
1212
1111
  }
1213
1112
 
1214
1113
  //#endregion
1215
1114
  //#region src/commands/stash.ts
1216
1115
  function registerStashCommands(program) {
1217
- program.command("st").description("Stash all changes (git stash)").action(async () => {
1218
- try {
1219
- await executeStash();
1220
- } catch (error) {
1221
- if (error instanceof PrequitoError) {
1222
- console.error(`✖ ${error.message}`);
1223
- process.exit(1);
1224
- }
1225
- throw error;
1226
- }
1227
- });
1228
- program.command("stp").description("Pop the latest stash (git stash pop)").action(async () => {
1229
- try {
1230
- await executeStashPop();
1231
- } catch (error) {
1232
- if (error instanceof PrequitoError) {
1233
- console.error(`✖ ${error.message}`);
1234
- process.exit(1);
1235
- }
1236
- throw error;
1237
- }
1238
- });
1239
- }
1240
- async function executeStash() {
1241
- if (!await isGitRepo()) {
1242
- console.error("✖ Not inside a git repository.");
1243
- process.exit(1);
1244
- }
1245
- const stop = spinner("Stashing changes...");
1246
- await stash();
1247
- stop("✔ Stashed.");
1248
- }
1249
- async function executeStashPop() {
1250
- if (!await isGitRepo()) {
1251
- console.error("✖ Not inside a git repository.");
1252
- process.exit(1);
1253
- }
1254
- const stop = spinner("Restoring stashed changes...");
1255
- await stashPop();
1256
- 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
+ }));
1257
1135
  }
1258
1136
 
1259
1137
  //#endregion
1260
1138
  //#region src/commands/log.ts
1261
1139
  function registerLogCommand(program) {
1262
- program.command("l [count]").alias("log").description("Compact log, default last 10 (e.g. guito l 20)").action(async (count) => {
1263
- try {
1264
- await executeLog(count);
1265
- } catch (error) {
1266
- if (error instanceof PrequitoError) {
1267
- console.error(`✖ ${error.message}`);
1268
- process.exit(1);
1269
- }
1270
- throw error;
1271
- }
1272
- });
1273
- }
1274
- async function executeLog(count) {
1275
- if (!await isGitRepo()) {
1276
- console.error("✖ Not inside a git repository.");
1277
- process.exit(1);
1278
- }
1279
- const num = count ? parseInt(count, 10) : 10;
1280
- if (isNaN(num) || num <= 0) {
1281
- console.error("✖ Count must be a positive integer.");
1282
- process.exit(1);
1283
- }
1284
- const output = await logOneline(num);
1285
- if (output) console.log(output.trimEnd());
1286
- 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
+ }));
1287
1147
  }
1288
1148
 
1289
1149
  //#endregion
1290
1150
  //#region src/commands/find.ts
1291
1151
  function registerFindCommands(program) {
1292
- 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) => {
1293
- try {
1294
- await executeFind(keyword, opts);
1295
- } catch (error) {
1296
- if (error instanceof PrequitoError) {
1297
- console.error(`✖ ${error.message}`);
1298
- process.exit(1);
1299
- }
1300
- throw error;
1301
- }
1302
- });
1303
- 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) => {
1304
- try {
1305
- await executeTag(tag, opts);
1306
- } catch (error) {
1307
- if (error instanceof PrequitoError) {
1308
- console.error(`✖ ${error.message}`);
1309
- process.exit(1);
1310
- }
1311
- 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.");
1312
1171
  }
1313
- });
1172
+ }));
1314
1173
  }
1315
- async function executeFind(keyword, opts) {
1316
- if (!await isGitRepo()) {
1317
- console.error("✖ Not inside a git repository.");
1318
- process.exit(1);
1319
- }
1320
- const count = opts.number ? parseInt(opts.number, 10) : void 0;
1321
- const stop = spinner(`Searching for "${keyword}"...`);
1322
- const output = await logGrep(keyword, count);
1323
- stop("");
1324
- if (output) console.log(output.trimEnd());
1325
- else console.log(`✨ No commits found matching "${keyword}".`);
1326
- }
1327
- async function executeTag(tag, opts) {
1328
- if (!await isGitRepo()) {
1329
- console.error("✖ Not inside a git repository.");
1330
- process.exit(1);
1331
- }
1332
- if (opts.all) {
1333
- console.log(`🏷️ Commits reachable from ${tag}:`);
1334
- const output = await logTagAll(tag);
1335
- if (output) console.log(output.trimEnd());
1336
- else console.log("✨ No commits found.");
1337
- } else {
1338
- console.log(`🏷️ Commits since ${tag}:`);
1339
- 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);
1340
1185
  if (output) console.log(output.trimEnd());
1341
- else console.log("✨ No commits since this tag.");
1342
- }
1186
+ else console.log("✨ No changes.");
1187
+ }));
1343
1188
  }
1344
1189
 
1345
1190
  //#endregion
1346
1191
  //#region src/cli.ts
1347
1192
  const program = new Command();
1348
- 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");
1349
1194
  registerCommitCommand(program);
1350
1195
  registerAmendPushCommands(program);
1351
1196
  registerRebaseCommands(program);
@@ -1359,6 +1204,7 @@ registerSwitchCommand(program);
1359
1204
  registerStashCommands(program);
1360
1205
  registerLogCommand(program);
1361
1206
  registerFindCommands(program);
1207
+ registerDiffCommand(program);
1362
1208
  program.parse();
1363
1209
 
1364
1210
  //#endregion