@abhaydesu/git-pilot 1.2.0 → 1.3.0

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 CHANGED
@@ -1,84 +1,112 @@
1
- <div align="center" display="inline">
2
- <img width="40" height="40" alt="logo" src="./public/logo-dark-new.png" />
3
- </div>
4
-
5
- # Git Pilot
6
-
7
- [![NPM Version](https://img.shields.io/npm/v/@abhaydesu/git-pilot)](https://www.npmjs.com/package/@abhaydesu/git-pilot)
8
-
9
- An AI-powered assistant that lives in your command line to streamline your Git workflow. Never write a commit message or look up a command again.
10
-
11
- ---
12
-
13
- ## The Problem
14
- Remembering complex commands like interactive rebase can be a pain. Writing well-formatted commit messages is a chore. Git is powerful, but its interface can sometimes get in the way of a fast workflow.
15
-
16
- **Git Pilot** solves this by acting as your intelligent copilot, translating your natural language intent into the exact commands and messages you need.
17
-
18
- ## Features
19
- * **AI-Powered Commit Messages:** Analyzes your staged changes (`git diff`) and generates a perfect commit message following the Conventional Commits specification. Just run `git pilot commit <your_intent>`
20
- * **Natural Language to Git Command:** Translate plain English requests like "squash the last 3 commits" into the precise Git command. One simple `git pilot run <your_request>`
21
- * **Magic Undo:** Made a mistake? `git pilot undo` analyzes your recent history and suggests the exact command to reverse your last action.
22
- * **Intelligent Branching:** Describe your goal, and `git pilot branch` will create a clean, conventional branch name for you.
23
- * **Interactive & Safe:** Always asks for your confirmation before executing any command. You can also **edit** any AI suggestion to get it just right.
24
-
25
- ## Installation
26
- Make sure you have Node.js (v18+) and Git installed. Then, run the following command to install Git Pilot globally:
27
-
28
- ```bash
29
- npm install -g @abhaydesu/git-pilot
30
- ```
31
-
32
- ### ◾ Usage
33
-
34
- #### *Generating a Commit Message*
35
- Stage your files (git add .), then run git pilot commit. This command analyzes your staged changes and generates a conventional commit message.
36
-
37
- ##### With an Intent
38
- Provide a brief description of your changes, and the AI will use it to write the commit message.
39
-
40
- ```Bash
41
-
42
- git pilot commit "add new user profile page"
43
- ```
44
-
45
- ##### Without an Intent
46
- If you don't provide an intent, the AI will analyze the diff of your staged files and generate a message automatically.
47
-
48
- ```Bash
49
-
50
- git pilot commit
51
- ```
52
- After running, you can accept, edit, or abort the suggested commit.
53
-
54
-
55
- #### *Running a Git Command*
56
- Run the run command with the task you want to perform. The tool will suggest a command and ask for confirmation.
57
-
58
- ```Bash
59
-
60
- git pilot run "cherry-pick the last commit from the main branch"
61
- ```
62
-
63
- #### *Creating a New Branch*
64
- Describe the purpose of your new branch, and let the AI generate a conventional name.
65
-
66
- ```Bash
67
-
68
- git pilot branch "create a new feature for the auth system"
69
- ```
70
-
71
- #### *Undoing a Mistake*
72
-
73
- If you've made a mistake (like a bad commit or merge), this command will analyze your history and suggest the safest way to undo it.
74
-
75
- ```Bash
76
-
77
- git pilot undo
78
- ```
79
-
80
- ### ◾ How It Works
81
- Git Pilot is a CLI tool that communicates with a secure backend API. This API uses Google's Gemini models to understand your intent and analyze code, keeping your API keys safe and off your local machine.
82
-
83
- ### ◾ License
1
+ <div align="center" display="inline">
2
+ <img width="40" height="40" alt="logo" src="./public/logo.png" />
3
+ </div>
4
+
5
+ # Git Pilot
6
+
7
+ [![NPM Version](https://img.shields.io/npm/v/@abhaydesu/git-pilot)](https://www.npmjs.com/package/@abhaydesu/git-pilot)
8
+
9
+ An AI-powered assistant that lives in your command line to streamline your Git workflow. Never write a commit message or look up a command again.
10
+
11
+ ---
12
+
13
+ ## The Problem
14
+ Remembering complex commands like interactive rebase can be a pain. Writing well-formatted commit messages is a chore. Git is powerful, but its interface can sometimes get in the way of a fast workflow.
15
+
16
+ **Git Pilot** solves this by acting as your intelligent copilot, translating your natural language intent into the exact commands and messages you need.
17
+
18
+ ## Features
19
+ * **AI-Powered Commit Messages:** Analyzes your staged changes (`git diff`) and generates a perfect commit message following the Conventional Commits specification. Just run `git pilot commit <your_intent>`
20
+ * **Natural Language to Git Command:** Translate plain English requests like "squash the last 3 commits" into the precise Git command. One simple `git pilot run <your_request>`
21
+ * **Magic Undo:** Made a mistake? `git pilot undo` analyzes your recent history and suggests the exact command to reverse your last action.
22
+ * **Intelligent Branching:** Describe your goal, and `git pilot branch` will create a clean, conventional branch name for you.
23
+ * **Interactive & Safe:** Always asks for your confirmation before executing any command. You can also **edit** any AI suggestion to get it just right. Suggested commands must be plain `git ...` commands and are never run through a shell, so chained commands (`;`, `&&`, `|`, `$(...)`) and non-git programs are refused.
24
+
25
+ ## Installation
26
+ Make sure you have Node.js (v20+) and Git installed. Then, run the following command to install Git Pilot globally:
27
+
28
+ ```bash
29
+ npm install -g @abhaydesu/git-pilot
30
+ ```
31
+
32
+ ### ◾ Usage
33
+
34
+ #### *Generating a Commit Message*
35
+ Stage your files (git add .), then run git pilot commit. This command analyzes your staged changes and generates a conventional commit message.
36
+
37
+ ##### With an Intent
38
+ Provide a brief description of your changes, and the AI will use it to write the commit message.
39
+
40
+ ```Bash
41
+
42
+ git pilot commit "add new user profile page"
43
+ ```
44
+
45
+ ##### Without an Intent
46
+ If you don't provide an intent, the AI will analyze the diff of your staged files and generate a message automatically.
47
+
48
+ ```Bash
49
+
50
+ git pilot commit
51
+ ```
52
+ After running, you can accept, edit, or abort the suggested commit.
53
+
54
+
55
+ #### *Running a Git Command*
56
+ Run the run command with the task you want to perform. The tool will suggest a command and ask for confirmation.
57
+
58
+ ```Bash
59
+
60
+ git pilot run "cherry-pick the last commit from the main branch"
61
+ ```
62
+
63
+ #### *Creating a New Branch*
64
+ Describe the purpose of your new branch, and let the AI generate a conventional name.
65
+
66
+ ```Bash
67
+
68
+ git pilot branch "create a new feature for the auth system"
69
+ ```
70
+
71
+ #### *Undoing a Mistake*
72
+
73
+ If you've made a mistake (like a bad commit or merge), this command will analyze your history and suggest the safest way to undo it.
74
+
75
+ ```Bash
76
+
77
+ git pilot undo
78
+ ```
79
+
80
+ Check the installed version with `git pilot --version`, or see all commands with `git pilot --help`.
81
+
82
+ ### ◾ Configuration
83
+
84
+ | Variable | Default | Description |
85
+ | --- | --- | --- |
86
+ | `GIT_PILOT_API_URL` | `https://git-pilot-api.vercel.app` | Base URL of the backend API. Set it to use a self-hosted or local instance, e.g. `GIT_PILOT_API_URL=http://localhost:3000 git pilot run "show status"`. |
87
+
88
+ Very large staged diffs (over ~800,000 characters) are not sent in full. Git Pilot sends the list of changed files instead and uses your intent to write the message.
89
+
90
+ ### ◾ Development
91
+
92
+ ```bash
93
+ git clone https://github.com/abhaydesu/git-pilot-cli.git
94
+ cd git-pilot-cli
95
+ npm install
96
+ node bin/git-pilot.js --help # or `npm link` to get a global `git-pilot`
97
+ ```
98
+
99
+ | Script | What it does |
100
+ | --- | --- |
101
+ | `npm test` | Unit tests (`node --test`) |
102
+ | `npm run test:e2e` | Runs the real CLI in a PTY against a fake API and a scratch repo (needs `expect`; skipped if missing) |
103
+ | `npm run lint` | ESLint |
104
+ | `npm run format` | Prettier |
105
+
106
+ Code layout: `bin/` (entry point), `src/commands/` (one file per command), `src/lib/` (`api`, `git`, `exec`, `ui`).
107
+
108
+ ### ◾ How It Works
109
+ Git Pilot is a CLI tool that communicates with a secure backend API. This API uses Google's Gemini models to understand your intent and analyze code, keeping your API keys safe and off your local machine.
110
+
111
+ ### ◾ License
84
112
  This project is licensed under the MIT License.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { buildProgram } from "../src/cli.js";
3
+
4
+ await buildProgram().parseAsync(process.argv);
package/package.json CHANGED
@@ -1,26 +1,55 @@
1
- {
2
- "name": "@abhaydesu/git-pilot",
3
- "version": "1.2.0",
4
- "description": "",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "bin": {
10
- "git-pilot": "./index.js"
11
- },
12
- "type": "module",
13
- "keywords": [],
14
- "author": "",
15
- "license": "ISC",
16
- "preferGlobal": true,
17
- "dependencies": {
18
- "axios": "^1.12.2",
19
- "chalk": "^5.6.2",
20
- "commander": "^14.0.1",
21
- "dotenv": "^17.2.2",
22
- "execa": "^9.6.0",
23
- "inquirer": "^12.9.6",
24
- "ora": "^9.0.0"
25
- }
26
- }
1
+ {
2
+ "name": "@abhaydesu/git-pilot",
3
+ "version": "1.3.0",
4
+ "description": "AI-powered Git assistant: commit messages, natural-language git commands, undo suggestions and branch names.",
5
+ "main": "src/cli.js",
6
+ "scripts": {
7
+ "test": "node --test \"test/*.test.js\"",
8
+ "test:e2e": "bash test/e2e/run.sh",
9
+ "lint": "eslint .",
10
+ "format": "prettier --write ."
11
+ },
12
+ "bin": {
13
+ "git-pilot": "bin/git-pilot.js"
14
+ },
15
+ "type": "module",
16
+ "keywords": [
17
+ "git",
18
+ "cli",
19
+ "ai",
20
+ "gemini",
21
+ "commit",
22
+ "conventional-commits"
23
+ ],
24
+ "author": "",
25
+ "license": "MIT",
26
+ "preferGlobal": true,
27
+ "dependencies": {
28
+ "axios": "^1.12.2",
29
+ "chalk": "^5.6.2",
30
+ "commander": "^14.0.1",
31
+ "execa": "^9.6.0",
32
+ "inquirer": "^12.11.1",
33
+ "ora": "^9.4.1",
34
+ "shell-quote": "^1.10.0"
35
+ },
36
+ "devDependencies": {
37
+ "@eslint/js": "^10.0.1",
38
+ "eslint": "^10.11.0",
39
+ "globals": "^17.12.0",
40
+ "prettier": "^3.9.9"
41
+ },
42
+ "files": [
43
+ "bin",
44
+ "src",
45
+ "public",
46
+ "README.md"
47
+ ],
48
+ "engines": {
49
+ "node": ">=20"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "git+https://github.com/abhaydesu/git-pilot-cli.git"
54
+ }
55
+ }
Binary file
package/src/cli.js ADDED
@@ -0,0 +1,40 @@
1
+ import { Command } from "commander";
2
+ import { readFileSync } from "node:fs";
3
+ import { commitCommand } from "./commands/commit.js";
4
+ import { runCommand } from "./commands/run.js";
5
+ import { undoCommand } from "./commands/undo.js";
6
+ import { branchCommand } from "./commands/branch.js";
7
+
8
+ const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
9
+
10
+ export function buildProgram() {
11
+ const program = new Command()
12
+ .name("git-pilot")
13
+ .description("An AI-powered Git assistant in your command line.")
14
+ .version(version);
15
+
16
+ program
17
+ .command("commit")
18
+ .description("Generate an AI-powered commit message.")
19
+ .argument("[intent]", 'Optional: The user\'s intent for the commit (e.g., "add a new feature")')
20
+ .action(commitCommand);
21
+
22
+ program
23
+ .command("run")
24
+ .description("Translate natural language into a Git command.")
25
+ .argument("<string>", 'The task you want to perform (e.g. "squash the last 3 commits")')
26
+ .action(runCommand);
27
+
28
+ program
29
+ .command("undo")
30
+ .description("Suggests a command to undo your last major Git action")
31
+ .action(undoCommand);
32
+
33
+ program
34
+ .command("branch")
35
+ .description("Generates a conventional branch name from a description and creates the branch.")
36
+ .argument("<string>", 'A description of the branch\'s purpose (e.g., "fix login bug")')
37
+ .action(branchCommand);
38
+
39
+ return program;
40
+ }
@@ -0,0 +1,41 @@
1
+ import chalk from "chalk";
2
+ import { callApi } from "../lib/api.js";
3
+ import * as git from "../lib/git.js";
4
+ import {
5
+ spin,
6
+ showBlock,
7
+ chooseAcceptEditAbort,
8
+ editText,
9
+ success,
10
+ aborted,
11
+ handleError,
12
+ } from "../lib/ui.js";
13
+
14
+ export async function branchCommand(description) {
15
+ const spinner = spin("Generating a conventional branch name...");
16
+ try {
17
+ const { branchName } = await callApi("pilot-branch", { description });
18
+ spinner.succeed();
19
+
20
+ showBlock("Suggested Branch Name", branchName);
21
+
22
+ const choice = await chooseAcceptEditAbort();
23
+ if (choice === "abort") return aborted("Branch creation aborted by user.");
24
+
25
+ const chosen =
26
+ choice === "edit"
27
+ ? await editText("Edit the branch name:", branchName, "Branch name cannot be empty")
28
+ : branchName;
29
+
30
+ const safeName = git.slugifyBranchName(chosen);
31
+ if (!safeName) {
32
+ console.log(chalk.red("Resulting branch name is empty or invalid. Aborting."));
33
+ process.exit(1);
34
+ }
35
+
36
+ await git.createBranch(safeName);
37
+ success(`✔ Switched to new branch '${safeName}'`);
38
+ } catch (error) {
39
+ handleError(error, spinner);
40
+ }
41
+ }
@@ -0,0 +1,48 @@
1
+ import { callApi, MAX_DIFF_CHARS } from "../lib/api.js";
2
+ import * as git from "../lib/git.js";
3
+ import {
4
+ spin,
5
+ showBlock,
6
+ chooseAcceptEditAbort,
7
+ editText,
8
+ success,
9
+ aborted,
10
+ handleError,
11
+ } from "../lib/ui.js";
12
+
13
+ const OVERSIZED_PREFIX =
14
+ "The staged changes are too large to display the full diff. " +
15
+ "Please generate a commit message based on the user's intent and this summary of changed files:\n\n";
16
+
17
+ export async function commitCommand(intent) {
18
+ const spinner = spin("Analyzing your staged changes...");
19
+ try {
20
+ const diff = await git.getStagedDiff();
21
+ if (!diff) {
22
+ spinner.warn("No staged changes found. Please stage your files with `git add`.");
23
+ return;
24
+ }
25
+
26
+ const diffToSend =
27
+ diff.length > MAX_DIFF_CHARS ? OVERSIZED_PREFIX + (await git.getStagedSummary()) : diff;
28
+
29
+ spinner.text = "Generating commit message...";
30
+ const { message } = await callApi("pilot-commit", { intent, diff: diffToSend });
31
+ spinner.succeed();
32
+
33
+ showBlock("Suggested Message", message);
34
+
35
+ const choice = await chooseAcceptEditAbort();
36
+ if (choice === "abort") return aborted("Commit aborted by user.");
37
+
38
+ const finalMessage =
39
+ choice === "edit"
40
+ ? await editText("Edit the commit message: ", message, "Commit message cannot be empty.")
41
+ : message;
42
+
43
+ await git.commit(finalMessage);
44
+ success("Commit successful!");
45
+ } catch (error) {
46
+ handleError(error, spinner);
47
+ }
48
+ }
@@ -0,0 +1,35 @@
1
+ import chalk from "chalk";
2
+ import { callApi } from "../lib/api.js";
3
+ import { parseGitCommand, runGit } from "../lib/exec.js";
4
+ import { spin, showBlock, confirm, success, aborted, handleError } from "../lib/ui.js";
5
+
6
+ export async function runCommand(request) {
7
+ const spinner = spin("Fetching the right command...");
8
+ try {
9
+ const { command } = await callApi("pilot-run", { request });
10
+
11
+ if (command.startsWith("Error:")) {
12
+ spinner.fail(command);
13
+ process.exit(1);
14
+ }
15
+ spinner.succeed();
16
+
17
+ // Validate before showing anything the user might be tempted to confirm.
18
+ const args = parseGitCommand(command);
19
+
20
+ console.log(
21
+ chalk.yellow.bold("\nWARNING: The suggested command will be executed on your repository.")
22
+ );
23
+ console.log(chalk.yellow("Always review commands carefully before confirming."));
24
+ showBlock("Suggested Command", command);
25
+
26
+ if (!(await confirm("Execute this command?"))) {
27
+ return aborted("Execution aborted by user.");
28
+ }
29
+
30
+ await runGit(args);
31
+ success("Command executed successfully!");
32
+ } catch (error) {
33
+ handleError(error, spinner);
34
+ }
35
+ }
@@ -0,0 +1,39 @@
1
+ import chalk from "chalk";
2
+ import { callApi } from "../lib/api.js";
3
+ import { parseGitCommand, runGit } from "../lib/exec.js";
4
+ import * as git from "../lib/git.js";
5
+ import { spin, showBlock, confirm, success, aborted, handleError } from "../lib/ui.js";
6
+
7
+ export async function undoCommand() {
8
+ const spinner = spin("Analyzing your recent Git history...");
9
+ try {
10
+ const reflog = await git.getReflog();
11
+
12
+ spinner.text = "Suggesting an undo command...";
13
+ const { command, explanation } = await callApi("pilot-undo", { reflog });
14
+
15
+ if (!command) {
16
+ spinner.warn("Couldn't determine a safe action to undo.");
17
+ return;
18
+ }
19
+ spinner.succeed();
20
+
21
+ const args = parseGitCommand(command);
22
+
23
+ showBlock("Suggested Undo Command", command);
24
+ console.log(chalk.yellow(explanation));
25
+
26
+ if (!(await confirm("Execute this undo command?"))) {
27
+ return aborted("Execution aborted by user.");
28
+ }
29
+
30
+ if (args[0] === "rebase" && args.includes("--abort") && !(await git.isRebaseInProgress())) {
31
+ return console.log(chalk.yellow("No rebase in progress — abort not necessary."));
32
+ }
33
+
34
+ await runGit(args);
35
+ success("Action successfully undone!");
36
+ } catch (error) {
37
+ handleError(error, spinner);
38
+ }
39
+ }
package/src/lib/api.js ADDED
@@ -0,0 +1,26 @@
1
+ import axios from "axios";
2
+
3
+ export const DEFAULT_API_URL = "https://git-pilot-api.vercel.app";
4
+
5
+ // The API rejects request bodies over 1 MB; keep the diff safely below that.
6
+ export const MAX_DIFF_CHARS = 800_000;
7
+
8
+ export const apiBaseUrl = () =>
9
+ (process.env.GIT_PILOT_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
10
+
11
+ /** POSTs `body` to `/api/<endpoint>` and returns the parsed response data. */
12
+ export async function callApi(endpoint, body) {
13
+ const { data } = await axios.post(`${apiBaseUrl()}/api/${endpoint}`, body, {
14
+ timeout: 30_000,
15
+ });
16
+ return data;
17
+ }
18
+
19
+ /** Human-readable message for an axios/network/other error. */
20
+ export function describeError(error) {
21
+ if (error.response) {
22
+ const detail = error.response.data?.error || "No message";
23
+ return `API Error: ${error.response.status} - ${detail}`;
24
+ }
25
+ return `An unexpected error occurred: ${error.message}`;
26
+ }
@@ -0,0 +1,34 @@
1
+ import { parse } from "shell-quote";
2
+ import { execa } from "execa";
3
+
4
+ export class UnsafeCommandError extends Error {}
5
+
6
+ /**
7
+ * Turns a suggested command string into an argv array for `git`, or throws.
8
+ * Only plain `git <args...>` is accepted: quoting is honored, but shell
9
+ * operators (;, &&, |, >, $(...)), comments and non-git binaries are rejected.
10
+ * Nothing is ever run through a shell.
11
+ */
12
+ export function parseGitCommand(command) {
13
+ // Keep `$VAR` literal instead of letting shell-quote expand it to "".
14
+ const tokens = parse(command, (key) => `$${key}`).map((token) =>
15
+ // Globs (`git add *.js`) are valid pathspecs that git expands itself.
16
+ typeof token === "object" && token.op === "glob" ? token.pattern : token
17
+ );
18
+
19
+ if (tokens.length === 0) {
20
+ throw new UnsafeCommandError("The suggested command is empty.");
21
+ }
22
+ if (tokens.some((token) => typeof token !== "string")) {
23
+ throw new UnsafeCommandError(
24
+ "The suggested command contains shell operators, which are not allowed."
25
+ );
26
+ }
27
+ if (tokens[0] !== "git" || tokens.length < 2) {
28
+ throw new UnsafeCommandError("Only git commands can be executed.");
29
+ }
30
+ return tokens.slice(1);
31
+ }
32
+
33
+ /** Runs `git <args>` with the terminal attached. */
34
+ export const runGit = (args) => execa("git", args, { stdio: "inherit" });
package/src/lib/git.js ADDED
@@ -0,0 +1,36 @@
1
+ import { execa } from "execa";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ const git = async (args) => (await execa("git", args)).stdout;
6
+
7
+ export const getStagedDiff = () => git(["diff", "--staged"]);
8
+ export const getStagedSummary = () => git(["diff", "--staged", "--name-status"]);
9
+ export const getReflog = () => git(["reflog", "-n", "15"]);
10
+ export const commit = (message) => git(["commit", "-m", message]);
11
+ export const createBranch = (name) => git(["switch", "-c", name]);
12
+
13
+ const exists = (p) =>
14
+ fs.access(p).then(
15
+ () => true,
16
+ () => false
17
+ );
18
+
19
+ export async function isRebaseInProgress() {
20
+ const gitDir = path.resolve(await git(["rev-parse", "--git-dir"]));
21
+ return (
22
+ (await exists(path.join(gitDir, "rebase-apply"))) ||
23
+ (await exists(path.join(gitDir, "rebase-merge")))
24
+ );
25
+ }
26
+
27
+ /** Lowercase, hyphenated, git-safe branch name; "" if nothing usable is left. */
28
+ export const slugifyBranchName = (name) =>
29
+ name
30
+ .toLowerCase()
31
+ .trim()
32
+ .replace(/\s+/g, "-")
33
+ .replace(/[^a-z0-9._\-/]/g, "")
34
+ .replace(/\/{2,}/g, "/")
35
+ .replace(/^-+|-+$/g, "")
36
+ .slice(0, 250);
package/src/lib/ui.js ADDED
@@ -0,0 +1,74 @@
1
+ import chalk from "chalk";
2
+ import inquirer from "inquirer";
3
+ import ora from "ora";
4
+ import { describeError } from "./api.js";
5
+ import { UnsafeCommandError } from "./exec.js";
6
+
7
+ // ora 9.x divides by stream.columns; a PTY that reports 0 columns makes its
8
+ // redraw loop spin forever. Give it a sane fallback.
9
+ if (!process.stderr.columns) process.stderr.columns = 80;
10
+
11
+ // discardStdin is off so the spinner never swallows the input inquirer needs next.
12
+ export const spin = (text) => ora({ text, discardStdin: false }).start();
13
+
14
+ export function showBlock(title, body) {
15
+ const bar = "-".repeat(title.length + 14);
16
+ console.log(chalk.green(`\n------ ${title} ------`));
17
+ console.log(chalk.cyan(body));
18
+ console.log(chalk.green(`${bar}\n`));
19
+ }
20
+
21
+ export async function confirm(message) {
22
+ const { ok } = await inquirer.prompt([{ type: "confirm", name: "ok", message, default: false }]);
23
+ return ok;
24
+ }
25
+
26
+ /** Accept / Edit / Abort menu; returns "accept" | "edit" | "abort". */
27
+ export async function chooseAcceptEditAbort() {
28
+ const { choice } = await inquirer.prompt([
29
+ {
30
+ type: "list",
31
+ name: "choice",
32
+ message: "What would you like to do?",
33
+ choices: [
34
+ { name: "Accept", value: "accept" },
35
+ { name: "Edit", value: "edit" },
36
+ { name: "Abort", value: "abort" },
37
+ ],
38
+ },
39
+ ]);
40
+ return choice;
41
+ }
42
+
43
+ /** Opens $EDITOR on `initial`; returns the trimmed result. */
44
+ export async function editText(message, initial, emptyMessage) {
45
+ const { text } = await inquirer.prompt([
46
+ {
47
+ type: "editor",
48
+ name: "text",
49
+ message,
50
+ default: initial,
51
+ validate: (input) => (input.trim().length > 0 ? true : emptyMessage),
52
+ },
53
+ ]);
54
+ return text.trim();
55
+ }
56
+
57
+ export const success = (text) => console.log(chalk.bgGreen.black(text));
58
+ export const aborted = (text) => console.log(chalk.red(text));
59
+
60
+ /** Prints an error, stops the spinner if it is running, and exits non-zero. */
61
+ export function handleError(error, spinner) {
62
+ if (spinner?.isSpinning) spinner.fail();
63
+
64
+ if (error?.name === "ExitPromptError") {
65
+ console.error(chalk.red("\nCancelled."));
66
+ process.exit(130);
67
+ }
68
+ if (error instanceof UnsafeCommandError) {
69
+ console.error(chalk.red(`Refusing to run: ${error.message}`));
70
+ } else {
71
+ console.error(chalk.red(describeError(error)));
72
+ }
73
+ process.exit(1);
74
+ }
package/index.js DELETED
@@ -1,368 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { program } from "commander";
4
- import { execa, execaCommand } from "execa";
5
- import axios from "axios";
6
- import inquirer from "inquirer";
7
- import chalk from "chalk";
8
- import ora from "ora";
9
-
10
- const API_BRANCH_URL = "https://git-pilot-api.vercel.app/api/pilot-branch";
11
- const API_COMMIT_URL = "https://git-pilot-api.vercel.app/api/pilot-commit";
12
- const API_COMMAND_URL = "https://git-pilot-api.vercel.app/api/pilot-run";
13
- const API_UNDO_URL = "https://git-pilot-api.vercel.app/api/pilot-undo";
14
-
15
- const getStagedDiff = async () => {
16
- try {
17
- const { stdout } = await execa("git", ["diff", "--staged"]);
18
- return stdout;
19
- } catch (error) {
20
- console.error(
21
- chalk.red("Error getting git diff:"),
22
- error?.message || error
23
- );
24
- process.exit(1);
25
- }
26
- };
27
-
28
- program
29
- .name("git-pilot")
30
- .description("An AI-powered Git assistant in your command line.");
31
-
32
- program
33
- .command("commit")
34
- .description("Generate an AI-powered commit message.")
35
- .argument(
36
- "[intent]",
37
- 'Optional: The user\'s intent for the commit (e.g., "add a new feature")'
38
- )
39
- .action(async (intent) => {
40
- const spinner = ora("Analyzing your staged changes...").start();
41
-
42
- const diff = await getStagedDiff();
43
-
44
- if (!diff) {
45
- spinner.warn(
46
- "No staged changes found. Please stage your files with `git add`."
47
- );
48
- process.exit(0);
49
- }
50
-
51
- const MAX_PAYLOAD_SIZE = 4 * 1024 * 1024;
52
- const diffSize = Buffer.byteLength(diff, 'utf8');
53
- let diffToSend = diff;
54
-
55
- if (diffSize > MAX_PAYLOAD_SIZE) {
56
-
57
- const { stdout: diffSummary } = await execa("git", ["diff", "--staged", "--name-status"]);
58
-
59
- diffToSend = "The staged changes are too large to display the full diff. " +
60
- "Please generate a commit message based on the user's intent and this summary of changed files:\n\n" +
61
- diffSummary;
62
- }
63
-
64
- try {
65
- spinner.text = "Generating commit message...";
66
-
67
- const response = await axios.post(
68
- API_COMMIT_URL,
69
- { intent, diff: diffToSend },
70
- { timeout: 30000 }
71
- );
72
-
73
- const suggestedMessage = response.data.message;
74
-
75
- spinner.succeed();
76
-
77
- console.log(chalk.green("\n--- Suggested Message ---"));
78
- console.log(chalk.cyan(suggestedMessage));
79
- console.log(chalk.green("-------------------------\n"));
80
-
81
- const { choice } = await inquirer.prompt([
82
- {
83
- type: "list",
84
- name: "choice",
85
- message: "What would you like to do?",
86
- choices: [
87
- { name: "Accept", value: "accept" },
88
- { name: "Edit", value: "edit" },
89
- { name: "Abort", value: "abort" },
90
- ],
91
- },
92
- ]);
93
-
94
- if (choice === 'abort') {
95
- console.log(chalk.red('Commit aborted by user.'));
96
- process.exit(0);
97
- }
98
-
99
- if (choice == "edit") {
100
- const { editedMessage } = await inquirer.prompt([
101
- {
102
- type: "editor",
103
- name: "editedMessage",
104
- message: "Edit the commit message: ",
105
- default: suggestedMessage,
106
- validate: (input) =>
107
- input.length > 0 ? true : "Commit message cannot be empty.",
108
- },
109
- ]);
110
-
111
- await execa("git", ["commit", "-m", editedMessage]);
112
- console.log(chalk.bgGreen.black("Commit successful!"));
113
- }
114
-
115
- if (choice === "accept") {
116
- await execa("git", ["commit", "-m", suggestedMessage]);
117
- console.log(chalk.bgGreen.black("Commit successful!"));
118
- }
119
-
120
- } catch (error) {
121
- if (error.response) {
122
- console.error(
123
- chalk.red(
124
- `API Error: ${error.response.status} - ${error.response.data.error}`
125
- )
126
- );
127
- } else {
128
- console.error(
129
- chalk.red("An unexpected error occurred: ", error.message)
130
- );
131
- }
132
-
133
- process.exit(1);
134
- }
135
- });
136
-
137
- program
138
- .command("run")
139
- .description("Translate natural language into a Git command.")
140
- .argument(
141
- "<string>",
142
- 'The task you want to perform (e.g. "squash the last 3 commits")'
143
- )
144
- .action(async (request) => {
145
- try {
146
- const spinner = ora("Fetching the right command...").start();
147
-
148
- const response = await axios.post(
149
- API_COMMAND_URL,
150
- { request },
151
- { timeout: 30000 }
152
- );
153
- const suggestedCommand = response.data.command;
154
-
155
- if (suggestedCommand.startsWith("Error:")) {
156
- spinner.fail(suggestedCommand);
157
- process.exit(1);
158
- }
159
-
160
- spinner.succeed();
161
-
162
- console.log(
163
- chalk.yellow.bold(
164
- "\nWARNING: The suggested command will be executed in your shell."
165
- )
166
- );
167
- console.log(
168
- chalk.yellow("Always review commands carefully before confirming.\n")
169
- );
170
-
171
- console.log(chalk.green("\n------ Suggested Command ------"));
172
- console.log(chalk.cyan(suggestedCommand));
173
- console.log(chalk.green("-------------------------------\n"));
174
-
175
- const { confirm } = await inquirer.prompt([
176
- {
177
- type: "confirm",
178
- name: "confirm",
179
- message: "Execute this command?",
180
- default: false,
181
- },
182
- ]);
183
-
184
- if (confirm) {
185
- await execaCommand(suggestedCommand, { stdio: "inherit" });
186
- console.log(chalk.bgGreen.black("Command executed successfully!"));
187
- } else {
188
- console.log(chalk.red("Execution aborted by user."));
189
- }
190
- } catch (error) {
191
- if (error.response) {
192
- console.error(
193
- chalk.red(
194
- `API Error: ${error.response.status} - ${
195
- error.response.data.error || "No message"
196
- }`
197
- )
198
- );
199
- } else {
200
- console.error(
201
- chalk.red(`An unexpected error occurred: ${error.message}`)
202
- );
203
- }
204
-
205
- process.exit(1);
206
- }
207
- });
208
-
209
- program
210
- .command("undo")
211
- .description("Suggests a command to undo your last major Git action")
212
- .action(async () => {
213
- const spinner = ora("Analyzing your recent Git history...").start();
214
-
215
- try {
216
- const { stdout: reflog } = await execa("git", ["reflog", "-n", "15"]);
217
-
218
- spinner.text = "Suggesting an undo command...";
219
-
220
- const response = await axios.post(API_UNDO_URL, { reflog });
221
- const { command, explanation } = response.data;
222
-
223
- if (!command) {
224
- spinner.warn("Couldn't determine a safe action to undo.");
225
- process.exit(0);
226
- }
227
-
228
- spinner.succeed();
229
-
230
- console.log(chalk.green("\n--- Suggested Undo Command ---"));
231
- console.log(chalk.cyan(command));
232
- console.log(chalk.green("------------------------------\n"));
233
- console.log(chalk.yellow(explanation));
234
-
235
- const { confirm } = await inquirer.prompt([
236
- {
237
- type: "confirm",
238
- name: "confirm",
239
- message: "Execute this undo command?",
240
- default: false,
241
- },
242
- ]);
243
-
244
- if (confirm) {
245
- if (command.startsWith('git rebase --abort')) {
246
- try {
247
- const { stdout: gitDirRaw } = await execa('git', ['rev-parse', '--git-dir']);
248
- const gitDir = gitDirRaw.trim();
249
-
250
- const fs = await import('fs/promises');
251
-
252
- const rebaseApply = `${gitDir}/rebase-apply`;
253
- const rebaseMerge = `${gitDir}/rebase-merge`;
254
-
255
- const hasRebaseApply = await fs.access(rebaseApply).then(() => true).catch(() => false);
256
- const hasRebaseMerge = await fs.access(rebaseMerge).then(() => true).catch(() => false);
257
-
258
- if (!hasRebaseApply && !hasRebaseMerge) {
259
- console.log(chalk.yellow('No rebase in progress — abort not necessary.'));
260
- process.exit(0);
261
- }
262
- } catch (e) {
263
- }
264
- }
265
-
266
- await execaCommand(command, { stdio: "inherit" });
267
- console.log(chalk.bgGreen.black("Action successfully undone!"));
268
- } else {
269
- console.log(chalk.red("Execution aborted by user."));
270
- }
271
- } catch (error) {
272
- spinner.fail();
273
-
274
- if (error.response) {
275
- console.error(chalk.red(`API Error: ${error.response.status} - ${error.response.data.error || 'No message'}`));
276
- } else {
277
- console.error(chalk.red(`An unexpected error occurred: ${error.message}`));
278
- }
279
-
280
- process.exit(1);
281
- }
282
- });
283
-
284
- program
285
- .command('branch')
286
- .description('Generates a conventional branch name from a description and creates the branch.')
287
- .argument('<string>', 'A description of the branch\'s purpose (e.g., "fix login bug")')
288
- .action(async (description) => {
289
- const spinner = ora('Generating a conventional branch name...').start();
290
- try {
291
- const response = await axios.post(API_BRANCH_URL, { description });
292
- const { branchName } = response.data;
293
-
294
- spinner.succeed();
295
-
296
- console.log(chalk.green('\n--- Suggested Branch Name ---'));
297
- console.log(chalk.cyan(branchName));
298
- console.log(chalk.green('-----------------------------\n'));
299
-
300
- const { choice } = await inquirer.prompt([
301
- {
302
- type: 'list',
303
- name: 'choice',
304
- message: 'What would you like to do?',
305
- choices: [
306
- { name: 'Accept', value: 'accept' },
307
- { name: 'Edit', value: 'edit' },
308
- { name: 'Abort', value: 'abort' },
309
- ],
310
- },
311
- ]);
312
-
313
- if (choice === 'abort') {
314
- console.log(chalk.red('Branch creation aborted by user.'));
315
- process.exit(0);
316
- }
317
-
318
- let finalBranchName = branchName;
319
-
320
- if (choice === 'edit') {
321
- const editResp = await inquirer.prompt({
322
- type: 'editor',
323
- name: 'editedBranchName',
324
- message: 'Edit the branch name:',
325
- default: branchName,
326
- validate: value => (value && value.trim().length > 0) ? true : 'Branch name cannot be empty'
327
- }, {
328
- onCancel: () => {
329
- console.log(chalk.red('\nEdit cancelled.'));
330
- process.exit(0);
331
- }
332
- });
333
-
334
- finalBranchName = editResp.editedBranchName;
335
- }
336
-
337
- const slugifyBranchName = (name) =>
338
- name
339
- .toLowerCase()
340
- .trim()
341
- .replace(/\s+/g, '-')
342
- .replace(/[^a-z0-9._\-\/]/g, '')
343
- .replace(/\/{2,}/g, '/')
344
- .replace(/^-+|-+$/g, '')
345
- .slice(0, 250);
346
-
347
- const safeBranchName = slugifyBranchName(finalBranchName || '');
348
-
349
- if (!safeBranchName) {
350
- console.log(chalk.red('Resulting branch name is empty or invalid. Aborting.'));
351
- process.exit(1);
352
- }
353
-
354
- await execa('git', ['switch', '-c', safeBranchName]);
355
- console.log(chalk.bgGreen.black(`✔ Switched to new branch '${safeBranchName}'`));
356
-
357
- } catch (error) {
358
- spinner.fail();
359
- if (error.response) {
360
- console.error(chalk.red(`API Error: ${error.response.status} - ${error.response.data.error || 'No message'}`));
361
- } else {
362
- console.error(chalk.red(`An unexpected error occurred: ${error.message}`));
363
- }
364
- process.exit(1);
365
- }
366
- });
367
-
368
- program.parse(process.argv);