gut-cli 0.1.18 → 0.1.20

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
@@ -288,152 +288,66 @@ import { createOpenAI } from "@ai-sdk/openai";
288
288
  import { createAnthropic } from "@ai-sdk/anthropic";
289
289
  import { createOllama } from "ollama-ai-provider";
290
290
  import { z } from "zod";
291
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
292
- import { join as join2, dirname } from "path";
293
- import { fileURLToPath } from "url";
294
-
295
- // src/lib/config.ts
291
+ import { existsSync, readFileSync } from "fs";
292
+ import { join, dirname } from "path";
296
293
  import { homedir } from "os";
297
- import { join } from "path";
298
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
299
- import { execSync } from "child_process";
300
- var DEFAULT_CONFIG = {
301
- lang: "en"
302
- };
303
- function getGlobalConfigPath() {
304
- const configDir = join(homedir(), ".config", "gut");
305
- return join(configDir, "config.json");
306
- }
307
- function getRepoRoot() {
308
- try {
309
- return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
310
- } catch {
311
- return null;
312
- }
313
- }
314
- function getLocalConfigPath() {
315
- const repoRoot = getRepoRoot();
316
- if (!repoRoot) return null;
317
- return join(repoRoot, ".gut", "config.json");
318
- }
319
- function ensureGlobalConfigDir() {
320
- const configDir = join(homedir(), ".config", "gut");
321
- if (!existsSync(configDir)) {
322
- mkdirSync(configDir, { recursive: true });
323
- }
324
- }
325
- function ensureLocalConfigDir() {
326
- const repoRoot = getRepoRoot();
327
- if (!repoRoot) return;
328
- const gutDir = join(repoRoot, ".gut");
329
- if (!existsSync(gutDir)) {
330
- mkdirSync(gutDir, { recursive: true });
331
- }
332
- }
333
- function readConfigFile(path2) {
334
- if (!existsSync(path2)) return {};
335
- try {
336
- return JSON.parse(readFileSync(path2, "utf-8"));
337
- } catch {
338
- return {};
339
- }
340
- }
341
- function getGlobalConfig() {
342
- const globalPath = getGlobalConfigPath();
343
- return { ...DEFAULT_CONFIG, ...readConfigFile(globalPath) };
344
- }
345
- function getLocalConfig() {
346
- const localPath = getLocalConfigPath();
347
- if (!localPath) return {};
348
- return readConfigFile(localPath);
349
- }
350
- function getConfig() {
351
- const globalConfig = getGlobalConfig();
352
- const localConfig = getLocalConfig();
353
- return { ...globalConfig, ...localConfig };
354
- }
355
- function setGlobalConfig(key, value) {
356
- ensureGlobalConfigDir();
357
- const config = getGlobalConfig();
358
- config[key] = value;
359
- writeFileSync(getGlobalConfigPath(), JSON.stringify(config, null, 2));
360
- }
361
- function setLocalConfig(key, value) {
362
- const localPath = getLocalConfigPath();
363
- if (!localPath) {
364
- throw new Error("Not in a git repository");
365
- }
366
- ensureLocalConfigDir();
367
- const config = getLocalConfig();
368
- config[key] = value;
369
- writeFileSync(localPath, JSON.stringify(config, null, 2));
370
- }
371
- function getLanguage() {
372
- return getConfig().lang;
373
- }
374
- function setLanguage(lang, local = false) {
375
- if (local) {
376
- setLocalConfig("lang", lang);
377
- } else {
378
- setGlobalConfig("lang", lang);
379
- }
380
- }
381
- function getLanguageInstruction(lang) {
382
- switch (lang) {
383
- case "ja":
384
- return "\n\nIMPORTANT: Respond in Japanese (\u65E5\u672C\u8A9E\u3067\u56DE\u7B54\u3057\u3066\u304F\u3060\u3055\u3044).";
385
- case "en":
386
- default:
387
- return "";
388
- }
389
- }
390
- var VALID_LANGUAGES = ["en", "ja"];
391
- function isValidLanguage(lang) {
392
- return VALID_LANGUAGES.includes(lang);
393
- }
394
-
395
- // src/lib/ai.ts
294
+ import { fileURLToPath } from "url";
396
295
  var __filename = fileURLToPath(import.meta.url);
397
296
  var __dirname = dirname(__filename);
