gut-cli 0.1.8 → 0.1.10

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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command13 } from "commander";
4
+ import { Command as Command17 } from "commander";
5
5
 
6
6
  // src/commands/cleanup.ts
7
7
  import { Command } from "commander";
@@ -259,13 +259,11 @@ authCommand.command("status").description("Show which providers have API keys co
259
259
  }
260
260
  });
261
261
 
262
- // src/commands/ai-commit.ts
262
+ // src/commands/commit.ts
263
263
  import { Command as Command3 } from "commander";
264
264
  import chalk3 from "chalk";
265
265
  import ora2 from "ora";
266
266
  import { simpleGit as simpleGit2 } from "simple-git";
267
- import { existsSync, readFileSync } from "fs";
268
- import { join } from "path";
269
267
 
270
268
  // src/lib/ai.ts
271
269
  import { generateText, generateObject } from "ai";
@@ -273,6 +271,143 @@ import { createGoogleGenerativeAI } from "@ai-sdk/google";
273
271
  import { createOpenAI } from "@ai-sdk/openai";
274
272
  import { createAnthropic } from "@ai-sdk/anthropic";
275
273
  import { z } from "zod";
274
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
275
+ import { join as join2, dirname } from "path";
276
+ import { fileURLToPath } from "url";
277
+
278
+ // src/lib/config.ts
279
+ import { homedir } from "os";
280
+ import { join } from "path";
281
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
282
+ import { execSync } from "child_process";
283
+ var DEFAULT_CONFIG = {
284
+ lang: "en"
285
+ };
286
+ function getGlobalConfigPath() {
287
+ const configDir = join(homedir(), ".config", "gut");
288
+ return join(configDir, "config.json");
289
+ }
290
+ function getRepoRoot() {
291
+ try {
292
+ return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
293
+ } catch {
294
+ return null;
295
+ }
296
+ }
297
+ function getLocalConfigPath() {
298
+ const repoRoot = getRepoRoot();
299
+ if (!repoRoot) return null;
300
+ return join(repoRoot, ".gut", "config.json");
301
+ }
302
+ function ensureGlobalConfigDir() {
303
+ const configDir = join(homedir(), ".config", "gut");
304
+ if (!existsSync(configDir)) {
305
+ mkdirSync(configDir, { recursive: true });
306
+ }
307
+ }
308
+ function ensureLocalConfigDir() {
309
+ const repoRoot = getRepoRoot();
310
+ if (!repoRoot) return;
311
+ const gutDir = join(repoRoot, ".gut");
312
+ if (!existsSync(gutDir)) {
313
+ mkdirSync(gutDir, { recursive: true });
314
+ }
315
+ }
316
+ function readConfigFile(path2) {
317
+ if (!existsSync(path2)) return {};
318
+ try {
319
+ return JSON.parse(readFileSync(path2, "utf-8"));
320
+ } catch {
321
+ return {};
322
+ }
323
+ }
324
+ function getGlobalConfig() {
325
+ const globalPath = getGlobalConfigPath();
326
+ return { ...DEFAULT_CONFIG, ...readConfigFile(globalPath) };
327
+ }
328
+ function getLocalConfig() {
329
+ const localPath = getLocalConfigPath();
330
+ if (!localPath) return {};
331
+ return readConfigFile(localPath);
332
+ }
333
+ function getConfig() {
334
+ const globalConfig = getGlobalConfig();
335
+ const localConfig = getLocalConfig();
336
+ return { ...globalConfig, ...localConfig };
337
+ }
338
+ function setGlobalConfig(key, value) {
339
+ ensureGlobalConfigDir();
340
+ const config = getGlobalConfig();
341
+ config[key] = value;
342
+ writeFileSync(getGlobalConfigPath(), JSON.stringify(config, null, 2));
343
+ }
344
+ function setLocalConfig(key, value) {
345
+ const localPath = getLocalConfigPath();
346
+ if (!localPath) {
347
+ throw new Error("Not in a git repository");
348
+ }
349
+ ensureLocalConfigDir();
350
+ const config = getLocalConfig();
351
+ config[key] = value;
352
+ writeFileSync(localPath, JSON.stringify(config, null, 2));
353
+ }
354
+ function getLanguage() {
355
+ return getConfig().lang;
356
+ }
357
+ function setLanguage(lang, local = false) {
358
+ if (local) {
359
+ setLocalConfig("lang", lang);
360
+ } else {
361
+ setGlobalConfig("lang", lang);
362
+ }
363
+ }
364
+ function getLanguageInstruction(lang) {
365
+ switch (lang) {
366
+ case "ja":
367
+ return "\n\nIMPORTANT: Respond in Japanese (\u65E5\u672C\u8A9E\u3067\u56DE\u7B54\u3057\u3066\u304F\u3060\u3055\u3044).";
368
+ case "en":
369
+ default:
370
+ return "";
371
+ }
372
+ }
373
+ var VALID_LANGUAGES = ["en", "ja"];
374
+ function isValidLanguage(lang) {
375
+ return VALID_LANGUAGES.includes(lang);
376
+ }
377
+
378
+ // src/lib/ai.ts
379
+ var __filename = fileURLToPath(import.meta.url);
380
+ var __dirname = dirname(__filename);
381
+ var GUT_ROOT = join2(__dirname, "..");
382
+ function loadTemplate(name) {
383
+ const templatePath = join2(GUT_ROOT, ".gut", `${name}.md`);
384
+ if (existsSync2(templatePath)) {
385
+ return readFileSync2(templatePath, "utf-8");
386
+ }
387
+ throw new Error(`Template not found: ${templatePath}`);
388
+ }
389
+ function findTemplate(repoRoot, templateName) {
390
+ const templatePath = join2(repoRoot, ".gut", `${templateName}.md`);
391
+ if (existsSync2(templatePath)) {
392
+ return readFileSync2(templatePath, "utf-8");
393
+ }
394
+ return null;
395
+ }
396
+ function applyTemplate(userTemplate, templateName, variables) {
397
+ const langInstruction = getLanguageInstruction(getLanguage());
398
+ const isUserTemplate = !!userTemplate;
399
+ let result = userTemplate || loadTemplate(templateName);
400
+ result = result.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, content) => {
401
+ return variables[key] ? content : "";
402
+ });
403
+ for (const [key, value] of Object.entries(variables)) {
404
+ result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value || "");
405
+ }
406
+ if (!isUserTemplate) {
407
+ result += langInstruction;
408
+ }
409
+ return result;
410
+ }
276
411
  var DEFAULT_MODELS = {
277
412
  gemini: "gemini-2.0-flash",
278
413
  openai: "gpt-4o-mini",
@@ -301,33 +436,11 @@ async function getModel(options) {
301
436
  }
302
437
  }
303
438
  }
