@abhaydesu/git-pilot 1.3.1 → 2.0.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 +77 -66
- package/bin/git-pilot.js +8 -2
- package/package.json +1 -1
- package/src/cli.js +105 -9
- package/src/commands/auth.js +132 -0
- package/src/commands/branch.js +23 -8
- package/src/commands/commit.js +209 -26
- package/src/commands/run.js +36 -7
- package/src/commands/setup.js +208 -0
- package/src/commands/undo.js +141 -11
- package/src/lib/config.js +191 -0
- package/src/lib/credentials.js +440 -0
- package/src/lib/exec.js +10 -3
- package/src/lib/git.js +127 -9
- package/src/lib/prompts.js +72 -0
- package/src/lib/provider.js +353 -0
- package/src/lib/sanitizer.js +37 -0
- package/src/lib/scanner.js +74 -0
- package/src/lib/ui.js +112 -14
- package/src/lib/api.js +0 -59
package/src/commands/commit.js
CHANGED
|
@@ -1,50 +1,233 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import * as defaultGit from "../lib/git.js";
|
|
4
|
+
import { GeminiProvider, fitsInGeminiRequest, boundSummary } from "../lib/provider.js";
|
|
5
|
+
import { scanDiffForSecrets } from "../lib/scanner.js";
|
|
6
|
+
import { sanitizeTerminalText } from "../lib/sanitizer.js";
|
|
7
|
+
import { commitPrompt } from "../lib/prompts.js";
|
|
8
|
+
import { ensureProviderReady } from "./setup.js";
|
|
3
9
|
import {
|
|
4
10
|
spin,
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
showPanel,
|
|
12
|
+
chooseCommitAction,
|
|
7
13
|
editText,
|
|
8
14
|
success,
|
|
9
15
|
aborted,
|
|
16
|
+
warn,
|
|
10
17
|
handleError,
|
|
11
18
|
} from "../lib/ui.js";
|
|
12
19
|
|
|
13
|
-
const OVERSIZED_PREFIX =
|
|
20
|
+
export const OVERSIZED_PREFIX =
|
|
14
21
|
"The staged changes are too large to display the full diff. " +
|
|
15
22
|
"Please generate a commit message based on the user's intent and this summary of changed files:\n\n";
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Builds the commit prompt, falling back to a byte-bounded file summary if the
|
|
26
|
+
* full diff exceeds character or UTF-8 JSON request byte limits.
|
|
27
|
+
*/
|
|
28
|
+
export async function buildBoundedCommitPrompt({ intent, diff, getSummary }) {
|
|
29
|
+
const fullPrompt = commitPrompt({ intent, diff });
|
|
30
|
+
if (fitsInGeminiRequest(fullPrompt, diff)) {
|
|
31
|
+
return fullPrompt;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const rawSummary = await getSummary();
|
|
35
|
+
const safeSummary = boundSummary(rawSummary);
|
|
36
|
+
return commitPrompt({
|
|
37
|
+
intent,
|
|
38
|
+
diff: OVERSIZED_PREFIX + safeSummary,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function commitCommand(intent, options = {}, deps = {}) {
|
|
43
|
+
const git = deps.git || defaultGit;
|
|
44
|
+
const createProvider = deps.createProvider || ((opts) => new GeminiProvider(opts));
|
|
45
|
+
const chooseAction = deps.chooseAction || chooseCommitAction;
|
|
46
|
+
const editMessage = deps.editMessage || editText;
|
|
47
|
+
const ensureReady = deps.ensureReady || ensureProviderReady;
|
|
48
|
+
|
|
49
|
+
let activeSpinner;
|
|
19
50
|
try {
|
|
51
|
+
// Pre-flight 1: Check Git repository
|
|
52
|
+
if (!(await git.isInsideGitRepo())) {
|
|
53
|
+
console.error(
|
|
54
|
+
chalk.red(
|
|
55
|
+
"Fatal: Not inside a Git repository. Please run git-pilot inside a Git repository."
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Pre-flight 2: Check active Git states (rebase, merge, etc.)
|
|
62
|
+
const states = await git.getGitRepoState();
|
|
63
|
+
if (states.length > 0) {
|
|
64
|
+
warn(
|
|
65
|
+
`Notice: Git operation in progress (${states.join(", ")}). Your commit will apply to this operation.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Pre-flight 3: Check staged changes
|
|
70
|
+
const stagedFiles = await git.getStagedFiles();
|
|
71
|
+
if (stagedFiles.length === 0) {
|
|
72
|
+
console.log(chalk.yellow("\nNo staged changes found."));
|
|
73
|
+
console.log(
|
|
74
|
+
chalk.dim("Stage the changes you want to commit using: ") + chalk.cyan("git add <files>")
|
|
75
|
+
);
|
|
76
|
+
console.log("\nAvailable Git Pilot commands:");
|
|
77
|
+
console.log(
|
|
78
|
+
` ${chalk.bold("git pilot run <query>")} Translate natural language into a Git command`
|
|
79
|
+
);
|
|
80
|
+
console.log(` ${chalk.bold("git pilot undo")} Undo your last major Git action`);
|
|
81
|
+
console.log(` ${chalk.bold("git pilot branch <name>")} Create a conventional branch`);
|
|
82
|
+
console.log(
|
|
83
|
+
` ${chalk.bold("git pilot setup")} Configure your AI provider and key`
|
|
84
|
+
);
|
|
85
|
+
console.log(
|
|
86
|
+
` ${chalk.bold("git pilot auth status")} Inspect active credential status\n`
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Pre-flight 4: Read staged diff & best-effort local secret scan BEFORE setup/network
|
|
20
92
|
const diff = await git.getStagedDiff();
|
|
21
|
-
|
|
22
|
-
|
|
93
|
+
const scanResult = scanDiffForSecrets(diff);
|
|
94
|
+
if (!scanResult.clean) {
|
|
95
|
+
console.error(
|
|
96
|
+
chalk.bgRed.black.bold("\n SECURITY ALERT: Refusing to send diff to AI provider \n")
|
|
97
|
+
);
|
|
98
|
+
console.error(
|
|
99
|
+
chalk.red(
|
|
100
|
+
"Potential high-confidence secrets or credentials were detected in your staged changes:\n"
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
for (const finding of scanResult.findings) {
|
|
104
|
+
console.error(
|
|
105
|
+
` ${chalk.bold.red(finding.rule)} at line ${finding.line}: ${chalk.yellow(finding.preview)}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
console.error(
|
|
109
|
+
chalk.dim(
|
|
110
|
+
"\nPlease unstage or remove sensitive material before generating commit messages."
|
|
111
|
+
)
|
|
112
|
+
);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Non-interactive check (unless --dry-run is explicitly requested)
|
|
117
|
+
if (!process.stdout.isTTY && !options.dryRun && !deps.allowNonTty) {
|
|
118
|
+
console.error(
|
|
119
|
+
chalk.red(
|
|
120
|
+
"Error: Interactive commit flow requires a TTY terminal. Use --dry-run to output only the generated message."
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Pre-flight 5: Ensure API key and versioned data-sharing consent
|
|
127
|
+
const ready = await ensureReady({
|
|
128
|
+
dataDescription: "staged git diff and commit intent",
|
|
129
|
+
nonInteractive: Boolean(options.dryRun),
|
|
130
|
+
acceptNotice: Boolean(options.acceptNotice),
|
|
131
|
+
});
|
|
132
|
+
if (!ready) {
|
|
23
133
|
return;
|
|
24
134
|
}
|
|
135
|
+
const { config, keyInfo } = ready;
|
|
136
|
+
|
|
137
|
+
// Best-effort snapshot of staged index identity before generation
|
|
138
|
+
const initialSnapshot = await git.getStagedTreeHash();
|
|
139
|
+
|
|
140
|
+
const provider = createProvider({
|
|
141
|
+
apiKey: keyInfo.key,
|
|
142
|
+
model: config.model,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
activeSpinner = spin(
|
|
146
|
+
`Analyzing ${stagedFiles.length} staged file${stagedFiles.length === 1 ? "" : "s"} with ${config.provider}...`
|
|
147
|
+
);
|
|
25
148
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
149
|
+
const prompt = await buildBoundedCommitPrompt({
|
|
150
|
+
intent,
|
|
151
|
+
diff,
|
|
152
|
+
getSummary: () => git.getStagedSummary(),
|
|
153
|
+
});
|
|
154
|
+
let currentMessage = sanitizeTerminalText(await provider.generate(prompt));
|
|
155
|
+
|
|
156
|
+
if (options.dryRun) {
|
|
157
|
+
activeSpinner.stop();
|
|
158
|
+
process.stdout.write(`${currentMessage}\n`);
|
|
159
|
+
return;
|
|
29
160
|
}
|
|
30
161
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
162
|
+
activeSpinner.succeed();
|
|
163
|
+
|
|
164
|
+
// Interactive review loop
|
|
165
|
+
while (true) {
|
|
166
|
+
showPanel({
|
|
167
|
+
title: "Proposed Commit Message",
|
|
168
|
+
details: [
|
|
169
|
+
`Provider: ${config.provider} (${config.model})`,
|
|
170
|
+
`Staged files: ${stagedFiles.length} (${stagedFiles.slice(0, 3).join(", ")}${stagedFiles.length > 3 ? "..." : ""})`,
|
|
171
|
+
intent ? `User intent: "${intent}"` : "User intent: (none)",
|
|
172
|
+
],
|
|
173
|
+
content: currentMessage,
|
|
174
|
+
});
|
|
34
175
|
|
|
35
|
-
|
|
176
|
+
const action = await chooseAction();
|
|
36
177
|
|
|
37
|
-
|
|
38
|
-
|
|
178
|
+
if (action === "abort") {
|
|
179
|
+
return aborted("Commit cancelled by user. No changes were committed.");
|
|
180
|
+
}
|
|
39
181
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
182
|
+
if (action === "edit") {
|
|
183
|
+
currentMessage = await editMessage(
|
|
184
|
+
"Edit the commit message: ",
|
|
185
|
+
currentMessage,
|
|
186
|
+
"Commit message cannot be empty."
|
|
187
|
+
);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
44
190
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
191
|
+
if (action === "regenerate") {
|
|
192
|
+
activeSpinner = spin("Regenerating commit message with Gemini...");
|
|
193
|
+
const regenPrompt = await buildBoundedCommitPrompt({
|
|
194
|
+
intent,
|
|
195
|
+
diff,
|
|
196
|
+
getSummary: () => git.getStagedSummary(),
|
|
197
|
+
});
|
|
198
|
+
currentMessage = sanitizeTerminalText(await provider.generate(regenPrompt));
|
|
199
|
+
activeSpinner.succeed("New message generated.");
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (action === "accept") {
|
|
204
|
+
if (deps.beforeSnapshotRecheck) {
|
|
205
|
+
await deps.beforeSnapshotRecheck();
|
|
206
|
+
}
|
|
207
|
+
// Best-effort re-check of index snapshot immediately before invoking `git commit`
|
|
208
|
+
const postSnapshot = await git.getStagedTreeHash();
|
|
209
|
+
if (postSnapshot !== initialSnapshot) {
|
|
210
|
+
console.error(
|
|
211
|
+
chalk.red(
|
|
212
|
+
"\nError: Staged changes have changed since this commit message was generated."
|
|
213
|
+
)
|
|
214
|
+
);
|
|
215
|
+
console.error(
|
|
216
|
+
chalk.yellow(
|
|
217
|
+
"Please run `git pilot` again to generate a message for the updated index."
|
|
218
|
+
)
|
|
219
|
+
);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
activeSpinner = spin("Creating Git commit...");
|
|
224
|
+
await git.commit(currentMessage);
|
|
225
|
+
activeSpinner.succeed("Commit created successfully!");
|
|
226
|
+
success(`Committed with message: "${currentMessage.split("\n")[0]}"`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
handleError(err, activeSpinner);
|
|
49
232
|
}
|
|
50
233
|
}
|
package/src/commands/run.js
CHANGED
|
@@ -1,21 +1,50 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
2
|
+
import { GeminiProvider, stripFences } from "../lib/provider.js";
|
|
3
3
|
import { parseGitCommand, runGit } from "../lib/exec.js";
|
|
4
|
+
import { runPrompt, runRetryPrompt } from "../lib/prompts.js";
|
|
5
|
+
import { ensureProviderReady } from "./setup.js";
|
|
4
6
|
import { spin, showBlock, confirm, success, aborted, handleError } from "../lib/ui.js";
|
|
5
7
|
|
|
8
|
+
const ERROR_PREFIX = "Error:";
|
|
9
|
+
|
|
6
10
|
export async function runCommand(request) {
|
|
7
|
-
|
|
11
|
+
let spinner;
|
|
8
12
|
try {
|
|
9
|
-
const
|
|
13
|
+
const ready = await ensureProviderReady({
|
|
14
|
+
dataDescription: "natural-language Git command request",
|
|
15
|
+
});
|
|
16
|
+
if (!ready) return;
|
|
17
|
+
const { config, keyInfo } = ready;
|
|
18
|
+
|
|
19
|
+
spinner = spin("Translating your request into a Git command...");
|
|
20
|
+
const provider = new GeminiProvider({
|
|
21
|
+
apiKey: keyInfo.key,
|
|
22
|
+
model: config.model,
|
|
23
|
+
});
|
|
10
24
|
|
|
11
|
-
|
|
25
|
+
let command = stripFences(await provider.generate(runPrompt({ request })));
|
|
26
|
+
if (command.startsWith(ERROR_PREFIX)) {
|
|
12
27
|
spinner.fail(command);
|
|
13
28
|
process.exit(1);
|
|
14
29
|
}
|
|
15
|
-
spinner.succeed();
|
|
16
30
|
|
|
17
|
-
//
|
|
18
|
-
|
|
31
|
+
// Try parsing; if not safe, retry once with the model
|
|
32
|
+
let args;
|
|
33
|
+
try {
|
|
34
|
+
args = parseGitCommand(command);
|
|
35
|
+
} catch {
|
|
36
|
+
const [, verb = command] = command.split(/\s+/);
|
|
37
|
+
command = stripFences(
|
|
38
|
+
await provider.generate(runRetryPrompt({ request, previous: command, verb }))
|
|
39
|
+
);
|
|
40
|
+
if (command.startsWith(ERROR_PREFIX)) {
|
|
41
|
+
spinner.fail(command);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
args = parseGitCommand(command);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
spinner.succeed();
|
|
19
48
|
|
|
20
49
|
console.log(
|
|
21
50
|
chalk.yellow.bold("\nWARNING: The suggested command will be executed on your repository.")
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import {
|
|
3
|
+
loadConfig,
|
|
4
|
+
hasAcceptedCurrentNotice,
|
|
5
|
+
recordNoticeAcceptance,
|
|
6
|
+
CURRENT_NOTICE_VERSION,
|
|
7
|
+
} from "../lib/config.js";
|
|
8
|
+
import {
|
|
9
|
+
getApiKey,
|
|
10
|
+
getEnvApiKey,
|
|
11
|
+
getStoredApiKey,
|
|
12
|
+
setApiKey,
|
|
13
|
+
maskApiKey,
|
|
14
|
+
} from "../lib/credentials.js";
|
|
15
|
+
import { GeminiProvider, OFFICIAL_GEMINI_HOSTNAME } from "../lib/provider.js";
|
|
16
|
+
import {
|
|
17
|
+
spin,
|
|
18
|
+
confirm,
|
|
19
|
+
promptMaskedPassword,
|
|
20
|
+
success,
|
|
21
|
+
warn,
|
|
22
|
+
aborted,
|
|
23
|
+
handleError,
|
|
24
|
+
} from "../lib/ui.js";
|
|
25
|
+
|
|
26
|
+
export function printPrivacyNotice({
|
|
27
|
+
dataDescription = "staged diff and optional intent",
|
|
28
|
+
model = loadConfig().model,
|
|
29
|
+
} = {}) {
|
|
30
|
+
console.error(
|
|
31
|
+
chalk.bold.cyan(`\n✈ Git Pilot Data-Sharing & Privacy Notice (v${CURRENT_NOTICE_VERSION})`)
|
|
32
|
+
);
|
|
33
|
+
console.error(
|
|
34
|
+
chalk.dim("────────────────────────────────────────────────────────────────────────")
|
|
35
|
+
);
|
|
36
|
+
console.error(chalk.yellow(" • Provider: ") + `Google Gemini (${OFFICIAL_GEMINI_HOSTNAME})`);
|
|
37
|
+
console.error(chalk.yellow(" • Model: ") + model);
|
|
38
|
+
console.error(chalk.yellow(" • Sent: ") + dataDescription);
|
|
39
|
+
console.error(chalk.dim(" Unstaged and untracked files are not sent."));
|
|
40
|
+
console.error(
|
|
41
|
+
chalk.yellow(" • API key: ") + "Stored in your OS credential manager; sent directly to Google."
|
|
42
|
+
);
|
|
43
|
+
console.error(
|
|
44
|
+
chalk.yellow(" • Screening:") +
|
|
45
|
+
" Common secrets are checked locally, but this check is not exhaustive."
|
|
46
|
+
);
|
|
47
|
+
console.error(
|
|
48
|
+
chalk.dim("────────────────────────────────────────────────────────────────────────\n")
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function runGuidedSetup({ isFirstRun = false } = {}) {
|
|
53
|
+
let spinner;
|
|
54
|
+
try {
|
|
55
|
+
const config = loadConfig();
|
|
56
|
+
printPrivacyNotice({ model: config.model });
|
|
57
|
+
|
|
58
|
+
const envKey = getEnvApiKey("gemini");
|
|
59
|
+
const storedKey = await getStoredApiKey("gemini");
|
|
60
|
+
|
|
61
|
+
if (envKey) {
|
|
62
|
+
warn(
|
|
63
|
+
`${envKey.envVarName} is currently set in your environment (${maskApiKey(envKey.key)}). ` +
|
|
64
|
+
`Setup will save your key to the OS credential manager, but ${envKey.envVarName} will continue to shadow the stored key until you unset it in your shell.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (storedKey && !isFirstRun) {
|
|
69
|
+
warn(
|
|
70
|
+
`An API key is already stored in the OS credential manager (${maskApiKey(storedKey.key)}).`
|
|
71
|
+
);
|
|
72
|
+
const proceed = await confirm("Do you want to replace the stored key?", false);
|
|
73
|
+
if (!proceed) {
|
|
74
|
+
console.log("Setup cancelled.");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const agree = await confirm("Do you acknowledge this data flow and wish to proceed?", false);
|
|
80
|
+
if (!agree) {
|
|
81
|
+
console.log("Setup aborted. No changes were made.");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.error(chalk.dim("\nGet a Gemini API key at: https://aistudio.google.com/app/apikey\n"));
|
|
86
|
+
|
|
87
|
+
const key = await promptMaskedPassword("Enter your Gemini API key:");
|
|
88
|
+
if (!key) {
|
|
89
|
+
warn("Setup aborted: API key cannot be empty.");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
spinner = spin("Validating API key with Google Gemini...");
|
|
94
|
+
const provider = new GeminiProvider({ apiKey: key, model: config.model });
|
|
95
|
+
await provider.validateKey();
|
|
96
|
+
spinner.succeed("API key successfully validated!");
|
|
97
|
+
|
|
98
|
+
await setApiKey("gemini", key);
|
|
99
|
+
recordNoticeAcceptance();
|
|
100
|
+
|
|
101
|
+
console.log();
|
|
102
|
+
success(`Setup complete! Stored in OS credential manager as ${maskApiKey(key)}.`);
|
|
103
|
+
if (envKey) {
|
|
104
|
+
warn(
|
|
105
|
+
`Reminder: ${envKey.envVarName} is still set in your environment and will take precedence until unset.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
console.log(
|
|
109
|
+
chalk.cyan(
|
|
110
|
+
"\nTry it out: stage changes with `git add` and run " +
|
|
111
|
+
chalk.bold("`git pilot`") +
|
|
112
|
+
" to generate a commit message.\n"
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const reportedError =
|
|
117
|
+
err?.code === "MODEL_NOT_FOUND"
|
|
118
|
+
? new Error(
|
|
119
|
+
`${err.message}\nSetup did not save the entered key. Any previously stored key is unchanged.`,
|
|
120
|
+
{ cause: err }
|
|
121
|
+
)
|
|
122
|
+
: err;
|
|
123
|
+
handleError(reportedError, spinner);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Ensures both a valid API key and a versioned data-sharing notice acknowledgement
|
|
129
|
+
* are present before any provider request is made—including when GEMINI_API_KEY is supplied via env.
|
|
130
|
+
* In non-interactive or `--dry-run` mode, never prompts interactively; fails early with setup steps
|
|
131
|
+
* unless explicit automation consent (`--accept-notice` or `GIT_PILOT_ACCEPT_NOTICE=1`) is provided.
|
|
132
|
+
*/
|
|
133
|
+
export async function ensureProviderReady({
|
|
134
|
+
dataDescription = "staged git diff and commit intent",
|
|
135
|
+
nonInteractive = false,
|
|
136
|
+
acceptNotice = false,
|
|
137
|
+
} = {}) {
|
|
138
|
+
const isNonInteractive = Boolean(nonInteractive || !process.stdin.isTTY || !process.stdout.isTTY);
|
|
139
|
+
const hasExplicitConsent = Boolean(
|
|
140
|
+
acceptNotice ||
|
|
141
|
+
process.env.GIT_PILOT_ACCEPT_NOTICE === "1" ||
|
|
142
|
+
process.env.GIT_PILOT_ACCEPT_NOTICE === "true"
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
let config = loadConfig();
|
|
146
|
+
let keyInfo = await getApiKey(config.provider);
|
|
147
|
+
|
|
148
|
+
if (!keyInfo) {
|
|
149
|
+
if (isNonInteractive) {
|
|
150
|
+
console.error(
|
|
151
|
+
chalk.red(
|
|
152
|
+
"Error: No Gemini API key configured for non-interactive execution.\n" +
|
|
153
|
+
"To configure Git Pilot:\n" +
|
|
154
|
+
" 1. Run `git pilot setup` in an interactive terminal, OR\n" +
|
|
155
|
+
" 2. Set `GEMINI_API_KEY` in your environment and pass `--accept-notice` (or set `GIT_PILOT_ACCEPT_NOTICE=1`)."
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.error(chalk.yellow("\nNo API key found. Starting one-time guided setup...\n"));
|
|
162
|
+
await runGuidedSetup({ isFirstRun: true });
|
|
163
|
+
config = loadConfig();
|
|
164
|
+
keyInfo = await getApiKey(config.provider);
|
|
165
|
+
if (!keyInfo) {
|
|
166
|
+
console.error(chalk.red("Setup was not completed. Aborting."));
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!hasAcceptedCurrentNotice(config)) {
|
|
172
|
+
if (hasExplicitConsent) {
|
|
173
|
+
config = recordNoticeAcceptance();
|
|
174
|
+
return { config, keyInfo };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (isNonInteractive) {
|
|
178
|
+
printPrivacyNotice({ dataDescription, model: config.model });
|
|
179
|
+
console.error(
|
|
180
|
+
chalk.red(
|
|
181
|
+
"Error: Data-sharing notice has not been acknowledged yet (fresh configuration).\n" +
|
|
182
|
+
"To proceed in non-interactive or `--dry-run` mode:\n" +
|
|
183
|
+
" 1. Run `git pilot setup` once in an interactive terminal to review and accept the notice, OR\n" +
|
|
184
|
+
" 2. Pass `--accept-notice` (or set `GIT_PILOT_ACCEPT_NOTICE=1`) to explicitly acknowledge sending " +
|
|
185
|
+
`${dataDescription} to Google Gemini (${OFFICIAL_GEMINI_HOSTNAME}).`
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
printPrivacyNotice({ dataDescription, model: config.model });
|
|
192
|
+
const agreed = await confirm(
|
|
193
|
+
`Acknowledge sending ${dataDescription} to Google Gemini (${OFFICIAL_GEMINI_HOSTNAME})?`,
|
|
194
|
+
false
|
|
195
|
+
);
|
|
196
|
+
if (!agreed) {
|
|
197
|
+
aborted("Data-sharing notice was not accepted. No request was sent.");
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
config = recordNoticeAcceptance();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { config, keyInfo };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function setupCommand() {
|
|
207
|
+
await runGuidedSetup({ isFirstRun: false });
|
|
208
|
+
}
|