@abhaydesu/git-pilot 1.3.1 → 2.0.1

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
@@ -2,120 +2,131 @@
2
2
  <img width="40" height="40" alt="logo" src="./public/logo.png" />
3
3
  </div>
4
4
 
5
- # Git Pilot
5
+ # Git Pilot
6
6
 
7
7
  [![NPM Version](https://img.shields.io/npm/v/@abhaydesu/git-pilot)](https://www.npmjs.com/package/@abhaydesu/git-pilot)
8
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.
9
+ A local-first, AI-powered Git assistant that lives in your command line. Never write a commit message from scratch or look up a complex Git command again.
10
10
 
11
11
  ---
12
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
13
  ## 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.
14
+
15
+ - **Frictionless Commit Fast Path:** Simply run `git pilot` (or `git pilot commit`) with staged changes. It analyzes your staged diff and suggests a conventional commit message.
16
+ - **Bare Intent & Context Support:** Add optional intent without ceremony: `git pilot add auth throttling` or `git pilot -m "fix login bug"` (unquoted multi-word `-m add auth throttling` is also supported).
17
+ - **Scriptable Dry-Run Mode:** `git pilot --dry-run` sends spinners and warnings to `stderr` and writes only the generated commit message to `stdout`.
18
+ - **Personal Key & Local Execution:** Connects directly to Google Gemini (`https://generativelanguage.googleapis.com`) using your personal API key. Git Pilot's hosted servers are never in the request path.
19
+ - **OS Credential Manager Storage:** Your API key is stored locally in your operating system's native credential store (macOS Keychain, Windows Credential Manager, Linux Secret Service) without exposing secrets in process arguments.
20
+ - **Best-Effort Pre-flight Secret Screening:** Staged diffs are screened locally for common high-confidence credential patterns (such as private keys, AWS access keys, GitHub tokens, Slack tokens, and Google API keys) before transmission.
21
+ - **Best-Effort Index Modification Detection:** Checks your staged Git index tree hash (`git write-tree`) before generation and immediately before invoking `git commit` to detect concurrent staging changes.
22
+ - **Natural Language to Git Commands:** Translate plain English requests like "squash the last 3 commits" into precise Git commands with `git pilot run <query>`.
23
+ - **Safe Undo:** `git pilot undo` parses the structured action field of your latest reflog entry and checks live repository state before suggesting an undo command.
24
+ - **Intelligent Branching:** Describe your goal, and `git pilot branch` generates a clean, conventional branch name.
25
+ - **Safe Subprocess Execution:** Commands are validated against an allowlist and executed via `execa` using direct `argv` arrays (never through a shell). Provider API keys are stripped from the environment of all Git subprocesses and hooks, and diff collection disables external diff and textconv helpers (`--no-ext-diff --no-textconv`).
24
26
 
25
27
  ## Installation
26
- Make sure you have Node.js (v20+) and Git installed. Then, run the following command to install Git Pilot globally:
28
+
29
+ Requires Node.js (v20+) and Git. Install Git Pilot globally via npm:
27
30
 
28
31
  ```bash
29
32
  npm install -g @abhaydesu/git-pilot
30
33
  ```
31
34
 
32
- ### ◾ Usage
35
+ ## First-Run Setup
33
36
 
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.
37
+ On your first `git pilot` run with staged changes, Git Pilot starts guided setup if it does not find an API key. You can also start it directly:
36
38
 
37
- ##### With an Intent
38
- Provide a brief description of your changes, and the AI will use it to write the commit message.
39
+ ```bash
40
+ git pilot setup
41
+ ```
39
42
 
40
- ```Bash
43
+ Setup shows the provider, selected model, and the data sent before asking for consent. Enter your Gemini API key at the masked prompt. Git Pilot validates the key and model with Google before saving the key to your OS credential manager; if validation fails, the key is not saved. The CLI currently defaults to `gemini-3.5-flash-lite`.
41
44
 
42
- git pilot commit "add new user profile page"
45
+ Get a Gemini API key at [Google AI Studio](https://aistudio.google.com/app/apikey). Model availability depends on your Google project. Google currently limits access to Gemini 2.5 models for many new projects; if setup reports that a model is unavailable, check model access in Google AI Studio or set `model` in your Git Pilot config to a model available to your key, then run `git pilot setup` again. Config is stored at `~/.config/git-pilot/config.json` on macOS/Linux, or `%APPDATA%\git-pilot\config.json` on Windows. The previous built-in model is migrated automatically when an older config is loaded.
46
+
47
+ To inspect or remove your stored credentials at any time:
48
+
49
+ ```bash
50
+ git pilot auth status
51
+ git pilot auth remove
43
52
  ```
44
53
 
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.
54
+ ## Usage
47
55
 
48
- ```Bash
56
+ ### Generating a Commit Message (Fast Path)
49
57
 
50
- git pilot commit
51
- ```
52
- After running, you can accept, edit, or abort the suggested commit.
58
+ Stage your changes (`git add <files>`), then run `git pilot`:
53
59
 
60
+ ```bash
61
+ # Analyze staged diff automatically
62
+ git pilot
54
63
 
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.
64
+ # Provide intent as bare words (no `commit` subcommand needed)
65
+ git pilot add user profile page
57
66
 
58
- ```Bash
67
+ # Or use -m / --message-context
68
+ git pilot -m "add user profile page"
59
69
 
60
- git pilot run "cherry-pick the last commit from the main branch"
70
+ # Output only the message to stdout without committing
71
+ git pilot --dry-run
61
72
  ```
62
73
 
63
- #### *Creating a New Branch*
64
- Describe the purpose of your new branch, and let the AI generate a conventional name.
74
+ After generation, an interactive menu (defaulting to **Abort** so pressing Enter accidentally never creates an unintended commit) allows you to:
65
75
 
66
- ```Bash
76
+ 1. **Accept and commit:** Commits the staged changes using `git commit -m`.
77
+ 2. **Edit in $EDITOR:** Opens your configured editor to modify the message.
78
+ 3. **Regenerate message:** Asks Gemini for another suggestion.
79
+ 4. **Abort:** Cancels without modifying your Git repository.
67
80
 
68
- git pilot branch "create a new feature for the auth system"
69
- ```
81
+ Google receives each staged diff and intent sent for generation directly from the CLI. Calls use your Gemini API quota and follow your Google AI plan's limits and pricing. See Google's [Gemini API pricing](https://ai.google.dev/gemini-api/docs/pricing).
70
82
 
71
- #### *Undoing a Mistake*
83
+ ### Running a Git Command
72
84
 
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.
85
+ ```bash
86
+ git pilot run "cherry-pick the last commit from main"
87
+ ```
74
88
 
75
- ```Bash
89
+ ### Creating a Conventional Branch
76
90
 
77
- git pilot undo
91
+ ```bash
92
+ git pilot branch "create a new feature for the auth system"
78
93
  ```
79
94
 
80
- Check the installed version with `git pilot --version`, or see all commands with `git pilot --help`.
95
+ ### Undoing a Mistake
81
96
 
82
- ### ◾ Configuration
97
+ ```bash
98
+ git pilot undo
99
+ ```
83
100
 
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"`. |
101
+ ## Privacy & Security Model
87
102
 
88
- `GIT_PILOT_API_URL` must use `https` (plain `http` is accepted only for `localhost`), because your staged diffs are sent to that server.
103
+ - **Credential Storage & Transport:** The API key is stored locally in your operating system credential manager (macOS Keychain, Windows Credential Manager, Linux Secret Service). When making an AI request, your key is transmitted directly over TLS to Google's official Gemini endpoint (`https://generativelanguage.googleapis.com`) in the `x-goog-api-key` header; Git Pilot's hosted servers never receive your API key or repository diffs in personal-key mode.
104
+ - **Data Sent to Provider:** Only the staged diff (or a bounded file-status summary for diffs exceeding 800,000 characters or 1 MB UTF-8 JSON size) and optional user intent are transmitted when generating commit messages. Untracked and unstaged files are never read or sent. Before the first provider request, Git Pilot requires acknowledging a versioned data-sharing notice regardless of whether the key is stored in the OS credential manager or supplied via `GEMINI_API_KEY`. Provider data retention and model training terms are governed by Google's [Gemini API Terms of Service](https://ai.google.dev/terms).
105
+ - **Best-Effort Secret Screening:** Before sending any diff, Git Pilot screens staged changes for common high-confidence credential patterns. This heuristic screening is a best-effort safeguard and **cannot guarantee that a diff is free of all secrets or proprietary data**—always review staged changes before running `git pilot`.
106
+ - **Git Subprocess & Hook Isolation:** Git Pilot strips `GEMINI_API_KEY` and other provider secret variables from the environment of every Git subprocess (including `git commit` hooks) and passes `--no-ext-diff --no-textconv` whenever collecting diffs.
107
+ - **Index Modification Check:** Git Pilot records `git write-tree` before calling the provider and re-checks it immediately before invoking `git commit`. This provides best-effort detection if files were staged or unstaged while reviewing the proposal.
89
108
 
90
- 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.
109
+ ## Configuration & Environment Variables
91
110
 
92
- ### ◾ Privacy and Security
111
+ | Variable | Description |
112
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
113
+ | `GEMINI_API_KEY` | Optional ephemeral override for CI/headless environments. Takes precedence over (and shadows) the OS credential store without persisting to disk. `git pilot auth status` and `git pilot auth remove` distinguish between environment and OS-stored credentials. |
114
+ | `NO_COLOR` | Disables ANSI color codes in terminal output (complies with [no-color.org](https://no-color.org)). |
93
115
 
94
- * `git pilot commit` sends your **staged diff** (and intent) to the Git Pilot API, which forwards it to Google's Gemini API. `run` sends your request text, `undo` your last 15 reflog lines, and `branch` your description. **Don't stage secrets or code you can't share** when using `commit`.
95
- * Suggested commands are validated locally before you are even asked to confirm: only `git` with a known subcommand (`add`, `commit`, `push`, ...), no shell operators, no leading global options (`git -c ...`), and no options that run other programs (`--upload-pack`, `--exec`, ...).
96
- * Commands run without a shell, and only after you confirm.
97
- * To report a vulnerability, please open a private security advisory on the GitHub repository rather than a public issue.
116
+ Non-secret configuration is saved in your platform's standard configuration directory (`~/.config/git-pilot/config.json` on POSIX systems with restrictive `0600` permissions).
98
117
 
99
- ### ◾ Development
118
+ ## Development
100
119
 
101
120
  ```bash
102
121
  git clone https://github.com/abhaydesu/git-pilot-cli.git
103
122
  cd git-pilot-cli
104
123
  npm install
105
- node bin/git-pilot.js --help # or `npm link` to get a global `git-pilot`
124
+ npm test # Run unit tests
125
+ npm run test:e2e # Run end-to-end PTY tests (requires expect)
126
+ npm run lint # Run ESLint
127
+ npm run format # Format with Prettier
106
128
  ```
107
129
 
108
- | Script | What it does |
109
- | --- | --- |
110
- | `npm test` | Unit tests (`node --test`) |
111
- | `npm run test:e2e` | Runs the real CLI in a PTY against a fake API and a scratch repo (needs `expect`; skipped if missing) |
112
- | `npm run lint` | ESLint |
113
- | `npm run format` | Prettier |
114
-
115
- Code layout: `bin/` (entry point), `src/commands/` (one file per command), `src/lib/` (`api`, `git`, `exec`, `ui`).
116
-
117
- ### ◾ How It Works
118
- 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.
130
+ ## License
119
131
 
120
- ### ◾ License
121
- This project is licensed under the MIT License.
132
+ MIT License.
package/bin/git-pilot.js CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { buildProgram } from "../src/cli.js";
2
+ import { buildProgram, normalizeArgs } from "../src/cli.js";
3
+ import { handleError } from "../src/lib/ui.js";
3
4
 
4
- await buildProgram().parseAsync(process.argv);
5
+ try {
6
+ const normalized = normalizeArgs(process.argv);
7
+ await buildProgram().parseAsync(normalized);
8
+ } catch (err) {
9
+ handleError(err);
10
+ }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@abhaydesu/git-pilot",
3
- "version": "1.3.1",
3
+ "version": "2.0.1",
4
4
  "description": "AI-powered Git assistant: commit messages, natural-language git commands, undo suggestions and branch names.",
5
5
  "main": "src/cli.js",
6
6
  "scripts": {
7
- "test": "node --test \"test/*.test.js\"",
7
+ "test": "node scripts/run-tests.js",
8
8
  "test:e2e": "bash test/e2e/run.sh",
9
9
  "lint": "eslint .",
10
10
  "format": "prettier --write ."
@@ -31,7 +31,7 @@
31
31
  "execa": "^9.6.0",
32
32
  "inquirer": "^12.11.1",
33
33
  "ora": "^9.4.1",
34
- "shell-quote": "^1.10.0"
34
+ "shell-quote": "^1.12.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@eslint/js": "^10.0.1",
package/src/cli.js CHANGED
@@ -4,37 +4,133 @@ import { commitCommand } from "./commands/commit.js";
4
4
  import { runCommand } from "./commands/run.js";
5
5
  import { undoCommand } from "./commands/undo.js";
6
6
  import { branchCommand } from "./commands/branch.js";
7
+ import { setupCommand } from "./commands/setup.js";
8
+ import { authStatusCommand, authRemoveCommand } from "./commands/auth.js";
7
9
 
8
10
  const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
9
11
 
10
- export function buildProgram() {
12
+ const SUBCOMMANDS = new Set(["commit", "run", "undo", "branch", "setup", "auth", "help"]);
13
+
14
+ const GLOBAL_FLAGS = new Set(["-h", "--help", "-V", "--version"]);
15
+ const COMMIT_FLAGS = new Set(["-m", "--message-context", "--dry-run", "--accept-notice"]);
16
+
17
+ function isKnownCommitFlag(arg) {
18
+ return COMMIT_FLAGS.has(arg) || arg.startsWith("--message-context=") || arg.startsWith("-m=");
19
+ }
20
+
21
+ /**
22
+ * Combines `-m/--message-context` and any remaining positional intent words
23
+ * so multi-word unquoted `-m add login throttling` never drops words.
24
+ */
25
+ export function extractCommitIntent(intentParts, options = {}) {
26
+ const parts = [];
27
+ if (typeof options.messageContext === "string" && options.messageContext.trim()) {
28
+ parts.push(options.messageContext.trim());
29
+ }
30
+
31
+ if (Array.isArray(intentParts)) {
32
+ for (const part of intentParts) {
33
+ if (typeof part === "string" && part.trim()) {
34
+ parts.push(part.trim());
35
+ }
36
+ }
37
+ } else if (typeof intentParts === "string" && intentParts.trim()) {
38
+ parts.push(intentParts.trim());
39
+ }
40
+
41
+ return parts.length > 0 ? parts.join(" ") : undefined;
42
+ }
43
+
44
+ /**
45
+ * Normalizes CLI argv so bare `git pilot`, `git pilot add login throttling`,
46
+ * or `git pilot -m add login throttling` dispatches directly to `commit`.
47
+ * Unknown flags (e.g. `--unknown`) are left untouched so Commander reports an error.
48
+ */
49
+ export function normalizeArgs(argv) {
50
+ const [nodeBin, script, ...rest] = argv;
51
+ if (!rest || rest.length === 0) {
52
+ return [nodeBin, script, "commit"];
53
+ }
54
+
55
+ const firstArg = rest[0];
56
+ if (GLOBAL_FLAGS.has(firstArg) || SUBCOMMANDS.has(firstArg)) {
57
+ return argv;
58
+ }
59
+
60
+ // If the first argument is an unknown flag, let Commander reject it with help
61
+ if (firstArg.startsWith("-") && !isKnownCommitFlag(firstArg)) {
62
+ return argv;
63
+ }
64
+
65
+ // Preserve individual tokens so flags like -m and --dry-run are parsed by `commit`
66
+ // and all remaining positional tokens are joined by extractCommitIntent.
67
+ return [nodeBin, script, "commit", ...rest];
68
+ }
69
+
70
+ export function buildProgram({
71
+ handlers = {
72
+ commit: commitCommand,
73
+ setup: setupCommand,
74
+ authStatus: authStatusCommand,
75
+ authRemove: authRemoveCommand,
76
+ run: runCommand,
77
+ undo: undoCommand,
78
+ branch: branchCommand,
79
+ },
80
+ } = {}) {
11
81
  const program = new Command()
12
82
  .name("git-pilot")
13
- .description("An AI-powered Git assistant in your command line.")
83
+ .description("A local-first AI Git assistant in your command line.")
14
84
  .version(version);
15
85
 
16
86
  program
17
87
  .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);
88
+ .description("Generate an AI-powered commit message from staged changes.")
89
+ .argument("[intent...]", 'Optional intent for the commit (e.g., "add a new feature")')
90
+ .option("-m, --message-context <context>", "Provide context for the commit message")
91
+ .option("--dry-run", "Output only the generated message to stdout without committing")
92
+ .option(
93
+ "--accept-notice",
94
+ "Explicitly acknowledge sending staged diffs to Google Gemini for non-interactive runs"
95
+ )
96
+ .action(async (intentParts, options) => {
97
+ const intent = extractCommitIntent(intentParts, options);
98
+ await handlers.commit(intent, options);
99
+ });
100
+
101
+ program
102
+ .command("setup")
103
+ .description("Guided interactive setup for your Google Gemini API key.")
104
+ .action(handlers.setup);
105
+
106
+ const auth = program.command("auth").description("Inspect or manage stored AI credentials.");
107
+
108
+ auth
109
+ .command("status")
110
+ .description("Check credential storage and API key status.")
111
+ .action(handlers.authStatus);
112
+
113
+ auth
114
+ .command("remove")
115
+ .description("Remove stored API key from the local OS credential manager.")
116
+ .action(handlers.authRemove);
21
117
 
22
118
  program
23
119
  .command("run")
24
120
  .description("Translate natural language into a Git command.")
25
121
  .argument("<string>", 'The task you want to perform (e.g. "squash the last 3 commits")')
26
- .action(runCommand);
122
+ .action(handlers.run);
27
123
 
28
124
  program
29
125
  .command("undo")
30
- .description("Suggests a command to undo your last major Git action")
31
- .action(undoCommand);
126
+ .description("Suggests a command to undo your last major Git action.")
127
+ .action(handlers.undo);
32
128
 
33
129
  program
34
130
  .command("branch")
35
131
  .description("Generates a conventional branch name from a description and creates the branch.")
36
132
  .argument("<string>", 'A description of the branch\'s purpose (e.g., "fix login bug")')
37
- .action(branchCommand);
133
+ .action(handlers.branch);
38
134
 
39
135
  return program;
40
136
  }
@@ -0,0 +1,132 @@
1
+ import chalk from "chalk";
2
+ import { loadConfig, hasAcceptedCurrentNotice } from "../lib/config.js";
3
+ import {
4
+ getEnvApiKey,
5
+ getStoredApiKey,
6
+ deleteApiKey,
7
+ maskApiKey,
8
+ getCredentialBackend,
9
+ } from "../lib/credentials.js";
10
+ import { confirm, success, warn, handleError } from "../lib/ui.js";
11
+
12
+ function getStoreDisplayName(platform) {
13
+ if (platform === "darwin") return "macOS Keychain";
14
+ if (platform === "linux") return "Linux Secret Service";
15
+ if (platform === "win32") return "Windows Credential Manager";
16
+ return "Custom / In-Memory Store";
17
+ }
18
+
19
+ export async function authStatusCommand() {
20
+ try {
21
+ const config = loadConfig();
22
+ const backend = getCredentialBackend();
23
+ const backendStatus = await backend.isAvailable();
24
+ const envKey = getEnvApiKey(config.provider);
25
+ let storedKey = null;
26
+ if (backendStatus.available) {
27
+ storedKey = await getStoredApiKey(config.provider);
28
+ }
29
+
30
+ console.log(chalk.bold.cyan("\nGit Pilot Authentication Status"));
31
+ console.log(
32
+ chalk.dim("────────────────────────────────────────────────────────────────────────")
33
+ );
34
+
35
+ console.log(`Provider: ${chalk.green(config.provider)}`);
36
+ console.log(`Model: ${chalk.green(config.model)}`);
37
+
38
+ const storeName = getStoreDisplayName(backend.platform);
39
+ console.log(
40
+ `Credential Store: ${
41
+ backendStatus.available
42
+ ? chalk.green(storeName)
43
+ : chalk.red(`${storeName} (Unavailable: ${backendStatus.reason || "error"})`)
44
+ }`
45
+ );
46
+
47
+ if (storedKey) {
48
+ console.log(
49
+ `Stored Key (OS): ${chalk.cyan(maskApiKey(storedKey.key))} (${chalk.green("present in OS Credential Manager")})`
50
+ );
51
+ } else {
52
+ console.log(`Stored Key (OS): ${chalk.dim("None")}`);
53
+ }
54
+
55
+ if (envKey) {
56
+ const shadowNote = storedKey ? " — active, shadows stored OS credential" : " — active";
57
+ console.log(
58
+ `Env Override: ${chalk.cyan(maskApiKey(envKey.key))} (${chalk.yellow(`${envKey.envVarName}${shadowNote}`)})`
59
+ );
60
+ } else {
61
+ console.log(`Env Override: ${chalk.dim("Not set")}`);
62
+ }
63
+
64
+ const effectiveKey = envKey || storedKey;
65
+ if (effectiveKey) {
66
+ const activeSource =
67
+ effectiveKey.source === "env"
68
+ ? `Environment (${envKey.envVarName})`
69
+ : "OS Credential Manager";
70
+ console.log(
71
+ `Effective Key: ${chalk.cyan(maskApiKey(effectiveKey.key))} (${chalk.green(activeSource)})`
72
+ );
73
+ } else {
74
+ console.log(`Effective Key: ${chalk.red("Not configured")}`);
75
+ console.log(chalk.dim("\nRun `git pilot setup` to configure your API key."));
76
+ }
77
+
78
+ console.log(
79
+ `Privacy Notice: ${
80
+ hasAcceptedCurrentNotice(config)
81
+ ? chalk.green(
82
+ `Accepted v${config.dataSharingNoticeVersion} (${config.noticeAcceptedAt || "yes"})`
83
+ )
84
+ : chalk.yellow("Pending acknowledgement")
85
+ }`
86
+ );
87
+ console.log(
88
+ chalk.dim("────────────────────────────────────────────────────────────────────────\n")
89
+ );
90
+ } catch (err) {
91
+ handleError(err);
92
+ }
93
+ }
94
+
95
+ export async function authRemoveCommand() {
96
+ try {
97
+ const config = loadConfig();
98
+ const envKey = getEnvApiKey(config.provider);
99
+ const storedKey = await getStoredApiKey(config.provider);
100
+
101
+ if (!storedKey) {
102
+ if (envKey) {
103
+ warn(
104
+ `No API key is stored in the OS credential manager. The active key is provided via the ${envKey.envVarName} environment variable — unset it in your shell profile to remove it.`
105
+ );
106
+ } else {
107
+ warn("No stored API key was found in the OS credential manager.");
108
+ }
109
+ return;
110
+ }
111
+
112
+ const ok = await confirm(
113
+ `Are you sure you want to remove the stored ${config.provider} API key (${maskApiKey(storedKey.key)}) from the OS credential manager?`,
114
+ false
115
+ );
116
+ if (!ok) {
117
+ console.log("Removal cancelled.");
118
+ return;
119
+ }
120
+
121
+ await deleteApiKey(config.provider);
122
+ success("API key removed successfully from the OS credential manager.");
123
+
124
+ if (envKey) {
125
+ warn(
126
+ `Note: ${envKey.envVarName} is still set in your environment (${maskApiKey(envKey.key)}) and will continue to be used until unset.`
127
+ );
128
+ }
129
+ } catch (err) {
130
+ handleError(err);
131
+ }
132
+ }
@@ -1,6 +1,8 @@
1
1
  import chalk from "chalk";
2
- import { callApi, requireString } from "../lib/api.js";
2
+ import { GeminiProvider, stripFences } from "../lib/provider.js";
3
3
  import * as git from "../lib/git.js";
4
+ import { branchPrompt } from "../lib/prompts.js";
5
+ import { ensureProviderReady } from "./setup.js";
4
6
  import {
5
7
  spin,
6
8
  showBlock,
@@ -12,29 +14,42 @@ import {
12
14
  } from "../lib/ui.js";
13
15
 
14
16
  export async function branchCommand(description) {
15
- const spinner = spin("Generating a conventional branch name...");
17
+ let spinner;
16
18
  try {
17
- const branchName = requireString(await callApi("pilot-branch", { description }), "branchName");
19
+ const ready = await ensureProviderReady({
20
+ dataDescription: "branch description",
21
+ });
22
+ if (!ready) return;
23
+ const { config, keyInfo } = ready;
24
+
25
+ spinner = spin("Generating a conventional branch name...");
26
+ const provider = new GeminiProvider({
27
+ apiKey: keyInfo.key,
28
+ model: config.model,
29
+ });
30
+
31
+ const prompt = branchPrompt({ description });
32
+ const rawBranchName = stripFences(await provider.generate(prompt));
18
33
  spinner.succeed();
19
34
 
20
- showBlock("Suggested Branch Name", branchName);
35
+ showBlock("Suggested Branch Name", rawBranchName);
21
36
 
22
37
  const choice = await chooseAcceptEditAbort();
23
38
  if (choice === "abort") return aborted("Branch creation aborted by user.");
24
39
 
25
40
  const chosen =
26
41
  choice === "edit"
27
- ? await editText("Edit the branch name:", branchName, "Branch name cannot be empty")
28
- : branchName;
42
+ ? await editText("Edit the branch name:", rawBranchName, "Branch name cannot be empty")
43
+ : rawBranchName;
29
44
 
30
45
  const safeName = git.slugifyBranchName(chosen);
31
46
  if (!safeName) {
32
- console.log(chalk.red("Resulting branch name is empty or invalid. Aborting."));
47
+ console.error(chalk.red("Resulting branch name is empty or invalid. Aborting."));
33
48
  process.exit(1);
34
49
  }
35
50
 
36
51
  await git.createBranch(safeName);
37
- success(`✔ Switched to new branch '${safeName}'`);
52
+ success(`Switched to new branch '${safeName}'`);
38
53
  } catch (error) {
39
54
  handleError(error, spinner);
40
55
  }