gitxplain 0.1.3 → 0.1.8

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.
@@ -17,6 +17,19 @@ const COLORS = {
17
17
  red: "\x1b[31m"
18
18
  };
19
19
 
20
+ export function getBootHelpText() {
21
+ return [
22
+ "Available commands:",
23
+ " help Show this command list",
24
+ " repos Select a GitHub repository and commit for analysis",
25
+ " issues Summarize open issues for the selected repository",
26
+ " status Review local uncommitted git diff",
27
+ " download Download the selected repository at the selected commit",
28
+ " clear Reset the current chat history",
29
+ " exit Close the interactive session"
30
+ ].join("\n");
31
+ }
32
+
20
33
  export class ChatService {
21
34
  constructor(token, providerOverride, modelOverride, username) {
22
35
  this.token = token;
@@ -220,25 +233,32 @@ Analysis:
220
233
  });
221
234
  };
222
235
 
223
- console.log(`${COLORS.bold}${COLORS.cyan}GitHub Chat - Type 'repos' to select a repo, 'download' to clone the selected commit state, 'exit' to quit, 'clear' to reset history\n${COLORS.reset}`);
236
+ console.log(`${COLORS.bold}${COLORS.cyan}GitHub Chat${COLORS.reset}`);
237
+ console.log(`${COLORS.cyan}${getBootHelpText()}\n${COLORS.reset}`);
224
238
  console.log(`${COLORS.cyan}Model: ${this.config.model} (${this.config.provider})\n${COLORS.reset}`);
225
239
 