304
- async function generateCommitMessage(diff, options, convention) {
439
+ async function generateCommitMessage(diff, options, template) {
305
440
  const model = await getModel(options);
306
- const conventionInstructions = convention ? `
307
- IMPORTANT: Follow this project's commit message convention:
308
-
309
- --- CONVENTION START ---
310
- ${convention}
311
- --- CONVENTION END ---
312
- ` : `
313
- Rules:
314
- - Use format: <type>(<scope>): <description>
315
- - Types: feat, fix, docs, style, refactor, perf, test, chore, build, ci
316
- - Scope is optional but helpful
317
- - Description should be lowercase, imperative mood, no period at end
318
- - Keep the first line under 72 characters
319
- - If changes are complex, add a blank line and bullet points for details`;
320
- const prompt = `You are an expert at writing git commit messages.
321
-
322
- Analyze the following git diff and generate a concise, meaningful commit message.
323
- ${conventionInstructions}
324
-
325
- Git diff:
326
- \`\`\`
327
- ${diff.slice(0, 8e3)}
328
- \`\`\`
329
-
330
- Respond with ONLY the commit message, nothing else.`;
441
+ const prompt = applyTemplate(template, "commit", {
442
+ diff: diff.slice(0, 8e3)
443
+ });
331
444
  const result = await generateText({
332
445
  model,
333
446
  prompt,
@@ -335,45 +448,14 @@ Respond with ONLY the commit message, nothing else.`;
335
448
  });
336
449
  return result.text.trim();
337
450
  }
338
- async function generatePRDescription(context, options) {
451
+ async function generatePRDescription(context, options, template) {
339
452
  const model = await getModel(options);
340
- const templateInstructions = context.template ? `
341
- IMPORTANT: The repository has a PR template. You MUST fill in this template structure:
342
-
343
- --- PR TEMPLATE START ---
344
- ${context.template}
345
- --- PR TEMPLATE END ---
346
-
347
- Fill in each section of the template based on the changes. Keep the template structure intact.
348
- Replace placeholder text and fill in the sections appropriately.` : `
349
- Rules for description:
350
- - Description should have:
351
- - ## Summary section with 2-3 bullet points
352
- - ## Changes section listing key modifications
353
- - ## Test Plan section (suggest what to test)`;
354
- const prompt = `You are an expert at writing pull request descriptions.
355
-
356
- Generate a clear and informative PR title and description based on the following information.
357
-
358
- Branch: ${context.currentBranch} -> ${context.baseBranch}
359
-
360
- Commits:
361
- ${context.commits.map((c) => `- ${c}`).join("\n")}
362
-
363
- Diff summary (truncated):
364
- \`\`\`
365
- ${context.diff.slice(0, 6e3)}
366
- \`\`\`
367
- ${templateInstructions}
368
-
369
- Rules for title:
370
- - Title should be concise (under 72 chars), start with a verb
371
-
372
- Respond in JSON format:
373
- {
374
- "title": "...",
375
- "body": "..."
376
- }`;
453
+ const prompt = applyTemplate(template, "pr", {
454
+ baseBranch: context.baseBranch,
455
+ currentBranch: context.currentBranch,
456
+ commits: context.commits.map((c) => `- ${c}`).join("\n"),
457
+ diff: context.diff.slice(0, 6e3)
458
+ });
377
459
  const result = await generateText({
378
460
  model,
379
461
  prompt,
@@ -402,26 +484,15 @@ var CodeReviewSchema = z.object({
402
484
  ),
403
485
  positives: z.array(z.string()).describe("Good practices observed")
404
486
  });
405
- async function generateCodeReview(diff, options) {
487
+ async function generateCodeReview(diff, options, template) {
406
488
  const model = await getModel(options);
489
+ const prompt = applyTemplate(template, "review", {
490
+ diff: diff.slice(0, 1e4)
491
+ });
407
492
  const result = await generateObject({
408
493
  model,
409
494
  schema: CodeReviewSchema,
410
- prompt: `You are an expert code reviewer. Analyze the following git diff and provide a structured review.
411
-
412
- Focus on:
413
- - Bugs and potential issues
414
- - Security vulnerabilities
415
- - Performance concerns
416
- - Code style and best practices
417
- - Suggestions for improvement
418
-
419
- Git diff:
420
- \`\`\`
421
- ${diff.slice(0, 1e4)}
422
- \`\`\`
423
-
424
- Be constructive and specific. Include line numbers when possible.`
495
+ prompt
425
496
  });
426
497
  return result.object;
427
498
  }
@@ -436,43 +507,19 @@ var ChangelogSchema = z.object({
436
507
  ),
437
508
  summary: z.string().optional().describe("Brief summary of this release")
438
509
  });
439
- async function generateChangelog(context, options) {
510
+ async function generateChangelog(context, options, template) {
440
511
  const model = await getModel(options);
441
- const templateInstructions = context.template ? `
442
- IMPORTANT: Follow this project's changelog format:
443
-
444
- --- CHANGELOG TEMPLATE START ---
445
- ${context.template.slice(0, 2e3)}
446
- --- CHANGELOG TEMPLATE END ---
447
-
448
- Match the style, sections, and formatting of the existing changelog.` : `
449
- Use Keep a Changelog format (https://keepachangelog.com/):
450
- - Group changes by: Added, Changed, Deprecated, Removed, Fixed, Security
451
- - Each item should be a concise description of the change
452
- - Use past tense`;
453
512
  const commitList = context.commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message} (${c.author})`).join("\n");
513
+ const prompt = applyTemplate(template, "changelog", {
514
+ fromRef: context.fromRef,
515
+ toRef: context.toRef,
516
+ commits: commitList,
517
+ diff: context.diff.slice(0, 8e3)
518
+ });
454
519
  const result = await generateObject({
455
520
  model,
456
521
  schema: ChangelogSchema,
457
- prompt: `You are an expert at writing release notes and changelogs.
458
-
459
- Generate a changelog entry for changes from ${context.fromRef} to ${context.toRef}.
460
-
461
- Commits:
462
- ${commitList}
463
-
464
- Diff summary (truncated):
465
- \`\`\`
466
- ${context.diff.slice(0, 8e3)}
467
- \`\`\`
468
- ${templateInstructions}
469
-
470
- Focus on:
471
- - User-facing changes and improvements
472
- - Bug fixes and their impact
473
- - Breaking changes (highlight these)
474
- - Group related changes together
475
- - Write for end users, not developers (unless it's a library)`
522
+ prompt
476
523
  });
477
524
  return result.object;
478
525
  }
@@ -493,96 +540,55 @@ var ExplanationSchema = z.object({
493
540
  impact: z.string().describe("What impact or role this has in the project"),
494
541
  notes: z.array(z.string()).optional().describe("Important considerations or caveats")
495
542
  });
496
- async function generateExplanation(context, options, projectContext) {
543
+ async function generateExplanation(context, options, template) {
497
544
  const model = await getModel(options);
498
- const projectContextSection = projectContext ? `
499
- IMPORTANT: Use this project context to provide more accurate explanations:
500
-
501
- --- PROJECT CONTEXT START ---
502
- ${projectContext.slice(0, 4e3)}
503
- --- PROJECT CONTEXT END ---
504
- ` : "";
505
545
  if (context.type === "file-content") {
546
+ const prompt2 = applyTemplate(template, "explain-file", {
547
+ filePath: context.metadata.filePath || "",
548
+ content: context.content?.slice(0, 15e3) || ""
549
+ });
506
550
  const result2 = await generateObject({
507
551
  model,
508
552
  schema: ExplanationSchema,
509
- prompt: `You are an expert at explaining code in a clear and insightful way.
510
- ${projectContextSection}
511
- Analyze the following file and explain what it does, its purpose, and its role in a project.
512
-
513
- File: ${context.metadata.filePath}
514
-
515
- Content:
516
- \`\`\`
517
- ${context.content?.slice(0, 15e3)}
518
- \`\`\`
519
-
520
- Focus on:
521
- - What this file does (main functionality)
522
- - Its purpose and role in the codebase
523
- - Key functions, classes, or components it defines
524
- - Dependencies and what it interacts with
525
- - Any important patterns or architecture decisions
526
-
527
- Explain in a way that helps someone quickly understand this file's purpose and how it fits into the larger codebase.`
553
+ prompt: prompt2
528
554
  });
529
555
  return result2.object;
530
556
  }
531
557
  let contextInfo;
532
558
  let targetType;
533
559
  if (context.type === "pr") {
534
- contextInfo = `
535
- Pull Request: #${context.metadata.prNumber}
560
+ contextInfo = `Pull Request: #${context.metadata.prNumber}
536
561
  Title: ${context.title}
537
562
  Branch: ${context.metadata.headBranch} -> ${context.metadata.baseBranch}
538
563
  Commits:
539
- ${context.metadata.commits?.map((c) => `- ${c}`).join("\n") || "N/A"}
540
- `;
564
+ ${context.metadata.commits?.map((c) => `- ${c}`).join("\n") || "N/A"}`;
541
565
  targetType = "pull request";
542
566
  } else if (context.type === "file-history") {
543
- contextInfo = `
544
- File: ${context.metadata.filePath}
567
+ contextInfo = `File: ${context.metadata.filePath}
545
568
  Recent commits:
546
569
  ${context.metadata.commits?.map((c) => `- ${c}`).join("\n") || "N/A"}
547
570
  Latest author: ${context.metadata.author}
548
- Latest date: ${context.metadata.date}
549
- `;
571
+ Latest date: ${context.metadata.date}`;
550
572
  targetType = "file changes";
551
573
  } else if (context.type === "uncommitted" || context.type === "staged") {
552
- contextInfo = `
553
- ${context.type === "staged" ? "Staged changes (ready to commit)" : "Uncommitted changes (work in progress)"}
554
- `;
574
+ contextInfo = context.type === "staged" ? "Staged changes (ready to commit)" : "Uncommitted changes (work in progress)";
555
575
  targetType = context.type === "staged" ? "staged changes" : "uncommitted changes";
556
576
  } else {
557
- contextInfo = `
558
- Commit: ${context.metadata.hash?.slice(0, 7)}
577
+ contextInfo = `Commit: ${context.metadata.hash?.slice(0, 7)}
559
578
  Message: ${context.title}
560
579
  Author: ${context.metadata.author}
561
- Date: ${context.metadata.date}
562
- `;
580
+ Date: ${context.metadata.date}`;
563
581
  targetType = "commit";
564
582
  }
583
+ const prompt = applyTemplate(template, "explain", {
584
+ targetType,
585
+ context: contextInfo,
586
+ diff: context.diff?.slice(0, 12e3) || ""
587
+ });
565
588
  const result = await generateObject({
566
589
  model,
567
590
  schema: ExplanationSchema,
568
- prompt: `You are an expert at explaining code changes in a clear and insightful way.
569
- ${projectContextSection}
570
- Analyze the following ${targetType} and provide a comprehensive explanation.
571
-
572
- ${contextInfo}
573
-
574
- Diff:
575
- \`\`\`
576
- ${context.diff?.slice(0, 12e3)}
577
- \`\`\`
578
-
579
- Focus on:
580
- - What the changes accomplish (not just what files changed)
581
- - WHY these changes were likely made
582
- - The broader context and purpose
583
- - Any important implications or side effects
584
-
585
- Explain in a way that helps someone understand not just the "what" but the "why" behind these changes.`
591
+ prompt
586
592
  });
587
593
  return result.object;
588
594
  }
@@ -595,39 +601,18 @@ var CommitSearchSchema = z.object({
595
601
  ),
596
602
  summary: z.string().optional().describe("Brief summary of the search results")
597
603
  });
598
- async function searchCommits(query, commits, options, maxResults = 5, projectContext) {
604
+ async function searchCommits(query, commits, options, maxResults = 5, template) {
599
605
  const model = await getModel(options);
600
- const projectContextSection = projectContext ? `
601
- IMPORTANT: Use this project context to better understand the codebase:
602
-
603
- --- PROJECT CONTEXT START ---
604
- ${projectContext.slice(0, 3e3)}
605
- --- PROJECT CONTEXT END ---
606
- ` : "";
607
606
  const commitList = commits.map((c) => `${c.hash.slice(0, 7)} | ${c.author} | ${c.date.split("T")[0]} | ${c.message.split("\n")[0]}`).join("\n");
607
+ const prompt = applyTemplate(template, "find", {
608
+ query,
609
+ commits: commitList,
610
+ maxResults: String(maxResults)
611
+ });
608
612
  const result = await generateObject({
609
613
  model,
610
614
  schema: CommitSearchSchema,
611
- prompt: `You are an expert at understanding git history and finding relevant commits.
612
- ${projectContextSection}
613
- The user is looking for commits related to: "${query}"
614
-
615
- Here are the commits to search through:
616
- \`\`\`
617
- ${commitList}
618
- \`\`\`
619
-
620
- Find the commits that best match the user's query. Consider:
621
- - Commit messages that mention similar concepts
622
- - Related features, bug fixes, or changes
623
- - Semantic similarity (e.g., "login" matches "authentication")
624
-
625
- Return up to ${maxResults} matching commits, ordered by relevance (most relevant first).
626
- Only include commits that actually match the query - if none match well, return an empty array.
627
-
628
- For each match, provide:
629
- - The commit hash (first 7 characters are fine)
630
- - A brief reason explaining why this commit matches the query`
615
+ prompt
631
616
  });
632
617
  const enrichedMatches = result.object.matches.map((match) => {
633
618
  const commit = commits.find((c) => c.hash.startsWith(match.hash));
@@ -655,35 +640,13 @@ For each match, provide:
655
640
  summary: result.object.summary
656
641
  };
657
642
  }
658
- async function generateBranchName(description, options, context) {
643
+ async function generateBranchName(description, options, context, template) {
659
644
  const model = await getModel(options);
660
- const conventionInstructions = context?.convention ? `
661
- IMPORTANT: Follow this project's branch naming convention:
662
-
663
- --- CONVENTION START ---
664
- ${context.convention}
665
- --- CONVENTION END ---
666
- ` : `
667
- Rules:
668
- - Use format: <type>/<short-description>
669
- - Types: feature, fix, hotfix, chore, refactor, docs, test
670
- - Use kebab-case for description
671
- - Keep it short (under 50 chars total)
672
- - No special characters except hyphens and slashes`;
673
- const typeHint = context?.type ? `
674
- Branch type: ${context.type}` : "";
675
- const issueHint = context?.issue ? `
676
- Include issue number: ${context.issue}` : "";
677
- const prompt = `You are an expert at creating git branch names.
678
-
679
- Generate a clean, descriptive branch name for the following:
680
-
681
- Description: ${description}
682
- ${typeHint}
683
- ${issueHint}
684
- ${conventionInstructions}
685
-
686
- Respond with ONLY the branch name, nothing else.`;
645
+ const prompt = applyTemplate(template, "branch", {
646
+ description,
647
+ type: context?.type,
648
+ issue: context?.issue
649
+ });
687
650
  const result = await generateText({
688
651
  model,
689
652
  prompt,
@@ -691,24 +654,23 @@ Respond with ONLY the branch name, nothing else.`;
691
654
  });
692
655
  return result.text.trim().replace(/[^a-zA-Z0-9/_-]/g, "");
693
656
  }
694
- async function generateStashName(diff, options) {
657
+ async function generateBranchNameFromDiff(diff, options, template) {
695
658
  const model = await getModel(options);
696
- const prompt = `You are an expert at summarizing code changes.
697
-
698
- Generate a short, descriptive stash name for the following changes.
699
-
700
- Rules:
701
- - Start with "WIP: " prefix
702
- - Keep it under 50 characters total
703
- - Be specific about what the changes do
704
- - Use present tense
705
-
706
- Diff:
707
- \`\`\`
708
- ${diff.slice(0, 4e3)}
709
- \`\`\`
710
-
711
- Respond with ONLY the stash name, nothing else.`;
659
+ const prompt = applyTemplate(template, "checkout", {
660
+ diff: diff.slice(0, 8e3)
661
+ });
662
+ const result = await generateText({
663
+ model,
664
+ prompt,
665
+ maxTokens: 100
666
+ });
667
+ return result.text.trim().replace(/[^a-zA-Z0-9/_-]/g, "");
668
+ }
669
+ async function generateStashName(diff, options, template) {
670
+ const model = await getModel(options);
671
+ const prompt = applyTemplate(template, "stash", {
672
+ diff: diff.slice(0, 4e3)
673
+ });
712
674
  const result = await generateText({
713
675
  model,
714
676
  prompt,
@@ -716,63 +678,66 @@ Respond with ONLY the stash name, nothing else.`;
716
678
  });
717
679
  return result.text.trim();
718
680
  }
719
- async function resolveConflict(conflictedContent, context, options, strategy) {
681
+ var WorkSummarySchema = z.object({
682
+ title: z.string().describe("One-line title for the summary"),
683
+ overview: z.string().describe("Brief overview of what was accomplished"),
684
+ highlights: z.array(z.string()).describe("Key accomplishments or highlights"),
685
+ details: z.array(
686
+ z.object({
687
+ category: z.string().describe("Category (e.g., Feature, Bug Fix, Refactor)"),
688
+ items: z.array(z.string()).describe("List of items in this category")
689
+ })
690
+ ),
691
+ stats: z.object({
692
+ commits: z.number(),
693
+ filesChanged: z.number().optional(),
694
+ additions: z.number().optional(),
695
+ deletions: z.number().optional()
696
+ }).optional()
697
+ });
698
+ async function generateWorkSummary(context, options, format = "custom", template) {
699
+ const model = await getModel(options);
700
+ const commitList = context.commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message.split("\n")[0]} (${c.date.split("T")[0]})`).join("\n");
701
+ const formatHint = format === "daily" ? "This is a daily report. Focus on today's accomplishments." : format === "weekly" ? "This is a weekly report. Summarize the week's work at a higher level." : `This is a summary from ${context.since}${context.until ? ` to ${context.until}` : ""}.`;
702
+ const period = `${context.since}${context.until ? ` to ${context.until}` : " to now"}`;
703
+ const prompt = applyTemplate(template, "summary", {
704
+ author: context.author,
705
+ period,
706
+ format: formatHint,
707
+ commits: commitList,
708
+ diff: context.diff?.slice(0, 6e3)
709
+ });
710
+ const result = await generateObject({
711
+ model,
712
+ schema: WorkSummarySchema,
713
+ prompt
714
+ });
715
+ return {
716
+ ...result.object,
717
+ stats: {
718
+ commits: context.commits.length,
719
+ ...result.object.stats
720
+ }
721
+ };
722
+ }
723
+ async function resolveConflict(conflictedContent, context, options, template) {
720
724
  const model = await getModel(options);
721
- const strategyInstructions = strategy ? `
722
- IMPORTANT: Follow this project's merge strategy:
723
-
724
- --- MERGE STRATEGY START ---
725
- ${strategy}
726
- --- MERGE STRATEGY END ---
727
- ` : `
728
- Rules:
729
- - Understand the intent of both changes
730
- - Combine changes when both are valid additions
731
- - Choose the more complete/correct version when they conflict
732
- - Preserve all necessary functionality`;
725
+ const prompt = applyTemplate(template, "merge", {
726
+ filename: context.filename,
727
+ oursRef: context.oursRef,
728
+ theirsRef: context.theirsRef,
729
+ content: conflictedContent
730
+ });
733
731
  const result = await generateObject({
734
732
  model,
735
733
  schema: ConflictResolutionSchema,
736
- prompt: `You are an expert at resolving git merge conflicts intelligently.
737
-
738
- Analyze the following conflicted file and provide a resolution.
739
-
740
- File: ${context.filename}
741
- Merging: ${context.theirsRef} into ${context.oursRef}
742
-
743
- Conflicted content:
744
- \`\`\`
745
- ${conflictedContent}
746
- \`\`\`
747
- ${strategyInstructions}
748
-
749
- Additional rules:
750
- - The resolved content should be valid, working code
751
- - Do NOT include conflict markers (<<<<<<, =======, >>>>>>)
752
-
753
- Provide the fully resolved file content.`
734
+ prompt
754
735
  });
755
736
  return result.object;
756
737
  }
757
738
 
758
- // src/commands/ai-commit.ts
759
- var CONVENTION_PATHS = [
760
- ".gut/commit-convention.md",
761
- ".github/commit-convention.md",
762
- ".commit-convention.md",
763
- "docs/commit-convention.md",
764
- ".gitmessage"
765
- ];
766
- function findCommitConvention(repoRoot) {
767
- for (const conventionPath of CONVENTION_PATHS) {
768
- const fullPath = join(repoRoot, conventionPath);
769
- if (existsSync(fullPath)) {
770
- return readFileSync(fullPath, "utf-8");
771
- }
772
- }
773
- return null;
774
- }
775
- var aiCommitCommand = new Command3("ai-commit").alias("commit").description("Generate a commit message using AI").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-c, --commit", "Automatically commit with the generated message").option("-a, --all", "Force stage all changes (default: auto-stage if nothing staged)").action(async (options) => {
739
+ // src/commands/commit.ts
740
+ var commitCommand = new Command3("commit").description("Generate a commit message using AI").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-c, --commit", "Automatically commit with the generated message").option("-a, --all", "Force stage all changes (default: auto-stage if nothing staged)").action(async (options) => {
776
741
  const git = simpleGit2();
777
742
  const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
778
743
  const isRepo = await git.checkIsRepo();
@@ -786,8 +751,10 @@ var aiCommitCommand = new Command3("ai-commit").alias("commit").description("Gen
786
751
  }
787
752
  let diff = await git.diff(["--cached"]);
788
753
  if (!diff.trim()) {
754
+ const status = await git.status();
789
755
  const unstaged = await git.diff();
790
- if (!unstaged.trim()) {
756
+ const hasUntracked = status.not_added.length > 0 || status.created.length > 0;
757
+ if (!unstaged.trim() && !hasUntracked) {
791
758
  console.error(chalk3.yellow("No changes to commit."));
792
759
  process.exit(1);
793
760
  }
@@ -795,16 +762,16 @@ var aiCommitCommand = new Command3("ai-commit").alias("commit").description("Gen
795
762
  await git.add("-A");
796
763
  diff = await git.diff(["--cached"]);
797
764
  }
798
- const convention = findCommitConvention(repoRoot.trim());
799
- if (convention) {
800
- console.log(chalk3.gray("Using commit convention from project..."));
765
+ const template = findTemplate(repoRoot.trim(), "commit");
766
+ if (template) {
767
+ console.log(chalk3.gray("Using template from project..."));
801
768
  }
802
769
  const spinner = ora2("Generating commit message...").start();
803
770
  try {
804
771
  const message = await generateCommitMessage(
805
772
  diff,
806
773
  { provider, model: options.model },
807
- convention || void 0
774
+ template || void 0
808
775
  );
809
776
  spinner.stop();
810
777
  console.log(chalk3.bold("\nGenerated commit message:\n"));
@@ -832,14 +799,14 @@ var aiCommitCommand = new Command3("ai-commit").alias("commit").description("Gen
832
799
  console.log(chalk3.green("\u2713 Committed successfully"));
833
800
  } else if (answer.toLowerCase() === "e") {
834
801
  console.log(chalk3.gray("Opening editor..."));
835
- const { execSync: execSync4 } = await import("child_process");
802
+ const { execSync: execSync5 } = await import("child_process");
836
803
  const editor = process.env.EDITOR || process.env.VISUAL || "vi";
837
804
  const fs2 = await import("fs");
838
805
  const os = await import("os");
839
806
  const path2 = await import("path");
840
807
  const tmpFile = path2.join(os.tmpdir(), "gut-commit-msg.txt");
841
808
  fs2.writeFileSync(tmpFile, message);
842
- execSync4(`${editor} "${tmpFile}"`, { stdio: "inherit" });
809
+ execSync5(`${editor} "${tmpFile}"`, { stdio: "inherit" });
843
810
  const editedMessage = fs2.readFileSync(tmpFile, "utf-8").trim();
844
811
  fs2.unlinkSync(tmpFile);
845
812
  if (editedMessage) {
@@ -861,15 +828,14 @@ var aiCommitCommand = new Command3("ai-commit").alias("commit").description("Gen
861
828
  }
862
829
  });
863
830
 
864
- // src/commands/ai-pr.ts
831
+ // src/commands/pr.ts
865
832
  import { Command as Command4 } from "commander";
866
833
  import chalk4 from "chalk";
867
834
  import ora3 from "ora";
868
835
  import { simpleGit as simpleGit3 } from "simple-git";
869
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
870
- import { join as join2 } from "path";
871
- var PR_TEMPLATE_PATHS = [
872
- ".gut/pr-template.md",
836
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
837
+ import { join as join3 } from "path";
838
+ var GITHUB_PR_TEMPLATE_PATHS = [
873
839
  ".github/pull_request_template.md",
874
840
  ".github/PULL_REQUEST_TEMPLATE.md",
875
841
  "pull_request_template.md",
@@ -877,15 +843,15 @@ var PR_TEMPLATE_PATHS = [
877
843
  "docs/pull_request_template.md"
878
844
  ];
879
845
  function findPRTemplate(repoRoot) {
880
- for (const templatePath of PR_TEMPLATE_PATHS) {
881
- const fullPath = join2(repoRoot, templatePath);
882
- if (existsSync2(fullPath)) {
883
- return readFileSync2(fullPath, "utf-8");
846
+ for (const templatePath of GITHUB_PR_TEMPLATE_PATHS) {
847
+ const fullPath = join3(repoRoot, templatePath);
848
+ if (existsSync3(fullPath)) {
849
+ return readFileSync3(fullPath, "utf-8");
884
850
  }
885
851
  }
886
- return null;
852
+ return findTemplate(repoRoot, "pr");
887
853
  }
888
- var aiPrCommand = new Command4("ai-pr").alias("pr").description("Generate a pull request title and description using AI").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-b, --base <branch>", "Base branch to compare against (default: main or master)").option("--create", "Create the PR using gh CLI").option("--copy", "Copy the description to clipboard").action(async (options) => {
854
+ var prCommand = new Command4("pr").description("Generate a pull request title and description using AI").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-b, --base <branch>", "Base branch to compare against (default: main or master)").option("--create", "Create the PR using gh CLI").option("--copy", "Copy the description to clipboard").action(async (options) => {
889
855
  const git = simpleGit3();
890
856
  const isRepo = await git.checkIsRepo();
891
857
  if (!isRepo) {
@@ -931,13 +897,10 @@ var aiPrCommand = new Command4("ai-pr").alias("pr").description("Generate a pull
931
897
  baseBranch,
932
898
  currentBranch,
933
899
  commits,
934
- diff,
935
- template: template || void 0
900
+ diff
936
901
  },
937
- {
938
- provider,
939
- model: options.model
940
- }
902
+ { provider, model: options.model },
903
+ template || void 0
941
904
  );
942
905
  spinner.stop();
943
906
  console.log(chalk4.bold("\n\u{1F4DD} Generated PR:\n"));
@@ -948,11 +911,11 @@ var aiPrCommand = new Command4("ai-pr").alias("pr").description("Generate a pull
948
911
  console.log(chalk4.gray("\u2500".repeat(50)));
949
912
  if (options.copy) {
950
913
  try {
951
- const { execSync: execSync4 } = await import("child_process");
914
+ const { execSync: execSync5 } = await import("child_process");
952
915
  const fullText = `${title}
953
916
 
954
917
  ${body}`;
955
- execSync4("pbcopy", { input: fullText });
918
+ execSync5("pbcopy", { input: fullText });
956
919
  console.log(chalk4.green("\n\u2713 Copied to clipboard"));
957
920
  } catch {
958
921
  console.log(chalk4.yellow("\n\u26A0 Could not copy to clipboard"));
@@ -971,10 +934,10 @@ ${body}`;
971
934
  if (answer.toLowerCase() === "y") {
972
935
  const createSpinner = ora3("Creating PR...").start();
973
936
  try {
974
- const { execSync: execSync4 } = await import("child_process");
937
+ const { execSync: execSync5 } = await import("child_process");
975
938
  const escapedTitle = title.replace(/"/g, '\\"');
976
939
  const escapedBody = body.replace(/"/g, '\\"');
977
- execSync4(
940
+ execSync5(
978
941
  `gh pr create --title "${escapedTitle}" --body "${escapedBody}" --base ${baseBranch}`,
979
942
  { stdio: "pipe" }
980
943
  );
@@ -997,16 +960,16 @@ ${body}`;
997
960
  }
998
961
  });
999
962
 
1000
- // src/commands/ai-review.ts
963
+ // src/commands/review.ts
1001
964
  import { Command as Command5 } from "commander";
1002
965
  import chalk5 from "chalk";
1003
966
  import ora4 from "ora";
1004
967
  import { simpleGit as simpleGit4 } from "simple-git";
1005
- import { execSync } from "child_process";
968
+ import { execSync as execSync2 } from "child_process";
1006
969
  async function getPRDiff(prNumber) {
1007
970
  try {
1008
- const diff = execSync(`gh pr diff ${prNumber}`, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
1009
- const prJsonStr = execSync(`gh pr view ${prNumber} --json number,title,author,url`, { encoding: "utf-8" });
971
+ const diff = execSync2(`gh pr diff ${prNumber}`, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
972
+ const prJsonStr = execSync2(`gh pr view ${prNumber} --json number,title,author,url`, { encoding: "utf-8" });
1010
973
  const prJson = JSON.parse(prJsonStr);
1011
974
  return {
1012
975
  diff,
@@ -1024,7 +987,7 @@ async function getPRDiff(prNumber) {
1024
987
  throw error;
1025
988
  }
1026
989
  }
1027
- var aiReviewCommand = new Command5("ai-review").alias("review").description("Get an AI code review of your changes or a GitHub PR").argument("[pr-number]", "GitHub PR number to review").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-s, --staged", "Review only staged changes").option("-c, --commit <hash>", "Review a specific commit").option("--json", "Output as JSON").action(async (prNumber, options) => {
990
+ var reviewCommand = new Command5("review").description("Get an AI code review of your changes or a GitHub PR").argument("[pr-number]", "GitHub PR number to review").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-s, --staged", "Review only staged changes").option("-c, --commit <hash>", "Review a specific commit").option("--json", "Output as JSON").action(async (prNumber, options) => {
1028
991
  const git = simpleGit4();
1029
992
  const isRepo = await git.checkIsRepo();
1030
993
  if (!isRepo) {
@@ -1059,10 +1022,13 @@ var aiReviewCommand = new Command5("ai-review").alias("review").description("Get
1059
1022
  process.exit(0);
1060
1023
  }
1061
1024
  spinner.text = "AI is reviewing your code...";
1062
- const review = await generateCodeReview(diff, {
1063
- provider,
1064
- model: options.model
1065
- });
1025
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1026
+ const template = findTemplate(repoRoot.trim(), "review");
1027
+ const review = await generateCodeReview(
1028
+ diff,
1029
+ { provider, model: options.model },
1030
+ template || void 0
1031
+ );
1066
1032
  spinner.stop();
1067
1033
  if (options.json) {
1068
1034
  console.log(JSON.stringify({ prInfo, review }, null, 2));
@@ -1127,27 +1093,14 @@ function printReview(review) {
1127
1093
  console.log();
1128
1094
  }
1129
1095
 
1130
- // src/commands/ai-merge.ts
1096
+ // src/commands/merge.ts
1131
1097
  import { Command as Command6 } from "commander";
1132
1098
  import chalk6 from "chalk";
1133
1099
  import ora5 from "ora";
1134
1100
  import { simpleGit as simpleGit5 } from "simple-git";
1135
1101
  import * as fs from "fs";
1136
1102
  import * as path from "path";
1137
- var MERGE_STRATEGY_PATHS = [
1138
- ".gut/merge-strategy.md",
1139
- ".github/merge-strategy.md"
1140
- ];
1141
- function findMergeStrategy(repoRoot) {
1142
- for (const strategyPath of MERGE_STRATEGY_PATHS) {
1143
- const fullPath = path.join(repoRoot, strategyPath);
1144
- if (fs.existsSync(fullPath)) {
1145
- return fs.readFileSync(fullPath, "utf-8");
1146
- }
1147
- }
1148
- return null;
1149
- }
1150
- var aiMergeCommand = new Command6("ai-merge").alias("merge").description("Merge a branch with AI-powered conflict resolution").argument("<branch>", "Branch to merge").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("--no-commit", "Do not auto-commit after resolving").action(async (branch, options) => {
1103
+ var mergeCommand = new Command6("merge").description("Merge a branch with AI-powered conflict resolution").argument("<branch>", "Branch to merge").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("--no-commit", "Do not auto-commit after resolving").action(async (branch, options) => {
1151
1104
  const git = simpleGit5();
1152
1105
  const isRepo = await git.checkIsRepo();
1153
1106
  if (!isRepo) {
@@ -1183,9 +1136,9 @@ Merging ${chalk6.cyan(branch)} into ${chalk6.cyan(currentBranch)}...
1183
1136
  `));
1184
1137
  const spinner = ora5();
1185
1138
  const rootDir = await git.revparse(["--show-toplevel"]);
1186
- const strategy = findMergeStrategy(rootDir.trim());
1187
- if (strategy) {
1188
- console.log(chalk6.gray("Using merge strategy from project...\n"));
1139
+ const template = findTemplate(rootDir.trim(), "merge");
1140
+ if (template) {
1141
+ console.log(chalk6.gray("Using merge template from project...\n"));
1189
1142
  }
1190
1143
  for (const file of conflictedFiles) {
1191
1144
  const filePath = path.join(rootDir.trim(), file);
@@ -1205,7 +1158,7 @@ Merging ${chalk6.cyan(branch)} into ${chalk6.cyan(currentBranch)}...
1205
1158
  filename: file,
1206
1159
  oursRef: currentBranch,
1207
1160
  theirsRef: branch
1208
- }, { provider, model: options.model }, strategy || void 0);
1161
+ }, { provider, model: options.model }, template || void 0);
1209
1162
  spinner.stop();
1210
1163
  console.log(chalk6.cyan("\n\u{1F916} AI suggests:"));
1211
1164
  console.log(chalk6.gray("\u2500".repeat(50)));
@@ -1258,26 +1211,6 @@ import { Command as Command7 } from "commander";
1258
1211
  import chalk7 from "chalk";
1259
1212
  import ora6 from "ora";
1260
1213
  import { simpleGit as simpleGit6 } from "simple-git";
1261
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1262
- import { join as join4 } from "path";
1263
- var CHANGELOG_PATHS = [
1264
- ".gut/changelog-template.md",
1265
- ".gut/CHANGELOG.md",
1266
- "CHANGELOG.md",
1267
- "HISTORY.md",
1268
- "CHANGES.md",
1269
- "changelog.md",
1270
- "docs/CHANGELOG.md"
1271
- ];
1272
- function findChangelog(repoRoot) {
1273
- for (const changelogPath of CHANGELOG_PATHS) {
1274
- const fullPath = join4(repoRoot, changelogPath);
1275
- if (existsSync4(fullPath)) {
1276
- return readFileSync4(fullPath, "utf-8");
1277
- }
1278
- }
1279
- return null;
1280
- }
1281
1214
  function formatChangelog(changelog) {
1282
1215
  const lines = [];
1283
1216
  const header = changelog.version ? `## [${changelog.version}] - ${changelog.date}` : `## ${changelog.date}`;
@@ -1328,24 +1261,14 @@ var changelogCommand = new Command7("changelog").description("Generate a changel
1328
1261
  }));
1329
1262
  const diff = await git.diff([`${fromRef}...${toRef}`]);
1330
1263
  const repoRoot = await git.revparse(["--show-toplevel"]);
1331
- const existingChangelog = findChangelog(repoRoot.trim());
1332
- let template;
1333
- if (existingChangelog) {
1334
- const firstEntryMatch = existingChangelog.match(/## \[?[\d.]+\]?[\s\S]*?(?=## \[?[\d.]+\]?|$)/);
1335
- if (firstEntryMatch) {
1336
- template = firstEntryMatch[0].slice(0, 1500);
1337
- }
1338
- spinner.text = "Found existing changelog, matching style...";
1264
+ const template = findTemplate(repoRoot.trim(), "changelog");
1265
+ if (template) {
1266
+ spinner.text = "Using template from project...";
1339
1267
  }
1340
1268
  const changelog = await generateChangelog(
1341
- {
1342
- commits,
1343
- diff,
1344
- fromRef,
1345
- toRef,
1346
- template
1347
- },
1348
- { provider, model: options.model }
1269
+ { commits, diff, fromRef, toRef },
1270
+ { provider, model: options.model },
1271
+ template || void 0
1349
1272
  );
1350
1273
  spinner.stop();
1351
1274
  if (options.json) {
@@ -1358,7 +1281,7 @@ var changelogCommand = new Command7("changelog").description("Generate a changel
1358
1281
  console.log(chalk7.gray("\u2500".repeat(50)));
1359
1282
  console.log(chalk7.gray(`
1360
1283
  Range: ${fromRef}..${toRef} (${commits.length} commits)`));
1361
- if (existingChangelog) {
1284
+ if (template) {
1362
1285
  console.log(chalk7.gray("Style matched from existing CHANGELOG.md"));
1363
1286
  }
1364
1287
  } catch (error) {
@@ -1368,27 +1291,14 @@ Range: ${fromRef}..${toRef} (${commits.length} commits)`));
1368
1291
  }
1369
1292
  });
1370
1293
 
1371
- // src/commands/ai-explain.ts
1294
+ // src/commands/explain.ts
1372
1295
  import { Command as Command8 } from "commander";
1373
1296
  import chalk8 from "chalk";
1374
1297
  import ora7 from "ora";
1375
1298
  import { simpleGit as simpleGit7 } from "simple-git";
1376
- import { execSync as execSync2 } from "child_process";
1377
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
1378
- import { join as join5 } from "path";
1379
- var CONTEXT_PATHS = [
1380
- ".gut/explain.md"
1381
- ];
1382
- function findExplainContext(repoRoot) {
1383
- for (const contextPath of CONTEXT_PATHS) {
1384
- const fullPath = join5(repoRoot, contextPath);
1385
- if (existsSync5(fullPath)) {
1386
- return readFileSync5(fullPath, "utf-8");
1387
- }
1388
- }
1389
- return null;
1390
- }
1391
- var aiExplainCommand = new Command8("ai-explain").alias("explain").description("Get an AI-powered explanation of changes, commits, PRs, or files").argument("[target]", "Commit hash, PR number, PR URL, or file path (default: uncommitted changes)").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-s, --staged", "Explain only staged changes").option("-n, --commits <n>", "Number of commits to analyze for file history (default: 1)", "1").option("--history", "Explain file change history instead of content").option("--json", "Output as JSON").action(async (target, options) => {
1299
+ import { execSync as execSync3 } from "child_process";
1300
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
1301
+ var explainCommand = new Command8("explain").description("Get an AI-powered explanation of changes, commits, PRs, or files").argument("[target]", "Commit hash, PR number, PR URL, or file path (default: uncommitted changes)").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-s, --staged", "Explain only staged changes").option("-n, --commits <n>", "Number of commits to analyze for file history (default: 1)", "1").option("--history", "Explain file change history instead of content").option("--json", "Output as JSON").action(async (target, options) => {
1392
1302
  const git = simpleGit7();
1393
1303
  const isRepo = await git.checkIsRepo();
1394
1304
  if (!isRepo) {
@@ -1408,7 +1318,7 @@ var aiExplainCommand = new Command8("ai-explain").alias("explain").description("
1408
1318
  }
1409
1319
  } else {
1410
1320
  const isPR = target.match(/^#?\d+$/) || target.includes("/pull/");
1411
- const isFile = existsSync5(target);
1321
+ const isFile = existsSync4(target);
1412
1322
  if (isPR) {
1413
1323
  context = await getPRContext(target, spinner);
1414
1324
  } else if (isFile) {
@@ -1421,15 +1331,18 @@ var aiExplainCommand = new Command8("ai-explain").alias("explain").description("
1421
1331
  context = await getCommitContext(target, git, spinner);
1422
1332
  }
1423
1333
  }
1424
- const projectContext = findExplainContext(repoRoot.trim());
1425
- if (projectContext) {
1426
- console.log(chalk8.gray("Using project context..."));
1334
+ const isFileContent = context.type === "file-content";
1335
+ const templateName = isFileContent ? "explain-file" : "explain";
1336
+ const template = findTemplate(repoRoot.trim(), templateName);
1337
+ if (template) {
1338
+ console.log(chalk8.gray("Using template from project..."));
1427
1339
  }
1428
1340
  spinner.text = "AI is generating explanation...";
1429
- const explanation = await generateExplanation(context, {
1430
- provider,
1431
- model: options.model
1432
- }, projectContext || void 0);
1341
+ const explanation = await generateExplanation(
1342
+ context,
1343
+ { provider, model: options.model },
1344
+ template || void 0
1345
+ );
1433
1346
  spinner.stop();
1434
1347
  if (options.json) {
1435
1348
  console.log(JSON.stringify(explanation, null, 2));
@@ -1542,7 +1455,7 @@ async function getPRContext(target, spinner) {
1542
1455
  spinner.text = `Fetching PR #${prNumber}...`;
1543
1456
  let prInfo;
1544
1457
  try {
1545
- const prJson = execSync2(
1458
+ const prJson = execSync3(
1546
1459
  `gh pr view ${prNumber} --json title,url,baseRefName,headRefName,commits`,
1547
1460
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
1548
1461
  );
@@ -1553,7 +1466,7 @@ async function getPRContext(target, spinner) {
1553
1466
  spinner.text = `Getting diff for PR #${prNumber}...`;
1554
1467
  let diff;
1555
1468
  try {
1556
- diff = execSync2(`gh pr diff ${prNumber}`, {
1469
+ diff = execSync3(`gh pr diff ${prNumber}`, {
1557
1470
  encoding: "utf-8",
1558
1471
  stdio: ["pipe", "pipe", "pipe"],
1559
1472
  maxBuffer: 10 * 1024 * 1024
@@ -1615,24 +1528,12 @@ ${icon} Explanation
1615
1528
  }
1616
1529
  }
1617
1530
 
1618
- // src/commands/ai-find.ts
1531
+ // src/commands/find.ts
1619
1532
  import { Command as Command9 } from "commander";
1620
1533
  import chalk9 from "chalk";
1621
1534
  import ora8 from "ora";
1622
1535
  import { simpleGit as simpleGit8 } from "simple-git";
1623
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
1624
- import { join as join6 } from "path";
1625
- var CONTEXT_PATHS2 = [".gut/find.md"];
1626
- function findProjectContext(repoRoot) {
1627
- for (const contextPath of CONTEXT_PATHS2) {
1628
- const fullPath = join6(repoRoot, contextPath);
1629
- if (existsSync6(fullPath)) {
1630
- return readFileSync6(fullPath, "utf-8");
1631
- }
1632
- }
1633
- return null;
1634
- }
1635
- var aiFindCommand = new Command9("ai-find").alias("find").description("Find commits matching a vague description using AI").argument("<query>", 'Description of the change you are looking for (e.g., "login feature added")').option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-n, --num <n>", "Number of commits to search through", "100").option("--path <path>", "Limit search to commits affecting this path").option("--author <author>", "Limit search to commits by this author").option("--since <date>", "Limit search to commits after this date").option("--until <date>", "Limit search to commits before this date").option("--max-results <n>", "Maximum number of matching commits to return", "5").option("--json", "Output as JSON").action(async (query, options) => {
1536
+ var findCommand = new Command9("find").description("Find commits matching a vague description using AI").argument("<query>", 'Description of the change you are looking for (e.g., "login feature added")').option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-n, --num <n>", "Number of commits to search through", "100").option("--path <path>", "Limit search to commits affecting this path").option("--author <author>", "Limit search to commits by this author").option("--since <date>", "Limit search to commits after this date").option("--until <date>", "Limit search to commits before this date").option("--max-results <n>", "Maximum number of matching commits to return", "5").option("--json", "Output as JSON").action(async (query, options) => {
1636
1537
  const git = simpleGit8();
1637
1538
  const isRepo = await git.checkIsRepo();
1638
1539
  if (!isRepo) {
@@ -1670,16 +1571,13 @@ var aiFindCommand = new Command9("ai-find").alias("find").description("Find comm
1670
1571
  email: c.author_email,
1671
1572
  date: c.date
1672
1573
  }));
1673
- const projectContext = findProjectContext(repoRoot.trim());
1574
+ const template = findTemplate(repoRoot.trim(), "find");
1674
1575
  const results = await searchCommits(
1675
1576
  query,
1676
1577
  commits,
1677
- {
1678
- provider,
1679
- model: options.model
1680
- },
1578
+ { provider, model: options.model },
1681
1579
  parseInt(options.maxResults, 10),
1682
- projectContext || void 0
1580
+ template || void 0
1683
1581
  );
1684
1582
  spinner.stop();
1685
1583
  if (results.matches.length === 0) {
@@ -1725,27 +1623,15 @@ function printResults(results, query) {
1725
1623
  }
1726
1624
  }
1727
1625
 
1728
- // src/commands/ai-branch.ts
1626
+ // src/commands/branch.ts
1729
1627
  import { Command as Command10 } from "commander";
1730
1628
  import chalk10 from "chalk";
1731
1629
  import ora9 from "ora";
1732
1630
  import { simpleGit as simpleGit9 } from "simple-git";
1733
- import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
1734
- import { join as join7 } from "path";
1735
- import { execSync as execSync3 } from "child_process";
1736
- var CONVENTION_PATHS2 = [".gut/branch-convention.md", ".github/branch-convention.md"];
1737
- function findBranchConvention(repoRoot) {
1738
- for (const conventionPath of CONVENTION_PATHS2) {
1739
- const fullPath = join7(repoRoot, conventionPath);
1740
- if (existsSync7(fullPath)) {
1741
- return readFileSync7(fullPath, "utf-8");
1742
- }
1743
- }
1744
- return null;
1745
- }
1631
+ import { execSync as execSync4 } from "child_process";
1746
1632
  function getIssueInfo(issueNumber) {
1747
1633
  try {
1748
- const result = execSync3(`gh issue view ${issueNumber} --json title,body`, {
1634
+ const result = execSync4(`gh issue view ${issueNumber} --json title,body`, {
1749
1635
  encoding: "utf-8",
1750
1636
  stdio: ["pipe", "pipe", "pipe"]
1751
1637
  });
@@ -1754,7 +1640,7 @@ function getIssueInfo(issueNumber) {
1754
1640
  return null;
1755
1641
  }
1756
1642
  }
1757
- var aiBranchCommand = new Command10("ai-branch").alias("branch").description("Generate a branch name from issue number or description").argument("[issue]", "Issue number (e.g., 123 or #123)").option("-d, --description <description>", "Use description instead of issue").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-t, --type <type>", "Branch type (feature, fix, hotfix, chore, refactor)").option("-c, --checkout", "Create and checkout the branch").action(async (issue, options) => {
1643
+ var branchCommand = new Command10("branch").description("Generate a branch name from issue number or description").argument("[issue]", "Issue number (e.g., 123 or #123)").option("-d, --description <description>", "Use description instead of issue").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-t, --type <type>", "Branch type (feature, fix, hotfix, chore, refactor)").option("-c, --checkout", "Create and checkout the branch").action(async (issue, options) => {
1758
1644
  const git = simpleGit9();
1759
1645
  const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1760
1646
  const isRepo = await git.checkIsRepo();
@@ -1788,20 +1674,17 @@ ${issueInfo.body || ""}`;
1788
1674
  process.exit(1);
1789
1675
  }
1790
1676
  const provider = options.provider.toLowerCase();
1791
- const convention = findBranchConvention(repoRoot.trim());
1792
- if (convention) {
1793
- console.log(chalk10.gray("Using branch convention from project..."));
1677
+ const template = findTemplate(repoRoot.trim(), "branch");
1678
+ if (template) {
1679
+ console.log(chalk10.gray("Using template from project..."));
1794
1680
  }
1795
1681
  const spinner = ora9("Generating branch name...").start();
1796
1682
  try {
1797
1683
  const branchName = await generateBranchName(
1798
1684
  description,
1799
1685
  { provider, model: options.model },
1800
- {
1801
- type: options.type,
1802
- issue: issueNumber,
1803
- convention
1804
- }
1686
+ { type: options.type, issue: issueNumber },
1687
+ template || void 0
1805
1688
  );
1806
1689
  spinner.stop();
1807
1690
  console.log(chalk10.bold("\nGenerated branch name:\n"));
@@ -1835,33 +1718,111 @@ ${issueInfo.body || ""}`;
1835
1718
  }
1836
1719
  });
1837
1720
 
1838
- // src/commands/sync.ts
1721
+ // src/commands/checkout.ts
1839
1722
  import { Command as Command11 } from "commander";
1840
1723
  import chalk11 from "chalk";
1841
1724
  import ora10 from "ora";
1842
1725
  import { simpleGit as simpleGit10 } from "simple-git";
1843
- var syncCommand = new Command11("sync").description("Sync current branch with remote (fetch + rebase/merge)").option("-m, --merge", "Use merge instead of rebase").option("--no-push", "Skip push after syncing").option("--stash", "Auto-stash changes before sync").option("-f, --force", "Force sync even with uncommitted changes").action(async (options) => {
1726
+ var checkoutCommand = new Command11("checkout").description("Generate a branch name from current diff and checkout").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-y, --yes", "Skip confirmation and checkout directly").option("-s, --staged", "Use staged changes only instead of all changes").action(async (options) => {
1844
1727
  const git = simpleGit10();
1845
1728
  const isRepo = await git.checkIsRepo();
1846
1729
  if (!isRepo) {
1847
1730
  console.error(chalk11.red("Error: Not a git repository"));
1848
1731
  process.exit(1);
1849
1732
  }
1850
- const spinner = ora10("Checking repository status...").start();
1733
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1734
+ const spinner = ora10("Analyzing changes...").start();
1735
+ const status = await git.status();
1736
+ let diff;
1737
+ if (options.staged) {
1738
+ diff = await git.diff(["--cached"]);
1739
+ } else {
1740
+ const stagedDiff = await git.diff(["--cached"]);
1741
+ const unstagedDiff = await git.diff();
1742
+ diff = stagedDiff + "\n" + unstagedDiff;
1743
+ }
1744
+ const hasChanges = diff.trim() || status.not_added.length > 0 || status.created.length > 0;
1745
+ if (!hasChanges) {
1746
+ spinner.fail("No changes found");
1747
+ console.log(chalk11.gray("Make some changes first, then run gut checkout"));
1748
+ process.exit(1);
1749
+ }
1750
+ if (!diff.trim() && (status.not_added.length > 0 || status.created.length > 0)) {
1751
+ const untrackedFiles = [...status.not_added, ...status.created];
1752
+ diff = `New files:
1753
+ ${untrackedFiles.map((f) => `+ ${f}`).join("\n")}`;
1754
+ }
1755
+ spinner.text = "Generating branch name...";
1756
+ const provider = options.provider.toLowerCase();
1757
+ const template = findTemplate(repoRoot.trim(), "checkout");
1758
+ if (template) {
1759
+ console.log(chalk11.gray("\nUsing template from project..."));
1760
+ }
1761
+ try {
1762
+ const branchName = await generateBranchNameFromDiff(
1763
+ diff,
1764
+ { provider, model: options.model },
1765
+ template
1766
+ );
1767
+ spinner.stop();
1768
+ console.log(chalk11.bold("\nGenerated branch name:\n"));
1769
+ console.log(chalk11.green(` ${branchName}`));
1770
+ console.log();
1771
+ if (options.yes) {
1772
+ await git.checkoutLocalBranch(branchName);
1773
+ console.log(chalk11.green(`\u2713 Created and checked out branch: ${branchName}`));
1774
+ } else {
1775
+ const readline = await import("readline");
1776
+ const rl = readline.createInterface({
1777
+ input: process.stdin,
1778
+ output: process.stdout
1779
+ });
1780
+ const answer = await new Promise((resolve) => {
1781
+ rl.question(chalk11.cyan("Create and checkout this branch? (y/N) "), resolve);
1782
+ });
1783
+ rl.close();
1784
+ if (answer.toLowerCase() === "y") {
1785
+ await git.checkoutLocalBranch(branchName);
1786
+ console.log(chalk11.green(`\u2713 Created and checked out branch: ${branchName}`));
1787
+ } else {
1788
+ console.log(chalk11.gray("\nTo create manually:"));
1789
+ console.log(chalk11.gray(` git checkout -b ${branchName}`));
1790
+ }
1791
+ }
1792
+ } catch (error) {
1793
+ spinner.fail("Failed to generate branch name");
1794
+ console.error(chalk11.red(error instanceof Error ? error.message : "Unknown error"));
1795
+ process.exit(1);
1796
+ }
1797
+ });
1798
+
1799
+ // src/commands/sync.ts
1800
+ import { Command as Command12 } from "commander";
1801
+ import chalk12 from "chalk";
1802
+ import ora11 from "ora";
1803
+ import { simpleGit as simpleGit11 } from "simple-git";
1804
+ var syncCommand = new Command12("sync").description("Sync current branch with remote (fetch + rebase/merge)").option("-m, --merge", "Use merge instead of rebase").option("--no-push", "Skip push after syncing").option("--stash", "Auto-stash changes before sync").option("-f, --force", "Force sync even with uncommitted changes").action(async (options) => {
1805
+ const git = simpleGit11();
1806
+ const isRepo = await git.checkIsRepo();
1807
+ if (!isRepo) {
1808
+ console.error(chalk12.red("Error: Not a git repository"));
1809
+ process.exit(1);
1810
+ }
1811
+ const spinner = ora11("Checking repository status...").start();
1851
1812
  try {
1852
1813
  const status = await git.status();
1853
1814
  const hasChanges = !status.isClean();
1854
1815
  if (hasChanges && !options.stash && !options.force) {
1855
1816
  spinner.stop();
1856
- console.log(chalk11.yellow("You have uncommitted changes:"));
1817
+ console.log(chalk12.yellow("You have uncommitted changes:"));
1857
1818
  if (status.modified.length > 0) {
1858
- console.log(chalk11.gray(` Modified: ${status.modified.length} file(s)`));
1819
+ console.log(chalk12.gray(` Modified: ${status.modified.length} file(s)`));
1859
1820
  }
1860
1821
  if (status.not_added.length > 0) {
1861
- console.log(chalk11.gray(` Untracked: ${status.not_added.length} file(s)`));
1822
+ console.log(chalk12.gray(` Untracked: ${status.not_added.length} file(s)`));
1862
1823
  }
1863
1824
  console.log();
1864
- console.log(chalk11.gray("Use --stash to auto-stash, or --force to sync anyway"));
1825
+ console.log(chalk12.gray("Use --stash to auto-stash, or --force to sync anyway"));
1865
1826
  process.exit(1);
1866
1827
  }
1867
1828
  let stashed = false;
@@ -1880,11 +1841,11 @@ var syncCommand = new Command11("sync").description("Sync current branch with re
1880
1841
  const trackingBranch = status.tracking;
1881
1842
  if (!trackingBranch) {
1882
1843
  spinner.warn(`Branch ${currentBranch} has no upstream tracking branch`);
1883
- console.log(chalk11.gray(`
1844
+ console.log(chalk12.gray(`
1884
1845
  To set upstream: git push -u origin ${currentBranch}`));
1885
1846
  if (stashed) {
1886
1847
  await git.stash(["pop"]);
1887
- console.log(chalk11.gray("Restored stashed changes"));
1848
+ console.log(chalk12.gray("Restored stashed changes"));
1888
1849
  }
1889
1850
  return;
1890
1851
  }
@@ -1898,36 +1859,36 @@ To set upstream: git push -u origin ${currentBranch}`));
1898
1859
  }
1899
1860
  } catch (error) {
1900
1861
  spinner.fail(`${strategy} failed - you may have conflicts`);
1901
- console.log(chalk11.yellow("\nResolve conflicts and then:"));
1862
+ console.log(chalk12.yellow("\nResolve conflicts and then:"));
1902
1863
  if (options.merge) {
1903
- console.log(chalk11.gray(" git add . && git commit"));
1864
+ console.log(chalk12.gray(" git add . && git commit"));
1904
1865
  } else {
1905
- console.log(chalk11.gray(" git add . && git rebase --continue"));
1866
+ console.log(chalk12.gray(" git add . && git rebase --continue"));
1906
1867
  }
1907
1868
  if (stashed) {
1908
- console.log(chalk11.yellow("\nNote: You have stashed changes. Run `git stash pop` after resolving."));
1869
+ console.log(chalk12.yellow("\nNote: You have stashed changes. Run `git stash pop` after resolving."));
1909
1870
  }
1910
1871
  process.exit(1);
1911
1872
  }
1912
1873
  const newStatus = await git.status();
1913
1874
  const ahead = newStatus.ahead || 0;
1914
1875
  const behind = newStatus.behind || 0;
1915
- spinner.succeed(chalk11.green("Synced successfully"));
1876
+ spinner.succeed(chalk12.green("Synced successfully"));
1916
1877
  if (behind > 0) {
1917
- console.log(chalk11.yellow(` \u2193 ${behind} commit(s) behind`));
1878
+ console.log(chalk12.yellow(` \u2193 ${behind} commit(s) behind`));
1918
1879
  }
1919
1880
  if (ahead > 0) {
1920
1881
  if (options.push !== false) {
1921
- const pushSpinner = ora10("Pushing to remote...").start();
1882
+ const pushSpinner = ora11("Pushing to remote...").start();
1922
1883
  try {
1923
1884
  await git.push();
1924
- pushSpinner.succeed(chalk11.green(`Pushed ${ahead} commit(s)`));
1885
+ pushSpinner.succeed(chalk12.green(`Pushed ${ahead} commit(s)`));
1925
1886
  } catch (error) {
1926
1887
  pushSpinner.fail("Push failed");
1927
- console.error(chalk11.red(error instanceof Error ? error.message : "Unknown error"));
1888
+ console.error(chalk12.red(error instanceof Error ? error.message : "Unknown error"));
1928
1889
  }
1929
1890
  } else {
1930
- console.log(chalk11.cyan(` \u2191 ${ahead} commit(s) ahead`));
1891
+ console.log(chalk12.cyan(` \u2191 ${ahead} commit(s) ahead`));
1931
1892
  }
1932
1893
  }
1933
1894
  if (stashed) {
@@ -1937,37 +1898,37 @@ To set upstream: git push -u origin ${currentBranch}`));
1937
1898
  spinner.succeed("Restored stashed changes");
1938
1899
  } catch {
1939
1900
  spinner.warn("Could not auto-restore stash (may have conflicts)");
1940
- console.log(chalk11.gray(" Run `git stash pop` manually"));
1901
+ console.log(chalk12.gray(" Run `git stash pop` manually"));
1941
1902
  }
1942
1903
  }
1943
1904
  } catch (error) {
1944
1905
  spinner.fail("Sync failed");
1945
- console.error(chalk11.red(error instanceof Error ? error.message : "Unknown error"));
1906
+ console.error(chalk12.red(error instanceof Error ? error.message : "Unknown error"));
1946
1907
  process.exit(1);
1947
1908
  }
1948
1909
  });
1949
1910
 
1950
1911
  // src/commands/stash.ts
1951
- import { Command as Command12 } from "commander";
1952
- import chalk12 from "chalk";
1953
- import ora11 from "ora";
1954
- import { simpleGit as simpleGit11 } from "simple-git";
1955
- var stashCommand = new Command12("stash").description("Stash changes with AI-generated name").argument("[name]", "Custom stash name (skips AI generation)").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-l, --list", "List all stashes").option("-a, --apply [index]", "Apply stash (default: latest)").option("--pop [index]", "Pop stash (default: latest)").option("-d, --drop [index]", "Drop stash").option("--clear", "Clear all stashes").action(async (name, options) => {
1956
- const git = simpleGit11();
1912
+ import { Command as Command13 } from "commander";
1913
+ import chalk13 from "chalk";
1914
+ import ora12 from "ora";
1915
+ import { simpleGit as simpleGit12 } from "simple-git";
1916
+ var stashCommand = new Command13("stash").description("Stash changes with AI-generated name").argument("[name]", "Custom stash name (skips AI generation)").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-l, --list", "List all stashes").option("-a, --apply [index]", "Apply stash (default: latest)").option("--pop [index]", "Pop stash (default: latest)").option("-d, --drop [index]", "Drop stash").option("--clear", "Clear all stashes").action(async (name, options) => {
1917
+ const git = simpleGit12();
1957
1918
  const isRepo = await git.checkIsRepo();
1958
1919
  if (!isRepo) {
1959
- console.error(chalk12.red("Error: Not a git repository"));
1920
+ console.error(chalk13.red("Error: Not a git repository"));
1960
1921
  process.exit(1);
1961
1922
  }
1962
1923
  if (options.list) {
1963
1924
  const stashList = await git.stashList();
1964
1925
  if (stashList.all.length === 0) {
1965
- console.log(chalk12.gray("No stashes found"));
1926
+ console.log(chalk13.gray("No stashes found"));
1966
1927
  return;
1967
1928
  }
1968
- console.log(chalk12.bold("\nStashes:\n"));
1929
+ console.log(chalk13.bold("\nStashes:\n"));
1969
1930
  stashList.all.forEach((stash, index) => {
1970
- console.log(` ${chalk12.cyan(index.toString())} ${stash.message}`);
1931
+ console.log(` ${chalk13.cyan(index.toString())} ${stash.message}`);
1971
1932
  });
1972
1933
  console.log();
1973
1934
  return;
@@ -1976,9 +1937,9 @@ var stashCommand = new Command12("stash").description("Stash changes with AI-gen
1976
1937
  const index = typeof options.apply === "string" ? options.apply : "0";
1977
1938
  try {
1978
1939
  await git.stash(["apply", `stash@{${index}}`]);
1979
- console.log(chalk12.green(`\u2713 Applied stash@{${index}}`));
1940
+ console.log(chalk13.green(`\u2713 Applied stash@{${index}}`));
1980
1941
  } catch (error) {
1981
- console.error(chalk12.red(`Failed to apply stash: ${error instanceof Error ? error.message : "Unknown error"}`));
1942
+ console.error(chalk13.red(`Failed to apply stash: ${error instanceof Error ? error.message : "Unknown error"}`));
1982
1943
  process.exit(1);
1983
1944
  }
1984
1945
  return;
@@ -1987,9 +1948,9 @@ var stashCommand = new Command12("stash").description("Stash changes with AI-gen
1987
1948
  const index = typeof options.pop === "string" ? options.pop : "0";
1988
1949
  try {
1989
1950
  await git.stash(["pop", `stash@{${index}}`]);
1990
- console.log(chalk12.green(`\u2713 Popped stash@{${index}}`));
1951
+ console.log(chalk13.green(`\u2713 Popped stash@{${index}}`));
1991
1952
  } catch (error) {
1992
- console.error(chalk12.red(`Failed to pop stash: ${error instanceof Error ? error.message : "Unknown error"}`));
1953
+ console.error(chalk13.red(`Failed to pop stash: ${error instanceof Error ? error.message : "Unknown error"}`));
1993
1954
  process.exit(1);
1994
1955
  }
1995
1956
  return;
@@ -1998,9 +1959,9 @@ var stashCommand = new Command12("stash").description("Stash changes with AI-gen
1998
1959
  const index = typeof options.drop === "string" ? options.drop : "0";
1999
1960
  try {
2000
1961
  await git.stash(["drop", `stash@{${index}}`]);
2001
- console.log(chalk12.green(`\u2713 Dropped stash@{${index}}`));
1962
+ console.log(chalk13.green(`\u2713 Dropped stash@{${index}}`));
2002
1963
  } catch (error) {
2003
- console.error(chalk12.red(`Failed to drop stash: ${error instanceof Error ? error.message : "Unknown error"}`));
1964
+ console.error(chalk13.red(`Failed to drop stash: ${error instanceof Error ? error.message : "Unknown error"}`));
2004
1965
  process.exit(1);
2005
1966
  }
2006
1967
  return;
@@ -2012,20 +1973,20 @@ var stashCommand = new Command12("stash").description("Stash changes with AI-gen
2012
1973
  output: process.stdout
2013
1974
  });
2014
1975
  const answer = await new Promise((resolve) => {
2015
- rl.question(chalk12.yellow("Clear all stashes? This cannot be undone. (y/N) "), resolve);
1976
+ rl.question(chalk13.yellow("Clear all stashes? This cannot be undone. (y/N) "), resolve);
2016
1977
  });
2017
1978
  rl.close();
2018
1979
  if (answer.toLowerCase() === "y") {
2019
1980
  await git.stash(["clear"]);
2020
- console.log(chalk12.green("\u2713 Cleared all stashes"));
1981
+ console.log(chalk13.green("\u2713 Cleared all stashes"));
2021
1982
  } else {
2022
- console.log(chalk12.gray("Cancelled"));
1983
+ console.log(chalk13.gray("Cancelled"));
2023
1984
  }
2024
1985
  return;
2025
1986
  }
2026
1987
  const status = await git.status();
2027
1988
  if (status.isClean()) {
2028
- console.log(chalk12.yellow("No changes to stash"));
1989
+ console.log(chalk13.yellow("No changes to stash"));
2029
1990
  return;
2030
1991
  }
2031
1992
  let stashName;
@@ -2039,9 +2000,11 @@ var stashCommand = new Command12("stash").description("Stash changes with AI-gen
2039
2000
  if (!fullDiff.trim()) {
2040
2001
  stashName = `WIP: untracked files (${status.not_added.length} files)`;
2041
2002
  } else {
2042
- const spinner = ora11("Generating stash name...").start();
2003
+ const spinner = ora12("Generating stash name...").start();
2043
2004
  try {
2044
- stashName = await generateStashName(fullDiff, { provider, model: options.model });
2005
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2006
+ const template = findTemplate(repoRoot.trim(), "stash");
2007
+ stashName = await generateStashName(fullDiff, { provider, model: options.model }, template || void 0);
2045
2008
  spinner.stop();
2046
2009
  } catch (error) {
2047
2010
  spinner.fail("Failed to generate name, using default");
@@ -2050,23 +2013,298 @@ var stashCommand = new Command12("stash").description("Stash changes with AI-gen
2050
2013
  }
2051
2014
  }
2052
2015
  await git.stash(["push", "-u", "-m", stashName]);
2053
- console.log(chalk12.green(`\u2713 Stashed: ${stashName}`));
2016
+ console.log(chalk13.green(`\u2713 Stashed: ${stashName}`));
2017
+ });
2018
+
2019
+ // src/commands/summary.ts
2020
+ import { Command as Command14 } from "commander";
2021
+ import chalk14 from "chalk";
2022
+ import ora13 from "ora";
2023
+ import { simpleGit as simpleGit13 } from "simple-git";
2024
+ var summaryCommand = new Command14("summary").description("Generate a work summary from your commits (for daily/weekly reports)").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("--since <date>", "Start date (default: today)", "today").option("--until <date>", "End date").option("--author <author>", "Filter by author (default: current user)").option("--daily", "Generate daily report (alias for --since today)").option("--weekly", 'Generate weekly report (alias for --since "1 week ago")').option("--with-diff", "Include diff analysis for more detail").option("--markdown", "Output as markdown").option("--json", "Output as JSON").option("--copy", "Copy to clipboard").action(async (options) => {
2025
+ const git = simpleGit13();
2026
+ const isRepo = await git.checkIsRepo();
2027
+ if (!isRepo) {
2028
+ console.error(chalk14.red("Error: Not a git repository"));
2029
+ process.exit(1);
2030
+ }
2031
+ const provider = options.provider.toLowerCase();
2032
+ const spinner = ora13("Generating summary...").start();
2033
+ try {
2034
+ let author = options.author;
2035
+ if (!author) {
2036
+ const config = await git.listConfig();
2037
+ author = config.all["user.name"] || "";
2038
+ if (!author) {
2039
+ spinner.fail("Could not determine git user. Use --author to specify.");
2040
+ process.exit(1);
2041
+ }
2042
+ }
2043
+ let since = options.since;
2044
+ let format = "custom";
2045
+ if (options.daily) {
2046
+ since = "today";
2047
+ format = "daily";
2048
+ } else if (options.weekly) {
2049
+ since = "1 week ago";
2050
+ format = "weekly";
2051
+ } else if (since === "today") {
2052
+ format = "daily";
2053
+ }
2054
+ const sinceDate = resolveDate(since);
2055
+ spinner.text = `Fetching commits by ${author} since ${since}...`;
2056
+ const logOptions = [`--author=${author}`, `--since=${sinceDate}`];
2057
+ if (options.until) {
2058
+ logOptions.push(`--until=${resolveDate(options.until)}`);
2059
+ }
2060
+ const log = await git.log(logOptions);
2061
+ if (log.all.length === 0) {
2062
+ spinner.info(`No commits found for ${author} since ${since}`);
2063
+ process.exit(0);
2064
+ }
2065
+ spinner.text = `Analyzing ${log.all.length} commits...`;
2066
+ let diff;
2067
+ if (options.withDiff && log.all.length > 0) {
2068
+ const oldest = log.all[log.all.length - 1].hash;
2069
+ const newest = log.all[0].hash;
2070
+ try {
2071
+ diff = await git.diff([`${oldest}^`, newest]);
2072
+ } catch {
2073
+ }
2074
+ }
2075
+ const commits = log.all.map((c) => ({
2076
+ hash: c.hash,
2077
+ message: c.message,
2078
+ date: c.date
2079
+ }));
2080
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2081
+ const template = findTemplate(repoRoot.trim(), "summary");
2082
+ const summary = await generateWorkSummary(
2083
+ { commits, author, since, until: options.until, diff },
2084
+ { provider, model: options.model },
2085
+ format,
2086
+ template || void 0
2087
+ );
2088
+ spinner.stop();
2089
+ if (options.json) {
2090
+ console.log(JSON.stringify(summary, null, 2));
2091
+ return;
2092
+ }
2093
+ const output = options.markdown ? formatMarkdown(summary, author, since, options.until) : null;
2094
+ if (options.copy) {
2095
+ const textToCopy = output || formatMarkdown(summary, author, since, options.until);
2096
+ const { execSync: execSync5 } = await import("child_process");
2097
+ try {
2098
+ execSync5("pbcopy", { input: textToCopy });
2099
+ console.log(chalk14.green("Summary copied to clipboard!"));
2100
+ console.log();
2101
+ } catch {
2102
+ console.log(chalk14.yellow("Could not copy to clipboard"));
2103
+ }
2104
+ }
2105
+ if (options.markdown) {
2106
+ console.log(output);
2107
+ } else {
2108
+ printSummary(summary, author, since, options.until);
2109
+ }
2110
+ } catch (error) {
2111
+ spinner.fail("Failed to generate summary");
2112
+ console.error(chalk14.red(error instanceof Error ? error.message : "Unknown error"));
2113
+ process.exit(1);
2114
+ }
2115
+ });
2116
+ function formatMarkdown(summary, author, since, until) {
2117
+ const lines = [];
2118
+ const period = until ? `${since} - ${until}` : `${since} - now`;
2119
+ lines.push(`# ${summary.title}`);
2120
+ lines.push("");
2121
+ lines.push(`**Author:** ${author}`);
2122
+ lines.push(`**Period:** ${period}`);
2123
+ if (summary.stats) {
2124
+ lines.push(`**Commits:** ${summary.stats.commits}`);
2125
+ }
2126
+ lines.push("");
2127
+ lines.push("## Overview");
2128
+ lines.push(summary.overview);
2129
+ lines.push("");
2130
+ if (summary.highlights.length > 0) {
2131
+ lines.push("## Highlights");
2132
+ for (const highlight of summary.highlights) {
2133
+ lines.push(`- ${highlight}`);
2134
+ }
2135
+ lines.push("");
2136
+ }
2137
+ if (summary.details.length > 0) {
2138
+ lines.push("## Details");
2139
+ for (const section of summary.details) {
2140
+ lines.push(`### ${section.category}`);
2141
+ for (const item of section.items) {
2142
+ lines.push(`- ${item}`);
2143
+ }
2144
+ lines.push("");
2145
+ }
2146
+ }
2147
+ return lines.join("\n");
2148
+ }
2149
+ function resolveDate(dateStr) {
2150
+ const now = /* @__PURE__ */ new Date();
2151
+ if (dateStr === "today") {
2152
+ return formatDate(now);
2153
+ } else if (dateStr === "yesterday") {
2154
+ const d = new Date(now);
2155
+ d.setDate(d.getDate() - 1);
2156
+ return formatDate(d);
2157
+ } else if (dateStr.match(/^(\d+)\s+(day|days)\s+ago$/i)) {
2158
+ const match = dateStr.match(/^(\d+)\s+(day|days)\s+ago$/i);
2159
+ const days = parseInt(match[1], 10);
2160
+ const d = new Date(now);
2161
+ d.setDate(d.getDate() - days);
2162
+ return formatDate(d);
2163
+ } else if (dateStr.match(/^(\d+)\s+(week|weeks)\s+ago$/i)) {
2164
+ const match = dateStr.match(/^(\d+)\s+(week|weeks)\s+ago$/i);
2165
+ const weeks = parseInt(match[1], 10);
2166
+ const d = new Date(now);
2167
+ d.setDate(d.getDate() - weeks * 7);
2168
+ return formatDate(d);
2169
+ }
2170
+ return dateStr;
2171
+ }
2172
+ function formatDate(d) {
2173
+ const year = d.getFullYear();
2174
+ const month = String(d.getMonth() + 1).padStart(2, "0");
2175
+ const day = String(d.getDate()).padStart(2, "0");
2176
+ return `${year}-${month}-${day} 00:00:00`;
2177
+ }
2178
+ function printSummary(summary, author, since, until) {
2179
+ const period = until ? `${since} - ${until}` : `${since} - now`;
2180
+ console.log(chalk14.bold(`
2181
+ \u{1F4CA} ${summary.title}
2182
+ `));
2183
+ console.log(chalk14.gray(`Author: ${author}`));
2184
+ console.log(chalk14.gray(`Period: ${period}`));
2185
+ if (summary.stats) {
2186
+ console.log(chalk14.gray(`Commits: ${summary.stats.commits}`));
2187
+ }
2188
+ console.log();
2189
+ console.log(chalk14.cyan("Overview:"));
2190
+ console.log(` ${summary.overview}`);
2191
+ console.log();
2192
+ if (summary.highlights.length > 0) {
2193
+ console.log(chalk14.cyan("Highlights:"));
2194
+ for (const highlight of summary.highlights) {
2195
+ console.log(` ${chalk14.green("\u2605")} ${highlight}`);
2196
+ }
2197
+ console.log();
2198
+ }
2199
+ if (summary.details.length > 0) {
2200
+ console.log(chalk14.cyan("Details:"));
2201
+ for (const section of summary.details) {
2202
+ console.log(` ${chalk14.yellow(section.category)}`);
2203
+ for (const item of section.items) {
2204
+ console.log(` \u2022 ${item}`);
2205
+ }
2206
+ }
2207
+ console.log();
2208
+ }
2209
+ }
2210
+
2211
+ // src/commands/config.ts
2212
+ import { Command as Command15 } from "commander";
2213
+ import chalk15 from "chalk";
2214
+ var configCommand = new Command15("config").description("Manage gut configuration");
2215
+ configCommand.command("set <key> <value>").description("Set a configuration value").option("--local", "Set for current repository only").action((key, value, options) => {
2216
+ if (key === "lang") {
2217
+ if (!isValidLanguage(value)) {
2218
+ console.error(chalk15.red(`Invalid language: ${value}`));
2219
+ console.error(chalk15.gray(`Valid languages: ${VALID_LANGUAGES.join(", ")}`));
2220
+ process.exit(1);
2221
+ }
2222
+ try {
2223
+ setLanguage(value, options.local ?? false);
2224
+ const scope = options.local ? "(local)" : "(global)";
2225
+ console.log(chalk15.green(`\u2713 Language set to: ${value} ${scope}`));
2226
+ } catch (err) {
2227
+ console.error(chalk15.red(err.message));
2228
+ process.exit(1);
2229
+ }
2230
+ } else {
2231
+ console.error(chalk15.red(`Unknown config key: ${key}`));
2232
+ console.error(chalk15.gray("Available keys: lang"));
2233
+ process.exit(1);
2234
+ }
2235
+ });
2236
+ configCommand.command("get <key>").description("Get a configuration value").action((key) => {
2237
+ const config = getConfig();
2238
+ if (key in config) {
2239
+ console.log(config[key]);
2240
+ } else {
2241
+ console.error(chalk15.red(`Unknown config key: ${key}`));
2242
+ process.exit(1);
2243
+ }
2244
+ });
2245
+ configCommand.command("list").description("List all configuration values").action(() => {
2246
+ const globalConfig = getGlobalConfig();
2247
+ const localConfig = getLocalConfig();
2248
+ const effectiveConfig = getConfig();
2249
+ console.log(chalk15.bold("Configuration:"));
2250
+ console.log();
2251
+ for (const key of Object.keys(effectiveConfig)) {
2252
+ const value = effectiveConfig[key];
2253
+ const isLocal = key in localConfig;
2254
+ const scope = isLocal ? chalk15.cyan(" (local)") : chalk15.gray(" (global)");
2255
+ console.log(` ${chalk15.cyan(key)}: ${value}${scope}`);
2256
+ }
2257
+ if (Object.keys(localConfig).length > 0) {
2258
+ console.log();
2259
+ console.log(chalk15.gray("Local config: .gut/config.json"));
2260
+ }
2261
+ });
2262
+
2263
+ // src/commands/lang.ts
2264
+ import { Command as Command16 } from "commander";
2265
+ import chalk16 from "chalk";
2266
+ var langCommand = new Command16("lang").description("Set or show output language").argument("[language]", `Language to set (${VALID_LANGUAGES.join(", ")})`).option("--local", "Set for current repository only").action((language, options) => {
2267
+ if (!language) {
2268
+ const lang = getLanguage();
2269
+ const localConfig = getLocalConfig();
2270
+ const isLocal = "lang" in localConfig;
2271
+ const scope = isLocal ? chalk16.cyan("(local)") : chalk16.gray("(global)");
2272
+ console.log(`${lang} ${scope}`);
2273
+ return;
2274
+ }
2275
+ if (!isValidLanguage(language)) {
2276
+ console.error(chalk16.red(`Invalid language: ${language}`));
2277
+ console.error(chalk16.gray(`Valid languages: ${VALID_LANGUAGES.join(", ")}`));
2278
+ process.exit(1);
2279
+ }
2280
+ try {
2281
+ setLanguage(language, options.local ?? false);
2282
+ const scope = options.local ? "(local)" : "(global)";
2283
+ console.log(chalk16.green(`\u2713 Language set to: ${language} ${scope}`));
2284
+ } catch (err) {
2285
+ console.error(chalk16.red(err.message));
2286
+ process.exit(1);
2287
+ }
2054
2288
  });
2055
2289
 
2056
2290
  // src/index.ts
2057
- var program = new Command13();
2291
+ var program = new Command17();
2058
2292
  program.name("gut").description("Git Utility Tool - AI-powered git commands").version("0.1.0");
2059
2293
  program.addCommand(cleanupCommand);
2060
2294
  program.addCommand(authCommand);
2061
- program.addCommand(aiCommitCommand);
2062
- program.addCommand(aiPrCommand);
2063
- program.addCommand(aiReviewCommand);
2064
- program.addCommand(aiMergeCommand);
2295
+ program.addCommand(commitCommand);
2296
+ program.addCommand(prCommand);
2297
+ program.addCommand(reviewCommand);
2298
+ program.addCommand(mergeCommand);
2065
2299
  program.addCommand(changelogCommand);
2066
- program.addCommand(aiExplainCommand);
2067
- program.addCommand(aiFindCommand);
2068
- program.addCommand(aiBranchCommand);
2300
+ program.addCommand(explainCommand);
2301
+ program.addCommand(findCommand);
2302
+ program.addCommand(branchCommand);
2303
+ program.addCommand(checkoutCommand);
2069
2304
  program.addCommand(syncCommand);
2070
2305
  program.addCommand(stashCommand);
2306
+ program.addCommand(summaryCommand);
2307
+ program.addCommand(configCommand);
2308
+ program.addCommand(langCommand);
2071
2309
  program.parse();
2072
2310
  //# sourceMappingURL=index.js.map