@abhaydesu/git-pilot 1.3.0 → 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/src/lib/ui.js CHANGED
@@ -1,28 +1,76 @@
1
1
  import chalk from "chalk";
2
2
  import inquirer from "inquirer";
3
3
  import ora from "ora";
4
- import { describeError } from "./api.js";
4
+ import process from "node:process";
5
5
  import { UnsafeCommandError } from "./exec.js";
6
+ import { ProviderError } from "./provider.js";
7
+ import { GitCommitError } from "./git.js";
8
+ import { CredentialStoreUnavailableError, redactSecretText } from "./credentials.js";
9
+ import { sanitizeTerminalText } from "./sanitizer.js";
6
10
 
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.
11
+ // Check NO_COLOR standard (https://no-color.org)
12
+ if (process.env.NO_COLOR) {
13
+ chalk.level = 0;
14
+ }
15
+
16
+ // ora 9.x column fallback
9
17
  if (!process.stderr.columns) process.stderr.columns = 80;
10
18
 
11
- // discardStdin is off so the spinner never swallows the input inquirer needs next.
12
- export const spin = (text) => ora({ text, discardStdin: false }).start();
19
+ export const spin = (text) => ora({ text, discardStdin: false, stream: process.stderr }).start();
13
20
 
14
21
  export function showBlock(title, body) {
15
- const bar = "-".repeat(title.length + 14);
22
+ const bar = "-".repeat(Math.max(title.length + 14, 30));
16
23
  console.log(chalk.green(`\n------ ${title} ------`));
17
24
  console.log(chalk.cyan(body));
18
25
  console.log(chalk.green(`${bar}\n`));
19
26
  }
20
27
 
