gut-cli 0.1.8 → 0.1.10
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/README.md +155 -32
- package/dist/index.js +752 -514
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command17 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/cleanup.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -259,13 +259,11 @@ authCommand.command("status").description("Show which providers have API keys co
|
|
|
259
259
|
}
|
|
260
260
|
});
|
|
261
261
|
|
|
262
|
-
// src/commands/
|
|
262
|
+
// src/commands/commit.ts
|
|
263
263
|
import { Command as Command3 } from "commander";
|
|
264
264
|
import chalk3 from "chalk";
|
|
265
265
|
import ora2 from "ora";
|
|
266
266
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
267
|
-
import { existsSync, readFileSync } from "fs";
|
|
268
|
-
import { join } from "path";
|
|
269
267
|
|
|
270
268
|
// src/lib/ai.ts
|
|
271
269
|
import { generateText, generateObject } from "ai";
|
|
@@ -273,6 +271,143 @@ import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
|
273
271
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
274
272
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
275
273
|
import { z } from "zod";
|
|
274
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
275
|
+
import { join as join2, dirname } from "path";
|
|
276
|
+
import { fileURLToPath } from "url";
|
|
277
|
+
|
|
278
|
+
// src/lib/config.ts
|
|
279
|
+
import { homedir } from "os";
|
|
280
|
+
import { join } from "path";
|
|
281
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
282
|
+
import { execSync } from "child_process";
|
|
283
|
+
var DEFAULT_CONFIG = {
|
|
284
|
+
lang: "en"
|
|
285
|
+
};
|
|
286
|
+
function getGlobalConfigPath() {
|
|
287
|
+
const configDir = join(homedir(), ".config", "gut");
|
|
288
|
+
return join(configDir, "config.json");
|
|
289
|
+
}
|
|
290
|
+
function getRepoRoot() {
|
|
291
|
+
try {
|
|
292
|
+
return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function getLocalConfigPath() {
|
|
298
|
+
const repoRoot = getRepoRoot();
|
|
299
|
+
if (!repoRoot) return null;
|
|
300
|
+
return join(repoRoot, ".gut", "config.json");
|
|
301
|
+
}
|
|
302
|
+
function ensureGlobalConfigDir() {
|
|
303
|
+
const configDir = join(homedir(), ".config", "gut");
|
|
304
|
+
if (!existsSync(configDir)) {
|
|
305
|
+
mkdirSync(configDir, { recursive: true });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function ensureLocalConfigDir() {
|
|
309
|
+
const repoRoot = getRepoRoot();
|
|
310
|
+
if (!repoRoot) return;
|
|
311
|
+
const gutDir = join(repoRoot, ".gut");
|
|
312
|
+
if (!existsSync(gutDir)) {
|
|
313
|
+
mkdirSync(gutDir, { recursive: true });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function readConfigFile(path2) {
|
|
317
|
+
if (!existsSync(path2)) return {};
|
|
318
|
+
try {
|
|
319
|
+
return JSON.parse(readFileSync(path2, "utf-8"));
|
|
320
|
+
} catch {
|
|
321
|
+
return {};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function getGlobalConfig() {
|
|
325
|
+
const globalPath = getGlobalConfigPath();
|
|
326
|
+
return { ...DEFAULT_CONFIG, ...readConfigFile(globalPath) };
|
|
327
|
+
}
|
|
328
|
+
function getLocalConfig() {
|
|
329
|
+
const localPath = getLocalConfigPath();
|
|
330
|
+
if (!localPath) return {};
|
|
331
|
+
return readConfigFile(localPath);
|
|
332
|
+
}
|
|
333
|
+
function getConfig() {
|
|
334
|
+
const globalConfig = getGlobalConfig();
|
|
335
|
+
const localConfig = getLocalConfig();
|
|
336
|
+
return { ...globalConfig, ...localConfig };
|
|
337
|
+
}
|
|
338
|
+
function setGlobalConfig(key, value) {
|
|
339
|
+
ensureGlobalConfigDir();
|
|
340
|
+
const config = getGlobalConfig();
|
|
341
|
+
config[key] = value;
|
|
342
|
+
writeFileSync(getGlobalConfigPath(), JSON.stringify(config, null, 2));
|
|
343
|
+
}
|
|
344
|
+
function setLocalConfig(key, value) {
|
|
345
|
+
const localPath = getLocalConfigPath();
|
|
346
|
+
if (!localPath) {
|
|
347
|
+
throw new Error("Not in a git repository");
|
|
348
|
+
}
|
|
349
|
+
ensureLocalConfigDir();
|
|
350
|
+
const config = getLocalConfig();
|
|
351
|
+
config[key] = value;
|
|
352
|
+
writeFileSync(localPath, JSON.stringify(config, null, 2));
|
|
353
|
+
}
|
|
354
|
+
function getLanguage() {
|
|
355
|
+
return getConfig().lang;
|
|
356
|
+
}
|
|
357
|
+
function setLanguage(lang, local = false) {
|
|
358
|
+
if (local) {
|
|
359
|
+
setLocalConfig("lang", lang);
|
|
360
|
+
} else {
|
|
361
|
+
setGlobalConfig("lang", lang);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
function getLanguageInstruction(lang) {
|
|
365
|
+
switch (lang) {
|
|
366
|
+
case "ja":
|
|
367
|
+
return "\n\nIMPORTANT: Respond in Japanese (\u65E5\u672C\u8A9E\u3067\u56DE\u7B54\u3057\u3066\u304F\u3060\u3055\u3044).";
|
|
368
|
+
case "en":
|
|
369
|
+
default:
|
|
370
|
+
return "";
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
var VALID_LANGUAGES = ["en", "ja"];
|
|
374
|
+
function isValidLanguage(lang) {
|
|
375
|
+
return VALID_LANGUAGES.includes(lang);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/lib/ai.ts
|
|
379
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
380
|
+
var __dirname = dirname(__filename);
|
|
381
|
+
var GUT_ROOT = join2(__dirname, "..");
|
|
382
|
+
function loadTemplate(name) {
|
|
383
|
+
const templatePath = join2(GUT_ROOT, ".gut", `${name}.md`);
|
|
384
|
+
if (existsSync2(templatePath)) {
|
|
385
|
+
return readFileSync2(templatePath, "utf-8");
|
|
386
|
+
}
|
|
387
|
+
throw new Error(`Template not found: ${templatePath}`);
|
|
388
|
+
}
|
|
389
|
+
function findTemplate(repoRoot, templateName) {
|
|
390
|
+
const templatePath = join2(repoRoot, ".gut", `${templateName}.md`);
|
|
391
|
+
if (existsSync2(templatePath)) {
|
|
392
|
+
return readFileSync2(templatePath, "utf-8");
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
function applyTemplate(userTemplate, templateName, variables) {
|
|
397
|
+
const langInstruction = getLanguageInstruction(getLanguage());
|
|
398
|
+
const isUserTemplate = !!userTemplate;
|
|
399
|
+
let result = userTemplate || loadTemplate(templateName);
|
|
400
|
+
result = result.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, content) => {
|
|
401
|
+
return variables[key] ? content : "";
|
|
402
|
+
});
|
|
403
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
404
|
+
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value || "");
|
|
405
|
+
}
|
|
406
|
+
if (!isUserTemplate) {
|
|
407
|
+
result += langInstruction;
|
|
408
|
+
}
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
276
411
|
var DEFAULT_MODELS = {
|
|
277
412
|
gemini: "gemini-2.0-flash",
|
|
278
413
|
openai: "gpt-4o-mini",
|
|
@@ -301,33 +436,11 @@ async function getModel(options) {
|
|
|
301
436
|
}
|
|
302
437
|
}
|
|
303
438
|
}
|
|
304
|
-
async function generateCommitMessage(diff, options,
|
|
439
|
+
async function generateCommitMessage(diff, options, template) {
|
|
305
440
|
const model = await getModel(options);
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
--- CONVENTION START ---
|
|
310
|
-
${convention}
|
|
311
|
-
--- CONVENTION END ---
|
|
312
|
-
` : `
|
|
313
|
-
Rules:
|
|
314
|
-
- Use format: <type>(<scope>): <description>
|
|
315
|
-
- Types: feat, fix, docs, style, refactor, perf, test, chore, build, ci
|
|
316
|
-
- Scope is optional but helpful
|
|
317
|
-
- Description should be lowercase, imperative mood, no period at end
|
|
318
|
-
- Keep the first line under 72 characters
|
|
319
|
-
- If changes are complex, add a blank line and bullet points for details`;
|
|
320
|
-
const prompt = `You are an expert at writing git commit messages.
|
|
321
|
-
|
|
322
|
-
Analyze the following git diff and generate a concise, meaningful commit message.
|
|
323
|
-
${conventionInstructions}
|
|
324
|
-
|
|
325
|
-
Git diff:
|
|
326
|
-
\`\`\`
|
|
327
|
-
${diff.slice(0, 8e3)}
|
|
328
|
-
\`\`\`
|
|
329
|
-
|
|
330
|
-
Respond with ONLY the commit message, nothing else.`;
|
|
441
|
+
const prompt = applyTemplate(template, "commit", {
|
|
442
|
+
diff: diff.slice(0, 8e3)
|
|
443
|
+
});
|
|
331
444
|
const result = await generateText({
|
|
332
445
|
model,
|
|
333
446
|
prompt,
|
|
@@ -335,45 +448,14 @@ Respond with ONLY the commit message, nothing else.`;
|
|
|
335
448
|
});
|
|
336
449
|
return result.text.trim();
|
|
337
450
|
}
|
|
338
|
-
async function generatePRDescription(context, options) {
|
|
451
|
+
async function generatePRDescription(context, options, template) {
|
|
339
452
|
const model = await getModel(options);
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
Fill in each section of the template based on the changes. Keep the template structure intact.
|
|
348
|
-
Replace placeholder text and fill in the sections appropriately.` : `
|
|
349
|
-
Rules for description:
|
|
350
|
-
- Description should have:
|
|
351
|
-
- ## Summary section with 2-3 bullet points
|
|
352
|
-
- ## Changes section listing key modifications
|
|
353
|
-
- ## Test Plan section (suggest what to test)`;
|
|
354
|
-
const prompt = `You are an expert at writing pull request descriptions.
|
|
355
|
-
|
|
356
|
-
Generate a clear and informative PR title and description based on the following information.
|
|
357
|
-
|
|
358
|
-
Branch: ${context.currentBranch} -> ${context.baseBranch}
|
|
359
|
-
|
|
360
|
-
Commits:
|
|
361
|
-
${context.commits.map((c) => `- ${c}`).join("\n")}
|
|
362
|
-
|
|
363
|
-
Diff summary (truncated):
|
|
364
|
-
\`\`\`
|
|
365
|
-
${context.diff.slice(0, 6e3)}
|
|
366
|
-
\`\`\`
|
|
367
|
-
${templateInstructions}
|
|
368
|
-
|
|
369
|
-
Rules for title:
|
|
370
|
-
- Title should be concise (under 72 chars), start with a verb
|
|
371
|
-
|
|
372
|
-
Respond in JSON format:
|
|
373
|
-
{
|
|
374
|
-
"title": "...",
|
|
375
|
-
"body": "..."
|
|
376
|
-
}`;
|
|
453
|
+
const prompt = applyTemplate(template, "pr", {
|
|
454
|
+
baseBranch: context.baseBranch,
|
|
455
|
+
currentBranch: context.currentBranch,
|
|
456
|
+
commits: context.commits.map((c) => `- ${c}`).join("\n"),
|
|
457
|
+
diff: context.diff.slice(0, 6e3)
|
|
458
|
+
});
|
|
377
459
|
const result = await generateText({
|
|
378
460
|
model,
|
|
379
461
|
prompt,
|
|
@@ -402,26 +484,15 @@ var CodeReviewSchema = z.object({
|
|
|
402
484
|
),
|
|
403
485
|
positives: z.array(z.string()).describe("Good practices observed")
|
|
404
486
|
});
|
|
405
|
-
async function generateCodeReview(diff, options) {
|
|
487
|
+
async function generateCodeReview(diff, options, template) {
|
|
406
488
|
const model = await getModel(options);
|
|
489
|
+
const prompt = applyTemplate(template, "review", {
|
|
490
|
+
diff: diff.slice(0, 1e4)
|
|
491
|
+
});
|
|
407
492
|
const result = await generateObject({
|
|
408
493
|
model,
|
|
409
494
|
schema: CodeReviewSchema,
|
|
410
|
-
prompt
|
|
411
|
-
|
|
412
|
-
Focus on:
|
|
413
|
-
- Bugs and potential issues
|
|
414
|
-
- Security vulnerabilities
|
|
415
|
-
- Performance concerns
|
|
416
|
-
- Code style and best practices
|
|
417
|
-
- Suggestions for improvement
|
|
418
|
-
|
|
419
|
-
Git diff:
|
|
420
|
-
\`\`\`
|
|
421
|
-
${diff.slice(0, 1e4)}
|
|
422
|
-
\`\`\`
|
|
423
|
-
|
|
424
|
-
Be constructive and specific. Include line numbers when possible.`
|
|
495
|
+
prompt
|
|
425
496
|
});
|
|
426
497
|
return result.object;
|
|
427
498
|
}
|
|
@@ -436,43 +507,19 @@ var ChangelogSchema = z.object({
|
|
|
436
507
|
),
|
|
437
508
|
summary: z.string().optional().describe("Brief summary of this release")
|
|
438
509
|
});
|
|
439
|
-
async function generateChangelog(context, options) {
|
|
510
|
+
async function generateChangelog(context, options, template) {
|
|
440
511
|
const model = await getModel(options);
|
|
441
|
-
const templateInstructions = context.template ? `
|
|
442
|
-
IMPORTANT: Follow this project's changelog format:
|
|
443
|
-
|
|
444
|
-
--- CHANGELOG TEMPLATE START ---
|
|
445
|
-
${context.template.slice(0, 2e3)}
|
|
446
|
-
--- CHANGELOG TEMPLATE END ---
|
|
447
|
-
|
|
448
|
-
Match the style, sections, and formatting of the existing changelog.` : `
|
|
449
|
-
Use Keep a Changelog format (https://keepachangelog.com/):
|
|
450
|
-
- Group changes by: Added, Changed, Deprecated, Removed, Fixed, Security
|
|
451
|
-
- Each item should be a concise description of the change
|
|
452
|
-
- Use past tense`;
|
|
453
512
|
const commitList = context.commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message} (${c.author})`).join("\n");
|
|
513
|
+
const prompt = applyTemplate(template, "changelog", {
|
|
514
|
+
fromRef: context.fromRef,
|
|
515
|
+
toRef: context.toRef,
|
|
516
|
+
commits: commitList,
|
|
517
|
+
diff: context.diff.slice(0, 8e3)
|
|
518
|
+
});
|
|
454
519
|
const result = await generateObject({
|
|
455
520
|
model,
|
|
456
521
|
schema: ChangelogSchema,
|
|
457
|
-
prompt
|
|
458
|
-
|
|
459
|
-
Generate a changelog entry for changes from ${context.fromRef} to ${context.toRef}.
|
|
460
|
-
|
|
461
|
-
Commits:
|
|
462
|
-
${commitList}
|
|
463
|
-
|
|
464
|
-
Diff summary (truncated):
|
|
465
|
-
\`\`\`
|
|
466
|
-
${context.diff.slice(0, 8e3)}
|
|
467
|
-
\`\`\`
|
|
468
|
-
${templateInstructions}
|
|
469
|
-
|
|
470
|
-
Focus on:
|
|
471
|
-
- User-facing changes and improvements
|
|
472
|
-
- Bug fixes and their impact
|
|
473
|
-
- Breaking changes (highlight these)
|
|
474
|
-
- Group related changes together
|
|
475
|
-
- Write for end users, not developers (unless it's a library)`
|
|
522
|
+
prompt
|
|
476
523
|
});
|
|
477
524
|
return result.object;
|
|
478
525
|
}
|
|
@@ -493,96 +540,55 @@ var ExplanationSchema = z.object({
|
|
|
493
540
|
impact: z.string().describe("What impact or role this has in the project"),
|
|
494
541
|
notes: z.array(z.string()).optional().describe("Important considerations or caveats")
|
|
495
542
|
});
|
|
496
|
-
async function generateExplanation(context, options,
|
|
543
|
+
async function generateExplanation(context, options, template) {
|
|
497
544
|
const model = await getModel(options);
|
|
498
|
-
const projectContextSection = projectContext ? `
|
|
499
|
-
IMPORTANT: Use this project context to provide more accurate explanations:
|
|
500
|
-
|
|
501
|
-
--- PROJECT CONTEXT START ---
|
|
502
|
-
${projectContext.slice(0, 4e3)}
|
|
503
|
-
--- PROJECT CONTEXT END ---
|
|
504
|
-
` : "";
|
|
505
545
|
if (context.type === "file-content") {
|
|
546
|
+
const prompt2 = applyTemplate(template, "explain-file", {
|
|
547
|
+
filePath: context.metadata.filePath || "",
|
|
548
|
+
content: context.content?.slice(0, 15e3) || ""
|
|
549
|
+
});
|
|
506
550
|
const result2 = await generateObject({
|
|
507
551
|
model,
|
|
508
552
|
schema: ExplanationSchema,
|
|
509
|
-
prompt:
|
|
510
|
-
${projectContextSection}
|
|
511
|
-
Analyze the following file and explain what it does, its purpose, and its role in a project.
|
|
512
|
-
|
|
513
|
-
File: ${context.metadata.filePath}
|
|
514
|
-
|
|
515
|
-
Content:
|
|
516
|
-
\`\`\`
|
|
517
|
-
${context.content?.slice(0, 15e3)}
|
|
518
|
-
\`\`\`
|
|
519
|
-
|
|
520
|
-
Focus on:
|
|
521
|
-
- What this file does (main functionality)
|
|
522
|
-
- Its purpose and role in the codebase
|
|
523
|
-
- Key functions, classes, or components it defines
|
|
524
|
-
- Dependencies and what it interacts with
|
|
525
|
-
- Any important patterns or architecture decisions
|
|
526
|
-
|
|
527
|
-
Explain in a way that helps someone quickly understand this file's purpose and how it fits into the larger codebase.`
|
|
553
|
+
prompt: prompt2
|
|
528
554
|
});
|
|
529
555
|
return result2.object;
|
|
530
556
|
}
|
|
531
557
|
let contextInfo;
|
|
532
558
|
let targetType;
|
|
533
559
|
if (context.type === "pr") {
|
|
534
|
-
contextInfo = `
|
|
535
|
-
Pull Request: #${context.metadata.prNumber}
|
|
560
|
+
contextInfo = `Pull Request: #${context.metadata.prNumber}
|
|
536
561
|
Title: ${context.title}
|
|
537
562
|
Branch: ${context.metadata.headBranch} -> ${context.metadata.baseBranch}
|
|
538
563
|
Commits:
|
|
539
|
-
${context.metadata.commits?.map((c) => `- ${c}`).join("\n") || "N/A"}
|
|
540
|
-
`;
|
|
564
|
+
${context.metadata.commits?.map((c) => `- ${c}`).join("\n") || "N/A"}`;
|
|
541
565
|
targetType = "pull request";
|
|
542
566
|
} else if (context.type === "file-history") {
|
|
543
|
-
contextInfo = `
|
|
544
|
-
File: ${context.metadata.filePath}
|
|
567
|
+
contextInfo = `File: ${context.metadata.filePath}
|
|
545
568
|
Recent commits:
|
|
546
569
|
${context.metadata.commits?.map((c) => `- ${c}`).join("\n") || "N/A"}
|
|
547
570
|
Latest author: ${context.metadata.author}
|
|
548
|
-
Latest date: ${context.metadata.date}
|
|
549
|
-
`;
|
|
571
|
+
Latest date: ${context.metadata.date}`;
|
|
550
572
|
targetType = "file changes";
|
|
551
573
|
} else if (context.type === "uncommitted" || context.type === "staged") {
|
|
552
|
-
contextInfo =
|
|
553
|
-
${context.type === "staged" ? "Staged changes (ready to commit)" : "Uncommitted changes (work in progress)"}
|
|
554
|
-
`;
|
|
574
|
+
contextInfo = context.type === "staged" ? "Staged changes (ready to commit)" : "Uncommitted changes (work in progress)";
|
|
555
575
|
targetType = context.type === "staged" ? "staged changes" : "uncommitted changes";
|
|
556
576
|
} else {
|
|
557
|
-
contextInfo = `
|
|
558
|
-
Commit: ${context.metadata.hash?.slice(0, 7)}
|
|
577
|
+
contextInfo = `Commit: ${context.metadata.hash?.slice(0, 7)}
|
|
559
578
|
Message: ${context.title}
|
|
560
579
|
Author: ${context.metadata.author}
|
|
561
|
-
Date: ${context.metadata.date}
|
|
562
|
-
`;
|
|
580
|
+
Date: ${context.metadata.date}`;
|
|
563
581
|
targetType = "commit";
|
|
564
582
|
}
|
|
583
|
+
const prompt = applyTemplate(template, "explain", {
|
|
584
|
+
targetType,
|
|
585
|
+
context: contextInfo,
|
|
586
|
+
diff: context.diff?.slice(0, 12e3) || ""
|
|
587
|
+
});
|
|
565
588
|
const result = await generateObject({
|
|
566
589
|
model,
|
|
567
590
|
schema: ExplanationSchema,
|
|
568
|
-
prompt
|
|
569
|
-
${projectContextSection}
|
|
570
|
-
Analyze the following ${targetType} and provide a comprehensive explanation.
|
|
571
|
-
|
|
572
|
-
${contextInfo}
|
|
573
|
-
|
|
574
|
-
Diff:
|
|
575
|
-
\`\`\`
|
|
576
|
-
${context.diff?.slice(0, 12e3)}
|
|
577
|
-
\`\`\`
|
|
578
|
-
|
|
579
|
-
Focus on:
|
|
580
|
-
- What the changes accomplish (not just what files changed)
|
|
581
|
-
- WHY these changes were likely made
|
|
582
|
-
- The broader context and purpose
|
|
583
|
-
- Any important implications or side effects
|
|
584
|
-
|
|
585
|
-
Explain in a way that helps someone understand not just the "what" but the "why" behind these changes.`
|
|
591
|
+
prompt
|
|
586
592
|
});
|
|
587
593
|
return result.object;
|
|
588
594
|
}
|
|
@@ -595,39 +601,18 @@ var CommitSearchSchema = z.object({
|
|
|
595
601
|
),
|
|
596
602
|
summary: z.string().optional().describe("Brief summary of the search results")
|
|
597
603
|
});
|
|
598
|
-
async function searchCommits(query, commits, options, maxResults = 5,
|
|
604
|
+
async function searchCommits(query, commits, options, maxResults = 5, template) {
|
|
599
605
|
const model = await getModel(options);
|
|
600
|
-
const projectContextSection = projectContext ? `
|
|
601
|
-
IMPORTANT: Use this project context to better understand the codebase:
|
|
602
|
-
|
|
603
|
-
--- PROJECT CONTEXT START ---
|
|
604
|
-
${projectContext.slice(0, 3e3)}
|
|
605
|
-
--- PROJECT CONTEXT END ---
|
|
606
|
-
` : "";
|
|
607
606
|
const commitList = commits.map((c) => `${c.hash.slice(0, 7)} | ${c.author} | ${c.date.split("T")[0]} | ${c.message.split("\n")[0]}`).join("\n");
|
|
607
|
+
const prompt = applyTemplate(template, "find", {
|
|
608
|
+
query,
|
|
609
|
+
commits: commitList,
|
|
610
|
+
maxResults: String(maxResults)
|
|
611
|
+
});
|
|
608
612
|
const result = await generateObject({
|
|
609
613
|
model,
|
|
610
614
|
schema: CommitSearchSchema,
|
|
611
|
-
prompt
|
|
612
|
-
${projectContextSection}
|
|
613
|
-
The user is looking for commits related to: "${query}"
|
|
614
|
-
|
|
615
|
-
Here are the commits to search through:
|
|
616
|
-
\`\`\`
|
|
617
|
-
${commitList}
|
|
618
|
-
\`\`\`
|
|
619
|
-
|
|
620
|
-
Find the commits that best match the user's query. Consider:
|
|
621
|
-
- Commit messages that mention similar concepts
|
|
622
|
-
- Related features, bug fixes, or changes
|
|
623
|
-
- Semantic similarity (e.g., "login" matches "authentication")
|
|
624
|
-
|
|
625
|
-
Return up to ${maxResults} matching commits, ordered by relevance (most relevant first).
|
|
626
|
-
Only include commits that actually match the query - if none match well, return an empty array.
|
|
627
|
-
|
|
628
|
-
For each match, provide:
|
|
629
|
-
- The commit hash (first 7 characters are fine)
|
|
630
|
-
- A brief reason explaining why this commit matches the query`
|
|
615
|
+
prompt
|
|
631
616
|
});
|
|
632
617
|
const enrichedMatches = result.object.matches.map((match) => {
|
|
633
618
|
const commit = commits.find((c) => c.hash.startsWith(match.hash));
|
|
@@ -655,35 +640,13 @@ For each match, provide:
|
|
|
655
640
|
summary: result.object.summary
|
|
656
641
|
};
|
|
657
642
|
}
|
|
658
|
-
async function generateBranchName(description, options, context) {
|
|
643
|
+
async function generateBranchName(description, options, context, template) {
|
|
659
644
|
const model = await getModel(options);
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
--- CONVENTION END ---
|
|
666
|
-
` : `
|
|
667
|
-
Rules:
|
|
668
|
-
- Use format: <type>/<short-description>
|
|
669
|
-
- Types: feature, fix, hotfix, chore, refactor, docs, test
|
|
670
|
-
- Use kebab-case for description
|
|
671
|
-
- Keep it short (under 50 chars total)
|
|
672
|
-
- No special characters except hyphens and slashes`;
|
|
673
|
-
const typeHint = context?.type ? `
|
|
674
|
-
Branch type: ${context.type}` : "";
|
|
675
|
-
const issueHint = context?.issue ? `
|
|
676
|
-
Include issue number: ${context.issue}` : "";
|
|
677
|
-
const prompt = `You are an expert at creating git branch names.
|
|
678
|
-
|
|
679
|
-
Generate a clean, descriptive branch name for the following:
|
|
680
|
-
|
|
681
|
-
Description: ${description}
|
|
682
|
-
${typeHint}
|
|
683
|
-
${issueHint}
|
|
684
|
-
${conventionInstructions}
|
|
685
|
-
|
|
686
|
-
Respond with ONLY the branch name, nothing else.`;
|
|
645
|
+
const prompt = applyTemplate(template, "branch", {
|
|
646
|
+
description,
|
|
647
|
+
type: context?.type,
|
|
648
|
+
issue: context?.issue
|
|
649
|
+
});
|
|
687
650
|
const result = await generateText({
|
|
688
651
|
model,
|
|
689
652
|
prompt,
|
|
@@ -691,24 +654,23 @@ Respond with ONLY the branch name, nothing else.`;
|
|
|
691
654
|
});
|
|
692
655
|
return result.text.trim().replace(/[^a-zA-Z0-9/_-]/g, "");
|
|
693
656
|
}
|
|
694
|
-
async function
|
|
657
|
+
async function generateBranchNameFromDiff(diff, options, template) {
|
|
695
658
|
const model = await getModel(options);
|
|
696
|
-
const prompt =
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
Respond with ONLY the stash name, nothing else.`;
|
|
659
|
+
const prompt = applyTemplate(template, "checkout", {
|
|
660
|
+
diff: diff.slice(0, 8e3)
|
|
661
|
+
});
|
|
662
|
+
const result = await generateText({
|
|
663
|
+
model,
|
|
664
|
+
prompt,
|
|
665
|
+
maxTokens: 100
|
|
666
|
+
});
|
|
667
|
+
return result.text.trim().replace(/[^a-zA-Z0-9/_-]/g, "");
|
|
668
|
+
}
|
|
669
|
+
async function generateStashName(diff, options, template) {
|
|
670
|
+
const model = await getModel(options);
|
|
671
|
+
const prompt = applyTemplate(template, "stash", {
|
|
672
|
+
diff: diff.slice(0, 4e3)
|
|
673
|
+
});
|
|
712
674
|
const result = await generateText({
|
|
713
675
|
model,
|
|
714
676
|
prompt,
|
|
@@ -716,63 +678,66 @@ Respond with ONLY the stash name, nothing else.`;
|
|
|
716
678
|
});
|
|
717
679
|
return result.text.trim();
|
|
718
680
|
}
|
|
719
|
-
|
|
681
|
+
var WorkSummarySchema = z.object({
|
|
682
|
+
title: z.string().describe("One-line title for the summary"),
|
|
683
|
+
overview: z.string().describe("Brief overview of what was accomplished"),
|
|
684
|
+
highlights: z.array(z.string()).describe("Key accomplishments or highlights"),
|
|
685
|
+
details: z.array(
|
|
686
|
+
z.object({
|
|
687
|
+
category: z.string().describe("Category (e.g., Feature, Bug Fix, Refactor)"),
|
|
688
|
+
items: z.array(z.string()).describe("List of items in this category")
|
|
689
|
+
})
|
|
690
|
+
),
|
|
691
|
+
stats: z.object({
|
|
692
|
+
commits: z.number(),
|
|
693
|
+
filesChanged: z.number().optional(),
|
|
694
|
+
additions: z.number().optional(),
|
|
695
|
+
deletions: z.number().optional()
|
|
696
|
+
}).optional()
|
|
697
|
+
});
|
|
698
|
+
async function generateWorkSummary(context, options, format = "custom", template) {
|
|
699
|
+
const model = await getModel(options);
|
|
700
|
+
const commitList = context.commits.map((c) => `- ${c.hash.slice(0, 7)} ${c.message.split("\n")[0]} (${c.date.split("T")[0]})`).join("\n");
|
|
701
|
+
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}` : ""}.`;
|
|
702
|
+
const period = `${context.since}${context.until ? ` to ${context.until}` : " to now"}`;
|
|
703
|
+
const prompt = applyTemplate(template, "summary", {
|
|
704
|
+
author: context.author,
|
|
705
|
+
period,
|
|
706
|
+
format: formatHint,
|
|
707
|
+
commits: commitList,
|
|
708
|
+
diff: context.diff?.slice(0, 6e3)
|
|
709
|
+
});
|
|
710
|
+
const result = await generateObject({
|
|
711
|
+
model,
|
|
712
|
+
schema: WorkSummarySchema,
|
|
713
|
+
prompt
|
|
714
|
+
});
|
|
715
|
+
return {
|
|
716
|
+
...result.object,
|
|
717
|
+
stats: {
|
|
718
|
+
commits: context.commits.length,
|
|
719
|
+
...result.object.stats
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
async function resolveConflict(conflictedContent, context, options, template) {
|
|
720
724
|
const model = await getModel(options);
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
` : `
|
|
728
|
-
Rules:
|
|
729
|
-
- Understand the intent of both changes
|
|
730
|
-
- Combine changes when both are valid additions
|
|
731
|
-
- Choose the more complete/correct version when they conflict
|
|
732
|
-
- Preserve all necessary functionality`;
|
|
725
|
+
const prompt = applyTemplate(template, "merge", {
|
|
726
|
+
filename: context.filename,
|
|
727
|
+
oursRef: context.oursRef,
|
|
728
|
+
theirsRef: context.theirsRef,
|
|
729
|
+
content: conflictedContent
|
|
730
|
+
});
|
|
733
731
|
const result = await generateObject({
|
|
734
732
|
model,
|
|
735
733
|
schema: ConflictResolutionSchema,
|
|
736
|
-
prompt
|
|
737
|
-
|
|
738
|
-
Analyze the following conflicted file and provide a resolution.
|
|
739
|
-
|
|
740
|
-
File: ${context.filename}
|
|
741
|
-
Merging: ${context.theirsRef} into ${context.oursRef}
|
|
742
|
-
|
|
743
|
-
Conflicted content:
|
|
744
|
-
\`\`\`
|
|
745
|
-
${conflictedContent}
|
|
746
|
-
\`\`\`
|
|
747
|
-
${strategyInstructions}
|
|
748
|
-
|
|
749
|
-
Additional rules:
|
|
750
|
-
- The resolved content should be valid, working code
|
|
751
|
-
- Do NOT include conflict markers (<<<<<<, =======, >>>>>>)
|
|
752
|
-
|
|
753
|
-
Provide the fully resolved file content.`
|
|
734
|
+
prompt
|
|
754
735
|
});
|
|
755
736
|
return result.object;
|
|
756
737
|
}
|
|
757
738
|
|
|
758
|
-
// src/commands/
|
|
759
|
-
var
|
|
760
|
-
".gut/commit-convention.md",
|
|
761
|
-
".github/commit-convention.md",
|
|
762
|
-
".commit-convention.md",
|
|
763
|
-
"docs/commit-convention.md",
|
|
764
|
-
".gitmessage"
|
|
765
|
-
];
|
|
766
|
-
function findCommitConvention(repoRoot) {
|
|
767
|
-
for (const conventionPath of CONVENTION_PATHS) {
|
|
768
|
-
const fullPath = join(repoRoot, conventionPath);
|
|
769
|
-
if (existsSync(fullPath)) {
|
|
770
|
-
return readFileSync(fullPath, "utf-8");
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
return null;
|
|
774
|
-
}
|
|
775
|
-
var aiCommitCommand = new Command3("ai-commit").alias("commit").description("Generate a commit message using AI").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-c, --commit", "Automatically commit with the generated message").option("-a, --all", "Force stage all changes (default: auto-stage if nothing staged)").action(async (options) => {
|
|
739
|
+
// src/commands/commit.ts
|
|
740
|
+
var commitCommand = new Command3("commit").description("Generate a commit message using AI").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-c, --commit", "Automatically commit with the generated message").option("-a, --all", "Force stage all changes (default: auto-stage if nothing staged)").action(async (options) => {
|
|
776
741
|
const git = simpleGit2();
|
|
777
742
|
const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
|
|
778
743
|
const isRepo = await git.checkIsRepo();
|
|
@@ -786,8 +751,10 @@ var aiCommitCommand = new Command3("ai-commit").alias("commit").description("Gen
|
|
|
786
751
|
}
|
|
787
752
|
let diff = await git.diff(["--cached"]);
|
|
788
753
|
if (!diff.trim()) {
|
|
754
|
+
const status = await git.status();
|
|
789
755
|
const unstaged = await git.diff();
|
|
790
|
-
|
|
756
|
+
const hasUntracked = status.not_added.length > 0 || status.created.length > 0;
|
|
757
|
+
if (!unstaged.trim() && !hasUntracked) {
|
|
791
758
|
console.error(chalk3.yellow("No changes to commit."));
|
|
792
759
|
process.exit(1);
|
|
793
760
|
}
|
|
@@ -795,16 +762,16 @@ var aiCommitCommand = new Command3("ai-commit").alias("commit").description("Gen
|
|
|
795
762
|
await git.add("-A");
|
|
796
763
|
diff = await git.diff(["--cached"]);
|
|
797
764
|
}
|
|
798
|
-
const
|
|
799
|
-
if (
|
|
800
|
-
console.log(chalk3.gray("Using
|
|
765
|
+
const template = findTemplate(repoRoot.trim(), "commit");
|
|
766
|
+
if (template) {
|
|
767
|
+
console.log(chalk3.gray("Using template from project..."));
|
|
801
768
|
}
|
|
802
769
|
const spinner = ora2("Generating commit message...").start();
|
|
803
770
|
try {
|
|
804
771
|
const message = await generateCommitMessage(
|
|
805
772
|
diff,
|
|
806
773
|
{ provider, model: options.model },
|
|
807
|
-
|
|
774
|
+
template || void 0
|
|
808
775
|
);
|
|
809
776
|
spinner.stop();
|
|
810
777
|
console.log(chalk3.bold("\nGenerated commit message:\n"));
|
|
@@ -832,14 +799,14 @@ var aiCommitCommand = new Command3("ai-commit").alias("commit").description("Gen
|
|
|
832
799
|
console.log(chalk3.green("\u2713 Committed successfully"));
|
|
833
800
|
} else if (answer.toLowerCase() === "e") {
|
|
834
801
|
console.log(chalk3.gray("Opening editor..."));
|
|
835
|
-
const { execSync:
|
|
802
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
836
803
|
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
837
804
|
const fs2 = await import("fs");
|
|
838
805
|
const os = await import("os");
|
|
839
806
|
const path2 = await import("path");
|
|
840
807
|
const tmpFile = path2.join(os.tmpdir(), "gut-commit-msg.txt");
|
|
841
808
|
fs2.writeFileSync(tmpFile, message);
|
|
842
|
-
|
|
809
|
+
execSync5(`${editor} "${tmpFile}"`, { stdio: "inherit" });
|
|
843
810
|
const editedMessage = fs2.readFileSync(tmpFile, "utf-8").trim();
|
|
844
811
|
fs2.unlinkSync(tmpFile);
|
|
845
812
|
if (editedMessage) {
|
|
@@ -861,15 +828,14 @@ var aiCommitCommand = new Command3("ai-commit").alias("commit").description("Gen
|
|
|
861
828
|
}
|
|
862
829
|
});
|
|
863
830
|
|
|
864
|
-
// src/commands/
|
|
831
|
+
// src/commands/pr.ts
|
|
865
832
|
import { Command as Command4 } from "commander";
|
|
866
833
|
import chalk4 from "chalk";
|
|
867
834
|
import ora3 from "ora";
|
|
868
835
|
import { simpleGit as simpleGit3 } from "simple-git";
|
|
869
|
-
import { existsSync as
|
|
870
|
-
import { join as
|
|
871
|
-
var
|
|
872
|
-
".gut/pr-template.md",
|
|
836
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
837
|
+
import { join as join3 } from "path";
|
|
838
|
+
var GITHUB_PR_TEMPLATE_PATHS = [
|
|
873
839
|
".github/pull_request_template.md",
|
|
874
840
|
".github/PULL_REQUEST_TEMPLATE.md",
|
|
875
841
|
"pull_request_template.md",
|
|
@@ -877,15 +843,15 @@ var PR_TEMPLATE_PATHS = [
|
|
|
877
843
|
"docs/pull_request_template.md"
|
|
878
844
|
];
|
|
879
845
|
function findPRTemplate(repoRoot) {
|
|
880
|
-
for (const templatePath of
|
|
881
|
-
const fullPath =
|
|
882
|
-
if (
|
|
883
|
-
return
|
|
846
|
+
for (const templatePath of GITHUB_PR_TEMPLATE_PATHS) {
|
|
847
|
+
const fullPath = join3(repoRoot, templatePath);
|
|
848
|
+
if (existsSync3(fullPath)) {
|
|
849
|
+
return readFileSync3(fullPath, "utf-8");
|
|
884
850
|
}
|
|
885
851
|
}
|
|
886
|
-
return
|
|
852
|
+
return findTemplate(repoRoot, "pr");
|
|
887
853
|
}
|
|
888
|
-
var
|
|
854
|
+
var prCommand = new Command4("pr").description("Generate a pull request title and description using AI").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").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) => {
|
|
889
855
|
const git = simpleGit3();
|
|
890
856
|
const isRepo = await git.checkIsRepo();
|
|
891
857
|
if (!isRepo) {
|
|
@@ -931,13 +897,10 @@ var aiPrCommand = new Command4("ai-pr").alias("pr").description("Generate a pull
|
|
|
931
897
|
baseBranch,
|
|
932
898
|
currentBranch,
|
|
933
899
|
commits,
|
|
934
|
-
diff
|
|
935
|
-
template: template || void 0
|
|
900
|
+
diff
|
|
936
901
|
},
|
|
937
|
-
{
|
|
938
|
-
|
|
939
|
-
model: options.model
|
|
940
|
-
}
|
|
902
|
+
{ provider, model: options.model },
|
|
903
|
+
template || void 0
|
|
941
904
|
);
|
|
942
905
|
spinner.stop();
|
|
943
906
|
console.log(chalk4.bold("\n\u{1F4DD} Generated PR:\n"));
|
|
@@ -948,11 +911,11 @@ var aiPrCommand = new Command4("ai-pr").alias("pr").description("Generate a pull
|
|
|
948
911
|
console.log(chalk4.gray("\u2500".repeat(50)));
|
|
949
912
|
if (options.copy) {
|
|
950
913
|
try {
|
|
951
|
-
const { execSync:
|
|
914
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
952
915
|
const fullText = `${title}
|
|
953
916
|
|
|
954
917
|
${body}`;
|
|
955
|
-
|
|
918
|
+
execSync5("pbcopy", { input: fullText });
|
|
956
919
|
console.log(chalk4.green("\n\u2713 Copied to clipboard"));
|
|
957
920
|
} catch {
|
|
958
921
|
console.log(chalk4.yellow("\n\u26A0 Could not copy to clipboard"));
|
|
@@ -971,10 +934,10 @@ ${body}`;
|
|
|
971
934
|
if (answer.toLowerCase() === "y") {
|
|
972
935
|
const createSpinner = ora3("Creating PR...").start();
|
|
973
936
|
try {
|
|
974
|
-
const { execSync:
|
|
937
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
975
938
|
const escapedTitle = title.replace(/"/g, '\\"');
|
|
976
939
|
const escapedBody = body.replace(/"/g, '\\"');
|
|
977
|
-
|
|
940
|
+
execSync5(
|
|
978
941
|
`gh pr create --title "${escapedTitle}" --body "${escapedBody}" --base ${baseBranch}`,
|
|
979
942
|
{ stdio: "pipe" }
|
|
980
943
|
);
|
|
@@ -997,16 +960,16 @@ ${body}`;
|
|
|
997
960
|
}
|
|
998
961
|
});
|
|
999
962
|
|
|
1000
|
-
// src/commands/
|
|
963
|
+
// src/commands/review.ts
|
|
1001
964
|
import { Command as Command5 } from "commander";
|
|
1002
965
|
import chalk5 from "chalk";
|
|
1003
966
|
import ora4 from "ora";
|
|
1004
967
|
import { simpleGit as simpleGit4 } from "simple-git";
|
|
1005
|
-
import { execSync } from "child_process";
|
|
968
|
+
import { execSync as execSync2 } from "child_process";
|
|
1006
969
|
async function getPRDiff(prNumber) {
|
|
1007
970
|
try {
|
|
1008
|
-
const diff =
|
|
1009
|
-
const prJsonStr =
|
|
971
|
+
const diff = execSync2(`gh pr diff ${prNumber}`, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
|
|
972
|
+
const prJsonStr = execSync2(`gh pr view ${prNumber} --json number,title,author,url`, { encoding: "utf-8" });
|
|
1010
973
|
const prJson = JSON.parse(prJsonStr);
|
|
1011
974
|
return {
|
|
1012
975
|
diff,
|
|
@@ -1024,7 +987,7 @@ async function getPRDiff(prNumber) {
|
|
|
1024
987
|
throw error;
|
|
1025
988
|
}
|
|
1026
989
|
}
|
|
1027
|
-
var
|
|
990
|
+
var reviewCommand = new Command5("review").description("Get an AI code review of your changes or a GitHub PR").argument("[pr-number]", "GitHub PR number to review").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-s, --staged", "Review only staged changes").option("-c, --commit <hash>", "Review a specific commit").option("--json", "Output as JSON").action(async (prNumber, options) => {
|
|
1028
991
|
const git = simpleGit4();
|
|
1029
992
|
const isRepo = await git.checkIsRepo();
|
|
1030
993
|
if (!isRepo) {
|
|
@@ -1059,10 +1022,13 @@ var aiReviewCommand = new Command5("ai-review").alias("review").description("Get
|
|
|
1059
1022
|
process.exit(0);
|
|
1060
1023
|
}
|
|
1061
1024
|
spinner.text = "AI is reviewing your code...";
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1025
|
+
const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
|
|
1026
|
+
const template = findTemplate(repoRoot.trim(), "review");
|
|
1027
|
+
const review = await generateCodeReview(
|
|
1028
|
+
diff,
|
|
1029
|
+
{ provider, model: options.model },
|
|
1030
|
+
template || void 0
|
|
1031
|
+
);
|
|
1066
1032
|
spinner.stop();
|
|
1067
1033
|
if (options.json) {
|
|
1068
1034
|
console.log(JSON.stringify({ prInfo, review }, null, 2));
|
|
@@ -1127,27 +1093,14 @@ function printReview(review) {
|
|
|
1127
1093
|
console.log();
|
|
1128
1094
|
}
|
|
1129
1095
|
|
|
1130
|
-
// src/commands/
|
|
1096
|
+
// src/commands/merge.ts
|
|
1131
1097
|
import { Command as Command6 } from "commander";
|
|
1132
1098
|
import chalk6 from "chalk";
|
|
1133
1099
|
import ora5 from "ora";
|
|
1134
1100
|
import { simpleGit as simpleGit5 } from "simple-git";
|
|
1135
1101
|
import * as fs from "fs";
|
|
1136
1102
|
import * as path from "path";
|
|
1137
|
-
var
|
|
1138
|
-
".gut/merge-strategy.md",
|
|
1139
|
-
".github/merge-strategy.md"
|
|
1140
|
-
];
|
|
1141
|
-
function findMergeStrategy(repoRoot) {
|
|
1142
|
-
for (const strategyPath of MERGE_STRATEGY_PATHS) {
|
|
1143
|
-
const fullPath = path.join(repoRoot, strategyPath);
|
|
1144
|
-
if (fs.existsSync(fullPath)) {
|
|
1145
|
-
return fs.readFileSync(fullPath, "utf-8");
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
return null;
|
|
1149
|
-
}
|
|
1150
|
-
var aiMergeCommand = new Command6("ai-merge").alias("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) => {
|
|
1103
|
+
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) => {
|
|
1151
1104
|
const git = simpleGit5();
|
|
1152
1105
|
const isRepo = await git.checkIsRepo();
|
|
1153
1106
|
if (!isRepo) {
|
|
@@ -1183,9 +1136,9 @@ Merging ${chalk6.cyan(branch)} into ${chalk6.cyan(currentBranch)}...
|
|
|
1183
1136
|
`));
|
|
1184
1137
|
const spinner = ora5();
|
|
1185
1138
|
const rootDir = await git.revparse(["--show-toplevel"]);
|
|
1186
|
-
const
|
|
1187
|
-
if (
|
|
1188
|
-
console.log(chalk6.gray("Using merge
|
|
1139
|
+
const template = findTemplate(rootDir.trim(), "merge");
|
|
1140
|
+
if (template) {
|
|
1141
|
+
console.log(chalk6.gray("Using merge template from project...\n"));
|
|
1189
1142
|
}
|
|
1190
1143
|
for (const file of conflictedFiles) {
|
|
1191
1144
|
const filePath = path.join(rootDir.trim(), file);
|
|
@@ -1205,7 +1158,7 @@ Merging ${chalk6.cyan(branch)} into ${chalk6.cyan(currentBranch)}...
|
|
|
1205
1158
|
filename: file,
|
|
1206
1159
|
oursRef: currentBranch,
|
|
1207
1160
|
theirsRef: branch
|
|
1208
|
-
}, { provider, model: options.model },
|
|
1161
|
+
}, { provider, model: options.model }, template || void 0);
|
|
1209
1162
|
spinner.stop();
|
|
1210
1163
|
console.log(chalk6.cyan("\n\u{1F916} AI suggests:"));
|
|
1211
1164
|
console.log(chalk6.gray("\u2500".repeat(50)));
|
|
@@ -1258,26 +1211,6 @@ import { Command as Command7 } from "commander";
|
|
|
1258
1211
|
import chalk7 from "chalk";
|
|
1259
1212
|
import ora6 from "ora";
|
|
1260
1213
|
import { simpleGit as simpleGit6 } from "simple-git";
|
|
1261
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
1262
|
-
import { join as join4 } from "path";
|
|
1263
|
-
var CHANGELOG_PATHS = [
|
|
1264
|
-
".gut/changelog-template.md",
|
|
1265
|
-
".gut/CHANGELOG.md",
|
|
1266
|
-
"CHANGELOG.md",
|
|
1267
|
-
"HISTORY.md",
|
|
1268
|
-
"CHANGES.md",
|
|
1269
|
-
"changelog.md",
|
|
1270
|
-
"docs/CHANGELOG.md"
|
|
1271
|
-
];
|
|
1272
|
-
function findChangelog(repoRoot) {
|
|
1273
|
-
for (const changelogPath of CHANGELOG_PATHS) {
|
|
1274
|
-
const fullPath = join4(repoRoot, changelogPath);
|
|
1275
|
-
if (existsSync4(fullPath)) {
|
|
1276
|
-
return readFileSync4(fullPath, "utf-8");
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
return null;
|
|
1280
|
-
}
|
|
1281
1214
|
function formatChangelog(changelog) {
|
|
1282
1215
|
const lines = [];
|
|
1283
1216
|
const header = changelog.version ? `## [${changelog.version}] - ${changelog.date}` : `## ${changelog.date}`;
|
|
@@ -1328,24 +1261,14 @@ var changelogCommand = new Command7("changelog").description("Generate a changel
|
|
|
1328
1261
|
}));
|
|
1329
1262
|
const diff = await git.diff([`${fromRef}...${toRef}`]);
|
|
1330
1263
|
const repoRoot = await git.revparse(["--show-toplevel"]);
|
|
1331
|
-
const
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
const firstEntryMatch = existingChangelog.match(/## \[?[\d.]+\]?[\s\S]*?(?=## \[?[\d.]+\]?|$)/);
|
|
1335
|
-
if (firstEntryMatch) {
|
|
1336
|
-
template = firstEntryMatch[0].slice(0, 1500);
|
|
1337
|
-
}
|
|
1338
|
-
spinner.text = "Found existing changelog, matching style...";
|
|
1264
|
+
const template = findTemplate(repoRoot.trim(), "changelog");
|
|
1265
|
+
if (template) {
|
|
1266
|
+
spinner.text = "Using template from project...";
|
|
1339
1267
|
}
|
|
1340
1268
|
const changelog = await generateChangelog(
|
|
1341
|
-
{
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
fromRef,
|
|
1345
|
-
toRef,
|
|
1346
|
-
template
|
|
1347
|
-
},
|
|
1348
|
-
{ provider, model: options.model }
|
|
1269
|
+
{ commits, diff, fromRef, toRef },
|
|
1270
|
+
{ provider, model: options.model },
|
|
1271
|
+
template || void 0
|
|
1349
1272
|
);
|
|
1350
1273
|
spinner.stop();
|
|
1351
1274
|
if (options.json) {
|
|
@@ -1358,7 +1281,7 @@ var changelogCommand = new Command7("changelog").description("Generate a changel
|
|
|
1358
1281
|
console.log(chalk7.gray("\u2500".repeat(50)));
|
|
1359
1282
|
console.log(chalk7.gray(`
|
|
1360
1283
|
Range: ${fromRef}..${toRef} (${commits.length} commits)`));
|
|
1361
|
-
if (
|
|
1284
|
+
if (template) {
|
|
1362
1285
|
console.log(chalk7.gray("Style matched from existing CHANGELOG.md"));
|
|
1363
1286
|
}
|
|
1364
1287
|
} catch (error) {
|
|
@@ -1368,27 +1291,14 @@ Range: ${fromRef}..${toRef} (${commits.length} commits)`));
|
|
|
1368
1291
|
}
|
|
1369
1292
|
});
|
|
1370
1293
|
|
|
1371
|
-
// src/commands/
|
|
1294
|
+
// src/commands/explain.ts
|
|
1372
1295
|
import { Command as Command8 } from "commander";
|
|
1373
1296
|
import chalk8 from "chalk";
|
|
1374
1297
|
import ora7 from "ora";
|
|
1375
1298
|
import { simpleGit as simpleGit7 } from "simple-git";
|
|
1376
|
-
import { execSync as
|
|
1377
|
-
import { existsSync as
|
|
1378
|
-
|
|
1379
|
-
var CONTEXT_PATHS = [
|
|
1380
|
-
".gut/explain.md"
|
|
1381
|
-
];
|
|
1382
|
-
function findExplainContext(repoRoot) {
|
|
1383
|
-
for (const contextPath of CONTEXT_PATHS) {
|
|
1384
|
-
const fullPath = join5(repoRoot, contextPath);
|
|
1385
|
-
if (existsSync5(fullPath)) {
|
|
1386
|
-
return readFileSync5(fullPath, "utf-8");
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
return null;
|
|
1390
|
-
}
|
|
1391
|
-
var aiExplainCommand = new Command8("ai-explain").alias("explain").description("Get an AI-powered explanation of changes, commits, PRs, or files").argument("[target]", "Commit hash, PR number, PR URL, or file path (default: uncommitted changes)").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").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) => {
|
|
1299
|
+
import { execSync as execSync3 } from "child_process";
|
|
1300
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
1301
|
+
var explainCommand = new Command8("explain").description("Get an AI-powered explanation of changes, commits, PRs, or files").argument("[target]", "Commit hash, PR number, PR URL, or file path (default: uncommitted changes)").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").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) => {
|
|
1392
1302
|
const git = simpleGit7();
|
|
1393
1303
|
const isRepo = await git.checkIsRepo();
|
|
1394
1304
|
if (!isRepo) {
|
|
@@ -1408,7 +1318,7 @@ var aiExplainCommand = new Command8("ai-explain").alias("explain").description("
|
|
|
1408
1318
|
}
|
|
1409
1319
|
} else {
|
|
1410
1320
|
const isPR = target.match(/^#?\d+$/) || target.includes("/pull/");
|
|
1411
|
-
const isFile =
|
|
1321
|
+
const isFile = existsSync4(target);
|
|
1412
1322
|
if (isPR) {
|
|
1413
1323
|
context = await getPRContext(target, spinner);
|
|
1414
1324
|
} else if (isFile) {
|
|
@@ -1421,15 +1331,18 @@ var aiExplainCommand = new Command8("ai-explain").alias("explain").description("
|
|
|
1421
1331
|
context = await getCommitContext(target, git, spinner);
|
|
1422
1332
|
}
|
|
1423
1333
|
}
|
|
1424
|
-
const
|
|
1425
|
-
|
|
1426
|
-
|
|
1334
|
+
const isFileContent = context.type === "file-content";
|
|
1335
|
+
const templateName = isFileContent ? "explain-file" : "explain";
|
|
1336
|
+
const template = findTemplate(repoRoot.trim(), templateName);
|
|
1337
|
+
if (template) {
|
|
1338
|
+
console.log(chalk8.gray("Using template from project..."));
|
|
1427
1339
|
}
|
|
1428
1340
|
spinner.text = "AI is generating explanation...";
|
|
1429
|
-
const explanation = await generateExplanation(
|
|
1430
|
-
|
|
1431
|
-
model: options.model
|
|
1432
|
-
|
|
1341
|
+
const explanation = await generateExplanation(
|
|
1342
|
+
context,
|
|
1343
|
+
{ provider, model: options.model },
|
|
1344
|
+
template || void 0
|
|
1345
|
+
);
|
|
1433
1346
|
spinner.stop();
|
|
1434
1347
|
if (options.json) {
|
|
1435
1348
|
console.log(JSON.stringify(explanation, null, 2));
|
|
@@ -1542,7 +1455,7 @@ async function getPRContext(target, spinner) {
|
|
|
1542
1455
|
spinner.text = `Fetching PR #${prNumber}...`;
|
|
1543
1456
|
let prInfo;
|
|
1544
1457
|
try {
|
|
1545
|
-
const prJson =
|
|
1458
|
+
const prJson = execSync3(
|
|
1546
1459
|
`gh pr view ${prNumber} --json title,url,baseRefName,headRefName,commits`,
|
|
1547
1460
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
1548
1461
|
);
|
|
@@ -1553,7 +1466,7 @@ async function getPRContext(target, spinner) {
|
|
|
1553
1466
|
spinner.text = `Getting diff for PR #${prNumber}...`;
|
|
1554
1467
|
let diff;
|
|
1555
1468
|
try {
|
|
1556
|
-
diff =
|
|
1469
|
+
diff = execSync3(`gh pr diff ${prNumber}`, {
|
|
1557
1470
|
encoding: "utf-8",
|
|
1558
1471
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1559
1472
|
maxBuffer: 10 * 1024 * 1024
|
|
@@ -1615,24 +1528,12 @@ ${icon} Explanation
|
|
|
1615
1528
|
}
|
|
1616
1529
|
}
|
|
1617
1530
|
|
|
1618
|
-
// src/commands/
|
|
1531
|
+
// src/commands/find.ts
|
|
1619
1532
|
import { Command as Command9 } from "commander";
|
|
1620
1533
|
import chalk9 from "chalk";
|
|
1621
1534
|
import ora8 from "ora";
|
|
1622
1535
|
import { simpleGit as simpleGit8 } from "simple-git";
|
|
1623
|
-
|
|
1624
|
-
import { join as join6 } from "path";
|
|
1625
|
-
var CONTEXT_PATHS2 = [".gut/find.md"];
|
|
1626
|
-
function findProjectContext(repoRoot) {
|
|
1627
|
-
for (const contextPath of CONTEXT_PATHS2) {
|
|
1628
|
-
const fullPath = join6(repoRoot, contextPath);
|
|
1629
|
-
if (existsSync6(fullPath)) {
|
|
1630
|
-
return readFileSync6(fullPath, "utf-8");
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
return null;
|
|
1634
|
-
}
|
|
1635
|
-
var aiFindCommand = new Command9("ai-find").alias("find").description("Find commits matching a vague description using AI").argument("<query>", 'Description of the change you are looking for (e.g., "login feature added")').option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").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) => {
|
|
1536
|
+
var findCommand = new Command9("find").description("Find commits matching a vague description using AI").argument("<query>", 'Description of the change you are looking for (e.g., "login feature added")').option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").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) => {
|
|
1636
1537
|
const git = simpleGit8();
|
|
1637
1538
|
const isRepo = await git.checkIsRepo();
|
|
1638
1539
|
if (!isRepo) {
|
|
@@ -1670,16 +1571,13 @@ var aiFindCommand = new Command9("ai-find").alias("find").description("Find comm
|
|
|
1670
1571
|
email: c.author_email,
|
|
1671
1572
|
date: c.date
|
|
1672
1573
|
}));
|
|
1673
|
-
const
|
|
1574
|
+
const template = findTemplate(repoRoot.trim(), "find");
|
|
1674
1575
|
const results = await searchCommits(
|
|
1675
1576
|
query,
|
|
1676
1577
|
commits,
|
|
1677
|
-
{
|
|
1678
|
-
provider,
|
|
1679
|
-
model: options.model
|
|
1680
|
-
},
|
|
1578
|
+
{ provider, model: options.model },
|
|
1681
1579
|
parseInt(options.maxResults, 10),
|
|
1682
|
-
|
|
1580
|
+
template || void 0
|
|
1683
1581
|
);
|
|
1684
1582
|
spinner.stop();
|
|
1685
1583
|
if (results.matches.length === 0) {
|
|
@@ -1725,27 +1623,15 @@ function printResults(results, query) {
|
|
|
1725
1623
|
}
|
|
1726
1624
|
}
|
|
1727
1625
|
|
|
1728
|
-
// src/commands/
|
|
1626
|
+
// src/commands/branch.ts
|
|
1729
1627
|
import { Command as Command10 } from "commander";
|
|
1730
1628
|
import chalk10 from "chalk";
|
|
1731
1629
|
import ora9 from "ora";
|
|
1732
1630
|
import { simpleGit as simpleGit9 } from "simple-git";
|
|
1733
|
-
import {
|
|
1734
|
-
import { join as join7 } from "path";
|
|
1735
|
-
import { execSync as execSync3 } from "child_process";
|
|
1736
|
-
var CONVENTION_PATHS2 = [".gut/branch-convention.md", ".github/branch-convention.md"];
|
|
1737
|
-
function findBranchConvention(repoRoot) {
|
|
1738
|
-
for (const conventionPath of CONVENTION_PATHS2) {
|
|
1739
|
-
const fullPath = join7(repoRoot, conventionPath);
|
|
1740
|
-
if (existsSync7(fullPath)) {
|
|
1741
|
-
return readFileSync7(fullPath, "utf-8");
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
return null;
|
|
1745
|
-
}
|
|
1631
|
+
import { execSync as execSync4 } from "child_process";
|
|
1746
1632
|
function getIssueInfo(issueNumber) {
|
|
1747
1633
|
try {
|
|
1748
|
-
const result =
|
|
1634
|
+
const result = execSync4(`gh issue view ${issueNumber} --json title,body`, {
|
|
1749
1635
|
encoding: "utf-8",
|
|
1750
1636
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1751
1637
|
});
|
|
@@ -1754,7 +1640,7 @@ function getIssueInfo(issueNumber) {
|
|
|
1754
1640
|
return null;
|
|
1755
1641
|
}
|
|
1756
1642
|
}
|
|
1757
|
-
var
|
|
1643
|
+
var branchCommand = new Command10("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)", "gemini").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) => {
|
|
1758
1644
|
const git = simpleGit9();
|
|
1759
1645
|
const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
|
|
1760
1646
|
const isRepo = await git.checkIsRepo();
|
|
@@ -1788,20 +1674,17 @@ ${issueInfo.body || ""}`;
|
|
|
1788
1674
|
process.exit(1);
|
|
1789
1675
|
}
|
|
1790
1676
|
const provider = options.provider.toLowerCase();
|
|
1791
|
-
const
|
|
1792
|
-
if (
|
|
1793
|
-
console.log(chalk10.gray("Using
|
|
1677
|
+
const template = findTemplate(repoRoot.trim(), "branch");
|
|
1678
|
+
if (template) {
|
|
1679
|
+
console.log(chalk10.gray("Using template from project..."));
|
|
1794
1680
|
}
|
|
1795
1681
|
const spinner = ora9("Generating branch name...").start();
|
|
1796
1682
|
try {
|
|
1797
1683
|
const branchName = await generateBranchName(
|
|
1798
1684
|
description,
|
|
1799
1685
|
{ provider, model: options.model },
|
|
1800
|
-
{
|
|
1801
|
-
|
|
1802
|
-
issue: issueNumber,
|
|
1803
|
-
convention
|
|
1804
|
-
}
|
|
1686
|
+
{ type: options.type, issue: issueNumber },
|
|
1687
|
+
template || void 0
|
|
1805
1688
|
);
|
|
1806
1689
|
spinner.stop();
|
|
1807
1690
|
console.log(chalk10.bold("\nGenerated branch name:\n"));
|
|
@@ -1835,33 +1718,111 @@ ${issueInfo.body || ""}`;
|
|
|
1835
1718
|
}
|
|
1836
1719
|
});
|
|
1837
1720
|
|
|
1838
|
-
// src/commands/
|
|
1721
|
+
// src/commands/checkout.ts
|
|
1839
1722
|
import { Command as Command11 } from "commander";
|
|
1840
1723
|
import chalk11 from "chalk";
|
|
1841
1724
|
import ora10 from "ora";
|
|
1842
1725
|
import { simpleGit as simpleGit10 } from "simple-git";
|
|
1843
|
-
var
|
|
1726
|
+
var checkoutCommand = new Command11("checkout").description("Generate a branch name from current diff and checkout").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").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) => {
|
|
1844
1727
|
const git = simpleGit10();
|
|
1845
1728
|
const isRepo = await git.checkIsRepo();
|
|
1846
1729
|
if (!isRepo) {
|
|
1847
1730
|
console.error(chalk11.red("Error: Not a git repository"));
|
|
1848
1731
|
process.exit(1);
|
|
1849
1732
|
}
|
|
1850
|
-
const
|
|
1733
|
+
const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
|
|
1734
|
+
const spinner = ora10("Analyzing changes...").start();
|
|
1735
|
+
const status = await git.status();
|
|
1736
|
+
let diff;
|
|
1737
|
+
if (options.staged) {
|
|
1738
|
+
diff = await git.diff(["--cached"]);
|
|
1739
|
+
} else {
|
|
1740
|
+
const stagedDiff = await git.diff(["--cached"]);
|
|
1741
|
+
const unstagedDiff = await git.diff();
|
|
1742
|
+
diff = stagedDiff + "\n" + unstagedDiff;
|
|
1743
|
+
}
|
|
1744
|
+
const hasChanges = diff.trim() || status.not_added.length > 0 || status.created.length > 0;
|
|
1745
|
+
if (!hasChanges) {
|
|
1746
|
+
spinner.fail("No changes found");
|
|
1747
|
+
console.log(chalk11.gray("Make some changes first, then run gut checkout"));
|
|
1748
|
+
process.exit(1);
|
|
1749
|
+
}
|
|
1750
|
+
if (!diff.trim() && (status.not_added.length > 0 || status.created.length > 0)) {
|
|
1751
|
+
const untrackedFiles = [...status.not_added, ...status.created];
|
|
1752
|
+
diff = `New files:
|
|
1753
|
+
${untrackedFiles.map((f) => `+ ${f}`).join("\n")}`;
|
|
1754
|
+
}
|
|
1755
|
+
spinner.text = "Generating branch name...";
|
|
1756
|
+
const provider = options.provider.toLowerCase();
|
|
1757
|
+
const template = findTemplate(repoRoot.trim(), "checkout");
|
|
1758
|
+
if (template) {
|
|
1759
|
+
console.log(chalk11.gray("\nUsing template from project..."));
|
|
1760
|
+
}
|
|
1761
|
+
try {
|
|
1762
|
+
const branchName = await generateBranchNameFromDiff(
|
|
1763
|
+
diff,
|
|
1764
|
+
{ provider, model: options.model },
|
|
1765
|
+
template
|
|
1766
|
+
);
|
|
1767
|
+
spinner.stop();
|
|
1768
|
+
console.log(chalk11.bold("\nGenerated branch name:\n"));
|
|
1769
|
+
console.log(chalk11.green(` ${branchName}`));
|
|
1770
|
+
console.log();
|
|
1771
|
+
if (options.yes) {
|
|
1772
|
+
await git.checkoutLocalBranch(branchName);
|
|
1773
|
+
console.log(chalk11.green(`\u2713 Created and checked out branch: ${branchName}`));
|
|
1774
|
+
} else {
|
|
1775
|
+
const readline = await import("readline");
|
|
1776
|
+
const rl = readline.createInterface({
|
|
1777
|
+
input: process.stdin,
|
|
1778
|
+
output: process.stdout
|
|
1779
|
+
});
|
|
1780
|
+
const answer = await new Promise((resolve) => {
|
|
1781
|
+
rl.question(chalk11.cyan("Create and checkout this branch? (y/N) "), resolve);
|
|
1782
|
+
});
|
|
1783
|
+
rl.close();
|
|
1784
|
+
if (answer.toLowerCase() === "y") {
|
|
1785
|
+
await git.checkoutLocalBranch(branchName);
|
|
1786
|
+
console.log(chalk11.green(`\u2713 Created and checked out branch: ${branchName}`));
|
|
1787
|
+
} else {
|
|
1788
|
+
console.log(chalk11.gray("\nTo create manually:"));
|
|
1789
|
+
console.log(chalk11.gray(` git checkout -b ${branchName}`));
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
spinner.fail("Failed to generate branch name");
|
|
1794
|
+
console.error(chalk11.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1795
|
+
process.exit(1);
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
// src/commands/sync.ts
|
|
1800
|
+
import { Command as Command12 } from "commander";
|
|
1801
|
+
import chalk12 from "chalk";
|
|
1802
|
+
import ora11 from "ora";
|
|
1803
|
+
import { simpleGit as simpleGit11 } from "simple-git";
|
|
1804
|
+
var syncCommand = new Command12("sync").description("Sync current branch with remote (fetch + rebase/merge)").option("-m, --merge", "Use merge instead of rebase").option("--no-push", "Skip push after syncing").option("--stash", "Auto-stash changes before sync").option("-f, --force", "Force sync even with uncommitted changes").action(async (options) => {
|
|
1805
|
+
const git = simpleGit11();
|
|
1806
|
+
const isRepo = await git.checkIsRepo();
|
|
1807
|
+
if (!isRepo) {
|
|
1808
|
+
console.error(chalk12.red("Error: Not a git repository"));
|
|
1809
|
+
process.exit(1);
|
|
1810
|
+
}
|
|
1811
|
+
const spinner = ora11("Checking repository status...").start();
|
|
1851
1812
|
try {
|
|
1852
1813
|
const status = await git.status();
|
|
1853
1814
|
const hasChanges = !status.isClean();
|
|
1854
1815
|
if (hasChanges && !options.stash && !options.force) {
|
|
1855
1816
|
spinner.stop();
|
|
1856
|
-
console.log(
|
|
1817
|
+
console.log(chalk12.yellow("You have uncommitted changes:"));
|
|
1857
1818
|
if (status.modified.length > 0) {
|
|
1858
|
-
console.log(
|
|
1819
|
+
console.log(chalk12.gray(` Modified: ${status.modified.length} file(s)`));
|
|
1859
1820
|
}
|
|
1860
1821
|
if (status.not_added.length > 0) {
|
|
1861
|
-
console.log(
|
|
1822
|
+
console.log(chalk12.gray(` Untracked: ${status.not_added.length} file(s)`));
|
|
1862
1823
|
}
|
|
1863
1824
|
console.log();
|
|
1864
|
-
console.log(
|
|
1825
|
+
console.log(chalk12.gray("Use --stash to auto-stash, or --force to sync anyway"));
|
|
1865
1826
|
process.exit(1);
|
|
1866
1827
|
}
|
|
1867
1828
|
let stashed = false;
|
|
@@ -1880,11 +1841,11 @@ var syncCommand = new Command11("sync").description("Sync current branch with re
|
|
|
1880
1841
|
const trackingBranch = status.tracking;
|
|
1881
1842
|
if (!trackingBranch) {
|
|
1882
1843
|
spinner.warn(`Branch ${currentBranch} has no upstream tracking branch`);
|
|
1883
|
-
console.log(
|
|
1844
|
+
console.log(chalk12.gray(`
|
|
1884
1845
|
To set upstream: git push -u origin ${currentBranch}`));
|
|
1885
1846
|
if (stashed) {
|
|
1886
1847
|
await git.stash(["pop"]);
|
|
1887
|
-
console.log(
|
|
1848
|
+
console.log(chalk12.gray("Restored stashed changes"));
|
|
1888
1849
|
}
|
|
1889
1850
|
return;
|
|
1890
1851
|
}
|
|
@@ -1898,36 +1859,36 @@ To set upstream: git push -u origin ${currentBranch}`));
|
|
|
1898
1859
|
}
|
|
1899
1860
|
} catch (error) {
|
|
1900
1861
|
spinner.fail(`${strategy} failed - you may have conflicts`);
|
|
1901
|
-
console.log(
|
|
1862
|
+
console.log(chalk12.yellow("\nResolve conflicts and then:"));
|
|
1902
1863
|
if (options.merge) {
|
|
1903
|
-
console.log(
|
|
1864
|
+
console.log(chalk12.gray(" git add . && git commit"));
|
|
1904
1865
|
} else {
|
|
1905
|
-
console.log(
|
|
1866
|
+
console.log(chalk12.gray(" git add . && git rebase --continue"));
|
|
1906
1867
|
}
|
|
1907
1868
|
if (stashed) {
|
|
1908
|
-
console.log(
|
|
1869
|
+
console.log(chalk12.yellow("\nNote: You have stashed changes. Run `git stash pop` after resolving."));
|
|
1909
1870
|
}
|
|
1910
1871
|
process.exit(1);
|
|
1911
1872
|
}
|
|
1912
1873
|
const newStatus = await git.status();
|
|
1913
1874
|
const ahead = newStatus.ahead || 0;
|
|
1914
1875
|
const behind = newStatus.behind || 0;
|
|
1915
|
-
spinner.succeed(
|
|
1876
|
+
spinner.succeed(chalk12.green("Synced successfully"));
|
|
1916
1877
|
if (behind > 0) {
|
|
1917
|
-
console.log(
|
|
1878
|
+
console.log(chalk12.yellow(` \u2193 ${behind} commit(s) behind`));
|
|
1918
1879
|
}
|
|
1919
1880
|
if (ahead > 0) {
|
|
1920
1881
|
if (options.push !== false) {
|
|
1921
|
-
const pushSpinner =
|
|
1882
|
+
const pushSpinner = ora11("Pushing to remote...").start();
|
|
1922
1883
|
try {
|
|
1923
1884
|
await git.push();
|
|
1924
|
-
pushSpinner.succeed(
|
|
1885
|
+
pushSpinner.succeed(chalk12.green(`Pushed ${ahead} commit(s)`));
|
|
1925
1886
|
} catch (error) {
|
|
1926
1887
|
pushSpinner.fail("Push failed");
|
|
1927
|
-
console.error(
|
|
1888
|
+
console.error(chalk12.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1928
1889
|
}
|
|
1929
1890
|
} else {
|
|
1930
|
-
console.log(
|
|
1891
|
+
console.log(chalk12.cyan(` \u2191 ${ahead} commit(s) ahead`));
|
|
1931
1892
|
}
|
|
1932
1893
|
}
|
|
1933
1894
|
if (stashed) {
|
|
@@ -1937,37 +1898,37 @@ To set upstream: git push -u origin ${currentBranch}`));
|
|
|
1937
1898
|
spinner.succeed("Restored stashed changes");
|
|
1938
1899
|
} catch {
|
|
1939
1900
|
spinner.warn("Could not auto-restore stash (may have conflicts)");
|
|
1940
|
-
console.log(
|
|
1901
|
+
console.log(chalk12.gray(" Run `git stash pop` manually"));
|
|
1941
1902
|
}
|
|
1942
1903
|
}
|
|
1943
1904
|
} catch (error) {
|
|
1944
1905
|
spinner.fail("Sync failed");
|
|
1945
|
-
console.error(
|
|
1906
|
+
console.error(chalk12.red(error instanceof Error ? error.message : "Unknown error"));
|
|
1946
1907
|
process.exit(1);
|
|
1947
1908
|
}
|
|
1948
1909
|
});
|
|
1949
1910
|
|
|
1950
1911
|
// src/commands/stash.ts
|
|
1951
|
-
import { Command as
|
|
1952
|
-
import
|
|
1953
|
-
import
|
|
1954
|
-
import { simpleGit as
|
|
1955
|
-
var stashCommand = new
|
|
1956
|
-
const git =
|
|
1912
|
+
import { Command as Command13 } from "commander";
|
|
1913
|
+
import chalk13 from "chalk";
|
|
1914
|
+
import ora12 from "ora";
|
|
1915
|
+
import { simpleGit as simpleGit12 } from "simple-git";
|
|
1916
|
+
var stashCommand = new Command13("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)", "gemini").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) => {
|
|
1917
|
+
const git = simpleGit12();
|
|
1957
1918
|
const isRepo = await git.checkIsRepo();
|
|
1958
1919
|
if (!isRepo) {
|
|
1959
|
-
console.error(
|
|
1920
|
+
console.error(chalk13.red("Error: Not a git repository"));
|
|
1960
1921
|
process.exit(1);
|
|
1961
1922
|
}
|
|
1962
1923
|
if (options.list) {
|
|
1963
1924
|
const stashList = await git.stashList();
|
|
1964
1925
|
if (stashList.all.length === 0) {
|
|
1965
|
-
console.log(
|
|
1926
|
+
console.log(chalk13.gray("No stashes found"));
|
|
1966
1927
|
return;
|
|
1967
1928
|
}
|
|
1968
|
-
console.log(
|
|
1929
|
+
console.log(chalk13.bold("\nStashes:\n"));
|
|
1969
1930
|
stashList.all.forEach((stash, index) => {
|
|
1970
|
-
console.log(` ${
|
|
1931
|
+
console.log(` ${chalk13.cyan(index.toString())} ${stash.message}`);
|
|
1971
1932
|
});
|
|
1972
1933
|
console.log();
|
|
1973
1934
|
return;
|
|
@@ -1976,9 +1937,9 @@ var stashCommand = new Command12("stash").description("Stash changes with AI-gen
|
|
|
1976
1937
|
const index = typeof options.apply === "string" ? options.apply : "0";
|
|
1977
1938
|
try {
|
|
1978
1939
|
await git.stash(["apply", `stash@{${index}}`]);
|
|
1979
|
-
console.log(
|
|
1940
|
+
console.log(chalk13.green(`\u2713 Applied stash@{${index}}`));
|
|
1980
1941
|
} catch (error) {
|
|
1981
|
-
console.error(
|
|
1942
|
+
console.error(chalk13.red(`Failed to apply stash: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
1982
1943
|
process.exit(1);
|
|
1983
1944
|
}
|
|
1984
1945
|
return;
|
|
@@ -1987,9 +1948,9 @@ var stashCommand = new Command12("stash").description("Stash changes with AI-gen
|
|
|
1987
1948
|
const index = typeof options.pop === "string" ? options.pop : "0";
|
|
1988
1949
|
try {
|
|
1989
1950
|
await git.stash(["pop", `stash@{${index}}`]);
|
|
1990
|
-
console.log(
|
|
1951
|
+
console.log(chalk13.green(`\u2713 Popped stash@{${index}}`));
|
|
1991
1952
|
} catch (error) {
|
|
1992
|
-
console.error(
|
|
1953
|
+
console.error(chalk13.red(`Failed to pop stash: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
1993
1954
|
process.exit(1);
|
|
1994
1955
|
}
|
|
1995
1956
|
return;
|
|
@@ -1998,9 +1959,9 @@ var stashCommand = new Command12("stash").description("Stash changes with AI-gen
|
|
|
1998
1959
|
const index = typeof options.drop === "string" ? options.drop : "0";
|
|
1999
1960
|
try {
|
|
2000
1961
|
await git.stash(["drop", `stash@{${index}}`]);
|
|
2001
|
-
console.log(
|
|
1962
|
+
console.log(chalk13.green(`\u2713 Dropped stash@{${index}}`));
|
|
2002
1963
|
} catch (error) {
|
|
2003
|
-
console.error(
|
|
1964
|
+
console.error(chalk13.red(`Failed to drop stash: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
2004
1965
|
process.exit(1);
|
|
2005
1966
|
}
|
|
2006
1967
|
return;
|
|
@@ -2012,20 +1973,20 @@ var stashCommand = new Command12("stash").description("Stash changes with AI-gen
|
|
|
2012
1973
|
output: process.stdout
|
|
2013
1974
|
});
|
|
2014
1975
|
const answer = await new Promise((resolve) => {
|
|
2015
|
-
rl.question(
|
|
1976
|
+
rl.question(chalk13.yellow("Clear all stashes? This cannot be undone. (y/N) "), resolve);
|
|
2016
1977
|
});
|
|
2017
1978
|
rl.close();
|
|
2018
1979
|
if (answer.toLowerCase() === "y") {
|
|
2019
1980
|
await git.stash(["clear"]);
|
|
2020
|
-
console.log(
|
|
1981
|
+
console.log(chalk13.green("\u2713 Cleared all stashes"));
|
|
2021
1982
|
} else {
|
|
2022
|
-
console.log(
|
|
1983
|
+
console.log(chalk13.gray("Cancelled"));
|
|
2023
1984
|
}
|
|
2024
1985
|
return;
|
|
2025
1986
|
}
|
|
2026
1987
|
const status = await git.status();
|
|
2027
1988
|
if (status.isClean()) {
|
|
2028
|
-
console.log(
|
|
1989
|
+
console.log(chalk13.yellow("No changes to stash"));
|
|
2029
1990
|
return;
|
|
2030
1991
|
}
|
|
2031
1992
|
let stashName;
|
|
@@ -2039,9 +2000,11 @@ var stashCommand = new Command12("stash").description("Stash changes with AI-gen
|
|
|
2039
2000
|
if (!fullDiff.trim()) {
|
|
2040
2001
|
stashName = `WIP: untracked files (${status.not_added.length} files)`;
|
|
2041
2002
|
} else {
|
|
2042
|
-
const spinner =
|
|
2003
|
+
const spinner = ora12("Generating stash name...").start();
|
|
2043
2004
|
try {
|
|
2044
|
-
|
|
2005
|
+
const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
|
|
2006
|
+
const template = findTemplate(repoRoot.trim(), "stash");
|
|
2007
|
+
stashName = await generateStashName(fullDiff, { provider, model: options.model }, template || void 0);
|
|
2045
2008
|
spinner.stop();
|
|
2046
2009
|
} catch (error) {
|
|
2047
2010
|
spinner.fail("Failed to generate name, using default");
|
|
@@ -2050,23 +2013,298 @@ var stashCommand = new Command12("stash").description("Stash changes with AI-gen
|
|
|
2050
2013
|
}
|
|
2051
2014
|
}
|
|
2052
2015
|
await git.stash(["push", "-u", "-m", stashName]);
|
|
2053
|
-
console.log(
|
|
2016
|
+
console.log(chalk13.green(`\u2713 Stashed: ${stashName}`));
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
// src/commands/summary.ts
|
|
2020
|
+
import { Command as Command14 } from "commander";
|
|
2021
|
+
import chalk14 from "chalk";
|
|
2022
|
+
import ora13 from "ora";
|
|
2023
|
+
import { simpleGit as simpleGit13 } from "simple-git";
|
|
2024
|
+
var summaryCommand = new Command14("summary").description("Generate a work summary from your commits (for daily/weekly reports)").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").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) => {
|
|
2025
|
+
const git = simpleGit13();
|
|
2026
|
+
const isRepo = await git.checkIsRepo();
|
|
2027
|
+
if (!isRepo) {
|
|
2028
|
+
console.error(chalk14.red("Error: Not a git repository"));
|
|
2029
|
+
process.exit(1);
|
|
2030
|
+
}
|
|
2031
|
+
const provider = options.provider.toLowerCase();
|
|
2032
|
+
const spinner = ora13("Generating summary...").start();
|
|
2033
|
+
try {
|
|
2034
|
+
let author = options.author;
|
|
2035
|
+
if (!author) {
|
|
2036
|
+
const config = await git.listConfig();
|
|
2037
|
+
author = config.all["user.name"] || "";
|
|
2038
|
+
if (!author) {
|
|
2039
|
+
spinner.fail("Could not determine git user. Use --author to specify.");
|
|
2040
|
+
process.exit(1);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
let since = options.since;
|
|
2044
|
+
let format = "custom";
|
|
2045
|
+
if (options.daily) {
|
|
2046
|
+
since = "today";
|
|
2047
|
+
format = "daily";
|
|
2048
|
+
} else if (options.weekly) {
|
|
2049
|
+
since = "1 week ago";
|
|
2050
|
+
format = "weekly";
|
|
2051
|
+
} else if (since === "today") {
|
|
2052
|
+
format = "daily";
|
|
2053
|
+
}
|
|
2054
|
+
const sinceDate = resolveDate(since);
|
|
2055
|
+
spinner.text = `Fetching commits by ${author} since ${since}...`;
|
|
2056
|
+
const logOptions = [`--author=${author}`, `--since=${sinceDate}`];
|
|
2057
|
+
if (options.until) {
|
|
2058
|
+
logOptions.push(`--until=${resolveDate(options.until)}`);
|
|
2059
|
+
}
|
|
2060
|
+
const log = await git.log(logOptions);
|
|
2061
|
+
if (log.all.length === 0) {
|
|
2062
|
+
spinner.info(`No commits found for ${author} since ${since}`);
|
|
2063
|
+
process.exit(0);
|
|
2064
|
+
}
|
|
2065
|
+
spinner.text = `Analyzing ${log.all.length} commits...`;
|
|
2066
|
+
let diff;
|
|
2067
|
+
if (options.withDiff && log.all.length > 0) {
|
|
2068
|
+
const oldest = log.all[log.all.length - 1].hash;
|
|
2069
|
+
const newest = log.all[0].hash;
|
|
2070
|
+
try {
|
|
2071
|
+
diff = await git.diff([`${oldest}^`, newest]);
|
|
2072
|
+
} catch {
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
const commits = log.all.map((c) => ({
|
|
2076
|
+
hash: c.hash,
|
|
2077
|
+
message: c.message,
|
|
2078
|
+
date: c.date
|
|
2079
|
+
}));
|
|
2080
|
+
const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
|
|
2081
|
+
const template = findTemplate(repoRoot.trim(), "summary");
|
|
2082
|
+
const summary = await generateWorkSummary(
|
|
2083
|
+
{ commits, author, since, until: options.until, diff },
|
|
2084
|
+
{ provider, model: options.model },
|
|
2085
|
+
format,
|
|
2086
|
+
template || void 0
|
|
2087
|
+
);
|
|
2088
|
+
spinner.stop();
|
|
2089
|
+
if (options.json) {
|
|
2090
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
const output = options.markdown ? formatMarkdown(summary, author, since, options.until) : null;
|
|
2094
|
+
if (options.copy) {
|
|
2095
|
+
const textToCopy = output || formatMarkdown(summary, author, since, options.until);
|
|
2096
|
+
const { execSync: execSync5 } = await import("child_process");
|
|
2097
|
+
try {
|
|
2098
|
+
execSync5("pbcopy", { input: textToCopy });
|
|
2099
|
+
console.log(chalk14.green("Summary copied to clipboard!"));
|
|
2100
|
+
console.log();
|
|
2101
|
+
} catch {
|
|
2102
|
+
console.log(chalk14.yellow("Could not copy to clipboard"));
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
if (options.markdown) {
|
|
2106
|
+
console.log(output);
|
|
2107
|
+
} else {
|
|
2108
|
+
printSummary(summary, author, since, options.until);
|
|
2109
|
+
}
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
spinner.fail("Failed to generate summary");
|
|
2112
|
+
console.error(chalk14.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2113
|
+
process.exit(1);
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
function formatMarkdown(summary, author, since, until) {
|
|
2117
|
+
const lines = [];
|
|
2118
|
+
const period = until ? `${since} - ${until}` : `${since} - now`;
|
|
2119
|
+
lines.push(`# ${summary.title}`);
|
|
2120
|
+
lines.push("");
|
|
2121
|
+
lines.push(`**Author:** ${author}`);
|
|
2122
|
+
lines.push(`**Period:** ${period}`);
|
|
2123
|
+
if (summary.stats) {
|
|
2124
|
+
lines.push(`**Commits:** ${summary.stats.commits}`);
|
|
2125
|
+
}
|
|
2126
|
+
lines.push("");
|
|
2127
|
+
lines.push("## Overview");
|
|
2128
|
+
lines.push(summary.overview);
|
|
2129
|
+
lines.push("");
|
|
2130
|
+
if (summary.highlights.length > 0) {
|
|
2131
|
+
lines.push("## Highlights");
|
|
2132
|
+
for (const highlight of summary.highlights) {
|
|
2133
|
+
lines.push(`- ${highlight}`);
|
|
2134
|
+
}
|
|
2135
|
+
lines.push("");
|
|
2136
|
+
}
|
|
2137
|
+
if (summary.details.length > 0) {
|
|
2138
|
+
lines.push("## Details");
|
|
2139
|
+
for (const section of summary.details) {
|
|
2140
|
+
lines.push(`### ${section.category}`);
|
|
2141
|
+
for (const item of section.items) {
|
|
2142
|
+
lines.push(`- ${item}`);
|
|
2143
|
+
}
|
|
2144
|
+
lines.push("");
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
return lines.join("\n");
|
|
2148
|
+
}
|
|
2149
|
+
function resolveDate(dateStr) {
|
|
2150
|
+
const now = /* @__PURE__ */ new Date();
|
|
2151
|
+
if (dateStr === "today") {
|
|
2152
|
+
return formatDate(now);
|
|
2153
|
+
} else if (dateStr === "yesterday") {
|
|
2154
|
+
const d = new Date(now);
|
|
2155
|
+
d.setDate(d.getDate() - 1);
|
|
2156
|
+
return formatDate(d);
|
|
2157
|
+
} else if (dateStr.match(/^(\d+)\s+(day|days)\s+ago$/i)) {
|
|
2158
|
+
const match = dateStr.match(/^(\d+)\s+(day|days)\s+ago$/i);
|
|
2159
|
+
const days = parseInt(match[1], 10);
|
|
2160
|
+
const d = new Date(now);
|
|
2161
|
+
d.setDate(d.getDate() - days);
|
|
2162
|
+
return formatDate(d);
|
|
2163
|
+
} else if (dateStr.match(/^(\d+)\s+(week|weeks)\s+ago$/i)) {
|
|
2164
|
+
const match = dateStr.match(/^(\d+)\s+(week|weeks)\s+ago$/i);
|
|
2165
|
+
const weeks = parseInt(match[1], 10);
|
|
2166
|
+
const d = new Date(now);
|
|
2167
|
+
d.setDate(d.getDate() - weeks * 7);
|
|
2168
|
+
return formatDate(d);
|
|
2169
|
+
}
|
|
2170
|
+
return dateStr;
|
|
2171
|
+
}
|
|
2172
|
+
function formatDate(d) {
|
|
2173
|
+
const year = d.getFullYear();
|
|
2174
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
2175
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
2176
|
+
return `${year}-${month}-${day} 00:00:00`;
|
|
2177
|
+
}
|
|
2178
|
+
function printSummary(summary, author, since, until) {
|
|
2179
|
+
const period = until ? `${since} - ${until}` : `${since} - now`;
|
|
2180
|
+
console.log(chalk14.bold(`
|
|
2181
|
+
\u{1F4CA} ${summary.title}
|
|
2182
|
+
`));
|
|
2183
|
+
console.log(chalk14.gray(`Author: ${author}`));
|
|
2184
|
+
console.log(chalk14.gray(`Period: ${period}`));
|
|
2185
|
+
if (summary.stats) {
|
|
2186
|
+
console.log(chalk14.gray(`Commits: ${summary.stats.commits}`));
|
|
2187
|
+
}
|
|
2188
|
+
console.log();
|
|
2189
|
+
console.log(chalk14.cyan("Overview:"));
|
|
2190
|
+
console.log(` ${summary.overview}`);
|
|
2191
|
+
console.log();
|
|
2192
|
+
if (summary.highlights.length > 0) {
|
|
2193
|
+
console.log(chalk14.cyan("Highlights:"));
|
|
2194
|
+
for (const highlight of summary.highlights) {
|
|
2195
|
+
console.log(` ${chalk14.green("\u2605")} ${highlight}`);
|
|
2196
|
+
}
|
|
2197
|
+
console.log();
|
|
2198
|
+
}
|
|
2199
|
+
if (summary.details.length > 0) {
|
|
2200
|
+
console.log(chalk14.cyan("Details:"));
|
|
2201
|
+
for (const section of summary.details) {
|
|
2202
|
+
console.log(` ${chalk14.yellow(section.category)}`);
|
|
2203
|
+
for (const item of section.items) {
|
|
2204
|
+
console.log(` \u2022 ${item}`);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
console.log();
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// src/commands/config.ts
|
|
2212
|
+
import { Command as Command15 } from "commander";
|
|
2213
|
+
import chalk15 from "chalk";
|
|
2214
|
+
var configCommand = new Command15("config").description("Manage gut configuration");
|
|
2215
|
+
configCommand.command("set <key> <value>").description("Set a configuration value").option("--local", "Set for current repository only").action((key, value, options) => {
|
|
2216
|
+
if (key === "lang") {
|
|
2217
|
+
if (!isValidLanguage(value)) {
|
|
2218
|
+
console.error(chalk15.red(`Invalid language: ${value}`));
|
|
2219
|
+
console.error(chalk15.gray(`Valid languages: ${VALID_LANGUAGES.join(", ")}`));
|
|
2220
|
+
process.exit(1);
|
|
2221
|
+
}
|
|
2222
|
+
try {
|
|
2223
|
+
setLanguage(value, options.local ?? false);
|
|
2224
|
+
const scope = options.local ? "(local)" : "(global)";
|
|
2225
|
+
console.log(chalk15.green(`\u2713 Language set to: ${value} ${scope}`));
|
|
2226
|
+
} catch (err) {
|
|
2227
|
+
console.error(chalk15.red(err.message));
|
|
2228
|
+
process.exit(1);
|
|
2229
|
+
}
|
|
2230
|
+
} else {
|
|
2231
|
+
console.error(chalk15.red(`Unknown config key: ${key}`));
|
|
2232
|
+
console.error(chalk15.gray("Available keys: lang"));
|
|
2233
|
+
process.exit(1);
|
|
2234
|
+
}
|
|
2235
|
+
});
|
|
2236
|
+
configCommand.command("get <key>").description("Get a configuration value").action((key) => {
|
|
2237
|
+
const config = getConfig();
|
|
2238
|
+
if (key in config) {
|
|
2239
|
+
console.log(config[key]);
|
|
2240
|
+
} else {
|
|
2241
|
+
console.error(chalk15.red(`Unknown config key: ${key}`));
|
|
2242
|
+
process.exit(1);
|
|
2243
|
+
}
|
|
2244
|
+
});
|
|
2245
|
+
configCommand.command("list").description("List all configuration values").action(() => {
|
|
2246
|
+
const globalConfig = getGlobalConfig();
|
|
2247
|
+
const localConfig = getLocalConfig();
|
|
2248
|
+
const effectiveConfig = getConfig();
|
|
2249
|
+
console.log(chalk15.bold("Configuration:"));
|
|
2250
|
+
console.log();
|
|
2251
|
+
for (const key of Object.keys(effectiveConfig)) {
|
|
2252
|
+
const value = effectiveConfig[key];
|
|
2253
|
+
const isLocal = key in localConfig;
|
|
2254
|
+
const scope = isLocal ? chalk15.cyan(" (local)") : chalk15.gray(" (global)");
|
|
2255
|
+
console.log(` ${chalk15.cyan(key)}: ${value}${scope}`);
|
|
2256
|
+
}
|
|
2257
|
+
if (Object.keys(localConfig).length > 0) {
|
|
2258
|
+
console.log();
|
|
2259
|
+
console.log(chalk15.gray("Local config: .gut/config.json"));
|
|
2260
|
+
}
|
|
2261
|
+
});
|
|
2262
|
+
|
|
2263
|
+
// src/commands/lang.ts
|
|
2264
|
+
import { Command as Command16 } from "commander";
|
|
2265
|
+
import chalk16 from "chalk";
|
|
2266
|
+
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) => {
|
|
2267
|
+
if (!language) {
|
|
2268
|
+
const lang = getLanguage();
|
|
2269
|
+
const localConfig = getLocalConfig();
|
|
2270
|
+
const isLocal = "lang" in localConfig;
|
|
2271
|
+
const scope = isLocal ? chalk16.cyan("(local)") : chalk16.gray("(global)");
|
|
2272
|
+
console.log(`${lang} ${scope}`);
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
if (!isValidLanguage(language)) {
|
|
2276
|
+
console.error(chalk16.red(`Invalid language: ${language}`));
|
|
2277
|
+
console.error(chalk16.gray(`Valid languages: ${VALID_LANGUAGES.join(", ")}`));
|
|
2278
|
+
process.exit(1);
|
|
2279
|
+
}
|
|
2280
|
+
try {
|
|
2281
|
+
setLanguage(language, options.local ?? false);
|
|
2282
|
+
const scope = options.local ? "(local)" : "(global)";
|
|
2283
|
+
console.log(chalk16.green(`\u2713 Language set to: ${language} ${scope}`));
|
|
2284
|
+
} catch (err) {
|
|
2285
|
+
console.error(chalk16.red(err.message));
|
|
2286
|
+
process.exit(1);
|
|
2287
|
+
}
|
|
2054
2288
|
});
|
|
2055
2289
|
|
|
2056
2290
|
// src/index.ts
|
|
2057
|
-
var program = new
|
|
2291
|
+
var program = new Command17();
|
|
2058
2292
|
program.name("gut").description("Git Utility Tool - AI-powered git commands").version("0.1.0");
|
|
2059
2293
|
program.addCommand(cleanupCommand);
|
|
2060
2294
|
program.addCommand(authCommand);
|
|
2061
|
-
program.addCommand(
|
|
2062
|
-
program.addCommand(
|
|
2063
|
-
program.addCommand(
|
|
2064
|
-
program.addCommand(
|
|
2295
|
+
program.addCommand(commitCommand);
|
|
2296
|
+
program.addCommand(prCommand);
|
|
2297
|
+
program.addCommand(reviewCommand);
|
|
2298
|
+
program.addCommand(mergeCommand);
|
|
2065
2299
|
program.addCommand(changelogCommand);
|
|
2066
|
-
program.addCommand(
|
|
2067
|
-
program.addCommand(
|
|
2068
|
-
program.addCommand(
|
|
2300
|
+
program.addCommand(explainCommand);
|
|
2301
|
+
program.addCommand(findCommand);
|
|
2302
|
+
program.addCommand(branchCommand);
|
|
2303
|
+
program.addCommand(checkoutCommand);
|
|
2069
2304
|
program.addCommand(syncCommand);
|
|
2070
2305
|
program.addCommand(stashCommand);
|
|
2306
|
+
program.addCommand(summaryCommand);
|
|
2307
|
+
program.addCommand(configCommand);
|
|
2308
|
+
program.addCommand(langCommand);
|
|
2071
2309
|
program.parse();
|
|
2072
2310
|
//# sourceMappingURL=index.js.map
|