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