gut-cli 0.1.22 → 0.1.24

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,
@@ -410,34 +479,29 @@ async function generateCommitMessage(diff, options, template) {
410
479
  });
411
480
  return result.text.trim();
412
481
  }
482
+ var PRDescriptionSchema = z.object({
483
+ title: z.string().describe("Concise PR title (50-72 chars)"),
484
+ body: z.string().describe("PR description in markdown format")
485
+ });
413
486
  async function generatePRDescription(context, options, template) {
414
487
  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:
421
- \`\`\`json
422
- {
423
- "title": "...",
424
- "body": "..."
425
- }
426
- \`\`\``);
427
- const result = await generateText({
488
+ const prompt = buildPrompt(
489
+ template,
490
+ "pr",
491
+ {
492
+ baseBranch: context.baseBranch,
493
+ currentBranch: context.currentBranch,
494
+ commits: context.commits.map((c) => `- ${c}`).join("\n"),
495
+ diff: context.diff.slice(0, 6e3)
496
+ },
497
+ options.language
498
+ );
499
+ const result = await generateObject({
428
500
  model,
429
- prompt,
430
- maxTokens: 2e3
501
+ schema: PRDescriptionSchema,
502
+ prompt
431
503
  });
432
- try {
433
- const cleaned = result.text.replace(/```json\n?|\n?```/g, "").trim();
434
- return JSON.parse(cleaned);
435
- } catch {
436
- return {
437
- title: context.currentBranch.replace(/[-_]/g, " "),
438
- body: result.text
439
- };
440
- }
504
+ return result.object;
441
505
  }
442
506
  var CodeReviewSchema = z.object({
443
507
  summary: z.string().describe("Brief overall assessment"),
@@ -454,9 +518,14 @@ var CodeReviewSchema = z.object({
454
518
  });
455
519
  async function generateCodeReview(diff, options, template) {
456
520
  const model = await getModel(options);
457
- const prompt = buildPrompt(template, "review", {
458
- diff: diff.slice(0, 1e4)
459
- }, options.language);
521
+ const prompt = buildPrompt(
522
+ template,
523
+ "review",
524
+ {
525
+ diff: diff.slice(0, 1e4)
526
+ },
527
+ options.language
528
+ );
460
529
  const result = await generateObject({
461
530
  model,
462
531
  schema: CodeReviewSchema,
@@ -478,13 +547,18 @@ var ChangelogSchema = z.object({
478
547
  async function generateChangelog(context, options, template) {
479
548
  const model = await getModel(options);
480
549
  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);
550
+ const prompt = buildPrompt(
551
+ template,
552
+ "changelog",
553
+ {
554
+ fromRef: context.fromRef,
555
+ toRef: context.toRef,
556
+ commits: commitList,
557
+ diff: context.diff.slice(0, 8e3),
558
+ todayDate: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
559
+ },
560
+ options.language
561
+ );
488
562
  const result = await generateObject({
489
563
  model,
490
564
  schema: ChangelogSchema,
@@ -512,10 +586,15 @@ var ExplanationSchema = z.object({
512
586
  async function generateExplanation(context, options, template) {
513
587
  const model = await getModel(options);
514
588
  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);
589
+ const prompt2 = buildPrompt(
590
+ template,
591
+ "explain-file",
592
+ {
593
+ filePath: context.metadata.filePath || "",
594
+ content: context.content?.slice(0, 15e3) || ""
595
+ },
596
+ options.language
597
+ );
519
598
  const result2 = await generateObject({
520
599
  model,
521
600
  schema: ExplanationSchema,
@@ -549,11 +628,16 @@ Author: ${context.metadata.author}
549
628
  Date: ${context.metadata.date}`;
550
629
  targetType = "commit";
551
630
  }
552
- const prompt = buildPrompt(template, "explain", {
553
- targetType,
554
- contextInfo,
555
- diff: context.diff?.slice(0, 12e3) || ""
556
- }, options.language);
631
+ const prompt = buildPrompt(
632
+ template,
633
+ "explain",
634
+ {
635
+ targetType,
636
+ contextInfo,
637
+ diff: context.diff?.slice(0, 12e3) || ""
638
+ },
639
+ options.language
640
+ );
557
641
  const result = await generateObject({
558
642
  model,
559
643
  schema: ExplanationSchema,
@@ -572,12 +656,19 @@ var CommitSearchSchema = z.object({
572
656
  });
573
657
  async function searchCommits(query, commits, options, maxResults = 5, template) {
574
658
  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);
659
+ const commitList = commits.map(
660
+ (c) => `${c.hash.slice(0, 7)} | ${c.author} | ${c.date.split("T")[0]} | ${c.message.split("\n")[0]}`
661
+ ).join("\n");
662
+ const prompt = buildPrompt(
663
+ template,
664
+ "find",
665
+ {
666
+ query,
667
+ commits: commitList,
668
+ maxResults: String(maxResults)
669
+ },
670
+ options.language
671
+ );
581
672
  const result = await generateObject({
582
673
  model,
583
674
  schema: CommitSearchSchema,
@@ -606,11 +697,17 @@ async function searchCommits(query, commits, options, maxResults = 5, template)
606
697
  }
607
698
  async function generateBranchName(description, options, context, template) {
608
699
  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.");
700
+ const prompt = buildPrompt(
701
+ template,
702
+ "branch",
703
+ {
704
+ description,
705
+ type: context?.type,
706
+ issue: context?.issue
707
+ },
708
+ options.language,
709
+ "Respond with ONLY the branch name, nothing else."
710
+ );
614
711
  const result = await generateText({
615
712
  model,
616
713
  prompt,
@@ -620,9 +717,15 @@ async function generateBranchName(description, options, context, template) {
620
717
  }
621
718
  async function generateBranchNameFromDiff(diff, options, template) {
622
719
  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.");
720
+ const prompt = buildPrompt(
721
+ template,
722
+ "checkout",
723
+ {
724
+ diff: diff.slice(0, 8e3)
725
+ },
726
+ options.language,
727
+ "Respond with ONLY the branch name, nothing else."
728
+ );
626
729
  const result = await generateText({
627
730
  model,
628
731
  prompt,
@@ -632,9 +735,15 @@ async function generateBranchNameFromDiff(diff, options, template) {
632
735
  }
633
736
  async function generateStashName(diff, options, template) {
634
737
  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.");
738
+ const prompt = buildPrompt(
739
+ template,
740
+ "stash",
741
+ {
742
+ diff: diff.slice(0, 4e3)
743
+ },
744
+ options.language,
745
+ "Respond with ONLY the stash name, nothing else."
746
+ );
638
747
  const result = await generateText({
639
748
  model,
640
749
  prompt,
@@ -664,13 +773,18 @@ async function generateWorkSummary(context, options, format = "custom", template
664
773
  const commitList = context.commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message.split("\n")[0]} (${c.date.split("T")[0]})`).join("\n");
665
774
  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
775
  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);
776
+ const prompt = buildPrompt(
777
+ template,
778
+ "summary",
779
+ {
780
+ author: context.author,
781
+ period,
782
+ format: formatHint,
783
+ commits: commitList,
784
+ diff: context.diff?.slice(0, 6e3)
785
+ },
786
+ options.language
787
+ );
674
788
  const result = await generateObject({
675
789
  model,
676
790
  schema: WorkSummarySchema,
@@ -686,12 +800,17 @@ async function generateWorkSummary(context, options, format = "custom", template
686
800
  }
687
801
  async function resolveConflict(conflictedContent, context, options, template) {
688
802
  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);
803
+ const prompt = buildPrompt(
804
+ template,
805
+ "merge",
806
+ {
807
+ filename: context.filename,
808
+ oursRef: context.oursRef,
809
+ theirsRef: context.theirsRef,
810
+ content: conflictedContent
811
+ },
812
+ options.language
813
+ );
695
814
  const result = await generateObject({
696
815
  model,
697
816
  schema: ConflictResolutionSchema,
@@ -701,11 +820,17 @@ async function resolveConflict(conflictedContent, context, options, template) {
701
820
  }
702
821
  async function generateGitignore(context, options, template) {
703
822
  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.");
823
+ const prompt = buildPrompt(
824
+ template,
825
+ "gitignore",
826
+ {
827
+ files: context.files,
828
+ configFiles: context.configFiles,
829
+ existingGitignore: context.existingGitignore
830
+ },
831
+ options.language,
832
+ "Respond with ONLY the .gitignore content, nothing else. No explanations or markdown code blocks."
833
+ );
709
834
  const result = await generateText({
710
835
  model,
711
836
  prompt,
@@ -714,54 +839,106 @@ async function generateGitignore(context, options, template) {
714
839
  return result.text.trim();
715
840
  }
716
841
 
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();
720
- const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
721
- const isRepo = await git.checkIsRepo();
722
- if (!isRepo) {
723
- console.error(chalk3.red("Error: Not a git repository"));
724
- process.exit(1);
842
+ // src/lib/gh.ts
843
+ import { execSync as execSync2 } from "child_process";
844
+ import chalk2 from "chalk";
845
+ var ghInstalledCache = null;
846
+ function isGhCliInstalled() {
847
+ if (ghInstalledCache !== null) {
848
+ return ghInstalledCache;
725
849
  }
726
- const provider = options.provider.toLowerCase();
727
- if (options.all) {
728
- await git.add("-A");
850
+ try {
851
+ execSync2("gh --version", { stdio: "pipe" });
852
+ ghInstalledCache = true;
853
+ return true;
854
+ } catch {
855
+ ghInstalledCache = false;
856
+ return false;
729
857
  }
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."));
737
- process.exit(1);
738
- }
739
- console.log(chalk3.gray("No staged changes, staging all changes..."));
740
- await git.add("-A");
741
- diff = await git.diff(["--cached"]);
858
+ }
859
+ function printGhNotInstalledMessage() {
860
+ console.log(chalk2.yellow("\n\u26A0 GitHub CLI (gh) is not installed"));
861
+ console.log(chalk2.gray(" This command requires gh CLI. Install it:"));
862
+ console.log(chalk2.gray(" brew install gh (macOS)"));
863
+ console.log(chalk2.gray(" https://cli.github.com/"));
864
+ }
865
+ function requireGhCli() {
866
+ if (!isGhCliInstalled()) {
867
+ printGhNotInstalledMessage();
868
+ return false;
742
869
  }
743
- const template = findTemplate(repoRoot.trim(), "commit");
870
+ return true;
871
+ }
872
+
873
+ // src/commands/branch.ts
874
+ function getIssueInfo(issueNumber) {
875
+ try {
876
+ const result = execSync3(`gh issue view ${issueNumber} --json title,body`, {
877
+ encoding: "utf-8",
878
+ stdio: ["pipe", "pipe", "pipe"]
879
+ });
880
+ return JSON.parse(result);
881
+ } catch {
882
+ return null;
883
+ }
884
+ }
885
+ 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) => {
886
+ const git = simpleGit();
887
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
888
+ const isRepo = await git.checkIsRepo();
889
+ if (!isRepo) {
890
+ console.error(chalk3.red("Error: Not a git repository"));
891
+ process.exit(1);
892
+ }
893
+ let description;
894
+ let issueNumber;
895
+ if (options.description) {
896
+ description = options.description;
897
+ } else if (issue) {
898
+ if (!requireGhCli()) {
899
+ process.exit(1);
900
+ }
901
+ const cleanedIssue = issue.replace(/^#/, "");
902
+ issueNumber = cleanedIssue;
903
+ const spinner2 = ora(`Fetching issue #${cleanedIssue}...`).start();
904
+ const issueInfo = getIssueInfo(cleanedIssue);
905
+ if (!issueInfo) {
906
+ spinner2.fail(`Could not fetch issue #${issueNumber}`);
907
+ console.log(chalk3.gray("Make sure you are authenticated: gh auth login"));
908
+ process.exit(1);
909
+ }
910
+ spinner2.stop();
911
+ console.log(chalk3.gray(`Issue: ${issueInfo.title}`));
912
+ description = `${issueInfo.title}
913
+
914
+ ${issueInfo.body || ""}`;
915
+ } else {
916
+ console.error(chalk3.red("Error: Please provide an issue number or use -d for description"));
917
+ console.log(chalk3.gray("Usage:"));
918
+ console.log(chalk3.gray(" gut branch 123"));
919
+ console.log(chalk3.gray(' gut branch -d "add user authentication"'));
920
+ process.exit(1);
921
+ }
922
+ const provider = await resolveProvider(options.provider);
923
+ const template = findTemplate(repoRoot.trim(), "branch");
744
924
  if (template) {
745
925
  console.log(chalk3.gray("Using template from project..."));
746
926
  }
747
- const spinner = ora2("Generating commit message...").start();
927
+ const spinner = ora("Generating branch name...").start();
748
928
  try {
749
- const message = await generateCommitMessage(
750
- diff,
929
+ const branchName = await generateBranchName(
930
+ description,
751
931
  { provider, model: options.model },
932
+ { type: options.type, issue: issueNumber },
752
933
  template || void 0
753
934
  );
754
935
  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
- }
936
+ console.log(chalk3.bold("\nGenerated branch name:\n"));
937
+ console.log(chalk3.green(` ${branchName}`));
761
938
  console.log();
762
- if (options.commit) {
763
- await git.commit(message);
764
- console.log(chalk3.green("\u2713 Committed successfully"));
939
+ if (options.checkout) {
940
+ await git.checkoutLocalBranch(branchName);
941
+ console.log(chalk3.green(`\u2713 Created and checked out branch: ${branchName}`));
765
942
  } else {
766
943
  const readline = await import("readline");
767
944
  const rl = readline.createInterface({
@@ -769,566 +946,512 @@ var commitCommand = new Command3("commit").description("Generate a commit messag
769
946
  output: process.stdout
770
947
  });
771
948
  const answer = await new Promise((resolve) => {
772
- rl.question(chalk3.cyan("Commit with this message? (y/N/e to edit) "), resolve);
949
+ rl.question(chalk3.cyan("Create and checkout this branch? (y/N) "), resolve);
773
950
  });
774
951
  rl.close();
775
952
  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
- }
953
+ await git.checkoutLocalBranch(branchName);
954
+ console.log(chalk3.green(`\u2713 Created and checked out branch: ${branchName}`));
796
955
  } 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]}"`));
956
+ console.log(chalk3.gray("\nTo create manually:"));
957
+ console.log(chalk3.gray(` git checkout -b ${branchName}`));
800
958
  }
801
959
  }
802
960
  } catch (error) {
803
- spinner.fail("Failed to generate commit message");
961
+ spinner.fail("Failed to generate branch name");
804
962
  console.error(chalk3.red(error instanceof Error ? error.message : "Unknown error"));
805
963
  process.exit(1);
806
964
  }
807
965
  });
808
966
 
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";
967
+ // src/commands/changelog.ts
820
968
  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;
969
+ import { Command as Command3 } from "commander";
970
+ import ora2 from "ora";
971
+ import { simpleGit as simpleGit2 } from "simple-git";
972
+ function formatChangelog(changelog) {
973
+ const lines = [];
974
+ const header = changelog.version ? `## [${changelog.version}] - ${changelog.date}` : `## ${changelog.date}`;
975
+ lines.push(header);
976
+ lines.push("");
977
+ if (changelog.summary) {
978
+ lines.push(changelog.summary);
979
+ lines.push("");
845
980
  }
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");
981
+ for (const section of changelog.sections) {
982
+ if (section.items.length > 0) {
983
+ lines.push(`### ${section.type}`);
984
+ for (const item of section.items) {
985
+ lines.push(`- ${item}`);
986
+ }
987
+ lines.push("");
862
988
  }
863
989
  }
864
- return findTemplate(repoRoot, "pr");
990
+ return lines.join("\n");
865
991
  }
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();
992
+ 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) => {
993
+ const git = simpleGit2();
868
994
  const isRepo = await git.checkIsRepo();
869
995
  if (!isRepo) {
870
- console.error(chalk5.red("Error: Not a git repository"));
996
+ console.error(chalk4.red("Error: Not a git repository"));
871
997
  process.exit(1);
872
998
  }
873
- const provider = options.provider.toLowerCase();
874
- const spinner = ora3("Analyzing branch...").start();
999
+ const provider = await resolveProvider(options.provider);
1000
+ const spinner = ora2("Analyzing commits...").start();
875
1001
  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);
1002
+ let fromRef = from;
1003
+ let toRef = to;
1004
+ if (options.tag) {
1005
+ fromRef = options.tag;
1006
+ toRef = "HEAD";
891
1007
  }
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);
1008
+ const log = await git.log({ from: fromRef, to: toRef });
1009
+ if (log.all.length === 0) {
1010
+ spinner.info("No commits found in range");
1011
+ process.exit(0);
898
1012
  }
899
- const diff = await git.diff([`${baseBranch}...${currentBranch}`]);
1013
+ spinner.text = `Found ${log.all.length} commits, generating changelog...`;
1014
+ const commits = log.all.map((c) => ({
1015
+ hash: c.hash,
1016
+ message: c.message,
1017
+ author: c.author_name,
1018
+ date: c.date
1019
+ }));
1020
+ const diff = await git.diff([`${fromRef}...${toRef}`]);
900
1021
  const repoRoot = await git.revparse(["--show-toplevel"]);
901
- const template = findPRTemplate(repoRoot.trim());
1022
+ const template = findTemplate(repoRoot.trim(), "changelog");
902
1023
  if (template) {
903
- spinner.text = "Found PR template, generating description...";
904
- } else {
905
- spinner.text = "Generating PR description...";
1024
+ spinner.text = "Using template from project...";
906
1025
  }
907
- const { title, body } = await generatePRDescription(
908
- {
909
- baseBranch,
910
- currentBranch,
911
- commits,
912
- diff
913
- },
1026
+ const changelog = await generateChangelog(
1027
+ { commits, diff, fromRef, toRef },
914
1028
  { provider, model: options.model },
915
1029
  template || void 0
916
1030
  );
917
1031
  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
- }
1032
+ if (options.json) {
1033
+ console.log(JSON.stringify(changelog, null, 2));
1034
+ return;
934
1035
  }
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
- }
1036
+ console.log(chalk4.bold("\n\u{1F4CB} Generated Changelog\n"));
1037
+ console.log(chalk4.gray("\u2500".repeat(50)));
1038
+ console.log(formatChangelog(changelog));
1039
+ console.log(chalk4.gray("\u2500".repeat(50)));
1040
+ console.log(chalk4.gray(`
1041
+ Range: ${fromRef}..${toRef} (${commits.length} commits)`));
1042
+ if (template) {
1043
+ console.log(chalk4.gray("Style matched from existing CHANGELOG.md"));
972
1044
  }
973
1045
  } catch (error) {
974
- spinner.fail("Failed to generate PR description");
975
- console.error(chalk5.red(error instanceof Error ? error.message : "Unknown error"));
1046
+ spinner.fail("Failed to generate changelog");
1047
+ console.error(chalk4.red(error instanceof Error ? error.message : "Unknown error"));
976
1048
  process.exit(1);
977
1049
  }
978
1050
  });
979
1051
 
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) {
1052
+ // src/commands/checkout.ts
1053
+ import chalk5 from "chalk";
1054
+ import { Command as Command4 } from "commander";
1055
+ import ora3 from "ora";
1056
+ import { simpleGit as simpleGit3 } from "simple-git";
1057
+ 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) => {
1058
+ const git = simpleGit3();
1059
+ const isRepo = await git.checkIsRepo();
1060
+ if (!isRepo) {
1061
+ console.error(chalk5.red("Error: Not a git repository"));
1062
+ process.exit(1);
1063
+ }
1064
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1065
+ const spinner = ora3("Analyzing changes...").start();
1066
+ const status = await git.status();
1067
+ let diff;
1068
+ if (options.staged) {
1069
+ diff = await git.diff(["--cached"]);
1070
+ } else {
1071
+ const stagedDiff = await git.diff(["--cached"]);
1072
+ const unstagedDiff = await git.diff();
1073
+ diff = `${stagedDiff}
1074
+ ${unstagedDiff}`;
1075
+ }
1076
+ const hasChanges = diff.trim() || status.not_added.length > 0 || status.created.length > 0;
1077
+ if (!hasChanges) {
1078
+ spinner.fail("No changes found");
1079
+ console.log(chalk5.gray("Make some changes first, then run gut checkout"));
1080
+ process.exit(1);
1081
+ }
1082
+ if (!diff.trim() && (status.not_added.length > 0 || status.created.length > 0)) {
1083
+ const untrackedFiles = [...status.not_added, ...status.created];
1084
+ diff = `New files:
1085
+ ${untrackedFiles.map((f) => `+ ${f}`).join("\n")}`;
1086
+ }
1087
+ spinner.text = "Generating branch name...";
1088
+ const provider = await resolveProvider(options.provider);
1089
+ const template = findTemplate(repoRoot.trim(), "checkout");
1090
+ if (template) {
1091
+ console.log(chalk5.gray("\nUsing template from project..."));
1092
+ }
987
1093
  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 {
1094
+ const branchName = await generateBranchNameFromDiff(
992
1095
  diff,
993
- prInfo: {
994
- number: prJson.number,
995
- title: prJson.title,
996
- author: prJson.author.login,
997
- url: prJson.url
1096
+ { provider, model: options.model },
1097
+ template
1098
+ );
1099
+ spinner.stop();
1100
+ console.log(chalk5.bold("\nGenerated branch name:\n"));
1101
+ console.log(chalk5.green(` ${branchName}`));
1102
+ console.log();
1103
+ if (options.yes) {
1104
+ await git.checkoutLocalBranch(branchName);
1105
+ console.log(chalk5.green(`\u2713 Created and checked out branch: ${branchName}`));
1106
+ } else {
1107
+ const readline = await import("readline");
1108
+ const rl = readline.createInterface({
1109
+ input: process.stdin,
1110
+ output: process.stdout
1111
+ });
1112
+ const answer = await new Promise((resolve) => {
1113
+ rl.question(chalk5.cyan("Create and checkout this branch? (y/N) "), resolve);
1114
+ });
1115
+ rl.close();
1116
+ if (answer.toLowerCase() === "y") {
1117
+ await git.checkoutLocalBranch(branchName);
1118
+ console.log(chalk5.green(`\u2713 Created and checked out branch: ${branchName}`));
1119
+ } else {
1120
+ console.log(chalk5.gray("\nTo create manually:"));
1121
+ console.log(chalk5.gray(` git checkout -b ${branchName}`));
998
1122
  }
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
1123
  }
1004
- throw error;
1124
+ } catch (error) {
1125
+ spinner.fail("Failed to generate branch name");
1126
+ console.error(chalk5.red(error instanceof Error ? error.message : "Unknown error"));
1127
+ process.exit(1);
1005
1128
  }
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) => {
1129
+ });
1130
+
1131
+ // src/commands/cleanup.ts
1132
+ import chalk6 from "chalk";
1133
+ import { Command as Command5 } from "commander";
1134
+ import ora4 from "ora";
1135
+ import { simpleGit as simpleGit4 } from "simple-git";
1136
+ 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) => {
1008
1137
  const git = simpleGit4();
1009
1138
  const isRepo = await git.checkIsRepo();
1010
1139
  if (!isRepo) {
1011
1140
  console.error(chalk6.red("Error: Not a git repository"));
1012
1141
  process.exit(1);
1013
1142
  }
1014
- const provider = options.provider.toLowerCase();
1015
- const spinner = ora4("Getting diff...").start();
1143
+ const spinner = ora4("Fetching branch information...").start();
1016
1144
  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(
1049
- diff,
1050
- { provider, model: options.model },
1051
- template || void 0
1052
- );
1145
+ await git.fetch(["--prune"]);
1146
+ const currentBranch = (await git.branch()).current;
1147
+ const baseBranch = options.base || await detectBaseBranch(git);
1148
+ spinner.text = `Using ${chalk6.cyan(baseBranch)} as base branch`;
1149
+ const mergedResult = await git.branch(["--merged", baseBranch]);
1150
+ const mergedBranches = mergedResult.all.filter((branch) => {
1151
+ const cleanName = branch.trim().replace(/^\* /, "");
1152
+ return cleanName !== currentBranch && cleanName !== baseBranch && !cleanName.startsWith("remotes/") && cleanName !== "main" && cleanName !== "master" && cleanName !== "develop";
1153
+ });
1053
1154
  spinner.stop();
1054
- if (options.json) {
1055
- console.log(JSON.stringify({ prInfo, review }, null, 2));
1155
+ if (mergedBranches.length === 0) {
1156
+ console.log(chalk6.green("\u2713 No merged branches to clean up"));
1056
1157
  return;
1057
1158
  }
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}`));
1159
+ console.log(chalk6.yellow(`
1160
+ Found ${mergedBranches.length} merged branch(es):
1161
+ `));
1162
+ mergedBranches.forEach((branch) => {
1163
+ console.log(` ${chalk6.red("\u2022")} ${branch}`);
1164
+ });
1165
+ if (options.dryRun) {
1166
+ console.log(chalk6.blue("\n(dry-run mode - no branches were deleted)"));
1167
+ return;
1062
1168
  }
1063
- printReview(review);
1064
- } catch (error) {
1065
- spinner.fail("Failed to generate review");
1066
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1067
- process.exit(1);
1068
- }
1069
- });
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}`);
1169
+ if (!options.force) {
1170
+ const readline = await import("readline");
1171
+ const rl = readline.createInterface({
1172
+ input: process.stdin,
1173
+ output: process.stdout
1174
+ });
1175
+ const answer = await new Promise((resolve) => {
1176
+ rl.question(chalk6.yellow("\nDelete these branches? (y/N) "), resolve);
1177
+ });
1178
+ rl.close();
1179
+ if (answer.toLowerCase() !== "y") {
1180
+ console.log(chalk6.gray("Cancelled"));
1181
+ return;
1096
1182
  }
1097
1183
  }
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}`);
1184
+ const deleteSpinner = ora4("Deleting branches...").start();
1185
+ for (const branch of mergedBranches) {
1186
+ try {
1187
+ await git.deleteLocalBranch(branch, true);
1188
+ deleteSpinner.text = `Deleted ${branch}`;
1189
+ if (options.remote) {
1190
+ try {
1191
+ await git.push("origin", `:${branch}`);
1192
+ } catch {
1193
+ }
1194
+ }
1195
+ } catch {
1196
+ deleteSpinner.warn(`Failed to delete ${branch}`);
1197
+ }
1105
1198
  }
1199
+ deleteSpinner.succeed(chalk6.green(`Deleted ${mergedBranches.length} branch(es)`));
1200
+ } catch (err) {
1201
+ spinner.fail("Failed to cleanup branches");
1202
+ console.error(chalk6.red(err instanceof Error ? err.message : "Unknown error"));
1203
+ process.exit(1);
1106
1204
  }
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();
1205
+ });
1206
+ async function detectBaseBranch(git) {
1207
+ const branches = await git.branch();
1208
+ if (branches.all.includes("main")) return "main";
1209
+ if (branches.all.includes("master")) return "master";
1210
+ return "main";
1115
1211
  }
1116
1212
 
1117
- // src/commands/merge.ts
1118
- import { Command as Command6 } from "commander";
1213
+ // src/commands/commit.ts
1119
1214
  import chalk7 from "chalk";
1215
+ import { Command as Command6 } from "commander";
1120
1216
  import ora5 from "ora";
1121
1217
  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) => {
1218
+ 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
1219
  const git = simpleGit5();
1220
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1126
1221
  const isRepo = await git.checkIsRepo();
1127
1222
  if (!isRepo) {
1128
1223
  console.error(chalk7.red("Error: Not a git repository"));
1129
1224
  process.exit(1);
1130
1225
  }
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) {
1226
+ const provider = await resolveProvider(options.provider);
1227
+ if (options.all) {
1228
+ await git.add("-A");
1148
1229
  }
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);
1230
+ let diff = await git.diff(["--cached"]);
1231
+ if (!diff.trim()) {
1232
+ const status = await git.status();
1233
+ const unstaged = await git.diff();
1234
+ const hasUntracked = status.not_added.length > 0 || status.created.length > 0;
1235
+ if (!unstaged.trim() && !hasUntracked) {
1236
+ console.error(chalk7.yellow("No changes to commit."));
1237
+ process.exit(1);
1238
+ }
1239
+ console.log(chalk7.gray("No staged changes, staging all changes..."));
1240
+ await git.add("-A");
1241
+ diff = await git.diff(["--cached"]);
1155
1242
  }
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");
1243
+ const template = findTemplate(repoRoot.trim(), "commit");
1161
1244
  if (template) {
1162
- console.log(chalk7.gray("Using merge template from project...\n"));
1245
+ console.log(chalk7.gray("Using template from project..."));
1163
1246
  }
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)));
1247
+ const spinner = ora5("Generating commit message...").start();
1248
+ try {
1249
+ const message = await generateCommitMessage(
1250
+ diff,
1251
+ { provider, model: options.model },
1252
+ template || void 0
1253
+ );
1254
+ spinner.stop();
1255
+ console.log(chalk7.bold("\nGenerated commit message:\n"));
1256
+ console.log(chalk7.green(` ${message.split("\n")[0]}`));
1257
+ if (message.includes("\n")) {
1258
+ const details = message.split("\n").slice(1).join("\n");
1259
+ console.log(
1260
+ chalk7.gray(
1261
+ details.split("\n").map((l) => ` ${l}`).join("\n")
1262
+ )
1263
+ );
1175
1264
  }
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}`));
1265
+ console.log();
1266
+ if (options.commit) {
1267
+ await git.commit(message);
1268
+ console.log(chalk7.green("\u2713 Committed successfully"));
1269
+ } else {
1192
1270
  const readline = await import("readline");
1193
1271
  const rl = readline.createInterface({
1194
1272
  input: process.stdin,
1195
1273
  output: process.stdout
1196
1274
  });
1197
1275
  const answer = await new Promise((resolve) => {
1198
- rl.question(chalk7.cyan("\nAccept this resolution? (y/n/s to skip) "), resolve);
1276
+ rl.question(chalk7.cyan("Commit with this message? (y/N/e to edit) "), resolve);
1199
1277
  });
1200
1278
  rl.close();
1201
1279
  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}`));
1280
+ await git.commit(message);
1281
+ console.log(chalk7.green("\u2713 Committed successfully"));
1282
+ } else if (answer.toLowerCase() === "e") {
1283
+ console.log(chalk7.gray("Opening editor..."));
1284
+ const { execSync: execSync9 } = await import("child_process");
1285
+ const editor = process.env.EDITOR || process.env.VISUAL || "vi";
1286
+ const fs2 = await import("fs");
1287
+ const os = await import("os");
1288
+ const path2 = await import("path");
1289
+ const tmpFile = path2.join(os.tmpdir(), "gut-commit-msg.txt");
1290
+ fs2.writeFileSync(tmpFile, message);
1291
+ execSync9(`${editor} "${tmpFile}"`, { stdio: "inherit" });
1292
+ const editedMessage = fs2.readFileSync(tmpFile, "utf-8").trim();
1293
+ fs2.unlinkSync(tmpFile);
1294
+ if (editedMessage) {
1295
+ await git.commit(editedMessage);
1296
+ console.log(chalk7.green("\u2713 Committed successfully"));
1297
+ } else {
1298
+ console.log(chalk7.yellow("Commit cancelled (empty message)"));
1299
+ }
1207
1300
  } else {
1208
- console.log(chalk7.yellow(`\u2717 Rejected - resolve manually: ${file}`));
1301
+ console.log(chalk7.gray("Commit cancelled"));
1302
+ console.log(chalk7.gray("\nTo commit manually:"));
1303
+ console.log(chalk7.gray(` git commit -m "${message.split("\n")[0]}"`));
1209
1304
  }
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
1305
  }
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"));
1306
+ } catch (error) {
1307
+ spinner.fail("Failed to generate commit message");
1308
+ console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
1309
+ process.exit(1);
1227
1310
  }
1228
1311
  });
1229
1312
 
1230
- // src/commands/changelog.ts
1231
- import { Command as Command7 } from "commander";
1313
+ // src/commands/config.ts
1314
+ import { execSync as execSync4 } from "child_process";
1315
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
1316
+ import { homedir as homedir3 } from "os";
1317
+ import { join as join3 } from "path";
1232
1318
  import chalk8 from "chalk";
1233
- import ora6 from "ora";
1319
+ import { Command as Command7 } from "commander";
1234
1320
  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");
1321
+ function openFolder(path2) {
1322
+ const platform = process.platform;
1323
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? 'start ""' : "xdg-open";
1324
+ execSync4(`${cmd} "${path2}"`);
1254
1325
  }
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";
1326
+ var configCommand = new Command7("config").description("Manage gut configuration");
1327
+ configCommand.command("set <key> <value>").description("Set a configuration value").option("--local", "Set for current repository only").action((key, value, options) => {
1328
+ if (key === "lang") {
1329
+ if (!isValidLanguage(value)) {
1330
+ console.error(chalk8.red(`Invalid language: ${value}`));
1331
+ console.error(chalk8.gray(`Valid languages: ${VALID_LANGUAGES.join(", ")}`));
1332
+ process.exit(1);
1270
1333
  }
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);
1334
+ try {
1335
+ setLanguage(value, options.local ?? false);
1336
+ const scope = options.local ? "(local)" : "(global)";
1337
+ console.log(chalk8.green(`\u2713 Language set to: ${value} ${scope}`));
1338
+ } catch (err) {
1339
+ console.error(chalk8.red(err.message));
1340
+ process.exit(1);
1275
1341
  }
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...";
1342
+ } else if (key === "model") {
1343
+ try {
1344
+ setModel(value, options.local ?? false);
1345
+ const scope = options.local ? "(local)" : "(global)";
1346
+ console.log(chalk8.green(`\u2713 Model set to: ${value} ${scope}`));
1347
+ console.log(
1348
+ chalk8.gray(
1349
+ `Default models: ${Object.entries(DEFAULT_MODELS).map(([k, v]) => `${k}=${v}`).join(", ")}`
1350
+ )
1351
+ );
1352
+ } catch (err) {
1353
+ console.error(chalk8.red(err.message));
1354
+ process.exit(1);
1288
1355
  }
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;
1356
+ } else if (key === "provider") {
1357
+ if (!isValidProvider(value)) {
1358
+ console.error(chalk8.red(`Invalid provider: ${value}`));
1359
+ console.error(chalk8.gray(`Valid providers: ${VALID_PROVIDERS.join(", ")}`));
1360
+ process.exit(1);
1298
1361
  }
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"));
1362
+ try {
1363
+ setProvider(value, options.local ?? false);
1364
+ const scope = options.local ? "(local)" : "(global)";
1365
+ console.log(chalk8.green(`\u2713 Provider set to: ${value} ${scope}`));
1366
+ } catch (err) {
1367
+ console.error(chalk8.red(err.message));
1368
+ process.exit(1);
1369
+ }
1370
+ } else {
1371
+ console.error(chalk8.red(`Unknown config key: ${key}`));
1372
+ console.error(chalk8.gray("Available keys: lang, model, provider"));
1373
+ process.exit(1);
1374
+ }
1375
+ });
1376
+ configCommand.command("get <key>").description("Get a configuration value").action((key) => {
1377
+ const config = getConfig();
1378
+ if (key in config) {
1379
+ console.log(config[key]);
1380
+ } else {
1381
+ console.error(chalk8.red(`Unknown config key: ${key}`));
1382
+ process.exit(1);
1383
+ }
1384
+ });
1385
+ configCommand.command("list").description("List all configuration values").action(() => {
1386
+ const localConfig = getLocalConfig();
1387
+ const effectiveConfig = getConfig();
1388
+ console.log(chalk8.bold("Configuration:"));
1389
+ console.log();
1390
+ for (const key of Object.keys(effectiveConfig)) {
1391
+ const value = effectiveConfig[key];
1392
+ const isLocal = key in localConfig;
1393
+ const scope = isLocal ? chalk8.cyan(" (local)") : chalk8.gray(" (global)");
1394
+ console.log(` ${chalk8.cyan(key)}: ${value}${scope}`);
1395
+ }
1396
+ if (Object.keys(localConfig).length > 0) {
1397
+ console.log();
1398
+ console.log(chalk8.gray("Local config: .gut/config.json"));
1399
+ }
1400
+ });
1401
+ 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) => {
1402
+ const git = simpleGit6();
1403
+ const isLocal = options.local === true;
1404
+ let targetPath;
1405
+ if (isLocal) {
1406
+ const isRepo = await git.checkIsRepo();
1407
+ if (!isRepo) {
1408
+ console.error(chalk8.red("Error: Not a git repository"));
1409
+ console.error(chalk8.gray("Use --global to open global config folder"));
1410
+ process.exit(1);
1411
+ }
1412
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1413
+ targetPath = join3(repoRoot.trim(), ".gut");
1414
+ } else {
1415
+ if (options.templates) {
1416
+ targetPath = join3(homedir3(), ".config", "gut", "templates");
1417
+ } else {
1418
+ targetPath = join3(homedir3(), ".config", "gut");
1307
1419
  }
1420
+ }
1421
+ if (!existsSync3(targetPath)) {
1422
+ mkdirSync2(targetPath, { recursive: true });
1423
+ console.log(chalk8.green(`Created ${targetPath}`));
1424
+ }
1425
+ try {
1426
+ openFolder(targetPath);
1427
+ console.log(chalk8.green(`Opened: ${targetPath}`));
1308
1428
  } catch (error) {
1309
- spinner.fail("Failed to generate changelog");
1310
- console.error(chalk8.red(error instanceof Error ? error.message : "Unknown error"));
1429
+ console.error(chalk8.red(`Failed to open folder: ${targetPath}`));
1430
+ console.error(chalk8.gray(error.message));
1311
1431
  process.exit(1);
1312
1432
  }
1313
1433
  });
1314
1434
 
1315
1435
  // src/commands/explain.ts
1316
- import { Command as Command8 } from "commander";
1436
+ import { execSync as execSync5 } from "child_process";
1437
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1317
1438
  import chalk9 from "chalk";
1318
- import ora7 from "ora";
1439
+ import { Command as Command8 } from "commander";
1440
+ import ora6 from "ora";
1319
1441
  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) => {
1442
+ var explainCommand = new Command8("explain").description("Get an AI-powered explanation of changes, commits, PRs, or files").argument(
1443
+ "[target]",
1444
+ "Commit hash, PR number, PR URL, or file path (default: uncommitted changes)"
1445
+ ).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
1446
  const git = simpleGit7();
1324
1447
  const isRepo = await git.checkIsRepo();
1325
1448
  if (!isRepo) {
1326
1449
  console.error(chalk9.red("Error: Not a git repository"));
1327
1450
  process.exit(1);
1328
1451
  }
1329
- const provider = options.provider.toLowerCase();
1452
+ const provider = await resolveProvider(options.provider);
1330
1453
  const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1331
- const spinner = ora7("Analyzing...").start();
1454
+ const spinner = ora6("Analyzing...").start();
1332
1455
  try {
1333
1456
  let context;
1334
1457
  if (!target || options.staged) {
@@ -1339,7 +1462,7 @@ var explainCommand = new Command8("explain").description("Get an AI-powered expl
1339
1462
  }
1340
1463
  } else {
1341
1464
  const isPR = target.match(/^#?\d+$/) || target.includes("/pull/");
1342
- const isFile = existsSync3(target);
1465
+ const isFile = existsSync4(target);
1343
1466
  if (isPR) {
1344
1467
  spinner.stop();
1345
1468
  if (!requireGhCli()) {
@@ -1349,7 +1472,12 @@ var explainCommand = new Command8("explain").description("Get an AI-powered expl
1349
1472
  context = await getPRContext(target, spinner);
1350
1473
  } else if (isFile) {
1351
1474
  if (options.history) {
1352
- context = await getFileHistoryContext(target, git, spinner, parseInt(options.commits, 10));
1475
+ context = await getFileHistoryContext(
1476
+ target,
1477
+ git,
1478
+ spinner,
1479
+ parseInt(options.commits, 10)
1480
+ );
1353
1481
  } else {
1354
1482
  context = await getFileContentContext(target, spinner);
1355
1483
  }
@@ -1385,7 +1513,8 @@ async function getUncommittedContext(git, spinner) {
1385
1513
  spinner.text = "Analyzing uncommitted changes...";
1386
1514
  const stagedDiff = await git.diff(["--cached"]);
1387
1515
  const unstagedDiff = await git.diff();
1388
- const diff = (stagedDiff + "\n" + unstagedDiff).trim();
1516
+ const diff = `${stagedDiff}
1517
+ ${unstagedDiff}`.trim();
1389
1518
  if (!diff) {
1390
1519
  throw new Error("No uncommitted changes to analyze");
1391
1520
  }
@@ -1430,7 +1559,7 @@ async function getCommitContext(hash, git, spinner) {
1430
1559
  }
1431
1560
  async function getFileContentContext(filePath, spinner) {
1432
1561
  spinner.text = `Reading ${filePath}...`;
1433
- const content = readFileSync4(filePath, "utf-8");
1562
+ const content = readFileSync3(filePath, "utf-8");
1434
1563
  return {
1435
1564
  type: "file-content",
1436
1565
  title: filePath,
@@ -1481,18 +1610,20 @@ async function getPRContext(target, spinner) {
1481
1610
  spinner.text = `Fetching PR #${prNumber}...`;
1482
1611
  let prInfo;
1483
1612
  try {
1484
- const prJson = execSync4(
1613
+ const prJson = execSync5(
1485
1614
  `gh pr view ${prNumber} --json title,url,baseRefName,headRefName,commits`,
1486
1615
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
1487
1616
  );
1488
1617
  prInfo = JSON.parse(prJson);
1489
1618
  } catch {
1490
- throw new Error(`Failed to fetch PR #${prNumber}. Make sure gh CLI is installed and authenticated.`);
1619
+ throw new Error(
1620
+ `Failed to fetch PR #${prNumber}. Make sure gh CLI is installed and authenticated.`
1621
+ );
1491
1622
  }
1492
1623
  spinner.text = `Getting diff for PR #${prNumber}...`;
1493
1624
  let diff;
1494
1625
  try {
1495
- diff = execSync4(`gh pr diff ${prNumber}`, {
1626
+ diff = execSync5(`gh pr diff ${prNumber}`, {
1496
1627
  encoding: "utf-8",
1497
1628
  stdio: ["pipe", "pipe", "pipe"],
1498
1629
  maxBuffer: 10 * 1024 * 1024
@@ -1555,20 +1686,23 @@ ${icon} Explanation
1555
1686
  }
1556
1687
 
1557
1688
  // src/commands/find.ts
1558
- import { Command as Command9 } from "commander";
1559
1689
  import chalk10 from "chalk";
1560
- import ora8 from "ora";
1690
+ import { Command as Command9 } from "commander";
1691
+ import ora7 from "ora";
1561
1692
  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) => {
1693
+ var findCommand = new Command9("find").description("Find commits matching a vague description using AI").argument(
1694
+ "<query>",
1695
+ 'Description of the change you are looking for (e.g., "login feature added")'
1696
+ ).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
1697
  const git = simpleGit8();
1564
1698
  const isRepo = await git.checkIsRepo();
1565
1699
  if (!isRepo) {
1566
1700
  console.error(chalk10.red("Error: Not a git repository"));
1567
1701
  process.exit(1);
1568
1702
  }
1569
- const provider = options.provider.toLowerCase();
1703
+ const provider = await resolveProvider(options.provider);
1570
1704
  const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1571
- const spinner = ora8("Searching commits...").start();
1705
+ const spinner = ora7("Searching commits...").start();
1572
1706
  try {
1573
1707
  const logOptions = [`-n`, options.num];
1574
1708
  if (options.path) {
@@ -1649,417 +1783,932 @@ function printResults(results, query) {
1649
1783
  }
1650
1784
  }
1651
1785
 
1652
- // src/commands/branch.ts
1653
- import { Command as Command10 } from "commander";
1786
+ // src/commands/gitignore.ts
1787
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
1788
+ import { join as join4 } from "path";
1654
1789
  import chalk11 from "chalk";
1655
- import ora9 from "ora";
1790
+ import { Command as Command10 } from "commander";
1791
+ import ora8 from "ora";
1656
1792
  import { simpleGit as simpleGit9 } from "simple-git";
1657
- import { execSync as execSync5 } from "child_process";
1658
- function getIssueInfo(issueNumber) {
1793
+ var CONFIG_FILES = [
1794
+ // JavaScript/TypeScript
1795
+ "package.json",
1796
+ "tsconfig.json",
1797
+ "vite.config.ts",
1798
+ "vite.config.js",
1799
+ "next.config.js",
1800
+ "next.config.mjs",
1801
+ "nuxt.config.ts",
1802
+ "astro.config.mjs",
1803
+ // Python
1804
+ "pyproject.toml",
1805
+ "setup.py",
1806
+ "requirements.txt",
1807
+ "Pipfile",
1808
+ "poetry.lock",
1809
+ // Go
1810
+ "go.mod",
1811
+ "go.sum",
1812
+ // Rust
1813
+ "Cargo.toml",
1814
+ "Cargo.lock",
1815
+ // Java/Kotlin
1816
+ "pom.xml",
1817
+ "build.gradle",
1818
+ "build.gradle.kts",
1819
+ // Ruby
1820
+ "Gemfile",
1821
+ "Gemfile.lock",
1822
+ // PHP
1823
+ "composer.json",
1824
+ "composer.lock",
1825
+ // .NET
1826
+ "*.csproj",
1827
+ "*.fsproj",
1828
+ "*.sln",
1829
+ // Elixir
1830
+ "mix.exs",
1831
+ // Dart/Flutter
1832
+ "pubspec.yaml",
1833
+ // Swift
1834
+ "Package.swift"
1835
+ ];
1836
+ function getFiles(dir, maxDepth = 3, currentDepth = 0) {
1837
+ if (currentDepth >= maxDepth) return [];
1838
+ const files = [];
1659
1839
  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);
1840
+ const entries = readdirSync(dir, { withFileTypes: true });
1841
+ for (const entry of entries) {
1842
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "vendor" || entry.name === "target" || entry.name === "__pycache__" || entry.name === "venv" || entry.name === ".venv") {
1843
+ continue;
1844
+ }
1845
+ const fullPath = join4(dir, entry.name);
1846
+ if (entry.isDirectory()) {
1847
+ files.push(`${entry.name}/`);
1848
+ const subFiles = getFiles(fullPath, maxDepth, currentDepth + 1);
1849
+ files.push(...subFiles.map((f) => `${entry.name}/${f}`));
1850
+ } else {
1851
+ files.push(entry.name);
1852
+ }
1853
+ }
1665
1854
  } catch {
1666
- return null;
1667
1855
  }
1856
+ return files;
1668
1857
  }
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);
1858
+ function findConfigFiles(repoRoot) {
1859
+ const found = /* @__PURE__ */ new Map();
1860
+ for (const configFile of CONFIG_FILES) {
1861
+ if (configFile.includes("*")) {
1862
+ const ext = configFile.replace("*", "");
1863
+ try {
1864
+ const entries = readdirSync(repoRoot);
1865
+ for (const entry of entries) {
1866
+ if (entry.endsWith(ext)) {
1867
+ const content = readFileSync4(join4(repoRoot, entry), "utf-8");
1868
+ found.set(entry, content.slice(0, 2e3));
1869
+ }
1870
+ }
1871
+ } catch {
1872
+ }
1873
+ } else {
1874
+ const filePath = join4(repoRoot, configFile);
1875
+ if (existsSync5(filePath)) {
1876
+ try {
1877
+ const content = readFileSync4(filePath, "utf-8");
1878
+ found.set(configFile, content.slice(0, 2e3));
1879
+ } catch {
1880
+ }
1881
+ }
1693
1882
  }
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
1883
  }
1706
- const provider = options.provider.toLowerCase();
1707
- const template = findTemplate(repoRoot.trim(), "branch");
1884
+ return found;
1885
+ }
1886
+ 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) => {
1887
+ const git = simpleGit9();
1888
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1889
+ const root = repoRoot.trim();
1890
+ const provider = await resolveProvider(options.provider);
1891
+ const template = findTemplate(root, "gitignore");
1708
1892
  if (template) {
1709
1893
  console.log(chalk11.gray("Using template from project..."));
1710
1894
  }
1711
- const spinner = ora9("Generating branch name...").start();
1895
+ const spinner = ora8("Analyzing project structure...").start();
1896
+ const files = getFiles(root);
1897
+ const configFiles = findConfigFiles(root);
1898
+ const gitignorePath = join4(root, options.output);
1899
+ let existingGitignore;
1900
+ if (existsSync5(gitignorePath)) {
1901
+ existingGitignore = readFileSync4(gitignorePath, "utf-8");
1902
+ }
1903
+ let configFilesStr = "";
1904
+ if (configFiles.size > 0) {
1905
+ const entries = [];
1906
+ for (const [name, content] of configFiles) {
1907
+ entries.push(`### ${name}
1908
+ \`\`\`
1909
+ ${content}
1910
+ \`\`\``);
1911
+ }
1912
+ configFilesStr = entries.join("\n\n");
1913
+ }
1914
+ spinner.text = "Generating .gitignore...";
1712
1915
  try {
1713
- const branchName = await generateBranchName(
1714
- description,
1916
+ const gitignoreContent = await generateGitignore(
1917
+ {
1918
+ files: files.slice(0, 200).join("\n"),
1919
+ configFiles: configFilesStr,
1920
+ existingGitignore
1921
+ },
1715
1922
  { provider, model: options.model },
1716
- { type: options.type, issue: issueNumber },
1717
1923
  template || void 0
1718
1924
  );
1719
1925
  spinner.stop();
1720
- console.log(chalk11.bold("\nGenerated branch name:\n"));
1721
- console.log(chalk11.green(` ${branchName}`));
1926
+ if (options.stdout) {
1927
+ console.log(gitignoreContent);
1928
+ return;
1929
+ }
1930
+ console.log(chalk11.bold("\nGenerated .gitignore:\n"));
1931
+ console.log(chalk11.gray("\u2500".repeat(50)));
1932
+ console.log(gitignoreContent);
1933
+ console.log(chalk11.gray("\u2500".repeat(50)));
1722
1934
  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 {
1935
+ if (existsSync5(gitignorePath) && !options.yes) {
1727
1936
  const readline = await import("readline");
1728
1937
  const rl = readline.createInterface({
1729
1938
  input: process.stdin,
1730
1939
  output: process.stdout
1731
1940
  });
1732
1941
  const answer = await new Promise((resolve) => {
1733
- rl.question(chalk11.cyan("Create and checkout this branch? (y/N) "), resolve);
1942
+ rl.question(chalk11.cyan(`${options.output} already exists. Overwrite? (y/N) `), resolve);
1734
1943
  });
1735
1944
  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}`));
1945
+ if (answer.toLowerCase() !== "y") {
1946
+ console.log(chalk11.gray("Aborted."));
1947
+ return;
1742
1948
  }
1743
1949
  }
1950
+ writeFileSync2(gitignorePath, gitignoreContent);
1951
+ console.log(chalk11.green(`\u2713 Wrote ${options.output}`));
1744
1952
  } catch (error) {
1745
- spinner.fail("Failed to generate branch name");
1953
+ spinner.fail("Failed to generate .gitignore");
1746
1954
  console.error(chalk11.red(error instanceof Error ? error.message : "Unknown error"));
1747
1955
  process.exit(1);
1748
1956
  }
1749
1957
  });
1750
1958
 
1751
- // src/commands/checkout.ts
1752
- import { Command as Command11 } from "commander";
1959
+ // src/commands/init.ts
1960
+ import { execSync as execSync6 } from "child_process";
1961
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1962
+ import { homedir as homedir4 } from "os";
1963
+ import { dirname as dirname2, join as join5 } from "path";
1964
+ import { fileURLToPath as fileURLToPath2 } from "url";
1965
+ import { createAnthropic as createAnthropic2 } from "@ai-sdk/anthropic";
1966
+ import { createGoogleGenerativeAI as createGoogleGenerativeAI2 } from "@ai-sdk/google";
1967
+ import { createOpenAI as createOpenAI2 } from "@ai-sdk/openai";
1968
+ import { generateText as generateText2 } from "ai";
1753
1969
  import chalk12 from "chalk";
1754
- import ora10 from "ora";
1970
+ import { Command as Command11 } from "commander";
1971
+ import ora9 from "ora";
1755
1972
  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);
1973
+ function openFolder2(path2) {
1974
+ const platform = process.platform;
1975
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? 'start ""' : "xdg-open";
1976
+ execSync6(`${cmd} "${path2}"`);
1977
+ }
1978
+ var __filename2 = fileURLToPath2(import.meta.url);
1979
+ var __dirname2 = dirname2(__filename2);
1980
+ var GUT_ROOT2 = join5(__dirname2, "..");
1981
+ var TEMPLATE_FILES = [
1982
+ "branch.md",
1983
+ "changelog.md",
1984
+ "checkout.md",
1985
+ "commit.md",
1986
+ "explain.md",
1987
+ "explain-file.md",
1988
+ "find.md",
1989
+ "merge.md",
1990
+ "pr.md",
1991
+ "review.md",
1992
+ "stash.md",
1993
+ "summary.md"
1994
+ ];
1995
+ async function translateTemplate(content, targetLang, provider) {
1996
+ const apiKey = await getApiKey(provider);
1997
+ if (!apiKey) {
1998
+ throw new Error(`No API key found for ${provider}`);
1762
1999
  }
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;
2000
+ const modelName = getDefaultModel(provider);
2001
+ let model;
2002
+ switch (provider) {
2003
+ case "gemini": {
2004
+ const google = createGoogleGenerativeAI2({ apiKey });
2005
+ model = google(modelName);
2006
+ break;
2007
+ }
2008
+ case "openai": {
2009
+ const openai = createOpenAI2({ apiKey });
2010
+ model = openai(modelName);
2011
+ break;
2012
+ }
2013
+ case "anthropic": {
2014
+ const anthropic = createAnthropic2({ apiKey });
2015
+ model = anthropic(modelName);
2016
+ break;
2017
+ }
2018
+ default:
2019
+ throw new Error(`Unsupported provider for translation: ${provider}`);
1773
2020
  }
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);
2021
+ const langNames = {
2022
+ ja: "Japanese",
2023
+ en: "English",
2024
+ zh: "Chinese",
2025
+ ko: "Korean",
2026
+ es: "Spanish",
2027
+ fr: "French",
2028
+ de: "German"
2029
+ };
2030
+ const targetLangName = langNames[targetLang] || targetLang;
2031
+ const { text } = await generateText2({
2032
+ model,
2033
+ prompt: `Translate the following prompt template to ${targetLangName}.
2034
+ Keep all {{variable}} placeholders exactly as they are - do not translate them.
2035
+ Keep the markdown formatting intact.
2036
+ Only translate the instructional text.
2037
+
2038
+ Template to translate:
2039
+ ${content}
2040
+
2041
+ Translated template:`
2042
+ });
2043
+ return text.trim();
2044
+ }
2045
+ var initCommand = new Command11("init").description("Initialize .gut/ templates in your project or globally").option(
2046
+ "-p, --provider <provider>",
2047
+ "AI provider for translation (gemini, openai, anthropic, ollama)"
2048
+ ).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) => {
2049
+ const isGlobal = options.global === true;
2050
+ const git = simpleGit10();
2051
+ let targetDir;
2052
+ if (isGlobal) {
2053
+ targetDir = join5(homedir4(), ".config", "gut", "templates");
2054
+ } else {
2055
+ const isRepo = await git.checkIsRepo();
2056
+ if (!isRepo) {
2057
+ console.error(chalk12.red("Error: Not a git repository"));
2058
+ console.error(chalk12.gray("Use --global to initialize templates globally"));
2059
+ process.exit(1);
2060
+ }
2061
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2062
+ targetDir = join5(repoRoot.trim(), ".gut");
1779
2063
  }
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")}`;
2064
+ if (!existsSync6(targetDir)) {
2065
+ mkdirSync3(targetDir, { recursive: true });
2066
+ console.log(chalk12.green(`Created ${targetDir}`));
1784
2067
  }
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..."));
2068
+ console.log(
2069
+ chalk12.blue(
2070
+ isGlobal ? "Initializing global templates...\n" : "Initializing project templates...\n"
2071
+ )
2072
+ );
2073
+ const sourceDir = join5(GUT_ROOT2, ".gut");
2074
+ const lang = getLanguage();
2075
+ const langSourceDir = join5(GUT_ROOT2, ".gut", lang);
2076
+ const hasPreTranslated = lang !== "en" && existsSync6(langSourceDir);
2077
+ const needsTranslation = options.translate !== false && lang !== "en" && !hasPreTranslated;
2078
+ const provider = needsTranslation ? await resolveProvider(options.provider) : null;
2079
+ if (hasPreTranslated) {
2080
+ console.log(chalk12.gray(`Language: ${lang} - using pre-translated templates
2081
+ `));
2082
+ } else if (needsTranslation) {
2083
+ console.log(chalk12.gray(`Language: ${lang} - templates will be translated
2084
+ `));
1790
2085
  }
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}`));
2086
+ const spinner = ora9();
2087
+ let copied = 0;
2088
+ let skipped = 0;
2089
+ for (const filename of TEMPLATE_FILES) {
2090
+ const langSourcePath = join5(langSourceDir, filename);
2091
+ const defaultSourcePath = join5(sourceDir, filename);
2092
+ const sourcePath = hasPreTranslated && existsSync6(langSourcePath) ? langSourcePath : defaultSourcePath;
2093
+ const targetPath = join5(targetDir, filename);
2094
+ if (!existsSync6(sourcePath)) {
2095
+ continue;
2096
+ }
2097
+ if (existsSync6(targetPath) && !options.force) {
2098
+ console.log(chalk12.gray(` Skipped: ${filename} (already exists)`));
2099
+ skipped++;
2100
+ continue;
2101
+ }
2102
+ let content = readFileSync5(sourcePath, "utf-8");
2103
+ if (needsTranslation && sourcePath === defaultSourcePath && provider) {
2104
+ spinner.start(`Translating ${filename}...`);
2105
+ try {
2106
+ content = await translateTemplate(content, lang, provider);
2107
+ spinner.succeed(`Translated: ${filename}`);
2108
+ } catch (err) {
2109
+ spinner.fail(`Failed to translate ${filename}`);
2110
+ console.error(chalk12.red(` ${err instanceof Error ? err.message : "Unknown error"}`));
2111
+ console.log(chalk12.gray(` Using original English template`));
1820
2112
  }
2113
+ } else {
2114
+ console.log(chalk12.green(` Copied: ${filename}`));
2115
+ }
2116
+ writeFileSync3(targetPath, content);
2117
+ copied++;
2118
+ }
2119
+ console.log();
2120
+ if (copied > 0) {
2121
+ const location = isGlobal ? "~/.config/gut/templates/" : ".gut/";
2122
+ console.log(chalk12.green(`\u2713 ${copied} template(s) initialized in ${location}`));
2123
+ }
2124
+ if (skipped > 0) {
2125
+ console.log(chalk12.gray(` ${skipped} template(s) skipped (use --force to overwrite)`));
2126
+ }
2127
+ if (isGlobal) {
2128
+ console.log(chalk12.gray("\nGlobal templates will be used as fallback for all projects."));
2129
+ console.log(
2130
+ chalk12.gray("Project-level templates (.gut/) take priority over global templates.")
2131
+ );
2132
+ } else {
2133
+ console.log(chalk12.gray("\nYou can now customize these templates for your project."));
2134
+ }
2135
+ if (options.open) {
2136
+ try {
2137
+ openFolder2(targetDir);
2138
+ console.log(chalk12.green(`
2139
+ Opened: ${targetDir}`));
2140
+ } catch {
2141
+ console.error(chalk12.red(`
2142
+ Failed to open folder: ${targetDir}`));
1821
2143
  }
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
2144
  }
1827
2145
  });
1828
2146
 
1829
- // src/commands/sync.ts
1830
- import { Command as Command12 } from "commander";
2147
+ // src/commands/lang.ts
1831
2148
  import chalk13 from "chalk";
1832
- import ora11 from "ora";
2149
+ import { Command as Command12 } from "commander";
2150
+ 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) => {
2151
+ if (!language) {
2152
+ const lang = getLanguage();
2153
+ const localConfig = getLocalConfig();
2154
+ const isLocal = "lang" in localConfig;
2155
+ const scope = isLocal ? chalk13.cyan("(local)") : chalk13.gray("(global)");
2156
+ console.log(`${lang} ${scope}`);
2157
+ return;
2158
+ }
2159
+ if (!isValidLanguage(language)) {
2160
+ console.error(chalk13.red(`Invalid language: ${language}`));
2161
+ console.error(chalk13.gray(`Valid languages: ${VALID_LANGUAGES.join(", ")}`));
2162
+ process.exit(1);
2163
+ }
2164
+ try {
2165
+ setLanguage(language, options.local ?? false);
2166
+ const scope = options.local ? "(local)" : "(global)";
2167
+ console.log(chalk13.green(`\u2713 Language set to: ${language} ${scope}`));
2168
+ } catch (err) {
2169
+ console.error(chalk13.red(err.message));
2170
+ process.exit(1);
2171
+ }
2172
+ });
2173
+
2174
+ // src/commands/merge.ts
2175
+ import * as fs from "fs";
2176
+ import * as path from "path";
2177
+ import chalk14 from "chalk";
2178
+ import { Command as Command13 } from "commander";
2179
+ import ora10 from "ora";
1833
2180
  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) => {
2181
+ 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) => {
1835
2182
  const git = simpleGit11();
1836
2183
  const isRepo = await git.checkIsRepo();
1837
2184
  if (!isRepo) {
1838
- console.error(chalk13.red("Error: Not a git repository"));
2185
+ console.error(chalk14.red("Error: Not a git repository"));
1839
2186
  process.exit(1);
1840
2187
  }
1841
- const spinner = ora11("Checking repository status...").start();
2188
+ const provider = await resolveProvider(options.provider);
2189
+ const status = await git.status();
2190
+ if (status.modified.length > 0 || status.staged.length > 0) {
2191
+ console.error(chalk14.red("Error: Working directory has uncommitted changes"));
2192
+ console.log(chalk14.gray("Please commit or stash your changes first"));
2193
+ process.exit(1);
2194
+ }
2195
+ const branchInfo = await git.branch();
2196
+ const currentBranch = branchInfo.current;
2197
+ console.log(
2198
+ chalk14.bold(`
2199
+ Merging ${chalk14.cyan(branch)} into ${chalk14.cyan(currentBranch)}...
2200
+ `)
2201
+ );
1842
2202
  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;
2203
+ await git.merge([branch]);
2204
+ console.log(chalk14.green("\u2713 Merged successfully (no conflicts)"));
2205
+ return;
2206
+ } catch {
2207
+ }
2208
+ const conflictStatus = await git.status();
2209
+ const conflictedFiles = conflictStatus.conflicted;
2210
+ if (conflictedFiles.length === 0) {
2211
+ console.error(chalk14.red("Merge failed for unknown reason"));
2212
+ await git.merge(["--abort"]);
2213
+ process.exit(1);
2214
+ }
2215
+ console.log(chalk14.yellow(`\u26A0 ${conflictedFiles.length} conflict(s) detected
2216
+ `));
2217
+ const spinner = ora10();
2218
+ const rootDir = await git.revparse(["--show-toplevel"]);
2219
+ const template = findTemplate(rootDir.trim(), "merge");
2220
+ if (template) {
2221
+ console.log(chalk14.gray("Using merge template from project...\n"));
2222
+ }
2223
+ for (const file of conflictedFiles) {
2224
+ const filePath = path.join(rootDir.trim(), file);
2225
+ const content = fs.readFileSync(filePath, "utf-8");
2226
+ console.log(chalk14.bold(`
2227
+ \u{1F4C4} ${file}`));
2228
+ const conflictMatch = content.match(/<<<<<<< HEAD[\s\S]*?>>>>>>>.+/g);
2229
+ if (conflictMatch) {
2230
+ console.log(chalk14.gray("\u2500".repeat(50)));
2231
+ console.log(chalk14.gray(conflictMatch[0].slice(0, 500)));
2232
+ if (conflictMatch[0].length > 500) console.log(chalk14.gray("..."));
2233
+ console.log(chalk14.gray("\u2500".repeat(50)));
1881
2234
  }
1882
- const strategy = options.merge ? "merge" : "rebase";
1883
- spinner.text = `Syncing with ${trackingBranch} (${strategy})...`;
2235
+ spinner.start("AI is analyzing conflict...");
1884
2236
  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
- }
2237
+ const resolution = await resolveConflict(
2238
+ content,
2239
+ {
2240
+ filename: file,
2241
+ oursRef: currentBranch,
2242
+ theirsRef: branch
2243
+ },
2244
+ { provider, model: options.model },
2245
+ template || void 0
2246
+ );
2247
+ spinner.stop();
2248
+ console.log(chalk14.cyan("\n\u{1F916} AI suggests:"));
2249
+ console.log(chalk14.gray("\u2500".repeat(50)));
2250
+ const preview = resolution.resolvedContent.slice(0, 800);
2251
+ console.log(preview);
2252
+ if (resolution.resolvedContent.length > 800) console.log(chalk14.gray("..."));
2253
+ console.log(chalk14.gray("\u2500".repeat(50)));
2254
+ console.log(chalk14.gray(`Strategy: ${resolution.strategy}`));
2255
+ console.log(chalk14.gray(`Reason: ${resolution.explanation}`));
2256
+ const readline = await import("readline");
2257
+ const rl = readline.createInterface({
2258
+ input: process.stdin,
2259
+ output: process.stdout
2260
+ });
2261
+ const answer = await new Promise((resolve) => {
2262
+ rl.question(chalk14.cyan("\nAccept this resolution? (y/n/s to skip) "), resolve);
2263
+ });
2264
+ rl.close();
2265
+ if (answer.toLowerCase() === "y") {
2266
+ fs.writeFileSync(filePath, resolution.resolvedContent);
2267
+ await git.add(file);
2268
+ console.log(chalk14.green(`\u2713 Resolved ${file}`));
2269
+ } else if (answer.toLowerCase() === "s") {
2270
+ console.log(chalk14.yellow(`\u23ED Skipped ${file}`));
1920
2271
  } 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"));
2272
+ console.log(chalk14.yellow(`\u2717 Rejected - resolve manually: ${file}`));
1932
2273
  }
2274
+ } catch (err) {
2275
+ spinner.fail("AI resolution failed");
2276
+ console.error(chalk14.red(err instanceof Error ? err.message : "Unknown error"));
2277
+ console.log(chalk14.yellow(`Please resolve manually: ${file}`));
1933
2278
  }
1934
- } catch (error) {
1935
- spinner.fail("Sync failed");
1936
- console.error(chalk13.red(error instanceof Error ? error.message : "Unknown error"));
1937
- process.exit(1);
2279
+ }
2280
+ const finalStatus = await git.status();
2281
+ if (finalStatus.conflicted.length > 0) {
2282
+ console.log(chalk14.yellow(`
2283
+ \u26A0 ${finalStatus.conflicted.length} conflict(s) remaining`));
2284
+ console.log(chalk14.gray("Resolve manually and run: git add <files> && git commit"));
2285
+ } else if (options.commit !== false) {
2286
+ await git.commit(`Merge branch '${branch}' into ${currentBranch}`);
2287
+ console.log(chalk14.green("\n\u2713 All conflicts resolved and committed"));
2288
+ } else {
2289
+ console.log(chalk14.green("\n\u2713 All conflicts resolved"));
2290
+ console.log(chalk14.gray("Run: git commit"));
1938
2291
  }
1939
2292
  });
1940
2293
 
1941
- // src/commands/stash.ts
1942
- import { Command as Command13 } from "commander";
1943
- import chalk14 from "chalk";
1944
- import ora12 from "ora";
2294
+ // src/commands/pr.ts
2295
+ import { execSync as execSync7 } from "child_process";
2296
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
2297
+ import { join as join7 } from "path";
2298
+ import chalk15 from "chalk";
2299
+ import { Command as Command14 } from "commander";
2300
+ import ora11 from "ora";
1945
2301
  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) => {
2302
+ var GITHUB_PR_TEMPLATE_PATHS = [
2303
+ ".github/pull_request_template.md",
2304
+ ".github/PULL_REQUEST_TEMPLATE.md",
2305
+ "pull_request_template.md",
2306
+ "PULL_REQUEST_TEMPLATE.md",
2307
+ "docs/pull_request_template.md"
2308
+ ];
2309
+ function findPRTemplate(repoRoot) {
2310
+ for (const templatePath of GITHUB_PR_TEMPLATE_PATHS) {
2311
+ const fullPath = join7(repoRoot, templatePath);
2312
+ if (existsSync7(fullPath)) {
2313
+ return readFileSync7(fullPath, "utf-8");
2314
+ }
2315
+ }
2316
+ return findTemplate(repoRoot, "pr");
2317
+ }
2318
+ 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) => {
1947
2319
  const git = simpleGit12();
1948
2320
  const isRepo = await git.checkIsRepo();
1949
2321
  if (!isRepo) {
1950
- console.error(chalk14.red("Error: Not a git repository"));
2322
+ console.error(chalk15.red("Error: Not a git repository"));
1951
2323
  process.exit(1);
1952
2324
  }
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;
2325
+ const provider = await resolveProvider(options.provider);
2326
+ const spinner = ora11("Analyzing branch...").start();
2327
+ try {
2328
+ const branchInfo = await git.branch();
2329
+ const currentBranch = branchInfo.current;
2330
+ let baseBranch = options.base;
2331
+ if (!baseBranch) {
2332
+ if (branchInfo.all.includes("main")) {
2333
+ baseBranch = "main";
2334
+ } else if (branchInfo.all.includes("master")) {
2335
+ baseBranch = "master";
2336
+ } else {
2337
+ baseBranch = "main";
2338
+ }
1958
2339
  }
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;
1965
- }
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"}`));
2340
+ if (currentBranch === baseBranch) {
2341
+ spinner.fail(`Already on ${baseBranch} branch`);
1973
2342
  process.exit(1);
1974
2343
  }
1975
- return;
1976
- }
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"}`));
2344
+ spinner.text = `Comparing ${currentBranch} to ${baseBranch}...`;
2345
+ const log = await git.log({ from: baseBranch, to: currentBranch });
2346
+ const commits = log.all.map((c) => c.message.split("\n")[0]);
2347
+ if (commits.length === 0) {
2348
+ spinner.fail("No commits found between branches");
1984
2349
  process.exit(1);
1985
2350
  }
1986
- return;
1987
- }
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);
2351
+ const diff = await git.diff([`${baseBranch}...${currentBranch}`]);
2352
+ const repoRoot = await git.revparse(["--show-toplevel"]);
2353
+ const template = findPRTemplate(repoRoot.trim());
2354
+ if (template) {
2355
+ spinner.text = "Found PR template, generating description...";
2356
+ } else {
2357
+ spinner.text = "Generating PR description...";
1996
2358
  }
1997
- return;
1998
- }
1999
- if (options.clear) {
2000
- const readline = await import("readline");
2001
- const rl = readline.createInterface({
2002
- input: process.stdin,
2359
+ const { title, body } = await generatePRDescription(
2360
+ {
2361
+ baseBranch,
2362
+ currentBranch,
2363
+ commits,
2364
+ diff
2365
+ },
2366
+ { provider, model: options.model },
2367
+ template || void 0
2368
+ );
2369
+ spinner.stop();
2370
+ console.log(chalk15.bold("\n\u{1F4DD} Generated PR:\n"));
2371
+ console.log(chalk15.cyan("Title:"), chalk15.white(title));
2372
+ console.log(chalk15.cyan("\nDescription:"));
2373
+ console.log(chalk15.gray("\u2500".repeat(50)));
2374
+ console.log(body);
2375
+ console.log(chalk15.gray("\u2500".repeat(50)));
2376
+ if (options.copy) {
2377
+ try {
2378
+ const fullText = `${title}
2379
+
2380
+ ${body}`;
2381
+ execSync7("pbcopy", { input: fullText });
2382
+ console.log(chalk15.green("\n\u2713 Copied to clipboard"));
2383
+ } catch {
2384
+ console.log(chalk15.yellow("\n\u26A0 Could not copy to clipboard"));
2385
+ }
2386
+ }
2387
+ const ghInstalled = isGhCliInstalled();
2388
+ if (!ghInstalled) {
2389
+ console.log(chalk15.yellow("\n\u26A0 GitHub CLI (gh) is not installed"));
2390
+ console.log(chalk15.gray(" To create PRs directly from gut, install gh CLI:"));
2391
+ console.log(chalk15.gray(" brew install gh (macOS)"));
2392
+ console.log(chalk15.gray(" https://cli.github.com/"));
2393
+ if (!options.copy) {
2394
+ console.log(chalk15.gray("\nTip: Use --copy to copy to clipboard"));
2395
+ }
2396
+ } else {
2397
+ const readline = await import("readline");
2398
+ const rl = readline.createInterface({
2399
+ input: process.stdin,
2400
+ output: process.stdout
2401
+ });
2402
+ const promptMessage = options.create ? chalk15.cyan("\nCreate PR with this description? (y/N) ") : chalk15.cyan("\nCreate PR with gh CLI? (y/N) ");
2403
+ const answer = await new Promise((resolve) => {
2404
+ rl.question(promptMessage, resolve);
2405
+ });
2406
+ rl.close();
2407
+ if (answer.toLowerCase() === "y") {
2408
+ const createSpinner = ora11("Creating PR...").start();
2409
+ try {
2410
+ const escapedTitle = title.replace(/"/g, '\\"');
2411
+ const escapedBody = body.replace(/"/g, '\\"');
2412
+ execSync7(
2413
+ `gh pr create --title "${escapedTitle}" --body "${escapedBody}" --base ${baseBranch}`,
2414
+ { stdio: "pipe" }
2415
+ );
2416
+ createSpinner.succeed("PR created successfully!");
2417
+ } catch {
2418
+ createSpinner.fail("Failed to create PR");
2419
+ console.error(chalk15.gray("Make sure gh CLI is authenticated: gh auth login"));
2420
+ }
2421
+ } else if (!options.copy) {
2422
+ console.log(chalk15.gray("\nTip: Use --copy to copy to clipboard"));
2423
+ }
2424
+ }
2425
+ } catch (err) {
2426
+ spinner.fail("Failed to generate PR description");
2427
+ console.error(chalk15.red(err instanceof Error ? err.message : "Unknown error"));
2428
+ process.exit(1);
2429
+ }
2430
+ });
2431
+
2432
+ // src/commands/review.ts
2433
+ import { execSync as execSync8 } from "child_process";
2434
+ import chalk16 from "chalk";
2435
+ import { Command as Command15 } from "commander";
2436
+ import ora12 from "ora";
2437
+ import { simpleGit as simpleGit13 } from "simple-git";
2438
+ async function getPRDiff(prNumber) {
2439
+ try {
2440
+ const diff = execSync8(`gh pr diff ${prNumber}`, {
2441
+ encoding: "utf-8",
2442
+ maxBuffer: 10 * 1024 * 1024
2443
+ });
2444
+ const prJsonStr = execSync8(`gh pr view ${prNumber} --json number,title,author,url`, {
2445
+ encoding: "utf-8"
2446
+ });
2447
+ const prJson = JSON.parse(prJsonStr);
2448
+ return {
2449
+ diff,
2450
+ prInfo: {
2451
+ number: prJson.number,
2452
+ title: prJson.title,
2453
+ author: prJson.author.login,
2454
+ url: prJson.url
2455
+ }
2456
+ };
2457
+ } catch (err) {
2458
+ if (err instanceof Error && err.message.includes("gh: command not found")) {
2459
+ throw new Error("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/", {
2460
+ cause: err
2461
+ });
2462
+ }
2463
+ throw err;
2464
+ }
2465
+ }
2466
+ 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) => {
2467
+ const git = simpleGit13();
2468
+ const isRepo = await git.checkIsRepo();
2469
+ if (!isRepo) {
2470
+ console.error(chalk16.red("Error: Not a git repository"));
2471
+ process.exit(1);
2472
+ }
2473
+ const provider = await resolveProvider(options.provider);
2474
+ const spinner = ora12("Getting diff...").start();
2475
+ try {
2476
+ let diff;
2477
+ let prInfo = null;
2478
+ if (prNumber) {
2479
+ spinner.stop();
2480
+ if (!requireGhCli()) {
2481
+ process.exit(1);
2482
+ }
2483
+ spinner.start(`Fetching PR #${prNumber}...`);
2484
+ const result = await getPRDiff(prNumber);
2485
+ diff = result.diff;
2486
+ prInfo = result.prInfo;
2487
+ spinner.text = `Reviewing PR #${prNumber}...`;
2488
+ } else if (options.commit) {
2489
+ diff = await git.diff([`${options.commit}^`, options.commit]);
2490
+ spinner.text = `Reviewing commit ${options.commit.slice(0, 7)}...`;
2491
+ } else if (options.staged) {
2492
+ diff = await git.diff(["--cached"]);
2493
+ spinner.text = "Reviewing staged changes...";
2494
+ } else {
2495
+ diff = await git.diff();
2496
+ const stagedDiff = await git.diff(["--cached"]);
2497
+ diff = `${stagedDiff}
2498
+ ${diff}`;
2499
+ spinner.text = "Reviewing uncommitted changes...";
2500
+ }
2501
+ if (!diff.trim()) {
2502
+ spinner.info("No changes to review");
2503
+ process.exit(0);
2504
+ }
2505
+ spinner.text = "AI is reviewing your code...";
2506
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2507
+ const template = findTemplate(repoRoot.trim(), "review");
2508
+ const review = await generateCodeReview(
2509
+ diff,
2510
+ { provider, model: options.model },
2511
+ template || void 0
2512
+ );
2513
+ spinner.stop();
2514
+ if (options.json) {
2515
+ console.log(JSON.stringify({ prInfo, review }, null, 2));
2516
+ return;
2517
+ }
2518
+ if (prInfo) {
2519
+ console.log(chalk16.bold(`
2520
+ \u{1F517} PR #${prInfo.number}: ${prInfo.title}`));
2521
+ console.log(chalk16.gray(` by ${prInfo.author} - ${prInfo.url}`));
2522
+ }
2523
+ printReview(review);
2524
+ } catch (error) {
2525
+ spinner.fail("Failed to generate review");
2526
+ console.error(chalk16.red(error instanceof Error ? error.message : "Unknown error"));
2527
+ process.exit(1);
2528
+ }
2529
+ });
2530
+ function printReview(review) {
2531
+ console.log(chalk16.bold("\n\u{1F50D} AI Code Review\n"));
2532
+ console.log(chalk16.cyan("Summary:"));
2533
+ console.log(` ${review.summary}
2534
+ `);
2535
+ if (review.issues.length > 0) {
2536
+ console.log(chalk16.cyan("Issues Found:"));
2537
+ for (const issue of review.issues) {
2538
+ const severityColors = {
2539
+ critical: chalk16.red,
2540
+ warning: chalk16.yellow,
2541
+ suggestion: chalk16.blue
2542
+ };
2543
+ const severityIcons = {
2544
+ critical: "\u{1F534}",
2545
+ warning: "\u{1F7E1}",
2546
+ suggestion: "\u{1F4A1}"
2547
+ };
2548
+ const color = severityColors[issue.severity];
2549
+ const icon = severityIcons[issue.severity];
2550
+ console.log(`
2551
+ ${icon} ${color(issue.severity.toUpperCase())}`);
2552
+ console.log(` ${chalk16.gray("File:")} ${issue.file}${issue.line ? `:${issue.line}` : ""}`);
2553
+ console.log(` ${issue.message}`);
2554
+ if (issue.suggestion) {
2555
+ console.log(` ${chalk16.green("\u2192")} ${issue.suggestion}`);
2556
+ }
2557
+ }
2558
+ } else {
2559
+ console.log(chalk16.green(" \u2713 No issues found!\n"));
2560
+ }
2561
+ if (review.positives.length > 0) {
2562
+ console.log(chalk16.cyan("\nGood Practices:"));
2563
+ for (const positive of review.positives) {
2564
+ console.log(` ${chalk16.green("\u2713")} ${positive}`);
2565
+ }
2566
+ }
2567
+ const criticalCount = review.issues.filter((i) => i.severity === "critical").length;
2568
+ const warningCount = review.issues.filter((i) => i.severity === "warning").length;
2569
+ const suggestionCount = review.issues.filter((i) => i.severity === "suggestion").length;
2570
+ console.log(chalk16.gray("\n\u2500".repeat(40)));
2571
+ console.log(
2572
+ ` ${chalk16.red(criticalCount)} critical ${chalk16.yellow(warningCount)} warnings ${chalk16.blue(suggestionCount)} suggestions`
2573
+ );
2574
+ console.log();
2575
+ }
2576
+
2577
+ // src/commands/stash.ts
2578
+ import chalk17 from "chalk";
2579
+ import { Command as Command16 } from "commander";
2580
+ import ora13 from "ora";
2581
+ import { simpleGit as simpleGit14 } from "simple-git";
2582
+ 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) => {
2583
+ const git = simpleGit14();
2584
+ const isRepo = await git.checkIsRepo();
2585
+ if (!isRepo) {
2586
+ console.error(chalk17.red("Error: Not a git repository"));
2587
+ process.exit(1);
2588
+ }
2589
+ if (options.list) {
2590
+ const stashList = await git.stashList();
2591
+ if (stashList.all.length === 0) {
2592
+ console.log(chalk17.gray("No stashes found"));
2593
+ return;
2594
+ }
2595
+ console.log(chalk17.bold("\nStashes:\n"));
2596
+ stashList.all.forEach((stash, index) => {
2597
+ console.log(` ${chalk17.cyan(index.toString())} ${stash.message}`);
2598
+ });
2599
+ console.log();
2600
+ return;
2601
+ }
2602
+ if (options.apply !== void 0) {
2603
+ const index = typeof options.apply === "string" ? options.apply : "0";
2604
+ try {
2605
+ await git.stash(["apply", `stash@{${index}}`]);
2606
+ console.log(chalk17.green(`\u2713 Applied stash@{${index}}`));
2607
+ } catch (err) {
2608
+ console.error(
2609
+ chalk17.red(
2610
+ `Failed to apply stash: ${err instanceof Error ? err.message : "Unknown error"}`
2611
+ )
2612
+ );
2613
+ process.exit(1);
2614
+ }
2615
+ return;
2616
+ }
2617
+ if (options.pop !== void 0) {
2618
+ const index = typeof options.pop === "string" ? options.pop : "0";
2619
+ try {
2620
+ await git.stash(["pop", `stash@{${index}}`]);
2621
+ console.log(chalk17.green(`\u2713 Popped stash@{${index}}`));
2622
+ } catch (err) {
2623
+ console.error(
2624
+ chalk17.red(`Failed to pop stash: ${err instanceof Error ? err.message : "Unknown error"}`)
2625
+ );
2626
+ process.exit(1);
2627
+ }
2628
+ return;
2629
+ }
2630
+ if (options.drop !== void 0) {
2631
+ const index = typeof options.drop === "string" ? options.drop : "0";
2632
+ try {
2633
+ await git.stash(["drop", `stash@{${index}}`]);
2634
+ console.log(chalk17.green(`\u2713 Dropped stash@{${index}}`));
2635
+ } catch (err) {
2636
+ console.error(
2637
+ chalk17.red(`Failed to drop stash: ${err instanceof Error ? err.message : "Unknown error"}`)
2638
+ );
2639
+ process.exit(1);
2640
+ }
2641
+ return;
2642
+ }
2643
+ if (options.clear) {
2644
+ const readline = await import("readline");
2645
+ const rl = readline.createInterface({
2646
+ input: process.stdin,
2003
2647
  output: process.stdout
2004
2648
  });
2005
2649
  const answer = await new Promise((resolve) => {
2006
- rl.question(chalk14.yellow("Clear all stashes? This cannot be undone. (y/N) "), resolve);
2650
+ rl.question(chalk17.yellow("Clear all stashes? This cannot be undone. (y/N) "), resolve);
2007
2651
  });
2008
2652
  rl.close();
2009
2653
  if (answer.toLowerCase() === "y") {
2010
2654
  await git.stash(["clear"]);
2011
- console.log(chalk14.green("\u2713 Cleared all stashes"));
2655
+ console.log(chalk17.green("\u2713 Cleared all stashes"));
2012
2656
  } else {
2013
- console.log(chalk14.gray("Cancelled"));
2657
+ console.log(chalk17.gray("Cancelled"));
2014
2658
  }
2015
2659
  return;
2016
2660
  }
2017
2661
  const status = await git.status();
2018
2662
  if (status.isClean()) {
2019
- console.log(chalk14.yellow("No changes to stash"));
2663
+ console.log(chalk17.yellow("No changes to stash"));
2020
2664
  return;
2021
2665
  }
2022
2666
  let stashName;
2023
2667
  if (name) {
2024
2668
  stashName = name;
2025
2669
  } else {
2026
- const provider = options.provider.toLowerCase();
2670
+ const provider = await resolveProvider(options.provider);
2027
2671
  const diff = await git.diff();
2028
2672
  const stagedDiff = await git.diff(["--cached"]);
2029
- const fullDiff = diff + "\n" + stagedDiff;
2673
+ const fullDiff = `${diff}
2674
+ ${stagedDiff}`;
2030
2675
  if (!fullDiff.trim()) {
2031
2676
  stashName = `WIP: untracked files (${status.not_added.length} files)`;
2032
2677
  } else {
2033
- const spinner = ora12("Generating stash name...").start();
2678
+ const spinner = ora13("Generating stash name...").start();
2034
2679
  try {
2035
2680
  const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2036
2681
  const template = findTemplate(repoRoot.trim(), "stash");
2037
- stashName = await generateStashName(fullDiff, { provider, model: options.model }, template || void 0);
2682
+ stashName = await generateStashName(
2683
+ fullDiff,
2684
+ { provider, model: options.model },
2685
+ template || void 0
2686
+ );
2038
2687
  spinner.stop();
2039
- } catch (error) {
2688
+ } catch {
2040
2689
  spinner.fail("Failed to generate name, using default");
2041
2690
  stashName = `WIP: ${status.modified.length} modified, ${status.not_added.length} untracked`;
2042
2691
  }
2043
2692
  }
2044
2693
  }
2045
2694
  await git.stash(["push", "-u", "-m", stashName]);
2046
- console.log(chalk14.green(`\u2713 Stashed: ${stashName}`));
2695
+ console.log(chalk17.green(`\u2713 Stashed: ${stashName}`));
2047
2696
  });
2048
2697
 
2049
2698
  // src/commands/summary.ts
2050
- import { Command as Command14 } from "commander";
2051
- 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();
2699
+ import chalk18 from "chalk";
2700
+ import { Command as Command17 } from "commander";
2701
+ import ora14 from "ora";
2702
+ import { simpleGit as simpleGit15 } from "simple-git";
2703
+ 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) => {
2704
+ const git = simpleGit15();
2056
2705
  const isRepo = await git.checkIsRepo();
2057
2706
  if (!isRepo) {
2058
- console.error(chalk15.red("Error: Not a git repository"));
2707
+ console.error(chalk18.red("Error: Not a git repository"));
2059
2708
  process.exit(1);
2060
2709
  }
2061
- const provider = options.provider.toLowerCase();
2062
- const spinner = ora13("Generating summary...").start();
2710
+ const provider = await resolveProvider(options.provider);
2711
+ const spinner = ora14("Generating summary...").start();
2063
2712
  try {
2064
2713
  let author = options.author;
2065
2714
  if (!author) {
@@ -2126,10 +2775,10 @@ var summaryCommand = new Command14("summary").description("Generate a work summa
2126
2775
  const { execSync: execSync9 } = await import("child_process");
2127
2776
  try {
2128
2777
  execSync9("pbcopy", { input: textToCopy });
2129
- console.log(chalk15.green("Summary copied to clipboard!"));
2778
+ console.log(chalk18.green("Summary copied to clipboard!"));
2130
2779
  console.log();
2131
2780
  } catch {
2132
- console.log(chalk15.yellow("Could not copy to clipboard"));
2781
+ console.log(chalk18.yellow("Could not copy to clipboard"));
2133
2782
  }
2134
2783
  }
2135
2784
  if (options.markdown) {
@@ -2139,7 +2788,7 @@ var summaryCommand = new Command14("summary").description("Generate a work summa
2139
2788
  }
2140
2789
  } catch (error) {
2141
2790
  spinner.fail("Failed to generate summary");
2142
- console.error(chalk15.red(error instanceof Error ? error.message : "Unknown error"));
2791
+ console.error(chalk18.red(error instanceof Error ? error.message : "Unknown error"));
2143
2792
  process.exit(1);
2144
2793
  }
2145
2794
  });
@@ -2207,29 +2856,29 @@ function formatDate(d) {
2207
2856
  }
2208
2857
  function printSummary(summary, author, since, until) {
2209
2858
  const period = until ? `${since} - ${until}` : `${since} - now`;
2210
- console.log(chalk15.bold(`
2859
+ console.log(chalk18.bold(`
2211
2860
  \u{1F4CA} ${summary.title}
2212
2861
  `));
2213
- console.log(chalk15.gray(`Author: ${author}`));
2214
- console.log(chalk15.gray(`Period: ${period}`));
2862
+ console.log(chalk18.gray(`Author: ${author}`));
2863
+ console.log(chalk18.gray(`Period: ${period}`));
2215
2864
  if (summary.stats) {
2216
- console.log(chalk15.gray(`Commits: ${summary.stats.commits}`));
2865
+ console.log(chalk18.gray(`Commits: ${summary.stats.commits}`));
2217
2866
  }
2218
2867
  console.log();
2219
- console.log(chalk15.cyan("Overview:"));
2868
+ console.log(chalk18.cyan("Overview:"));
2220
2869
  console.log(` ${summary.overview}`);
2221
2870
  console.log();
2222
2871
  if (summary.highlights.length > 0) {
2223
- console.log(chalk15.cyan("Highlights:"));
2872
+ console.log(chalk18.cyan("Highlights:"));
2224
2873
  for (const highlight of summary.highlights) {
2225
- console.log(` ${chalk15.green("\u2605")} ${highlight}`);
2874
+ console.log(` ${chalk18.green("\u2605")} ${highlight}`);
2226
2875
  }
2227
2876
  console.log();
2228
2877
  }
2229
2878
  if (summary.details.length > 0) {
2230
- console.log(chalk15.cyan("Details:"));
2879
+ console.log(chalk18.cyan("Details:"));
2231
2880
  for (const section of summary.details) {
2232
- console.log(` ${chalk15.yellow(section.category)}`);
2881
+ console.log(` ${chalk18.yellow(section.category)}`);
2233
2882
  for (const item of section.items) {
2234
2883
  console.log(` \u2022 ${item}`);
2235
2884
  }
@@ -2238,561 +2887,116 @@ function printSummary(summary, author, since, until) {
2238
2887
  }
2239
2888
  }
2240
2889
 
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 });
2278
- }
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 });
2286
- }
2287
- }
2288
- function readConfigFile(path2) {
2289
- if (!existsSync4(path2)) return {};
2290
- try {
2291
- return JSON.parse(readFileSync5(path2, "utf-8"));
2292
- } catch {
2293
- return {};
2294
- }
2295
- }
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");
2320
- }
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);
2334
- }
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(", ")}`));
2353
- process.exit(1);
2354
- }
2355
- 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}`));
2359
- } catch (err) {
2360
- console.error(chalk16.red(err.message));
2361
- process.exit(1);
2362
- }
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"));
2392
- }
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"));
2403
- process.exit(1);
2404
- }
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");
2410
- } else {
2411
- targetPath = join5(homedir3(), ".config", "gut");
2412
- }
2413
- }
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);
2425
- }
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(", ")}`));
2890
+ // src/commands/sync.ts
2891
+ import chalk19 from "chalk";
2892
+ import { Command as Command18 } from "commander";
2893
+ import ora15 from "ora";
2894
+ import { simpleGit as simpleGit16 } from "simple-git";
2895
+ 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) => {
2896
+ const git = simpleGit16();
2897
+ const isRepo = await git.checkIsRepo();
2898
+ if (!isRepo) {
2899
+ console.error(chalk19.red("Error: Not a git repository"));
2443
2900
  process.exit(1);
2444
2901
  }
2902
+ const spinner = ora15("Checking repository status...").start();
2445
2903
  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);
