gut-cli 0.1.21 → 0.1.23

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
@@ -3,94 +3,138 @@
3
3
  // src/index.ts
4
4
  import { Command as Command19 } from "commander";
5
5
 
6
- // src/commands/cleanup.ts
7
- import { Command } from "commander";
6
+ // src/commands/auth.ts
8
7
  import chalk from "chalk";
9
- import ora from "ora";
10
- import { simpleGit } from "simple-git";
11
- var cleanupCommand = new Command("cleanup").description("Delete merged branches safely").option("-r, --remote", "Also delete remote branches").option("-f, --force", "Skip confirmation prompt").option("--dry-run", "Show branches that would be deleted without deleting").option("--base <branch>", "Base branch to compare against (default: main or master)").action(async (options) => {
12
- const git = simpleGit();
13
- const isRepo = await git.checkIsRepo();
14
- if (!isRepo) {
15
- console.error(chalk.red("Error: Not a git repository"));
16
- process.exit(1);
8
+ import { Command } from "commander";
9
+
10
+ // src/lib/credentials.ts
11
+ import { createRequire } from "module";
12
+
13
+ // src/lib/config.ts
14
+ import { execSync } from "child_process";
15
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
16
+ import { homedir } from "os";
17
+ import { join } from "path";
18
+ var DEFAULT_CONFIG = {
19
+ lang: "en"
20
+ };
21
+ var DEFAULT_MODELS = {
22
+ gemini: "gemini-2.5-flash",
23
+ openai: "gpt-4.1-mini",
24
+ anthropic: "claude-sonnet-4-5",
25
+ ollama: "llama3.3"
26
+ };
27
+ function getGlobalConfigPath() {
28
+ const configDir = join(homedir(), ".config", "gut");
29
+ return join(configDir, "config.json");
30
+ }
31
+ function getRepoRoot() {
32
+ try {
33
+ return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ function getLocalConfigPath() {
39
+ const repoRoot = getRepoRoot();
40
+ if (!repoRoot) return null;
41
+ return join(repoRoot, ".gut", "config.json");
42
+ }
43
+ function ensureGlobalConfigDir() {
44
+ const configDir = join(homedir(), ".config", "gut");
45
+ if (!existsSync(configDir)) {
46
+ mkdirSync(configDir, { recursive: true });
47
+ }
48
+ }
49
+ function ensureLocalConfigDir() {
50
+ const repoRoot = getRepoRoot();
51
+ if (!repoRoot) return;
52
+ const gutDir = join(repoRoot, ".gut");
53
+ if (!existsSync(gutDir)) {
54
+ mkdirSync(gutDir, { recursive: true });
17
55
  }
18
- const spinner = ora("Fetching branch information...").start();
56
+ }
57
+ function readConfigFile(path2) {
58
+ if (!existsSync(path2)) return {};
19
59
  try {
20
- await git.fetch(["--prune"]);
21
- const currentBranch = (await git.branch()).current;
22
- const baseBranch = options.base || await detectBaseBranch(git);
23
- spinner.text = `Using ${chalk.cyan(baseBranch)} as base branch`;
24
- const mergedResult = await git.branch(["--merged", baseBranch]);
25
- const mergedBranches = mergedResult.all.filter((branch) => {
26
- const cleanName = branch.trim().replace(/^\* /, "");
27
- return cleanName !== currentBranch && cleanName !== baseBranch && !cleanName.startsWith("remotes/") && cleanName !== "main" && cleanName !== "master" && cleanName !== "develop";
28
- });
29
- spinner.stop();
30
- if (mergedBranches.length === 0) {
31
- console.log(chalk.green("\u2713 No merged branches to clean up"));
32
- return;
33
- }
34
- console.log(chalk.yellow(`
35
- Found ${mergedBranches.length} merged branch(es):
36
- `));
37
- mergedBranches.forEach((branch) => {
38
- console.log(` ${chalk.red("\u2022")} ${branch}`);
39
- });
40
- if (options.dryRun) {
41
- console.log(chalk.blue("\n(dry-run mode - no branches were deleted)"));
42
- return;
43
- }
44
- if (!options.force) {
45
- const readline = await import("readline");
46
- const rl = readline.createInterface({
47
- input: process.stdin,
48
- output: process.stdout
49
- });
50
- const answer = await new Promise((resolve) => {
51
- rl.question(chalk.yellow("\nDelete these branches? (y/N) "), resolve);
52
- });
53
- rl.close();
54
- if (answer.toLowerCase() !== "y") {
55
- console.log(chalk.gray("Cancelled"));
56
- return;
57
- }
58
- }
59
- const deleteSpinner = ora("Deleting branches...").start();
60
- for (const branch of mergedBranches) {
61
- try {
62
- await git.deleteLocalBranch(branch, true);
63
- deleteSpinner.text = `Deleted ${branch}`;
64
- if (options.remote) {
65
- try {
66
- await git.push("origin", `:${branch}`);
67
- } catch {
68
- }
69
- }
70
- } catch (error) {
71
- deleteSpinner.warn(`Failed to delete ${branch}`);
72
- }
73
- }
74
- deleteSpinner.succeed(chalk.green(`Deleted ${mergedBranches.length} branch(es)`));
75
- } catch (error) {
76
- spinner.fail("Failed to cleanup branches");
77
- console.error(chalk.red(error instanceof Error ? error.message : "Unknown error"));
78
- process.exit(1);
60
+ return JSON.parse(readFileSync(path2, "utf-8"));
61
+ } catch {
62
+ return {};
63
+ }
64
+ }
65
+ function getGlobalConfig() {
66
+ const globalPath = getGlobalConfigPath();
67
+ return { ...DEFAULT_CONFIG, ...readConfigFile(globalPath) };
68
+ }
69
+ function getLocalConfig() {
70
+ const localPath = getLocalConfigPath();
71
+ if (!localPath) return {};
72
+ return readConfigFile(localPath);
73
+ }
74
+ function getConfig() {
75
+ const globalConfig = getGlobalConfig();
76
+ const localConfig = getLocalConfig();
77
+ return { ...globalConfig, ...localConfig };
78
+ }
79
+ function setGlobalConfig(key, value) {
80
+ ensureGlobalConfigDir();
81
+ const config = getGlobalConfig();
82
+ config[key] = value;
83
+ writeFileSync(getGlobalConfigPath(), JSON.stringify(config, null, 2));
84
+ }
85
+ function setLocalConfig(key, value) {
86
+ const localPath = getLocalConfigPath();
87
+ if (!localPath) {
88
+ throw new Error("Not in a git repository");
89
+ }
90
+ ensureLocalConfigDir();
91
+ const config = getLocalConfig();
92
+ config[key] = value;
93
+ writeFileSync(localPath, JSON.stringify(config, null, 2));
94
+ }
95
+ function getLanguage() {
96
+ return getConfig().lang;
97
+ }
98
+ function setLanguage(lang, local = false) {
99
+ if (local) {
100
+ setLocalConfig("lang", lang);
101
+ } else {
102
+ setGlobalConfig("lang", lang);
103
+ }
104
+ }
105
+ var VALID_LANGUAGES = ["en", "ja"];
106
+ function isValidLanguage(lang) {
107
+ return VALID_LANGUAGES.includes(lang);
108
+ }
109
+ function getConfiguredModel() {
110
+ return getConfig().model;
111
+ }
112
+ function setModel(model, local = false) {
113
+ if (local) {
114
+ setLocalConfig("model", model);
115
+ } else {
116
+ setGlobalConfig("model", model);
117
+ }
118
+ }
119
+ function getDefaultModel(provider) {
120
+ return DEFAULT_MODELS[provider] || DEFAULT_MODELS.gemini;
121
+ }
122
+ var VALID_PROVIDERS = ["gemini", "openai", "anthropic", "ollama"];
123
+ function isValidProvider(provider) {
124
+ return VALID_PROVIDERS.includes(provider);
125
+ }
126
+ function getConfiguredProvider() {
127
+ return getConfig().provider;
128
+ }
129
+ function setProvider(provider, local = false) {
130
+ if (local) {
131
+ setLocalConfig("provider", provider);
132
+ } else {
133
+ setGlobalConfig("provider", provider);
79
134
  }
80
- });
81
- async function detectBaseBranch(git) {
82
- const branches = await git.branch();
83
- if (branches.all.includes("main")) return "main";
84
- if (branches.all.includes("master")) return "master";
85
- return "main";
86
135
  }
87
-
88
- // src/commands/auth.ts
89
- import { Command as Command2 } from "commander";
90
- import chalk2 from "chalk";
91
136
 
92
137
  // src/lib/credentials.ts
93
- import { createRequire } from "module";
94
138
  var SERVICE_NAME = "gut-cli";
95
139
  var PROVIDER_KEY_MAP = {
96
140
  gemini: "gemini-api-key",
@@ -167,12 +211,34 @@ function getProviderDisplayName(provider) {
167
211
  };
168
212
  return names[provider];
169
213
  }
214
+ async function getFirstAvailableProvider() {
215
+ const providers = ["gemini", "openai", "anthropic"];
216
+ for (const provider of providers) {
217
+ const key = await getApiKey(provider);
218
+ if (key) {
219
+ return provider;
220
+ }
221
+ }
222
+ return "ollama";
223
+ }
224
+ async function resolveProvider(cliProvider) {
225
+ if (cliProvider) {
226
+ return cliProvider.toLowerCase();
227
+ }
228
+ const configProvider = getConfiguredProvider();
229
+ if (configProvider) {
230
+ if (configProvider === "ollama" || await getApiKey(configProvider)) {
231
+ return configProvider;
232
+ }
233
+ }
234
+ return getFirstAvailableProvider();
235
+ }
170
236
 
171
237
  // src/commands/auth.ts
172
238
  var PROVIDERS = ["gemini", "openai", "anthropic"];
173
239
  async function readSecretInput(prompt) {
174
240
  return new Promise((resolve) => {
175
- process.stdout.write(chalk2.cyan(prompt));
241
+ process.stdout.write(chalk.cyan(prompt));
176
242
  let input = "";
177
243
  const stdin = process.stdin;
178
244
  stdin.setRawMode(true);
@@ -207,126 +273,127 @@ async function readSecretInput(prompt) {
207
273
  stdin.on("data", onData);
208
274
  });
209
275
  }
210
- var authCommand = new Command2("auth").description("Manage API key authentication");
276
+ var authCommand = new Command("auth").description("Manage API key authentication");
211
277
  authCommand.command("login").description("Save an API key to the system keychain").requiredOption("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)").option("-k, --key <key>", "API key (if not provided, will prompt)").action(async (options) => {
212
278
  const provider = options.provider.toLowerCase();
213
279
  if (!PROVIDERS.includes(provider)) {
214
- console.error(chalk2.red(`Invalid provider: ${options.provider}`));
215
- console.error(chalk2.gray(`Valid providers: ${PROVIDERS.join(", ")}`));
280
+ console.error(chalk.red(`Invalid provider: ${options.provider}`));
281
+ console.error(chalk.gray(`Valid providers: ${PROVIDERS.join(", ")}`));
216
282
  process.exit(1);
217
283
  }
218
284
  let apiKey = options.key;
219
285
  if (!apiKey) {
220
286
  const providerName = getProviderDisplayName(provider);
221
- console.log(chalk2.bold(`
287
+ console.log(chalk.bold(`
222
288
  \u{1F511} ${providerName} API Key Setup
223
289
  `));
224
- console.log(chalk2.gray(`Your API key will be stored securely in the system keychain.`));
290
+ console.log(chalk.gray(`Your API key will be stored securely in the system keychain.`));
225
291
  console.log();
226
292
  apiKey = await readSecretInput(`Enter ${providerName} API key: `);
227
293
  }
228
294
  if (!apiKey || apiKey.trim() === "") {
229
- console.error(chalk2.red("API key cannot be empty"));
295
+ console.error(chalk.red("API key cannot be empty"));
230
296
  process.exit(1);
231
297
  }
232
298
  try {
233
299
  await saveApiKey(provider, apiKey.trim());
234
- console.log(chalk2.green(`
235
- \u2713 API key for ${getProviderDisplayName(provider)} saved to system keychain`));
236
- } catch (error) {
237
- console.error(chalk2.red("Failed to save API key"));
238
- console.error(chalk2.gray(error instanceof Error ? error.message : "Unknown error"));
300
+ console.log(
301
+ chalk.green(`
302
+ \u2713 API key for ${getProviderDisplayName(provider)} saved to system keychain`)
303
+ );
304
+ } catch (err) {
305
+ console.error(chalk.red("Failed to save API key"));
306
+ console.error(chalk.gray(err instanceof Error ? err.message : "Unknown error"));
239
307
  process.exit(1);
240
308
  }
241
309
  });
242
310
  authCommand.command("logout").description("Remove an API key from the system keychain").requiredOption("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)").action(async (options) => {
243
311
  const provider = options.provider.toLowerCase();
244
312
  if (!PROVIDERS.includes(provider)) {
245
- console.error(chalk2.red(`Invalid provider: ${options.provider}`));
313
+ console.error(chalk.red(`Invalid provider: ${options.provider}`));
246
314
  process.exit(1);
247
315
  }
248
316
  try {
249
317
  const deleted = await deleteApiKey(provider);
250
318
  if (deleted) {
251
- console.log(chalk2.green(`\u2713 API key for ${getProviderDisplayName(provider)} removed`));
319
+ console.log(chalk.green(`\u2713 API key for ${getProviderDisplayName(provider)} removed`));
252
320
  } else {
253
- console.log(chalk2.yellow(`No API key found for ${getProviderDisplayName(provider)}`));
321
+ console.log(chalk.yellow(`No API key found for ${getProviderDisplayName(provider)}`));
254
322
  }
255
- } catch (error) {
256
- console.error(chalk2.red("Failed to remove API key"));
323
+ } catch {
324
+ console.error(chalk.red("Failed to remove API key"));
257
325
  process.exit(1);
258
326
  }
259
327
  });
260
328
  authCommand.command("status").description("Show which providers have API keys configured").action(async () => {
261
329
  try {
262
330
  const providers = await listProviders();
263
- console.log(chalk2.bold("\nAPI Key Status:\n"));
331
+ console.log(chalk.bold("\nAPI Key Status:\n"));
264
332
  for (const { provider, hasKey } of providers) {
265
- const status = hasKey ? chalk2.green("\u2713 configured") : chalk2.gray("\u25CB not set");
333
+ const status = hasKey ? chalk.green("\u2713 configured") : chalk.gray("\u25CB not set");
266
334
  console.log(` ${getProviderDisplayName(provider).padEnd(20)} ${status}`);
267
335
  }
268
- console.log(
269
- chalk2.gray("\nKeys can also be set via environment variables:")
270
- );
271
- console.log(chalk2.gray(" GUT_GEMINI_API_KEY, GUT_OPENAI_API_KEY, GUT_ANTHROPIC_API_KEY\n"));
272
- } catch (error) {
273
- console.error(chalk2.red("Failed to check status"));
336
+ console.log(chalk.gray("\nKeys can also be set via environment variables:"));
337
+ console.log(chalk.gray(" GUT_GEMINI_API_KEY, GUT_OPENAI_API_KEY, GUT_ANTHROPIC_API_KEY\n"));
338
+ } catch {
339
+ console.error(chalk.red("Failed to check status"));
274
340
  process.exit(1);
275
341
  }
276
342
  });
277
343
 
278
- // src/commands/commit.ts
279
- import { Command as Command3 } from "commander";
344
+ // src/commands/branch.ts
345
+ import { execSync as execSync3 } from "child_process";
280
346
  import chalk3 from "chalk";
281
- import ora2 from "ora";
282
- import { simpleGit as simpleGit2 } from "simple-git";
347
+ import { Command as Command2 } from "commander";
348
+ import ora from "ora";
349
+ import { simpleGit } from "simple-git";
283
350
 
284
351
  // src/lib/ai.ts
285
- import { generateText, generateObject } from "ai";
352
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
353
+ import { homedir as homedir2 } from "os";
354
+ import { dirname, join as join2 } from "path";
355
+ import { fileURLToPath } from "url";
356
+ import { createAnthropic } from "@ai-sdk/anthropic";
286
357
  import { createGoogleGenerativeAI } from "@ai-sdk/google";
287
358
  import { createOpenAI } from "@ai-sdk/openai";
288
- import { createAnthropic } from "@ai-sdk/anthropic";
359
+ import { generateObject, generateText } from "ai";
289
360
  import { createOllama } from "ollama-ai-provider";
290
361
  import { z } from "zod";
291
- import { existsSync, readFileSync } from "fs";
292
- import { join, dirname } from "path";
293
- import { homedir } from "os";
294
- import { fileURLToPath } from "url";
295
362
  var __filename = fileURLToPath(import.meta.url);
296
363
  var __dirname = dirname(__filename);
297
364
  function findGutRoot() {
298
365
  let current = __dirname;
299
366
  for (let i = 0; i < 5; i++) {
300
- const gutPath = join(current, ".gut");
301
- if (existsSync(gutPath)) {
367
+ const gutPath = join2(current, ".gut");
368
+ if (existsSync2(gutPath)) {
302
369
  return current;
303
370
  }
304
371
  current = dirname(current);
305
372
  }
306
- return join(__dirname, "..");
373
+ return join2(__dirname, "..");
307
374
  }
308
375
  var GUT_ROOT = findGutRoot();
309
376
  function loadTemplate(name) {
310
- const templatePath = join(GUT_ROOT, ".gut", `${name}.md`);
311
- if (existsSync(templatePath)) {
312
- return readFileSync(templatePath, "utf-8");
377
+ const templatePath = join2(GUT_ROOT, ".gut", `${name}.md`);
378
+ if (existsSync2(templatePath)) {
379
+ return readFileSync2(templatePath, "utf-8");
313
380
  }
314
381
  throw new Error(`Template not found: ${templatePath}`);
315
382
  }
316
383
  function getGlobalTemplatesDir() {
317
- return join(homedir(), ".config", "gut", "templates");
384
+ return join2(homedir2(), ".config", "gut", "templates");
318
385
  }
319
386
  function findGlobalTemplate(templateName) {
320
- const templatePath = join(getGlobalTemplatesDir(), `${templateName}.md`);
321
- if (existsSync(templatePath)) {
322
- return readFileSync(templatePath, "utf-8");
387
+ const templatePath = join2(getGlobalTemplatesDir(), `${templateName}.md`);
388
+ if (existsSync2(templatePath)) {
389
+ return readFileSync2(templatePath, "utf-8");
323
390
  }
324
391
  return null;
325
392
  }
326
393
  function findTemplate(repoRoot, templateName) {
327
- const projectTemplatePath = join(repoRoot, ".gut", `${templateName}.md`);
328
- if (existsSync(projectTemplatePath)) {
329
- return readFileSync(projectTemplatePath, "utf-8");
394
+ const projectTemplatePath = join2(repoRoot, ".gut", `${templateName}.md`);
395
+ if (existsSync2(projectTemplatePath)) {
396
+ return readFileSync2(projectTemplatePath, "utf-8");
330
397
  }
331
398
  const globalTemplate = findGlobalTemplate(templateName);
332
399
  if (globalTemplate) {
@@ -352,16 +419,12 @@ ${value}
352
419
  <output-format>
353
420
  ${outputFormat}
354
421
  </output-format>` : "";
355
- return contextXml + "<instructions>\n" + template + langInstruction + "\n</instructions>" + outputSection;
422
+ return `${contextXml}<instructions>
423
+ ${template}${langInstruction}
424
+ </instructions>${outputSection}`;
356
425
  }
357
- var DEFAULT_MODELS = {
358
- gemini: "gemini-2.0-flash",
359
- openai: "gpt-4o-mini",
360
- anthropic: "claude-sonnet-4-20250514",
361
- ollama: "llama3.2"
362
- };
363
426
  async function getModel(options) {
364
- const modelName = options.model || DEFAULT_MODELS[options.provider];
427
+ const modelName = options.model || getConfiguredModel() || getDefaultModel(options.provider);
365
428
  async function resolveApiKey() {
366
429
  if (options.apiKey) return options.apiKey;
367
430
  return getApiKey(options.provider);
@@ -400,9 +463,15 @@ async function getModel(options) {
400
463
  }
401
464
  async function generateCommitMessage(diff, options, template) {
402
465
  const model = await getModel(options);
403
- const prompt = buildPrompt(template, "commit", {
404
- diff: diff.slice(0, 8e3)
405
- }, options.language, "Respond with ONLY the commit message, nothing else.");
466
+ const prompt = buildPrompt(
467
+ template,
468
+ "commit",
469
+ {
470
+ diff: diff.slice(0, 8e3)
471
+ },
472
+ options.language,
473
+ "Respond with ONLY the commit message, nothing else."
474
+ );
406
475
  const result = await generateText({
407
476
  model,
408
477
  prompt,
@@ -412,18 +481,24 @@ async function generateCommitMessage(diff, options, template) {
412
481
  }
413
482
  async function generatePRDescription(context, options, template) {
414
483
  const model = await getModel(options);
415
- const prompt = buildPrompt(template, "pr", {
416
- baseBranch: context.baseBranch,
417
- currentBranch: context.currentBranch,
418
- commits: context.commits.map((c) => `- ${c}`).join("\n"),
419
- diff: context.diff.slice(0, 6e3)
420
- }, options.language, `Respond in JSON format:
484
+ const prompt = buildPrompt(
485
+ template,
486
+ "pr",
487
+ {
488
+ baseBranch: context.baseBranch,
489
+ currentBranch: context.currentBranch,
490
+ commits: context.commits.map((c) => `- ${c}`).join("\n"),
491
+ diff: context.diff.slice(0, 6e3)
492
+ },
493
+ options.language,
494
+ `Respond in JSON format:
421
495
  \`\`\`json
422
496
  {
423
497
  "title": "...",
424
498
  "body": "..."
425
499
  }
426
- \`\`\``);
500
+ \`\`\``
501
+ );
427
502
  const result = await generateText({
428
503
  model,
429
504
  prompt,
@@ -454,9 +529,14 @@ var CodeReviewSchema = z.object({
454
529
  });
455
530
  async function generateCodeReview(diff, options, template) {
456
531
  const model = await getModel(options);
457
- const prompt = buildPrompt(template, "review", {
458
- diff: diff.slice(0, 1e4)
459
- }, options.language);
532
+ const prompt = buildPrompt(
533
+ template,
534
+ "review",
535
+ {
536
+ diff: diff.slice(0, 1e4)
537
+ },
538
+ options.language
539
+ );
460
540
  const result = await generateObject({
461
541
  model,
462
542
  schema: CodeReviewSchema,
@@ -478,13 +558,18 @@ var ChangelogSchema = z.object({
478
558
  async function generateChangelog(context, options, template) {
479
559
  const model = await getModel(options);
480
560
  const commitList = context.commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message} (${c.author})`).join("\n");
481
- const prompt = buildPrompt(template, "changelog", {
482
- fromRef: context.fromRef,
483
- toRef: context.toRef,
484
- commits: commitList,
485
- diff: context.diff.slice(0, 8e3),
486
- todayDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
487
- }, options.language);
561
+ const prompt = buildPrompt(
562
+ template,
563
+ "changelog",
564
+ {
565
+ fromRef: context.fromRef,
566
+ toRef: context.toRef,
567
+ commits: commitList,
568
+ diff: context.diff.slice(0, 8e3),
569
+ todayDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
570
+ },
571
+ options.language
572
+ );
488
573
  const result = await generateObject({
489
574
  model,
490
575
  schema: ChangelogSchema,
@@ -512,10 +597,15 @@ var ExplanationSchema = z.object({
512
597
  async function generateExplanation(context, options, template) {
513
598
  const model = await getModel(options);
514
599
  if (context.type === "file-content") {
515
- const prompt2 = buildPrompt(template, "explain-file", {
516
- filePath: context.metadata.filePath || "",
517
- content: context.content?.slice(0, 15e3) || ""
518
- }, options.language);
600
+ const prompt2 = buildPrompt(
601
+ template,
602
+ "explain-file",
603
+ {
604
+ filePath: context.metadata.filePath || "",
605
+ content: context.content?.slice(0, 15e3) || ""
606
+ },
607
+ options.language
608
+ );
519
609
  const result2 = await generateObject({
520
610
  model,
521
611
  schema: ExplanationSchema,
@@ -549,11 +639,16 @@ Author: ${context.metadata.author}
549
639
  Date: ${context.metadata.date}`;
550
640
  targetType = "commit";
551
641
  }
552
- const prompt = buildPrompt(template, "explain", {
553
- targetType,
554
- contextInfo,
555
- diff: context.diff?.slice(0, 12e3) || ""
556
- }, options.language);
642
+ const prompt = buildPrompt(
643
+ template,
644
+ "explain",
645
+ {
646
+ targetType,
647
+ contextInfo,
648
+ diff: context.diff?.slice(0, 12e3) || ""
649
+ },
650
+ options.language
651
+ );
557
652
  const result = await generateObject({
558
653
  model,
559
654
  schema: ExplanationSchema,
@@ -572,12 +667,19 @@ var CommitSearchSchema = z.object({
572
667
  });
573
668
  async function searchCommits(query, commits, options, maxResults = 5, template) {
574
669
  const model = await getModel(options);
575
- const commitList = commits.map((c) => `${c.hash.slice(0, 7)} | ${c.author} | ${c.date.split("T")[0]} | ${c.message.split("\n")[0]}`).join("\n");
576
- const prompt = buildPrompt(template, "find", {
577
- query,
578
- commits: commitList,
579
- maxResults: String(maxResults)
580
- }, options.language);
670
+ const commitList = commits.map(
671
+ (c) => `${c.hash.slice(0, 7)} | ${c.author} | ${c.date.split("T")[0]} | ${c.message.split("\n")[0]}`
672
+ ).join("\n");
673
+ const prompt = buildPrompt(
674
+ template,
675
+ "find",
676
+ {
677
+ query,
678
+ commits: commitList,
679
+ maxResults: String(maxResults)
680
+ },
681
+ options.language
682
+ );
581
683
  const result = await generateObject({
582
684
  model,
583
685
  schema: CommitSearchSchema,
@@ -606,11 +708,17 @@ async function searchCommits(query, commits, options, maxResults = 5, template)
606
708
  }
607
709
  async function generateBranchName(description, options, context, template) {
608
710
  const model = await getModel(options);
609
- const prompt = buildPrompt(template, "branch", {
610
- description,
611
- type: context?.type,
612
- issue: context?.issue
613
- }, options.language, "Respond with ONLY the branch name, nothing else.");
711
+ const prompt = buildPrompt(
712
+ template,
713
+ "branch",
714
+ {
715
+ description,
716
+ type: context?.type,
717
+ issue: context?.issue
718
+ },
719
+ options.language,
720
+ "Respond with ONLY the branch name, nothing else."
721
+ );
614
722
  const result = await generateText({
615
723
  model,
616
724
  prompt,
@@ -620,9 +728,15 @@ async function generateBranchName(description, options, context, template) {
620
728
  }
621
729
  async function generateBranchNameFromDiff(diff, options, template) {
622
730
  const model = await getModel(options);
623
- const prompt = buildPrompt(template, "checkout", {
624
- diff: diff.slice(0, 8e3)
625
- }, options.language, "Respond with ONLY the branch name, nothing else.");
731
+ const prompt = buildPrompt(
732
+ template,
733
+ "checkout",
734
+ {
735
+ diff: diff.slice(0, 8e3)
736
+ },
737
+ options.language,
738
+ "Respond with ONLY the branch name, nothing else."
739
+ );
626
740
  const result = await generateText({
627
741
  model,
628
742
  prompt,
@@ -632,9 +746,15 @@ async function generateBranchNameFromDiff(diff, options, template) {
632
746
  }
633
747
  async function generateStashName(diff, options, template) {
634
748
  const model = await getModel(options);
635
- const prompt = buildPrompt(template, "stash", {
636
- diff: diff.slice(0, 4e3)
637
- }, options.language, "Respond with ONLY the stash name, nothing else.");
749
+ const prompt = buildPrompt(
750
+ template,
751
+ "stash",
752
+ {
753
+ diff: diff.slice(0, 4e3)
754
+ },
755
+ options.language,
756
+ "Respond with ONLY the stash name, nothing else."
757
+ );
638
758
  const result = await generateText({
639
759
  model,
640
760
  prompt,
@@ -664,13 +784,18 @@ async function generateWorkSummary(context, options, format = "custom", template
664
784
  const commitList = context.commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message.split("\n")[0]} (${c.date.split("T")[0]})`).join("\n");
665
785
  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}` : ""}.`;
666
786
  const period = `${context.since}${context.until ? ` to ${context.until}` : " to now"}`;
667
- const prompt = buildPrompt(template, "summary", {
668
- author: context.author,
669
- period,
670
- format: formatHint,
671
- commits: commitList,
672
- diff: context.diff?.slice(0, 6e3)
673
- }, options.language);
787
+ const prompt = buildPrompt(
788
+ template,
789
+ "summary",
790
+ {
791
+ author: context.author,
792
+ period,
793
+ format: formatHint,
794
+ commits: commitList,
795
+ diff: context.diff?.slice(0, 6e3)
796
+ },
797
+ options.language
798
+ );
674
799
  const result = await generateObject({
675
800
  model,
676
801
  schema: WorkSummarySchema,
@@ -686,12 +811,17 @@ async function generateWorkSummary(context, options, format = "custom", template
686
811
  }
687
812
  async function resolveConflict(conflictedContent, context, options, template) {
688
813
  const model = await getModel(options);
689
- const prompt = buildPrompt(template, "merge", {
690
- filename: context.filename,
691
- oursRef: context.oursRef,
692
- theirsRef: context.theirsRef,
693
- content: conflictedContent
694
- }, options.language);
814
+ const prompt = buildPrompt(
815
+ template,
816
+ "merge",
817
+ {
818
+ filename: context.filename,
819
+ oursRef: context.oursRef,
820
+ theirsRef: context.theirsRef,
821
+ content: conflictedContent
822
+ },
823
+ options.language
824
+ );
695
825
  const result = await generateObject({
696
826
  model,
697
827
  schema: ConflictResolutionSchema,
@@ -701,11 +831,17 @@ async function resolveConflict(conflictedContent, context, options, template) {
701
831
  }
702
832
  async function generateGitignore(context, options, template) {
703
833
  const model = await getModel(options);
704
- const prompt = buildPrompt(template, "gitignore", {
705
- files: context.files,
706
- configFiles: context.configFiles,
707
- existingGitignore: context.existingGitignore
708
- }, options.language, "Respond with ONLY the .gitignore content, nothing else. No explanations or markdown code blocks.");
834
+ const prompt = buildPrompt(
835
+ template,
836
+ "gitignore",
837
+ {
838
+ files: context.files,
839
+ configFiles: context.configFiles,
840
+ existingGitignore: context.existingGitignore
841
+ },
842
+ options.language,
843
+ "Respond with ONLY the .gitignore content, nothing else. No explanations or markdown code blocks."
844
+ );
709
845
  const result = await generateText({
710
846
  model,
711
847
  prompt,
@@ -714,54 +850,106 @@ async function generateGitignore(context, options, template) {
714
850
  return result.text.trim();
715
851
  }
716
852
 
717
- // src/commands/commit.ts
718
- 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) => {
719
- const git = simpleGit2();
853
+ // src/lib/gh.ts
854
+ import { execSync as execSync2 } from "child_process";
855
+ import chalk2 from "chalk";
856
+ var ghInstalledCache = null;
857
+ function isGhCliInstalled() {
858
+ if (ghInstalledCache !== null) {
859
+ return ghInstalledCache;
860
+ }
861
+ try {
862
+ execSync2("gh --version", { stdio: "pipe" });
863
+ ghInstalledCache = true;
864
+ return true;
865
+ } catch {
866
+ ghInstalledCache = false;
867
+ return false;
868
+ }
869
+ }
870
+ function printGhNotInstalledMessage() {
871
+ console.log(chalk2.yellow("\n\u26A0 GitHub CLI (gh) is not installed"));
872
+ console.log(chalk2.gray(" This command requires gh CLI. Install it:"));
873
+ console.log(chalk2.gray(" brew install gh (macOS)"));
874
+ console.log(chalk2.gray(" https://cli.github.com/"));
875
+ }
876
+ function requireGhCli() {
877
+ if (!isGhCliInstalled()) {
878
+ printGhNotInstalledMessage();
879
+ return false;
880
+ }
881
+ return true;
882
+ }
883
+
884
+ // src/commands/branch.ts
885
+ function getIssueInfo(issueNumber) {
886
+ try {
887
+ const result = execSync3(`gh issue view ${issueNumber} --json title,body`, {
888
+ encoding: "utf-8",
889
+ stdio: ["pipe", "pipe", "pipe"]
890
+ });
891
+ return JSON.parse(result);
892
+ } catch {
893
+ return null;
894
+ }
895
+ }
896
+ var branchCommand = new Command2("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, ollama)").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) => {
897
+ const git = simpleGit();
720
898
  const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
721
899
  const isRepo = await git.checkIsRepo();
722
900
  if (!isRepo) {
723
901
  console.error(chalk3.red("Error: Not a git repository"));
724
902
  process.exit(1);
725
903
  }
726
- const provider = options.provider.toLowerCase();
727
- if (options.all) {
728
- await git.add("-A");
729
- }
730
- let diff = await git.diff(["--cached"]);
731
- if (!diff.trim()) {
732
- const status = await git.status();
733
- const unstaged = await git.diff();
734
- const hasUntracked = status.not_added.length > 0 || status.created.length > 0;
735
- if (!unstaged.trim() && !hasUntracked) {
736
- console.error(chalk3.yellow("No changes to commit."));
904
+ let description;
905
+ let issueNumber;
906
+ if (options.description) {
907
+ description = options.description;
908
+ } else if (issue) {
909
+ if (!requireGhCli()) {
737
910
  process.exit(1);
738
911
  }
739
- console.log(chalk3.gray("No staged changes, staging all changes..."));
740
- await git.add("-A");
741
- diff = await git.diff(["--cached"]);
912
+ const cleanedIssue = issue.replace(/^#/, "");
913
+ issueNumber = cleanedIssue;
914
+ const spinner2 = ora(`Fetching issue #${cleanedIssue}...`).start();
915
+ const issueInfo = getIssueInfo(cleanedIssue);
916
+ if (!issueInfo) {
917
+ spinner2.fail(`Could not fetch issue #${issueNumber}`);
918
+ console.log(chalk3.gray("Make sure you are authenticated: gh auth login"));
919
+ process.exit(1);
920
+ }
921
+ spinner2.stop();
922
+ console.log(chalk3.gray(`Issue: ${issueInfo.title}`));
923
+ description = `${issueInfo.title}
924
+
925
+ ${issueInfo.body || ""}`;
926
+ } else {
927
+ console.error(chalk3.red("Error: Please provide an issue number or use -d for description"));
928
+ console.log(chalk3.gray("Usage:"));
929
+ console.log(chalk3.gray(" gut branch 123"));
930
+ console.log(chalk3.gray(' gut branch -d "add user authentication"'));
931
+ process.exit(1);
742
932
  }
743
- const template = findTemplate(repoRoot.trim(), "commit");
933
+ const provider = await resolveProvider(options.provider);
934
+ const template = findTemplate(repoRoot.trim(), "branch");
744
935
  if (template) {
745
936
  console.log(chalk3.gray("Using template from project..."));
746
937
  }
747
- const spinner = ora2("Generating commit message...").start();
938
+ const spinner = ora("Generating branch name...").start();
748
939
  try {
749
- const message = await generateCommitMessage(
750
- diff,
940
+ const branchName = await generateBranchName(
941
+ description,
751
942
  { provider, model: options.model },
943
+ { type: options.type, issue: issueNumber },
752
944
  template || void 0
753
945
  );
754
946
  spinner.stop();
755
- console.log(chalk3.bold("\nGenerated commit message:\n"));
756
- console.log(chalk3.green(` ${message.split("\n")[0]}`));
757
- if (message.includes("\n")) {
758
- const details = message.split("\n").slice(1).join("\n");
759
- console.log(chalk3.gray(details.split("\n").map((l) => ` ${l}`).join("\n")));
760
- }
947
+ console.log(chalk3.bold("\nGenerated branch name:\n"));
948
+ console.log(chalk3.green(` ${branchName}`));
761
949
  console.log();
762
- if (options.commit) {
763
- await git.commit(message);
764
- console.log(chalk3.green("\u2713 Committed successfully"));
950
+ if (options.checkout) {
951
+ await git.checkoutLocalBranch(branchName);
952
+ console.log(chalk3.green(`\u2713 Created and checked out branch: ${branchName}`));
765
953
  } else {
766
954
  const readline = await import("readline");
767
955
  const rl = readline.createInterface({
@@ -769,566 +957,512 @@ var commitCommand = new Command3("commit").description("Generate a commit messag
769
957
  output: process.stdout
770
958
  });
771
959
  const answer = await new Promise((resolve) => {
772
- rl.question(chalk3.cyan("Commit with this message? (y/N/e to edit) "), resolve);
960
+ rl.question(chalk3.cyan("Create and checkout this branch? (y/N) "), resolve);
773
961
  });
774
962
  rl.close();
775
963
  if (answer.toLowerCase() === "y") {
776
- await git.commit(message);
777
- console.log(chalk3.green("\u2713 Committed successfully"));
778
- } else if (answer.toLowerCase() === "e") {
779
- console.log(chalk3.gray("Opening editor..."));
780
- const { execSync: execSync9 } = await import("child_process");
781
- const editor = process.env.EDITOR || process.env.VISUAL || "vi";
782
- const fs2 = await import("fs");
783
- const os = await import("os");
784
- const path2 = await import("path");
785
- const tmpFile = path2.join(os.tmpdir(), "gut-commit-msg.txt");
786
- fs2.writeFileSync(tmpFile, message);
787
- execSync9(`${editor} "${tmpFile}"`, { stdio: "inherit" });
788
- const editedMessage = fs2.readFileSync(tmpFile, "utf-8").trim();
789
- fs2.unlinkSync(tmpFile);
790
- if (editedMessage) {
791
- await git.commit(editedMessage);
792
- console.log(chalk3.green("\u2713 Committed successfully"));
793
- } else {
794
- console.log(chalk3.yellow("Commit cancelled (empty message)"));
795
- }
964
+ await git.checkoutLocalBranch(branchName);
965
+ console.log(chalk3.green(`\u2713 Created and checked out branch: ${branchName}`));
796
966
  } else {
797
- console.log(chalk3.gray("Commit cancelled"));
798
- console.log(chalk3.gray("\nTo commit manually:"));
799
- console.log(chalk3.gray(` git commit -m "${message.split("\n")[0]}"`));
967
+ console.log(chalk3.gray("\nTo create manually:"));
968
+ console.log(chalk3.gray(` git checkout -b ${branchName}`));
800
969
  }
801
970
  }
802
971
  } catch (error) {
803
- spinner.fail("Failed to generate commit message");
972
+ spinner.fail("Failed to generate branch name");
804
973
  console.error(chalk3.red(error instanceof Error ? error.message : "Unknown error"));
805
974
  process.exit(1);
806
975
  }
807
976
  });
808
977
 
809
- // src/commands/pr.ts
810
- import { Command as Command4 } from "commander";
811
- import chalk5 from "chalk";
812
- import ora3 from "ora";
813
- import { simpleGit as simpleGit3 } from "simple-git";
814
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
815
- import { join as join2 } from "path";
816
- import { execSync as execSync2 } from "child_process";
817
-
818
- // src/lib/gh.ts
819
- import { execSync } from "child_process";
978
+ // src/commands/changelog.ts
820
979
  import chalk4 from "chalk";
821
- var ghInstalledCache = null;
822
- function isGhCliInstalled() {
823
- if (ghInstalledCache !== null) {
824
- return ghInstalledCache;
825
- }
826
- try {
827
- execSync("gh --version", { stdio: "pipe" });
828
- ghInstalledCache = true;
829
- return true;
830
- } catch {
831
- ghInstalledCache = false;
832
- return false;
833
- }
834
- }
835
- function printGhNotInstalledMessage() {
836
- console.log(chalk4.yellow("\n\u26A0 GitHub CLI (gh) is not installed"));
837
- console.log(chalk4.gray(" This command requires gh CLI. Install it:"));
838
- console.log(chalk4.gray(" brew install gh (macOS)"));
839
- console.log(chalk4.gray(" https://cli.github.com/"));
840
- }
841
- function requireGhCli() {
842
- if (!isGhCliInstalled()) {
843
- printGhNotInstalledMessage();
844
- return false;
980
+ import { Command as Command3 } from "commander";
981
+ import ora2 from "ora";
982
+ import { simpleGit as simpleGit2 } from "simple-git";
983
+ function formatChangelog(changelog) {
984
+ const lines = [];
985
+ const header = changelog.version ? `## [${changelog.version}] - ${changelog.date}` : `## ${changelog.date}`;
986
+ lines.push(header);
987
+ lines.push("");
988
+ if (changelog.summary) {
989
+ lines.push(changelog.summary);
990
+ lines.push("");
845
991
  }
846
- return true;
847
- }
848
-
849
- // src/commands/pr.ts
850
- var GITHUB_PR_TEMPLATE_PATHS = [
851
- ".github/pull_request_template.md",
852
- ".github/PULL_REQUEST_TEMPLATE.md",
853
- "pull_request_template.md",
854
- "PULL_REQUEST_TEMPLATE.md",
855
- "docs/pull_request_template.md"
856
- ];
857
- function findPRTemplate(repoRoot) {
858
- for (const templatePath of GITHUB_PR_TEMPLATE_PATHS) {
859
- const fullPath = join2(repoRoot, templatePath);
860
- if (existsSync2(fullPath)) {
861
- return readFileSync2(fullPath, "utf-8");
992
+ for (const section of changelog.sections) {
993
+ if (section.items.length > 0) {
994
+ lines.push(`### ${section.type}`);
995
+ for (const item of section.items) {
996
+ lines.push(`- ${item}`);
997
+ }
998
+ lines.push("");
862
999
  }
863
1000
  }
864
- return findTemplate(repoRoot, "pr");
1001
+ return lines.join("\n");
865
1002
  }
866
- 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) => {
867
- const git = simpleGit3();
1003
+ var changelogCommand = new Command3("changelog").description("Generate a changelog from commits between refs").argument("[from]", "Starting ref (tag, branch, commit)", "HEAD~10").argument("[to]", "Ending ref", "HEAD").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic, ollama)").option("-m, --model <model>", "Model to use (provider-specific)").option("-t, --tag <tag>", "Generate changelog since this tag").option("--json", "Output as JSON").action(async (from, to, options) => {
1004
+ const git = simpleGit2();
868
1005
  const isRepo = await git.checkIsRepo();
869
1006
  if (!isRepo) {
870
- console.error(chalk5.red("Error: Not a git repository"));
1007
+ console.error(chalk4.red("Error: Not a git repository"));
871
1008
  process.exit(1);
872
1009
  }
873
- const provider = options.provider.toLowerCase();
874
- const spinner = ora3("Analyzing branch...").start();
1010
+ const provider = await resolveProvider(options.provider);
1011
+ const spinner = ora2("Analyzing commits...").start();
875
1012
  try {
876
- const branchInfo = await git.branch();
877
- const currentBranch = branchInfo.current;
878
- let baseBranch = options.base;
879
- if (!baseBranch) {
880
- if (branchInfo.all.includes("main")) {
881
- baseBranch = "main";
882
- } else if (branchInfo.all.includes("master")) {
883
- baseBranch = "master";
884
- } else {
885
- baseBranch = "main";
886
- }
887
- }
888
- if (currentBranch === baseBranch) {
889
- spinner.fail(`Already on ${baseBranch} branch`);
890
- process.exit(1);
1013
+ let fromRef = from;
1014
+ let toRef = to;
1015
+ if (options.tag) {
1016
+ fromRef = options.tag;
1017
+ toRef = "HEAD";
891
1018
  }
892
- spinner.text = `Comparing ${currentBranch} to ${baseBranch}...`;
893
- const log = await git.log({ from: baseBranch, to: currentBranch });
894
- const commits = log.all.map((c) => c.message.split("\n")[0]);
895
- if (commits.length === 0) {
896
- spinner.fail("No commits found between branches");
897
- process.exit(1);
1019
+ const log = await git.log({ from: fromRef, to: toRef });
1020
+ if (log.all.length === 0) {
1021
+ spinner.info("No commits found in range");
1022
+ process.exit(0);
898
1023
  }
899
- const diff = await git.diff([`${baseBranch}...${currentBranch}`]);
1024
+ spinner.text = `Found ${log.all.length} commits, generating changelog...`;
1025
+ const commits = log.all.map((c) => ({
1026
+ hash: c.hash,
1027
+ message: c.message,
1028
+ author: c.author_name,
1029
+ date: c.date
1030
+ }));
1031
+ const diff = await git.diff([`${fromRef}...${toRef}`]);
900
1032
  const repoRoot = await git.revparse(["--show-toplevel"]);
901
- const template = findPRTemplate(repoRoot.trim());
1033
+ const template = findTemplate(repoRoot.trim(), "changelog");
902
1034
  if (template) {
903
- spinner.text = "Found PR template, generating description...";
904
- } else {
905
- spinner.text = "Generating PR description...";
1035
+ spinner.text = "Using template from project...";
906
1036
  }
907
- const { title, body } = await generatePRDescription(
908
- {
909
- baseBranch,
910
- currentBranch,
911
- commits,
912
- diff
913
- },
1037
+ const changelog = await generateChangelog(
1038
+ { commits, diff, fromRef, toRef },
914
1039
  { provider, model: options.model },
915
1040
  template || void 0
916
1041
  );
917
1042
  spinner.stop();
918
- console.log(chalk5.bold("\n\u{1F4DD} Generated PR:\n"));
919
- console.log(chalk5.cyan("Title:"), chalk5.white(title));
920
- console.log(chalk5.cyan("\nDescription:"));
921
- console.log(chalk5.gray("\u2500".repeat(50)));
922
- console.log(body);
923
- console.log(chalk5.gray("\u2500".repeat(50)));
924
- if (options.copy) {
925
- try {
926
- const fullText = `${title}
927
-
928
- ${body}`;
929
- execSync2("pbcopy", { input: fullText });
930
- console.log(chalk5.green("\n\u2713 Copied to clipboard"));
931
- } catch {
932
- console.log(chalk5.yellow("\n\u26A0 Could not copy to clipboard"));
933
- }
1043
+ if (options.json) {
1044
+ console.log(JSON.stringify(changelog, null, 2));
1045
+ return;
934
1046
  }
935
- const ghInstalled = isGhCliInstalled();
936
- if (!ghInstalled) {
937
- console.log(chalk5.yellow("\n\u26A0 GitHub CLI (gh) is not installed"));
938
- console.log(chalk5.gray(" To create PRs directly from gut, install gh CLI:"));
939
- console.log(chalk5.gray(" brew install gh (macOS)"));
940
- console.log(chalk5.gray(" https://cli.github.com/"));
941
- if (!options.copy) {
942
- console.log(chalk5.gray("\nTip: Use --copy to copy to clipboard"));
943
- }
944
- } else {
945
- const readline = await import("readline");
946
- const rl = readline.createInterface({
947
- input: process.stdin,
948
- output: process.stdout
949
- });
950
- const promptMessage = options.create ? chalk5.cyan("\nCreate PR with this description? (y/N) ") : chalk5.cyan("\nCreate PR with gh CLI? (y/N) ");
951
- const answer = await new Promise((resolve) => {
952
- rl.question(promptMessage, resolve);
953
- });
954
- rl.close();
955
- if (answer.toLowerCase() === "y") {
956
- const createSpinner = ora3("Creating PR...").start();
957
- try {
958
- const escapedTitle = title.replace(/"/g, '\\"');
959
- const escapedBody = body.replace(/"/g, '\\"');
960
- execSync2(
961
- `gh pr create --title "${escapedTitle}" --body "${escapedBody}" --base ${baseBranch}`,
962
- { stdio: "pipe" }
963
- );
964
- createSpinner.succeed("PR created successfully!");
965
- } catch (error) {
966
- createSpinner.fail("Failed to create PR");
967
- console.error(chalk5.gray("Make sure gh CLI is authenticated: gh auth login"));
968
- }
969
- } else if (!options.copy) {
970
- console.log(chalk5.gray("\nTip: Use --copy to copy to clipboard"));
971
- }
1047
+ console.log(chalk4.bold("\n\u{1F4CB} Generated Changelog\n"));
1048
+ console.log(chalk4.gray("\u2500".repeat(50)));
1049
+ console.log(formatChangelog(changelog));
1050
+ console.log(chalk4.gray("\u2500".repeat(50)));
1051
+ console.log(chalk4.gray(`
1052
+ Range: ${fromRef}..${toRef} (${commits.length} commits)`));
1053
+ if (template) {
1054
+ console.log(chalk4.gray("Style matched from existing CHANGELOG.md"));
972
1055
  }
973
1056
  } catch (error) {
974
- spinner.fail("Failed to generate PR description");
975
- console.error(chalk5.red(error instanceof Error ? error.message : "Unknown error"));
1057
+ spinner.fail("Failed to generate changelog");
1058
+ console.error(chalk4.red(error instanceof Error ? error.message : "Unknown error"));
976
1059
  process.exit(1);
977
1060
  }
978
1061
  });
979
1062
 
980
- // src/commands/review.ts
981
- import { Command as Command5 } from "commander";
982
- import chalk6 from "chalk";
983
- import ora4 from "ora";
984
- import { simpleGit as simpleGit4 } from "simple-git";
985
- import { execSync as execSync3 } from "child_process";
986
- async function getPRDiff(prNumber) {
987
- try {
988
- const diff = execSync3(`gh pr diff ${prNumber}`, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
989
- const prJsonStr = execSync3(`gh pr view ${prNumber} --json number,title,author,url`, { encoding: "utf-8" });
990
- const prJson = JSON.parse(prJsonStr);
991
- return {
992
- diff,
993
- prInfo: {
994
- number: prJson.number,
995
- title: prJson.title,
996
- author: prJson.author.login,
997
- url: prJson.url
998
- }
999
- };
1000
- } catch (error) {
1001
- if (error instanceof Error && error.message.includes("gh: command not found")) {
1002
- throw new Error("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/");
1003
- }
1004
- throw error;
1005
- }
1006
- }
1007
- 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) => {
1008
- const git = simpleGit4();
1063
+ // src/commands/checkout.ts
1064
+ import chalk5 from "chalk";
1065
+ import { Command as Command4 } from "commander";
1066
+ import ora3 from "ora";
1067
+ import { simpleGit as simpleGit3 } from "simple-git";
1068
+ var checkoutCommand = new Command4("checkout").description("Generate a branch name from current diff and checkout").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic, ollama)").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) => {
1069
+ const git = simpleGit3();
1009
1070
  const isRepo = await git.checkIsRepo();
1010
1071
  if (!isRepo) {
1011
- console.error(chalk6.red("Error: Not a git repository"));
1072
+ console.error(chalk5.red("Error: Not a git repository"));
1012
1073
  process.exit(1);
1013
1074
  }
1014
- const provider = options.provider.toLowerCase();
1015
- const spinner = ora4("Getting diff...").start();
1075
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1076
+ const spinner = ora3("Analyzing changes...").start();
1077
+ const status = await git.status();
1078
+ let diff;
1079
+ if (options.staged) {
1080
+ diff = await git.diff(["--cached"]);
1081
+ } else {
1082
+ const stagedDiff = await git.diff(["--cached"]);
1083
+ const unstagedDiff = await git.diff();
1084
+ diff = `${stagedDiff}
1085
+ ${unstagedDiff}`;
1086
+ }
1087
+ const hasChanges = diff.trim() || status.not_added.length > 0 || status.created.length > 0;
1088
+ if (!hasChanges) {
1089
+ spinner.fail("No changes found");
1090
+ console.log(chalk5.gray("Make some changes first, then run gut checkout"));
1091
+ process.exit(1);
1092
+ }
1093
+ if (!diff.trim() && (status.not_added.length > 0 || status.created.length > 0)) {
1094
+ const untrackedFiles = [...status.not_added, ...status.created];
1095
+ diff = `New files:
1096
+ ${untrackedFiles.map((f) => `+ ${f}`).join("\n")}`;
1097
+ }
1098
+ spinner.text = "Generating branch name...";
1099
+ const provider = await resolveProvider(options.provider);
1100
+ const template = findTemplate(repoRoot.trim(), "checkout");
1101
+ if (template) {
1102
+ console.log(chalk5.gray("\nUsing template from project..."));
1103
+ }
1016
1104
  try {
1017
- let diff;
1018
- let prInfo = null;
1019
- if (prNumber) {
1020
- spinner.stop();
1021
- if (!requireGhCli()) {
1022
- process.exit(1);
1023
- }
1024
- spinner.start(`Fetching PR #${prNumber}...`);
1025
- const result = await getPRDiff(prNumber);
1026
- diff = result.diff;
1027
- prInfo = result.prInfo;
1028
- spinner.text = `Reviewing PR #${prNumber}...`;
1029
- } else if (options.commit) {
1030
- diff = await git.diff([`${options.commit}^`, options.commit]);
1031
- spinner.text = `Reviewing commit ${options.commit.slice(0, 7)}...`;
1032
- } else if (options.staged) {
1033
- diff = await git.diff(["--cached"]);
1034
- spinner.text = "Reviewing staged changes...";
1035
- } else {
1036
- diff = await git.diff();
1037
- const stagedDiff = await git.diff(["--cached"]);
1038
- diff = stagedDiff + "\n" + diff;
1039
- spinner.text = "Reviewing uncommitted changes...";
1040
- }
1041
- if (!diff.trim()) {
1042
- spinner.info("No changes to review");
1043
- process.exit(0);
1044
- }
1045
- spinner.text = "AI is reviewing your code...";
1046
- const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1047
- const template = findTemplate(repoRoot.trim(), "review");
1048
- const review = await generateCodeReview(
1105
+ const branchName = await generateBranchNameFromDiff(
1049
1106
  diff,
1050
1107
  { provider, model: options.model },
1051
- template || void 0
1108
+ template
1052
1109
  );
1053
1110
  spinner.stop();
1054
- if (options.json) {
1055
- console.log(JSON.stringify({ prInfo, review }, null, 2));
1056
- return;
1057
- }
1058
- if (prInfo) {
1059
- console.log(chalk6.bold(`
1060
- \u{1F517} PR #${prInfo.number}: ${prInfo.title}`));
1061
- console.log(chalk6.gray(` by ${prInfo.author} - ${prInfo.url}`));
1111
+ console.log(chalk5.bold("\nGenerated branch name:\n"));
1112
+ console.log(chalk5.green(` ${branchName}`));
1113
+ console.log();
1114
+ if (options.yes) {
1115
+ await git.checkoutLocalBranch(branchName);
1116
+ console.log(chalk5.green(`\u2713 Created and checked out branch: ${branchName}`));
1117
+ } else {
1118
+ const readline = await import("readline");
1119
+ const rl = readline.createInterface({
1120
+ input: process.stdin,
1121
+ output: process.stdout
1122
+ });
1123
+ const answer = await new Promise((resolve) => {
1124
+ rl.question(chalk5.cyan("Create and checkout this branch? (y/N) "), resolve);
1125
+ });
1126
+ rl.close();
1127
+ if (answer.toLowerCase() === "y") {
1128
+ await git.checkoutLocalBranch(branchName);
1129
+ console.log(chalk5.green(`\u2713 Created and checked out branch: ${branchName}`));
1130
+ } else {
1131
+ console.log(chalk5.gray("\nTo create manually:"));
1132
+ console.log(chalk5.gray(` git checkout -b ${branchName}`));
1133
+ }
1062
1134
  }
1063
- printReview(review);
1064
1135
  } catch (error) {
1065
- spinner.fail("Failed to generate review");
1066
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1136
+ spinner.fail("Failed to generate branch name");
1137
+ console.error(chalk5.red(error instanceof Error ? error.message : "Unknown error"));
1067
1138
  process.exit(1);
1068
1139
  }
1069
1140
  });
1070
- function printReview(review) {
1071
- console.log(chalk6.bold("\n\u{1F50D} AI Code Review\n"));
1072
- console.log(chalk6.cyan("Summary:"));
1073
- console.log(` ${review.summary}
1074
- `);
1075
- if (review.issues.length > 0) {
1076
- console.log(chalk6.cyan("Issues Found:"));
1077
- for (const issue of review.issues) {
1078
- const severityColors = {
1079
- critical: chalk6.red,
1080
- warning: chalk6.yellow,
1081
- suggestion: chalk6.blue
1082
- };
1083
- const severityIcons = {
1084
- critical: "\u{1F534}",
1085
- warning: "\u{1F7E1}",
1086
- suggestion: "\u{1F4A1}"
1087
- };
1088
- const color = severityColors[issue.severity];
1089
- const icon = severityIcons[issue.severity];
1090
- console.log(`
1091
- ${icon} ${color(issue.severity.toUpperCase())}`);
1092
- console.log(` ${chalk6.gray("File:")} ${issue.file}${issue.line ? `:${issue.line}` : ""}`);
1093
- console.log(` ${issue.message}`);
1094
- if (issue.suggestion) {
1095
- console.log(` ${chalk6.green("\u2192")} ${issue.suggestion}`);
1141
+
1142
+ // src/commands/cleanup.ts
1143
+ import chalk6 from "chalk";
1144
+ import { Command as Command5 } from "commander";
1145
+ import ora4 from "ora";
1146
+ import { simpleGit as simpleGit4 } from "simple-git";
1147
+ var cleanupCommand = new Command5("cleanup").description("Delete merged branches safely").option("-r, --remote", "Also delete remote branches").option("-f, --force", "Skip confirmation prompt").option("--dry-run", "Show branches that would be deleted without deleting").option("--base <branch>", "Base branch to compare against (default: main or master)").action(async (options) => {
1148
+ const git = simpleGit4();
1149
+ const isRepo = await git.checkIsRepo();
1150
+ if (!isRepo) {
1151
+ console.error(chalk6.red("Error: Not a git repository"));
1152
+ process.exit(1);
1153
+ }
1154
+ const spinner = ora4("Fetching branch information...").start();
1155
+ try {
1156
+ await git.fetch(["--prune"]);
1157
+ const currentBranch = (await git.branch()).current;
1158
+ const baseBranch = options.base || await detectBaseBranch(git);
1159
+ spinner.text = `Using ${chalk6.cyan(baseBranch)} as base branch`;
1160
+ const mergedResult = await git.branch(["--merged", baseBranch]);
1161
+ const mergedBranches = mergedResult.all.filter((branch) => {
1162
+ const cleanName = branch.trim().replace(/^\* /, "");
1163
+ return cleanName !== currentBranch && cleanName !== baseBranch && !cleanName.startsWith("remotes/") && cleanName !== "main" && cleanName !== "master" && cleanName !== "develop";
1164
+ });
1165
+ spinner.stop();
1166
+ if (mergedBranches.length === 0) {
1167
+ console.log(chalk6.green("\u2713 No merged branches to clean up"));
1168
+ return;
1169
+ }
1170
+ console.log(chalk6.yellow(`
1171
+ Found ${mergedBranches.length} merged branch(es):
1172
+ `));
1173
+ mergedBranches.forEach((branch) => {
1174
+ console.log(` ${chalk6.red("\u2022")} ${branch}`);
1175
+ });
1176
+ if (options.dryRun) {
1177
+ console.log(chalk6.blue("\n(dry-run mode - no branches were deleted)"));
1178
+ return;
1179
+ }
1180
+ if (!options.force) {
1181
+ const readline = await import("readline");
1182
+ const rl = readline.createInterface({
1183
+ input: process.stdin,
1184
+ output: process.stdout
1185
+ });
1186
+ const answer = await new Promise((resolve) => {
1187
+ rl.question(chalk6.yellow("\nDelete these branches? (y/N) "), resolve);
1188
+ });
1189
+ rl.close();
1190
+ if (answer.toLowerCase() !== "y") {
1191
+ console.log(chalk6.gray("Cancelled"));
1192
+ return;
1096
1193
  }
1097
1194
  }
1098
- } else {
1099
- console.log(chalk6.green(" \u2713 No issues found!\n"));
1100
- }
1101
- if (review.positives.length > 0) {
1102
- console.log(chalk6.cyan("\nGood Practices:"));
1103
- for (const positive of review.positives) {
1104
- console.log(` ${chalk6.green("\u2713")} ${positive}`);
1195
+ const deleteSpinner = ora4("Deleting branches...").start();
1196
+ for (const branch of mergedBranches) {
1197
+ try {
1198
+ await git.deleteLocalBranch(branch, true);
1199
+ deleteSpinner.text = `Deleted ${branch}`;
1200
+ if (options.remote) {
1201
+ try {
1202
+ await git.push("origin", `:${branch}`);
1203
+ } catch {
1204
+ }
1205
+ }
1206
+ } catch {
1207
+ deleteSpinner.warn(`Failed to delete ${branch}`);
1208
+ }
1105
1209
  }
1210
+ deleteSpinner.succeed(chalk6.green(`Deleted ${mergedBranches.length} branch(es)`));
1211
+ } catch (err) {
1212
+ spinner.fail("Failed to cleanup branches");
1213
+ console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
1214
+ process.exit(1);
1106
1215
  }
1107
- const criticalCount = review.issues.filter((i) => i.severity === "critical").length;
1108
- const warningCount = review.issues.filter((i) => i.severity === "warning").length;
1109
- const suggestionCount = review.issues.filter((i) => i.severity === "suggestion").length;
1110
- console.log(chalk6.gray("\n\u2500".repeat(40)));
1111
- console.log(
1112
- ` ${chalk6.red(criticalCount)} critical ${chalk6.yellow(warningCount)} warnings ${chalk6.blue(suggestionCount)} suggestions`
1113
- );
1114
- console.log();
1216
+ });
1217
+ async function detectBaseBranch(git) {
1218
+ const branches = await git.branch();
1219
+ if (branches.all.includes("main")) return "main";
1220
+ if (branches.all.includes("master")) return "master";
1221
+ return "main";
1115
1222
  }
1116
1223
 
1117
- // src/commands/merge.ts
1118
- import { Command as Command6 } from "commander";
1224
+ // src/commands/commit.ts
1119
1225
  import chalk7 from "chalk";
1226
+ import { Command as Command6 } from "commander";
1120
1227
  import ora5 from "ora";
1121
1228
  import { simpleGit as simpleGit5 } from "simple-git";
1122
- import * as fs from "fs";
1123
- import * as path from "path";
1124
- 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) => {
1229
+ var commitCommand = new Command6("commit").description("Generate a commit message using AI").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic, ollama)").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) => {
1125
1230
  const git = simpleGit5();
1231
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1126
1232
  const isRepo = await git.checkIsRepo();
1127
1233
  if (!isRepo) {
1128
1234
  console.error(chalk7.red("Error: Not a git repository"));
1129
1235
  process.exit(1);
1130
1236
  }
1131
- const provider = options.provider.toLowerCase();
1132
- const status = await git.status();
1133
- if (status.modified.length > 0 || status.staged.length > 0) {
1134
- console.error(chalk7.red("Error: Working directory has uncommitted changes"));
1135
- console.log(chalk7.gray("Please commit or stash your changes first"));
1136
- process.exit(1);
1137
- }
1138
- const branchInfo = await git.branch();
1139
- const currentBranch = branchInfo.current;
1140
- console.log(chalk7.bold(`
1141
- Merging ${chalk7.cyan(branch)} into ${chalk7.cyan(currentBranch)}...
1142
- `));
1143
- try {
1144
- await git.merge([branch]);
1145
- console.log(chalk7.green("\u2713 Merged successfully (no conflicts)"));
1146
- return;
1147
- } catch (error) {
1237
+ const provider = await resolveProvider(options.provider);
1238
+ if (options.all) {
1239
+ await git.add("-A");
1148
1240
  }
1149
- const conflictStatus = await git.status();
1150
- const conflictedFiles = conflictStatus.conflicted;
1151
- if (conflictedFiles.length === 0) {
1152
- console.error(chalk7.red("Merge failed for unknown reason"));
1153
- await git.merge(["--abort"]);
1154
- process.exit(1);
1241
+ let diff = await git.diff(["--cached"]);
1242
+ if (!diff.trim()) {
1243
+ const status = await git.status();
1244
+ const unstaged = await git.diff();
1245
+ const hasUntracked = status.not_added.length > 0 || status.created.length > 0;
1246
+ if (!unstaged.trim() && !hasUntracked) {
1247
+ console.error(chalk7.yellow("No changes to commit."));
1248
+ process.exit(1);
1249
+ }
1250
+ console.log(chalk7.gray("No staged changes, staging all changes..."));
1251
+ await git.add("-A");
1252
+ diff = await git.diff(["--cached"]);
1155
1253
  }
1156
- console.log(chalk7.yellow(`\u26A0 ${conflictedFiles.length} conflict(s) detected
1157
- `));
1158
- const spinner = ora5();
1159
- const rootDir = await git.revparse(["--show-toplevel"]);
1160
- const template = findTemplate(rootDir.trim(), "merge");
1254
+ const template = findTemplate(repoRoot.trim(), "commit");
1161
1255
  if (template) {
1162
- console.log(chalk7.gray("Using merge template from project...\n"));
1256
+ console.log(chalk7.gray("Using template from project..."));
1163
1257
  }
1164
- for (const file of conflictedFiles) {
1165
- const filePath = path.join(rootDir.trim(), file);
1166
- const content = fs.readFileSync(filePath, "utf-8");
1167
- console.log(chalk7.bold(`
1168
- \u{1F4C4} ${file}`));
1169
- const conflictMatch = content.match(/<<<<<<< HEAD[\s\S]*?>>>>>>>.+/g);
1170
- if (conflictMatch) {
1171
- console.log(chalk7.gray("\u2500".repeat(50)));
1172
- console.log(chalk7.gray(conflictMatch[0].slice(0, 500)));
1173
- if (conflictMatch[0].length > 500) console.log(chalk7.gray("..."));
1174
- console.log(chalk7.gray("\u2500".repeat(50)));
1258
+ const spinner = ora5("Generating commit message...").start();
1259
+ try {
1260
+ const message = await generateCommitMessage(
1261
+ diff,
1262
+ { provider, model: options.model },
1263
+ template || void 0
1264
+ );
1265
+ spinner.stop();
1266
+ console.log(chalk7.bold("\nGenerated commit message:\n"));
1267
+ console.log(chalk7.green(` ${message.split("\n")[0]}`));
1268
+ if (message.includes("\n")) {
1269
+ const details = message.split("\n").slice(1).join("\n");
1270
+ console.log(
1271
+ chalk7.gray(
1272
+ details.split("\n").map((l) => ` ${l}`).join("\n")
1273
+ )
1274
+ );
1175
1275
  }
1176
- spinner.start("AI is analyzing conflict...");
1177
- try {
1178
- const resolution = await resolveConflict(content, {
1179
- filename: file,
1180
- oursRef: currentBranch,
1181
- theirsRef: branch
1182
- }, { provider, model: options.model }, template || void 0);
1183
- spinner.stop();
1184
- console.log(chalk7.cyan("\n\u{1F916} AI suggests:"));
1185
- console.log(chalk7.gray("\u2500".repeat(50)));
1186
- const preview = resolution.resolvedContent.slice(0, 800);
1187
- console.log(preview);
1188
- if (resolution.resolvedContent.length > 800) console.log(chalk7.gray("..."));
1189
- console.log(chalk7.gray("\u2500".repeat(50)));
1190
- console.log(chalk7.gray(`Strategy: ${resolution.strategy}`));
1191
- console.log(chalk7.gray(`Reason: ${resolution.explanation}`));
1276
+ console.log();
1277
+ if (options.commit) {
1278
+ await git.commit(message);
1279
+ console.log(chalk7.green("\u2713 Committed successfully"));
1280
+ } else {
1192
1281
  const readline = await import("readline");
1193
1282
  const rl = readline.createInterface({
1194
1283
  input: process.stdin,
1195
1284
  output: process.stdout
1196
1285
  });
1197
1286
  const answer = await new Promise((resolve) => {
1198
- rl.question(chalk7.cyan("\nAccept this resolution? (y/n/s to skip) "), resolve);
1287
+ rl.question(chalk7.cyan("Commit with this message? (y/N/e to edit) "), resolve);
1199
1288
  });
1200
1289
  rl.close();
1201
1290
  if (answer.toLowerCase() === "y") {
1202
- fs.writeFileSync(filePath, resolution.resolvedContent);
1203
- await git.add(file);
1204
- console.log(chalk7.green(`\u2713 Resolved ${file}`));
1205
- } else if (answer.toLowerCase() === "s") {
1206
- console.log(chalk7.yellow(`\u23ED Skipped ${file}`));
1207
- } else {
1208
- console.log(chalk7.yellow(`\u2717 Rejected - resolve manually: ${file}`));
1209
- }
1210
- } catch (error) {
1211
- spinner.fail("AI resolution failed");
1212
- console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
1213
- console.log(chalk7.yellow(`Please resolve manually: ${file}`));
1214
- }
1215
- }
1216
- const finalStatus = await git.status();
1217
- if (finalStatus.conflicted.length > 0) {
1218
- console.log(chalk7.yellow(`
1219
- \u26A0 ${finalStatus.conflicted.length} conflict(s) remaining`));
1220
- console.log(chalk7.gray("Resolve manually and run: git add <files> && git commit"));
1221
- } else if (options.commit !== false) {
1222
- await git.commit(`Merge branch '${branch}' into ${currentBranch}`);
1223
- console.log(chalk7.green("\n\u2713 All conflicts resolved and committed"));
1224
- } else {
1225
- console.log(chalk7.green("\n\u2713 All conflicts resolved"));
1226
- console.log(chalk7.gray("Run: git commit"));
1227
- }
1291
+ await git.commit(message);
1292
+ console.log(chalk7.green("\u2713 Committed successfully"));
1293
+ } else if (answer.toLowerCase() === "e") {
1294
+ console.log(chalk7.gray("Opening editor..."));
1295
+ const { execSync: execSync9 } = await import("child_process");
1296
+ const editor = process.env.EDITOR || process.env.VISUAL || "vi";
1297
+ const fs2 = await import("fs");
1298
+ const os = await import("os");
1299
+ const path2 = await import("path");
1300
+ const tmpFile = path2.join(os.tmpdir(), "gut-commit-msg.txt");
1301
+ fs2.writeFileSync(tmpFile, message);
1302
+ execSync9(`${editor} "${tmpFile}"`, { stdio: "inherit" });
1303
+ const editedMessage = fs2.readFileSync(tmpFile, "utf-8").trim();
1304
+ fs2.unlinkSync(tmpFile);
1305
+ if (editedMessage) {
1306
+ await git.commit(editedMessage);
1307
+ console.log(chalk7.green("\u2713 Committed successfully"));
1308
+ } else {
1309
+ console.log(chalk7.yellow("Commit cancelled (empty message)"));
1310
+ }
1311
+ } else {
1312
+ console.log(chalk7.gray("Commit cancelled"));
1313
+ console.log(chalk7.gray("\nTo commit manually:"));
1314
+ console.log(chalk7.gray(` git commit -m "${message.split("\n")[0]}"`));
1315
+ }
1316
+ }
1317
+ } catch (error) {
1318
+ spinner.fail("Failed to generate commit message");
1319
+ console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
1320
+ process.exit(1);
1321
+ }
1228
1322
  });
1229
1323
 
1230
- // src/commands/changelog.ts
1231
- import { Command as Command7 } from "commander";
1324
+ // src/commands/config.ts
1325
+ import { execSync as execSync4 } from "child_process";
1326
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
1327
+ import { homedir as homedir3 } from "os";
1328
+ import { join as join3 } from "path";
1232
1329
  import chalk8 from "chalk";
1233
- import ora6 from "ora";
1330
+ import { Command as Command7 } from "commander";
1234
1331
  import { simpleGit as simpleGit6 } from "simple-git";
1235
- function formatChangelog(changelog) {
1236
- const lines = [];
1237
- const header = changelog.version ? `## [${changelog.version}] - ${changelog.date}` : `## ${changelog.date}`;
1238
- lines.push(header);
1239
- lines.push("");
1240
- if (changelog.summary) {
1241
- lines.push(changelog.summary);
1242
- lines.push("");
1243
- }
1244
- for (const section of changelog.sections) {
1245
- if (section.items.length > 0) {
1246
- lines.push(`### ${section.type}`);
1247
- for (const item of section.items) {
1248
- lines.push(`- ${item}`);
1249
- }
1250
- lines.push("");
1251
- }
1252
- }
1253
- return lines.join("\n");
1332
+ function openFolder(path2) {
1333
+ const platform = process.platform;
1334
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? 'start ""' : "xdg-open";
1335
+ execSync4(`${cmd} "${path2}"`);
1254
1336
  }
1255
- var changelogCommand = new Command7("changelog").description("Generate a changelog from commits between refs").argument("[from]", "Starting ref (tag, branch, commit)", "HEAD~10").argument("[to]", "Ending ref", "HEAD").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-t, --tag <tag>", "Generate changelog since this tag").option("--json", "Output as JSON").action(async (from, to, options) => {
1256
- const git = simpleGit6();
1257
- const isRepo = await git.checkIsRepo();
1258
- if (!isRepo) {
1259
- console.error(chalk8.red("Error: Not a git repository"));
1260
- process.exit(1);
1261
- }
1262
- const provider = options.provider.toLowerCase();
1263
- const spinner = ora6("Analyzing commits...").start();
1264
- try {
1265
- let fromRef = from;
1266
- let toRef = to;
1267
- if (options.tag) {
1268
- fromRef = options.tag;
1269
- toRef = "HEAD";
1337
+ var configCommand = new Command7("config").description("Manage gut configuration");
1338
+ configCommand.command("set <key> <value>").description("Set a configuration value").option("--local", "Set for current repository only").action((key, value, options) => {
1339
+ if (key === "lang") {
1340
+ if (!isValidLanguage(value)) {
1341
+ console.error(chalk8.red(`Invalid language: ${value}`));
1342
+ console.error(chalk8.gray(`Valid languages: ${VALID_LANGUAGES.join(", ")}`));
1343
+ process.exit(1);
1270
1344
  }
1271
- const log = await git.log({ from: fromRef, to: toRef });
1272
- if (log.all.length === 0) {
1273
- spinner.info("No commits found in range");
1274
- process.exit(0);
1345
+ try {
1346
+ setLanguage(value, options.local ?? false);
1347
+ const scope = options.local ? "(local)" : "(global)";
1348
+ console.log(chalk8.green(`\u2713 Language set to: ${value} ${scope}`));
1349
+ } catch (err) {
1350
+ console.error(chalk8.red(err.message));
1351
+ process.exit(1);
1275
1352
  }
1276
- spinner.text = `Found ${log.all.length} commits, generating changelog...`;
1277
- const commits = log.all.map((c) => ({
1278
- hash: c.hash,
1279
- message: c.message,
1280
- author: c.author_name,
1281
- date: c.date
1282
- }));
1283
- const diff = await git.diff([`${fromRef}...${toRef}`]);
1284
- const repoRoot = await git.revparse(["--show-toplevel"]);
1285
- const template = findTemplate(repoRoot.trim(), "changelog");
1286
- if (template) {
1287
- spinner.text = "Using template from project...";
1353
+ } else if (key === "model") {
1354
+ try {
1355
+ setModel(value, options.local ?? false);
1356
+ const scope = options.local ? "(local)" : "(global)";
1357
+ console.log(chalk8.green(`\u2713 Model set to: ${value} ${scope}`));
1358
+ console.log(
1359
+ chalk8.gray(
1360
+ `Default models: ${Object.entries(DEFAULT_MODELS).map(([k, v]) => `${k}=${v}`).join(", ")}`
1361
+ )
1362
+ );
1363
+ } catch (err) {
1364
+ console.error(chalk8.red(err.message));
1365
+ process.exit(1);
1288
1366
  }
1289
- const changelog = await generateChangelog(
1290
- { commits, diff, fromRef, toRef },
1291
- { provider, model: options.model },
1292
- template || void 0
1293
- );
1294
- spinner.stop();
1295
- if (options.json) {
1296
- console.log(JSON.stringify(changelog, null, 2));
1297
- return;
1367
+ } else if (key === "provider") {
1368
+ if (!isValidProvider(value)) {
1369
+ console.error(chalk8.red(`Invalid provider: ${value}`));
1370
+ console.error(chalk8.gray(`Valid providers: ${VALID_PROVIDERS.join(", ")}`));
1371
+ process.exit(1);
1298
1372
  }
1299
- console.log(chalk8.bold("\n\u{1F4CB} Generated Changelog\n"));
1300
- console.log(chalk8.gray("\u2500".repeat(50)));
1301
- console.log(formatChangelog(changelog));
1302
- console.log(chalk8.gray("\u2500".repeat(50)));
1303
- console.log(chalk8.gray(`
1304
- Range: ${fromRef}..${toRef} (${commits.length} commits)`));
1305
- if (template) {
1306
- console.log(chalk8.gray("Style matched from existing CHANGELOG.md"));
1373
+ try {
1374
+ setProvider(value, options.local ?? false);
1375
+ const scope = options.local ? "(local)" : "(global)";
1376
+ console.log(chalk8.green(`\u2713 Provider set to: ${value} ${scope}`));
1377
+ } catch (err) {
1378
+ console.error(chalk8.red(err.message));
1379
+ process.exit(1);
1380
+ }
1381
+ } else {
1382
+ console.error(chalk8.red(`Unknown config key: ${key}`));
1383
+ console.error(chalk8.gray("Available keys: lang, model, provider"));
1384
+ process.exit(1);
1385
+ }
1386
+ });
1387
+ configCommand.command("get <key>").description("Get a configuration value").action((key) => {
1388
+ const config = getConfig();
1389
+ if (key in config) {
1390
+ console.log(config[key]);
1391
+ } else {
1392
+ console.error(chalk8.red(`Unknown config key: ${key}`));
1393
+ process.exit(1);
1394
+ }
1395
+ });
1396
+ configCommand.command("list").description("List all configuration values").action(() => {
1397
+ const localConfig = getLocalConfig();
1398
+ const effectiveConfig = getConfig();
1399
+ console.log(chalk8.bold("Configuration:"));
1400
+ console.log();
1401
+ for (const key of Object.keys(effectiveConfig)) {
1402
+ const value = effectiveConfig[key];
1403
+ const isLocal = key in localConfig;
1404
+ const scope = isLocal ? chalk8.cyan(" (local)") : chalk8.gray(" (global)");
1405
+ console.log(` ${chalk8.cyan(key)}: ${value}${scope}`);
1406
+ }
1407
+ if (Object.keys(localConfig).length > 0) {
1408
+ console.log();
1409
+ console.log(chalk8.gray("Local config: .gut/config.json"));
1410
+ }
1411
+ });
1412
+ 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) => {
1413
+ const git = simpleGit6();
1414
+ const isLocal = options.local === true;
1415
+ let targetPath;
1416
+ if (isLocal) {
1417
+ const isRepo = await git.checkIsRepo();
1418
+ if (!isRepo) {
1419
+ console.error(chalk8.red("Error: Not a git repository"));
1420
+ console.error(chalk8.gray("Use --global to open global config folder"));
1421
+ process.exit(1);
1307
1422
  }
1423
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1424
+ targetPath = join3(repoRoot.trim(), ".gut");
1425
+ } else {
1426
+ if (options.templates) {
1427
+ targetPath = join3(homedir3(), ".config", "gut", "templates");
1428
+ } else {
1429
+ targetPath = join3(homedir3(), ".config", "gut");
1430
+ }
1431
+ }
1432
+ if (!existsSync3(targetPath)) {
1433
+ mkdirSync2(targetPath, { recursive: true });
1434
+ console.log(chalk8.green(`Created ${targetPath}`));
1435
+ }
1436
+ try {
1437
+ openFolder(targetPath);
1438
+ console.log(chalk8.green(`Opened: ${targetPath}`));
1308
1439
  } catch (error) {
1309
- spinner.fail("Failed to generate changelog");
1310
- console.error(chalk8.red(error instanceof Error ? error.message : "Unknown error"));
1440
+ console.error(chalk8.red(`Failed to open folder: ${targetPath}`));
1441
+ console.error(chalk8.gray(error.message));
1311
1442
  process.exit(1);
1312
1443
  }
1313
1444
  });
1314
1445
 
1315
1446
  // src/commands/explain.ts
1316
- import { Command as Command8 } from "commander";
1447
+ import { execSync as execSync5 } from "child_process";
1448
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1317
1449
  import chalk9 from "chalk";
1318
- import ora7 from "ora";
1450
+ import { Command as Command8 } from "commander";
1451
+ import ora6 from "ora";
1319
1452
  import { simpleGit as simpleGit7 } from "simple-git";
1320
- import { execSync as execSync4 } from "child_process";
1321
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
1322
- 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) => {
1453
+ var explainCommand = new Command8("explain").description("Get an AI-powered explanation of changes, commits, PRs, or files").argument(
1454
+ "[target]",
1455
+ "Commit hash, PR number, PR URL, or file path (default: uncommitted changes)"
1456
+ ).option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic, ollama)").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) => {
1323
1457
  const git = simpleGit7();
1324
1458
  const isRepo = await git.checkIsRepo();
1325
1459
  if (!isRepo) {
1326
1460
  console.error(chalk9.red("Error: Not a git repository"));
1327
1461
  process.exit(1);
1328
1462
  }
1329
- const provider = options.provider.toLowerCase();
1463
+ const provider = await resolveProvider(options.provider);
1330
1464
  const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1331
- const spinner = ora7("Analyzing...").start();
1465
+ const spinner = ora6("Analyzing...").start();
1332
1466
  try {
1333
1467
  let context;
1334
1468
  if (!target || options.staged) {
@@ -1339,7 +1473,7 @@ var explainCommand = new Command8("explain").description("Get an AI-powered expl
1339
1473
  }
1340
1474
  } else {
1341
1475
  const isPR = target.match(/^#?\d+$/) || target.includes("/pull/");
1342
- const isFile = existsSync3(target);
1476
+ const isFile = existsSync4(target);
1343
1477
  if (isPR) {
1344
1478
  spinner.stop();
1345
1479
  if (!requireGhCli()) {
@@ -1349,7 +1483,12 @@ var explainCommand = new Command8("explain").description("Get an AI-powered expl
1349
1483
  context = await getPRContext(target, spinner);
1350
1484
  } else if (isFile) {
1351
1485
  if (options.history) {
1352
- context = await getFileHistoryContext(target, git, spinner, parseInt(options.commits, 10));
1486
+ context = await getFileHistoryContext(
1487
+ target,
1488
+ git,
1489
+ spinner,
1490
+ parseInt(options.commits, 10)
1491
+ );
1353
1492
  } else {
1354
1493
  context = await getFileContentContext(target, spinner);
1355
1494
  }
@@ -1385,7 +1524,8 @@ async function getUncommittedContext(git, spinner) {
1385
1524
  spinner.text = "Analyzing uncommitted changes...";
1386
1525
  const stagedDiff = await git.diff(["--cached"]);
1387
1526
  const unstagedDiff = await git.diff();
1388
- const diff = (stagedDiff + "\n" + unstagedDiff).trim();
1527
+ const diff = `${stagedDiff}
1528
+ ${unstagedDiff}`.trim();
1389
1529
  if (!diff) {
1390
1530
  throw new Error("No uncommitted changes to analyze");
1391
1531
  }
@@ -1430,7 +1570,7 @@ async function getCommitContext(hash, git, spinner) {
1430
1570
  }
1431
1571
  async function getFileContentContext(filePath, spinner) {
1432
1572
  spinner.text = `Reading ${filePath}...`;
1433
- const content = readFileSync4(filePath, "utf-8");
1573
+ const content = readFileSync3(filePath, "utf-8");
1434
1574
  return {
1435
1575
  type: "file-content",
1436
1576
  title: filePath,
@@ -1481,18 +1621,20 @@ async function getPRContext(target, spinner) {
1481
1621
  spinner.text = `Fetching PR #${prNumber}...`;
1482
1622
  let prInfo;
1483
1623
  try {
1484
- const prJson = execSync4(
1624
+ const prJson = execSync5(
1485
1625
  `gh pr view ${prNumber} --json title,url,baseRefName,headRefName,commits`,
1486
1626
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
1487
1627
  );
1488
1628
  prInfo = JSON.parse(prJson);
1489
1629
  } catch {
1490
- throw new Error(`Failed to fetch PR #${prNumber}. Make sure gh CLI is installed and authenticated.`);
1630
+ throw new Error(
1631
+ `Failed to fetch PR #${prNumber}. Make sure gh CLI is installed and authenticated.`
1632
+ );
1491
1633
  }
1492
1634
  spinner.text = `Getting diff for PR #${prNumber}...`;
1493
1635
  let diff;
1494
1636
  try {
1495
- diff = execSync4(`gh pr diff ${prNumber}`, {
1637
+ diff = execSync5(`gh pr diff ${prNumber}`, {
1496
1638
  encoding: "utf-8",
1497
1639
  stdio: ["pipe", "pipe", "pipe"],
1498
1640
  maxBuffer: 10 * 1024 * 1024
@@ -1555,20 +1697,23 @@ ${icon} Explanation
1555
1697
  }
1556
1698
 
1557
1699
  // src/commands/find.ts
1558
- import { Command as Command9 } from "commander";
1559
1700
  import chalk10 from "chalk";
1560
- import ora8 from "ora";
1701
+ import { Command as Command9 } from "commander";
1702
+ import ora7 from "ora";
1561
1703
  import { simpleGit as simpleGit8 } from "simple-git";
1562
- 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) => {
1704
+ var findCommand = new Command9("find").description("Find commits matching a vague description using AI").argument(
1705
+ "<query>",
1706
+ 'Description of the change you are looking for (e.g., "login feature added")'
1707
+ ).option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic, ollama)").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) => {
1563
1708
  const git = simpleGit8();
1564
1709
  const isRepo = await git.checkIsRepo();
1565
1710
  if (!isRepo) {
1566
1711
  console.error(chalk10.red("Error: Not a git repository"));
1567
1712
  process.exit(1);
1568
1713
  }
1569
- const provider = options.provider.toLowerCase();
1714
+ const provider = await resolveProvider(options.provider);
1570
1715
  const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1571
- const spinner = ora8("Searching commits...").start();
1716
+ const spinner = ora7("Searching commits...").start();
1572
1717
  try {
1573
1718
  const logOptions = [`-n`, options.num];
1574
1719
  if (options.path) {
@@ -1649,1150 +1794,1220 @@ function printResults(results, query) {
1649
1794
  }
1650
1795
  }
1651
1796
 
1652
- // src/commands/branch.ts
1653
- import { Command as Command10 } from "commander";
1797
+ // src/commands/gitignore.ts
1798
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1799
+ import { join as join4 } from "path";
1654
1800
  import chalk11 from "chalk";
1655
- import ora9 from "ora";
1801
+ import { Command as Command10 } from "commander";
1802
+ import ora8 from "ora";
1656
1803
  import { simpleGit as simpleGit9 } from "simple-git";
1657
- import { execSync as execSync5 } from "child_process";
1658
- function getIssueInfo(issueNumber) {
1804
+ var CONFIG_FILES = [
1805
+ // JavaScript/TypeScript
1806
+ "package.json",
1807
+ "tsconfig.json",
1808
+ "vite.config.ts",
1809
+ "vite.config.js",
1810
+ "next.config.js",
1811
+ "next.config.mjs",
1812
+ "nuxt.config.ts",
1813
+ "astro.config.mjs",
1814
+ // Python
1815
+ "pyproject.toml",
1816
+ "setup.py",
1817
+ "requirements.txt",
1818
+ "Pipfile",
1819
+ "poetry.lock",
1820
+ // Go
1821
+ "go.mod",
1822
+ "go.sum",
1823
+ // Rust
1824
+ "Cargo.toml",
1825
+ "Cargo.lock",
1826
+ // Java/Kotlin
1827
+ "pom.xml",
1828
+ "build.gradle",
1829
+ "build.gradle.kts",
1830
+ // Ruby
1831
+ "Gemfile",
1832
+ "Gemfile.lock",
1833
+ // PHP
1834
+ "composer.json",
1835
+ "composer.lock",
1836
+ // .NET
1837
+ "*.csproj",
1838
+ "*.fsproj",
1839
+ "*.sln",
1840
+ // Elixir
1841
+ "mix.exs",
1842
+ // Dart/Flutter
1843
+ "pubspec.yaml",
1844
+ // Swift
1845
+ "Package.swift"
1846
+ ];
1847
+ function getFiles(dir, maxDepth = 3, currentDepth = 0) {
1848
+ if (currentDepth >= maxDepth) return [];
1849
+ const files = [];
1659
1850
  try {
1660
- const result = execSync5(`gh issue view ${issueNumber} --json title,body`, {
1661
- encoding: "utf-8",
1662
- stdio: ["pipe", "pipe", "pipe"]
1663
- });
1664
- return JSON.parse(result);
1851
+ const entries = readdirSync(dir, { withFileTypes: true });
1852
+ for (const entry of entries) {
1853
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "vendor" || entry.name === "target" || entry.name === "__pycache__" || entry.name === "venv" || entry.name === ".venv") {
1854
+ continue;
1855
+ }
1856
+ const fullPath = join4(dir, entry.name);
1857
+ if (entry.isDirectory()) {
1858
+ files.push(`${entry.name}/`);
1859
+ const subFiles = getFiles(fullPath, maxDepth, currentDepth + 1);
1860
+ files.push(...subFiles.map((f) => `${entry.name}/${f}`));
1861
+ } else {
1862
+ files.push(entry.name);
1863
+ }
1864
+ }
1665
1865
  } catch {
1666
- return null;
1667
1866
  }
1867
+ return files;
1668
1868
  }
1669
- 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) => {
1670
- const git = simpleGit9();
1671
- const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1672
- const isRepo = await git.checkIsRepo();
1673
- if (!isRepo) {
1674
- console.error(chalk11.red("Error: Not a git repository"));
1675
- process.exit(1);
1676
- }
1677
- let description;
1678
- let issueNumber;
1679
- if (options.description) {
1680
- description = options.description;
1681
- } else if (issue) {
1682
- if (!requireGhCli()) {
1683
- process.exit(1);
1684
- }
1685
- const cleanedIssue = issue.replace(/^#/, "");
1686
- issueNumber = cleanedIssue;
1687
- const spinner2 = ora9(`Fetching issue #${cleanedIssue}...`).start();
1688
- const issueInfo = getIssueInfo(cleanedIssue);
1689
- if (!issueInfo) {
1690
- spinner2.fail(`Could not fetch issue #${issueNumber}`);
1691
- console.log(chalk11.gray("Make sure you are authenticated: gh auth login"));
1692
- process.exit(1);
1869
+ function findConfigFiles(repoRoot) {
1870
+ const found = /* @__PURE__ */ new Map();
1871
+ for (const configFile of CONFIG_FILES) {
1872
+ if (configFile.includes("*")) {
1873
+ const ext = configFile.replace("*", "");
1874
+ try {
1875
+ const entries = readdirSync(repoRoot);
1876
+ for (const entry of entries) {
1877
+ if (entry.endsWith(ext)) {
1878
+ const content = readFileSync4(join4(repoRoot, entry), "utf-8");
1879
+ found.set(entry, content.slice(0, 2e3));
1880
+ }
1881
+ }
1882
+ } catch {
1883
+ }
1884
+ } else {
1885
+ const filePath = join4(repoRoot, configFile);
1886
+ if (existsSync5(filePath)) {
1887
+ try {
1888
+ const content = readFileSync4(filePath, "utf-8");
1889
+ found.set(configFile, content.slice(0, 2e3));
1890
+ } catch {
1891
+ }
1892
+ }
1693
1893
  }
1694
- spinner2.stop();
1695
- console.log(chalk11.gray(`Issue: ${issueInfo.title}`));
1696
- description = `${issueInfo.title}
1697
-
1698
- ${issueInfo.body || ""}`;
1699
- } else {
1700
- console.error(chalk11.red("Error: Please provide an issue number or use -d for description"));
1701
- console.log(chalk11.gray("Usage:"));
1702
- console.log(chalk11.gray(" gut branch 123"));
1703
- console.log(chalk11.gray(' gut branch -d "add user authentication"'));
1704
- process.exit(1);
1705
1894
  }
1706
- const provider = options.provider.toLowerCase();
1707
- const template = findTemplate(repoRoot.trim(), "branch");
1895
+ return found;
1896
+ }
1897
+ var gitignoreCommand = new Command10("gitignore").description("Generate .gitignore from current codebase").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic, ollama)").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) => {
1898
+ const git = simpleGit9();
1899
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1900
+ const root = repoRoot.trim();
1901
+ const provider = await resolveProvider(options.provider);
1902
+ const template = findTemplate(root, "gitignore");
1708
1903
  if (template) {
1709
1904
  console.log(chalk11.gray("Using template from project..."));
1710
1905
  }
1711
- const spinner = ora9("Generating branch name...").start();
1906
+ const spinner = ora8("Analyzing project structure...").start();
1907
+ const files = getFiles(root);
1908
+ const configFiles = findConfigFiles(root);
1909
+ const gitignorePath = join4(root, options.output);
1910
+ let existingGitignore;
1911
+ if (existsSync5(gitignorePath)) {
1912
+ existingGitignore = readFileSync4(gitignorePath, "utf-8");
1913
+ }
1914
+ let configFilesStr = "";
1915
+ if (configFiles.size > 0) {
1916
+ const entries = [];
1917
+ for (const [name, content] of configFiles) {
1918
+ entries.push(`### ${name}
1919
+ \`\`\`
1920
+ ${content}
1921
+ \`\`\``);
1922
+ }
1923
+ configFilesStr = entries.join("\n\n");
1924
+ }
1925
+ spinner.text = "Generating .gitignore...";
1712
1926
  try {
1713
- const branchName = await generateBranchName(
1714
- description,
1927
+ const gitignoreContent = await generateGitignore(
1928
+ {
1929
+ files: files.slice(0, 200).join("\n"),
1930
+ configFiles: configFilesStr,
1931
+ existingGitignore
1932
+ },
1715
1933
  { provider, model: options.model },
1716
- { type: options.type, issue: issueNumber },
1717
1934
  template || void 0
1718
1935
  );
1719
1936
  spinner.stop();
1720
- console.log(chalk11.bold("\nGenerated branch name:\n"));
1721
- console.log(chalk11.green(` ${branchName}`));
1937
+ if (options.stdout) {
1938
+ console.log(gitignoreContent);
1939
+ return;
1940
+ }
1941
+ console.log(chalk11.bold("\nGenerated .gitignore:\n"));
1942
+ console.log(chalk11.gray("\u2500".repeat(50)));
1943
+ console.log(gitignoreContent);
1944
+ console.log(chalk11.gray("\u2500".repeat(50)));
1722
1945
  console.log();
1723
- if (options.checkout) {
1724
- await git.checkoutLocalBranch(branchName);
1725
- console.log(chalk11.green(`\u2713 Created and checked out branch: ${branchName}`));
1726
- } else {
1946
+ if (existsSync5(gitignorePath) && !options.yes) {
1727
1947
  const readline = await import("readline");
1728
1948
  const rl = readline.createInterface({
1729
1949
  input: process.stdin,
1730
1950
  output: process.stdout
1731
1951
  });
1732
1952
  const answer = await new Promise((resolve) => {
1733
- rl.question(chalk11.cyan("Create and checkout this branch? (y/N) "), resolve);
1953
+ rl.question(chalk11.cyan(`${options.output} already exists. Overwrite? (y/N) `), resolve);
1734
1954
  });
1735
1955
  rl.close();
1736
- if (answer.toLowerCase() === "y") {
1737
- await git.checkoutLocalBranch(branchName);
1738
- console.log(chalk11.green(`\u2713 Created and checked out branch: ${branchName}`));
1739
- } else {
1740
- console.log(chalk11.gray("\nTo create manually:"));
1741
- console.log(chalk11.gray(` git checkout -b ${branchName}`));
1956
+ if (answer.toLowerCase() !== "y") {
1957
+ console.log(chalk11.gray("Aborted."));
1958
+ return;
1742
1959
  }
1743
1960
  }
1961
+ writeFileSync2(gitignorePath, gitignoreContent);
1962
+ console.log(chalk11.green(`\u2713 Wrote ${options.output}`));
1744
1963
  } catch (error) {
1745
- spinner.fail("Failed to generate branch name");
1964
+ spinner.fail("Failed to generate .gitignore");
1746
1965
  console.error(chalk11.red(error instanceof Error ? error.message : "Unknown error"));
1747
1966
  process.exit(1);
1748
1967
  }
1749
1968
  });
1750
1969
 
1751
- // src/commands/checkout.ts
1752
- import { Command as Command11 } from "commander";
1970
+ // src/commands/init.ts
1971
+ import { execSync as execSync6 } from "child_process";
1972
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1973
+ import { homedir as homedir4 } from "os";
1974
+ import { dirname as dirname2, join as join5 } from "path";
1975
+ import { fileURLToPath as fileURLToPath2 } from "url";
1976
+ import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
1977
+ import { createGoogleGenerativeAI as createGoogleGenerativeAI2 } from "@ai-sdk/google";
1978
+ import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
1979
+ import { generateText as generateText2 } from "ai";
1753
1980
  import chalk12 from "chalk";
1754
- import ora10 from "ora";
1981
+ import { Command as Command11 } from "commander";
1982
+ import ora9 from "ora";
1755
1983
  import { simpleGit as simpleGit10 } from "simple-git";
1756
- 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) => {
1757
- const git = simpleGit10();
1758
- const isRepo = await git.checkIsRepo();
1759
- if (!isRepo) {
1760
- console.error(chalk12.red("Error: Not a git repository"));
1761
- process.exit(1);
1984
+ function openFolder2(path2) {
1985
+ const platform = process.platform;
1986
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? 'start ""' : "xdg-open";
1987
+ execSync6(`${cmd} "${path2}"`);
1988
+ }
1989
+ var __filename2 = fileURLToPath2(import.meta.url);
1990
+ var __dirname2 = dirname2(__filename2);
1991
+ var GUT_ROOT2 = join5(__dirname2, "..");
1992
+ var TEMPLATE_FILES = [
1993
+ "branch.md",
1994
+ "changelog.md",
1995
+ "checkout.md",
1996
+ "commit.md",
1997
+ "explain.md",
1998
+ "explain-file.md",
1999
+ "find.md",
2000
+ "merge.md",
2001
+ "pr.md",
2002
+ "review.md",
2003
+ "stash.md",
2004
+ "summary.md"
2005
+ ];
2006
+ async function translateTemplate(content, targetLang, provider) {
2007
+ const apiKey = await getApiKey(provider);
2008
+ if (!apiKey) {
2009
+ throw new Error(`No API key found for ${provider}`);
1762
2010
  }
1763
- const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1764
- const spinner = ora10("Analyzing changes...").start();
1765
- const status = await git.status();
1766
- let diff;
1767
- if (options.staged) {
1768
- diff = await git.diff(["--cached"]);
1769
- } else {
1770
- const stagedDiff = await git.diff(["--cached"]);
1771
- const unstagedDiff = await git.diff();
1772
- diff = stagedDiff + "\n" + unstagedDiff;
2011
+ const modelName = getDefaultModel(provider);
2012
+ let model;
2013
+ switch (provider) {
2014
+ case "gemini": {
2015
+ const google = createGoogleGenerativeAI2({ apiKey });
2016
+ model = google(modelName);
2017
+ break;
2018
+ }
2019
+ case "openai": {
2020
+ const openai = createOpenAI2({ apiKey });
2021
+ model = openai(modelName);
2022
+ break;
2023
+ }
2024
+ case "anthropic": {
2025
+ const anthropic = createAnthropic2({ apiKey });
2026
+ model = anthropic(modelName);
2027
+ break;
2028
+ }
2029
+ default:
2030
+ throw new Error(`Unsupported provider for translation: ${provider}`);
1773
2031
  }
1774
- const hasChanges = diff.trim() || status.not_added.length > 0 || status.created.length > 0;
1775
- if (!hasChanges) {
1776
- spinner.fail("No changes found");
1777
- console.log(chalk12.gray("Make some changes first, then run gut checkout"));
1778
- process.exit(1);
2032
+ const langNames = {
2033
+ ja: "Japanese",
2034
+ en: "English",
2035
+ zh: "Chinese",
2036
+ ko: "Korean",
2037
+ es: "Spanish",
2038
+ fr: "French",
2039
+ de: "German"
2040
+ };
2041
+ const targetLangName = langNames[targetLang] || targetLang;
2042
+ const { text } = await generateText2({
2043
+ model,
2044
+ prompt: `Translate the following prompt template to ${targetLangName}.
2045
+ Keep all {{variable}} placeholders exactly as they are - do not translate them.
2046
+ Keep the markdown formatting intact.
2047
+ Only translate the instructional text.
2048
+
2049
+ Template to translate:
2050
+ ${content}
2051
+
2052
+ Translated template:`
2053
+ });
2054
+ return text.trim();
2055
+ }
2056
+ var initCommand = new Command11("init").description("Initialize .gut/ templates in your project or globally").option(
2057
+ "-p, --provider <provider>",
2058
+ "AI provider for translation (gemini, openai, anthropic, ollama)"
2059
+ ).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) => {
2060
+ const isGlobal = options.global === true;
2061
+ const git = simpleGit10();
2062
+ let targetDir;
2063
+ if (isGlobal) {
2064
+ targetDir = join5(homedir4(), ".config", "gut", "templates");
2065
+ } else {
2066
+ const isRepo = await git.checkIsRepo();
2067
+ if (!isRepo) {
2068
+ console.error(chalk12.red("Error: Not a git repository"));
2069
+ console.error(chalk12.gray("Use --global to initialize templates globally"));
2070
+ process.exit(1);
2071
+ }
2072
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2073
+ targetDir = join5(repoRoot.trim(), ".gut");
1779
2074
  }
1780
- if (!diff.trim() && (status.not_added.length > 0 || status.created.length > 0)) {
1781
- const untrackedFiles = [...status.not_added, ...status.created];
1782
- diff = `New files:
1783
- ${untrackedFiles.map((f) => `+ ${f}`).join("\n")}`;
2075
+ if (!existsSync6(targetDir)) {
2076
+ mkdirSync3(targetDir, { recursive: true });
2077
+ console.log(chalk12.green(`Created ${targetDir}`));
1784
2078
  }
1785
- spinner.text = "Generating branch name...";
1786
- const provider = options.provider.toLowerCase();
1787
- const template = findTemplate(repoRoot.trim(), "checkout");
1788
- if (template) {
1789
- console.log(chalk12.gray("\nUsing template from project..."));
2079
+ console.log(
2080
+ chalk12.blue(
2081
+ isGlobal ? "Initializing global templates...\n" : "Initializing project templates...\n"
2082
+ )
2083
+ );
2084
+ const sourceDir = join5(GUT_ROOT2, ".gut");
2085
+ const lang = getLanguage();
2086
+ const langSourceDir = join5(GUT_ROOT2, ".gut", lang);
2087
+ const hasPreTranslated = lang !== "en" && existsSync6(langSourceDir);
2088
+ const needsTranslation = options.translate !== false && lang !== "en" && !hasPreTranslated;
2089
+ const provider = needsTranslation ? await resolveProvider(options.provider) : null;
2090
+ if (hasPreTranslated) {
2091
+ console.log(chalk12.gray(`Language: ${lang} - using pre-translated templates
2092
+ `));
2093
+ } else if (needsTranslation) {
2094
+ console.log(chalk12.gray(`Language: ${lang} - templates will be translated
2095
+ `));
1790
2096
  }
1791
- try {
1792
- const branchName = await generateBranchNameFromDiff(
1793
- diff,
1794
- { provider, model: options.model },
1795
- template
1796
- );
1797
- spinner.stop();
1798
- console.log(chalk12.bold("\nGenerated branch name:\n"));
1799
- console.log(chalk12.green(` ${branchName}`));
1800
- console.log();
1801
- if (options.yes) {
1802
- await git.checkoutLocalBranch(branchName);
1803
- console.log(chalk12.green(`\u2713 Created and checked out branch: ${branchName}`));
1804
- } else {
1805
- const readline = await import("readline");
1806
- const rl = readline.createInterface({
1807
- input: process.stdin,
1808
- output: process.stdout
1809
- });
1810
- const answer = await new Promise((resolve) => {
1811
- rl.question(chalk12.cyan("Create and checkout this branch? (y/N) "), resolve);
1812
- });
1813
- rl.close();
1814
- if (answer.toLowerCase() === "y") {
1815
- await git.checkoutLocalBranch(branchName);
1816
- console.log(chalk12.green(`\u2713 Created and checked out branch: ${branchName}`));
1817
- } else {
1818
- console.log(chalk12.gray("\nTo create manually:"));
1819
- console.log(chalk12.gray(` git checkout -b ${branchName}`));
2097
+ const spinner = ora9();
2098
+ let copied = 0;
2099
+ let skipped = 0;
2100
+ for (const filename of TEMPLATE_FILES) {
2101
+ const langSourcePath = join5(langSourceDir, filename);
2102
+ const defaultSourcePath = join5(sourceDir, filename);
2103
+ const sourcePath = hasPreTranslated && existsSync6(langSourcePath) ? langSourcePath : defaultSourcePath;
2104
+ const targetPath = join5(targetDir, filename);
2105
+ if (!existsSync6(sourcePath)) {
2106
+ continue;
2107
+ }
2108
+ if (existsSync6(targetPath) && !options.force) {
2109
+ console.log(chalk12.gray(` Skipped: ${filename} (already exists)`));
2110
+ skipped++;
2111
+ continue;
2112
+ }
2113
+ let content = readFileSync5(sourcePath, "utf-8");
2114
+ if (needsTranslation && sourcePath === defaultSourcePath && provider) {
2115
+ spinner.start(`Translating ${filename}...`);
2116
+ try {
2117
+ content = await translateTemplate(content, lang, provider);
2118
+ spinner.succeed(`Translated: ${filename}`);
2119
+ } catch (err) {
2120
+ spinner.fail(`Failed to translate ${filename}`);
2121
+ console.error(chalk12.red(` ${err instanceof Error ? err.message : "Unknown error"}`));
2122
+ console.log(chalk12.gray(` Using original English template`));
1820
2123
  }
2124
+ } else {
2125
+ console.log(chalk12.green(` Copied: ${filename}`));
1821
2126
  }
1822
- } catch (error) {
1823
- spinner.fail("Failed to generate branch name");
1824
- console.error(chalk12.red(error instanceof Error ? error.message : "Unknown error"));
1825
- process.exit(1);
1826
- }
1827
- });
1828
-
1829
- // src/commands/sync.ts
1830
- import { Command as Command12 } from "commander";
1831
- import chalk13 from "chalk";
1832
- import ora11 from "ora";
1833
- import { simpleGit as simpleGit11 } from "simple-git";
1834
- 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) => {
1835
- const git = simpleGit11();
1836
- const isRepo = await git.checkIsRepo();
1837
- if (!isRepo) {
1838
- console.error(chalk13.red("Error: Not a git repository"));
1839
- process.exit(1);
2127
+ writeFileSync3(targetPath, content);
2128
+ copied++;
1840
2129
  }
1841
- const spinner = ora11("Checking repository status...").start();
1842
- try {
1843
- const status = await git.status();
1844
- const hasChanges = !status.isClean();
1845
- if (hasChanges && !options.stash && !options.force) {
1846
- spinner.stop();
1847
- console.log(chalk13.yellow("You have uncommitted changes:"));
1848
- if (status.modified.length > 0) {
1849
- console.log(chalk13.gray(` Modified: ${status.modified.length} file(s)`));
1850
- }
1851
- if (status.not_added.length > 0) {
1852
- console.log(chalk13.gray(` Untracked: ${status.not_added.length} file(s)`));
1853
- }
1854
- console.log();
1855
- console.log(chalk13.gray("Use --stash to auto-stash, or --force to sync anyway"));
1856
- process.exit(1);
1857
- }
1858
- let stashed = false;
1859
- if (hasChanges && options.stash) {
1860
- spinner.text = "Stashing changes...";
1861
- await git.stash(["push", "-m", `gut-sync: auto-stash before sync`]);
1862
- stashed = true;
1863
- }
1864
- spinner.text = "Fetching from remote...";
1865
- await git.fetch(["--all", "--prune"]);
1866
- const currentBranch = status.current;
1867
- if (!currentBranch) {
1868
- spinner.fail("Could not determine current branch");
1869
- process.exit(1);
1870
- }
1871
- const trackingBranch = status.tracking;
1872
- if (!trackingBranch) {
1873
- spinner.warn(`Branch ${currentBranch} has no upstream tracking branch`);
1874
- console.log(chalk13.gray(`
1875
- To set upstream: git push -u origin ${currentBranch}`));
1876
- if (stashed) {
1877
- await git.stash(["pop"]);
1878
- console.log(chalk13.gray("Restored stashed changes"));
1879
- }
1880
- return;
1881
- }
1882
- const strategy = options.merge ? "merge" : "rebase";
1883
- spinner.text = `Syncing with ${trackingBranch} (${strategy})...`;
2130
+ console.log();
2131
+ if (copied > 0) {
2132
+ const location = isGlobal ? "~/.config/gut/templates/" : ".gut/";
2133
+ console.log(chalk12.green(`\u2713 ${copied} template(s) initialized in ${location}`));
2134
+ }
2135
+ if (skipped > 0) {
2136
+ console.log(chalk12.gray(` ${skipped} template(s) skipped (use --force to overwrite)`));
2137
+ }
2138
+ if (isGlobal) {
2139
+ console.log(chalk12.gray("\nGlobal templates will be used as fallback for all projects."));
2140
+ console.log(
2141
+ chalk12.gray("Project-level templates (.gut/) take priority over global templates.")
2142
+ );
2143
+ } else {
2144
+ console.log(chalk12.gray("\nYou can now customize these templates for your project."));
2145
+ }
2146
+ if (options.open) {
1884
2147
  try {
1885
- if (options.merge) {
1886
- await git.merge([trackingBranch]);
1887
- } else {
1888
- await git.rebase([trackingBranch]);
1889
- }
1890
- } catch (error) {
1891
- spinner.fail(`${strategy} failed - you may have conflicts`);
1892
- console.log(chalk13.yellow("\nResolve conflicts and then:"));
1893
- if (options.merge) {
1894
- console.log(chalk13.gray(" git add . && git commit"));
1895
- } else {
1896
- console.log(chalk13.gray(" git add . && git rebase --continue"));
1897
- }
1898
- if (stashed) {
1899
- console.log(chalk13.yellow("\nNote: You have stashed changes. Run `git stash pop` after resolving."));
1900
- }
1901
- process.exit(1);
1902
- }
1903
- const newStatus = await git.status();
1904
- const ahead = newStatus.ahead || 0;
1905
- const behind = newStatus.behind || 0;
1906
- spinner.succeed(chalk13.green("Synced successfully"));
1907
- if (behind > 0) {
1908
- console.log(chalk13.yellow(` \u2193 ${behind} commit(s) behind`));
1909
- }
1910
- if (ahead > 0) {
1911
- if (options.push !== false) {
1912
- const pushSpinner = ora11("Pushing to remote...").start();
1913
- try {
1914
- await git.push();
1915
- pushSpinner.succeed(chalk13.green(`Pushed ${ahead} commit(s)`));
1916
- } catch (error) {
1917
- pushSpinner.fail("Push failed");
1918
- console.error(chalk13.red(error instanceof Error ? error.message : "Unknown error"));
1919
- }
1920
- } else {
1921
- console.log(chalk13.cyan(` \u2191 ${ahead} commit(s) ahead`));
1922
- }
1923
- }
1924
- if (stashed) {
1925
- spinner.start("Restoring stashed changes...");
1926
- try {
1927
- await git.stash(["pop"]);
1928
- spinner.succeed("Restored stashed changes");
1929
- } catch {
1930
- spinner.warn("Could not auto-restore stash (may have conflicts)");
1931
- console.log(chalk13.gray(" Run `git stash pop` manually"));
1932
- }
2148
+ openFolder2(targetDir);
2149
+ console.log(chalk12.green(`
2150
+ Opened: ${targetDir}`));
2151
+ } catch {
2152
+ console.error(chalk12.red(`
2153
+ Failed to open folder: ${targetDir}`));
1933
2154
  }
1934
- } catch (error) {
1935
- spinner.fail("Sync failed");
1936
- console.error(chalk13.red(error instanceof Error ? error.message : "Unknown error"));
2155
+ }
2156
+ });
2157
+
2158
+ // src/commands/lang.ts
2159
+ import chalk13 from "chalk";
2160
+ import { Command as Command12 } from "commander";
2161
+ var langCommand = new Command12("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) => {
2162
+ if (!language) {
2163
+ const lang = getLanguage();
2164
+ const localConfig = getLocalConfig();
2165
+ const isLocal = "lang" in localConfig;
2166
+ const scope = isLocal ? chalk13.cyan("(local)") : chalk13.gray("(global)");
2167
+ console.log(`${lang} ${scope}`);
2168
+ return;
2169
+ }
2170
+ if (!isValidLanguage(language)) {
2171
+ console.error(chalk13.red(`Invalid language: ${language}`));
2172
+ console.error(chalk13.gray(`Valid languages: ${VALID_LANGUAGES.join(", ")}`));
2173
+ process.exit(1);
2174
+ }
2175
+ try {
2176
+ setLanguage(language, options.local ?? false);
2177
+ const scope = options.local ? "(local)" : "(global)";
2178
+ console.log(chalk13.green(`\u2713 Language set to: ${language} ${scope}`));
2179
+ } catch (err) {
2180
+ console.error(chalk13.red(err.message));
1937
2181
  process.exit(1);
1938
2182
  }
1939
2183
  });
1940
2184
 
1941
- // src/commands/stash.ts
1942
- import { Command as Command13 } from "commander";
2185
+ // src/commands/merge.ts
2186
+ import * as fs from "fs";
2187
+ import * as path from "path";
1943
2188
  import chalk14 from "chalk";
1944
- import ora12 from "ora";
1945
- import { simpleGit as simpleGit12 } from "simple-git";
1946
- 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) => {
1947
- const git = simpleGit12();
2189
+ import { Command as Command13 } from "commander";
2190
+ import ora10 from "ora";
2191
+ import { simpleGit as simpleGit11 } from "simple-git";
2192
+ var mergeCommand = new Command13("merge").description("Merge a branch with AI-powered conflict resolution").argument("<branch>", "Branch to merge").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic, ollama)").option("-m, --model <model>", "Model to use (provider-specific)").option("--no-commit", "Do not auto-commit after resolving").action(async (branch, options) => {
2193
+ const git = simpleGit11();
1948
2194
  const isRepo = await git.checkIsRepo();
1949
2195
  if (!isRepo) {
1950
2196
  console.error(chalk14.red("Error: Not a git repository"));
1951
2197
  process.exit(1);
1952
2198
  }
1953
- if (options.list) {
1954
- const stashList = await git.stashList();
1955
- if (stashList.all.length === 0) {
1956
- console.log(chalk14.gray("No stashes found"));
1957
- return;
1958
- }
1959
- console.log(chalk14.bold("\nStashes:\n"));
1960
- stashList.all.forEach((stash, index) => {
1961
- console.log(` ${chalk14.cyan(index.toString())} ${stash.message}`);
1962
- });
1963
- console.log();
1964
- return;
2199
+ const provider = await resolveProvider(options.provider);
2200
+ const status = await git.status();
2201
+ if (status.modified.length > 0 || status.staged.length > 0) {
2202
+ console.error(chalk14.red("Error: Working directory has uncommitted changes"));
2203
+ console.log(chalk14.gray("Please commit or stash your changes first"));
2204
+ process.exit(1);
1965
2205
  }
1966
- if (options.apply !== void 0) {
1967
- const index = typeof options.apply === "string" ? options.apply : "0";
1968
- try {
1969
- await git.stash(["apply", `stash@{${index}}`]);
1970
- console.log(chalk14.green(`\u2713 Applied stash@{${index}}`));
1971
- } catch (error) {
1972
- console.error(chalk14.red(`Failed to apply stash: ${error instanceof Error ? error.message : "Unknown error"}`));
1973
- process.exit(1);
1974
- }
2206
+ const branchInfo = await git.branch();
2207
+ const currentBranch = branchInfo.current;
2208
+ console.log(
2209
+ chalk14.bold(`
2210
+ Merging ${chalk14.cyan(branch)} into ${chalk14.cyan(currentBranch)}...
2211
+ `)
2212
+ );
2213
+ try {
2214
+ await git.merge([branch]);
2215
+ console.log(chalk14.green("\u2713 Merged successfully (no conflicts)"));
1975
2216
  return;
2217
+ } catch {
1976
2218
  }
1977
- if (options.pop !== void 0) {
1978
- const index = typeof options.pop === "string" ? options.pop : "0";
1979
- try {
1980
- await git.stash(["pop", `stash@{${index}}`]);
1981
- console.log(chalk14.green(`\u2713 Popped stash@{${index}}`));
1982
- } catch (error) {
1983
- console.error(chalk14.red(`Failed to pop stash: ${error instanceof Error ? error.message : "Unknown error"}`));
1984
- process.exit(1);
1985
- }
1986
- return;
2219
+ const conflictStatus = await git.status();
2220
+ const conflictedFiles = conflictStatus.conflicted;
2221
+ if (conflictedFiles.length === 0) {
2222
+ console.error(chalk14.red("Merge failed for unknown reason"));
2223
+ await git.merge(["--abort"]);
2224
+ process.exit(1);
1987
2225
  }
1988
- if (options.drop !== void 0) {
1989
- const index = typeof options.drop === "string" ? options.drop : "0";
1990
- try {
1991
- await git.stash(["drop", `stash@{${index}}`]);
1992
- console.log(chalk14.green(`\u2713 Dropped stash@{${index}}`));
1993
- } catch (error) {
1994
- console.error(chalk14.red(`Failed to drop stash: ${error instanceof Error ? error.message : "Unknown error"}`));
1995
- process.exit(1);
1996
- }
1997
- return;
2226
+ console.log(chalk14.yellow(`\u26A0 ${conflictedFiles.length} conflict(s) detected
2227
+ `));
2228
+ const spinner = ora10();
2229
+ const rootDir = await git.revparse(["--show-toplevel"]);
2230
+ const template = findTemplate(rootDir.trim(), "merge");
2231
+ if (template) {
2232
+ console.log(chalk14.gray("Using merge template from project...\n"));
1998
2233
  }
1999
- if (options.clear) {
2000
- const readline = await import("readline");
2001
- const rl = readline.createInterface({
2002
- input: process.stdin,
2003
- output: process.stdout
2004
- });
2005
- const answer = await new Promise((resolve) => {
2006
- rl.question(chalk14.yellow("Clear all stashes? This cannot be undone. (y/N) "), resolve);
2007
- });
2008
- rl.close();
2009
- if (answer.toLowerCase() === "y") {
2010
- await git.stash(["clear"]);
2011
- console.log(chalk14.green("\u2713 Cleared all stashes"));
2012
- } else {
2013
- console.log(chalk14.gray("Cancelled"));
2234
+ for (const file of conflictedFiles) {
2235
+ const filePath = path.join(rootDir.trim(), file);
2236
+ const content = fs.readFileSync(filePath, "utf-8");
2237
+ console.log(chalk14.bold(`
2238
+ \u{1F4C4} ${file}`));
2239
+ const conflictMatch = content.match(/<<<<<<< HEAD[\s\S]*?>>>>>>>.+/g);
2240
+ if (conflictMatch) {
2241
+ console.log(chalk14.gray("\u2500".repeat(50)));
2242
+ console.log(chalk14.gray(conflictMatch[0].slice(0, 500)));
2243
+ if (conflictMatch[0].length > 500) console.log(chalk14.gray("..."));
2244
+ console.log(chalk14.gray("\u2500".repeat(50)));
2014
2245
  }
2015
- return;
2016
- }
2017
- const status = await git.status();
2018
- if (status.isClean()) {
2019
- console.log(chalk14.yellow("No changes to stash"));
2020
- return;
2021
- }
2022
- let stashName;
2023
- if (name) {
2024
- stashName = name;
2025
- } else {
2026
- const provider = options.provider.toLowerCase();
2027
- const diff = await git.diff();
2028
- const stagedDiff = await git.diff(["--cached"]);
2029
- const fullDiff = diff + "\n" + stagedDiff;
2030
- if (!fullDiff.trim()) {
2031
- stashName = `WIP: untracked files (${status.not_added.length} files)`;
2032
- } else {
2033
- const spinner = ora12("Generating stash name...").start();
2034
- try {
2035
- const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2036
- const template = findTemplate(repoRoot.trim(), "stash");
2037
- stashName = await generateStashName(fullDiff, { provider, model: options.model }, template || void 0);
2038
- spinner.stop();
2039
- } catch (error) {
2040
- spinner.fail("Failed to generate name, using default");
2041
- stashName = `WIP: ${status.modified.length} modified, ${status.not_added.length} untracked`;
2246
+ spinner.start("AI is analyzing conflict...");
2247
+ try {
2248
+ const resolution = await resolveConflict(
2249
+ content,
2250
+ {
2251
+ filename: file,
2252
+ oursRef: currentBranch,
2253
+ theirsRef: branch
2254
+ },
2255
+ { provider, model: options.model },
2256
+ template || void 0
2257
+ );
2258
+ spinner.stop();
2259
+ console.log(chalk14.cyan("\n\u{1F916} AI suggests:"));
2260
+ console.log(chalk14.gray("\u2500".repeat(50)));
2261
+ const preview = resolution.resolvedContent.slice(0, 800);
2262
+ console.log(preview);
2263
+ if (resolution.resolvedContent.length > 800) console.log(chalk14.gray("..."));
2264
+ console.log(chalk14.gray("\u2500".repeat(50)));
2265
+ console.log(chalk14.gray(`Strategy: ${resolution.strategy}`));
2266
+ console.log(chalk14.gray(`Reason: ${resolution.explanation}`));
2267
+ const readline = await import("readline");
2268
+ const rl = readline.createInterface({
2269
+ input: process.stdin,
2270
+ output: process.stdout
2271
+ });
2272
+ const answer = await new Promise((resolve) => {
2273
+ rl.question(chalk14.cyan("\nAccept this resolution? (y/n/s to skip) "), resolve);
2274
+ });
2275
+ rl.close();
2276
+ if (answer.toLowerCase() === "y") {
2277
+ fs.writeFileSync(filePath, resolution.resolvedContent);
2278
+ await git.add(file);
2279
+ console.log(chalk14.green(`\u2713 Resolved ${file}`));
2280
+ } else if (answer.toLowerCase() === "s") {
2281
+ console.log(chalk14.yellow(`\u23ED Skipped ${file}`));
2282
+ } else {
2283
+ console.log(chalk14.yellow(`\u2717 Rejected - resolve manually: ${file}`));
2042
2284
  }
2285
+ } catch (err) {
2286
+ spinner.fail("AI resolution failed");
2287
+ console.error(chalk14.red(err instanceof Error ? err.message : "Unknown error"));
2288
+ console.log(chalk14.yellow(`Please resolve manually: ${file}`));
2043
2289
  }
2044
2290
  }
2045
- await git.stash(["push", "-u", "-m", stashName]);
2046
- console.log(chalk14.green(`\u2713 Stashed: ${stashName}`));
2291
+ const finalStatus = await git.status();
2292
+ if (finalStatus.conflicted.length > 0) {
2293
+ console.log(chalk14.yellow(`
2294
+ \u26A0 ${finalStatus.conflicted.length} conflict(s) remaining`));
2295
+ console.log(chalk14.gray("Resolve manually and run: git add <files> && git commit"));
2296
+ } else if (options.commit !== false) {
2297
+ await git.commit(`Merge branch '${branch}' into ${currentBranch}`);
2298
+ console.log(chalk14.green("\n\u2713 All conflicts resolved and committed"));
2299
+ } else {
2300
+ console.log(chalk14.green("\n\u2713 All conflicts resolved"));
2301
+ console.log(chalk14.gray("Run: git commit"));
2302
+ }
2047
2303
  });
2048
2304
 
2049
- // src/commands/summary.ts
2050
- import { Command as Command14 } from "commander";
2305
+ // src/commands/pr.ts
2306
+ import { execSync as execSync7 } from "child_process";
2307
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
2308
+ import { join as join7 } from "path";
2051
2309
  import chalk15 from "chalk";
2052
- import ora13 from "ora";
2053
- import { simpleGit as simpleGit13 } from "simple-git";
2054
- 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) => {
2055
- const git = simpleGit13();
2056
- const isRepo = await git.checkIsRepo();
2057
- if (!isRepo) {
2058
- console.error(chalk15.red("Error: Not a git repository"));
2059
- process.exit(1);
2060
- }
2061
- const provider = options.provider.toLowerCase();
2062
- const spinner = ora13("Generating summary...").start();
2063
- try {
2064
- let author = options.author;
2065
- if (!author) {
2066
- const config = await git.listConfig();
2067
- author = config.all["user.name"] || "";
2068
- if (!author) {
2069
- spinner.fail("Could not determine git user. Use --author to specify.");
2070
- process.exit(1);
2071
- }
2310
+ import { Command as Command14 } from "commander";
2311
+ import ora11 from "ora";
2312
+ import { simpleGit as simpleGit12 } from "simple-git";
2313
+ var GITHUB_PR_TEMPLATE_PATHS = [
2314
+ ".github/pull_request_template.md",
2315
+ ".github/PULL_REQUEST_TEMPLATE.md",
2316
+ "pull_request_template.md",
2317
+ "PULL_REQUEST_TEMPLATE.md",
2318
+ "docs/pull_request_template.md"
2319
+ ];
2320
+ function findPRTemplate(repoRoot) {
2321
+ for (const templatePath of GITHUB_PR_TEMPLATE_PATHS) {
2322
+ const fullPath = join7(repoRoot, templatePath);
2323
+ if (existsSync7(fullPath)) {
2324
+ return readFileSync7(fullPath, "utf-8");
2072
2325
  }
2073
- let since = options.since;
2074
- let format = "custom";
2075
- if (options.daily) {
2076
- since = "today";
2077
- format = "daily";
2078
- } else if (options.weekly) {
2079
- since = "1 week ago";
2080
- format = "weekly";
2081
- } else if (since === "today") {
2082
- format = "daily";
2326
+ }
2327
+ return findTemplate(repoRoot, "pr");
2328
+ }
2329
+ var prCommand = new Command14("pr").description("Generate a pull request title and description using AI").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic, ollama)").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) => {
2330
+ const git = simpleGit12();
2331
+ const isRepo = await git.checkIsRepo();
2332
+ if (!isRepo) {
2333
+ console.error(chalk15.red("Error: Not a git repository"));
2334
+ process.exit(1);
2335
+ }
2336
+ const provider = await resolveProvider(options.provider);
2337
+ const spinner = ora11("Analyzing branch...").start();
2338
+ try {
2339
+ const branchInfo = await git.branch();
2340
+ const currentBranch = branchInfo.current;
2341
+ let baseBranch = options.base;
2342
+ if (!baseBranch) {
2343
+ if (branchInfo.all.includes("main")) {
2344
+ baseBranch = "main";
2345
+ } else if (branchInfo.all.includes("master")) {
2346
+ baseBranch = "master";
2347
+ } else {
2348
+ baseBranch = "main";
2349
+ }
2083
2350
  }
2084
- const sinceDate = resolveDate(since);
2085
- spinner.text = `Fetching commits by ${author} since ${since}...`;
2086
- const logOptions = [`--author=${author}`, `--since=${sinceDate}`];
2087
- if (options.until) {
2088
- logOptions.push(`--until=${resolveDate(options.until)}`);
2351
+ if (currentBranch === baseBranch) {
2352
+ spinner.fail(`Already on ${baseBranch} branch`);
2353
+ process.exit(1);
2089
2354
  }
2090
- const log = await git.log(logOptions);
2091
- if (log.all.length === 0) {
2092
- spinner.info(`No commits found for ${author} since ${since}`);
2093
- process.exit(0);
2355
+ spinner.text = `Comparing ${currentBranch} to ${baseBranch}...`;
2356
+ const log = await git.log({ from: baseBranch, to: currentBranch });
2357
+ const commits = log.all.map((c) => c.message.split("\n")[0]);
2358
+ if (commits.length === 0) {
2359
+ spinner.fail("No commits found between branches");
2360
+ process.exit(1);
2094
2361
  }
2095
- spinner.text = `Analyzing ${log.all.length} commits...`;
2096
- let diff;
2097
- if (options.withDiff && log.all.length > 0) {
2098
- const oldest = log.all[log.all.length - 1].hash;
2099
- const newest = log.all[0].hash;
2100
- try {
2101
- diff = await git.diff([`${oldest}^`, newest]);
2102
- } catch {
2103
- }
2362
+ const diff = await git.diff([`${baseBranch}...${currentBranch}`]);
2363
+ const repoRoot = await git.revparse(["--show-toplevel"]);
2364
+ const template = findPRTemplate(repoRoot.trim());
2365
+ if (template) {
2366
+ spinner.text = "Found PR template, generating description...";
2367
+ } else {
2368
+ spinner.text = "Generating PR description...";
2104
2369
  }
2105
- const commits = log.all.map((c) => ({
2106
- hash: c.hash,
2107
- message: c.message,
2108
- date: c.date
2109
- }));
2110
- const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2111
- const template = findTemplate(repoRoot.trim(), "summary");
2112
- const summary = await generateWorkSummary(
2113
- { commits, author, since, until: options.until, diff },
2370
+ const { title, body } = await generatePRDescription(
2371
+ {
2372
+ baseBranch,
2373
+ currentBranch,
2374
+ commits,
2375
+ diff
2376
+ },
2114
2377
  { provider, model: options.model },
2115
- format,
2116
2378
  template || void 0
2117
2379
  );
2118
2380
  spinner.stop();
2119
- if (options.json) {
2120
- console.log(JSON.stringify(summary, null, 2));
2121
- return;
2122
- }
2123
- const output = options.markdown ? formatMarkdown(summary, author, since, options.until) : null;
2381
+ console.log(chalk15.bold("\n\u{1F4DD} Generated PR:\n"));
2382
+ console.log(chalk15.cyan("Title:"), chalk15.white(title));
2383
+ console.log(chalk15.cyan("\nDescription:"));
2384
+ console.log(chalk15.gray("\u2500".repeat(50)));
2385
+ console.log(body);
2386
+ console.log(chalk15.gray("\u2500".repeat(50)));
2124
2387
  if (options.copy) {
2125
- const textToCopy = output || formatMarkdown(summary, author, since, options.until);
2126
- const { execSync: execSync9 } = await import("child_process");
2127
2388
  try {
2128
- execSync9("pbcopy", { input: textToCopy });
2129
- console.log(chalk15.green("Summary copied to clipboard!"));
2130
- console.log();
2389
+ const fullText = `${title}
2390
+
2391
+ ${body}`;
2392
+ execSync7("pbcopy", { input: fullText });
2393
+ console.log(chalk15.green("\n\u2713 Copied to clipboard"));
2131
2394
  } catch {
2132
- console.log(chalk15.yellow("Could not copy to clipboard"));
2395
+ console.log(chalk15.yellow("\n\u26A0 Could not copy to clipboard"));
2133
2396
  }
2134
2397
  }
2135
- if (options.markdown) {
2136
- console.log(output);
2398
+ const ghInstalled = isGhCliInstalled();
2399
+ if (!ghInstalled) {
2400
+ console.log(chalk15.yellow("\n\u26A0 GitHub CLI (gh) is not installed"));
2401
+ console.log(chalk15.gray(" To create PRs directly from gut, install gh CLI:"));
2402
+ console.log(chalk15.gray(" brew install gh (macOS)"));
2403
+ console.log(chalk15.gray(" https://cli.github.com/"));
2404
+ if (!options.copy) {
2405
+ console.log(chalk15.gray("\nTip: Use --copy to copy to clipboard"));
2406
+ }
2137
2407
  } else {
2138
- printSummary(summary, author, since, options.until);
2408
+ const readline = await import("readline");
2409
+ const rl = readline.createInterface({
2410
+ input: process.stdin,
2411
+ output: process.stdout
2412
+ });
2413
+ const promptMessage = options.create ? chalk15.cyan("\nCreate PR with this description? (y/N) ") : chalk15.cyan("\nCreate PR with gh CLI? (y/N) ");
2414
+ const answer = await new Promise((resolve) => {
2415
+ rl.question(promptMessage, resolve);
2416
+ });
2417
+ rl.close();
2418
+ if (answer.toLowerCase() === "y") {
2419
+ const createSpinner = ora11("Creating PR...").start();
2420
+ try {
2421
+ const escapedTitle = title.replace(/"/g, '\\"');
2422
+ const escapedBody = body.replace(/"/g, '\\"');
2423
+ execSync7(
2424
+ `gh pr create --title "${escapedTitle}" --body "${escapedBody}" --base ${baseBranch}`,
2425
+ { stdio: "pipe" }
2426
+ );
2427
+ createSpinner.succeed("PR created successfully!");
2428
+ } catch {
2429
+ createSpinner.fail("Failed to create PR");
2430
+ console.error(chalk15.gray("Make sure gh CLI is authenticated: gh auth login"));
2431
+ }
2432
+ } else if (!options.copy) {
2433
+ console.log(chalk15.gray("\nTip: Use --copy to copy to clipboard"));
2434
+ }
2139
2435
  }
2140
- } catch (error) {
2141
- spinner.fail("Failed to generate summary");
2142
- console.error(chalk15.red(error instanceof Error ? error.message : "Unknown error"));
2436
+ } catch (err) {
2437
+ spinner.fail("Failed to generate PR description");
2438
+ console.error(chalk15.red(err instanceof Error ? err.message : "Unknown error"));
2143
2439
  process.exit(1);
2144
2440
  }
2145
2441
  });
2146
- function formatMarkdown(summary, author, since, until) {
2147
- const lines = [];
2148
- const period = until ? `${since} - ${until}` : `${since} - now`;
2149
- lines.push(`# ${summary.title}`);
2150
- lines.push("");
2151
- lines.push(`**Author:** ${author}`);
2152
- lines.push(`**Period:** ${period}`);
2153
- if (summary.stats) {
2154
- lines.push(`**Commits:** ${summary.stats.commits}`);
2155
- }
2156
- lines.push("");
2157
- lines.push("## Overview");
2158
- lines.push(summary.overview);
2159
- lines.push("");
2160
- if (summary.highlights.length > 0) {
2161
- lines.push("## Highlights");
2162
- for (const highlight of summary.highlights) {
2163
- lines.push(`- ${highlight}`);
2164
- }
2165
- lines.push("");
2166
- }
2167
- if (summary.details.length > 0) {
2168
- lines.push("## Details");
2169
- for (const section of summary.details) {
2170
- lines.push(`### ${section.category}`);
2171
- for (const item of section.items) {
2172
- lines.push(`- ${item}`);
2442
+
2443
+ // src/commands/review.ts
2444
+ import { execSync as execSync8 } from "child_process";
2445
+ import chalk16 from "chalk";
2446
+ import { Command as Command15 } from "commander";
2447
+ import ora12 from "ora";
2448
+ import { simpleGit as simpleGit13 } from "simple-git";
2449
+ async function getPRDiff(prNumber) {
2450
+ try {
2451
+ const diff = execSync8(`gh pr diff ${prNumber}`, {
2452
+ encoding: "utf-8",
2453
+ maxBuffer: 10 * 1024 * 1024
2454
+ });
2455
+ const prJsonStr = execSync8(`gh pr view ${prNumber} --json number,title,author,url`, {
2456
+ encoding: "utf-8"
2457
+ });
2458
+ const prJson = JSON.parse(prJsonStr);
2459
+ return {
2460
+ diff,
2461
+ prInfo: {
2462
+ number: prJson.number,
2463
+ title: prJson.title,
2464
+ author: prJson.author.login,
2465
+ url: prJson.url
2173
2466
  }
2174
- lines.push("");
2467
+ };
2468
+ } catch (err) {
2469
+ if (err instanceof Error && err.message.includes("gh: command not found")) {
2470
+ throw new Error("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/", {
2471
+ cause: err
2472
+ });
2175
2473
  }
2474
+ throw err;
2176
2475
  }
2177
- return lines.join("\n");
2178
- }
2179
- function resolveDate(dateStr) {
2180
- const now = /* @__PURE__ */ new Date();
2181
- if (dateStr === "today") {
2182
- return formatDate(now);
2183
- } else if (dateStr === "yesterday") {
2184
- const d = new Date(now);
2185
- d.setDate(d.getDate() - 1);
2186
- return formatDate(d);
2187
- } else if (dateStr.match(/^(\d+)\s+(day|days)\s+ago$/i)) {
2188
- const match = dateStr.match(/^(\d+)\s+(day|days)\s+ago$/i);
2189
- const days = parseInt(match[1], 10);
2190
- const d = new Date(now);
2191
- d.setDate(d.getDate() - days);
2192
- return formatDate(d);
2193
- } else if (dateStr.match(/^(\d+)\s+(week|weeks)\s+ago$/i)) {
2194
- const match = dateStr.match(/^(\d+)\s+(week|weeks)\s+ago$/i);
2195
- const weeks = parseInt(match[1], 10);
2196
- const d = new Date(now);
2197
- d.setDate(d.getDate() - weeks * 7);
2198
- return formatDate(d);
2199
- }
2200
- return dateStr;
2201
- }
2202
- function formatDate(d) {
2203
- const year = d.getFullYear();
2204
- const month = String(d.getMonth() + 1).padStart(2, "0");
2205
- const day = String(d.getDate()).padStart(2, "0");
2206
- return `${year}-${month}-${day} 00:00:00`;
2207
2476
  }
2208
- function printSummary(summary, author, since, until) {
2209
- const period = until ? `${since} - ${until}` : `${since} - now`;
2210
- console.log(chalk15.bold(`
2211
- \u{1F4CA} ${summary.title}
2212
- `));
2213
- console.log(chalk15.gray(`Author: ${author}`));
2214
- console.log(chalk15.gray(`Period: ${period}`));
2215
- if (summary.stats) {
2216
- console.log(chalk15.gray(`Commits: ${summary.stats.commits}`));
2217
- }
2218
- console.log();
2219
- console.log(chalk15.cyan("Overview:"));
2220
- console.log(` ${summary.overview}`);
2221
- console.log();
2222
- if (summary.highlights.length > 0) {
2223
- console.log(chalk15.cyan("Highlights:"));
2224
- for (const highlight of summary.highlights) {
2225
- console.log(` ${chalk15.green("\u2605")} ${highlight}`);
2226
- }
2227
- console.log();
2477
+ var reviewCommand = new Command15("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, ollama)").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) => {
2478
+ const git = simpleGit13();
2479
+ const isRepo = await git.checkIsRepo();
2480
+ if (!isRepo) {
2481
+ console.error(chalk16.red("Error: Not a git repository"));
2482
+ process.exit(1);
2228
2483
  }
2229
- if (summary.details.length > 0) {
2230
- console.log(chalk15.cyan("Details:"));
2231
- for (const section of summary.details) {
2232
- console.log(` ${chalk15.yellow(section.category)}`);
2233
- for (const item of section.items) {
2234
- console.log(` \u2022 ${item}`);
2484
+ const provider = await resolveProvider(options.provider);
2485
+ const spinner = ora12("Getting diff...").start();
2486
+ try {
2487
+ let diff;
2488
+ let prInfo = null;
2489
+ if (prNumber) {
2490
+ spinner.stop();
2491
+ if (!requireGhCli()) {
2492
+ process.exit(1);
2235
2493
  }
2236
- }
2237
- console.log();
2238
- }
2239
- }
2240
-
2241
- // src/commands/config.ts
2242
- import { Command as Command15 } from "commander";
2243
- import chalk16 from "chalk";
2244
- import { execSync as execSync7 } from "child_process";
2245
- import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
2246
- import { join as join5 } from "path";
2247
- import { homedir as homedir3 } from "os";
2248
- import { simpleGit as simpleGit14 } from "simple-git";
2249
-
2250
- // src/lib/config.ts
2251
- import { homedir as homedir2 } from "os";
2252
- import { join as join4 } from "path";
2253
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
2254
- import { execSync as execSync6 } from "child_process";
2255
- var DEFAULT_CONFIG = {
2256
- lang: "en"
2257
- };
2258
- function getGlobalConfigPath() {
2259
- const configDir = join4(homedir2(), ".config", "gut");
2260
- return join4(configDir, "config.json");
2261
- }
2262
- function getRepoRoot() {
2263
- try {
2264
- return execSync6("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
2265
- } catch {
2266
- return null;
2267
- }
2268
- }
2269
- function getLocalConfigPath() {
2270
- const repoRoot = getRepoRoot();
2271
- if (!repoRoot) return null;
2272
- return join4(repoRoot, ".gut", "config.json");
2273
- }
2274
- function ensureGlobalConfigDir() {
2275
- const configDir = join4(homedir2(), ".config", "gut");
2276
- if (!existsSync4(configDir)) {
2277
- mkdirSync(configDir, { recursive: true });
2494
+ spinner.start(`Fetching PR #${prNumber}...`);
2495
+ const result = await getPRDiff(prNumber);
2496
+ diff = result.diff;
2497
+ prInfo = result.prInfo;
2498
+ spinner.text = `Reviewing PR #${prNumber}...`;
2499
+ } else if (options.commit) {
2500
+ diff = await git.diff([`${options.commit}^`, options.commit]);
2501
+ spinner.text = `Reviewing commit ${options.commit.slice(0, 7)}...`;
2502
+ } else if (options.staged) {
2503
+ diff = await git.diff(["--cached"]);
2504
+ spinner.text = "Reviewing staged changes...";
2505
+ } else {
2506
+ diff = await git.diff();
2507
+ const stagedDiff = await git.diff(["--cached"]);
2508
+ diff = `${stagedDiff}
2509
+ ${diff}`;
2510
+ spinner.text = "Reviewing uncommitted changes...";
2511
+ }
2512
+ if (!diff.trim()) {
2513
+ spinner.info("No changes to review");
2514
+ process.exit(0);
2515
+ }
2516
+ spinner.text = "AI is reviewing your code...";
2517
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2518
+ const template = findTemplate(repoRoot.trim(), "review");
2519
+ const review = await generateCodeReview(
2520
+ diff,
2521
+ { provider, model: options.model },
2522
+ template || void 0
2523
+ );
2524
+ spinner.stop();
2525
+ if (options.json) {
2526
+ console.log(JSON.stringify({ prInfo, review }, null, 2));
2527
+ return;
2528
+ }
2529
+ if (prInfo) {
2530
+ console.log(chalk16.bold(`
2531
+ \u{1F517} PR #${prInfo.number}: ${prInfo.title}`));
2532
+ console.log(chalk16.gray(` by ${prInfo.author} - ${prInfo.url}`));
2533
+ }
2534
+ printReview(review);
2535
+ } catch (error) {
2536
+ spinner.fail("Failed to generate review");
2537
+ console.error(chalk16.red(error instanceof Error ? error.message : "Unknown error"));
2538
+ process.exit(1);
2278
2539
  }
2279
- }
2280
- function ensureLocalConfigDir() {
2281
- const repoRoot = getRepoRoot();
2282
- if (!repoRoot) return;
2283
- const gutDir = join4(repoRoot, ".gut");
2284
- if (!existsSync4(gutDir)) {
2285
- mkdirSync(gutDir, { recursive: true });
2540
+ });
2541
+ function printReview(review) {
2542
+ console.log(chalk16.bold("\n\u{1F50D} AI Code Review\n"));
2543
+ console.log(chalk16.cyan("Summary:"));
2544
+ console.log(` ${review.summary}
2545
+ `);
2546
+ if (review.issues.length > 0) {
2547
+ console.log(chalk16.cyan("Issues Found:"));
2548
+ for (const issue of review.issues) {
2549
+ const severityColors = {
2550
+ critical: chalk16.red,
2551
+ warning: chalk16.yellow,
2552
+ suggestion: chalk16.blue
2553
+ };
2554
+ const severityIcons = {
2555
+ critical: "\u{1F534}",
2556
+ warning: "\u{1F7E1}",
2557
+ suggestion: "\u{1F4A1}"
2558
+ };
2559
+ const color = severityColors[issue.severity];
2560
+ const icon = severityIcons[issue.severity];
2561
+ console.log(`
2562
+ ${icon} ${color(issue.severity.toUpperCase())}`);
2563
+ console.log(` ${chalk16.gray("File:")} ${issue.file}${issue.line ? `:${issue.line}` : ""}`);
2564
+ console.log(` ${issue.message}`);
2565
+ if (issue.suggestion) {
2566
+ console.log(` ${chalk16.green("\u2192")} ${issue.suggestion}`);
2567
+ }
2568
+ }
2569
+ } else {
2570
+ console.log(chalk16.green(" \u2713 No issues found!\n"));
2286
2571
  }
2287
- }
2288
- function readConfigFile(path2) {
2289
- if (!existsSync4(path2)) return {};
2290
- try {
2291
- return JSON.parse(readFileSync5(path2, "utf-8"));
2292
- } catch {
2293
- return {};
2572
+ if (review.positives.length > 0) {
2573
+ console.log(chalk16.cyan("\nGood Practices:"));
2574
+ for (const positive of review.positives) {
2575
+ console.log(` ${chalk16.green("\u2713")} ${positive}`);
2576
+ }
2294
2577
  }
2578
+ const criticalCount = review.issues.filter((i) => i.severity === "critical").length;
2579
+ const warningCount = review.issues.filter((i) => i.severity === "warning").length;
2580
+ const suggestionCount = review.issues.filter((i) => i.severity === "suggestion").length;
2581
+ console.log(chalk16.gray("\n\u2500".repeat(40)));
2582
+ console.log(
2583
+ ` ${chalk16.red(criticalCount)} critical ${chalk16.yellow(warningCount)} warnings ${chalk16.blue(suggestionCount)} suggestions`
2584
+ );
2585
+ console.log();
2295
2586
  }
2296
- function getGlobalConfig() {
2297
- const globalPath = getGlobalConfigPath();
2298
- return { ...DEFAULT_CONFIG, ...readConfigFile(globalPath) };
2299
- }
2300
- function getLocalConfig() {
2301
- const localPath = getLocalConfigPath();
2302
- if (!localPath) return {};
2303
- return readConfigFile(localPath);
2304
- }
2305
- function getConfig() {
2306
- const globalConfig = getGlobalConfig();
2307
- const localConfig = getLocalConfig();
2308
- return { ...globalConfig, ...localConfig };
2309
- }
2310
- function setGlobalConfig(key, value) {
2311
- ensureGlobalConfigDir();
2312
- const config = getGlobalConfig();
2313
- config[key] = value;
2314
- writeFileSync2(getGlobalConfigPath(), JSON.stringify(config, null, 2));
2315
- }
2316
- function setLocalConfig(key, value) {
2317
- const localPath = getLocalConfigPath();
2318
- if (!localPath) {
2319
- throw new Error("Not in a git repository");
2587
+
2588
+ // src/commands/stash.ts
2589
+ import chalk17 from "chalk";
2590
+ import { Command as Command16 } from "commander";
2591
+ import ora13 from "ora";
2592
+ import { simpleGit as simpleGit14 } from "simple-git";
2593
+ var stashCommand = new Command16("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, ollama)").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) => {
2594
+ const git = simpleGit14();
2595
+ const isRepo = await git.checkIsRepo();
2596
+ if (!isRepo) {
2597
+ console.error(chalk17.red("Error: Not a git repository"));
2598
+ process.exit(1);
2320
2599
  }
2321
- ensureLocalConfigDir();
2322
- const config = getLocalConfig();
2323
- config[key] = value;
2324
- writeFileSync2(localPath, JSON.stringify(config, null, 2));
2325
- }
2326
- function getLanguage() {
2327
- return getConfig().lang;
2328
- }
2329
- function setLanguage(lang, local = false) {
2330
- if (local) {
2331
- setLocalConfig("lang", lang);
2332
- } else {
2333
- setGlobalConfig("lang", lang);
2600
+ if (options.list) {
2601
+ const stashList = await git.stashList();
2602
+ if (stashList.all.length === 0) {
2603
+ console.log(chalk17.gray("No stashes found"));
2604
+ return;
2605
+ }
2606
+ console.log(chalk17.bold("\nStashes:\n"));
2607
+ stashList.all.forEach((stash, index) => {
2608
+ console.log(` ${chalk17.cyan(index.toString())} ${stash.message}`);
2609
+ });
2610
+ console.log();
2611
+ return;
2334
2612
  }
2335
- }
2336
- var VALID_LANGUAGES = ["en", "ja"];
2337
- function isValidLanguage(lang) {
2338
- return VALID_LANGUAGES.includes(lang);
2339
- }
2340
-
2341
- // src/commands/config.ts
2342
- function openFolder(path2) {
2343
- const platform = process.platform;
2344
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? 'start ""' : "xdg-open";
2345
- execSync7(`${cmd} "${path2}"`);
2346
- }
2347
- var configCommand = new Command15("config").description("Manage gut configuration");
2348
- configCommand.command("set <key> <value>").description("Set a configuration value").option("--local", "Set for current repository only").action((key, value, options) => {
2349
- if (key === "lang") {
2350
- if (!isValidLanguage(value)) {
2351
- console.error(chalk16.red(`Invalid language: ${value}`));
2352
- console.error(chalk16.gray(`Valid languages: ${VALID_LANGUAGES.join(", ")}`));
2613
+ if (options.apply !== void 0) {
2614
+ const index = typeof options.apply === "string" ? options.apply : "0";
2615
+ try {
2616
+ await git.stash(["apply", `stash@{${index}}`]);
2617
+ console.log(chalk17.green(`\u2713 Applied stash@{${index}}`));
2618
+ } catch (err) {
2619
+ console.error(
2620
+ chalk17.red(
2621
+ `Failed to apply stash: ${err instanceof Error ? err.message : "Unknown error"}`
2622
+ )
2623
+ );
2353
2624
  process.exit(1);
2354
2625
  }
2626
+ return;
2627
+ }
2628
+ if (options.pop !== void 0) {
2629
+ const index = typeof options.pop === "string" ? options.pop : "0";
2355
2630
  try {
2356
- setLanguage(value, options.local ?? false);
2357
- const scope = options.local ? "(local)" : "(global)";
2358
- console.log(chalk16.green(`\u2713 Language set to: ${value} ${scope}`));
2631
+ await git.stash(["pop", `stash@{${index}}`]);
2632
+ console.log(chalk17.green(`\u2713 Popped stash@{${index}}`));
2359
2633
  } catch (err) {
2360
- console.error(chalk16.red(err.message));
2634
+ console.error(
2635
+ chalk17.red(`Failed to pop stash: ${err instanceof Error ? err.message : "Unknown error"}`)
2636
+ );
2361
2637
  process.exit(1);
2362
2638
  }
2363
- } else {
2364
- console.error(chalk16.red(`Unknown config key: ${key}`));
2365
- console.error(chalk16.gray("Available keys: lang"));
2366
- process.exit(1);
2367
- }
2368
- });
2369
- configCommand.command("get <key>").description("Get a configuration value").action((key) => {
2370
- const config = getConfig();
2371
- if (key in config) {
2372
- console.log(config[key]);
2373
- } else {
2374
- console.error(chalk16.red(`Unknown config key: ${key}`));
2375
- process.exit(1);
2376
- }
2377
- });
2378
- configCommand.command("list").description("List all configuration values").action(() => {
2379
- const localConfig = getLocalConfig();
2380
- const effectiveConfig = getConfig();
2381
- console.log(chalk16.bold("Configuration:"));
2382
- console.log();
2383
- for (const key of Object.keys(effectiveConfig)) {
2384
- const value = effectiveConfig[key];
2385
- const isLocal = key in localConfig;
2386
- const scope = isLocal ? chalk16.cyan(" (local)") : chalk16.gray(" (global)");
2387
- console.log(` ${chalk16.cyan(key)}: ${value}${scope}`);
2388
- }
2389
- if (Object.keys(localConfig).length > 0) {
2390
- console.log();
2391
- console.log(chalk16.gray("Local config: .gut/config.json"));
2639
+ return;
2392
2640
  }
2393
- });
2394
- 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) => {
2395
- const git = simpleGit14();
2396
- const isLocal = options.local === true;
2397
- let targetPath;
2398
- if (isLocal) {
2399
- const isRepo = await git.checkIsRepo();
2400
- if (!isRepo) {
2401
- console.error(chalk16.red("Error: Not a git repository"));
2402
- console.error(chalk16.gray("Use --global to open global config folder"));
2641
+ if (options.drop !== void 0) {
2642
+ const index = typeof options.drop === "string" ? options.drop : "0";
2643
+ try {
2644
+ await git.stash(["drop", `stash@{${index}}`]);
2645
+ console.log(chalk17.green(`\u2713 Dropped stash@{${index}}`));
2646
+ } catch (err) {
2647
+ console.error(
2648
+ chalk17.red(`Failed to drop stash: ${err instanceof Error ? err.message : "Unknown error"}`)
2649
+ );
2403
2650
  process.exit(1);
2404
2651
  }
2405
- const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2406
- targetPath = join5(repoRoot.trim(), ".gut");
2407
- } else {
2408
- if (options.templates) {
2409
- targetPath = join5(homedir3(), ".config", "gut", "templates");
2652
+ return;
2653
+ }
2654
+ if (options.clear) {
2655
+ const readline = await import("readline");
2656
+ const rl = readline.createInterface({
2657
+ input: process.stdin,
2658
+ output: process.stdout
2659
+ });
2660
+ const answer = await new Promise((resolve) => {
2661
+ rl.question(chalk17.yellow("Clear all stashes? This cannot be undone. (y/N) "), resolve);
2662
+ });
2663
+ rl.close();
2664
+ if (answer.toLowerCase() === "y") {
2665
+ await git.stash(["clear"]);
2666
+ console.log(chalk17.green("\u2713 Cleared all stashes"));
2410
2667
  } else {
2411
- targetPath = join5(homedir3(), ".config", "gut");
2668
+ console.log(chalk17.gray("Cancelled"));
2412
2669
  }
2670
+ return;
2413
2671
  }
2414
- if (!existsSync5(targetPath)) {
2415
- mkdirSync2(targetPath, { recursive: true });
2416
- console.log(chalk16.green(`Created ${targetPath}`));
2417
- }
2418
- try {
2419
- openFolder(targetPath);
2420
- console.log(chalk16.green(`Opened: ${targetPath}`));
2421
- } catch (error) {
2422
- console.error(chalk16.red(`Failed to open folder: ${targetPath}`));
2423
- console.error(chalk16.gray(error.message));
2424
- process.exit(1);
2672
+ const status = await git.status();
2673
+ if (status.isClean()) {
2674
+ console.log(chalk17.yellow("No changes to stash"));
2675
+ return;
2425
2676
  }
2426
- });
2427
-
2428
- // src/commands/lang.ts
2429
- import { Command as Command16 } from "commander";
2430
- import chalk17 from "chalk";
2431
- 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) => {
2432
- if (!language) {
2433
- const lang = getLanguage();
2434
- const localConfig = getLocalConfig();
2435
- const isLocal = "lang" in localConfig;
2436
- const scope = isLocal ? chalk17.cyan("(local)") : chalk17.gray("(global)");
2437
- console.log(`${lang} ${scope}`);
2438
- return;
2439
- }
2440
- if (!isValidLanguage(language)) {
2441
- console.error(chalk17.red(`Invalid language: ${language}`));
2442
- console.error(chalk17.gray(`Valid languages: ${VALID_LANGUAGES.join(", ")}`));
2443
- process.exit(1);
2444
- }
2445
- try {
2446
- setLanguage(language, options.local ?? false);
2447
- const scope = options.local ? "(local)" : "(global)";
2448
- console.log(chalk17.green(`\u2713 Language set to: ${language} ${scope}`));
2449
- } catch (err) {
2450
- console.error(chalk17.red(err.message));
2451
- process.exit(1);
2677
+ let stashName;
2678
+ if (name) {
2679
+ stashName = name;
2680
+ } else {
2681
+ const provider = await resolveProvider(options.provider);
2682
+ const diff = await git.diff();
2683
+ const stagedDiff = await git.diff(["--cached"]);
2684
+ const fullDiff = `${diff}
2685
+ ${stagedDiff}`;
2686
+ if (!fullDiff.trim()) {
2687
+ stashName = `WIP: untracked files (${status.not_added.length} files)`;
2688
+ } else {
2689
+ const spinner = ora13("Generating stash name...").start();
2690
+ try {
2691
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2692
+ const template = findTemplate(repoRoot.trim(), "stash");
2693
+ stashName = await generateStashName(
2694
+ fullDiff,
2695
+ { provider, model: options.model },
2696
+ template || void 0
2697
+ );
2698
+ spinner.stop();
2699
+ } catch {
2700
+ spinner.fail("Failed to generate name, using default");
2701
+ stashName = `WIP: ${status.modified.length} modified, ${status.not_added.length} untracked`;
2702
+ }
2703
+ }
2452
2704
  }
2705
+ await git.stash(["push", "-u", "-m", stashName]);
2706
+ console.log(chalk17.green(`\u2713 Stashed: ${stashName}`));
2453
2707
  });
2454
2708
 
2455
- // src/commands/init.ts
2456
- import { Command as Command17 } from "commander";
2709
+ // src/commands/summary.ts
2457
2710
  import chalk18 from "chalk";
2711
+ import { Command as Command17 } from "commander";
2458
2712
  import ora14 from "ora";
2459
2713
  import { simpleGit as simpleGit15 } from "simple-git";
2460
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
2461
- import { join as join6, dirname as dirname2 } from "path";
2462
- import { homedir as homedir4 } from "os";
2463
- import { fileURLToPath as fileURLToPath2 } from "url";
2464
- import { execSync as execSync8 } from "child_process";
2465
- import { generateText as generateText2 } from "ai";
2466
- import { createGoogleGenerativeAI as createGoogleGenerativeAI2 } from "@ai-sdk/google";
2467
- import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
2468
- import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
2469
- function openFolder2(path2) {
2470
- const platform = process.platform;
2471
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? 'start ""' : "xdg-open";
2472
- execSync8(`${cmd} "${path2}"`);
2473
- }
2474
- var __filename2 = fileURLToPath2(import.meta.url);
2475
- var __dirname2 = dirname2(__filename2);
2476
- var GUT_ROOT2 = join6(__dirname2, "..");
2477
- var TEMPLATE_FILES = [
2478
- "branch.md",
2479
- "changelog.md",
2480
- "checkout.md",
2481
- "commit.md",
2482
- "explain.md",
2483
- "explain-file.md",
2484
- "find.md",
2485
- "merge.md",
2486
- "pr.md",
2487
- "review.md",
2488
- "stash.md",
2489
- "summary.md"
2490
- ];
2491
- async function translateTemplate(content, targetLang, provider) {
2492
- const apiKey = await getApiKey(provider);
2493
- if (!apiKey) {
2494
- throw new Error(`No API key found for ${provider}`);
2714
+ var summaryCommand = new Command17("summary").description("Generate a work summary from your commits (for daily/weekly reports)").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic, ollama)").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) => {
2715
+ const git = simpleGit15();
2716
+ const isRepo = await git.checkIsRepo();
2717
+ if (!isRepo) {
2718
+ console.error(chalk18.red("Error: Not a git repository"));
2719
+ process.exit(1);
2495
2720
  }
2496
- const modelName = provider === "gemini" ? "gemini-2.0-flash" : provider === "openai" ? "gpt-4o-mini" : "claude-sonnet-4-20250514";
2497
- let model;
2498
- switch (provider) {
2499
- case "gemini": {
2500
- const google = createGoogleGenerativeAI2({ apiKey });
2501
- model = google(modelName);
2502
- break;
2721
+ const provider = await resolveProvider(options.provider);
2722
+ const spinner = ora14("Generating summary...").start();
2723
+ try {
2724
+ let author = options.author;
2725
+ if (!author) {
2726
+ const config = await git.listConfig();
2727
+ author = config.all["user.name"] || "";
2728
+ if (!author) {
2729
+ spinner.fail("Could not determine git user. Use --author to specify.");
2730
+ process.exit(1);
2731
+ }
2503
2732
  }
2504
- case "openai": {
2505
- const openai = createOpenAI2({ apiKey });
2506
- model = openai(modelName);
2507
- break;
2733
+ let since = options.since;
2734
+ let format = "custom";
2735
+ if (options.daily) {
2736
+ since = "today";
2737
+ format = "daily";
2738
+ } else if (options.weekly) {
2739
+ since = "1 week ago";
2740
+ format = "weekly";
2741
+ } else if (since === "today") {
2742
+ format = "daily";
2508
2743
  }
2509
- case "anthropic": {
2510
- const anthropic = createAnthropic2({ apiKey });
2511
- model = anthropic(modelName);
2512
- break;
2744
+ const sinceDate = resolveDate(since);
2745
+ spinner.text = `Fetching commits by ${author} since ${since}...`;
2746
+ const logOptions = [`--author=${author}`, `--since=${sinceDate}`];
2747
+ if (options.until) {
2748
+ logOptions.push(`--until=${resolveDate(options.until)}`);
2513
2749
  }
2514
- default:
2515
- throw new Error(`Unsupported provider for translation: ${provider}`);
2516
- }
2517
- const langNames = {
2518
- ja: "Japanese",
2519
- en: "English",
2520
- zh: "Chinese",
2521
- ko: "Korean",
2522
- es: "Spanish",
2523
- fr: "French",
2524
- de: "German"
2525
- };
2526
- const targetLangName = langNames[targetLang] || targetLang;
2527
- const { text } = await generateText2({
2528
- model,
2529
- prompt: `Translate the following prompt template to ${targetLangName}.
2530
- Keep all {{variable}} placeholders exactly as they are - do not translate them.
2531
- Keep the markdown formatting intact.
2532
- Only translate the instructional text.
2533
-
2534
- Template to translate:
2535
- ${content}
2536
-
2537
- Translated template:`
2538
- });
2539
- return text.trim();
2540
- }
2541
- 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) => {
2542
- const isGlobal = options.global === true;
2543
- const git = simpleGit15();
2544
- let targetDir;
2545
- if (isGlobal) {
2546
- targetDir = join6(homedir4(), ".config", "gut", "templates");
2547
- } else {
2548
- const isRepo = await git.checkIsRepo();
2549
- if (!isRepo) {
2550
- console.error(chalk18.red("Error: Not a git repository"));
2551
- console.error(chalk18.gray("Use --global to initialize templates globally"));
2552
- process.exit(1);
2750
+ const log = await git.log(logOptions);
2751
+ if (log.all.length === 0) {
2752
+ spinner.info(`No commits found for ${author} since ${since}`);
2753
+ process.exit(0);
2754
+ }
2755
+ spinner.text = `Analyzing ${log.all.length} commits...`;
2756
+ let diff;
2757
+ if (options.withDiff && log.all.length > 0) {
2758
+ const oldest = log.all[log.all.length - 1].hash;
2759
+ const newest = log.all[0].hash;
2760
+ try {
2761
+ diff = await git.diff([`${oldest}^`, newest]);
2762
+ } catch {
2763
+ }
2553
2764
  }
2765
+ const commits = log.all.map((c) => ({
2766
+ hash: c.hash,
2767
+ message: c.message,
2768
+ date: c.date
2769
+ }));
2554
2770
  const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2555
- targetDir = join6(repoRoot.trim(), ".gut");
2771
+ const template = findTemplate(repoRoot.trim(), "summary");
2772
+ const summary = await generateWorkSummary(
2773
+ { commits, author, since, until: options.until, diff },
2774
+ { provider, model: options.model },
2775
+ format,
2776
+ template || void 0
2777
+ );
2778
+ spinner.stop();
2779
+ if (options.json) {
2780
+ console.log(JSON.stringify(summary, null, 2));
2781
+ return;
2782
+ }
2783
+ const output = options.markdown ? formatMarkdown(summary, author, since, options.until) : null;
2784
+ if (options.copy) {
2785
+ const textToCopy = output || formatMarkdown(summary, author, since, options.until);
2786
+ const { execSync: execSync9 } = await import("child_process");
2787
+ try {
2788
+ execSync9("pbcopy", { input: textToCopy });
2789
+ console.log(chalk18.green("Summary copied to clipboard!"));
2790
+ console.log();
2791
+ } catch {
2792
+ console.log(chalk18.yellow("Could not copy to clipboard"));
2793
+ }
2794
+ }
2795
+ if (options.markdown) {
2796
+ console.log(output);
2797
+ } else {
2798
+ printSummary(summary, author, since, options.until);
2799
+ }
2800
+ } catch (error) {
2801
+ spinner.fail("Failed to generate summary");
2802
+ console.error(chalk18.red(error instanceof Error ? error.message : "Unknown error"));
2803
+ process.exit(1);
2556
2804
  }
2557
- if (!existsSync6(targetDir)) {
2558
- mkdirSync3(targetDir, { recursive: true });
2559
- console.log(chalk18.green(`Created ${targetDir}`));
2805
+ });
2806
+ function formatMarkdown(summary, author, since, until) {
2807
+ const lines = [];
2808
+ const period = until ? `${since} - ${until}` : `${since} - now`;
2809
+ lines.push(`# ${summary.title}`);
2810
+ lines.push("");
2811
+ lines.push(`**Author:** ${author}`);
2812
+ lines.push(`**Period:** ${period}`);
2813
+ if (summary.stats) {
2814
+ lines.push(`**Commits:** ${summary.stats.commits}`);
2560
2815
  }
2561
- console.log(chalk18.blue(isGlobal ? "Initializing global templates...\n" : "Initializing project templates...\n"));
2562
- const sourceDir = join6(GUT_ROOT2, ".gut");
2563
- const lang = getLanguage();
2564
- const needsTranslation = options.translate !== false && lang !== "en";
2565
- const provider = options.provider.toLowerCase();
2566
- if (needsTranslation) {
2567
- console.log(chalk18.gray(`Language: ${lang} - templates will be translated
2816
+ lines.push("");
2817
+ lines.push("## Overview");
2818
+ lines.push(summary.overview);
2819
+ lines.push("");
2820
+ if (summary.highlights.length > 0) {
2821
+ lines.push("## Highlights");
2822
+ for (const highlight of summary.highlights) {
2823
+ lines.push(`- ${highlight}`);
2824
+ }
2825
+ lines.push("");
2826
+ }
2827
+ if (summary.details.length > 0) {
2828
+ lines.push("## Details");
2829
+ for (const section of summary.details) {
2830
+ lines.push(`### ${section.category}`);
2831
+ for (const item of section.items) {
2832
+ lines.push(`- ${item}`);
2833
+ }
2834
+ lines.push("");
2835
+ }
2836
+ }
2837
+ return lines.join("\n");
2838
+ }
2839
+ function resolveDate(dateStr) {
2840
+ const now = /* @__PURE__ */ new Date();
2841
+ if (dateStr === "today") {
2842
+ return formatDate(now);
2843
+ } else if (dateStr === "yesterday") {
2844
+ const d = new Date(now);
2845
+ d.setDate(d.getDate() - 1);
2846
+ return formatDate(d);
2847
+ } else if (dateStr.match(/^(\d+)\s+(day|days)\s+ago$/i)) {
2848
+ const match = dateStr.match(/^(\d+)\s+(day|days)\s+ago$/i);
2849
+ const days = parseInt(match[1], 10);
2850
+ const d = new Date(now);
2851
+ d.setDate(d.getDate() - days);
2852
+ return formatDate(d);
2853
+ } else if (dateStr.match(/^(\d+)\s+(week|weeks)\s+ago$/i)) {
2854
+ const match = dateStr.match(/^(\d+)\s+(week|weeks)\s+ago$/i);
2855
+ const weeks = parseInt(match[1], 10);
2856
+ const d = new Date(now);
2857
+ d.setDate(d.getDate() - weeks * 7);
2858
+ return formatDate(d);
2859
+ }
2860
+ return dateStr;
2861
+ }
2862
+ function formatDate(d) {
2863
+ const year = d.getFullYear();
2864
+ const month = String(d.getMonth() + 1).padStart(2, "0");
2865
+ const day = String(d.getDate()).padStart(2, "0");
2866
+ return `${year}-${month}-${day} 00:00:00`;
2867
+ }
2868
+ function printSummary(summary, author, since, until) {
2869
+ const period = until ? `${since} - ${until}` : `${since} - now`;
2870
+ console.log(chalk18.bold(`
2871
+ \u{1F4CA} ${summary.title}
2568
2872
  `));
2569
- }
2570
- const spinner = ora14();
2571
- let copied = 0;
2572
- let skipped = 0;
2573
- for (const filename of TEMPLATE_FILES) {
2574
- const sourcePath = join6(sourceDir, filename);
2575
- const targetPath = join6(targetDir, filename);
2576
- if (!existsSync6(sourcePath)) {
2577
- continue;
2578
- }
2579
- if (existsSync6(targetPath) && !options.force) {
2580
- console.log(chalk18.gray(` Skipped: ${filename} (already exists)`));
2581
- skipped++;
2582
- continue;
2583
- }
2584
- let content = readFileSync6(sourcePath, "utf-8");
2585
- if (needsTranslation) {
2586
- spinner.start(`Translating ${filename}...`);
2587
- try {
2588
- content = await translateTemplate(content, lang, provider);
2589
- spinner.succeed(`Translated: ${filename}`);
2590
- } catch (error) {
2591
- spinner.fail(`Failed to translate ${filename}`);
2592
- console.error(chalk18.red(` ${error instanceof Error ? error.message : "Unknown error"}`));
2593
- console.log(chalk18.gray(` Using original English template`));
2594
- }
2595
- } else {
2596
- console.log(chalk18.green(` Copied: ${filename}`));
2597
- }
2598
- writeFileSync3(targetPath, content);
2599
- copied++;
2873
+ console.log(chalk18.gray(`Author: ${author}`));
2874
+ console.log(chalk18.gray(`Period: ${period}`));
2875
+ if (summary.stats) {
2876
+ console.log(chalk18.gray(`Commits: ${summary.stats.commits}`));
2600
2877
  }
2601
2878
  console.log();
2602
- if (copied > 0) {
2603
- const location = isGlobal ? "~/.config/gut/templates/" : ".gut/";
2604
- console.log(chalk18.green(`\u2713 ${copied} template(s) initialized in ${location}`));
2605
- }
2606
- if (skipped > 0) {
2607
- console.log(chalk18.gray(` ${skipped} template(s) skipped (use --force to overwrite)`));
2608
- }
2609
- if (isGlobal) {
2610
- console.log(chalk18.gray("\nGlobal templates will be used as fallback for all projects."));
2611
- console.log(chalk18.gray("Project-level templates (.gut/) take priority over global templates."));
2612
- } else {
2613
- console.log(chalk18.gray("\nYou can now customize these templates for your project."));
2879
+ console.log(chalk18.cyan("Overview:"));
2880
+ console.log(` ${summary.overview}`);
2881
+ console.log();
2882
+ if (summary.highlights.length > 0) {
2883
+ console.log(chalk18.cyan("Highlights:"));
2884
+ for (const highlight of summary.highlights) {
2885
+ console.log(` ${chalk18.green("\u2605")} ${highlight}`);
2886
+ }
2887
+ console.log();
2614
2888
  }
2615
- if (options.open) {
2616
- try {
2617
- openFolder2(targetDir);
2618
- console.log(chalk18.green(`
2619
- Opened: ${targetDir}`));
2620
- } catch (error) {
2621
- console.error(chalk18.red(`
2622
- Failed to open folder: ${targetDir}`));
2889
+ if (summary.details.length > 0) {
2890
+ console.log(chalk18.cyan("Details:"));
2891
+ for (const section of summary.details) {
2892
+ console.log(` ${chalk18.yellow(section.category)}`);
2893
+ for (const item of section.items) {
2894
+ console.log(` \u2022 ${item}`);
2895
+ }
2623
2896
  }
2897
+ console.log();
2624
2898
  }
2625
- });
2899
+ }
2626
2900
 
2627
- // src/commands/gitignore.ts
2628
- import { Command as Command18 } from "commander";
2901
+ // src/commands/sync.ts
2629
2902
  import chalk19 from "chalk";
2903
+ import { Command as Command18 } from "commander";
2630
2904
  import ora15 from "ora";
2631
2905
  import { simpleGit as simpleGit16 } from "simple-git";
2632
- import { readdirSync, readFileSync as readFileSync7, existsSync as existsSync7, writeFileSync as writeFileSync4 } from "fs";
2633
- import { join as join7 } from "path";
2634
- var CONFIG_FILES = [
2635
- // JavaScript/TypeScript
2636
- "package.json",
2637
- "tsconfig.json",
2638
- "vite.config.ts",
2639
- "vite.config.js",
2640
- "next.config.js",
2641
- "next.config.mjs",
2642
- "nuxt.config.ts",
2643
- "astro.config.mjs",
2644
- // Python
2645
- "pyproject.toml",
2646
- "setup.py",
2647
- "requirements.txt",
2648
- "Pipfile",
2649
- "poetry.lock",
2650
- // Go
2651
- "go.mod",
2652
- "go.sum",
2653
- // Rust
2654
- "Cargo.toml",
2655
- "Cargo.lock",
2656
- // Java/Kotlin
2657
- "pom.xml",
2658
- "build.gradle",
2659
- "build.gradle.kts",
2660
- // Ruby
2661
- "Gemfile",
2662
- "Gemfile.lock",
2663
- // PHP
2664
- "composer.json",
2665
- "composer.lock",
2666
- // .NET
2667
- "*.csproj",
2668
- "*.fsproj",
2669
- "*.sln",
2670
- // Elixir
2671
- "mix.exs",
2672
- // Dart/Flutter
2673
- "pubspec.yaml",
2674
- // Swift
2675
- "Package.swift"
2676
- ];
2677
- function getFiles(dir, maxDepth = 3, currentDepth = 0) {
2678
- if (currentDepth >= maxDepth) return [];
2679
- const files = [];
2906
+ var syncCommand = new Command18("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) => {
2907
+ const git = simpleGit16();
2908
+ const isRepo = await git.checkIsRepo();
2909
+ if (!isRepo) {
2910
+ console.error(chalk19.red("Error: Not a git repository"));
2911
+ process.exit(1);
2912
+ }
2913
+ const spinner = ora15("Checking repository status...").start();
2680
2914
  try {
2681
- const entries = readdirSync(dir, { withFileTypes: true });
2682
- for (const entry of entries) {
2683
- if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "vendor" || entry.name === "target" || entry.name === "__pycache__" || entry.name === "venv" || entry.name === ".venv") {
2684
- continue;
2915
+ const status = await git.status();
2916
+ const hasChanges = !status.isClean();
2917
+ if (hasChanges && !options.stash && !options.force) {
2918
+ spinner.stop();
2919
+ console.log(chalk19.yellow("You have uncommitted changes:"));
2920
+ if (status.modified.length > 0) {
2921
+ console.log(chalk19.gray(` Modified: ${status.modified.length} file(s)`));
2685
2922
  }
2686
- const fullPath = join7(dir, entry.name);
2687
- if (entry.isDirectory()) {
2688
- files.push(entry.name + "/");
2689
- const subFiles = getFiles(fullPath, maxDepth, currentDepth + 1);
2690
- files.push(...subFiles.map((f) => entry.name + "/" + f));
2691
- } else {
2692
- files.push(entry.name);
2923
+ if (status.not_added.length > 0) {
2924
+ console.log(chalk19.gray(` Untracked: ${status.not_added.length} file(s)`));
2693
2925
  }
2926
+ console.log();
2927
+ console.log(chalk19.gray("Use --stash to auto-stash, or --force to sync anyway"));
2928
+ process.exit(1);
2694
2929
  }
2695
- } catch {
2696
- }
2697
- return files;
2698
- }
2699
- function findConfigFiles(repoRoot) {
2700
- const found = /* @__PURE__ */ new Map();
2701
- for (const configFile of CONFIG_FILES) {
2702
- if (configFile.includes("*")) {
2703
- const ext = configFile.replace("*", "");
2704
- try {
2705
- const entries = readdirSync(repoRoot);
2706
- for (const entry of entries) {
2707
- if (entry.endsWith(ext)) {
2708
- const content = readFileSync7(join7(repoRoot, entry), "utf-8");
2709
- found.set(entry, content.slice(0, 2e3));
2710
- }
2711
- }
2712
- } catch {
2930
+ let stashed = false;
2931
+ if (hasChanges && options.stash) {
2932
+ spinner.text = "Stashing changes...";
2933
+ await git.stash(["push", "-m", `gut-sync: auto-stash before sync`]);
2934
+ stashed = true;
2935
+ }
2936
+ spinner.text = "Fetching from remote...";
2937
+ await git.fetch(["--all", "--prune"]);
2938
+ const currentBranch = status.current;
2939
+ if (!currentBranch) {
2940
+ spinner.fail("Could not determine current branch");
2941
+ process.exit(1);
2942
+ }
2943
+ const trackingBranch = status.tracking;
2944
+ if (!trackingBranch) {
2945
+ spinner.warn(`Branch ${currentBranch} has no upstream tracking branch`);
2946
+ console.log(chalk19.gray(`
2947
+ To set upstream: git push -u origin ${currentBranch}`));
2948
+ if (stashed) {
2949
+ await git.stash(["pop"]);
2950
+ console.log(chalk19.gray("Restored stashed changes"));
2713
2951
  }
2714
- } else {
2715
- const filePath = join7(repoRoot, configFile);
2716
- if (existsSync7(filePath)) {
2717
- try {
2718
- const content = readFileSync7(filePath, "utf-8");
2719
- found.set(configFile, content.slice(0, 2e3));
2720
- } catch {
2721
- }
2952
+ return;
2953
+ }
2954
+ const strategy = options.merge ? "merge" : "rebase";
2955
+ spinner.text = `Syncing with ${trackingBranch} (${strategy})...`;
2956
+ try {
2957
+ if (options.merge) {
2958
+ await git.merge([trackingBranch]);
2959
+ } else {
2960
+ await git.rebase([trackingBranch]);
2961
+ }
2962
+ } catch {
2963
+ spinner.fail(`${strategy} failed - you may have conflicts`);
2964
+ console.log(chalk19.yellow("\nResolve conflicts and then:"));
2965
+ if (options.merge) {
2966
+ console.log(chalk19.gray(" git add . && git commit"));
2967
+ } else {
2968
+ console.log(chalk19.gray(" git add . && git rebase --continue"));
2969
+ }
2970
+ if (stashed) {
2971
+ console.log(
2972
+ chalk19.yellow("\nNote: You have stashed changes. Run `git stash pop` after resolving.")
2973
+ );
2722
2974
  }
2975
+ process.exit(1);
2723
2976
  }
2724
- }
2725
- return found;
2726
- }
2727
- 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) => {
2728
- const git = simpleGit16();
2729
- const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2730
- const root = repoRoot.trim();
2731
- const provider = options.provider.toLowerCase();
2732
- const template = findTemplate(root, "gitignore");
2733
- if (template) {
2734
- console.log(chalk19.gray("Using template from project..."));
2735
- }
2736
- const spinner = ora15("Analyzing project structure...").start();
2737
- const files = getFiles(root);
2738
- const configFiles = findConfigFiles(root);
2739
- const gitignorePath = join7(root, options.output);
2740
- let existingGitignore;
2741
- if (existsSync7(gitignorePath)) {
2742
- existingGitignore = readFileSync7(gitignorePath, "utf-8");
2743
- }
2744
- let configFilesStr = "";
2745
- if (configFiles.size > 0) {
2746
- const entries = [];
2747
- for (const [name, content] of configFiles) {
2748
- entries.push(`### ${name}
2749
- \`\`\`
2750
- ${content}
2751
- \`\`\``);
2977
+ const newStatus = await git.status();
2978
+ const ahead = newStatus.ahead || 0;
2979
+ const behind = newStatus.behind || 0;
2980
+ spinner.succeed(chalk19.green("Synced successfully"));
2981
+ if (behind > 0) {
2982
+ console.log(chalk19.yellow(` \u2193 ${behind} commit(s) behind`));
2752
2983
  }
2753
- configFilesStr = entries.join("\n\n");
2754
- }
2755
- spinner.text = "Generating .gitignore...";
2756
- try {
2757
- const gitignoreContent = await generateGitignore(
2758
- {
2759
- files: files.slice(0, 200).join("\n"),
2760
- configFiles: configFilesStr,
2761
- existingGitignore
2762
- },
2763
- { provider, model: options.model },
2764
- template || void 0
2765
- );
2766
- spinner.stop();
2767
- if (options.stdout) {
2768
- console.log(gitignoreContent);
2769
- return;
2984
+ if (ahead > 0) {
2985
+ if (options.push !== false) {
2986
+ const pushSpinner = ora15("Pushing to remote...").start();
2987
+ try {
2988
+ await git.push();
2989
+ pushSpinner.succeed(chalk19.green(`Pushed ${ahead} commit(s)`));
2990
+ } catch (err) {
2991
+ pushSpinner.fail("Push failed");
2992
+ console.error(chalk19.red(err instanceof Error ? err.message : "Unknown error"));
2993
+ }
2994
+ } else {
2995
+ console.log(chalk19.cyan(` \u2191 ${ahead} commit(s) ahead`));
2996
+ }
2770
2997
  }
2771
- console.log(chalk19.bold("\nGenerated .gitignore:\n"));
2772
- console.log(chalk19.gray("\u2500".repeat(50)));
2773
- console.log(gitignoreContent);
2774
- console.log(chalk19.gray("\u2500".repeat(50)));
2775
- console.log();
2776
- if (existsSync7(gitignorePath) && !options.yes) {
2777
- const readline = await import("readline");
2778
- const rl = readline.createInterface({
2779
- input: process.stdin,
2780
- output: process.stdout
2781
- });
2782
- const answer = await new Promise((resolve) => {
2783
- rl.question(chalk19.cyan(`${options.output} already exists. Overwrite? (y/N) `), resolve);
2784
- });
2785
- rl.close();
2786
- if (answer.toLowerCase() !== "y") {
2787
- console.log(chalk19.gray("Aborted."));
2788
- return;
2998
+ if (stashed) {
2999
+ spinner.start("Restoring stashed changes...");
3000
+ try {
3001
+ await git.stash(["pop"]);
3002
+ spinner.succeed("Restored stashed changes");
3003
+ } catch {
3004
+ spinner.warn("Could not auto-restore stash (may have conflicts)");
3005
+ console.log(chalk19.gray(" Run `git stash pop` manually"));
2789
3006
  }
2790
3007
  }
2791
- writeFileSync4(gitignorePath, gitignoreContent);
2792
- console.log(chalk19.green(`\u2713 Wrote ${options.output}`));
2793
- } catch (error) {
2794
- spinner.fail("Failed to generate .gitignore");
2795
- console.error(chalk19.red(error instanceof Error ? error.message : "Unknown error"));
3008
+ } catch (err) {
3009
+ spinner.fail("Sync failed");
3010
+ console.error(chalk19.red(err instanceof Error ? err.message : "Unknown error"));
2796
3011
  process.exit(1);
2797
3012
  }
2798
3013
  });