@commitguard/cli 0.0.14 → 0.0.15

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 (2) hide show
  1. package/dist/index.mjs +268 -287
  2. package/package.json +1 -1
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";
6
9
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
7
10
  import { homedir } from "node:os";
8
11
  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.14";
20
+ var version = "0.0.15";
21
21
  var package_default = {
22
22
  name: "@commitguard/cli",
23
23
  type: "module",
@@ -68,279 +68,8 @@ var package_default = {
68
68
  "tsdown": "^0.18.3",
69
69
  "typescript": "^5.9.3",
70
70
  "vitest": "^4.0.16"
71
- }
72
- };
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
- }
71
+ }
72
+ };
344
73
 
345
74
  //#endregion
346
75
  //#region src/data/ignore.json
@@ -465,16 +194,49 @@ var ignore = [
465
194
  "*.rdb"
466
195
  ];
467
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." };
230
+
468
231
  //#endregion
469
232
  //#region src/utils/git.ts
470
- function getStagedDiff(context) {
471
- const gitContextCommand = context === "minimal" ? [] : ["--function-context"];
233
+ function getStagedDiff() {
472
234
  try {
473
235
  return addGitLineNumbers(execFileSync("git", [
474
236
  "diff",
475
237
  "--cached",
476
238
  "--no-color",
477
- ...gitContextCommand,
239
+ "--function-context",
478
240
  "--diff-algorithm=histogram",
479
241
  "--diff-filter=AMC",
480
242
  "--",
@@ -493,15 +255,14 @@ function getStagedDiff(context) {
493
255
  return "";
494
256
  }
495
257
  }
496
- function getLastDiff(context) {
497
- const gitContextCommand = context === "minimal" ? [] : ["--function-context"];
258
+ function getLastDiff() {
498
259
  try {
499
260
  return execFileSync("git", [
500
261
  "diff",
501
262
  "HEAD~1",
502
263
  "HEAD",
503
264
  "--no-color",
504
- ...gitContextCommand,
265
+ "--function-context",
505
266
  "--diff-algorithm=histogram",
506
267
  "--diff-filter=AMC",
507
268
  "--",
@@ -630,7 +391,7 @@ async function bypassCommitGuard() {
630
391
  const apiKey = process.env.COMMITGUARD_API_KEY || getGlobalKey() || null;
631
392
  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");
632
393
  const apiUrl = process.env.COMMITGUARD_API_BYPASS_URL || "https://api.commitguard.ai/v1/bypass";
633
- const diff = getLastDiff(loadConfig().context);
394
+ const diff = getLastDiff();
634
395
  const controller = new AbortController();
635
396
  const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
636
397
  try {
@@ -657,6 +418,226 @@ async function bypassCommitGuard() {
657
418
  }
658
419
  }
659
420
 
421
+ //#endregion
422
+ //#region src/utils/config.ts
423
+ const MAX_CUSTOM_PROMPT_LENGTH = 500;
424
+ const CONFIG_DIR = join(homedir(), ".commitguard");
425
+ const PROJECTS_CONFIG_PATH = join(CONFIG_DIR, "projects.json");
426
+ let projectsConfigCache = null;
427
+ const GIT_DIR$1 = ".git";
428
+ function ensureConfigDir() {
429
+ if (!existsSync(CONFIG_DIR)) try {
430
+ mkdirSync(CONFIG_DIR, { recursive: true });
431
+ } catch (e) {
432
+ consola.error(`Failed to create config directory at ${CONFIG_DIR}: ${e.message}`);
433
+ }
434
+ }
435
+ function getDefaultConfig() {
436
+ return {
437
+ checks: {
438
+ security: true,
439
+ performance: true,
440
+ codeQuality: true,
441
+ architecture: true
442
+ },
443
+ severityLevels: {
444
+ critical: true,
445
+ warning: true,
446
+ suggestion: false
447
+ },
448
+ customRule: ""
449
+ };
450
+ }
451
+ let projectIdCache = null;
452
+ function getProjectId() {
453
+ if (projectIdCache) return projectIdCache;
454
+ try {
455
+ projectIdCache = execFileSync("git", [
456
+ "rev-list",
457
+ "--max-parents=0",
458
+ "HEAD"
459
+ ], {
460
+ encoding: "utf8",
461
+ stdio: [
462
+ "pipe",
463
+ "pipe",
464
+ "ignore"
465
+ ]
466
+ }).trim().split("\n")[0];
467
+ return projectIdCache;
468
+ } catch {
469
+ consola.error("Warning: Unable to determine project ID. Using current working directory as fallback project ID.");
470
+ projectIdCache = process.cwd();
471
+ return projectIdCache;
472
+ }
473
+ }
474
+ function loadProjectsConfig() {
475
+ if (projectsConfigCache) return projectsConfigCache;
476
+ if (existsSync(PROJECTS_CONFIG_PATH)) try {
477
+ const content = readFileSync(PROJECTS_CONFIG_PATH, "utf8");
478
+ projectsConfigCache = JSON.parse(content);
479
+ return projectsConfigCache;
480
+ } catch {
481
+ consola.warn("Failed to parse projects config");
482
+ }
483
+ projectsConfigCache = {};
484
+ return projectsConfigCache;
485
+ }
486
+ function saveProjectsConfig(projects) {
487
+ try {
488
+ ensureConfigDir();
489
+ writeFileSync(PROJECTS_CONFIG_PATH, JSON.stringify(projects, null, 2));
490
+ projectsConfigCache = projects;
491
+ } catch (e) {
492
+ consola.error(`Failed to save projects config: ${e.message}`);
493
+ }
494
+ }
495
+ function loadConfig() {
496
+ const projectId = getProjectId();
497
+ return loadProjectsConfig()[projectId] || getDefaultConfig();
498
+ }
499
+ async function manageConfig() {
500
+ if (!existsSync(GIT_DIR$1)) {
501
+ cancel(MESSAGES.noGit);
502
+ return;
503
+ }
504
+ const projectId = getProjectId();
505
+ const currentConfig = loadConfig();
506
+ intro(`CommitGuard Configuration`);
507
+ const enabledChecks = await multiselect({
508
+ message: "Select enabled checks for this project:",
509
+ options: [
510
+ {
511
+ value: "security",
512
+ label: "Security"
513
+ },
514
+ {
515
+ value: "performance",
516
+ label: "Performance"
517
+ },
518
+ {
519
+ value: "codeQuality",
520
+ label: "Code Quality"
521
+ },
522
+ {
523
+ value: "architecture",
524
+ label: "Architecture"
525
+ }
526
+ ],
527
+ initialValues: Object.entries(currentConfig.checks).filter(([_, enabled]) => enabled).map(([key]) => key)
528
+ });
529
+ if (isCancel(enabledChecks)) {
530
+ cancel("Configuration cancelled");
531
+ return;
532
+ }
533
+ const enabledSeverity = await multiselect({
534
+ message: "Select severity levels for enabled checks:",
535
+ options: [
536
+ {
537
+ value: "suggestion",
538
+ label: "Suggestion"
539
+ },
540
+ {
541
+ value: "warning",
542
+ label: "Warning"
543
+ },
544
+ {
545
+ value: "critical",
546
+ label: "Critical"
547
+ }
548
+ ],
549
+ initialValues: Object.entries(currentConfig.severityLevels).filter(([_, enabled]) => enabled).map(([key]) => key)
550
+ });
551
+ if (isCancel(enabledSeverity)) {
552
+ cancel("Configuration cancelled");
553
+ return;
554
+ }
555
+ let customRule = currentConfig.customRule;
556
+ if (currentConfig.customRule) {
557
+ log.info(`Current custom rule: ${currentConfig.customRule}`);
558
+ const editCustomRule = await confirm({
559
+ message: "Would you like to edit the custom rule? (Currently only available to pro users)",
560
+ initialValue: false
561
+ });
562
+ if (isCancel(editCustomRule)) {
563
+ cancel("Configuration cancelled");
564
+ return;
565
+ }
566
+ if (editCustomRule) {
567
+ const newCustomRule = await text({
568
+ message: "Enter new custom rule (leave empty to remove):",
569
+ initialValue: currentConfig.customRule,
570
+ validate: (value) => {
571
+ const val = String(value).trim();
572
+ if (!val) return void 0;
573
+ if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`;
574
+ }
575
+ });
576
+ if (isCancel(newCustomRule)) {
577
+ cancel("Configuration cancelled");
578
+ return;
579
+ }
580
+ customRule = String(newCustomRule).trim();
581
+ }
582
+ } else {
583
+ const addCustomRule = await confirm({
584
+ message: "Would you like to add a custom rule for this project? (Currently only available to pro users)",
585
+ initialValue: false
586
+ });
587
+ if (isCancel(addCustomRule)) {
588
+ cancel("Configuration cancelled");
589
+ return;
590
+ }
591
+ if (addCustomRule) {
592
+ const newCustomRule = await text({
593
+ message: "Enter custom rule (leave empty to skip):",
594
+ placeholder: "e.g., Check for proper error handling in async functions",
595
+ validate: (value) => {
596
+ const val = String(value).trim();
597
+ if (!val) return void 0;
598
+ if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`;
599
+ }
600
+ });
601
+ if (isCancel(newCustomRule)) {
602
+ cancel("Configuration cancelled");
603
+ return;
604
+ }
605
+ customRule = String(newCustomRule).trim();
606
+ }
607
+ }
608
+ const newConfig = {
609
+ checks: {
610
+ security: enabledChecks.includes("security"),
611
+ performance: enabledChecks.includes("performance"),
612
+ codeQuality: enabledChecks.includes("codeQuality"),
613
+ architecture: enabledChecks.includes("architecture")
614
+ },
615
+ severityLevels: {
616
+ suggestion: enabledSeverity.includes("suggestion"),
617
+ warning: enabledSeverity.includes("warning"),
618
+ critical: enabledSeverity.includes("critical")
619
+ },
620
+ customRule
621
+ };
622
+ if (JSON.stringify(newConfig) === JSON.stringify(currentConfig)) {
623
+ outro("No changes made to the configuration.");
624
+ return;
625
+ }
626
+ const confirmUpdate = await confirm({ message: "Save this configuration?" });
627
+ if (isCancel(confirmUpdate)) {
628
+ cancel("Configuration cancelled");
629
+ return;
630
+ }
631
+ if (!confirmUpdate) {
632
+ outro("Configuration not saved.");
633
+ return;
634
+ }
635
+ const projects = loadProjectsConfig();
636
+ projects[projectId] = newConfig;
637
+ saveProjectsConfig(projects);
638
+ outro("✓ Configuration updated for this project!");
639
+ }
640
+
660
641
  //#endregion
661
642
  //#region src/utils/eslint.ts
662
643
  const cacheDir = join(homedir(), ".cache", "commitguard");
@@ -944,7 +925,7 @@ function groupIssuesByFile(issues = []) {
944
925
  };
945
926
  }
946
927
  async function onStaged() {
947
- const diff = getStagedDiff(config.context);
928
+ const diff = getStagedDiff();
948
929
  if (!diff.trim()) {
949
930
  clearCache();
950
931
  return;
@@ -965,7 +946,7 @@ async function onStaged() {
965
946
  }
966
947
  }
967
948
  function getCachedAnalysis(diff, diffHash) {
968
- const effectiveDiff = diff ?? getStagedDiff(config.context);
949
+ const effectiveDiff = diff ?? getStagedDiff();
969
950
  if (!effectiveDiff.trim()) return {
970
951
  analysis: {
971
952
  status: "pass",
@@ -984,7 +965,7 @@ function getCachedAnalysis(diff, diffHash) {
984
965
  };
985
966
  }
986
967
  async function validateCommit() {
987
- const diff = getStagedDiff(config.context);
968
+ const diff = getStagedDiff();
988
969
  const diffHash = diff.trim() ? createDiffHash(diff) : "";
989
970
  const cached = getCachedAnalysis(diff, diffHash);
990
971
  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.14",
4
+ "version": "0.0.15",
5
5
  "description": "AI-powered git commit checker that blocks bad code before it ships",
6
6
  "license": "MIT",
7
7
  "repository": {