398
297
  function findGutRoot() {
399
298
  let current = __dirname;
400
299
  for (let i = 0; i < 5; i++) {
401
- const gutPath = join2(current, ".gut");
402
- if (existsSync2(gutPath)) {
300
+ const gutPath = join(current, ".gut");
301
+ if (existsSync(gutPath)) {
403
302
  return current;
404
303
  }
405
304
  current = dirname(current);
406
305
  }
407
- return join2(__dirname, "..");
306
+ return join(__dirname, "..");
408
307
  }
409
308
  var GUT_ROOT = findGutRoot();
410
309
  function loadTemplate(name) {
411
- const templatePath = join2(GUT_ROOT, ".gut", `${name}.md`);
412
- if (existsSync2(templatePath)) {
413
- return readFileSync2(templatePath, "utf-8");
310
+ const templatePath = join(GUT_ROOT, ".gut", `${name}.md`);
311
+ if (existsSync(templatePath)) {
312
+ return readFileSync(templatePath, "utf-8");
414
313
  }
415
314
  throw new Error(`Template not found: ${templatePath}`);
416
315
  }
417
- function findTemplate(repoRoot, templateName) {
418
- const templatePath = join2(repoRoot, ".gut", `${templateName}.md`);
419
- if (existsSync2(templatePath)) {
420
- return readFileSync2(templatePath, "utf-8");
316
+ function getGlobalTemplatesDir() {
317
+ return join(homedir(), ".config", "gut", "templates");
318
+ }
319
+ function findGlobalTemplate(templateName) {
320
+ const templatePath = join(getGlobalTemplatesDir(), `${templateName}.md`);
321
+ if (existsSync(templatePath)) {
322
+ return readFileSync(templatePath, "utf-8");
421
323
  }
422
324
  return null;
423
325
  }
424
- function applyTemplate(userTemplate, templateName, variables) {
425
- const langInstruction = getLanguageInstruction(getLanguage());
426
- let result = userTemplate || loadTemplate(templateName);
427
- result = result.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, content) => {
428
- return variables[key] ? content : "";
429
- });
430
- for (const [key, value] of Object.entries(variables)) {
431
- result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value || "");
326
+ function findTemplate(repoRoot, templateName) {
327
+ const projectTemplatePath = join(repoRoot, ".gut", `${templateName}.md`);
328
+ if (existsSync(projectTemplatePath)) {
329
+ return readFileSync(projectTemplatePath, "utf-8");
432
330
  }
433
- if (langInstruction) {
434
- result += langInstruction;
331
+ const globalTemplate = findGlobalTemplate(templateName);
332
+ if (globalTemplate) {
333
+ return globalTemplate;
435
334
  }
436
- return result;
335
+ return null;
336
+ }
337
+ function buildPrompt(userTemplate, templateName, context, language) {
338
+ let contextXml = "<context>\n";
339
+ for (const [key, value] of Object.entries(context)) {
340
+ if (value) {
341
+ contextXml += `<${key}>
342
+ ${value}
343
+ </${key}>
344
+ `;
345
+ }
346
+ }
347
+ contextXml += "</context>\n\n";
348
+ const template = userTemplate || loadTemplate(templateName);
349
+ const langInstruction = language === "ja" ? "\n\nIMPORTANT: Respond in Japanese (\u65E5\u672C\u8A9E\u3067\u56DE\u7B54\u3057\u3066\u304F\u3060\u3055\u3044)." : "";
350
+ return contextXml + "<instructions>\n" + template + langInstruction + "\n</instructions>";
437
351
  }
438
352
  var DEFAULT_MODELS = {
439
353
  gemini: "gemini-2.0-flash",
@@ -481,9 +395,9 @@ async function getModel(options) {
481
395
  }
482
396
  async function generateCommitMessage(diff, options, template) {
483
397
  const model = await getModel(options);
484
- const prompt = applyTemplate(template, "commit", {
398
+ const prompt = buildPrompt(template, "commit", {
485
399
  diff: diff.slice(0, 8e3)
486
- });
400
+ }, options.language);
487
401
  const result = await generateText({
488
402
  model,
489
403
  prompt,
@@ -493,12 +407,12 @@ async function generateCommitMessage(diff, options, template) {
493
407
  }
494
408
  async function generatePRDescription(context, options, template) {
495
409
  const model = await getModel(options);
496
- const prompt = applyTemplate(template, "pr", {
410
+ const prompt = buildPrompt(template, "pr", {
497
411
  baseBranch: context.baseBranch,
498
412
  currentBranch: context.currentBranch,
499
413
  commits: context.commits.map((c) => `- ${c}`).join("\n"),
500
414
  diff: context.diff.slice(0, 6e3)
501
- });
415
+ }, options.language);
502
416
  const result = await generateText({
503
417
  model,
504
418
  prompt,
@@ -529,9 +443,9 @@ var CodeReviewSchema = z.object({
529
443
  });
530
444
  async function generateCodeReview(diff, options, template) {
531
445
  const model = await getModel(options);
532
- const prompt = applyTemplate(template, "review", {
446
+ const prompt = buildPrompt(template, "review", {
533
447
  diff: diff.slice(0, 1e4)
534
- });
448
+ }, options.language);
535
449
  const result = await generateObject({
536
450
  model,
537
451
  schema: CodeReviewSchema,
@@ -553,13 +467,13 @@ var ChangelogSchema = z.object({
553
467
  async function generateChangelog(context, options, template) {
554
468
  const model = await getModel(options);
555
469
  const commitList = context.commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message} (${c.author})`).join("\n");
556
- const prompt = applyTemplate(template, "changelog", {
470
+ const prompt = buildPrompt(template, "changelog", {
557
471
  fromRef: context.fromRef,
558
472
  toRef: context.toRef,
559
473
  commits: commitList,
560
474
  diff: context.diff.slice(0, 8e3),
561
475
  todayDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
562
- });
476
+ }, options.language);
563
477
  const result = await generateObject({
564
478
  model,
565
479
  schema: ChangelogSchema,
@@ -587,10 +501,10 @@ var ExplanationSchema = z.object({
587
501
  async function generateExplanation(context, options, template) {
588
502
  const model = await getModel(options);
589
503
  if (context.type === "file-content") {
590
- const prompt2 = applyTemplate(template, "explain-file", {
504
+ const prompt2 = buildPrompt(template, "explain-file", {
591
505
  filePath: context.metadata.filePath || "",
592
506
  content: context.content?.slice(0, 15e3) || ""
593
- });
507
+ }, options.language);
594
508
  const result2 = await generateObject({
595
509
  model,
596
510
  schema: ExplanationSchema,
@@ -624,11 +538,11 @@ Author: ${context.metadata.author}
624
538
  Date: ${context.metadata.date}`;
625
539
  targetType = "commit";
626
540
  }
627
- const prompt = applyTemplate(template, "explain", {
541
+ const prompt = buildPrompt(template, "explain", {
628
542
  targetType,
629
- context: contextInfo,
543
+ contextInfo,
630
544
  diff: context.diff?.slice(0, 12e3) || ""
631
- });
545
+ }, options.language);
632
546
  const result = await generateObject({
633
547
  model,
634
548
  schema: ExplanationSchema,
@@ -648,11 +562,11 @@ var CommitSearchSchema = z.object({
648
562
  async function searchCommits(query, commits, options, maxResults = 5, template) {
649
563
  const model = await getModel(options);
650
564
  const commitList = commits.map((c) => `${c.hash.slice(0, 7)} | ${c.author} | ${c.date.split("T")[0]} | ${c.message.split("\n")[0]}`).join("\n");
651
- const prompt = applyTemplate(template, "find", {
565
+ const prompt = buildPrompt(template, "find", {
652
566
  query,
653
567
  commits: commitList,
654
568
  maxResults: String(maxResults)
655
- });
569
+ }, options.language);
656
570
  const result = await generateObject({
657
571
  model,
658
572
  schema: CommitSearchSchema,
@@ -681,11 +595,11 @@ async function searchCommits(query, commits, options, maxResults = 5, template)
681
595
  }
682
596
  async function generateBranchName(description, options, context, template) {
683
597
  const model = await getModel(options);
684
- const prompt = applyTemplate(template, "branch", {
598
+ const prompt = buildPrompt(template, "branch", {
685
599
  description,
686
600
  type: context?.type,
687
601
  issue: context?.issue
688
- });
602
+ }, options.language);
689
603
  const result = await generateText({
690
604
  model,
691
605
  prompt,
@@ -695,9 +609,9 @@ async function generateBranchName(description, options, context, template) {
695
609
  }
696
610
  async function generateBranchNameFromDiff(diff, options, template) {
697
611
  const model = await getModel(options);
698
- const prompt = applyTemplate(template, "checkout", {
612
+ const prompt = buildPrompt(template, "checkout", {
699
613
  diff: diff.slice(0, 8e3)
700
- });
614
+ }, options.language);
701
615
  const result = await generateText({
702
616
  model,
703
617
  prompt,
@@ -707,9 +621,9 @@ async function generateBranchNameFromDiff(diff, options, template) {
707
621
  }
708
622
  async function generateStashName(diff, options, template) {
709
623
  const model = await getModel(options);
710
- const prompt = applyTemplate(template, "stash", {
624
+ const prompt = buildPrompt(template, "stash", {
711
625
  diff: diff.slice(0, 4e3)
712
- });
626
+ }, options.language);
713
627
  const result = await generateText({
714
628
  model,
715
629
  prompt,
@@ -739,13 +653,13 @@ async function generateWorkSummary(context, options, format = "custom", template
739
653
  const commitList = context.commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message.split("\n")[0]} (${c.date.split("T")[0]})`).join("\n");
740
654
  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}` : ""}.`;
741
655
  const period = `${context.since}${context.until ? ` to ${context.until}` : " to now"}`;
742
- const prompt = applyTemplate(template, "summary", {
656
+ const prompt = buildPrompt(template, "summary", {
743
657
  author: context.author,
744
658
  period,
745
659
  format: formatHint,
746
660
  commits: commitList,
747
661
  diff: context.diff?.slice(0, 6e3)
748
- });
662
+ }, options.language);
749
663
  const result = await generateObject({
750
664
  model,
751
665
  schema: WorkSummarySchema,
@@ -761,12 +675,12 @@ async function generateWorkSummary(context, options, format = "custom", template
761
675
  }
762
676
  async function resolveConflict(conflictedContent, context, options, template) {
763
677
  const model = await getModel(options);
764
- const prompt = applyTemplate(template, "merge", {
678
+ const prompt = buildPrompt(template, "merge", {
765
679
  filename: context.filename,
766
680
  oursRef: context.oursRef,
767
681
  theirsRef: context.theirsRef,
768
682
  content: conflictedContent
769
- });
683
+ }, options.language);
770
684
  const result = await generateObject({
771
685
  model,
772
686
  schema: ConflictResolutionSchema,
@@ -776,11 +690,11 @@ async function resolveConflict(conflictedContent, context, options, template) {
776
690
  }
777
691
  async function generateGitignore(context, options, template) {
778
692
  const model = await getModel(options);
779
- const prompt = applyTemplate(template, "gitignore", {
693
+ const prompt = buildPrompt(template, "gitignore", {
780
694
  files: context.files,
781
695
  configFiles: context.configFiles,
782
696
  existingGitignore: context.existingGitignore
783
- });
697
+ }, options.language);
784
698
  const result = await generateText({
785
699
  model,
786
700
  prompt,
@@ -852,14 +766,14 @@ var commitCommand = new Command3("commit").description("Generate a commit messag
852
766
  console.log(chalk3.green("\u2713 Committed successfully"));
853
767
  } else if (answer.toLowerCase() === "e") {
854
768
  console.log(chalk3.gray("Opening editor..."));
855
- const { execSync: execSync7 } = await import("child_process");
769
+ const { execSync: execSync9 } = await import("child_process");
856
770
  const editor = process.env.EDITOR || process.env.VISUAL || "vi";
857
771
  const fs2 = await import("fs");
858
772
  const os = await import("os");
859
773
  const path2 = await import("path");
860
774
  const tmpFile = path2.join(os.tmpdir(), "gut-commit-msg.txt");
861
775
  fs2.writeFileSync(tmpFile, message);
862
- execSync7(`${editor} "${tmpFile}"`, { stdio: "inherit" });
776
+ execSync9(`${editor} "${tmpFile}"`, { stdio: "inherit" });
863
777
  const editedMessage = fs2.readFileSync(tmpFile, "utf-8").trim();
864
778
  fs2.unlinkSync(tmpFile);
865
779
  if (editedMessage) {
@@ -886,12 +800,12 @@ import { Command as Command4 } from "commander";
886
800
  import chalk5 from "chalk";
887
801
  import ora3 from "ora";
888
802
  import { simpleGit as simpleGit3 } from "simple-git";
889
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
890
- import { join as join3 } from "path";
891
- import { execSync as execSync3 } from "child_process";
803
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
804
+ import { join as join2 } from "path";
805
+ import { execSync as execSync2 } from "child_process";
892
806
 
893
807
  // src/lib/gh.ts
894
- import { execSync as execSync2 } from "child_process";
808
+ import { execSync } from "child_process";
895
809
  import chalk4 from "chalk";
896
810
  var ghInstalledCache = null;
897
811
  function isGhCliInstalled() {
@@ -899,7 +813,7 @@ function isGhCliInstalled() {
899
813
  return ghInstalledCache;
900
814
  }
901
815
  try {
902
- execSync2("gh --version", { stdio: "pipe" });
816
+ execSync("gh --version", { stdio: "pipe" });
903
817
  ghInstalledCache = true;
904
818
  return true;
905
819
  } catch {
@@ -931,9 +845,9 @@ var GITHUB_PR_TEMPLATE_PATHS = [
931
845
  ];
932
846
  function findPRTemplate(repoRoot) {
933
847
  for (const templatePath of GITHUB_PR_TEMPLATE_PATHS) {
934
- const fullPath = join3(repoRoot, templatePath);
935
- if (existsSync3(fullPath)) {
936
- return readFileSync3(fullPath, "utf-8");
848
+ const fullPath = join2(repoRoot, templatePath);
849
+ if (existsSync2(fullPath)) {
850
+ return readFileSync2(fullPath, "utf-8");
937
851
  }
938
852
  }
939
853
  return findTemplate(repoRoot, "pr");
@@ -1001,7 +915,7 @@ var prCommand = new Command4("pr").description("Generate a pull request title an
1001
915
  const fullText = `${title}
1002
916
 
1003
917
  ${body}`;
1004
- execSync3("pbcopy", { input: fullText });
918
+ execSync2("pbcopy", { input: fullText });
1005
919
  console.log(chalk5.green("\n\u2713 Copied to clipboard"));
1006
920
  } catch {
1007
921
  console.log(chalk5.yellow("\n\u26A0 Could not copy to clipboard"));
@@ -1032,7 +946,7 @@ ${body}`;
1032
946
  try {
1033
947
  const escapedTitle = title.replace(/"/g, '\\"');
1034
948
  const escapedBody = body.replace(/"/g, '\\"');
1035
- execSync3(
949
+ execSync2(
1036
950
  `gh pr create --title "${escapedTitle}" --body "${escapedBody}" --base ${baseBranch}`,
1037
951
  { stdio: "pipe" }
1038
952
  );
@@ -1057,11 +971,11 @@ import { Command as Command5 } from "commander";
1057
971
  import chalk6 from "chalk";
1058
972
  import ora4 from "ora";
1059
973
  import { simpleGit as simpleGit4 } from "simple-git";
1060
- import { execSync as execSync4 } from "child_process";
974
+ import { execSync as execSync3 } from "child_process";
1061
975
  async function getPRDiff(prNumber) {
1062
976
  try {
1063
- const diff = execSync4(`gh pr diff ${prNumber}`, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
1064
- const prJsonStr = execSync4(`gh pr view ${prNumber} --json number,title,author,url`, { encoding: "utf-8" });
977
+ const diff = execSync3(`gh pr diff ${prNumber}`, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
978
+ const prJsonStr = execSync3(`gh pr view ${prNumber} --json number,title,author,url`, { encoding: "utf-8" });
1065
979
  const prJson = JSON.parse(prJsonStr);
1066
980
  return {
1067
981
  diff,
@@ -1392,8 +1306,8 @@ import { Command as Command8 } from "commander";
1392
1306
  import chalk9 from "chalk";
1393
1307
  import ora7 from "ora";
1394
1308
  import { simpleGit as simpleGit7 } from "simple-git";
1395
- import { execSync as execSync5 } from "child_process";
1396
- import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
1309
+ import { execSync as execSync4 } from "child_process";
1310
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
1397
1311
  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) => {
1398
1312
  const git = simpleGit7();
1399
1313
  const isRepo = await git.checkIsRepo();
@@ -1414,7 +1328,7 @@ var explainCommand = new Command8("explain").description("Get an AI-powered expl
1414
1328
  }
1415
1329
  } else {
1416
1330
  const isPR = target.match(/^#?\d+$/) || target.includes("/pull/");
1417
- const isFile = existsSync4(target);
1331
+ const isFile = existsSync3(target);
1418
1332
  if (isPR) {
1419
1333
  spinner.stop();
1420
1334
  if (!requireGhCli()) {
@@ -1505,7 +1419,7 @@ async function getCommitContext(hash, git, spinner) {
1505
1419
  }
1506
1420
  async function getFileContentContext(filePath, spinner) {
1507
1421
  spinner.text = `Reading ${filePath}...`;
1508
- const content = readFileSync5(filePath, "utf-8");
1422
+ const content = readFileSync4(filePath, "utf-8");
1509
1423
  return {
1510
1424
  type: "file-content",
1511
1425
  title: filePath,
@@ -1556,7 +1470,7 @@ async function getPRContext(target, spinner) {
1556
1470
  spinner.text = `Fetching PR #${prNumber}...`;
1557
1471
  let prInfo;
1558
1472
  try {
1559
- const prJson = execSync5(
1473
+ const prJson = execSync4(
1560
1474
  `gh pr view ${prNumber} --json title,url,baseRefName,headRefName,commits`,
1561
1475
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
1562
1476
  );
@@ -1567,7 +1481,7 @@ async function getPRContext(target, spinner) {
1567
1481
  spinner.text = `Getting diff for PR #${prNumber}...`;
1568
1482
  let diff;
1569
1483
  try {
1570
- diff = execSync5(`gh pr diff ${prNumber}`, {
1484
+ diff = execSync4(`gh pr diff ${prNumber}`, {
1571
1485
  encoding: "utf-8",
1572
1486
  stdio: ["pipe", "pipe", "pipe"],
1573
1487
  maxBuffer: 10 * 1024 * 1024
@@ -1729,10 +1643,10 @@ import { Command as Command10 } from "commander";
1729
1643
  import chalk11 from "chalk";
1730
1644
  import ora9 from "ora";
1731
1645
  import { simpleGit as simpleGit9 } from "simple-git";
1732
- import { execSync as execSync6 } from "child_process";
1646
+ import { execSync as execSync5 } from "child_process";
1733
1647
  function getIssueInfo(issueNumber) {
1734
1648
  try {
1735
- const result = execSync6(`gh issue view ${issueNumber} --json title,body`, {
1649
+ const result = execSync5(`gh issue view ${issueNumber} --json title,body`, {
1736
1650
  encoding: "utf-8",
1737
1651
  stdio: ["pipe", "pipe", "pipe"]
1738
1652
  });
@@ -1757,9 +1671,10 @@ var branchCommand = new Command10("branch").description("Generate a branch name
1757
1671
  if (!requireGhCli()) {
1758
1672
  process.exit(1);
1759
1673
  }
1760
- issueNumber = issue.replace(/^#/, "");
1761
- const spinner2 = ora9(`Fetching issue #${issueNumber}...`).start();
1762
- const issueInfo = getIssueInfo(issueNumber);
1674
+ const cleanedIssue = issue.replace(/^#/, "");
1675
+ issueNumber = cleanedIssue;
1676
+ const spinner2 = ora9(`Fetching issue #${cleanedIssue}...`).start();
1677
+ const issueInfo = getIssueInfo(cleanedIssue);
1763
1678
  if (!issueInfo) {
1764
1679
  spinner2.fail(`Could not fetch issue #${issueNumber}`);
1765
1680
  console.log(chalk11.gray("Make sure you are authenticated: gh auth login"));
@@ -2197,9 +2112,9 @@ var summaryCommand = new Command14("summary").description("Generate a work summa
2197
2112
  const output = options.markdown ? formatMarkdown(summary, author, since, options.until) : null;
2198
2113
  if (options.copy) {
2199
2114
  const textToCopy = output || formatMarkdown(summary, author, since, options.until);
2200
- const { execSync: execSync7 } = await import("child_process");
2115
+ const { execSync: execSync9 } = await import("child_process");
2201
2116
  try {
2202
- execSync7("pbcopy", { input: textToCopy });
2117
+ execSync9("pbcopy", { input: textToCopy });
2203
2118
  console.log(chalk15.green("Summary copied to clipboard!"));
2204
2119
  console.log();
2205
2120
  } catch {
@@ -2315,6 +2230,109 @@ function printSummary(summary, author, since, until) {
2315
2230
  // src/commands/config.ts
2316
2231
  import { Command as Command15 } from "commander";
2317
2232
  import chalk16 from "chalk";
2233
+ import { execSync as execSync7 } from "child_process";
2234
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
2235
+ import { join as join5 } from "path";
2236
+ import { homedir as homedir3 } from "os";
2237
+ import { simpleGit as simpleGit14 } from "simple-git";
2238
+
2239
+ // src/lib/config.ts
2240
+ import { homedir as homedir2 } from "os";
2241
+ import { join as join4 } from "path";
2242
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
2243
+ import { execSync as execSync6 } from "child_process";
2244
+ var DEFAULT_CONFIG = {
2245
+ lang: "en"
2246
+ };
2247
+ function getGlobalConfigPath() {
2248
+ const configDir = join4(homedir2(), ".config", "gut");
2249
+ return join4(configDir, "config.json");
2250
+ }
2251
+ function getRepoRoot() {
2252
+ try {
2253
+ return execSync6("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
2254
+ } catch {
2255
+ return null;
2256
+ }
2257
+ }
2258
+ function getLocalConfigPath() {
2259
+ const repoRoot = getRepoRoot();
2260
+ if (!repoRoot) return null;
2261
+ return join4(repoRoot, ".gut", "config.json");
2262
+ }
2263
+ function ensureGlobalConfigDir() {
2264
+ const configDir = join4(homedir2(), ".config", "gut");
2265
+ if (!existsSync4(configDir)) {
2266
+ mkdirSync(configDir, { recursive: true });
2267
+ }
2268
+ }
2269
+ function ensureLocalConfigDir() {
2270
+ const repoRoot = getRepoRoot();
2271
+ if (!repoRoot) return;
2272
+ const gutDir = join4(repoRoot, ".gut");
2273
+ if (!existsSync4(gutDir)) {
2274
+ mkdirSync(gutDir, { recursive: true });
2275
+ }
2276
+ }
2277
+ function readConfigFile(path2) {
2278
+ if (!existsSync4(path2)) return {};
2279
+ try {
2280
+ return JSON.parse(readFileSync5(path2, "utf-8"));
2281
+ } catch {
2282
+ return {};
2283
+ }
2284
+ }
2285
+ function getGlobalConfig() {
2286
+ const globalPath = getGlobalConfigPath();
2287
+ return { ...DEFAULT_CONFIG, ...readConfigFile(globalPath) };
2288
+ }
2289
+ function getLocalConfig() {
2290
+ const localPath = getLocalConfigPath();
2291
+ if (!localPath) return {};
2292
+ return readConfigFile(localPath);
2293
+ }
2294
+ function getConfig() {
2295
+ const globalConfig = getGlobalConfig();
2296
+ const localConfig = getLocalConfig();
2297
+ return { ...globalConfig, ...localConfig };
2298
+ }
2299
+ function setGlobalConfig(key, value) {
2300
+ ensureGlobalConfigDir();
2301
+ const config = getGlobalConfig();
2302
+ config[key] = value;
2303
+ writeFileSync2(getGlobalConfigPath(), JSON.stringify(config, null, 2));
2304
+ }
2305
+ function setLocalConfig(key, value) {
2306
+ const localPath = getLocalConfigPath();
2307
+ if (!localPath) {
2308
+ throw new Error("Not in a git repository");
2309
+ }
2310
+ ensureLocalConfigDir();
2311
+ const config = getLocalConfig();
2312
+ config[key] = value;
2313
+ writeFileSync2(localPath, JSON.stringify(config, null, 2));
2314
+ }
2315
+ function getLanguage() {
2316
+ return getConfig().lang;
2317
+ }
2318
+ function setLanguage(lang, local = false) {
2319
+ if (local) {
2320
+ setLocalConfig("lang", lang);
2321
+ } else {
2322
+ setGlobalConfig("lang", lang);
2323
+ }
2324
+ }
2325
+ var VALID_LANGUAGES = ["en", "ja"];
2326
+ function isValidLanguage(lang) {
2327
+ return VALID_LANGUAGES.includes(lang);
2328
+ }
2329
+
2330
+ // src/commands/config.ts
2331
+ function openFolder(path2) {
2332
+ const platform = process.platform;
2333
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? 'start ""' : "xdg-open";
2334
+ execSync7(`${cmd} "${path2}"`);
2335
+ }
2318
2336
  var configCommand = new Command15("config").description("Manage gut configuration");
2319
2337
  configCommand.command("set <key> <value>").description("Set a configuration value").option("--local", "Set for current repository only").action((key, value, options) => {
2320
2338
  if (key === "lang") {
@@ -2347,7 +2365,6 @@ configCommand.command("get <key>").description("Get a configuration value").acti
2347
2365
  }
2348
2366
  });
2349
2367
  configCommand.command("list").description("List all configuration values").action(() => {
2350
- const globalConfig = getGlobalConfig();
2351
2368
  const localConfig = getLocalConfig();
2352
2369
  const effectiveConfig = getConfig();
2353
2370
  console.log(chalk16.bold("Configuration:"));
@@ -2363,6 +2380,39 @@ configCommand.command("list").description("List all configuration values").actio
2363
2380
  console.log(chalk16.gray("Local config: .gut/config.json"));
2364
2381
  }
2365
2382
  });
2383
+ configCommand.command("open").description("Open configuration or templates folder").option("-t, --templates", "Open templates folder instead of config").option("-g, --global", "Open global folder (default)").option("-l, --local", "Open local/project folder").action(async (options) => {
2384
+ const git = simpleGit14();
2385
+ const isLocal = options.local === true;
2386
+ let targetPath;
2387
+ if (isLocal) {
2388
+ const isRepo = await git.checkIsRepo();
2389
+ if (!isRepo) {
2390
+ console.error(chalk16.red("Error: Not a git repository"));
2391
+ console.error(chalk16.gray("Use --global to open global config folder"));
2392
+ process.exit(1);
2393
+ }
2394
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2395
+ targetPath = join5(repoRoot.trim(), ".gut");
2396
+ } else {
2397
+ if (options.templates) {
2398
+ targetPath = join5(homedir3(), ".config", "gut", "templates");
2399
+ } else {
2400
+ targetPath = join5(homedir3(), ".config", "gut");
2401
+ }
2402
+ }
2403
+ if (!existsSync5(targetPath)) {
2404
+ mkdirSync2(targetPath, { recursive: true });
2405
+ console.log(chalk16.green(`Created ${targetPath}`));
2406
+ }
2407
+ try {
2408
+ openFolder(targetPath);
2409
+ console.log(chalk16.green(`Opened: ${targetPath}`));
2410
+ } catch (error) {
2411
+ console.error(chalk16.red(`Failed to open folder: ${targetPath}`));
2412
+ console.error(chalk16.gray(error.message));
2413
+ process.exit(1);
2414
+ }
2415
+ });
2366
2416
 
2367
2417
  // src/commands/lang.ts
2368
2418
  import { Command as Command16 } from "commander";
@@ -2395,17 +2445,24 @@ var langCommand = new Command16("lang").description("Set or show output language
2395
2445
  import { Command as Command17 } from "commander";
2396
2446
  import chalk18 from "chalk";
2397
2447
  import ora14 from "ora";
2398
- import { simpleGit as simpleGit14 } from "simple-git";
2399
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
2400
- import { join as join5, dirname as dirname2 } from "path";
2448
+ import { simpleGit as simpleGit15 } from "simple-git";
2449
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
2450
+ import { join as join6, dirname as dirname2 } from "path";
2451
+ import { homedir as homedir4 } from "os";
2401
2452
  import { fileURLToPath as fileURLToPath2 } from "url";
2453
+ import { execSync as execSync8 } from "child_process";
2402
2454
  import { generateText as generateText2 } from "ai";
2403
2455
  import { createGoogleGenerativeAI as createGoogleGenerativeAI2 } from "@ai-sdk/google";
2404
2456
  import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
2405
2457
  import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
2458
+ function openFolder2(path2) {
2459
+ const platform = process.platform;
2460
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? 'start ""' : "xdg-open";
2461
+ execSync8(`${cmd} "${path2}"`);
2462
+ }
2406
2463
  var __filename2 = fileURLToPath2(import.meta.url);
2407
2464
  var __dirname2 = dirname2(__filename2);
2408
- var GUT_ROOT2 = join5(__dirname2, "..");
2465
+ var GUT_ROOT2 = join6(__dirname2, "..");
2409
2466
  var TEMPLATE_FILES = [
2410
2467
  "branch.md",
2411
2468
  "changelog.md",
@@ -2443,6 +2500,8 @@ async function translateTemplate(content, targetLang, provider) {
2443
2500
  model = anthropic(modelName);
2444
2501
  break;
2445
2502
  }
2503
+ default:
2504
+ throw new Error(`Unsupported provider for translation: ${provider}`);
2446
2505
  }
2447
2506
  const langNames = {
2448
2507
  ja: "Japanese",
@@ -2468,20 +2527,28 @@ Translated template:`
2468
2527
  });
2469
2528
  return text.trim();
2470
2529
  }
2471
- var initCommand = new Command17("init").description("Initialize .gut/ templates in your project").option("-p, --provider <provider>", "AI provider for translation (gemini, openai, anthropic)", "gemini").option("-f, --force", "Overwrite existing templates").option("--no-translate", "Skip translation even if language is not English").action(async (options) => {
2472
- const git = simpleGit14();
2473
- const isRepo = await git.checkIsRepo();
2474
- if (!isRepo) {
2475
- console.error(chalk18.red("Error: Not a git repository"));
2476
- process.exit(1);
2530
+ var initCommand = new Command17("init").description("Initialize .gut/ templates in your project or globally").option("-p, --provider <provider>", "AI provider for translation (gemini, openai, anthropic)", "gemini").option("-f, --force", "Overwrite existing templates").option("-g, --global", "Initialize templates globally (~/.config/gut/templates/)").option("-o, --open", "Open the templates folder (can be used alone)").option("--no-translate", "Skip translation even if language is not English").action(async (options) => {
2531
+ const isGlobal = options.global === true;
2532
+ const git = simpleGit15();
2533
+ let targetDir;
2534
+ if (isGlobal) {
2535
+ targetDir = join6(homedir4(), ".config", "gut", "templates");
2536
+ } else {
2537
+ const isRepo = await git.checkIsRepo();
2538
+ if (!isRepo) {
2539
+ console.error(chalk18.red("Error: Not a git repository"));
2540
+ console.error(chalk18.gray("Use --global to initialize templates globally"));
2541
+ process.exit(1);
2542
+ }
2543
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2544
+ targetDir = join6(repoRoot.trim(), ".gut");
2477
2545
  }
2478
- const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2479
- const targetDir = join5(repoRoot.trim(), ".gut");
2480
- const sourceDir = join5(GUT_ROOT2, ".gut");
2481
- if (!existsSync5(targetDir)) {
2482
- mkdirSync2(targetDir, { recursive: true });
2546
+ if (!existsSync6(targetDir)) {
2547
+ mkdirSync3(targetDir, { recursive: true });
2483
2548
  console.log(chalk18.green(`Created ${targetDir}`));
2484
2549
  }
2550
+ console.log(chalk18.blue(isGlobal ? "Initializing global templates...\n" : "Initializing project templates...\n"));
2551
+ const sourceDir = join6(GUT_ROOT2, ".gut");
2485
2552
  const lang = getLanguage();
2486
2553
  const needsTranslation = options.translate !== false && lang !== "en";
2487
2554
  const provider = options.provider.toLowerCase();
@@ -2493,12 +2560,12 @@ var initCommand = new Command17("init").description("Initialize .gut/ templates
2493
2560
  let copied = 0;
2494
2561
  let skipped = 0;
2495
2562
  for (const filename of TEMPLATE_FILES) {
2496
- const sourcePath = join5(sourceDir, filename);
2497
- const targetPath = join5(targetDir, filename);
2498
- if (!existsSync5(sourcePath)) {
2563
+ const sourcePath = join6(sourceDir, filename);
2564
+ const targetPath = join6(targetDir, filename);
2565
+ if (!existsSync6(sourcePath)) {
2499
2566
  continue;
2500
2567
  }
2501
- if (existsSync5(targetPath) && !options.force) {
2568
+ if (existsSync6(targetPath) && !options.force) {
2502
2569
  console.log(chalk18.gray(` Skipped: ${filename} (already exists)`));
2503
2570
  skipped++;
2504
2571
  continue;
@@ -2522,21 +2589,37 @@ var initCommand = new Command17("init").description("Initialize .gut/ templates
2522
2589
  }
2523
2590
  console.log();
2524
2591
  if (copied > 0) {
2525
- console.log(chalk18.green(`\u2713 ${copied} template(s) initialized in .gut/`));
2592
+ const location = isGlobal ? "~/.config/gut/templates/" : ".gut/";
2593
+ console.log(chalk18.green(`\u2713 ${copied} template(s) initialized in ${location}`));
2526
2594
  }
2527
2595
  if (skipped > 0) {
2528
2596
  console.log(chalk18.gray(` ${skipped} template(s) skipped (use --force to overwrite)`));
2529
2597
  }
2530
- console.log(chalk18.gray("\nYou can now customize these templates for your project."));
2598
+ if (isGlobal) {
2599
+ console.log(chalk18.gray("\nGlobal templates will be used as fallback for all projects."));
2600
+ console.log(chalk18.gray("Project-level templates (.gut/) take priority over global templates."));
2601
+ } else {
2602
+ console.log(chalk18.gray("\nYou can now customize these templates for your project."));
2603
+ }
2604
+ if (options.open) {
2605
+ try {
2606
+ openFolder2(targetDir);
2607
+ console.log(chalk18.green(`
2608
+ Opened: ${targetDir}`));
2609
+ } catch (error) {
2610
+ console.error(chalk18.red(`
2611
+ Failed to open folder: ${targetDir}`));
2612
+ }
2613
+ }
2531
2614
  });
2532
2615
 
2533
2616
  // src/commands/gitignore.ts
2534
2617
  import { Command as Command18 } from "commander";
2535
2618
  import chalk19 from "chalk";
2536
2619
  import ora15 from "ora";
2537
- import { simpleGit as simpleGit15 } from "simple-git";
2538
- import { readdirSync as readdirSync2, readFileSync as readFileSync7, existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
2539
- import { join as join6 } from "path";
2620
+ import { simpleGit as simpleGit16 } from "simple-git";
2621
+ import { readdirSync, readFileSync as readFileSync7, existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
2622
+ import { join as join7 } from "path";
2540
2623
  var CONFIG_FILES = [
2541
2624
  // JavaScript/TypeScript
2542
2625
  "package.json",
@@ -2584,12 +2667,12 @@ function getFiles(dir, maxDepth = 3, currentDepth = 0) {
2584
2667
  if (currentDepth >= maxDepth) return [];
2585
2668
  const files = [];
2586
2669
  try {
2587
- const entries = readdirSync2(dir, { withFileTypes: true });
2670
+ const entries = readdirSync(dir, { withFileTypes: true });
2588
2671
  for (const entry of entries) {
2589
2672
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "vendor" || entry.name === "target" || entry.name === "__pycache__" || entry.name === "venv" || entry.name === ".venv") {
2590
2673
  continue;
2591
2674
  }
2592
- const fullPath = join6(dir, entry.name);
2675
+ const fullPath = join7(dir, entry.name);
2593
2676
  if (entry.isDirectory()) {
2594
2677
  files.push(entry.name + "/");
2595
2678
  const subFiles = getFiles(fullPath, maxDepth, currentDepth + 1);
@@ -2608,18 +2691,18 @@ function findConfigFiles(repoRoot) {
2608
2691
  if (configFile.includes("*")) {
2609
2692
  const ext = configFile.replace("*", "");
2610
2693
  try {
2611
- const entries = readdirSync2(repoRoot);
2694
+ const entries = readdirSync(repoRoot);
2612
2695
  for (const entry of entries) {
2613
2696
  if (entry.endsWith(ext)) {
2614
- const content = readFileSync7(join6(repoRoot, entry), "utf-8");
2697
+ const content = readFileSync7(join7(repoRoot, entry), "utf-8");
2615
2698
  found.set(entry, content.slice(0, 2e3));
2616
2699
  }
2617
2700
  }
2618
2701
  } catch {
2619
2702
  }
2620
2703
  } else {
2621
- const filePath = join6(repoRoot, configFile);
2622
- if (existsSync6(filePath)) {
2704
+ const filePath = join7(repoRoot, configFile);
2705
+ if (existsSync7(filePath)) {
2623
2706
  try {
2624
2707
  const content = readFileSync7(filePath, "utf-8");
2625
2708
  found.set(configFile, content.slice(0, 2e3));
@@ -2631,7 +2714,7 @@ function findConfigFiles(repoRoot) {
2631
2714
  return found;
2632
2715
  }
2633
2716
  var gitignoreCommand = new Command18("gitignore").description("Generate .gitignore from current codebase").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-o, --output <file>", "Output file (default: .gitignore)", ".gitignore").option("--stdout", "Print to stdout instead of file").option("-y, --yes", "Overwrite existing .gitignore without confirmation").action(async (options) => {
2634
- const git = simpleGit15();
2717
+ const git = simpleGit16();
2635
2718
  const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2636
2719
  const root = repoRoot.trim();
2637
2720
  const provider = options.provider.toLowerCase();
@@ -2642,9 +2725,9 @@ var gitignoreCommand = new Command18("gitignore").description("Generate .gitigno
2642
2725
  const spinner = ora15("Analyzing project structure...").start();
2643
2726
  const files = getFiles(root);
2644
2727
  const configFiles = findConfigFiles(root);
2645
- const gitignorePath = join6(root, options.output);
2728
+ const gitignorePath = join7(root, options.output);
2646
2729
  let existingGitignore;
2647
- if (existsSync6(gitignorePath)) {
2730
+ if (existsSync7(gitignorePath)) {
2648
2731
  existingGitignore = readFileSync7(gitignorePath, "utf-8");
2649
2732
  }
2650
2733
  let configFilesStr = "";
@@ -2679,7 +2762,7 @@ ${content}
2679
2762
  console.log(gitignoreContent);
2680
2763
  console.log(chalk19.gray("\u2500".repeat(50)));
2681
2764
  console.log();
2682
- if (existsSync6(gitignorePath) && !options.yes) {
2765
+ if (existsSync7(gitignorePath) && !options.yes) {
2683
2766
  const readline = await import("readline");
2684
2767
  const rl = readline.createInterface({
2685
2768
  input: process.stdin,