2452
- }
2453
- });
2454
-
2455
- // src/commands/init.ts
2456
- import { Command as Command17 } from "commander";
2457
- import chalk18 from "chalk";
2458
- import ora14 from "ora";
2459
- 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}`);
2495
- }
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;
2503
- }
2504
- case "openai": {
2505
- const openai = createOpenAI2({ apiKey });
2506
- model = openai(modelName);
2507
- break;
2508
- }
2509
- case "anthropic": {
2510
- const anthropic = createAnthropic2({ apiKey });
2511
- model = anthropic(modelName);
2512
- break;
2513
- }
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"));
2904
+ const status = await git.status();
2905
+ const hasChanges = !status.isClean();
2906
+ if (hasChanges && !options.stash && !options.force) {
2907
+ spinner.stop();
2908
+ console.log(chalk19.yellow("You have uncommitted changes:"));
2909
+ if (status.modified.length > 0) {
2910
+ console.log(chalk19.gray(` Modified: ${status.modified.length} file(s)`));
2911
+ }
2912
+ if (status.not_added.length > 0) {
2913
+ console.log(chalk19.gray(` Untracked: ${status.not_added.length} file(s)`));
2914
+ }
2915
+ console.log();
2916
+ console.log(chalk19.gray("Use --stash to auto-stash, or --force to sync anyway"));
2552
2917
  process.exit(1);
2553
2918
  }
2554
- const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2555
- targetDir = join6(repoRoot.trim(), ".gut");
2556
- }
2557
- if (!existsSync6(targetDir)) {
2558
- mkdirSync3(targetDir, { recursive: true });
2559
- console.log(chalk18.green(`Created ${targetDir}`));
2560
- }
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
2568
- `));
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;
2919
+ let stashed = false;
2920
+ if (hasChanges && options.stash) {
2921
+ spinner.text = "Stashing changes...";
2922
+ await git.stash(["push", "-m", `gut-sync: auto-stash before sync`]);
2923
+ stashed = true;
2578
2924
  }