226
240
  while (true) {
227
241
  const userInput = await question("You: ");
242
+ const normalizedInput = userInput.trim().toLowerCase();
243
+
244
+ if (normalizedInput === "help") {
245
+ console.log(`\n${COLORS.cyan}${getBootHelpText()}\n${COLORS.reset}`);
246
+ continue;
247
+ }
228
248
 
229
- if (userInput.toLowerCase() === "exit") {
249
+ if (normalizedInput === "exit") {
230
250
  console.log(`${COLORS.green}Goodbye!${COLORS.reset}`);
231
251
  rl.close();
232
252
  break;
233
253
  }
234
254
 
235
- if (userInput.toLowerCase() === "clear") {
255
+ if (normalizedInput === "clear") {
236
256
  this.conversationHistory = [];
237
257
  console.log(`${COLORS.yellow}Conversation history cleared.\n${COLORS.reset}`);
238
258
  continue;
239
259
  }
240
260
 
241
- if (userInput.toLowerCase() === "download") {
261
+ if (normalizedInput === "download") {
242
262
  if (!this.activeRepo || !this.activeCommit) {
243
263
  console.log(`${COLORS.yellow}No repository/commit selected. Please type 'repos' first.\n${COLORS.reset}`);
244
264
  continue;
@@ -257,7 +277,7 @@ Analysis:
257
277
  continue;
258
278
  }
259
279
 
260
- if (userInput.toLowerCase() === "repos") {
280
+ if (normalizedInput === "repos") {
261
281
  const selectedRepo = await this.showRepoSelector();
262
282
  if (selectedRepo) {
263
283
  this.activeRepo = selectedRepo;
@@ -302,7 +322,7 @@ Please acknowledge this selection in a maximum of 3 sentences, giving a brief su
302
322
  continue;
303
323
  }
304
324
 
305
- if (userInput.toLowerCase() === "status") {
325
+ if (normalizedInput === "status") {
306
326
  try {
307
327
  const diff = execSync("git diff").toString();
308
328
  if (!diff) {
@@ -319,7 +339,7 @@ Please acknowledge this selection in a maximum of 3 sentences, giving a brief su
319
339
  continue;
320
340
  }
321
341
 
322
- if (userInput.toLowerCase() === "issues") {
342
+ if (normalizedInput === "issues") {
323
343
  if (!this.activeRepo) {
324
344
  console.log(`${COLORS.yellow}No active repository. Please type 'repos' first.\n${COLORS.reset}`);
325
345
  continue;
@@ -656,7 +676,7 @@ Please acknowledge this selection in a maximum of 3 sentences, giving a brief su
656
676
  }
657
677
  }
658
678
 
659
- export async function startChatSession(token, username, providerOverride, modelOverride) {
679
+ export async function startChatSession(token, providerOverride, modelOverride, username) {
660
680
  const chatService = new ChatService(token, providerOverride, modelOverride, username);
661
681
  await chatService.initializeRepoContext();
662
682
  await chatService.startInteractiveChat();
@@ -1,7 +1,41 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
 
5
+ const ENV_CONFIG_KEYS = new Set([
6
+ "LLM_PROVIDER",
7
+ "LLM_MODEL",
8
+ "OPENAI_API_KEY",
9
+ "OPENAI_MODEL",
10
+ "OPENAI_BASE_URL",
11
+ "GROQ_API_KEY",
12
+ "GROQ_MODEL",
13
+ "GROQ_BASE_URL",
14
+ "OPENROUTER_API_KEY",
15
+ "OPENROUTER_MODEL",
16
+ "OPENROUTER_BASE_URL",
17
+ "OPENROUTER_SITE_URL",
18
+ "OPENROUTER_APP_NAME",
19
+ "GEMINI_API_KEY",
20
+ "GEMINI_MODEL",
21
+ "GEMINI_BASE_URL",
22
+ "OLLAMA_API_KEY",
23
+ "OLLAMA_MODEL",
24
+ "OLLAMA_BASE_URL",
25
+ "CHUTES_API_KEY",
26
+ "CHUTES_MODEL",
27
+ "CHUTES_BASE_URL"
28
+ ]);
29
+
30
+ const PROVIDER_API_KEY_FIELDS = {
31
+ openai: "OPENAI_API_KEY",
32
+ groq: "GROQ_API_KEY",
33
+ openrouter: "OPENROUTER_API_KEY",
34
+ gemini: "GEMINI_API_KEY",
35
+ ollama: "OLLAMA_API_KEY",
36
+ chutes: "CHUTES_API_KEY"
37
+ };
38
+
5
39
  function readJsonConfig(filePath) {
6
40
  if (!existsSync(filePath)) {
7
41
  return {};
@@ -14,9 +48,16 @@ function readJsonConfig(filePath) {
14
48
  }
15
49
  }
16
50
 
51
+ export function getUserConfigPath() {
52
+ return path.join(os.homedir(), ".gitxplain", "config.json");
53
+ }
54
+
55
+ export function loadUserConfig() {
56
+ return readJsonConfig(getUserConfigPath());
57
+ }
58
+
17
59
  export function loadConfig(cwd) {
18
- const homeDir = os.homedir();
19
- const userConfigPath = path.join(homeDir, ".gitxplain", "config.json");
60
+ const userConfigPath = getUserConfigPath();
20
61
  const projectConfigPath = path.join(cwd, ".gitxplainrc");
21
62
  const projectJsonConfigPath = path.join(cwd, ".gitxplainrc.json");
22
63
 
@@ -26,3 +67,34 @@ export function loadConfig(cwd) {
26
67
  ...readJsonConfig(projectJsonConfigPath)
27
68
  };
28
69
  }
70
+
71
+ export function applyConfigEnvironment(config) {
72
+ for (const [key, value] of Object.entries(config)) {
73
+ if (!ENV_CONFIG_KEYS.has(key)) {
74
+ continue;
75
+ }
76
+
77
+ if (typeof value === "string" && value !== "" && !process.env[key]) {
78
+ process.env[key] = value;
79
+ }
80
+ }
81
+ }
82
+
83
+ export function getProviderApiKeyField(provider) {
84
+ const normalized = provider?.toLowerCase();
85
+ return normalized ? PROVIDER_API_KEY_FIELDS[normalized] ?? null : null;
86
+ }
87
+
88
+ export function writeUserConfig(nextConfig) {
89
+ const configPath = getUserConfigPath();
90
+ mkdirSync(path.dirname(configPath), { recursive: true });
91
+ writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
92
+ return configPath;
93
+ }
94
+
95
+ export function updateUserConfig(updates) {
96
+ const currentConfig = loadUserConfig();
97
+ const nextConfig = { ...currentConfig, ...updates };
98
+ const configPath = writeUserConfig(nextConfig);
99
+ return { configPath, config: nextConfig };
100
+ }
@@ -1,4 +1,4 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
2
  import os from "node:os";
3
3
  import { mkdtempSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import path from "node:path";
@@ -87,6 +87,33 @@ export function runGitCommandUnchecked(args, cwd) {
87
87
  }
88
88
  }
89
89
 
90
+ export function listGitSubcommands() {
91
+ const output = execFileSync("git", ["help", "-a"], {
92
+ encoding: "utf8",
93
+ stdio: ["ignore", "pipe", "pipe"]
94
+ });
95
+
96
+ return new Set(
97
+ output
98
+ .split("\n")
99
+ .map((line) => line.match(/^\s{3}([a-z0-9][a-z0-9-]*)\s{2,}/i)?.[1] ?? null)
100
+ .filter(Boolean)
101
+ );
102
+ }
103
+
104
+ export function runNativeGitPassthrough(args, cwd) {
105
+ const result = spawnSync("git", args, {
106
+ cwd,
107
+ stdio: "inherit"
108
+ });
109
+
110
+ if (result.error) {
111
+ throw result.error;
112
+ }
113
+
114
+ return result.status ?? 0;
115
+ }
116
+
90
117
  export function isGitRepository(cwd) {
91
118
  try {
92
119
  return runGitCommand(["rev-parse", "--is-inside-work-tree"], cwd) === "true";
@@ -231,6 +258,21 @@ export function gitPush(cwd, remote = null, branch = null, runner = runGitComman
231
258
  return runner(args, cwd);
232
259
  }
233
260
 
261
+ export function gitPull(cwd, remote = null, branch = null, runner = runGitCommand) {
262
+ const args = ["pull"];
263
+
264
+ if (remote) {
265
+ args.push(remote);
266
+ }
267
+
268
+ if (branch) {
269
+ args.push(branch);
270
+ }
271
+
272
+ return runner(args, cwd);
273
+ }
274
+
275
+
234
276
  export function gitCreateAnnotatedTag(tagName, ref, message, cwd) {
235
277
  return runGitCommand(["tag", "-a", tagName, ref, "-m", message], cwd);
236
278
  }
@@ -438,8 +480,8 @@ export function isAncestorCommit(ancestorRef, descendantRef, cwd) {
438
480
  throw new Error(result.stderr || "Unable to determine commit ancestry.");
439
481
  }
440
482
 
441
- export function gitResetHard(ref, cwd) {
442
- return runGitCommand(["reset", "--hard", ref], cwd);
483
+ export function gitResetHard(ref, cwd, runner = runGitCommand) {
484
+ return runner(["reset", "--hard", ref], cwd);
443
485
  }
444
486
 
445
487
  export function gitCherryPickNoCommit(ref, cwd) {