@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/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,59 +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 (bytes, after JSON encoding) and diffs over
6
- // 800,000 characters.
7
- export const MAX_DIFF_CHARS = 800_000;
8
- const MAX_BODY_BYTES = 1_000_000;
9
-
10
- /** True if `body` (containing `diff`) is small enough for the API to accept. */
11
- export const fitsInRequest = (body, diff) =>
12
- diff.length <= MAX_DIFF_CHARS && Buffer.byteLength(JSON.stringify(body)) <= MAX_BODY_BYTES;
13
-
14
- const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
15
-
16
- export function apiBaseUrl() {
17
- const raw = (process.env.GIT_PILOT_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
18
-
19
- let url;
20
- try {
21
- url = new URL(raw);
22
- } catch {
23
- throw new Error(`GIT_PILOT_API_URL is not a valid URL: ${raw}`);
24
- }
25
- // Your staged diffs are sent to this server, so never over plain http except locally.
26
- const plainHttpOk = url.protocol === "http:" && LOCAL_HOSTS.has(url.hostname);
27
- if (url.protocol !== "https:" && !plainHttpOk) {
28
- throw new Error("GIT_PILOT_API_URL must use https (plain http is only allowed for localhost).");
29
- }
30
- return raw;
31
- }
32
-
33
- /** POSTs `body` to `/api/<endpoint>` and returns the parsed response data. */
34
- export async function callApi(endpoint, body) {
35
- const { data } = await axios.post(`${apiBaseUrl()}/api/${endpoint}`, body, {
36
- timeout: 30_000,
37
- maxRedirects: 0, // the API never redirects a POST; don't follow one to somewhere else
38
- maxContentLength: 1_000_000,
39
- });
40
- return data;
41
- }
42
-
43
- /** Returns `data[field]` if it is a non-empty string; otherwise throws a clear error. */
44
- export function requireString(data, field) {
45
- const value = data?.[field];
46
- if (typeof value !== "string" || value === "") {
47
- throw new Error(`Unexpected response from the API (missing "${field}").`);
48
- }
49
- return value;
50
- }
51
-
52
- /** Human-readable message for an axios/network/other error. */
53
- export function describeError(error) {
54
- if (error.response) {
55
- const detail = error.response.data?.error || "No message";
56
- return `API Error: ${error.response.status} - ${detail}`;
57
- }
58
- return `An unexpected error occurred: ${error.message}`;
59
- }