@commitguard/cli 0.0.13 → 0.0.14

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.
Files changed (3) hide show
  1. package/README.md +167 -0
  2. package/dist/index.mjs +373 -333
  3. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # CommitGuard
2
+
3
+ Protect your codebase with every commit. CommitGuard automatically analyzes your code for security vulnerabilities, performance issues, and code quality problems before they enter your repository. Grab your free API key at https://commitguard.ai.
4
+
5
+ ## Installation
6
+
7
+ Get CommitGuard running in under 60 seconds. It works seamlessly with any git repository and integrates automatically with VSCode and other git clients.
8
+
9
+ ### Quick Start
10
+
11
+ **1. Install CommitGuard globally**
12
+
13
+ ```bash
14
+ npm install -g @commitguard/cli
15
+ ```
16
+
17
+ **2. Navigate to your project**
18
+
19
+ ```bash
20
+ cd your-project
21
+ ```
22
+
23
+ **3. Initialize CommitGuard**
24
+
25
+ ```bash
26
+ commitguard init
27
+ ```
28
+
29
+ **4. Commit as usual**
30
+
31
+ CommitGuard now runs automatically on every commit. It works with all git integrations including VSCode, terminal, and other git clients.
32
+
33
+ ```bash
34
+ git commit -m "Add new feature"
35
+ [CommitGuard] Analyzing commit...
36
+ ✓ Commit passed all checks
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ### Bypassing Checks
42
+
43
+ To bypass CommitGuard checks for a specific commit, add `--skip` to your commit message:
44
+
45
+ ```bash
46
+ git commit -m "Add new feature --skip"
47
+ ```
48
+
49
+ The `--skip` flag is automatically removed from the final commit message. When using git in your terminal, you'll be prompted to confirm if you want to bypass checks.
50
+
51
+ ### Configuring Checks
52
+
53
+ View or update CommitGuard preferences for your repository:
54
+
55
+ ```bash
56
+ commitguard config
57
+ ```
58
+
59
+ On the Pro plan, you can add custom rules through the config comand.
60
+
61
+ ## CLI Commands
62
+
63
+ ### `commitguard init`
64
+
65
+ Install CommitGuard in your current repository.
66
+
67
+ ```bash
68
+ $ commitguard init
69
+ ✓ CommitGuard installed successfully
70
+ ```
71
+
72
+ ### `commitguard remove`
73
+
74
+ Remove CommitGuard from the current repository.
75
+
76
+ ```bash
77
+ $ commitguard remove
78
+ ✓ CommitGuard removed
79
+ ```
80
+
81
+ ### `commitguard config`
82
+
83
+ View or update CommitGuard preferences for the current repository. Pro plan users can add custom rules.
84
+
85
+ ```bash
86
+ $ commitguard config
87
+ Select enabled checks for this project:
88
+ Security, Performance, Code Quality, Architecture
89
+ ```
90
+
91
+ ### `commitguard keys`
92
+
93
+ Manage your global API key for CommitGuard.
94
+
95
+ ```bash
96
+ $ commitguard keys
97
+ Current API key: sk-ant-***************
98
+ ```
99
+
100
+ ## What Happens After Installation?
101
+
102
+ - Every commit is analyzed before it's created.
103
+ - No config files are added to your project
104
+ - Bypass checks anytime with `--skip` in your commit message
105
+ - Works seamlessly with all git clients and IDEs
106
+
107
+ ## Troubleshooting
108
+
109
+ <details>
110
+ <summary><strong>CommitGuard isn't running on commits</strong></summary>
111
+
112
+ Try reinstalling the hooks:
113
+
114
+ ```bash
115
+ commitguard remove
116
+ commitguard init
117
+ ```
118
+
119
+ Verify that the hooks are installed:
120
+
121
+ ```bash
122
+ ls -la .git/hooks/
123
+ ```
124
+
125
+ You should see a `pre-commit` hook file.
126
+ </details>
127
+
128
+ <details>
129
+ <summary><strong>How do I skip a single commit?</strong></summary>
130
+
131
+ Add `--skip` to your commit message:
132
+
133
+ ```bash
134
+ git commit -m "Emergency fix --skip"
135
+ ```
136
+
137
+ The `--skip` flag is automatically removed from the final message.
138
+ </details>
139
+
140
+ <details>
141
+ <summary><strong>How do I completely remove CommitGuard?</strong></summary>
142
+
143
+ Remove the hooks and uninstall the package:
144
+
145
+ ```bash
146
+ commitguard remove
147
+ npm uninstall -g @commitguard/cli
148
+ ```
149
+ </details>
150
+
151
+ ## Features
152
+
153
+ - **Security Analysis** - Detect vulnerabilities before they reach your repo
154
+ - **Performance Checks** - Identify performance bottlenecks early
155
+ - **Code Quality** - Maintain consistent code standards
156
+ - **Architecture Review** - Ensure architectural patterns are followed
157
+ - **Zero Configuration** - Works out of the box
158
+ - **Universal Compatibility** - Works with any git workflow
159
+ -- **Custom Rules** - Add your own custom checks to the code reviews
160
+
161
+ ## Support
162
+
163
+ For issues, feature requests, or questions related to this package please open an issue or email us at hello@commitguard.ai
164
+
165
+ ## License
166
+
167
+ MIT
package/dist/index.mjs CHANGED
@@ -3,12 +3,12 @@ import process from "node:process";
3
3
  import { consola } from "consola";
4
4
  import updateNotifier from "update-notifier";
5
5
  import { execFileSync, execSync } from "node:child_process";
6
- import { createHash } from "node:crypto";
7
- import { cancel, confirm, intro, isCancel, log, multiselect, note, outro, text } from "@clack/prompts";
8
- import { Entry } from "@napi-rs/keyring";
9
6
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
10
7
  import { homedir } from "node:os";
11
8
  import { dirname, join } from "node:path";
9
+ import { cancel, confirm, intro, isCancel, log, multiselect, note, outro, select, text } from "@clack/prompts";
10
+ import { createHash } from "node:crypto";
11
+ import { Entry } from "@napi-rs/keyring";
12
12
  import { fileURLToPath, pathToFileURL } from "node:url";
13
13
  import { readFile } from "node:fs/promises";
14
14
  import { findUp } from "find-up";
@@ -17,7 +17,7 @@ import stringWidth from "string-width";
17
17
  import "dotenv/config";
18
18
 
19
19
  //#region package.json
20
- var version = "0.0.13";
20
+ var version = "0.0.14";
21
21
  var package_default = {
22
22
  name: "@commitguard/cli",
23
23
  type: "module",
@@ -71,6 +71,277 @@ var package_default = {
71
71
  }
72
72
  };
73
73
 
74
+ //#endregion
75
+ //#region src/utils/global.ts
76
+ function createDiffHash(diff) {
77
+ return createHash("md5").update(diff).digest("base64url");
78
+ }
79
+ function addGitLineNumbers(diff) {
80
+ if (!diff.trim()) return diff;
81
+ const lines = diff.split("\n");
82
+ const result = [];
83
+ let oldLine = 0;
84
+ let newLine = 0;
85
+ for (const line of lines) if (line.startsWith("@@")) {
86
+ const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
87
+ if (match) {
88
+ oldLine = Number.parseInt(match[1], 10);
89
+ newLine = Number.parseInt(match[2], 10);
90
+ }
91
+ result.push(line);
92
+ } else if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("diff ") || line.startsWith("index ")) result.push(line);
93
+ else if (line.startsWith("-")) {
94
+ result.push(`${oldLine}:${line}`);
95
+ oldLine++;
96
+ } else if (line.startsWith("+")) {
97
+ result.push(`${newLine}:${line}`);
98
+ newLine++;
99
+ } else {
100
+ result.push(`${newLine}:${line}`);
101
+ oldLine++;
102
+ newLine++;
103
+ }
104
+ return result.join("\n");
105
+ }
106
+ const MESSAGES = { noGit: "No .git folder found. Run this inside a git repository." };
107
+
108
+ //#endregion
109
+ //#region src/utils/config.ts
110
+ const MAX_CUSTOM_PROMPT_LENGTH = 500;
111
+ const CONFIG_DIR = join(homedir(), ".commitguard");
112
+ const PROJECTS_CONFIG_PATH = join(CONFIG_DIR, "projects.json");
113
+ let projectsConfigCache = null;
114
+ const GIT_DIR$1 = ".git";
115
+ function ensureConfigDir() {
116
+ if (!existsSync(CONFIG_DIR)) try {
117
+ mkdirSync(CONFIG_DIR, { recursive: true });
118
+ } catch (e) {
119
+ consola.error(`Failed to create config directory at ${CONFIG_DIR}: ${e.message}`);
120
+ }
121
+ }
122
+ function getDefaultConfig() {
123
+ return {
124
+ context: "normal",
125
+ checks: {
126
+ security: true,
127
+ performance: true,
128
+ codeQuality: true,
129
+ architecture: true
130
+ },
131
+ severityLevels: {
132
+ critical: true,
133
+ warning: true,
134
+ suggestion: false
135
+ },
136
+ customRule: ""
137
+ };
138
+ }
139
+ let projectIdCache = null;
140
+ function getProjectId() {
141
+ if (projectIdCache) return projectIdCache;
142
+ try {
143
+ projectIdCache = execFileSync("git", [
144
+ "rev-list",
145
+ "--max-parents=0",
146
+ "HEAD"
147
+ ], {
148
+ encoding: "utf8",
149
+ stdio: [
150
+ "pipe",
151
+ "pipe",
152
+ "ignore"
153
+ ]
154
+ }).trim().split("\n")[0];
155
+ return projectIdCache;
156
+ } catch {
157
+ consola.error("Warning: Unable to determine project ID. Using current working directory as fallback project ID.");
158
+ projectIdCache = process.cwd();
159
+ return projectIdCache;
160
+ }
161
+ }
162
+ function loadProjectsConfig() {
163
+ if (projectsConfigCache) return projectsConfigCache;
164
+ if (existsSync(PROJECTS_CONFIG_PATH)) try {
165
+ const content = readFileSync(PROJECTS_CONFIG_PATH, "utf8");
166
+ projectsConfigCache = JSON.parse(content);
167
+ return projectsConfigCache;
168
+ } catch {
169
+ consola.warn("Failed to parse projects config");
170
+ }
171
+ projectsConfigCache = {};
172
+ return projectsConfigCache;
173
+ }
174
+ function saveProjectsConfig(projects) {
175
+ try {
176
+ ensureConfigDir();
177
+ writeFileSync(PROJECTS_CONFIG_PATH, JSON.stringify(projects, null, 2));
178
+ projectsConfigCache = projects;
179
+ } catch (e) {
180
+ consola.error(`Failed to save projects config: ${e.message}`);
181
+ }
182
+ }
183
+ function loadConfig() {
184
+ const projectId = getProjectId();
185
+ return loadProjectsConfig()[projectId] || getDefaultConfig();
186
+ }
187
+ async function manageConfig() {
188
+ if (!existsSync(GIT_DIR$1)) {
189
+ cancel(MESSAGES.noGit);
190
+ return;
191
+ }
192
+ const projectId = getProjectId();
193
+ const currentConfig = loadConfig();
194
+ intro(`CommitGuard Configuration`);
195
+ const enabledChecks = await multiselect({
196
+ message: "Select enabled checks for this project:",
197
+ options: [
198
+ {
199
+ value: "security",
200
+ label: "Security"
201
+ },
202
+ {
203
+ value: "performance",
204
+ label: "Performance"
205
+ },
206
+ {
207
+ value: "codeQuality",
208
+ label: "Code Quality"
209
+ },
210
+ {
211
+ value: "architecture",
212
+ label: "Architecture"
213
+ }
214
+ ],
215
+ initialValues: Object.entries(currentConfig.checks).filter(([_, enabled]) => enabled).map(([key]) => key)
216
+ });
217
+ if (isCancel(enabledChecks)) {
218
+ cancel("Configuration cancelled");
219
+ return;
220
+ }
221
+ const enabledSeverity = await multiselect({
222
+ message: "Select severity levels for enabled checks:",
223
+ options: [
224
+ {
225
+ value: "suggestion",
226
+ label: "Suggestion"
227
+ },
228
+ {
229
+ value: "warning",
230
+ label: "Warning"
231
+ },
232
+ {
233
+ value: "critical",
234
+ label: "Critical"
235
+ }
236
+ ],
237
+ initialValues: Object.entries(currentConfig.severityLevels).filter(([_, enabled]) => enabled).map(([key]) => key)
238
+ });
239
+ if (isCancel(enabledSeverity)) {
240
+ cancel("Configuration cancelled");
241
+ return;
242
+ }
243
+ const contextLevel = await select({
244
+ message: "Select context level for analysis:",
245
+ options: [{
246
+ value: "minimal",
247
+ label: "Minimal (Just Actual Changes)"
248
+ }, {
249
+ value: "normal",
250
+ label: "Normal (Actual Changes + Context Lines)"
251
+ }],
252
+ initialValue: currentConfig.context
253
+ });
254
+ if (isCancel(contextLevel)) {
255
+ cancel("Configuration cancelled");
256
+ return;
257
+ }
258
+ let customRule = currentConfig.customRule;
259
+ if (currentConfig.customRule) {
260
+ log.info(`Current custom rule: ${currentConfig.customRule}`);
261
+ const editCustomRule = await confirm({
262
+ message: "Would you like to edit the custom rule? (Currently only available to pro users)",
263
+ initialValue: false
264
+ });
265
+ if (isCancel(editCustomRule)) {
266
+ cancel("Configuration cancelled");
267
+ return;
268
+ }
269
+ if (editCustomRule) {
270
+ const newCustomRule = await text({
271
+ message: "Enter new custom rule (leave empty to remove):",
272
+ initialValue: currentConfig.customRule,
273
+ validate: (value) => {
274
+ const val = String(value).trim();
275
+ if (!val) return void 0;
276
+ if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`;
277
+ }
278
+ });
279
+ if (isCancel(newCustomRule)) {
280
+ cancel("Configuration cancelled");
281
+ return;
282
+ }
283
+ customRule = String(newCustomRule).trim();
284
+ }
285
+ } else {
286
+ const addCustomRule = await confirm({
287
+ message: "Would you like to add a custom rule for this project? (Currently only available to pro users)",
288
+ initialValue: false
289
+ });
290
+ if (isCancel(addCustomRule)) {
291
+ cancel("Configuration cancelled");
292
+ return;
293
+ }
294
+ if (addCustomRule) {
295
+ const newCustomRule = await text({
296
+ message: "Enter custom rule (leave empty to skip):",
297
+ placeholder: "e.g., Check for proper error handling in async functions",
298
+ validate: (value) => {
299
+ const val = String(value).trim();
300
+ if (!val) return void 0;
301
+ if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`;
302
+ }
303
+ });
304
+ if (isCancel(newCustomRule)) {
305
+ cancel("Configuration cancelled");
306
+ return;
307
+ }
308
+ customRule = String(newCustomRule).trim();
309
+ }
310
+ }
311
+ const newConfig = {
312
+ context: contextLevel,
313
+ checks: {
314
+ security: enabledChecks.includes("security"),
315
+ performance: enabledChecks.includes("performance"),
316
+ codeQuality: enabledChecks.includes("codeQuality"),
317
+ architecture: enabledChecks.includes("architecture")
318
+ },
319
+ severityLevels: {
320
+ suggestion: enabledSeverity.includes("suggestion"),
321
+ warning: enabledSeverity.includes("warning"),
322
+ critical: enabledSeverity.includes("critical")
323
+ },
324
+ customRule
325
+ };
326
+ if (JSON.stringify(newConfig) === JSON.stringify(currentConfig)) {
327
+ outro("No changes made to the configuration.");
328
+ return;
329
+ }
330
+ const confirmUpdate = await confirm({ message: "Save this configuration?" });
331
+ if (isCancel(confirmUpdate)) {
332
+ cancel("Configuration cancelled");
333
+ return;
334
+ }
335
+ if (!confirmUpdate) {
336
+ outro("Configuration not saved.");
337
+ return;
338
+ }
339
+ const projects = loadProjectsConfig();
340
+ projects[projectId] = newConfig;
341
+ saveProjectsConfig(projects);
342
+ outro("✓ Configuration updated for this project!");
343
+ }
344
+
74
345
  //#endregion
75
346
  //#region src/data/ignore.json
76
347
  var ignore = [
@@ -163,80 +434,47 @@ var ignore = [
163
434
  "*.svg",
164
435
  "*.webp",
165
436
  "*.avif",
166
- "*.ico",
167
- "*.mp4",
168
- "*.webm",
169
- "*.mov",
170
- "*.mp3",
171
- "*.wav",
172
- "*.ogg",
173
- "*.flac",
174
- "*.snap",
175
- "__snapshots__/**",
176
- "playwright-report/**",
177
- "test-results/**",
178
- ".nyc_output/**",
179
- "docs/.vitepress/dist/**",
180
- "storybook-static/**",
181
- ".DS_Store",
182
- "Thumbs.db",
183
- ".idea/**",
184
- ".vscode/**",
185
- "*.swp",
186
- "*.dump",
187
- "*.dmp",
188
- "coverage/**",
189
- "tmp/**",
190
- "temp/**",
191
- "*.lock",
192
- "*.sqlite",
193
- "*.db",
194
- "*.rdb"
195
- ];
196
-
197
- //#endregion
198
- //#region src/utils/global.ts
199
- function createDiffHash(diff) {
200
- return createHash("md5").update(diff).digest("base64url");
201
- }
202
- function addGitLineNumbers(diff) {
203
- if (!diff.trim()) return diff;
204
- const lines = diff.split("\n");
205
- const result = [];
206
- let oldLine = 0;
207
- let newLine = 0;
208
- for (const line of lines) if (line.startsWith("@@")) {
209
- const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
210
- if (match) {
211
- oldLine = Number.parseInt(match[1], 10);
212
- newLine = Number.parseInt(match[2], 10);
213
- }
214
- result.push(line);
215
- } else if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("diff ") || line.startsWith("index ")) result.push(line);
216
- else if (line.startsWith("-")) {
217
- result.push(`${oldLine}:${line}`);
218
- oldLine++;
219
- } else if (line.startsWith("+")) {
220
- result.push(`${newLine}:${line}`);
221
- newLine++;
222
- } else {
223
- result.push(`${newLine}:${line}`);
224
- oldLine++;
225
- newLine++;
226
- }
227
- return result.join("\n");
228
- }
229
- const MESSAGES = { noGit: "No .git folder found. Run this inside a git repository." };
437
+ "*.ico",
438
+ "*.mp4",
439
+ "*.webm",
440
+ "*.mov",
441
+ "*.mp3",
442
+ "*.wav",
443
+ "*.ogg",
444
+ "*.flac",
445
+ "*.snap",
446
+ "__snapshots__/**",
447
+ "playwright-report/**",
448
+ "test-results/**",
449
+ ".nyc_output/**",
450
+ "docs/.vitepress/dist/**",
451
+ "storybook-static/**",
452
+ ".DS_Store",
453
+ "Thumbs.db",
454
+ ".idea/**",
455
+ ".vscode/**",
456
+ "*.swp",
457
+ "*.dump",
458
+ "*.dmp",
459
+ "coverage/**",
460
+ "tmp/**",
461
+ "temp/**",
462
+ "*.lock",
463
+ "*.sqlite",
464
+ "*.db",
465
+ "*.rdb"
466
+ ];
230
467
 
231
468
  //#endregion
232
469
  //#region src/utils/git.ts
233
- function getStagedDiff() {
470
+ function getStagedDiff(context) {
471
+ const gitContextCommand = context === "minimal" ? [] : ["--function-context"];
234
472
  try {
235
473
  return addGitLineNumbers(execFileSync("git", [
236
474
  "diff",
237
475
  "--cached",
238
476
  "--no-color",
239
- "--function-context",
477
+ ...gitContextCommand,
240
478
  "--diff-algorithm=histogram",
241
479
  "--diff-filter=AMC",
242
480
  "--",
@@ -255,14 +493,15 @@ function getStagedDiff() {
255
493
  return "";
256
494
  }
257
495
  }
258
- function getLastDiff() {
496
+ function getLastDiff(context) {
497
+ const gitContextCommand = context === "minimal" ? [] : ["--function-context"];
259
498
  try {
260
499
  return execFileSync("git", [
261
500
  "diff",
262
501
  "HEAD~1",
263
502
  "HEAD",
264
503
  "--no-color",
265
- "--function-context",
504
+ ...gitContextCommand,
266
505
  "--diff-algorithm=histogram",
267
506
  "--diff-filter=AMC",
268
507
  "--",
@@ -347,274 +586,75 @@ async function manageGlobalKey() {
347
586
 
348
587
  //#endregion
349
588
  //#region src/utils/api.ts
350
- async function sendToCommitGuard(diff, eslint, config) {
589
+ const DEFAULT_TIMEOUT = 2e4;
590
+ async function sendToCommitGuard(diff, eslint, config$1) {
351
591
  const apiKey = process.env.COMMITGUARD_API_KEY || getGlobalKey() || null;
352
592
  if (!apiKey) throw new Error("No API key found. Set one globally with \"commitguard keys\" or add COMMITGUARD_API_KEY to your .env file. Get your free API key at https://commitguard.ai");
353
593
  const apiUrl = process.env.COMMITGUARD_API_URL || "https://api.commitguard.ai/v1/analyze";
354
- const response = await fetch(apiUrl, {
355
- method: "POST",
356
- headers: {
357
- "Content-Type": "application/json",
358
- "Authorization": `Bearer ${apiKey}`,
359
- "User-Agent": "commitguard-cli"
360
- },
361
- body: JSON.stringify({
362
- diff,
363
- eslint,
364
- config
365
- })
366
- });
367
- if (!response.ok) {
368
- const errorText = await response.text();
369
- let errorMessage = "Failed to analyze commit";
370
- if (response.status === 401) errorMessage = "Invalid API key. Check your key with \"commitguard keys\" or get a new one at https://commitguard.ai";
371
- else if (response.status === 429) errorMessage = "Rate limit exceeded. Please try again later";
372
- else if (response.status === 500) errorMessage = "CommitGuard service error. Please try again later";
373
- else if (response.status >= 400 && response.status < 500) errorMessage = `Request error: ${errorText || "Invalid request"}`;
374
- else errorMessage = `Service unavailable (${response.status}). Please try again later`;
375
- throw new Error(errorMessage);
594
+ const controller = new AbortController();
595
+ const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
596
+ try {
597
+ const response = await fetch(apiUrl, {
598
+ method: "POST",
599
+ headers: {
600
+ "Content-Type": "application/json",
601
+ "Authorization": `Bearer ${apiKey}`,
602
+ "User-Agent": "commitguard-cli"
603
+ },
604
+ body: JSON.stringify({
605
+ diff,
606
+ eslint,
607
+ config: config$1
608
+ }),
609
+ signal: controller.signal
610
+ });
611
+ clearTimeout(timeoutId);
612
+ if (!response.ok) {
613
+ const errorText = await response.text();
614
+ let errorMessage = "Failed to analyze commit";
615
+ if (response.status === 401) errorMessage = "Invalid API key. Check your key with \"commitguard keys\" or get a new one at https://commitguard.ai";
616
+ else if (response.status === 429) errorMessage = "Rate limit exceeded. Please try again later";
617
+ else if (response.status === 500) errorMessage = "CommitGuard service error. Please try again later";
618
+ else if (response.status >= 400 && response.status < 500) errorMessage = `Request error: ${errorText || "Invalid request"}`;
619
+ else errorMessage = `Service unavailable (${response.status}). Please try again later`;
620
+ throw new Error(errorMessage);
621
+ }
622
+ return await response.json();
623
+ } catch (error) {
624
+ clearTimeout(timeoutId);
625
+ if (error instanceof Error && error.name === "AbortError") throw new Error(`Request timed out after ${DEFAULT_TIMEOUT}ms`);
626
+ throw error;
376
627
  }
377
- return await response.json();
378
628
  }
379
629
  async function bypassCommitGuard() {
380
630
  const apiKey = process.env.COMMITGUARD_API_KEY || getGlobalKey() || null;
381
631
  if (!apiKey) throw new Error("No API key found. Set one globally with \"commitguard keys\" or add COMMITGUARD_API_KEY to your .env file. Get your free API key at https://commitguard.ai");
382
632
  const apiUrl = process.env.COMMITGUARD_API_BYPASS_URL || "https://api.commitguard.ai/v1/bypass";
383
- const diff = getLastDiff();
384
- const response = await fetch(apiUrl, {
385
- method: "POST",
386
- headers: {
387
- "Content-Type": "application/json",
388
- "Authorization": `Bearer ${apiKey}`,
389
- "User-Agent": "commitguard-cli"
390
- },
391
- body: JSON.stringify({ diff })
392
- });
393
- if (!response.ok) {
394
- const errorText = await response.text();
395
- throw new Error(`API request failed (${response.status}): ${errorText}`);
396
- }
397
- return await response.json();
398
- }
399
-
400
- //#endregion
401
- //#region src/utils/config.ts
402
- const MAX_CUSTOM_PROMPT_LENGTH = 500;
403
- const CONFIG_DIR = join(homedir(), ".commitguard");
404
- const PROJECTS_CONFIG_PATH = join(CONFIG_DIR, "projects.json");
405
- let projectsConfigCache = null;
406
- const GIT_DIR$1 = ".git";
407
- function ensureConfigDir() {
408
- if (!existsSync(CONFIG_DIR)) try {
409
- mkdirSync(CONFIG_DIR, { recursive: true });
410
- } catch (e) {
411
- consola.error(`Failed to create config directory at ${CONFIG_DIR}: ${e.message}`);
412
- }
413
- }
414
- function getDefaultConfig() {
415
- return {
416
- checks: {
417
- security: true,
418
- performance: true,
419
- codeQuality: true,
420
- architecture: true
421
- },
422
- severityLevels: {
423
- critical: true,
424
- warning: true,
425
- suggestion: false
426
- },
427
- customRule: ""
428
- };
429
- }
430
- let projectIdCache = null;
431
- function getProjectId() {
432
- if (projectIdCache) return projectIdCache;
433
- try {
434
- projectIdCache = execFileSync("git", [
435
- "rev-list",
436
- "--max-parents=0",
437
- "HEAD"
438
- ], {
439
- encoding: "utf8",
440
- stdio: [
441
- "pipe",
442
- "pipe",
443
- "ignore"
444
- ]
445
- }).trim().split("\n")[0];
446
- return projectIdCache;
447
- } catch {
448
- consola.error("Warning: Unable to determine project ID. Using current working directory as fallback project ID.");
449
- projectIdCache = process.cwd();
450
- return projectIdCache;
451
- }
452
- }
453
- function loadProjectsConfig() {
454
- if (projectsConfigCache) return projectsConfigCache;
455
- if (existsSync(PROJECTS_CONFIG_PATH)) try {
456
- const content = readFileSync(PROJECTS_CONFIG_PATH, "utf8");
457
- projectsConfigCache = JSON.parse(content);
458
- return projectsConfigCache;
459
- } catch {
460
- consola.warn("Failed to parse projects config");
461
- }
462
- projectsConfigCache = {};
463
- return projectsConfigCache;
464
- }
465
- function saveProjectsConfig(projects) {
633
+ const diff = getLastDiff(loadConfig().context);
634
+ const controller = new AbortController();
635
+ const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
466
636
  try {
467
- ensureConfigDir();
468
- writeFileSync(PROJECTS_CONFIG_PATH, JSON.stringify(projects, null, 2));
469
- projectsConfigCache = projects;
470
- } catch (e) {
471
- consola.error(`Failed to save projects config: ${e.message}`);
472
- }
473
- }
474
- function loadConfig() {
475
- const projectId = getProjectId();
476
- return loadProjectsConfig()[projectId] || getDefaultConfig();
477
- }
478
- async function manageConfig() {
479
- if (!existsSync(GIT_DIR$1)) {
480
- cancel(MESSAGES.noGit);
481
- return;
482
- }
483
- const projectId = getProjectId();
484
- const currentConfig = loadConfig();
485
- intro(`CommitGuard Configuration`);
486
- const enabledChecks = await multiselect({
487
- message: "Select enabled checks for this project:",
488
- options: [
489
- {
490
- value: "security",
491
- label: "Security"
492
- },
493
- {
494
- value: "performance",
495
- label: "Performance"
496
- },
497
- {
498
- value: "codeQuality",
499
- label: "Code Quality"
500
- },
501
- {
502
- value: "architecture",
503
- label: "Architecture"
504
- }
505
- ],
506
- initialValues: Object.entries(currentConfig.checks).filter(([_, enabled]) => enabled).map(([key]) => key)
507
- });
508
- if (isCancel(enabledChecks)) {
509
- cancel("Configuration cancelled");
510
- return;
511
- }
512
- const enabledSeverity = await multiselect({
513
- message: "Select severity levels for enabled checks:",
514
- options: [
515
- {
516
- value: "suggestion",
517
- label: "Suggestion"
637
+ const response = await fetch(apiUrl, {
638
+ method: "POST",
639
+ headers: {
640
+ "Content-Type": "application/json",
641
+ "Authorization": `Bearer ${apiKey}`,
642
+ "User-Agent": "commitguard-cli"
518
643
  },
519
- {
520
- value: "warning",
521
- label: "Warning"
522
- },
523
- {
524
- value: "critical",
525
- label: "Critical"
526
- }
527
- ],
528
- initialValues: Object.entries(currentConfig.severityLevels).filter(([_, enabled]) => enabled).map(([key]) => key)
529
- });
530
- if (isCancel(enabledSeverity)) {
531
- cancel("Configuration cancelled");
532
- return;
533
- }
534
- let customRule = currentConfig.customRule;
535
- if (currentConfig.customRule) {
536
- consola.info(`Current custom rule: ${currentConfig.customRule}`);
537
- const editCustomRule = await confirm({
538
- message: "Would you like to edit the custom rule? (Currently only available to pro users)",
539
- initialValue: false
540
- });
541
- if (isCancel(editCustomRule)) {
542
- cancel("Configuration cancelled");
543
- return;
544
- }
545
- if (editCustomRule) {
546
- const newCustomRule = await text({
547
- message: "Enter new custom rule (leave empty to remove):",
548
- initialValue: currentConfig.customRule,
549
- validate: (value) => {
550
- const val = String(value).trim();
551
- if (!val) return void 0;
552
- if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`;
553
- }
554
- });
555
- if (isCancel(newCustomRule)) {
556
- cancel("Configuration cancelled");
557
- return;
558
- }
559
- customRule = String(newCustomRule).trim();
560
- }
561
- } else {
562
- const addCustomRule = await confirm({
563
- message: "Would you like to add a custom rule for this project? (Currently only available to pro users)",
564
- initialValue: false
644
+ body: JSON.stringify({ diff }),
645
+ signal: controller.signal
565
646
  });
566
- if (isCancel(addCustomRule)) {
567
- cancel("Configuration cancelled");
568
- return;
569
- }
570
- if (addCustomRule) {
571
- const newCustomRule = await text({
572
- message: "Enter custom rule (leave empty to skip):",
573
- placeholder: "e.g., Check for proper error handling in async functions",
574
- validate: (value) => {
575
- const val = String(value).trim();
576
- if (!val) return void 0;
577
- if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`;
578
- }
579
- });
580
- if (isCancel(newCustomRule)) {
581
- cancel("Configuration cancelled");
582
- return;
583
- }
584
- customRule = String(newCustomRule).trim();
647
+ clearTimeout(timeoutId);
648
+ if (!response.ok) {
649
+ const errorText = await response.text();
650
+ throw new Error(`API request failed (${response.status}): ${errorText}`);
585
651
  }
652
+ return await response.json();
653
+ } catch (error) {
654
+ clearTimeout(timeoutId);
655
+ if (error instanceof Error && error.name === "AbortError") throw new Error(`Request timed out after ${DEFAULT_TIMEOUT}ms`);
656
+ throw error;
586
657
  }
587
- const newConfig = {
588
- checks: {
589
- security: enabledChecks.includes("security"),
590
- performance: enabledChecks.includes("performance"),
591
- codeQuality: enabledChecks.includes("codeQuality"),
592
- architecture: enabledChecks.includes("architecture")
593
- },
594
- severityLevels: {
595
- suggestion: enabledSeverity.includes("suggestion"),
596
- warning: enabledSeverity.includes("warning"),
597
- critical: enabledSeverity.includes("critical")
598
- },
599
- customRule
600
- };
601
- if (JSON.stringify(newConfig) === JSON.stringify(currentConfig)) {
602
- outro("No changes made to the configuration.");
603
- return;
604
- }
605
- const confirmUpdate = await confirm({ message: "Save this configuration?" });
606
- if (isCancel(confirmUpdate)) {
607
- cancel("Configuration cancelled");
608
- return;
609
- }
610
- if (!confirmUpdate) {
611
- outro("Configuration not saved.");
612
- return;
613
- }
614
- const projects = loadProjectsConfig();
615
- projects[projectId] = newConfig;
616
- saveProjectsConfig(projects);
617
- outro("✓ Configuration updated for this project!");
618
658
  }
619
659
 
620
660
  //#endregion
@@ -676,13 +716,13 @@ async function getEslintRules({ startDir = process.cwd(), overrideCache = false
676
716
  } catch {}
677
717
  return null;
678
718
  });
679
- const config = (await Promise.all(loaders)).find((r) => r !== null) ?? {
719
+ const config$1 = (await Promise.all(loaders)).find((r) => r !== null) ?? {
680
720
  rules: {},
681
721
  source: null
682
722
  };
683
- cache.setKey(cacheKey, config);
723
+ cache.setKey(cacheKey, config$1);
684
724
  cache.save(true);
685
- return config;
725
+ return config$1;
686
726
  }
687
727
 
688
728
  //#endregion
@@ -845,6 +885,7 @@ async function removeHooks() {
845
885
  //#endregion
846
886
  //#region src/utils/staged.ts
847
887
  const CACHE_PATH = join(".git", "commitguard-cache.json");
888
+ const config = loadConfig();
848
889
  const CATEGORY_LABELS = {
849
890
  security: "🚨 [SECURITY]",
850
891
  performance: "🚀 [PERFORMANCE]",
@@ -903,7 +944,7 @@ function groupIssuesByFile(issues = []) {
903
944
  };
904
945
  }
905
946
  async function onStaged() {
906
- const diff = getStagedDiff();
947
+ const diff = getStagedDiff(config.context);
907
948
  if (!diff.trim()) {
908
949
  clearCache();
909
950
  return;
@@ -912,7 +953,6 @@ async function onStaged() {
912
953
  const existingCache = readCache();
913
954
  if (existingCache && existingCache.hash === diffHash) return;
914
955
  try {
915
- const config = loadConfig();
916
956
  const response = await sendToCommitGuard(diff, (await getEslintRules()).rules, config);
917
957
  writeCache({
918
958
  hash: diffHash,
@@ -925,7 +965,7 @@ async function onStaged() {
925
965
  }
926
966
  }
927
967
  function getCachedAnalysis(diff, diffHash) {
928
- const effectiveDiff = diff ?? getStagedDiff();
968
+ const effectiveDiff = diff ?? getStagedDiff(config.context);
929
969
  if (!effectiveDiff.trim()) return {
930
970
  analysis: {
931
971
  status: "pass",
@@ -944,7 +984,7 @@ function getCachedAnalysis(diff, diffHash) {
944
984
  };
945
985
  }
946
986
  async function validateCommit() {
947
- const diff = getStagedDiff();
987
+ const diff = getStagedDiff(config.context);
948
988
  const diffHash = diff.trim() ? createDiffHash(diff) : "";
949
989
  const cached = getCachedAnalysis(diff, diffHash);
950
990
  if (!cached) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@commitguard/cli",
3
3
  "type": "module",
4
- "version": "0.0.13",
4
+ "version": "0.0.14",
5
5
  "description": "AI-powered git commit checker that blocks bad code before it ships",
6
6
  "license": "MIT",
7
7
  "repository": {