2579
- if (existsSync6(targetPath) && !options.force) {
2580
- console.log(chalk18.gray(` Skipped: ${filename} (already exists)`));
2581
- skipped++;
2582
- continue;
2925
+ spinner.text = "Fetching from remote...";
2926
+ await git.fetch(["--all", "--prune"]);
2927
+ const currentBranch = status.current;
2928
+ if (!currentBranch) {
2929
+ spinner.fail("Could not determine current branch");
2930
+ process.exit(1);
2583
2931
  }
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`));
2932
+ const trackingBranch = status.tracking;
2933
+ if (!trackingBranch) {
2934
+ spinner.warn(`Branch ${currentBranch} has no upstream tracking branch`);
2935
+ console.log(chalk19.gray(`
2936
+ To set upstream: git push -u origin ${currentBranch}`));
2937
+ if (stashed) {
2938
+ await git.stash(["pop"]);
2939
+ console.log(chalk19.gray("Restored stashed changes"));
2594
2940
  }
2595
- } else {
2596
- console.log(chalk18.green(` Copied: ${filename}`));
2941
+ return;
2597
2942
  }
2598
- writeFileSync3(targetPath, content);
2599
- copied++;
2600
- }
2601
- 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."));
2614
- }
2615
- if (options.open) {
2943
+ const strategy = options.merge ? "merge" : "rebase";
2944
+ spinner.text = `Syncing with ${trackingBranch} (${strategy})...`;
2616
2945
  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}`));
