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