preguito 0.1.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.
package/dist/cli.mjs ADDED
@@ -0,0 +1,1234 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { existsSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { homedir } from "node:os";
7
+ import { execFile, spawn } from "node:child_process";
8
+ import { promisify } from "node:util";
9
+ import { createInterface } from "node:readline/promises";
10
+ import { stdin, stdout } from "node:process";
11
+
12
+ //#region src/config/types.ts
13
+ const PREDEFINED_TYPES = [
14
+ {
15
+ key: "f",
16
+ label: "feat"
17
+ },
18
+ {
19
+ key: "x",
20
+ label: "fix"
21
+ },
22
+ {
23
+ key: "c",
24
+ label: "chore"
25
+ },
26
+ {
27
+ key: "t",
28
+ label: "test"
29
+ },
30
+ {
31
+ key: "l",
32
+ label: "lint"
33
+ },
34
+ {
35
+ key: "r",
36
+ label: "refactor"
37
+ },
38
+ {
39
+ key: "o",
40
+ label: "docs"
41
+ },
42
+ {
43
+ key: "y",
44
+ label: "style"
45
+ },
46
+ {
47
+ key: "e",
48
+ label: "perf"
49
+ },
50
+ {
51
+ key: "b",
52
+ label: "build"
53
+ }
54
+ ];
55
+ const PREDEFINED_ENVIRONMENTS = [
56
+ {
57
+ key: "p",
58
+ label: "prd"
59
+ },
60
+ {
61
+ key: "u",
62
+ label: "uat"
63
+ },
64
+ {
65
+ key: "h",
66
+ label: "homolog"
67
+ },
68
+ {
69
+ key: "d",
70
+ label: "dev"
71
+ },
72
+ {
73
+ key: "s",
74
+ label: "staging"
75
+ }
76
+ ];
77
+ function generateTemplate(features, hasPrefix) {
78
+ const { cardId, type, environment } = features;
79
+ return [
80
+ cardId ? hasPrefix ? "[{{prefix}}-{{card_id}}]" : "[{{card_id}}]" : "",
81
+ type && environment ? "{{type}}({{environment}}):" : type ? "{{type}}:" : environment ? "({{environment}}):" : "",
82
+ "<message>"
83
+ ].filter((p) => p !== "").join(" ");
84
+ }
85
+ const DEFAULT_CONFIG = {
86
+ template: "{{type}}: <message>",
87
+ features: {
88
+ cardId: false,
89
+ type: true,
90
+ environment: false
91
+ },
92
+ types: [
93
+ {
94
+ key: "f",
95
+ label: "feat"
96
+ },
97
+ {
98
+ key: "x",
99
+ label: "fix"
100
+ },
101
+ {
102
+ key: "c",
103
+ label: "chore"
104
+ },
105
+ {
106
+ key: "t",
107
+ label: "test"
108
+ },
109
+ {
110
+ key: "r",
111
+ label: "refactor"
112
+ },
113
+ {
114
+ key: "o",
115
+ label: "docs"
116
+ }
117
+ ],
118
+ environments: [],
119
+ defaults: {}
120
+ };
121
+ const CONFIG_PATHS = [
122
+ ".preguitorc",
123
+ ".preguitorc.json",
124
+ ".config/preguito/config.json"
125
+ ];
126
+
127
+ //#endregion
128
+ //#region src/utils/errors.ts
129
+ var PrequitoError = class extends Error {
130
+ constructor(message) {
131
+ super(message);
132
+ this.name = "PrequitoError";
133
+ }
134
+ };
135
+ var TemplateMissingVariableError = class extends PrequitoError {
136
+ missingVariables;
137
+ constructor(missing) {
138
+ super(`Missing template variable(s): ${missing.join(", ")}. Provide shortcodes or check your config.`);
139
+ this.name = "TemplateMissingVariableError";
140
+ this.missingVariables = missing;
141
+ }
142
+ };
143
+ var TemplateMissingMessageError = class extends PrequitoError {
144
+ constructor(placeholderName) {
145
+ super(`Template requires a message (the <${placeholderName}> placeholder). Provide it as the last argument(s).`);
146
+ this.name = "TemplateMissingMessageError";
147
+ }
148
+ };
149
+ var GitOperationError = class extends PrequitoError {
150
+ exitCode;
151
+ stderr;
152
+ constructor(operation, exitCode, stderr) {
153
+ super(`Git ${operation} failed (exit ${exitCode}): ${stderr.trim()}`);
154
+ this.name = "GitOperationError";
155
+ this.exitCode = exitCode;
156
+ this.stderr = stderr;
157
+ }
158
+ };
159
+
160
+ //#endregion
161
+ //#region src/template/engine.ts
162
+ const VARIABLE_PATTERN = /\{\{(\w+)\}\}/g;
163
+ const MESSAGE_PLACEHOLDER_PATTERN = /<(\w+)>/;
164
+ function parseTemplate(template) {
165
+ const variables = [];
166
+ let match;
167
+ const pattern = new RegExp(VARIABLE_PATTERN.source, "g");
168
+ while ((match = pattern.exec(template)) !== null) if (!variables.includes(match[1])) variables.push(match[1]);
169
+ const messageMatch = MESSAGE_PLACEHOLDER_PATTERN.exec(template);
170
+ return {
171
+ variables,
172
+ messagePlaceholder: messageMatch ? messageMatch[1] : null
173
+ };
174
+ }
175
+ function renderTemplate(template, context, message) {
176
+ const parsed = parseTemplate(template);
177
+ const missing = parsed.variables.filter((v) => !(v in context));
178
+ if (missing.length > 0) throw new TemplateMissingVariableError(missing);
179
+ let result = template.replace(/\{\{(\w+)\}\}/g, (_, varName) => {
180
+ return context[varName];
181
+ });
182
+ if (parsed.messagePlaceholder) {
183
+ if (!message) throw new TemplateMissingMessageError(parsed.messagePlaceholder);
184
+ result = result.replace(/<\w+>/, message);
185
+ }
186
+ return result;
187
+ }
188
+ function mergeContext(defaults, overrides) {
189
+ return {
190
+ ...defaults,
191
+ ...overrides
192
+ };
193
+ }
194
+
195
+ //#endregion
196
+ //#region src/config/loader.ts
197
+ async function findConfigPath() {
198
+ const cwd = process.cwd();
199
+ const home = homedir();
200
+ for (const relPath of [".preguitorc", ".preguitorc.json"]) {
201
+ const fullPath = join(cwd, relPath);
202
+ if (existsSync(fullPath)) return fullPath;
203
+ }
204
+ for (const relPath of CONFIG_PATHS) {
205
+ const fullPath = join(home, relPath);
206
+ if (existsSync(fullPath)) return fullPath;
207
+ }
208
+ return null;
209
+ }
210
+ async function loadConfig() {
211
+ const configPath = await findConfigPath();
212
+ if (!configPath) return null;
213
+ const raw = await readFile(configPath, "utf-8");
214
+ return validateConfig(JSON.parse(raw));
215
+ }
216
+ async function loadConfigOrDefault() {
217
+ return await loadConfig() ?? DEFAULT_CONFIG;
218
+ }
219
+ function validateConfig(obj) {
220
+ if (typeof obj !== "object" || obj === null) throw new Error("Config must be a JSON object");
221
+ const config = obj;
222
+ if (typeof config.template !== "string" || config.template.trim() === "") throw new Error("Config must have a non-empty \"template\" string field");
223
+ const defaults = {};
224
+ if (config.defaults !== void 0) {
225
+ if (typeof config.defaults !== "object" || config.defaults === null) throw new Error("\"defaults\" must be an object");
226
+ for (const [key, value] of Object.entries(config.defaults)) {
227
+ if (typeof value !== "string") throw new Error(`Default value for "${key}" must be a string`);
228
+ defaults[key] = value;
229
+ }
230
+ }
231
+ let features;
232
+ if (config.features && typeof config.features === "object") {
233
+ const f = config.features;
234
+ features = {
235
+ cardId: Boolean(f.cardId),
236
+ type: Boolean(f.type),
237
+ environment: Boolean(f.environment)
238
+ };
239
+ } else features = inferFeaturesFromTemplate(config.template);
240
+ let types = [];
241
+ if (Array.isArray(config.types)) types = validateShortcodeArray(config.types, "types");
242
+ else if (features.type) types = [...PREDEFINED_TYPES];
243
+ let environments = [];
244
+ if (Array.isArray(config.environments)) environments = validateShortcodeArray(config.environments, "environments");
245
+ else if (features.environment) environments = [...PREDEFINED_ENVIRONMENTS];
246
+ return {
247
+ template: config.template,
248
+ features,
249
+ types,
250
+ environments,
251
+ defaults
252
+ };
253
+ }
254
+ function inferFeaturesFromTemplate(template) {
255
+ const parsed = parseTemplate(template);
256
+ return {
257
+ cardId: parsed.variables.includes("card_id"),
258
+ type: parsed.variables.includes("type"),
259
+ environment: parsed.variables.includes("environment")
260
+ };
261
+ }
262
+ function validateShortcodeArray(arr, fieldName) {
263
+ const result = [];
264
+ for (let i = 0; i < arr.length; i++) {
265
+ const item = arr[i];
266
+ if (typeof item !== "object" || item === null || typeof item.key !== "string" || typeof item.label !== "string") throw new Error(`${fieldName}[${i}] must be an object with "key" and "label" string fields`);
267
+ const entry = item;
268
+ if (entry.key.length !== 1) throw new Error(`${fieldName}[${i}].key must be a single character, got "${entry.key}"`);
269
+ result.push({
270
+ key: entry.key,
271
+ label: entry.label
272
+ });
273
+ }
274
+ return result;
275
+ }
276
+ async function writeConfig(config) {
277
+ const configDir = join(homedir(), ".config", "preguito");
278
+ const configPath = join(configDir, "config.json");
279
+ if (!existsSync(configDir)) await mkdir(configDir, { recursive: true });
280
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
281
+ return configPath;
282
+ }
283
+
284
+ //#endregion
285
+ //#region src/commands/commit-parser.ts
286
+ function parsePositionalArgs(args, config) {
287
+ if (args.length === 0) throw new PrequitoError("No arguments provided. Usage: guito c [card_id] [shortcodes] <message...>");
288
+ const context = {};
289
+ let cursor = 0;
290
+ if (config.features.cardId) {
291
+ if (cursor >= args.length) throw new PrequitoError("Missing card ID. It must be the first argument.");
292
+ context.card_id = args[cursor];
293
+ cursor++;
294
+ }
295
+ if (config.features.type || config.features.environment) {
296
+ if (cursor >= args.length) throw new PrequitoError("Missing shortcodes argument. Provide type/environment shortcodes.");
297
+ const resolved = resolveShortcodes(args[cursor], config);
298
+ Object.assign(context, resolved);
299
+ cursor++;
300
+ }
301
+ const messageParts = args.slice(cursor);
302
+ if (messageParts.length === 0) throw new PrequitoError("Missing commit message.");
303
+ const message = messageParts.join(" ");
304
+ return {
305
+ context: mergeContext(config.defaults, context),
306
+ message
307
+ };
308
+ }
309
+ function resolveShortcodes(shortcodesStr, config) {
310
+ const result = {};
311
+ const typeMap = /* @__PURE__ */ new Map();
312
+ for (const t of config.types) typeMap.set(t.key, t.label);
313
+ const envMap = /* @__PURE__ */ new Map();
314
+ for (const e of config.environments) envMap.set(e.key, e.label);
315
+ let foundType = false;
316
+ let foundEnv = false;
317
+ for (const char of shortcodesStr) {
318
+ const isType = config.features.type && typeMap.has(char);
319
+ const isEnv = config.features.environment && envMap.has(char);
320
+ if (isType && isEnv) throw new PrequitoError(`Ambiguous shortcode "${char}" matches both type "${typeMap.get(char)}" and environment "${envMap.get(char)}". Fix your config to avoid conflicts.`);
321
+ if (isType) {
322
+ if (foundType) throw new PrequitoError(`Multiple type shortcodes found in "${shortcodesStr}". Only one type allowed.`);
323
+ result.type = typeMap.get(char);
324
+ foundType = true;
325
+ } else if (isEnv) {
326
+ if (foundEnv) throw new PrequitoError(`Multiple environment shortcodes found in "${shortcodesStr}". Only one environment allowed.`);
327
+ result.environment = envMap.get(char);
328
+ foundEnv = true;
329
+ } else throw new PrequitoError(`Unknown shortcode "${char}" in "${shortcodesStr}". Valid: ${[...config.types.map((t) => `${t.key}=${t.label}`), ...config.environments.map((e) => `${e.key}=${e.label}`)].join(", ")}`);
330
+ }
331
+ if (config.features.type && !foundType) throw new PrequitoError(`No type shortcode found in "${shortcodesStr}". Valid types: ${config.types.map((t) => `${t.key}=${t.label}`).join(", ")}`);
332
+ if (config.features.environment && !foundEnv) throw new PrequitoError(`No environment shortcode found in "${shortcodesStr}". Valid environments: ${config.environments.map((e) => `${e.key}=${e.label}`).join(", ")}`);
333
+ return result;
334
+ }
335
+
336
+ //#endregion
337
+ //#region src/git/operations.ts
338
+ const execFileAsync = promisify(execFile);
339
+ async function git(args, env) {
340
+ try {
341
+ const result = await execFileAsync("git", args, {
342
+ maxBuffer: 10 * 1024 * 1024,
343
+ env: env ? {
344
+ ...process.env,
345
+ ...env
346
+ } : void 0
347
+ });
348
+ return {
349
+ stdout: result.stdout,
350
+ stderr: result.stderr
351
+ };
352
+ } catch (error) {
353
+ const err = error;
354
+ throw new GitOperationError(args[0], err.code ?? 1, err.stderr ?? err.message ?? "Unknown git error");
355
+ }
356
+ }
357
+ async function isGitRepo() {
358
+ try {
359
+ await git(["rev-parse", "--is-inside-work-tree"]);
360
+ return true;
361
+ } catch {
362
+ return false;
363
+ }
364
+ }
365
+ async function getCurrentBranch() {
366
+ return (await git(["branch", "--show-current"])).stdout.trim();
367
+ }
368
+ async function hasStagedChanges() {
369
+ try {
370
+ await git([
371
+ "diff",
372
+ "--cached",
373
+ "--quiet"
374
+ ]);
375
+ return false;
376
+ } catch {
377
+ return true;
378
+ }
379
+ }
380
+ async function stageAll() {
381
+ await git(["add", "-A"]);
382
+ }
383
+ async function commit(message) {
384
+ return (await git([
385
+ "commit",
386
+ "-m",
387
+ message
388
+ ])).stdout;
389
+ }
390
+ async function commitAmend() {
391
+ return (await git([
392
+ "commit",
393
+ "--amend",
394
+ "--no-edit"
395
+ ])).stdout;
396
+ }
397
+ async function push() {
398
+ const result = await git(["push"]);
399
+ return result.stdout + result.stderr;
400
+ }
401
+ async function forcePush() {
402
+ const result = await git(["push", "--force"]);
403
+ return result.stdout + result.stderr;
404
+ }
405
+ async function forcePushLease() {
406
+ const result = await git(["push", "--force-with-lease"]);
407
+ return result.stdout + result.stderr;
408
+ }
409
+ async function checkout(branch) {
410
+ await git(["checkout", branch]);
411
+ }
412
+ async function pull() {
413
+ const result = await git(["pull"]);
414
+ return result.stdout + result.stderr;
415
+ }
416
+ async function rebase(branch) {
417
+ const result = await git(["rebase", branch]);
418
+ return result.stdout + result.stderr;
419
+ }
420
+ async function rebaseInteractiveEdit(hash) {
421
+ const sedCmd = `sed -i 's/^pick ${hash.slice(0, 7)}/edit ${hash.slice(0, 7)}/'`;
422
+ const result = await git([
423
+ "rebase",
424
+ "-i",
425
+ `${hash}^`
426
+ ], { GIT_SEQUENCE_EDITOR: sedCmd });
427
+ return result.stdout + result.stderr;
428
+ }
429
+ async function status() {
430
+ return (await git(["status", "--short"])).stdout;
431
+ }
432
+ async function gitInteractive(args) {
433
+ return new Promise((resolve, reject) => {
434
+ spawn("git", args, { stdio: "inherit" }).on("close", (code) => {
435
+ if (code === 0) resolve();
436
+ else reject(new GitOperationError(args[0], code ?? 1, "Interactive git command failed"));
437
+ });
438
+ });
439
+ }
440
+ async function rebaseInteractive(count) {
441
+ await gitInteractive([
442
+ "rebase",
443
+ "-i",
444
+ `HEAD~${count}`
445
+ ]);
446
+ }
447
+ async function pushUpstream$1(branch) {
448
+ const result = await git([
449
+ "push",
450
+ "--set-upstream",
451
+ "origin",
452
+ branch ?? await getCurrentBranch()
453
+ ]);
454
+ return result.stdout + result.stderr;
455
+ }
456
+ async function commitFixup(hash) {
457
+ return (await git([
458
+ "commit",
459
+ "--fixup",
460
+ hash
461
+ ])).stdout;
462
+ }
463
+ async function resetSoft(count = 1) {
464
+ return (await git([
465
+ "reset",
466
+ "--soft",
467
+ `HEAD~${count}`
468
+ ])).stdout;
469
+ }
470
+ async function createBranch(branch) {
471
+ await git([
472
+ "checkout",
473
+ "-b",
474
+ branch
475
+ ]);
476
+ }
477
+ async function stash() {
478
+ return (await git(["stash"])).stdout;
479
+ }
480
+ async function stashPop() {
481
+ return (await git(["stash", "pop"])).stdout;
482
+ }
483
+ async function logOneline(count = 10) {
484
+ return (await git([
485
+ "log",
486
+ "--oneline",
487
+ "-n",
488
+ String(count)
489
+ ])).stdout;
490
+ }
491
+ async function logGrep(keyword, count) {
492
+ const args = [
493
+ "log",
494
+ "--oneline",
495
+ "--all",
496
+ `--grep=${keyword}`
497
+ ];
498
+ if (count) args.push("-n", String(count));
499
+ return (await git(args)).stdout;
500
+ }
501
+ async function logTag(tag) {
502
+ return (await git([
503
+ "log",
504
+ "--oneline",
505
+ `${tag}..HEAD`
506
+ ])).stdout;
507
+ }
508
+ async function logTagAll(tag) {
509
+ return (await git([
510
+ "log",
511
+ "--oneline",
512
+ tag
513
+ ])).stdout;
514
+ }
515
+
516
+ //#endregion
517
+ //#region src/commands/commit.ts
518
+ function registerCommitCommand(program) {
519
+ program.command("c").alias("commit").description("Create a commit using your template").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) => {
520
+ try {
521
+ await executeCommit(args, opts);
522
+ } catch (error) {
523
+ if (error instanceof PrequitoError) {
524
+ console.error(`\u2716 ${error.message}`);
525
+ process.exit(1);
526
+ }
527
+ throw error;
528
+ }
529
+ });
530
+ }
531
+ async function executeCommit(args, opts) {
532
+ if (!await isGitRepo()) {
533
+ console.error("✖ Not inside a git repository.");
534
+ process.exit(1);
535
+ }
536
+ const config = await loadConfigOrDefault();
537
+ const { context, message } = parsePositionalArgs(args, config);
538
+ const commitMessage = renderTemplate(config.template, context, message);
539
+ if (opts.dryRun) {
540
+ console.log(commitMessage);
541
+ return;
542
+ }
543
+ if (opts.stage !== false) await stageAll();
544
+ if (!await hasStagedChanges()) {
545
+ console.error("✖ No staged changes to commit.");
546
+ process.exit(1);
547
+ }
548
+ console.log(`\u2192 Committing: ${commitMessage}`);
549
+ await commit(commitMessage);
550
+ console.log("✔ Committed.");
551
+ if (opts.force) {
552
+ console.log("→ Pushing (--force-with-lease)...");
553
+ await forcePushLease();
554
+ console.log("✔ Pushed.");
555
+ } else if (opts.push) {
556
+ console.log("→ Pushing...");
557
+ await push();
558
+ console.log("✔ Pushed.");
559
+ }
560
+ }
561
+
562
+ //#endregion
563
+ //#region src/commands/amend-push.ts
564
+ function registerAmendPushCommands(program) {
565
+ program.command("ap").description("Amend last commit + git push --force").action(async () => {
566
+ try {
567
+ await amendAndPush(false);
568
+ } catch (error) {
569
+ if (error instanceof PrequitoError) {
570
+ console.error(`✖ ${error.message}`);
571
+ process.exit(1);
572
+ }
573
+ throw error;
574
+ }
575
+ });
576
+ program.command("apl").description("Amend last commit + git push --force-with-lease").action(async () => {
577
+ try {
578
+ await amendAndPush(true);
579
+ } catch (error) {
580
+ if (error instanceof PrequitoError) {
581
+ console.error(`✖ ${error.message}`);
582
+ process.exit(1);
583
+ }
584
+ throw error;
585
+ }
586
+ });
587
+ }
588
+ async function amendAndPush(useLease) {
589
+ if (!await isGitRepo()) {
590
+ console.error("✖ Not inside a git repository.");
591
+ process.exit(1);
592
+ }
593
+ console.log("→ Staging all changes...");
594
+ await stageAll();
595
+ console.log("→ Amending last commit...");
596
+ await commitAmend();
597
+ console.log("✔ Amended.");
598
+ if (useLease) {
599
+ console.log("→ Pushing (--force-with-lease)...");
600
+ await forcePushLease();
601
+ } else {
602
+ console.log("→ Pushing (--force)...");
603
+ await forcePush();
604
+ }
605
+ console.log("✔ Pushed.");
606
+ }
607
+
608
+ //#endregion
609
+ //#region src/commands/rebase.ts
610
+ function registerRebaseCommands(program) {
611
+ program.command("r <branch>").alias("rebase").description("Quick rebase: checkout <branch>, pull, come back, rebase on top").action(async (branch) => {
612
+ try {
613
+ await quickRebase(branch);
614
+ } catch (error) {
615
+ if (error instanceof PrequitoError) {
616
+ console.error(`✖ ${error.message}`);
617
+ process.exit(1);
618
+ }
619
+ throw error;
620
+ }
621
+ });
622
+ program.command("ri <count>").description("Interactive rebase: open editor for the last <count> commits").action(async (count) => {
623
+ try {
624
+ await interactiveRebase(count);
625
+ } catch (error) {
626
+ if (error instanceof PrequitoError) {
627
+ console.error(`✖ ${error.message}`);
628
+ process.exit(1);
629
+ }
630
+ throw error;
631
+ }
632
+ });
633
+ program.command("re <hash>").description("Edit rebase: start interactive rebase with commit marked as edit").action(async (hash) => {
634
+ try {
635
+ await editRebase(hash);
636
+ } catch (error) {
637
+ if (error instanceof PrequitoError) {
638
+ console.error(`✖ ${error.message}`);
639
+ process.exit(1);
640
+ }
641
+ throw error;
642
+ }
643
+ });
644
+ }
645
+ async function quickRebase(branch) {
646
+ if (!await isGitRepo()) {
647
+ console.error("✖ Not inside a git repository.");
648
+ process.exit(1);
649
+ }
650
+ const currentBranch = await getCurrentBranch();
651
+ console.log(`→ Current branch: ${currentBranch}`);
652
+ console.log(`→ Checking out ${branch}...`);
653
+ await checkout(branch);
654
+ console.log(`→ Pulling ${branch}...`);
655
+ await pull();
656
+ console.log(`→ Checking out ${currentBranch}...`);
657
+ await checkout(currentBranch);
658
+ console.log(`→ Rebasing ${currentBranch} onto ${branch}...`);
659
+ await rebase(branch);
660
+ console.log("✔ Rebase complete.");
661
+ }
662
+ async function interactiveRebase(count) {
663
+ if (!await isGitRepo()) {
664
+ console.error("✖ Not inside a git repository.");
665
+ process.exit(1);
666
+ }
667
+ const num = parseInt(count, 10);
668
+ if (isNaN(num) || num <= 0) {
669
+ console.error("✖ Count must be a positive integer.");
670
+ process.exit(1);
671
+ }
672
+ console.log(`→ Starting interactive rebase for the last ${num} commit(s)...`);
673
+ await rebaseInteractive(num);
674
+ }
675
+ async function editRebase(hash) {
676
+ if (!await isGitRepo()) {
677
+ console.error("✖ Not inside a git repository.");
678
+ process.exit(1);
679
+ }
680
+ console.log(`→ Starting edit rebase on commit ${hash}...`);
681
+ await rebaseInteractiveEdit(hash);
682
+ console.log("✔ Rebase paused at the target commit. Make your changes, then run:");
683
+ console.log(" git add . && git rebase --continue");
684
+ }
685
+
686
+ //#endregion
687
+ //#region src/commands/usage-examples.ts
688
+ function printUsageExamples(config) {
689
+ console.log("\n 📖 Examples:\n");
690
+ const { features, types, environments, defaults } = config;
691
+ const exType = types[0];
692
+ const exEnv = environments[0];
693
+ const exCardId = "1234";
694
+ const exMessage = "add login endpoint";
695
+ if (features.cardId && features.type && features.environment && exType && exEnv) {
696
+ const shortcodes = `${exType.key}${exEnv.key}`;
697
+ const context = mergeContext(defaults, {
698
+ card_id: exCardId,
699
+ type: exType.label,
700
+ environment: exEnv.label
701
+ });
702
+ const rendered = renderTemplate(config.template, context, exMessage);
703
+ console.log(` $ guito c ${exCardId} ${shortcodes} "${exMessage}"`);
704
+ console.log(` → ${rendered}`);
705
+ if (types.length > 1 && environments.length > 1) {
706
+ const t2 = types[1];
707
+ const e2 = environments[1];
708
+ const ctx2 = mergeContext(defaults, {
709
+ card_id: "5678",
710
+ type: t2.label,
711
+ environment: e2.label
712
+ });
713
+ const r2 = renderTemplate(config.template, ctx2, "fix timeout bug");
714
+ console.log(`\n $ guito c 5678 ${t2.key}${e2.key} "fix timeout bug"`);
715
+ console.log(` → ${r2}`);
716
+ }
717
+ } else if (features.cardId && features.type && exType) {
718
+ const context = mergeContext(defaults, {
719
+ card_id: exCardId,
720
+ type: exType.label
721
+ });
722
+ const rendered = renderTemplate(config.template, context, exMessage);
723
+ console.log(` $ guito c ${exCardId} ${exType.key} "${exMessage}"`);
724
+ console.log(` → ${rendered}`);
725
+ } else if (features.cardId && features.environment && exEnv) {
726
+ const context = mergeContext(defaults, {
727
+ card_id: exCardId,
728
+ environment: exEnv.label
729
+ });
730
+ const rendered = renderTemplate(config.template, context, exMessage);
731
+ console.log(` $ guito c ${exCardId} ${exEnv.key} "${exMessage}"`);
732
+ console.log(` → ${rendered}`);
733
+ } else if (features.type && features.environment && exType && exEnv) {
734
+ const context = {
735
+ type: exType.label,
736
+ environment: exEnv.label
737
+ };
738
+ const rendered = renderTemplate(config.template, context, exMessage);
739
+ console.log(` $ guito c ${exType.key}${exEnv.key} "${exMessage}"`);
740
+ console.log(` → ${rendered}`);
741
+ } else if (features.type && exType) {
742
+ const context = { type: exType.label };
743
+ const rendered = renderTemplate(config.template, context, exMessage);
744
+ console.log(` $ guito c ${exType.key} "${exMessage}"`);
745
+ console.log(` → ${rendered}`);
746
+ } else if (features.cardId) {
747
+ const context = mergeContext(defaults, { card_id: exCardId });
748
+ const rendered = renderTemplate(config.template, context, exMessage);
749
+ console.log(` $ guito c ${exCardId} "${exMessage}"`);
750
+ console.log(` → ${rendered}`);
751
+ } else {
752
+ console.log(` $ guito c "${exMessage}"`);
753
+ console.log(` → ${exMessage}`);
754
+ }
755
+ console.log("\n Flags: -p (push) -f (force push) -d (dry-run) -S (skip staging)");
756
+ }
757
+
758
+ //#endregion
759
+ //#region src/commands/init.ts
760
+ function registerInitCommand(program) {
761
+ program.command("i").alias("init").description("Interactive setup to create your preguito config").option("--default", "Use default config without prompts").action(async (opts) => {
762
+ if (opts.default) {
763
+ const path = await writeConfig(DEFAULT_CONFIG);
764
+ console.log(`✅ Config written to ${path}`);
765
+ return;
766
+ }
767
+ await interactiveInit();
768
+ });
769
+ }
770
+ async function interactiveInit() {
771
+ const rl = createInterface({
772
+ input: stdin,
773
+ output: stdout
774
+ });
775
+ try {
776
+ console.log("\n✨ Welcome to preguito setup!\n");
777
+ console.log("📋 Choose which features to enable:\n");
778
+ const features = {
779
+ cardId: await askYesNo(rl, " 🎫 Include card/ticket ID in commits?"),
780
+ type: await askYesNo(rl, " 🏷️ Include commit type (feat, fix, chore...)?"),
781
+ environment: await askYesNo(rl, " 🌍 Include environment (prd, uat, dev...)?")
782
+ };
783
+ const defaults = {};
784
+ if (features.cardId) {
785
+ const prefix = await rl.question("\n🔤 Project prefix/sigla (e.g. PROJ, leave empty to skip): ");
786
+ if (prefix.trim()) defaults.prefix = prefix.trim().toUpperCase();
787
+ }
788
+ const hasPrefix = "prefix" in defaults;
789
+ let types = [];
790
+ if (features.type) {
791
+ console.log("\n🏷️ Available commit types:\n");
792
+ for (const t of PREDEFINED_TYPES) console.log(` ${t.key} → ${t.label}`);
793
+ types = selectEntries(PREDEFINED_TYPES, await rl.question("\n Which types? (comma-separated labels, or 'all'): "));
794
+ if (types.length === 0) {
795
+ console.log(" ℹ️ No valid types selected, using all.");
796
+ types = [...PREDEFINED_TYPES];
797
+ }
798
+ console.log("\n✏️ Customize shortcode letters (Enter to keep default):\n");
799
+ types = await customizeKeys(rl, types);
800
+ }
801
+ let environments = [];
802
+ if (features.environment) {
803
+ console.log("\n🌍 Available environments:\n");
804
+ for (const e of PREDEFINED_ENVIRONMENTS) console.log(` ${e.key} → ${e.label}`);
805
+ environments = selectEntries(PREDEFINED_ENVIRONMENTS, await rl.question("\n Which environments? (comma-separated labels, or 'all'): "));
806
+ if (environments.length === 0) {
807
+ console.log(" ℹ️ No valid environments selected, using all.");
808
+ environments = [...PREDEFINED_ENVIRONMENTS];
809
+ }
810
+ console.log("\n✏️ Customize shortcode letters (Enter to keep default):\n");
811
+ environments = await customizeKeys(rl, environments);
812
+ }
813
+ if (features.type && features.environment) {
814
+ let conflicts = findKeyConflicts(types, environments);
815
+ while (conflicts.length > 0) {
816
+ console.log("\n⚠️ Letter conflicts detected:\n");
817
+ for (const c of conflicts) console.log(` "${c.key}" is used for both type "${c.typeLabel}" and environment "${c.envLabel}"`);
818
+ console.log("\n Reassign letters for the conflicting types:\n");
819
+ for (const c of conflicts) {
820
+ const newKey = (await rl.question(` ${c.typeLabel} [${c.key}]: `)).trim().toLowerCase();
821
+ if (newKey && newKey.length === 1 && /^[a-z]$/.test(newKey)) {
822
+ const entry = types.find((t) => t.key === c.key && t.label === c.typeLabel);
823
+ if (entry) entry.key = newKey;
824
+ }
825
+ }
826
+ conflicts = findKeyConflicts(types, environments);
827
+ }
828
+ }
829
+ const template = generateTemplate(features, hasPrefix);
830
+ const config = {
831
+ template,
832
+ features,
833
+ types,
834
+ environments,
835
+ defaults
836
+ };
837
+ const path = await writeConfig(config);
838
+ console.log("\n─────────────────────────────────────");
839
+ console.log("✅ Setup complete!\n");
840
+ console.log(` 📄 Config saved to ${path}`);
841
+ console.log(` 📝 Template: ${template}`);
842
+ if (types.length > 0 || environments.length > 0) {
843
+ console.log("\n 🔑 Your shortcodes:");
844
+ if (types.length > 0) console.log(` Types: ${types.map((t) => `${t.key}=${t.label}`).join(", ")}`);
845
+ if (environments.length > 0) console.log(` Envs: ${environments.map((e) => `${e.key}=${e.label}`).join(", ")}`);
846
+ }
847
+ printUsageExamples(config);
848
+ console.log("\n 💡 Run 'guito cfg' to view your config anytime.");
849
+ console.log("─────────────────────────────────────\n");
850
+ } finally {
851
+ rl.close();
852
+ }
853
+ }
854
+ async function askYesNo(rl, question) {
855
+ return (await rl.question(`${question} (y/n): `)).trim().toLowerCase().startsWith("y");
856
+ }
857
+ function selectEntries(predefined, input) {
858
+ const trimmed = input.trim().toLowerCase();
859
+ if (trimmed === "all" || trimmed === "") return [...predefined];
860
+ const requested = trimmed.split(",").map((s) => s.trim());
861
+ return predefined.filter((entry) => requested.includes(entry.label) || requested.includes(entry.key));
862
+ }
863
+ async function customizeKeys(rl, entries) {
864
+ const result = [];
865
+ const usedKeys = /* @__PURE__ */ new Set();
866
+ for (const entry of entries) {
867
+ let key = (await rl.question(` ${entry.label} [${entry.key}]: `)).trim().toLowerCase();
868
+ if (!key) key = entry.key;
869
+ if (key.length !== 1 || !/^[a-z]$/.test(key)) {
870
+ console.log(` ⚠️ Invalid key "${key}", keeping "${entry.key}".`);
871
+ key = entry.key;
872
+ }
873
+ if (usedKeys.has(key)) {
874
+ console.log(` ⚠️ Key "${key}" already used, keeping "${entry.key}".`);
875
+ key = entry.key;
876
+ }
877
+ usedKeys.add(key);
878
+ result.push({
879
+ key,
880
+ label: entry.label
881
+ });
882
+ }
883
+ return result;
884
+ }
885
+ function findKeyConflicts(types, environments) {
886
+ const typeMap = new Map(types.map((t) => [t.key, t.label]));
887
+ const conflicts = [];
888
+ for (const env of environments) if (typeMap.has(env.key)) conflicts.push({
889
+ key: env.key,
890
+ typeLabel: typeMap.get(env.key),
891
+ envLabel: env.label
892
+ });
893
+ return conflicts;
894
+ }
895
+
896
+ //#endregion
897
+ //#region src/commands/config.ts
898
+ function registerConfigCommand(program) {
899
+ program.command("cfg").alias("config").description("Show current preguito configuration").option("--path", "Show only the config file path").option("--template", "Show only the template").action(async (opts) => {
900
+ const config = await loadConfig();
901
+ if (!config) {
902
+ console.log("No config found. Run \"guito i\" to create one.");
903
+ return;
904
+ }
905
+ if (opts.path) {
906
+ const path = await findConfigPath();
907
+ console.log(path);
908
+ return;
909
+ }
910
+ if (opts.template) {
911
+ console.log(config.template);
912
+ return;
913
+ }
914
+ const configPath = await findConfigPath();
915
+ console.log(`Config file: ${configPath}`);
916
+ console.log(`Template: ${config.template}`);
917
+ console.log("Features:");
918
+ console.log(` Card ID: ${config.features.cardId ? "enabled" : "disabled"}`);
919
+ console.log(` Commit type: ${config.features.type ? "enabled" : "disabled"}`);
920
+ console.log(` Environment: ${config.features.environment ? "enabled" : "disabled"}`);
921
+ if (config.defaults.prefix) console.log(`Prefix: ${config.defaults.prefix}`);
922
+ if (config.types.length > 0) {
923
+ console.log("Types:");
924
+ for (const t of config.types) console.log(` ${t.key} = ${t.label}`);
925
+ }
926
+ if (config.environments.length > 0) {
927
+ console.log("Environments:");
928
+ for (const e of config.environments) console.log(` ${e.key} = ${e.label}`);
929
+ }
930
+ printUsageExamples(config);
931
+ });
932
+ }
933
+
934
+ //#endregion
935
+ //#region src/commands/push.ts
936
+ function registerPushCommands(program) {
937
+ program.command("pu").description("Push current branch and set upstream tracking").action(async () => {
938
+ try {
939
+ await pushUpstream();
940
+ } catch (error) {
941
+ if (error instanceof PrequitoError) {
942
+ console.error(`✖ ${error.message}`);
943
+ process.exit(1);
944
+ }
945
+ throw error;
946
+ }
947
+ });
948
+ }
949
+ async function pushUpstream() {
950
+ if (!await isGitRepo()) {
951
+ console.error("✖ Not inside a git repository.");
952
+ process.exit(1);
953
+ }
954
+ const branch = await getCurrentBranch();
955
+ console.log(`→ Pushing with --set-upstream origin ${branch}...`);
956
+ await pushUpstream$1(branch);
957
+ console.log("✔ Pushed.");
958
+ }
959
+
960
+ //#endregion
961
+ //#region src/commands/fixup.ts
962
+ function registerFixupCommand(program) {
963
+ program.command("cf <hash>").description("Create a fixup commit targeting a specific commit").option("-p, --push", "Push after creating the fixup commit").option("-f, --force", "Push with --force-with-lease after creating").action(async (hash, opts) => {
964
+ try {
965
+ await executeFixup(hash, opts);
966
+ } catch (error) {
967
+ if (error instanceof PrequitoError) {
968
+ console.error(`✖ ${error.message}`);
969
+ process.exit(1);
970
+ }
971
+ throw error;
972
+ }
973
+ });
974
+ }
975
+ async function executeFixup(hash, opts) {
976
+ if (!await isGitRepo()) {
977
+ console.error("✖ Not inside a git repository.");
978
+ process.exit(1);
979
+ }
980
+ console.log("→ Staging all changes...");
981
+ await stageAll();
982
+ if (!await hasStagedChanges()) {
983
+ console.error("✖ No staged changes to commit.");
984
+ process.exit(1);
985
+ }
986
+ console.log(`→ Creating fixup commit for ${hash}...`);
987
+ await commitFixup(hash);
988
+ console.log("✔ Fixup commit created.");
989
+ if (opts.force) {
990
+ console.log("→ Pushing (--force-with-lease)...");
991
+ await forcePushLease();
992
+ console.log("✔ Pushed.");
993
+ } else if (opts.push) {
994
+ console.log("→ Pushing...");
995
+ await push();
996
+ console.log("✔ Pushed.");
997
+ }
998
+ }
999
+
1000
+ //#endregion
1001
+ //#region src/commands/undo.ts
1002
+ function registerUndoCommand(program) {
1003
+ program.command("u [count]").alias("undo").description("Undo the last N commits (soft reset, changes stay staged)").action(async (count) => {
1004
+ try {
1005
+ await executeUndo(count);
1006
+ } catch (error) {
1007
+ if (error instanceof PrequitoError) {
1008
+ console.error(`✖ ${error.message}`);
1009
+ process.exit(1);
1010
+ }
1011
+ throw error;
1012
+ }
1013
+ });
1014
+ }
1015
+ async function executeUndo(count) {
1016
+ if (!await isGitRepo()) {
1017
+ console.error("✖ Not inside a git repository.");
1018
+ process.exit(1);
1019
+ }
1020
+ const num = count ? parseInt(count, 10) : 1;
1021
+ if (isNaN(num) || num <= 0) {
1022
+ console.error("✖ Count must be a positive integer.");
1023
+ process.exit(1);
1024
+ }
1025
+ console.log(`→ Undoing last ${num} commit(s)...`);
1026
+ await resetSoft(num);
1027
+ console.log(`✔ Undid last ${num} commit(s). Changes are staged.`);
1028
+ const st = await status();
1029
+ if (st) console.log(st);
1030
+ }
1031
+
1032
+ //#endregion
1033
+ //#region src/commands/status.ts
1034
+ function registerStatusCommand(program) {
1035
+ program.command("s").alias("status").description("Show short git status").action(async () => {
1036
+ try {
1037
+ await executeStatus();
1038
+ } catch (error) {
1039
+ if (error instanceof PrequitoError) {
1040
+ console.error(`✖ ${error.message}`);
1041
+ process.exit(1);
1042
+ }
1043
+ throw error;
1044
+ }
1045
+ });
1046
+ }
1047
+ async function executeStatus() {
1048
+ if (!await isGitRepo()) {
1049
+ console.error("✖ Not inside a git repository.");
1050
+ process.exit(1);
1051
+ }
1052
+ const output = await status();
1053
+ if (output) console.log(output.trimEnd());
1054
+ else console.log("✨ Nothing to commit, working tree clean.");
1055
+ }
1056
+
1057
+ //#endregion
1058
+ //#region src/commands/switch.ts
1059
+ function registerSwitchCommand(program) {
1060
+ program.command("sw <branch>").alias("switch").description("Switch branches (use -n to create a new branch)").option("-n, --new", "Create a new branch").action(async (branch, opts) => {
1061
+ try {
1062
+ await executeSwitch(branch, opts);
1063
+ } catch (error) {
1064
+ if (error instanceof PrequitoError) {
1065
+ console.error(`✖ ${error.message}`);
1066
+ process.exit(1);
1067
+ }
1068
+ throw error;
1069
+ }
1070
+ });
1071
+ }
1072
+ async function executeSwitch(branch, opts) {
1073
+ if (!await isGitRepo()) {
1074
+ console.error("✖ Not inside a git repository.");
1075
+ process.exit(1);
1076
+ }
1077
+ if (opts.new) {
1078
+ console.log(`→ Creating and switching to ${branch}...`);
1079
+ await createBranch(branch);
1080
+ } else {
1081
+ console.log(`→ Switching to ${branch}...`);
1082
+ await checkout(branch);
1083
+ }
1084
+ console.log(`✔ On branch ${branch}.`);
1085
+ }
1086
+
1087
+ //#endregion
1088
+ //#region src/commands/stash.ts
1089
+ function registerStashCommands(program) {
1090
+ program.command("st").description("Stash all changes").action(async () => {
1091
+ try {
1092
+ await executeStash();
1093
+ } catch (error) {
1094
+ if (error instanceof PrequitoError) {
1095
+ console.error(`✖ ${error.message}`);
1096
+ process.exit(1);
1097
+ }
1098
+ throw error;
1099
+ }
1100
+ });
1101
+ program.command("stp").description("Pop the latest stash").action(async () => {
1102
+ try {
1103
+ await executeStashPop();
1104
+ } catch (error) {
1105
+ if (error instanceof PrequitoError) {
1106
+ console.error(`✖ ${error.message}`);
1107
+ process.exit(1);
1108
+ }
1109
+ throw error;
1110
+ }
1111
+ });
1112
+ }
1113
+ async function executeStash() {
1114
+ if (!await isGitRepo()) {
1115
+ console.error("✖ Not inside a git repository.");
1116
+ process.exit(1);
1117
+ }
1118
+ const output = await stash();
1119
+ console.log(output.trimEnd());
1120
+ }
1121
+ async function executeStashPop() {
1122
+ if (!await isGitRepo()) {
1123
+ console.error("✖ Not inside a git repository.");
1124
+ process.exit(1);
1125
+ }
1126
+ const output = await stashPop();
1127
+ console.log(output.trimEnd());
1128
+ }
1129
+
1130
+ //#endregion
1131
+ //#region src/commands/log.ts
1132
+ function registerLogCommand(program) {
1133
+ program.command("l [count]").alias("log").description("Show compact git log (default: last 10 commits)").action(async (count) => {
1134
+ try {
1135
+ await executeLog(count);
1136
+ } catch (error) {
1137
+ if (error instanceof PrequitoError) {
1138
+ console.error(`✖ ${error.message}`);
1139
+ process.exit(1);
1140
+ }
1141
+ throw error;
1142
+ }
1143
+ });
1144
+ }
1145
+ async function executeLog(count) {
1146
+ if (!await isGitRepo()) {
1147
+ console.error("✖ Not inside a git repository.");
1148
+ process.exit(1);
1149
+ }
1150
+ const num = count ? parseInt(count, 10) : 10;
1151
+ if (isNaN(num) || num <= 0) {
1152
+ console.error("✖ Count must be a positive integer.");
1153
+ process.exit(1);
1154
+ }
1155
+ const output = await logOneline(num);
1156
+ if (output) console.log(output.trimEnd());
1157
+ else console.log("✨ No commits found.");
1158
+ }
1159
+
1160
+ //#endregion
1161
+ //#region src/commands/find.ts
1162
+ function registerFindCommands(program) {
1163
+ program.command("f <keyword>").alias("find").description("Search commits by message keyword").option("-n, --number <count>", "Limit number of results").action(async (keyword, opts) => {
1164
+ try {
1165
+ await executeFind(keyword, opts);
1166
+ } catch (error) {
1167
+ if (error instanceof PrequitoError) {
1168
+ console.error(`✖ ${error.message}`);
1169
+ process.exit(1);
1170
+ }
1171
+ throw error;
1172
+ }
1173
+ });
1174
+ program.command("t <tag>").alias("tag").description("Show commits for a tag (default: tag..HEAD, --all: all commits reachable from tag)").option("-a, --all", "Show all commits reachable from the tag").action(async (tag, opts) => {
1175
+ try {
1176
+ await executeTag(tag, opts);
1177
+ } catch (error) {
1178
+ if (error instanceof PrequitoError) {
1179
+ console.error(`✖ ${error.message}`);
1180
+ process.exit(1);
1181
+ }
1182
+ throw error;
1183
+ }
1184
+ });
1185
+ }
1186
+ async function executeFind(keyword, opts) {
1187
+ if (!await isGitRepo()) {
1188
+ console.error("✖ Not inside a git repository.");
1189
+ process.exit(1);
1190
+ }
1191
+ const count = opts.number ? parseInt(opts.number, 10) : void 0;
1192
+ const output = await logGrep(keyword, count);
1193
+ if (output) console.log(output.trimEnd());
1194
+ else console.log(`✨ No commits found matching "${keyword}".`);
1195
+ }
1196
+ async function executeTag(tag, opts) {
1197
+ if (!await isGitRepo()) {
1198
+ console.error("✖ Not inside a git repository.");
1199
+ process.exit(1);
1200
+ }
1201
+ if (opts.all) {
1202
+ console.log(`Commits reachable from ${tag}:`);
1203
+ const output = await logTagAll(tag);
1204
+ if (output) console.log(output.trimEnd());
1205
+ else console.log("✨ No commits found.");
1206
+ } else {
1207
+ console.log(`Commits since ${tag}:`);
1208
+ const output = await logTag(tag);
1209
+ if (output) console.log(output.trimEnd());
1210
+ else console.log("✨ No commits since this tag.");
1211
+ }
1212
+ }
1213
+
1214
+ //#endregion
1215
+ //#region src/cli.ts
1216
+ const program = new Command();
1217
+ program.name("guito").description("preguito - a lazy git CLI with commit templates and shortcuts").version("0.1.0");
1218
+ registerCommitCommand(program);
1219
+ registerAmendPushCommands(program);
1220
+ registerRebaseCommands(program);
1221
+ registerInitCommand(program);
1222
+ registerConfigCommand(program);
1223
+ registerPushCommands(program);
1224
+ registerFixupCommand(program);
1225
+ registerUndoCommand(program);
1226
+ registerStatusCommand(program);
1227
+ registerSwitchCommand(program);
1228
+ registerStashCommands(program);
1229
+ registerLogCommand(program);
1230
+ registerFindCommands(program);
1231
+ program.parse();
1232
+
1233
+ //#endregion
1234
+ export { };