feat: git commands (#41)
* feat: add /commit-and-push for AI-generated commits and pushes * fix: handle missing upstream branch in git push * feat: add git commit-and-push subcommand with AI assistance * feat: automate staging changes with git add in commit workflow * chore: bump version to 0.0.12 * revert package json * chore(release): bump version to 0.0.12
This commit is contained in:
1 parent
c00feda728
commit
9642bdaeef
4 files changed
+349
-5
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@vibe-kit/grok-cli",
|
||||
"version": "0.0.10",
|
||||
"version": "0.0.12",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@vibe-kit/grok-cli",
|
||||
"version": "0.0.10",
|
||||
"version": "0.0.12",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@vibe-kit/grok-cli",
|
||||
"version": "0.0.11",
|
||||
"version": "0.0.12",
|
||||
"description": "An open-source AI agent that brings the power of Grok directly into your terminal.",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
|
||||
@@ -57,6 +57,7 @@ export function useInputHandler({
|
||||
{ command: "/help", description: "Show help information" },
|
||||
{ command: "/clear", description: "Clear chat history" },
|
||||
{ command: "/models", description: "Switch Grok Model" },
|
||||
{ command: "/commit-and-push", description: "AI commit & push to remote" },
|
||||
{ command: "/exit", description: "Exit the application" },
|
||||
];
|
||||
|
||||
@@ -104,6 +105,9 @@ Built-in Commands:
|
||||
/exit - Exit application
|
||||
exit, quit - Exit application
|
||||
|
||||
Git Commands:
|
||||
/commit-and-push - AI-generated commit + push to remote
|
||||
|
||||
Keyboard Shortcuts:
|
||||
Shift+Tab - Toggle auto-edit mode (bypass confirmations)
|
||||
|
||||
@@ -162,6 +166,189 @@ Available models: ${modelNames.join(", ")}`,
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trimmedInput === "/commit-and-push") {
|
||||
const userEntry: ChatEntry = {
|
||||
type: "user",
|
||||
content: "/commit-and-push",
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setChatHistory((prev) => [...prev, userEntry]);
|
||||
|
||||
setIsProcessing(true);
|
||||
setIsStreaming(true);
|
||||
|
||||
try {
|
||||
// First check if there are any changes at all
|
||||
const initialStatusResult = await agent.executeBashCommand("git status --porcelain");
|
||||
|
||||
if (!initialStatusResult.success || !initialStatusResult.output?.trim()) {
|
||||
const noChangesEntry: ChatEntry = {
|
||||
type: "assistant",
|
||||
content: "No changes to commit. Working directory is clean.",
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setChatHistory((prev) => [...prev, noChangesEntry]);
|
||||
setIsProcessing(false);
|
||||
setIsStreaming(false);
|
||||
setInput("");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add all changes
|
||||
const addResult = await agent.executeBashCommand("git add .");
|
||||
|
||||
if (!addResult.success) {
|
||||
const addErrorEntry: ChatEntry = {
|
||||
type: "assistant",
|
||||
content: `Failed to stage changes: ${addResult.error || 'Unknown error'}`,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setChatHistory((prev) => [...prev, addErrorEntry]);
|
||||
setIsProcessing(false);
|
||||
setIsStreaming(false);
|
||||
setInput("");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Show that changes were staged
|
||||
const addEntry: ChatEntry = {
|
||||
type: "tool_result",
|
||||
content: "Changes staged successfully",
|
||||
timestamp: new Date(),
|
||||
toolCall: {
|
||||
id: `git_add_${Date.now()}`,
|
||||
type: "function",
|
||||
function: {
|
||||
name: "bash",
|
||||
arguments: JSON.stringify({ command: "git add ." }),
|
||||
},
|
||||
},
|
||||
toolResult: addResult,
|
||||
};
|
||||
setChatHistory((prev) => [...prev, addEntry]);
|
||||
|
||||
// Get staged changes for commit message generation
|
||||
const diffResult = await agent.executeBashCommand("git diff --cached");
|
||||
|
||||
// Generate commit message using AI
|
||||
const commitPrompt = `Generate a concise, professional git commit message for these changes:
|
||||
|
||||
Git Status:
|
||||
${initialStatusResult.output}
|
||||
|
||||
Git Diff (staged changes):
|
||||
${diffResult.output || "No staged changes shown"}
|
||||
|
||||
Follow conventional commit format (feat:, fix:, docs:, etc.) and keep it under 72 characters.
|
||||
Respond with ONLY the commit message, no additional text.`;
|
||||
|
||||
let commitMessage = "";
|
||||
let streamingEntry: ChatEntry | null = null;
|
||||
|
||||
for await (const chunk of agent.processUserMessageStream(commitPrompt)) {
|
||||
if (chunk.type === "content" && chunk.content) {
|
||||
if (!streamingEntry) {
|
||||
const newEntry = {
|
||||
type: "assistant" as const,
|
||||
content: `Generating commit message...\n\n${chunk.content}`,
|
||||
timestamp: new Date(),
|
||||
isStreaming: true,
|
||||
};
|
||||
setChatHistory((prev) => [...prev, newEntry]);
|
||||
streamingEntry = newEntry;
|
||||
commitMessage = chunk.content;
|
||||
} else {
|
||||
commitMessage += chunk.content;
|
||||
setChatHistory((prev) =>
|
||||
prev.map((entry, idx) =>
|
||||
idx === prev.length - 1 && entry.isStreaming
|
||||
? { ...entry, content: `Generating commit message...\n\n${commitMessage}` }
|
||||
: entry
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (chunk.type === "done") {
|
||||
if (streamingEntry) {
|
||||
setChatHistory((prev) =>
|
||||
prev.map((entry) =>
|
||||
entry.isStreaming
|
||||
? { ...entry, content: `Generated commit message: "${commitMessage.trim()}"`, isStreaming: false }
|
||||
: entry
|
||||
)
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the commit
|
||||
const cleanCommitMessage = commitMessage.trim().replace(/^["']|["']$/g, '');
|
||||
const commitCommand = `git commit -m "${cleanCommitMessage}"`;
|
||||
const commitResult = await agent.executeBashCommand(commitCommand);
|
||||
|
||||
const commitEntry: ChatEntry = {
|
||||
type: "tool_result",
|
||||
content: commitResult.success
|
||||
? commitResult.output || "Commit successful"
|
||||
: commitResult.error || "Commit failed",
|
||||
timestamp: new Date(),
|
||||
toolCall: {
|
||||
id: `git_commit_${Date.now()}`,
|
||||
type: "function",
|
||||
function: {
|
||||
name: "bash",
|
||||
arguments: JSON.stringify({ command: commitCommand }),
|
||||
},
|
||||
},
|
||||
toolResult: commitResult,
|
||||
};
|
||||
setChatHistory((prev) => [...prev, commitEntry]);
|
||||
|
||||
// If commit was successful, push to remote
|
||||
if (commitResult.success) {
|
||||
// First try regular push, if it fails try with upstream setup
|
||||
let pushResult = await agent.executeBashCommand("git push");
|
||||
let pushCommand = "git push";
|
||||
|
||||
if (!pushResult.success && pushResult.error?.includes("no upstream branch")) {
|
||||
pushCommand = "git push -u origin HEAD";
|
||||
pushResult = await agent.executeBashCommand(pushCommand);
|
||||
}
|
||||
|
||||
const pushEntry: ChatEntry = {
|
||||
type: "tool_result",
|
||||
content: pushResult.success
|
||||
? pushResult.output || "Push successful"
|
||||
: pushResult.error || "Push failed",
|
||||
timestamp: new Date(),
|
||||
toolCall: {
|
||||
id: `git_push_${Date.now()}`,
|
||||
type: "function",
|
||||
function: {
|
||||
name: "bash",
|
||||
arguments: JSON.stringify({ command: pushCommand }),
|
||||
},
|
||||
},
|
||||
toolResult: pushResult,
|
||||
};
|
||||
setChatHistory((prev) => [...prev, pushEntry]);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
const errorEntry: ChatEntry = {
|
||||
type: "assistant",
|
||||
content: `Error during commit and push: ${error.message}`,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setChatHistory((prev) => [...prev, errorEntry]);
|
||||
}
|
||||
|
||||
setIsProcessing(false);
|
||||
setIsStreaming(false);
|
||||
setInput("");
|
||||
return true;
|
||||
}
|
||||
|
||||
const directBashCommands = [
|
||||
"ls",
|
||||
"pwd",
|
||||
|
||||
+159
-2
@@ -88,6 +88,112 @@ function loadBaseURL(): string | undefined {
|
||||
return baseURL;
|
||||
}
|
||||
|
||||
// Handle commit-and-push command in headless mode
|
||||
async function handleCommitAndPushHeadless(
|
||||
apiKey: string,
|
||||
baseURL?: string,
|
||||
model?: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const agent = new GrokAgent(apiKey, baseURL, model);
|
||||
|
||||
// Configure confirmation service for headless mode (auto-approve all operations)
|
||||
const confirmationService = ConfirmationService.getInstance();
|
||||
confirmationService.setSessionFlag("allOperations", true);
|
||||
|
||||
console.log("🤖 Processing commit and push...\n");
|
||||
console.log("> /commit-and-push\n");
|
||||
|
||||
// First check if there are any changes at all
|
||||
const initialStatusResult = await agent.executeBashCommand("git status --porcelain");
|
||||
|
||||
if (!initialStatusResult.success || !initialStatusResult.output?.trim()) {
|
||||
console.log("❌ No changes to commit. Working directory is clean.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("✅ git status: Changes detected");
|
||||
|
||||
// Add all changes
|
||||
const addResult = await agent.executeBashCommand("git add .");
|
||||
|
||||
if (!addResult.success) {
|
||||
console.log(`❌ git add: ${addResult.error || 'Failed to stage changes'}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("✅ git add: Changes staged");
|
||||
|
||||
// Get staged changes for commit message generation
|
||||
const diffResult = await agent.executeBashCommand("git diff --cached");
|
||||
|
||||
// Generate commit message using AI
|
||||
const commitPrompt = `Generate a concise, professional git commit message for these changes:
|
||||
|
||||
Git Status:
|
||||
${initialStatusResult.output}
|
||||
|
||||
Git Diff (staged changes):
|
||||
${diffResult.output || "No staged changes shown"}
|
||||
|
||||
Follow conventional commit format (feat:, fix:, docs:, etc.) and keep it under 72 characters.
|
||||
Respond with ONLY the commit message, no additional text.`;
|
||||
|
||||
console.log("🤖 Generating commit message...");
|
||||
|
||||
const commitMessageEntries = await agent.processUserMessage(commitPrompt);
|
||||
let commitMessage = "";
|
||||
|
||||
// Extract the commit message from the AI response
|
||||
for (const entry of commitMessageEntries) {
|
||||
if (entry.type === "assistant" && entry.content.trim()) {
|
||||
commitMessage = entry.content.trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!commitMessage) {
|
||||
console.log("❌ Failed to generate commit message");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Clean the commit message
|
||||
const cleanCommitMessage = commitMessage.replace(/^["']|["']$/g, '');
|
||||
console.log(`✅ Generated commit message: "${cleanCommitMessage}"`);
|
||||
|
||||
// Execute the commit
|
||||
const commitCommand = `git commit -m "${cleanCommitMessage}"`;
|
||||
const commitResult = await agent.executeBashCommand(commitCommand);
|
||||
|
||||
if (commitResult.success) {
|
||||
console.log(`✅ git commit: ${commitResult.output?.split('\n')[0] || 'Commit successful'}`);
|
||||
|
||||
// If commit was successful, push to remote
|
||||
// First try regular push, if it fails try with upstream setup
|
||||
let pushResult = await agent.executeBashCommand("git push");
|
||||
|
||||
if (!pushResult.success && pushResult.error?.includes("no upstream branch")) {
|
||||
console.log("🔄 Setting upstream and pushing...");
|
||||
pushResult = await agent.executeBashCommand("git push -u origin HEAD");
|
||||
}
|
||||
|
||||
if (pushResult.success) {
|
||||
console.log(`✅ git push: ${pushResult.output?.split('\n')[0] || 'Push successful'}`);
|
||||
} else {
|
||||
console.log(`❌ git push: ${pushResult.error || 'Push failed'}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.log(`❌ git commit: ${commitResult.error || 'Commit failed'}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error during commit and push:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Headless mode processing function
|
||||
async function processPromptHeadless(
|
||||
prompt: string,
|
||||
@@ -158,9 +264,9 @@ async function processPromptHeadless(
|
||||
program
|
||||
.name("grok")
|
||||
.description(
|
||||
"A conversational AI CLI tool powered by Grok-3 with text editor capabilities"
|
||||
"A conversational AI CLI tool powered by Grok with text editor capabilities"
|
||||
)
|
||||
.version("1.0.0")
|
||||
.version("1.0.1")
|
||||
.option("-d, --directory <dir>", "set working directory", process.cwd())
|
||||
.option("-k, --api-key <key>", "Grok API key (or set GROK_API_KEY env var)")
|
||||
.option(
|
||||
@@ -217,4 +323,55 @@ program
|
||||
}
|
||||
});
|
||||
|
||||
// Git subcommand
|
||||
const gitCommand = program
|
||||
.command("git")
|
||||
.description("Git operations with AI assistance");
|
||||
|
||||
gitCommand
|
||||
.command("commit-and-push")
|
||||
.description("Generate AI commit message and push to remote")
|
||||
.option("-d, --directory <dir>", "set working directory", process.cwd())
|
||||
.option("-k, --api-key <key>", "Grok API key (or set GROK_API_KEY env var)")
|
||||
.option(
|
||||
"-u, --base-url <url>",
|
||||
"Grok API base URL (or set GROK_BASE_URL env var)"
|
||||
)
|
||||
.option(
|
||||
"-m, --model <model>",
|
||||
"AI model to use (e.g., gemini-2.5-pro, grok-4-latest)"
|
||||
)
|
||||
.action(async (options) => {
|
||||
if (options.directory) {
|
||||
try {
|
||||
process.chdir(options.directory);
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`Error changing directory to ${options.directory}:`,
|
||||
error.message
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Get API key from options, environment, or user settings
|
||||
const apiKey = options.apiKey || loadApiKey();
|
||||
const baseURL = options.baseUrl || loadBaseURL();
|
||||
const model = options.model;
|
||||
|
||||
if (!apiKey) {
|
||||
console.error(
|
||||
"❌ Error: API key required. Set GROK_API_KEY environment variable, use --api-key flag, or save to ~/.grok/user-settings.json"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await handleCommitAndPushHeadless(apiKey, baseURL, model);
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error during git commit-and-push:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program.parse();
|
||||
Reference in new issue
Block a user