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.
- package/.github/workflows/ci.yml +28 -0
- package/.github/workflows/release.yml +27 -0
- package/IMPLEMENTATION.md +10 -10
- package/README.md +386 -110
- package/cli/index.js +359 -209
- package/cli/services/chatService.js +28 -8
- package/cli/services/configService.js +75 -3
- package/cli/services/gitService.js +45 -3
- package/cli/services/mergeService.js +303 -69
- package/cli/services/outputFormatter.js +2 -36
- package/cli/services/pipelineService.js +721 -0
- package/package.json +2 -2
|
@@ -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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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,
|
|
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
|
|
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
|
|
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) {
|