gut-cli 0.1.13 → 0.1.15

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.
@@ -0,0 +1,42 @@
1
+ You are an expert at creating .gitignore files.
2
+
3
+ Analyze the following project structure and configuration files to generate an appropriate .gitignore file.
4
+
5
+ ## Project Structure
6
+
7
+ ```
8
+ {{files}}
9
+ ```
10
+
11
+ {{#configFiles}}
12
+ ## Detected Config Files
13
+
14
+ {{configFiles}}
15
+ {{/configFiles}}
16
+
17
+ {{#existingGitignore}}
18
+ ## Existing .gitignore
19
+
20
+ ```
21
+ {{existingGitignore}}
22
+ ```
23
+ {{/existingGitignore}}
24
+
25
+ ## Rules
26
+
27
+ - Detect the language/framework being used (Node.js, Python, Go, Rust, Java, Ruby, PHP, .NET, etc.)
28
+ - Include common ignore patterns for the detected stack
29
+ - Include OS-specific files (.DS_Store, Thumbs.db, etc.)
30
+ - Include IDE/editor files (.vscode/, .idea/, *.swp, etc.)
31
+ - Include environment files (.env, .env.local, etc.)
32
+ - Include build outputs and dependencies based on detected stack
33
+ - Include log files and temporary files
34
+ - Do NOT ignore files that should be tracked (source code, configs, etc.)
35
+ - Keep the file organized with comments for each section
36
+ {{#existingGitignore}}
37
+ - Preserve any project-specific patterns from the existing .gitignore
38
+ {{/existingGitignore}}
39
+
40
+ ## Output
41
+
42
+ Respond with ONLY the .gitignore content, nothing else. Include section comments for clarity.
package/README.md CHANGED
@@ -35,6 +35,7 @@ npm install -g gut-cli
35
35
  | `gut config` | Manage configuration (language, etc.) |
36
36
  | `gut lang` | Set or show output language |
37
37
  | `gut init` | Initialize .gut/ templates in your project |
38
+ | `gut gitignore` | Generate .gitignore from codebase |
38
39
 
39
40
  ### `gut commit`
40
41
 
@@ -389,6 +390,32 @@ gut init --provider openai
389
390
 
390
391
  Templates are automatically translated to your configured language (set via `gut lang`).
391
392
 
393
+ ### `gut gitignore`
394
+
395
+ Generate a .gitignore file by analyzing your project structure.
396
+
397
+ ```bash
398
+ # Generate .gitignore (prompts before overwriting)
399
+ gut gitignore
400
+
401
+ # Auto-overwrite without confirmation
402
+ gut gitignore --yes
403
+
404
+ # Print to stdout instead of file
405
+ gut gitignore --stdout
406
+
407
+ # Use specific provider
408
+ gut gitignore --provider openai
409
+ ```
410
+
411
+ **How it works:**
412
+ - Scans your project structure (files and directories)
413
+ - Detects config files (package.json, Cargo.toml, go.mod, pyproject.toml, etc.)
414
+ - Identifies the language/framework stack
415
+ - Generates appropriate ignore patterns
416
+
417
+ **Template Support**: Create `.gut/gitignore.md` to customize the generation prompt.
418
+
392
419
  ### `gut cleanup`
393
420
 
394
421
  Delete merged branches safely.
@@ -457,6 +484,7 @@ gut looks for template files in your repository's `.gut/` folder. Each template
457
484
  | `.gut/changelog.md` | Changelog format |
458
485
  | `.gut/stash.md` | Stash name prompt |
459
486
  | `.gut/summary.md` | Work summary format |
487
+ | `.gut/gitignore.md` | Gitignore generation prompt |
460
488
  | `.github/pull_request_template.md` | GitHub PR template (prioritized over `.gut/pr.md`) |
461
489
 
462
490
  ## Development
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command18 } from "commander";
4
+ import { Command as Command19 } from "commander";
5
5
 
6
6
  // src/commands/cleanup.ts
7
7
  import { Command } from "commander";
@@ -114,6 +114,9 @@ async function getKeytar() {
114
114
  }
115
115
  }
116
116
  async function saveApiKey(provider, apiKey) {
117
+ if (provider === "ollama") {
118
+ throw new Error("Ollama does not require an API key");
119
+ }
117
120
  const keytar = await getKeytar();
118
121
  if (!keytar) {
119
122
  throw new Error("Keychain not available. Set environment variable instead.");
@@ -121,6 +124,9 @@ async function saveApiKey(provider, apiKey) {
121
124
  await keytar.setPassword(SERVICE_NAME, PROVIDER_KEY_MAP[provider], apiKey);
122
125
  }
123
126
  async function getApiKey(provider) {
127
+ if (provider === "ollama") {
128
+ return null;
129
+ }
124
130
  const envKey = process.env[ENV_VAR_MAP[provider]];
125
131
  if (envKey) return envKey;
126
132
  const fallbackKey = process.env[FALLBACK_ENV_MAP[provider]];
@@ -130,6 +136,9 @@ async function getApiKey(provider) {
130
136
  return keytar.getPassword(SERVICE_NAME, PROVIDER_KEY_MAP[provider]);
131
137
  }
132
138
  async function deleteApiKey(provider) {
139
+ if (provider === "ollama") {
140
+ throw new Error("Ollama does not use an API key");
141
+ }
133
142
  const keytar = await getKeytar();
134
143
  if (!keytar) {
135
144
  throw new Error("Keychain not available.");
@@ -137,20 +146,22 @@ async function deleteApiKey(provider) {
137
146
  return keytar.deletePassword(SERVICE_NAME, PROVIDER_KEY_MAP[provider]);
138
147
  }
139
148
  async function listProviders() {
140
- const providers = ["gemini", "openai", "anthropic"];
149
+ const apiKeyProviders = ["gemini", "openai", "anthropic"];
141
150
  const results = await Promise.all(
142
- providers.map(async (provider) => ({
151
+ apiKeyProviders.map(async (provider) => ({
143
152
  provider,
144
153
  hasKey: !!await getApiKey(provider)
145
154
  }))
146
155
  );
156
+ results.push({ provider: "ollama", hasKey: true });
147
157
  return results;
148
158
  }
149
159
  function getProviderDisplayName(provider) {
150
160
  const names = {
151
161
  gemini: "Google Gemini",
152
162
  openai: "OpenAI",
153
- anthropic: "Anthropic Claude"
163
+ anthropic: "Anthropic Claude",
164
+ ollama: "Ollama (Local)"
154
165
  };
155
166
  return names[provider];
156
167
  }
@@ -270,6 +281,7 @@ import { generateText, generateObject } from "ai";
270
281
  import { createGoogleGenerativeAI } from "@ai-sdk/google";
271
282
  import { createOpenAI } from "@ai-sdk/openai";
272
283
  import { createAnthropic } from "@ai-sdk/anthropic";
284
+ import { createOllama } from "ollama-ai-provider";
273
285
  import { z } from "zod";
274
286
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
275
287
  import { join as join2, dirname } from "path";
@@ -410,29 +422,45 @@ function applyTemplate(userTemplate, templateName, variables) {
410
422
  var DEFAULT_MODELS = {
411
423
  gemini: "gemini-2.0-flash",
412
424
  openai: "gpt-4o-mini",
413
- anthropic: "claude-sonnet-4-20250514"
425
+ anthropic: "claude-sonnet-4-20250514",
426
+ ollama: "llama3.2"
414
427
  };
415
428
  async function getModel(options) {
416
- const apiKey = await getApiKey(options.provider);
417
- if (!apiKey) {
418
- throw new Error(
419
- `No API key found for ${options.provider}. Run: gut auth login --provider ${options.provider}`
420
- );
421
- }
422
429
  const modelName = options.model || DEFAULT_MODELS[options.provider];
430
+ async function resolveApiKey() {
431
+ if (options.apiKey) return options.apiKey;
432
+ return getApiKey(options.provider);
433
+ }
434
+ if (options.provider !== "ollama") {
435
+ const apiKey = await resolveApiKey();
436
+ if (!apiKey) {
437
+ throw new Error(
438
+ `No API key found for ${options.provider}. Run: gut auth login --provider ${options.provider}`
439
+ );
440
+ }
441
+ }
423
442
  switch (options.provider) {
424
443
  case "gemini": {
444
+ const apiKey = await resolveApiKey();
425
445
  const google = createGoogleGenerativeAI({ apiKey });
426
446
  return google(modelName);
427
447
  }
428
448
  case "openai": {
449
+ const apiKey = await resolveApiKey();
429
450
  const openai = createOpenAI({ apiKey });
430
451
  return openai(modelName);
431
452
  }
432
453
  case "anthropic": {
454
+ const apiKey = await resolveApiKey();
433
455
  const anthropic = createAnthropic({ apiKey });
434
456
  return anthropic(modelName);
435
457
  }
458
+ case "ollama": {
459
+ const ollama = createOllama({
460
+ baseURL: options.ollamaBaseUrl || "http://localhost:11434/api"
461
+ });
462
+ return ollama(modelName);
463
+ }
436
464
  }
437
465
  }
438
466
  async function generateCommitMessage(diff, options, template) {
@@ -614,11 +642,12 @@ async function searchCommits(query, commits, options, maxResults = 5, template)
614
642
  schema: CommitSearchSchema,
615
643
  prompt
616
644
  });
617
- const enrichedMatches = result.object.matches.map((match) => {
645
+ const enrichedMatches = result.object.matches.map((match, index) => {
618
646
  const commit = commits.find((c) => c.hash.startsWith(match.hash));
619
647
  if (!commit) {
620
648
  return null;
621
649
  }
650
+ const relevance = index === 0 ? "high" : index < 3 ? "medium" : "low";
622
651
  return {
623
652
  hash: commit.hash,
624
653
  message: commit.message,
@@ -626,15 +655,9 @@ async function searchCommits(query, commits, options, maxResults = 5, template)
626
655
  email: commit.email,
627
656
  date: commit.date,
628
657
  reason: match.reason,
629
- relevance: "high"
630
- // First results are most relevant
658
+ relevance
631
659
  };
632
660
  }).filter((m) => m !== null);
633
- enrichedMatches.forEach((match, index) => {
634
- if (index === 0) match.relevance = "high";
635
- else if (index < 3) match.relevance = "medium";
636
- else match.relevance = "low";
637
- });
638
661
  return {
639
662
  matches: enrichedMatches,
640
663
  summary: result.object.summary
@@ -735,6 +758,20 @@ async function resolveConflict(conflictedContent, context, options, template) {
735
758
  });
736
759
  return result.object;
737
760
  }
761
+ async function generateGitignore(context, options, template) {
762
+ const model = await getModel(options);
763
+ const prompt = applyTemplate(template, "gitignore", {
764
+ files: context.files,
765
+ configFiles: context.configFiles,
766
+ existingGitignore: context.existingGitignore
767
+ });
768
+ const result = await generateText({
769
+ model,
770
+ prompt,
771
+ maxTokens: 2e3
772
+ });
773
+ return result.text.trim();
774
+ }
738
775
 
739
776
  // src/commands/commit.ts
740
777
  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) => {
@@ -2477,8 +2514,181 @@ var initCommand = new Command17("init").description("Initialize .gut/ templates
2477
2514
  console.log(chalk18.gray("\nYou can now customize these templates for your project."));
2478
2515
  });
2479
2516
 
2517
+ // src/commands/gitignore.ts
2518
+ import { Command as Command18 } from "commander";
2519
+ import chalk19 from "chalk";
2520
+ import ora15 from "ora";
2521
+ import { simpleGit as simpleGit15 } from "simple-git";
2522
+ import { readdirSync as readdirSync2, readFileSync as readFileSync7, existsSync as existsSync6, writeFileSync as writeFileSync4 } from "fs";
2523
+ import { join as join6 } from "path";
2524
+ var CONFIG_FILES = [
2525
+ // JavaScript/TypeScript
2526
+ "package.json",
2527
+ "tsconfig.json",
2528
+ "vite.config.ts",
2529
+ "vite.config.js",
2530
+ "next.config.js",
2531
+ "next.config.mjs",
2532
+ "nuxt.config.ts",
2533
+ "astro.config.mjs",
2534
+ // Python
2535
+ "pyproject.toml",
2536
+ "setup.py",
2537
+ "requirements.txt",
2538
+ "Pipfile",
2539
+ "poetry.lock",
2540
+ // Go
2541
+ "go.mod",
2542
+ "go.sum",
2543
+ // Rust
2544
+ "Cargo.toml",
2545
+ "Cargo.lock",
2546
+ // Java/Kotlin
2547
+ "pom.xml",
2548
+ "build.gradle",
2549
+ "build.gradle.kts",
2550
+ // Ruby
2551
+ "Gemfile",
2552
+ "Gemfile.lock",
2553
+ // PHP
2554
+ "composer.json",
2555
+ "composer.lock",
2556
+ // .NET
2557
+ "*.csproj",
2558
+ "*.fsproj",
2559
+ "*.sln",
2560
+ // Elixir
2561
+ "mix.exs",
2562
+ // Dart/Flutter
2563
+ "pubspec.yaml",
2564
+ // Swift
2565
+ "Package.swift"
2566
+ ];
2567
+ function getFiles(dir, maxDepth = 3, currentDepth = 0) {
2568
+ if (currentDepth >= maxDepth) return [];
2569
+ const files = [];
2570
+ try {
2571
+ const entries = readdirSync2(dir, { withFileTypes: true });
2572
+ for (const entry of entries) {
2573
+ if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "vendor" || entry.name === "target" || entry.name === "__pycache__" || entry.name === "venv" || entry.name === ".venv") {
2574
+ continue;
2575
+ }
2576
+ const fullPath = join6(dir, entry.name);
2577
+ if (entry.isDirectory()) {
2578
+ files.push(entry.name + "/");
2579
+ const subFiles = getFiles(fullPath, maxDepth, currentDepth + 1);
2580
+ files.push(...subFiles.map((f) => entry.name + "/" + f));
2581
+ } else {
2582
+ files.push(entry.name);
2583
+ }
2584
+ }
2585
+ } catch {
2586
+ }
2587
+ return files;
2588
+ }
2589
+ function findConfigFiles(repoRoot) {
2590
+ const found = /* @__PURE__ */ new Map();
2591
+ for (const configFile of CONFIG_FILES) {
2592
+ if (configFile.includes("*")) {
2593
+ const ext = configFile.replace("*", "");
2594
+ try {
2595
+ const entries = readdirSync2(repoRoot);
2596
+ for (const entry of entries) {
2597
+ if (entry.endsWith(ext)) {
2598
+ const content = readFileSync7(join6(repoRoot, entry), "utf-8");
2599
+ found.set(entry, content.slice(0, 2e3));
2600
+ }
2601
+ }
2602
+ } catch {
2603
+ }
2604
+ } else {
2605
+ const filePath = join6(repoRoot, configFile);
2606
+ if (existsSync6(filePath)) {
2607
+ try {
2608
+ const content = readFileSync7(filePath, "utf-8");
2609
+ found.set(configFile, content.slice(0, 2e3));
2610
+ } catch {
2611
+ }
2612
+ }
2613
+ }
2614
+ }
2615
+ return found;
2616
+ }
2617
+ 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) => {
2618
+ const git = simpleGit15();
2619
+ const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
2620
+ const root = repoRoot.trim();
2621
+ const provider = options.provider.toLowerCase();
2622
+ const template = findTemplate(root, "gitignore");
2623
+ if (template) {
2624
+ console.log(chalk19.gray("Using template from project..."));
2625
+ }
2626
+ const spinner = ora15("Analyzing project structure...").start();
2627
+ const files = getFiles(root);
2628
+ const configFiles = findConfigFiles(root);
2629
+ const gitignorePath = join6(root, options.output);
2630
+ let existingGitignore;
2631
+ if (existsSync6(gitignorePath)) {
2632
+ existingGitignore = readFileSync7(gitignorePath, "utf-8");
2633
+ }
2634
+ let configFilesStr = "";
2635
+ if (configFiles.size > 0) {
2636
+ const entries = [];
2637
+ for (const [name, content] of configFiles) {
2638
+ entries.push(`### ${name}
2639
+ \`\`\`
2640
+ ${content}
2641
+ \`\`\``);
2642
+ }
2643
+ configFilesStr = entries.join("\n\n");
2644
+ }
2645
+ spinner.text = "Generating .gitignore...";
2646
+ try {
2647
+ const gitignoreContent = await generateGitignore(
2648
+ {
2649
+ files: files.slice(0, 200).join("\n"),
2650
+ configFiles: configFilesStr,
2651
+ existingGitignore
2652
+ },
2653
+ { provider, model: options.model },
2654
+ template || void 0
2655
+ );
2656
+ spinner.stop();
2657
+ if (options.stdout) {
2658
+ console.log(gitignoreContent);
2659
+ return;
2660
+ }
2661
+ console.log(chalk19.bold("\nGenerated .gitignore:\n"));
2662
+ console.log(chalk19.gray("\u2500".repeat(50)));
2663
+ console.log(gitignoreContent);
2664
+ console.log(chalk19.gray("\u2500".repeat(50)));
2665
+ console.log();
2666
+ if (existsSync6(gitignorePath) && !options.yes) {
2667
+ const readline = await import("readline");
2668
+ const rl = readline.createInterface({
2669
+ input: process.stdin,
2670
+ output: process.stdout
2671
+ });
2672
+ const answer = await new Promise((resolve) => {
2673
+ rl.question(chalk19.cyan(`${options.output} already exists. Overwrite? (y/N) `), resolve);
2674
+ });
2675
+ rl.close();
2676
+ if (answer.toLowerCase() !== "y") {
2677
+ console.log(chalk19.gray("Aborted."));
2678
+ return;
2679
+ }
2680
+ }
2681
+ writeFileSync4(gitignorePath, gitignoreContent);
2682
+ console.log(chalk19.green(`\u2713 Wrote ${options.output}`));
2683
+ } catch (error) {
2684
+ spinner.fail("Failed to generate .gitignore");
2685
+ console.error(chalk19.red(error instanceof Error ? error.message : "Unknown error"));
2686
+ process.exit(1);
2687
+ }
2688
+ });
2689
+
2480
2690
  // src/index.ts
2481
- var program = new Command18();
2691
+ var program = new Command19();
2482
2692
  program.name("gut").description("Git Utility Tool - AI-powered git commands").version("0.1.0");
2483
2693
  program.addCommand(cleanupCommand);
2484
2694
  program.addCommand(authCommand);
@@ -2497,5 +2707,6 @@ program.addCommand(summaryCommand);
2497
2707
  program.addCommand(configCommand);
2498
2708
  program.addCommand(langCommand);
2499
2709
  program.addCommand(initCommand);
2710
+ program.addCommand(gitignoreCommand);
2500
2711
  program.parse();
2501
2712
  //# sourceMappingURL=index.js.map