21
- export async function confirm(message) {
22
- const { ok } = await inquirer.prompt([{ type: "confirm", name: "ok", message, default: false }]);
28
+ export function showPanel({ title, details = [], content, stream = process.stdout }) {
29
+ const write = (line) => stream.write(line + "\n");
30
+ write(chalk.bold.cyan(`\n╭─ ${title} ` + "─".repeat(Math.max(0, 50 - title.length))));
31
+ for (const detail of details) {
32
+ write(chalk.dim(`│ ${detail}`));
33
+ }
34
+ if (details.length > 0) {
35
+ write(chalk.dim(`├${"─".repeat(55)}`));
36
+ }
37
+ const lines = (content || "").split("\n");
38
+ for (const line of lines) {
39
+ write(`│ ${chalk.white(line)}`);
40
+ }
41
+ write(chalk.bold.cyan(`╰${"─".repeat(56)}\n`));
42
+ }
43
+
44
+ export async function confirm(message, defaultVal = false) {
45
+ const { ok } = await inquirer.prompt([
46
+ { type: "confirm", name: "ok", message, default: defaultVal },
47
+ ]);
23
48
  return ok;
24
49
  }
25
50
 
51
+ /**
52
+ * Action menu for commit flow: returns "accept" | "edit" | "regenerate" | "abort".
53
+ * Default selection is "abort" (non-destructive) so pressing Enter accidentally
54
+ * never creates an unintended commit.
55
+ */
56
+ export async function chooseCommitAction() {
57
+ const { action } = await inquirer.prompt([
58
+ {
59
+ type: "list",
60
+ name: "action",
61
+ message: "What would you like to do?",
62
+ default: "abort",
63
+ choices: [
64
+ { name: "Accept and commit", value: "accept" },
65
+ { name: "Edit in $EDITOR", value: "edit" },
66
+ { name: "Regenerate message", value: "regenerate" },
67
+ { name: "Abort", value: "abort" },
68
+ ],
69
+ },
70
+ ]);
71
+ return action;
72
+ }
73
+
26
74
  /** Accept / Edit / Abort menu; returns "accept" | "edit" | "abort". */
27
75
  export async function chooseAcceptEditAbort() {
28
76
  const { choice } = await inquirer.prompt([
@@ -30,6 +78,7 @@ export async function chooseAcceptEditAbort() {
30
78
  type: "list",
31
79
  name: "choice",
32
80
  message: "What would you like to do?",
81
+ default: "abort",
33
82
  choices: [
34
83
  { name: "Accept", value: "accept" },
35
84
  { name: "Edit", value: "edit" },
@@ -54,21 +103,70 @@ export async function editText(message, initial, emptyMessage) {
54
103
  return text.trim();
55
104
  }
56
105
 
57
- export const success = (text) => console.log(chalk.bgGreen.black(text));
58
- export const aborted = (text) => console.log(chalk.red(text));
106
+ /** Non-echoing masked password prompt */
107
+ export async function promptMaskedPassword(message) {
108
+ const { secret } = await inquirer.prompt([
109
+ {
110
+ type: "password",
111
+ name: "secret",
112
+ message,
113
+ mask: "*",
114
+ validate: (input) => (input.trim().length > 0 ? true : "Input cannot be empty."),
115
+ },
116
+ ]);
117
+ return secret.trim();
118
+ }
119
+
120
+ export const success = (text) => console.log(chalk.green(`✔ ${text}`));
121
+ export const warn = (text) => console.error(chalk.yellow(`⚠ ${text}`));
122
+ export const info = (text) => console.error(chalk.blue(`ℹ ${text}`));
123
+ export const aborted = (text) => console.log(chalk.dim(`Cancelled. ${text}`));
124
+
125
+ export function formatSafeErrorText(text) {
126
+ return sanitizeTerminalText(redactSecretText(String(text || "")));
127
+ }
59
128
 
60
129
  /** Prints an error, stops the spinner if it is running, and exits non-zero. */
61
130
  export function handleError(error, spinner) {
62
131
  if (spinner?.isSpinning) spinner.fail();
63
132
 
64
133
  if (error?.name === "ExitPromptError") {
65
- console.error(chalk.red("\nCancelled."));
134
+ console.error(chalk.dim("\nCancelled by user."));
66
135
  process.exit(130);
67
136
  }
137
+
138
+ if (error instanceof CredentialStoreUnavailableError) {
139
+ console.error(chalk.red.bold(`\nCredential Store Unavailable:`));
140
+ console.error(chalk.red(formatSafeErrorText(error.message)));
141
+ console.error(
142
+ chalk.yellow(
143
+ "\nTip: For CI or automated environments, you can alternatively set the GEMINI_API_KEY environment variable."
144
+ )
145
+ );
146
+ process.exit(1);
147
+ }
148
+
68
149
  if (error instanceof UnsafeCommandError) {
69
- console.error(chalk.red(`Refusing to run: ${error.message}`));
70
- } else {
71
- console.error(chalk.red(describeError(error)));
150
+ console.error(chalk.red(`Refusing to run: ${formatSafeErrorText(error.message)}`));
151
+ process.exit(1);
152
+ }
153
+
154
+ if (error instanceof ProviderError) {
155
+ console.error(chalk.red(`Provider Error: ${formatSafeErrorText(error.message)}`));
156
+ process.exit(1);
157
+ }
158
+
159
+ if (error instanceof GitCommitError) {
160
+ console.error(chalk.red(`\nCommit Error: ${formatSafeErrorText(error.message)}`));
161
+ console.error(chalk.yellow(`\nYour generated message was preserved:`));
162
+ console.error(chalk.cyan(sanitizeTerminalText(error.commitMessage)));
163
+ process.exit(1);
72
164
  }
165
+
166
+ console.error(
167
+ chalk.red(
168
+ `An unexpected error occurred: ${formatSafeErrorText(error?.message || String(error))}`
169
+ )
170
+ );
73
171
  process.exit(1);
74
172
  }
package/src/lib/api.js DELETED
@@ -1,26 +0,0 @@
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
- }