2623
- }
2624
- }
2625
- });
2626
-
2627
- // src/commands/gitignore.ts
2628
- import { Command as Command18 } from "commander";
2629
- import chalk19 from "chalk";
2630
- import ora15 from "ora";
2631
- 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 = [];
2680
- 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;
2946
+ if (options.merge) {
2947
+ await git.merge([trackingBranch]);
2948
+ } else {
2949
+ await git.rebase([trackingBranch]);
2685
2950
  }
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));
2951
+ } catch {
2952
+ spinner.fail(`${strategy} failed - you may have conflicts`);
2953
+ console.log(chalk19.yellow("\nResolve conflicts and then:"));
2954
+ if (options.merge) {
2955
+ console.log(chalk19.gray(" git add . && git commit"));
2691
2956
  } else {
2692
- files.push(entry.name);
2957
+ console.log(chalk19.gray(" git add . && git rebase --continue"));
2693
2958
  }
2694
- }
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 {
2959
+ if (stashed) {
2960
+ console.log(
2961
+ chalk19.yellow("\nNote: You have stashed changes. Run `git stash pop` after resolving.")
2962
+ );
2713
2963
  }
2714
- } else {
2715
- const filePath = join7(repoRoot, configFile);
2716
- if (existsSync7(filePath)) {
2964
+ process.exit(1);
2965
+ }
2966
+ const newStatus = await git.status();
2967
+ const ahead = newStatus.ahead || 0;
2968
+ const behind = newStatus.behind || 0;
2969
+ spinner.succeed(chalk19.green("Synced successfully"));
2970
+ if (behind > 0) {
2971
+ console.log(chalk19.yellow(` \u2193 ${behind} commit(s) behind`));
2972
+ }
2973
+ if (ahead > 0) {
2974
+ if (options.push !== false) {
2975
+ const pushSpinner = ora15("Pushing to remote...").start();
2717
2976
  try {
2718
- const content = readFileSync7(filePath, "utf-8");
2719
- found.set(configFile, content.slice(0, 2e3));
2720
- } catch {
2977
+ await git.push();
2978
+ pushSpinner.succeed(chalk19.green(`Pushed ${ahead} commit(s)`));
2979
+ } catch (err) {
2980
+ pushSpinner.fail("Push failed");
2981
+ console.error(chalk19.red(err instanceof Error ? err.message : "Unknown error"));
2721
2982
  }
2983
+ } else {
2984
+ console.log(chalk19.cyan(` \u2191 ${ahead} commit(s) ahead`));
2722
2985
  }
2723
2986
  }
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
- \`\`\``);
2752
- }
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;
2770
- }
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;
2987
+ if (stashed) {
2988
+ spinner.start("Restoring stashed changes...");
2989
+ try {
2990
+ await git.stash(["pop"]);
2991
+ spinner.succeed("Restored stashed changes");
2992
+ } catch {
2993
+ spinner.warn("Could not auto-restore stash (may have conflicts)");
2994
+ console.log(chalk19.gray(" Run `git stash pop` manually"));
2789
2995
  }
2790
2996
  }
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"));
2997
+ } catch (err) {
2998
+ spinner.fail("Sync failed");
2999
+ console.error(chalk19.red(err instanceof Error ? err.message : "Unknown error"));
2796
3000
  process.exit(1);
2797
3001
  }
2798
3002
  });