@commitguard/cli 0.0.3 → 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.
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.d.mts CHANGED
File without changes
package/dist/index.mjs CHANGED
@@ -1,28 +1,23 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from "node:module";
3
2
  import process from "node:process";
4
3
  import { consola } from "consola";
5
4
  import updateNotifier from "update-notifier";
6
5
  import { execFileSync, execSync } from "node:child_process";
7
- import { createHash } from "node:crypto";
8
- import { cancel, confirm, intro, isCancel, log, multiselect, note, outro, text } from "@clack/prompts";
9
- import { Entry } from "@napi-rs/keyring";
10
6
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
11
7
  import { homedir } from "node:os";
12
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
+ import { fileURLToPath, pathToFileURL } from "node:url";
13
13
  import { readFile } from "node:fs/promises";
14
- import { pathToFileURL } from "node:url";
15
14
  import { findUp } from "find-up";
16
15
  import { FlatCache } from "flat-cache";
17
16
  import stringWidth from "string-width";
18
17
  import "dotenv/config";
19
18
 
20
- //#region rolldown:runtime
21
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
22
-
23
- //#endregion
24
19
  //#region package.json
25
- var version = "0.0.3";
20
+ var version = "0.0.14";
26
21
  var package_default = {
27
22
  name: "@commitguard/cli",
28
23
  type: "module",
@@ -31,7 +26,7 @@ var package_default = {
31
26
  license: "MIT",
32
27
  repository: {
33
28
  "type": "git",
34
- "url": "git+https://github.com/moshetanzer/commitguard.git"
29
+ "url": "git+https://github.com/commitguard/cli.git"
35
30
  },
36
31
  exports: {
37
32
  ".": "./dist/index.mjs",
@@ -49,7 +44,8 @@ var package_default = {
49
44
  "typecheck": "tsc --noEmit",
50
45
  "prepublishOnly": "pnpm run build",
51
46
  "lint": "eslint .",
52
- "lint:fix": "eslint . --fix"
47
+ "lint:fix": "eslint . --fix",
48
+ "release": "pnpm typecheck && bumpp --tag --push --publish && npm publish"
53
49
  },
54
50
  dependencies: {
55
51
  "@clack/prompts": "^0.11.0",
@@ -75,6 +71,277 @@ var package_default = {
75
71
  }
76
72
  };
77
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
+
78
345
  //#endregion
79
346
  //#region src/data/ignore.json
80
347
  var ignore = [
@@ -170,77 +437,44 @@ var ignore = [
170
437
  "*.ico",
171
438
  "*.mp4",
172
439
  "*.webm",
173
- "*.mov",
174
- "*.mp3",
175
- "*.wav",
176
- "*.ogg",
177
- "*.flac",
178
- "*.snap",
179
- "__snapshots__/**",
180
- "playwright-report/**",
181
- "test-results/**",
182
- ".nyc_output/**",
183
- "docs/.vitepress/dist/**",
184
- "storybook-static/**",
185
- ".DS_Store",
186
- "Thumbs.db",
187
- ".idea/**",
188
- ".vscode/**",
189
- "*.swp",
190
- "*.dump",
191
- "*.dmp",
192
- "coverage/**",
193
- "tmp/**",
194
- "temp/**",
195
- "*.lock",
196
- "*.sqlite",
197
- "*.db",
198
- "*.rdb"
199
- ];
200
-
201
- //#endregion
202
- //#region src/utils/global.ts
203
- function createDiffHash(diff) {
204
- return createHash("md5").update(diff).digest("base64url");
205
- }
206
- function addGitLineNumbers(diff) {
207
- if (!diff.trim()) return diff;
208
- const lines = diff.split("\n");
209
- const result = [];
210
- let oldLine = 0;
211
- let newLine = 0;
212
- for (const line of lines) if (line.startsWith("@@")) {
213
- const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
214
- if (match) {
215
- oldLine = Number.parseInt(match[1], 10);
216
- newLine = Number.parseInt(match[2], 10);
217
- }
218
- result.push(line);
219
- } else if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("diff ") || line.startsWith("index ")) result.push(line);
220
- else if (line.startsWith("-")) {
221
- result.push(`${oldLine}:${line}`);
222
- oldLine++;
223
- } else if (line.startsWith("+")) {
224
- result.push(`${newLine}:${line}`);
225
- newLine++;
226
- } else {
227
- result.push(`${newLine}:${line}`);
228
- oldLine++;
229
- newLine++;
230
- }
231
- return result.join("\n");
232
- }
233
- const MESSAGES = { noGit: "No .git folder found. Run this inside a git repository." };
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
+ ];
234
467
 
235
468
  //#endregion
236
469
  //#region src/utils/git.ts
237
- function getStagedDiff() {
470
+ function getStagedDiff(context) {
471
+ const gitContextCommand = context === "minimal" ? [] : ["--function-context"];
238
472
  try {
239
473
  return addGitLineNumbers(execFileSync("git", [
240
474
  "diff",
241
475
  "--cached",
242
476
  "--no-color",
243
- "--function-context",
477
+ ...gitContextCommand,
244
478
  "--diff-algorithm=histogram",
245
479
  "--diff-filter=AMC",
246
480
  "--",
@@ -259,14 +493,15 @@ function getStagedDiff() {
259
493
  return "";
260
494
  }
261
495
  }
262
- function getLastDiff() {
496
+ function getLastDiff(context) {
497
+ const gitContextCommand = context === "minimal" ? [] : ["--function-context"];
263
498
  try {
264
499
  return execFileSync("git", [
265
500
  "diff",
266
501
  "HEAD~1",
267
502
  "HEAD",
268
503
  "--no-color",
269
- "--function-context",
504
+ ...gitContextCommand,
270
505
  "--diff-algorithm=histogram",
271
506
  "--diff-filter=AMC",
272
507
  "--",
@@ -351,274 +586,75 @@ async function manageGlobalKey() {
351
586
 
352
587
  //#endregion
353
588
  //#region src/utils/api.ts
354
- async function sendToCommitGuard(diff, eslint, config) {
589
+ const DEFAULT_TIMEOUT = 2e4;
590
+ async function sendToCommitGuard(diff, eslint, config$1) {
355
591
  const apiKey = process.env.COMMITGUARD_API_KEY || getGlobalKey() || null;
356
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");
357
593
  const apiUrl = process.env.COMMITGUARD_API_URL || "https://api.commitguard.ai/v1/analyze";
358
- const response = await fetch(apiUrl, {
359
- method: "POST",
360
- headers: {
361
- "Content-Type": "application/json",
362
- "Authorization": `Bearer ${apiKey}`,
363
- "User-Agent": "commitguard-cli"
364
- },
365
- body: JSON.stringify({
366
- diff,
367
- eslint,
368
- config
369
- })
370
- });
371
- if (!response.ok) {
372
- const errorText = await response.text();
373
- let errorMessage = "Failed to analyze commit";
374
- if (response.status === 401) errorMessage = "Invalid API key. Check your key with \"commitguard keys\" or get a new one at https://commitguard.ai";
375
- else if (response.status === 429) errorMessage = "Rate limit exceeded. Please try again later";
376
- else if (response.status === 500) errorMessage = "CommitGuard service error. Please try again later";
377
- else if (response.status >= 400 && response.status < 500) errorMessage = `Request error: ${errorText || "Invalid request"}`;
378
- else errorMessage = `Service unavailable (${response.status}). Please try again later`;
379
- 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;
380
627
  }
381
- return await response.json();
382
628
  }
383
629
  async function bypassCommitGuard() {
384
630
  const apiKey = process.env.COMMITGUARD_API_KEY || getGlobalKey() || null;
385
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");
386
632
  const apiUrl = process.env.COMMITGUARD_API_BYPASS_URL || "https://api.commitguard.ai/v1/bypass";
387
- const diff = getLastDiff();
388
- const response = await fetch(apiUrl, {
389
- method: "POST",
390
- headers: {
391
- "Content-Type": "application/json",
392
- "Authorization": `Bearer ${apiKey}`,
393
- "User-Agent": "commitguard-cli"
394
- },
395
- body: JSON.stringify({ diff })
396
- });
397
- if (!response.ok) {
398
- const errorText = await response.text();
399
- throw new Error(`API request failed (${response.status}): ${errorText}`);
400
- }
401
- return await response.json();
402
- }
403
-
404
- //#endregion
405
- //#region src/utils/config.ts
406
- const MAX_CUSTOM_PROMPT_LENGTH = 500;
407
- const CONFIG_DIR = join(homedir(), ".commitguard");
408
- const PROJECTS_CONFIG_PATH = join(CONFIG_DIR, "projects.json");
409
- let projectsConfigCache = null;
410
- const GIT_DIR$1 = ".git";
411
- function ensureConfigDir() {
412
- if (!existsSync(CONFIG_DIR)) try {
413
- mkdirSync(CONFIG_DIR, { recursive: true });
414
- } catch (e) {
415
- consola.error(`Failed to create config directory at ${CONFIG_DIR}: ${e.message}`);
416
- }
417
- }
418
- function getDefaultConfig() {
419
- return {
420
- checks: {
421
- security: true,
422
- performance: true,
423
- codeQuality: true,
424
- architecture: true
425
- },
426
- severityLevels: {
427
- critical: true,
428
- warning: true,
429
- suggestion: false
430
- },
431
- customRule: ""
432
- };
433
- }
434
- let projectIdCache = null;
435
- function getProjectId() {
436
- if (projectIdCache) return projectIdCache;
437
- try {
438
- projectIdCache = execFileSync("git", [
439
- "rev-list",
440
- "--max-parents=0",
441
- "HEAD"
442
- ], {
443
- encoding: "utf8",
444
- stdio: [
445
- "pipe",
446
- "pipe",
447
- "ignore"
448
- ]
449
- }).trim().split("\n")[0];
450
- return projectIdCache;
451
- } catch {
452
- consola.error("Warning: Unable to determine project ID. Using current working directory as fallback project ID.");
453
- projectIdCache = process.cwd();
454
- return projectIdCache;
455
- }
456
- }
457
- function loadProjectsConfig() {
458
- if (projectsConfigCache) return projectsConfigCache;
459
- if (existsSync(PROJECTS_CONFIG_PATH)) try {
460
- const content = readFileSync(PROJECTS_CONFIG_PATH, "utf8");
461
- projectsConfigCache = JSON.parse(content);
462
- return projectsConfigCache;
463
- } catch {
464
- consola.warn("Failed to parse projects config");
465
- }
466
- projectsConfigCache = {};
467
- return projectsConfigCache;
468
- }
469
- function saveProjectsConfig(projects) {
633
+ const diff = getLastDiff(loadConfig().context);
634
+ const controller = new AbortController();
635
+ const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
470
636
  try {
471
- ensureConfigDir();
472
- writeFileSync(PROJECTS_CONFIG_PATH, JSON.stringify(projects, null, 2));
473
- projectsConfigCache = projects;
474
- } catch (e) {
475
- consola.error(`Failed to save projects config: ${e.message}`);
476
- }
477
- }
478
- function loadConfig() {
479
- const projectId = getProjectId();
480
- return loadProjectsConfig()[projectId] || getDefaultConfig();
481
- }
482
- async function manageConfig() {
483
- if (!existsSync(GIT_DIR$1)) {
484
- cancel(MESSAGES.noGit);
485
- return;
486
- }
487
- const projectId = getProjectId();
488
- const currentConfig = loadConfig();
489
- intro(`CommitGuard Configuration`);
490
- const enabledChecks = await multiselect({
491
- message: "Select enabled checks for this project:",
492
- options: [
493
- {
494
- value: "security",
495
- label: "Security"
496
- },
497
- {
498
- value: "performance",
499
- label: "Performance"
500
- },
501
- {
502
- value: "codeQuality",
503
- label: "Code Quality"
504
- },
505
- {
506
- value: "architecture",
507
- label: "Architecture"
508
- }
509
- ],
510
- initialValues: Object.entries(currentConfig.checks).filter(([_, enabled]) => enabled).map(([key]) => key)
511
- });
512
- if (isCancel(enabledChecks)) {
513
- cancel("Configuration cancelled");
514
- return;
515
- }
516
- const enabledSeverity = await multiselect({
517
- message: "Select severity levels for enabled checks:",
518
- options: [
519
- {
520
- value: "suggestion",
521
- label: "Suggestion"
522
- },
523
- {
524
- value: "warning",
525
- label: "Warning"
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"
526
643
  },
527
- {
528
- value: "critical",
529
- label: "Critical"
530
- }
531
- ],
532
- initialValues: Object.entries(currentConfig.severityLevels).filter(([_, enabled]) => enabled).map(([key]) => key)
533
- });
534
- if (isCancel(enabledSeverity)) {
535
- cancel("Configuration cancelled");
536
- return;
537
- }
538
- let customRule = currentConfig.customRule;
539
- if (currentConfig.customRule) {
540
- consola.info(`Current custom rule: ${currentConfig.customRule}`);
541
- const editCustomRule = await confirm({
542
- message: "Would you like to edit the custom rule? (Currently only available to pro users)",
543
- initialValue: false
544
- });
545
- if (isCancel(editCustomRule)) {
546
- cancel("Configuration cancelled");
547
- return;
548
- }
549
- if (editCustomRule) {
550
- const newCustomRule = await text({
551
- message: "Enter new custom rule (leave empty to remove):",
552
- initialValue: currentConfig.customRule,
553
- validate: (value) => {
554
- const val = String(value).trim();
555
- if (!val) return void 0;
556
- if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`;
557
- }
558
- });
559
- if (isCancel(newCustomRule)) {
560
- cancel("Configuration cancelled");
561
- return;
562
- }
563
- customRule = String(newCustomRule).trim();
564
- }
565
- } else {
566
- const addCustomRule = await confirm({
567
- message: "Would you like to add a custom rule for this project? (Currently only available to pro users)",
568
- initialValue: false
644
+ body: JSON.stringify({ diff }),
645
+ signal: controller.signal
569
646
  });
570
- if (isCancel(addCustomRule)) {
571
- cancel("Configuration cancelled");
572
- return;
573
- }
574
- if (addCustomRule) {
575
- const newCustomRule = await text({
576
- message: "Enter custom rule (leave empty to skip):",
577
- placeholder: "e.g., Check for proper error handling in async functions",
578
- validate: (value) => {
579
- const val = String(value).trim();
580
- if (!val) return void 0;
581
- if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`;
582
- }
583
- });
584
- if (isCancel(newCustomRule)) {
585
- cancel("Configuration cancelled");
586
- return;
587
- }
588
- 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}`);
589
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;
590
657
  }
591
- const newConfig = {
592
- checks: {
593
- security: enabledChecks.includes("security"),
594
- performance: enabledChecks.includes("performance"),
595
- codeQuality: enabledChecks.includes("codeQuality"),
596
- architecture: enabledChecks.includes("architecture")
597
- },
598
- severityLevels: {
599
- suggestion: enabledSeverity.includes("suggestion"),
600
- warning: enabledSeverity.includes("warning"),
601
- critical: enabledSeverity.includes("critical")
602
- },
603
- customRule
604
- };
605
- if (JSON.stringify(newConfig) === JSON.stringify(currentConfig)) {
606
- outro("No changes made to the configuration.");
607
- return;
608
- }
609
- const confirmUpdate = await confirm({ message: "Save this configuration?" });
610
- if (isCancel(confirmUpdate)) {
611
- cancel("Configuration cancelled");
612
- return;
613
- }
614
- if (!confirmUpdate) {
615
- outro("Configuration not saved.");
616
- return;
617
- }
618
- const projects = loadProjectsConfig();
619
- projects[projectId] = newConfig;
620
- saveProjectsConfig(projects);
621
- outro("✓ Configuration updated for this project!");
622
658
  }
623
659
 
624
660
  //#endregion
@@ -680,13 +716,13 @@ async function getEslintRules({ startDir = process.cwd(), overrideCache = false
680
716
  } catch {}
681
717
  return null;
682
718
  });
683
- const config = (await Promise.all(loaders)).find((r) => r !== null) ?? {
719
+ const config$1 = (await Promise.all(loaders)).find((r) => r !== null) ?? {
684
720
  rules: {},
685
721
  source: null
686
722
  };
687
- cache.setKey(cacheKey, config);
723
+ cache.setKey(cacheKey, config$1);
688
724
  cache.save(true);
689
- return config;
725
+ return config$1;
690
726
  }
691
727
 
692
728
  //#endregion
@@ -741,7 +777,7 @@ async function installHooks() {
741
777
  }
742
778
  log.info("Installing CommitGuard...");
743
779
  const node = process.execPath.replace(/\\/g, "/");
744
- const cliPath = __require.resolve("commitguard").replace(/\\/g, "/");
780
+ const cliPath = fileURLToPath(import.meta.url).replace(/\\/g, "/");
745
781
  writeFileSync(COMMIT_MSG_HOOK_PATH, `#!/bin/sh
746
782
  ${COMMITGUARD_MARKER}
747
783
  # Auto-generated - do not edit manually
@@ -790,9 +826,14 @@ fi
790
826
 
791
827
  exit 0
792
828
  `, { mode: 493 });
793
- log.info("Analyzing ESLint configuration for better checks...");
794
- await getEslintRules({ overrideCache: true });
795
- log.success("ESLint configuration loaded.");
829
+ log.info("Checking ESLint configuration...");
830
+ try {
831
+ const eslintRules = await getEslintRules({ overrideCache: true });
832
+ if (Object.keys(eslintRules.rules).length > 0) log.success("ESLint configuration loaded.");
833
+ else log.info("No ESLint rules detected.");
834
+ } catch {
835
+ log.warn("Failed to load ESLint configuration.");
836
+ }
796
837
  if (!getGlobalKey() && process.env.COMMITGUARD_API_KEY === void 0) {
797
838
  if (await confirm({
798
839
  message: "No global API key found. Do you want to set it now?",
@@ -844,6 +885,7 @@ async function removeHooks() {
844
885
  //#endregion
845
886
  //#region src/utils/staged.ts
846
887
  const CACHE_PATH = join(".git", "commitguard-cache.json");
888
+ const config = loadConfig();
847
889
  const CATEGORY_LABELS = {
848
890
  security: "🚨 [SECURITY]",
849
891
  performance: "🚀 [PERFORMANCE]",
@@ -902,7 +944,7 @@ function groupIssuesByFile(issues = []) {
902
944
  };
903
945
  }
904
946
  async function onStaged() {
905
- const diff = getStagedDiff();
947
+ const diff = getStagedDiff(config.context);
906
948
  if (!diff.trim()) {
907
949
  clearCache();
908
950
  return;
@@ -911,7 +953,6 @@ async function onStaged() {
911
953
  const existingCache = readCache();
912
954
  if (existingCache && existingCache.hash === diffHash) return;
913
955
  try {
914
- const config = loadConfig();
915
956
  const response = await sendToCommitGuard(diff, (await getEslintRules()).rules, config);
916
957
  writeCache({
917
958
  hash: diffHash,
@@ -924,7 +965,7 @@ async function onStaged() {
924
965
  }
925
966
  }
926
967
  function getCachedAnalysis(diff, diffHash) {
927
- const effectiveDiff = diff ?? getStagedDiff();
968
+ const effectiveDiff = diff ?? getStagedDiff(config.context);
928
969
  if (!effectiveDiff.trim()) return {
929
970
  analysis: {
930
971
  status: "pass",
@@ -943,7 +984,7 @@ function getCachedAnalysis(diff, diffHash) {
943
984
  };
944
985
  }
945
986
  async function validateCommit() {
946
- const diff = getStagedDiff();
987
+ const diff = getStagedDiff(config.context);
947
988
  const diffHash = diff.trim() ? createDiffHash(diff) : "";
948
989
  const cached = getCachedAnalysis(diff, diffHash);
949
990
  if (!cached) {
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@commitguard/cli",
3
3
  "type": "module",
4
- "version": "0.0.3",
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": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/moshetanzer/commitguard.git"
9
+ "url": "git+https://github.com/commitguard/cli.git"
10
10
  },
11
11
  "exports": {
12
12
  ".": "./dist/index.mjs",
@@ -21,6 +21,16 @@
21
21
  "files": [
22
22
  "dist"
23
23
  ],
24
+ "scripts": {
25
+ "build": "tsdown",
26
+ "dev": "tsdown --watch",
27
+ "test": "vitest",
28
+ "typecheck": "tsc --noEmit",
29
+ "prepublishOnly": "pnpm run build",
30
+ "lint": "eslint .",
31
+ "lint:fix": "eslint . --fix",
32
+ "release": "pnpm typecheck && bumpp --tag --push --publish && npm publish"
33
+ },
24
34
  "dependencies": {
25
35
  "@clack/prompts": "^0.11.0",
26
36
  "@napi-rs/keyring": "^1.2.0",
@@ -42,13 +52,5 @@
42
52
  "tsdown": "^0.18.3",
43
53
  "typescript": "^5.9.3",
44
54
  "vitest": "^4.0.16"
45
- },
46
- "scripts": {
47
- "build": "tsdown",
48
- "dev": "tsdown --watch",
49
- "test": "vitest",
50
- "typecheck": "tsc --noEmit",
51
- "lint": "eslint .",
52
- "lint:fix": "eslint . --fix"
53
55
  }
54
- }
56
+ }