@commitguard/cli 0.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/dist/index.d.mts +2 -0
- package/dist/index.mjs +1055 -0
- package/package.json +54 -0
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1055 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { consola } from "consola";
|
|
5
|
+
import updateNotifier from "update-notifier";
|
|
6
|
+
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
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
14
|
+
import { pathToFileURL } from "node:url";
|
|
15
|
+
import { findUp } from "find-up";
|
|
16
|
+
import { FlatCache } from "flat-cache";
|
|
17
|
+
import stringWidth from "string-width";
|
|
18
|
+
import "dotenv/config";
|
|
19
|
+
|
|
20
|
+
//#region rolldown:runtime
|
|
21
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region package.json
|
|
25
|
+
var version = "0.0.1";
|
|
26
|
+
var package_default = {
|
|
27
|
+
name: "@commitguard/cli",
|
|
28
|
+
type: "module",
|
|
29
|
+
version,
|
|
30
|
+
description: "AI-powered git commit checker that blocks bad code before it ships",
|
|
31
|
+
license: "MIT",
|
|
32
|
+
repository: {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/moshetanzer/commitguard.git"
|
|
35
|
+
},
|
|
36
|
+
exports: {
|
|
37
|
+
".": "./dist/index.mjs",
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
main: "./dist/index.mjs",
|
|
41
|
+
module: "./dist/index.mjs",
|
|
42
|
+
types: "./dist/index.d.mts",
|
|
43
|
+
bin: { "commitguard": "./dist/index.mjs" },
|
|
44
|
+
files: ["dist"],
|
|
45
|
+
scripts: {
|
|
46
|
+
"build": "tsdown",
|
|
47
|
+
"dev": "tsdown --watch",
|
|
48
|
+
"test": "vitest",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"prepublishOnly": "pnpm run build",
|
|
51
|
+
"lint": "eslint .",
|
|
52
|
+
"lint:fix": "eslint . --fix"
|
|
53
|
+
},
|
|
54
|
+
dependencies: {
|
|
55
|
+
"@clack/prompts": "^0.11.0",
|
|
56
|
+
"@napi-rs/keyring": "^1.2.0",
|
|
57
|
+
"consola": "^3.4.2",
|
|
58
|
+
"dotenv": "^17.2.3",
|
|
59
|
+
"find-up": "^8.0.0",
|
|
60
|
+
"flat-cache": "^6.1.19",
|
|
61
|
+
"micromatch": "^4.0.8",
|
|
62
|
+
"string-width": "^8.1.0",
|
|
63
|
+
"update-notifier": "^7.3.1"
|
|
64
|
+
},
|
|
65
|
+
devDependencies: {
|
|
66
|
+
"@antfu/eslint-config": "^6.7.3",
|
|
67
|
+
"@types/micromatch": "^4.0.10",
|
|
68
|
+
"@types/node": "^24.10.4",
|
|
69
|
+
"@types/update-notifier": "^6.0.8",
|
|
70
|
+
"bumpp": "^10.3.2",
|
|
71
|
+
"eslint": "^9.39.2",
|
|
72
|
+
"tsdown": "^0.18.3",
|
|
73
|
+
"typescript": "^5.9.3",
|
|
74
|
+
"vitest": "^4.0.16"
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/data/ignore.json
|
|
80
|
+
var ignore = [
|
|
81
|
+
".git/**",
|
|
82
|
+
".svn/**",
|
|
83
|
+
".hg/**",
|
|
84
|
+
"node_modules/**",
|
|
85
|
+
"vendor/**",
|
|
86
|
+
".pnp/**",
|
|
87
|
+
"Pods/**",
|
|
88
|
+
"Packages/**",
|
|
89
|
+
"package.json",
|
|
90
|
+
"package-lock.json",
|
|
91
|
+
"pnpm-lock.yaml",
|
|
92
|
+
"yarn.lock",
|
|
93
|
+
"bun.lockb",
|
|
94
|
+
"composer.lock",
|
|
95
|
+
"poetry.lock",
|
|
96
|
+
"Pipfile.lock",
|
|
97
|
+
"Cargo.lock",
|
|
98
|
+
"go.sum",
|
|
99
|
+
"mix.lock",
|
|
100
|
+
"dist/**",
|
|
101
|
+
"build/**",
|
|
102
|
+
"out/**",
|
|
103
|
+
".output/**",
|
|
104
|
+
".nuxt/**",
|
|
105
|
+
".next/**",
|
|
106
|
+
".svelte-kit/**",
|
|
107
|
+
"public/build/**",
|
|
108
|
+
"target/**",
|
|
109
|
+
"bin/**",
|
|
110
|
+
"obj/**",
|
|
111
|
+
"Generated/**",
|
|
112
|
+
".angular/**",
|
|
113
|
+
".expo/**",
|
|
114
|
+
".vite/**",
|
|
115
|
+
".vercel/**",
|
|
116
|
+
".netlify/**",
|
|
117
|
+
"DerivedData/**",
|
|
118
|
+
"*.xcworkspace/xcuserdata/**",
|
|
119
|
+
"android/app/build/**",
|
|
120
|
+
"ios/build/**",
|
|
121
|
+
"*.class",
|
|
122
|
+
"*.jar",
|
|
123
|
+
"*.war",
|
|
124
|
+
"*.ear",
|
|
125
|
+
"*.kotlin_module",
|
|
126
|
+
"__pycache__/**",
|
|
127
|
+
"*.pyc",
|
|
128
|
+
"*.pyo",
|
|
129
|
+
"*.pyd",
|
|
130
|
+
"*.egg-info/**",
|
|
131
|
+
".pytest_cache/**",
|
|
132
|
+
".mypy_cache/**",
|
|
133
|
+
"target/**",
|
|
134
|
+
"*.o",
|
|
135
|
+
"*.obj",
|
|
136
|
+
"*.so",
|
|
137
|
+
"*.a",
|
|
138
|
+
"*.dll",
|
|
139
|
+
"*.exe",
|
|
140
|
+
"*.out",
|
|
141
|
+
"*.pdb",
|
|
142
|
+
"*.zip",
|
|
143
|
+
"*.tar",
|
|
144
|
+
"*.gz",
|
|
145
|
+
"*.bz2",
|
|
146
|
+
"*.7z",
|
|
147
|
+
"*.rar",
|
|
148
|
+
"*.iso",
|
|
149
|
+
"*.img",
|
|
150
|
+
"*.dmg",
|
|
151
|
+
"*.map",
|
|
152
|
+
"*.min.js",
|
|
153
|
+
"*.min.css",
|
|
154
|
+
"*.bundle.js",
|
|
155
|
+
"*.wasm",
|
|
156
|
+
"*.log",
|
|
157
|
+
"logs/**",
|
|
158
|
+
".cache/**",
|
|
159
|
+
".parcel-cache/**",
|
|
160
|
+
".eslintcache",
|
|
161
|
+
".tsbuildinfo",
|
|
162
|
+
"*.png",
|
|
163
|
+
"*.jpg",
|
|
164
|
+
"*.jpeg",
|
|
165
|
+
"*.gif",
|
|
166
|
+
"*.bmp",
|
|
167
|
+
"*.svg",
|
|
168
|
+
"*.webp",
|
|
169
|
+
"*.avif",
|
|
170
|
+
"*.ico",
|
|
171
|
+
"*.mp4",
|
|
172
|
+
"*.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
|
+
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/utils/git.ts
|
|
236
|
+
function getStagedDiff() {
|
|
237
|
+
try {
|
|
238
|
+
return addGitLineNumbers(execFileSync("git", [
|
|
239
|
+
"diff",
|
|
240
|
+
"--cached",
|
|
241
|
+
"--no-color",
|
|
242
|
+
"--function-context",
|
|
243
|
+
"--diff-algorithm=histogram",
|
|
244
|
+
"--diff-filter=AMC",
|
|
245
|
+
"--",
|
|
246
|
+
".",
|
|
247
|
+
...ignore.map((p) => `:(exclude)${p}`)
|
|
248
|
+
], {
|
|
249
|
+
encoding: "utf8",
|
|
250
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
251
|
+
stdio: [
|
|
252
|
+
"pipe",
|
|
253
|
+
"pipe",
|
|
254
|
+
"ignore"
|
|
255
|
+
]
|
|
256
|
+
}));
|
|
257
|
+
} catch {
|
|
258
|
+
return "";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function getLastDiff() {
|
|
262
|
+
try {
|
|
263
|
+
return execFileSync("git", [
|
|
264
|
+
"diff",
|
|
265
|
+
"HEAD~1",
|
|
266
|
+
"HEAD",
|
|
267
|
+
"--no-color",
|
|
268
|
+
"--function-context",
|
|
269
|
+
"--diff-algorithm=histogram",
|
|
270
|
+
"--diff-filter=AMC",
|
|
271
|
+
"--",
|
|
272
|
+
".",
|
|
273
|
+
...ignore.map((p) => `:(exclude)${p}`)
|
|
274
|
+
], {
|
|
275
|
+
encoding: "utf8",
|
|
276
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
277
|
+
stdio: [
|
|
278
|
+
"pipe",
|
|
279
|
+
"pipe",
|
|
280
|
+
"ignore"
|
|
281
|
+
]
|
|
282
|
+
});
|
|
283
|
+
} catch {
|
|
284
|
+
return "";
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region src/utils/key.ts
|
|
290
|
+
const store = new Entry("commit_guard", "global_key");
|
|
291
|
+
function setGlobalKey(apiKey) {
|
|
292
|
+
store.setPassword(apiKey);
|
|
293
|
+
}
|
|
294
|
+
function getGlobalKey() {
|
|
295
|
+
return store.getPassword();
|
|
296
|
+
}
|
|
297
|
+
function deleteGlobalKey() {
|
|
298
|
+
store.deletePassword();
|
|
299
|
+
}
|
|
300
|
+
function validateApiKey(value) {
|
|
301
|
+
if (!value || value.trim() === "") return "API key cannot be empty.";
|
|
302
|
+
if (value.startsWith("sk_") === false) return "Invalid API key format. It should start with \"sk_\".";
|
|
303
|
+
}
|
|
304
|
+
async function manageGlobalKey() {
|
|
305
|
+
try {
|
|
306
|
+
if (getGlobalKey()) {
|
|
307
|
+
intro("An existing API key was found.");
|
|
308
|
+
if (await confirm({
|
|
309
|
+
message: "Do you want to delete the existing global API key?",
|
|
310
|
+
initialValue: false
|
|
311
|
+
})) {
|
|
312
|
+
deleteGlobalKey();
|
|
313
|
+
log.success("Global API key deleted.");
|
|
314
|
+
if (await confirm({
|
|
315
|
+
message: "Do you want to add a new global API key?",
|
|
316
|
+
initialValue: true
|
|
317
|
+
})) {
|
|
318
|
+
const apiKey = await text({
|
|
319
|
+
message: "Enter your CommitGuard API key:",
|
|
320
|
+
placeholder: "sk_XXXXXXXXXXXXXXXXXXXXXX",
|
|
321
|
+
validate: validateApiKey
|
|
322
|
+
});
|
|
323
|
+
if (typeof apiKey === "string") {
|
|
324
|
+
setGlobalKey(apiKey);
|
|
325
|
+
outro("New global API key set successfully.");
|
|
326
|
+
}
|
|
327
|
+
} else outro("API key removed. You can set a new one later using `commitguard init` or `commitguard keys`.");
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
note("To get your free API key, visit https://commitguard.dev", "Get your free API key");
|
|
331
|
+
const apiKey = await text({
|
|
332
|
+
message: "Enter your CommitGuard API key:",
|
|
333
|
+
placeholder: "sk_XXXXXXXXXXXXXXXXXXXXXX",
|
|
334
|
+
validate: validateApiKey
|
|
335
|
+
});
|
|
336
|
+
if (typeof apiKey === "string") {
|
|
337
|
+
setGlobalKey(apiKey);
|
|
338
|
+
log.success("Global API key set successfully.");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} catch (error) {
|
|
342
|
+
const err = error;
|
|
343
|
+
if (err.name === "ExitPromptError") {
|
|
344
|
+
log.message("\nš Until next time!");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
log.error(`Error managing global API key: ${err.message}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/utils/api.ts
|
|
353
|
+
async function sendToCommitGuard(diff, eslint, config) {
|
|
354
|
+
const apiKey = process.env.COMMITGUARD_API_KEY || getGlobalKey() || null;
|
|
355
|
+
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.dev");
|
|
356
|
+
const apiUrl = process.env.COMMITGUARD_API_URL || "https://api.commitguard.ai/v1/analyze";
|
|
357
|
+
const response = await fetch(apiUrl, {
|
|
358
|
+
method: "POST",
|
|
359
|
+
headers: {
|
|
360
|
+
"Content-Type": "application/json",
|
|
361
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
362
|
+
"User-Agent": "commitguard-cli"
|
|
363
|
+
},
|
|
364
|
+
body: JSON.stringify({
|
|
365
|
+
diff,
|
|
366
|
+
eslint,
|
|
367
|
+
config
|
|
368
|
+
})
|
|
369
|
+
});
|
|
370
|
+
if (!response.ok) {
|
|
371
|
+
const errorText = await response.text();
|
|
372
|
+
throw new Error(`API request failed (${response.status}): ${errorText}`);
|
|
373
|
+
}
|
|
374
|
+
return await response.json();
|
|
375
|
+
}
|
|
376
|
+
async function bypassCommitGuard() {
|
|
377
|
+
const apiKey = process.env.COMMITGUARD_API_KEY || getGlobalKey() || null;
|
|
378
|
+
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.dev");
|
|
379
|
+
const apiUrl = process.env.COMMITGUARD_API_BYPASS_URL || "https://api.commitguard.ai/v1/bypass";
|
|
380
|
+
const diff = getLastDiff();
|
|
381
|
+
const response = await fetch(apiUrl, {
|
|
382
|
+
method: "POST",
|
|
383
|
+
headers: {
|
|
384
|
+
"Content-Type": "application/json",
|
|
385
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
386
|
+
"User-Agent": "commitguard-cli"
|
|
387
|
+
},
|
|
388
|
+
body: JSON.stringify({ diff })
|
|
389
|
+
});
|
|
390
|
+
if (!response.ok) {
|
|
391
|
+
const errorText = await response.text();
|
|
392
|
+
throw new Error(`API request failed (${response.status}): ${errorText}`);
|
|
393
|
+
}
|
|
394
|
+
return await response.json();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/utils/config.ts
|
|
399
|
+
const MAX_CUSTOM_PROMPT_LENGTH = 500;
|
|
400
|
+
const CONFIG_DIR = join(homedir(), ".commitguard");
|
|
401
|
+
const PROJECTS_CONFIG_PATH = join(CONFIG_DIR, "projects.json");
|
|
402
|
+
let projectsConfigCache = null;
|
|
403
|
+
function ensureConfigDir() {
|
|
404
|
+
if (!existsSync(CONFIG_DIR)) try {
|
|
405
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
406
|
+
} catch (e) {
|
|
407
|
+
consola.error(`Failed to create config directory at ${CONFIG_DIR}: ${e.message}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function getDefaultConfig() {
|
|
411
|
+
return {
|
|
412
|
+
checks: {
|
|
413
|
+
security: true,
|
|
414
|
+
performance: true,
|
|
415
|
+
codeQuality: true,
|
|
416
|
+
architecture: true
|
|
417
|
+
},
|
|
418
|
+
severityLevels: {
|
|
419
|
+
critical: true,
|
|
420
|
+
warning: true,
|
|
421
|
+
suggestion: true
|
|
422
|
+
},
|
|
423
|
+
customRule: ""
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
let projectIdCache = null;
|
|
427
|
+
function getProjectId() {
|
|
428
|
+
if (projectIdCache) return projectIdCache;
|
|
429
|
+
try {
|
|
430
|
+
projectIdCache = execFileSync("git", [
|
|
431
|
+
"rev-list",
|
|
432
|
+
"--max-parents=0",
|
|
433
|
+
"HEAD"
|
|
434
|
+
], {
|
|
435
|
+
encoding: "utf8",
|
|
436
|
+
stdio: [
|
|
437
|
+
"pipe",
|
|
438
|
+
"pipe",
|
|
439
|
+
"ignore"
|
|
440
|
+
]
|
|
441
|
+
}).trim().split("\n")[0];
|
|
442
|
+
return projectIdCache;
|
|
443
|
+
} catch {
|
|
444
|
+
consola.error("Warning: Unable to determine project ID. Using current working directory as fallback project ID.");
|
|
445
|
+
projectIdCache = process.cwd();
|
|
446
|
+
return projectIdCache;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function loadProjectsConfig() {
|
|
450
|
+
if (projectsConfigCache) return projectsConfigCache;
|
|
451
|
+
if (existsSync(PROJECTS_CONFIG_PATH)) try {
|
|
452
|
+
const content = readFileSync(PROJECTS_CONFIG_PATH, "utf8");
|
|
453
|
+
projectsConfigCache = JSON.parse(content);
|
|
454
|
+
return projectsConfigCache;
|
|
455
|
+
} catch {
|
|
456
|
+
consola.warn("Failed to parse projects config");
|
|
457
|
+
}
|
|
458
|
+
projectsConfigCache = {};
|
|
459
|
+
return projectsConfigCache;
|
|
460
|
+
}
|
|
461
|
+
function saveProjectsConfig(projects) {
|
|
462
|
+
try {
|
|
463
|
+
ensureConfigDir();
|
|
464
|
+
writeFileSync(PROJECTS_CONFIG_PATH, JSON.stringify(projects, null, 2));
|
|
465
|
+
projectsConfigCache = projects;
|
|
466
|
+
} catch (e) {
|
|
467
|
+
consola.error(`Failed to save projects config: ${e.message}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function loadConfig() {
|
|
471
|
+
const projectId = getProjectId();
|
|
472
|
+
return loadProjectsConfig()[projectId] || getDefaultConfig();
|
|
473
|
+
}
|
|
474
|
+
async function manageConfig() {
|
|
475
|
+
const projectId = getProjectId();
|
|
476
|
+
const currentConfig = loadConfig();
|
|
477
|
+
intro(`CommitGuard Configuration`);
|
|
478
|
+
const enabledChecks = await multiselect({
|
|
479
|
+
message: "Select enabled checks for this project:",
|
|
480
|
+
options: [
|
|
481
|
+
{
|
|
482
|
+
value: "security",
|
|
483
|
+
label: "Security"
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
value: "performance",
|
|
487
|
+
label: "Performance"
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
value: "codeQuality",
|
|
491
|
+
label: "Code Quality"
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
value: "architecture",
|
|
495
|
+
label: "Architecture"
|
|
496
|
+
}
|
|
497
|
+
],
|
|
498
|
+
initialValues: Object.entries(currentConfig.checks).filter(([_, enabled]) => enabled).map(([key]) => key)
|
|
499
|
+
});
|
|
500
|
+
if (isCancel(enabledChecks)) {
|
|
501
|
+
cancel("Configuration cancelled");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const enabledSeverity = await multiselect({
|
|
505
|
+
message: "Select severity levels for enabled checks:",
|
|
506
|
+
options: [
|
|
507
|
+
{
|
|
508
|
+
value: "suggestion",
|
|
509
|
+
label: "Suggestion"
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
value: "warning",
|
|
513
|
+
label: "Warning"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
value: "critical",
|
|
517
|
+
label: "Critical"
|
|
518
|
+
}
|
|
519
|
+
],
|
|
520
|
+
initialValues: Object.entries(currentConfig.severityLevels).filter(([_, enabled]) => enabled).map(([key]) => key)
|
|
521
|
+
});
|
|
522
|
+
if (isCancel(enabledSeverity)) {
|
|
523
|
+
cancel("Configuration cancelled");
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
let customRule = currentConfig.customRule;
|
|
527
|
+
if (currentConfig.customRule) {
|
|
528
|
+
consola.info(`Current custom rule: ${currentConfig.customRule}`);
|
|
529
|
+
const editCustomRule = await confirm({
|
|
530
|
+
message: "Would you like to edit the custom rule? (Currently only available to pro users)",
|
|
531
|
+
initialValue: false
|
|
532
|
+
});
|
|
533
|
+
if (isCancel(editCustomRule)) {
|
|
534
|
+
cancel("Configuration cancelled");
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (editCustomRule) {
|
|
538
|
+
const newCustomRule = await text({
|
|
539
|
+
message: "Enter new custom rule (leave empty to remove):",
|
|
540
|
+
initialValue: currentConfig.customRule,
|
|
541
|
+
validate: (value) => {
|
|
542
|
+
const val = String(value).trim();
|
|
543
|
+
if (!val) return void 0;
|
|
544
|
+
if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`;
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
if (isCancel(newCustomRule)) {
|
|
548
|
+
cancel("Configuration cancelled");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
customRule = String(newCustomRule).trim();
|
|
552
|
+
}
|
|
553
|
+
} else {
|
|
554
|
+
const addCustomRule = await confirm({
|
|
555
|
+
message: "Would you like to add a custom rule for this project? (Currently only available to pro users)",
|
|
556
|
+
initialValue: false
|
|
557
|
+
});
|
|
558
|
+
if (isCancel(addCustomRule)) {
|
|
559
|
+
cancel("Configuration cancelled");
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (addCustomRule) {
|
|
563
|
+
const newCustomRule = await text({
|
|
564
|
+
message: "Enter custom rule (leave empty to skip):",
|
|
565
|
+
placeholder: "e.g., Check for proper error handling in async functions",
|
|
566
|
+
validate: (value) => {
|
|
567
|
+
const val = String(value).trim();
|
|
568
|
+
if (!val) return void 0;
|
|
569
|
+
if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`;
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
if (isCancel(newCustomRule)) {
|
|
573
|
+
cancel("Configuration cancelled");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
customRule = String(newCustomRule).trim();
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const newConfig = {
|
|
580
|
+
checks: {
|
|
581
|
+
security: enabledChecks.includes("security"),
|
|
582
|
+
performance: enabledChecks.includes("performance"),
|
|
583
|
+
codeQuality: enabledChecks.includes("codeQuality"),
|
|
584
|
+
architecture: enabledChecks.includes("architecture")
|
|
585
|
+
},
|
|
586
|
+
severityLevels: {
|
|
587
|
+
suggestion: enabledSeverity.includes("suggestion"),
|
|
588
|
+
warning: enabledSeverity.includes("warning"),
|
|
589
|
+
critical: enabledSeverity.includes("critical")
|
|
590
|
+
},
|
|
591
|
+
customRule
|
|
592
|
+
};
|
|
593
|
+
if (JSON.stringify(newConfig) === JSON.stringify(currentConfig)) {
|
|
594
|
+
outro("No changes made to the configuration.");
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const confirmUpdate = await confirm({ message: "Save this configuration?" });
|
|
598
|
+
if (isCancel(confirmUpdate)) {
|
|
599
|
+
cancel("Configuration cancelled");
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (!confirmUpdate) {
|
|
603
|
+
outro("Configuration not saved.");
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const projects = loadProjectsConfig();
|
|
607
|
+
projects[projectId] = newConfig;
|
|
608
|
+
saveProjectsConfig(projects);
|
|
609
|
+
outro("ā Configuration updated for this project!");
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
//#endregion
|
|
613
|
+
//#region src/utils/eslint.ts
|
|
614
|
+
const cacheDir = join(homedir(), ".cache", "commitguard");
|
|
615
|
+
const cache = new FlatCache({
|
|
616
|
+
cacheDir,
|
|
617
|
+
cacheId: "eslint-config"
|
|
618
|
+
});
|
|
619
|
+
cache.load("eslint-config", cacheDir);
|
|
620
|
+
async function findProjectRoot(startDir) {
|
|
621
|
+
const packageJsonPath = await findUp("package.json");
|
|
622
|
+
return packageJsonPath ? dirname(packageJsonPath) : startDir;
|
|
623
|
+
}
|
|
624
|
+
async function getEslintRules({ startDir = process.cwd(), overrideCache = false } = {}) {
|
|
625
|
+
const cacheKey = `eslint-${startDir}`;
|
|
626
|
+
const cached = cache.getKey(cacheKey);
|
|
627
|
+
if (cached && !overrideCache) return cached;
|
|
628
|
+
const projectRoot = await findProjectRoot(startDir);
|
|
629
|
+
const loaders = [
|
|
630
|
+
".eslintrc",
|
|
631
|
+
".eslintrc.json",
|
|
632
|
+
".eslintrc.js",
|
|
633
|
+
".eslintrc.mjs",
|
|
634
|
+
"eslint.config.js",
|
|
635
|
+
"eslint.config.mjs",
|
|
636
|
+
"package.json"
|
|
637
|
+
].map((file) => join(projectRoot, file)).map(async (full) => {
|
|
638
|
+
if (!existsSync(full)) return null;
|
|
639
|
+
if (full.endsWith(".json") || full.endsWith(".eslintrc")) try {
|
|
640
|
+
const raw = JSON.parse(await readFile(full, "utf8"));
|
|
641
|
+
if (raw.rules) return {
|
|
642
|
+
rules: raw.rules,
|
|
643
|
+
source: full
|
|
644
|
+
};
|
|
645
|
+
} catch {}
|
|
646
|
+
if (full.endsWith(".js") || full.endsWith(".mjs")) try {
|
|
647
|
+
const mod = await import(pathToFileURL(full).href);
|
|
648
|
+
let cfg = mod.default || mod;
|
|
649
|
+
if (cfg instanceof Promise) cfg = await cfg;
|
|
650
|
+
if (Array.isArray(cfg)) {
|
|
651
|
+
const lastConfigWithRules = [...cfg].reverse().find((c) => c.rules && Object.keys(c.rules).length > 0);
|
|
652
|
+
if (lastConfigWithRules?.rules) return {
|
|
653
|
+
rules: lastConfigWithRules.rules,
|
|
654
|
+
source: full
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
if (cfg.rules) return {
|
|
658
|
+
rules: cfg.rules,
|
|
659
|
+
source: full
|
|
660
|
+
};
|
|
661
|
+
} catch {}
|
|
662
|
+
if (full.endsWith("package.json")) try {
|
|
663
|
+
const pkg = JSON.parse(await readFile(full, "utf8"));
|
|
664
|
+
if (pkg.eslintConfig && pkg.eslintConfig.rules) return {
|
|
665
|
+
rules: pkg.eslintConfig.rules,
|
|
666
|
+
source: full
|
|
667
|
+
};
|
|
668
|
+
} catch {}
|
|
669
|
+
return null;
|
|
670
|
+
});
|
|
671
|
+
const config = (await Promise.all(loaders)).find((r) => r !== null) ?? {
|
|
672
|
+
rules: {},
|
|
673
|
+
source: null
|
|
674
|
+
};
|
|
675
|
+
cache.setKey(cacheKey, config);
|
|
676
|
+
cache.save(true);
|
|
677
|
+
return config;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
//#endregion
|
|
681
|
+
//#region src/utils/install.ts
|
|
682
|
+
const COMMITGUARD_MARKER = "# CommitGuard commit-msg hook";
|
|
683
|
+
const POST_INDEX_MARKER = "# CommitGuard post-index-change hook";
|
|
684
|
+
const GIT_DIR = ".git";
|
|
685
|
+
const HOOKS_DIR = join(GIT_DIR, "hooks");
|
|
686
|
+
const COMMIT_MSG_HOOK_PATH = join(HOOKS_DIR, "commit-msg");
|
|
687
|
+
const POST_INDEX_HOOK_PATH = join(HOOKS_DIR, "post-index-change");
|
|
688
|
+
const MESSAGES = { noGit: "No .git folder found. Run this inside a git repository." };
|
|
689
|
+
async function installHooks() {
|
|
690
|
+
if (!existsSync(GIT_DIR)) {
|
|
691
|
+
cancel(MESSAGES.noGit);
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
if (!existsSync(HOOKS_DIR)) mkdirSync(HOOKS_DIR, { recursive: true });
|
|
695
|
+
try {
|
|
696
|
+
const versionMatch = execSync("git --version", { encoding: "utf8" }).match(/(\d+\.\d+\.\d+)/);
|
|
697
|
+
if ((versionMatch ? versionMatch[1] : "0.0.0") < "2.34.0") {
|
|
698
|
+
log.warn("Your Git version is below 2.34.0. CommitGuard requires Git 2.34.0 or higher to function properly.");
|
|
699
|
+
note("You can download the latest version of Git from https://git-scm.com/downloads", "How to update Git");
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
} catch {
|
|
703
|
+
log.warn("Unable to determine Git version. Please ensure you have Git 2.34.0 or higher installed for CommitGuard to function properly.");
|
|
704
|
+
note("You can download the latest version of Git from https://git-scm.com/downloads", "How to update Git");
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (existsSync(COMMIT_MSG_HOOK_PATH)) {
|
|
708
|
+
if (readFileSync(COMMIT_MSG_HOOK_PATH, "utf8").includes(COMMITGUARD_MARKER)) {
|
|
709
|
+
outro("CommitGuard is already installed.");
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (!await confirm({
|
|
713
|
+
message: "CommitGuard uses the git `commit-msg` hook to function properly. A commit-msg hook already exists. Do you want to overwrite it?",
|
|
714
|
+
initialValue: true
|
|
715
|
+
})) {
|
|
716
|
+
outro("Installation cancelled. CommitGuard was not installed.");
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (existsSync(POST_INDEX_HOOK_PATH)) {
|
|
721
|
+
if (!readFileSync(POST_INDEX_HOOK_PATH, "utf8").includes(POST_INDEX_MARKER)) {
|
|
722
|
+
if (!await confirm({
|
|
723
|
+
message: "A post-index-change hook already exists. Do you want to overwrite it?",
|
|
724
|
+
initialValue: true
|
|
725
|
+
})) {
|
|
726
|
+
log.error("Installation cancelled. CommitGuard was not installed.");
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
log.info("Installing CommitGuard...");
|
|
732
|
+
const node = process.execPath.replace(/\\/g, "/");
|
|
733
|
+
const cliPath = __require.resolve("commitguard").replace(/\\/g, "/");
|
|
734
|
+
writeFileSync(COMMIT_MSG_HOOK_PATH, `#!/bin/sh
|
|
735
|
+
${COMMITGUARD_MARKER}
|
|
736
|
+
# Auto-generated - do not edit manually
|
|
737
|
+
|
|
738
|
+
commit_msg_file="$1"
|
|
739
|
+
|
|
740
|
+
if grep -q -- "--skip" "$commit_msg_file"; then
|
|
741
|
+
echo "ā ļø CommitGuard bypassed with --skip"
|
|
742
|
+
|
|
743
|
+
sed 's/--skip//g' "$commit_msg_file" > "$commit_msg_file.tmp"
|
|
744
|
+
mv "$commit_msg_file.tmp" "$commit_msg_file"
|
|
745
|
+
|
|
746
|
+
trap '(sleep 1 && "${node}" "${cliPath}" bypass > /dev/null 2>&1 &)' EXIT
|
|
747
|
+
|
|
748
|
+
exit 0
|
|
749
|
+
fi
|
|
750
|
+
|
|
751
|
+
if [ ! -s "$commit_msg_file" ] || ! grep -qv '^#' "$commit_msg_file"; then
|
|
752
|
+
exit 0
|
|
753
|
+
fi
|
|
754
|
+
|
|
755
|
+
if [ -t 1 ]; then
|
|
756
|
+
"${node}" "${cliPath}" pre-commit < /dev/tty
|
|
757
|
+
RESULT=$?
|
|
758
|
+
else
|
|
759
|
+
"${node}" "${cliPath}" pre-commit
|
|
760
|
+
RESULT=$?
|
|
761
|
+
fi
|
|
762
|
+
|
|
763
|
+
if [ $RESULT -ne 0 ]; then
|
|
764
|
+
exit 1
|
|
765
|
+
fi
|
|
766
|
+
|
|
767
|
+
exit 0
|
|
768
|
+
`, { mode: 493 });
|
|
769
|
+
writeFileSync(POST_INDEX_HOOK_PATH, `#!/bin/sh
|
|
770
|
+
${POST_INDEX_MARKER}
|
|
771
|
+
# Auto-generated - do not edit manually
|
|
772
|
+
|
|
773
|
+
# Skip if only flags changed (not actual content)
|
|
774
|
+
if [ "$1" = "1" ]; then
|
|
775
|
+
exit 0
|
|
776
|
+
fi
|
|
777
|
+
|
|
778
|
+
"${node}" "${cliPath}" staged > /dev/null 2>&1 &
|
|
779
|
+
|
|
780
|
+
exit 0
|
|
781
|
+
`, { mode: 493 });
|
|
782
|
+
log.info("Analyzing ESLint configuration for better checks...");
|
|
783
|
+
await getEslintRules({ overrideCache: true });
|
|
784
|
+
log.success("ESLint configuration loaded.");
|
|
785
|
+
if (!getGlobalKey() && process.env.COMMITGUARD_API_KEY === void 0) {
|
|
786
|
+
if (await confirm({
|
|
787
|
+
message: "No global API key found. Do you want to set it now?",
|
|
788
|
+
initialValue: true
|
|
789
|
+
})) await manageGlobalKey();
|
|
790
|
+
}
|
|
791
|
+
outro("You are all set! CommitGuard has been installed successfully.");
|
|
792
|
+
}
|
|
793
|
+
async function listHooks() {
|
|
794
|
+
if (!existsSync(GIT_DIR) || !existsSync(HOOKS_DIR)) {
|
|
795
|
+
outro(MESSAGES.noGit);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const hooks = readdirSync(HOOKS_DIR).filter((file) => {
|
|
799
|
+
const filePath = join(HOOKS_DIR, file);
|
|
800
|
+
return statSync(filePath).isFile() && !file.endsWith(".sample") && !file.startsWith(".") && (readFileSync(filePath, "utf8").includes(COMMITGUARD_MARKER) || readFileSync(filePath, "utf8").includes(POST_INDEX_MARKER));
|
|
801
|
+
});
|
|
802
|
+
if (hooks.length === 0) {
|
|
803
|
+
outro();
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
for (const hook of hooks) log.success(hook);
|
|
807
|
+
outro("Run \"commitguard remove\" to uninstall CommitGuard.");
|
|
808
|
+
}
|
|
809
|
+
async function removeHooks() {
|
|
810
|
+
if (!existsSync(GIT_DIR)) {
|
|
811
|
+
cancel(MESSAGES.noGit);
|
|
812
|
+
process.exit(1);
|
|
813
|
+
}
|
|
814
|
+
const commitMsgExists = existsSync(COMMIT_MSG_HOOK_PATH);
|
|
815
|
+
const postIndexExists = existsSync(POST_INDEX_HOOK_PATH);
|
|
816
|
+
if (!commitMsgExists && !postIndexExists) {
|
|
817
|
+
log.info("CommitGuard is not installed in this repository.");
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (!await confirm({
|
|
821
|
+
message: "Are you sure you want to remove CommitGuard from this repository?",
|
|
822
|
+
initialValue: false
|
|
823
|
+
})) {
|
|
824
|
+
outro("CommitGuard uninstallation cancelled.");
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (commitMsgExists) unlinkSync(COMMIT_MSG_HOOK_PATH);
|
|
828
|
+
if (postIndexExists) unlinkSync(POST_INDEX_HOOK_PATH);
|
|
829
|
+
log.success("CommitGuard uninstalled successfully!");
|
|
830
|
+
outro("Your commits are no longer be secured by CommitGuard.");
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
//#endregion
|
|
834
|
+
//#region src/utils/staged.ts
|
|
835
|
+
const CACHE_PATH = join(".git", "commitguard-cache.json");
|
|
836
|
+
const CATEGORY_LABELS = {
|
|
837
|
+
security: "šØ [SECURITY]",
|
|
838
|
+
performance: "š [PERFORMANCE]",
|
|
839
|
+
code_quality: "⨠[CODE QUALITY]",
|
|
840
|
+
architecture: "šļø [ARCHITECTURE]"
|
|
841
|
+
};
|
|
842
|
+
const SEVERITY = {
|
|
843
|
+
critical: "CRITICAL",
|
|
844
|
+
warning: "WARNING",
|
|
845
|
+
suggestion: "SUGGESTION"
|
|
846
|
+
};
|
|
847
|
+
const LABEL_WIDTH = Math.max(...Object.values(CATEGORY_LABELS).map((label) => stringWidth(label)));
|
|
848
|
+
function padLabel(label) {
|
|
849
|
+
const pad = LABEL_WIDTH - stringWidth(label);
|
|
850
|
+
return label + " ".repeat(pad);
|
|
851
|
+
}
|
|
852
|
+
const SEVERITY_WIDTH = Math.max(...Object.values(SEVERITY).map((sev) => stringWidth(sev) + 2));
|
|
853
|
+
function padSeverity(severity) {
|
|
854
|
+
const pad = SEVERITY_WIDTH - stringWidth(severity);
|
|
855
|
+
return severity + " ".repeat(pad);
|
|
856
|
+
}
|
|
857
|
+
let memoryCache = null;
|
|
858
|
+
function readCache() {
|
|
859
|
+
if (memoryCache) return memoryCache;
|
|
860
|
+
if (!existsSync(CACHE_PATH)) return null;
|
|
861
|
+
try {
|
|
862
|
+
const content = readFileSync(CACHE_PATH, "utf8");
|
|
863
|
+
memoryCache = JSON.parse(content);
|
|
864
|
+
return memoryCache;
|
|
865
|
+
} catch {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
function writeCache(data) {
|
|
870
|
+
memoryCache = data;
|
|
871
|
+
writeFileSync(CACHE_PATH, JSON.stringify(data));
|
|
872
|
+
}
|
|
873
|
+
function clearCache() {
|
|
874
|
+
memoryCache = null;
|
|
875
|
+
if (existsSync(CACHE_PATH)) unlinkSync(CACHE_PATH);
|
|
876
|
+
}
|
|
877
|
+
function groupIssuesByFile(issues = []) {
|
|
878
|
+
const grouped = {};
|
|
879
|
+
const noFile = [];
|
|
880
|
+
for (const issue of issues) {
|
|
881
|
+
if (!issue.file) {
|
|
882
|
+
noFile.push(issue);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (!grouped[issue.file]) grouped[issue.file] = [];
|
|
886
|
+
grouped[issue.file].push(issue);
|
|
887
|
+
}
|
|
888
|
+
return {
|
|
889
|
+
grouped,
|
|
890
|
+
noFile
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
async function onStaged() {
|
|
894
|
+
const diff = getStagedDiff();
|
|
895
|
+
if (!diff.trim()) {
|
|
896
|
+
clearCache();
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
const diffHash = createDiffHash(diff);
|
|
900
|
+
const existingCache = readCache();
|
|
901
|
+
if (existingCache && existingCache.hash === diffHash) return;
|
|
902
|
+
try {
|
|
903
|
+
const config = loadConfig();
|
|
904
|
+
const response = await sendToCommitGuard(diff, (await getEslintRules()).rules, config);
|
|
905
|
+
writeCache({
|
|
906
|
+
hash: diffHash,
|
|
907
|
+
timestamp: Date.now(),
|
|
908
|
+
diff,
|
|
909
|
+
analysis: response
|
|
910
|
+
});
|
|
911
|
+
} catch (error) {
|
|
912
|
+
consola.error("Analysis failed:", error);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
function getCachedAnalysis(diff, diffHash) {
|
|
916
|
+
const effectiveDiff = diff ?? getStagedDiff();
|
|
917
|
+
if (!effectiveDiff.trim()) return {
|
|
918
|
+
analysis: {
|
|
919
|
+
status: "pass",
|
|
920
|
+
issues: []
|
|
921
|
+
},
|
|
922
|
+
age: 0
|
|
923
|
+
};
|
|
924
|
+
const effectiveDiffHash = diffHash ?? createDiffHash(effectiveDiff);
|
|
925
|
+
const cache$1 = readCache();
|
|
926
|
+
if (!cache$1) return null;
|
|
927
|
+
if (cache$1.hash !== effectiveDiffHash) return null;
|
|
928
|
+
const age = Math.round((Date.now() - cache$1.timestamp) / 1e3);
|
|
929
|
+
return {
|
|
930
|
+
analysis: cache$1.analysis,
|
|
931
|
+
age
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
async function validateCommit() {
|
|
935
|
+
const diff = getStagedDiff();
|
|
936
|
+
const diffHash = diff.trim() ? createDiffHash(diff) : "";
|
|
937
|
+
const cached = getCachedAnalysis(diff, diffHash);
|
|
938
|
+
if (!cached) {
|
|
939
|
+
await onStaged();
|
|
940
|
+
const newCached = getCachedAnalysis(diff, diffHash);
|
|
941
|
+
if (!newCached) {
|
|
942
|
+
consola.error("Analysis failed");
|
|
943
|
+
process.exit(1);
|
|
944
|
+
}
|
|
945
|
+
await displayResults(newCached.analysis);
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
await displayResults(cached.analysis);
|
|
949
|
+
}
|
|
950
|
+
async function displayResults(analysis) {
|
|
951
|
+
const isTTY = process.stdout.isTTY;
|
|
952
|
+
if (analysis.status === "pass" || analysis.approved || !analysis.issues || analysis.issues.length === 0) {
|
|
953
|
+
consola.success("All checks passed");
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const count = analysis.issues?.length ?? 0;
|
|
957
|
+
if (!isTTY) consola.log(`CommitGuard Detected ${count} issue${count === 1 ? "" : "s"}.`);
|
|
958
|
+
const { grouped, noFile } = groupIssuesByFile(analysis.issues);
|
|
959
|
+
const fileCount = Object.keys(grouped).length;
|
|
960
|
+
if (isTTY) consola.log(`\nCommitGuard Detected ${count} issue${count === 1 ? "" : "s"} in ${fileCount} file${fileCount === 1 ? "" : "s"}:`);
|
|
961
|
+
else consola.log(`\nIssues in ${fileCount} file${fileCount === 1 ? "" : "s"}:`);
|
|
962
|
+
for (const [file, issues] of Object.entries(grouped)) {
|
|
963
|
+
let output = `\nš ${file}\n`;
|
|
964
|
+
const lastIdx = issues.length - 1;
|
|
965
|
+
for (let i = 0; i < issues.length; i++) {
|
|
966
|
+
const issue = issues[i];
|
|
967
|
+
const prefix = i === lastIdx ? " āā" : " āā";
|
|
968
|
+
const label = padLabel(CATEGORY_LABELS[issue.category] ?? "ā¢");
|
|
969
|
+
const severity = padSeverity(issue.severity ? `[${issue.severity.toUpperCase()}]` : "[INFO]");
|
|
970
|
+
const location = issue.file ? ` (${issue.file}${issue.line ? `:${issue.line}` : ""})` : "";
|
|
971
|
+
output += `${prefix} ${label} ${severity} ${issue.message}${location}\n`;
|
|
972
|
+
}
|
|
973
|
+
process.stdout.write(output);
|
|
974
|
+
}
|
|
975
|
+
if (noFile.length > 0) {
|
|
976
|
+
consola.log("\nš General Issues");
|
|
977
|
+
noFile.forEach((issue, idx) => {
|
|
978
|
+
const prefix = idx === noFile.length - 1 ? " āā" : " āā";
|
|
979
|
+
const emoji = CATEGORY_LABELS[issue.category] || "ā¢";
|
|
980
|
+
consola.log(`${prefix} ${emoji} ${issue.message}`);
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
consola.log("\nFix these issues and try again.\n");
|
|
984
|
+
if (!isTTY) {
|
|
985
|
+
consola.log("--------------------");
|
|
986
|
+
consola.log("Unfortunately, CommitGuard cannot prompt for easy confirmation in non-interactive mode. To see how this works, please use git in a supported terminal.");
|
|
987
|
+
consola.log("To bypass this check, add --skip anywhere in your commit message");
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
if (await consola.prompt("Do you want to ignore these issues and commit anyway?", {
|
|
991
|
+
type: "confirm",
|
|
992
|
+
initial: false
|
|
993
|
+
})) {
|
|
994
|
+
consola.log("ā ļø Commit forced by user despite detected issues.\n");
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
consola.log("\nš” To bypass this check, add --skip anywhere in your commit message\n");
|
|
998
|
+
process.exit(1);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
//#endregion
|
|
1002
|
+
//#region src/index.ts
|
|
1003
|
+
updateNotifier({ pkg: package_default }).notify();
|
|
1004
|
+
const command = process.argv[2];
|
|
1005
|
+
(async () => {
|
|
1006
|
+
try {
|
|
1007
|
+
switch (command) {
|
|
1008
|
+
case "init":
|
|
1009
|
+
await installHooks();
|
|
1010
|
+
break;
|
|
1011
|
+
case "config":
|
|
1012
|
+
await manageConfig();
|
|
1013
|
+
break;
|
|
1014
|
+
case "pre-commit":
|
|
1015
|
+
await validateCommit();
|
|
1016
|
+
break;
|
|
1017
|
+
case "list":
|
|
1018
|
+
case "ls":
|
|
1019
|
+
await listHooks();
|
|
1020
|
+
break;
|
|
1021
|
+
case "remove":
|
|
1022
|
+
case "uninstall":
|
|
1023
|
+
await removeHooks();
|
|
1024
|
+
break;
|
|
1025
|
+
case "keys":
|
|
1026
|
+
await manageGlobalKey();
|
|
1027
|
+
break;
|
|
1028
|
+
case "bypass":
|
|
1029
|
+
await bypassCommitGuard();
|
|
1030
|
+
break;
|
|
1031
|
+
case "staged":
|
|
1032
|
+
await onStaged();
|
|
1033
|
+
break;
|
|
1034
|
+
default: consola.box(`
|
|
1035
|
+
CommitGuard - AI-powered git commit checker v${version}
|
|
1036
|
+
|
|
1037
|
+
Usage:
|
|
1038
|
+
commitguard init Initialize CommitGuard in the current git repository
|
|
1039
|
+
commitguard remove Remove CommitGuard from the current git repository
|
|
1040
|
+
commitguard config Configure CommitGuard settings for the current repository
|
|
1041
|
+
commitguard keys Manage your CommitGuard API key
|
|
1042
|
+
|
|
1043
|
+
Links:
|
|
1044
|
+
Documentation: https://commitguard.ai/docs
|
|
1045
|
+
Dashboard: https://commitguard.ai/dashboard
|
|
1046
|
+
Support: https://commitguard.ai/support`);
|
|
1047
|
+
}
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
consola.error("CommitGuard error:", error);
|
|
1050
|
+
process.exit(1);
|
|
1051
|
+
}
|
|
1052
|
+
})();
|
|
1053
|
+
|
|
1054
|
+
//#endregion
|
|
1055
|
+
export { };
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@commitguard/cli",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"description": "AI-powered git commit checker that blocks bad code before it ships",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/moshetanzer/commitguard.git"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/index.mjs",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.mjs",
|
|
16
|
+
"module": "./dist/index.mjs",
|
|
17
|
+
"types": "./dist/index.d.mts",
|
|
18
|
+
"bin": {
|
|
19
|
+
"commitguard": "./dist/index.mjs"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@clack/prompts": "^0.11.0",
|
|
26
|
+
"@napi-rs/keyring": "^1.2.0",
|
|
27
|
+
"consola": "^3.4.2",
|
|
28
|
+
"dotenv": "^17.2.3",
|
|
29
|
+
"find-up": "^8.0.0",
|
|
30
|
+
"flat-cache": "^6.1.19",
|
|
31
|
+
"micromatch": "^4.0.8",
|
|
32
|
+
"string-width": "^8.1.0",
|
|
33
|
+
"update-notifier": "^7.3.1"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@antfu/eslint-config": "^6.7.3",
|
|
37
|
+
"@types/micromatch": "^4.0.10",
|
|
38
|
+
"@types/node": "^24.10.4",
|
|
39
|
+
"@types/update-notifier": "^6.0.8",
|
|
40
|
+
"bumpp": "^10.3.2",
|
|
41
|
+
"eslint": "^9.39.2",
|
|
42
|
+
"tsdown": "^0.18.3",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"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
|
+
}
|
|
54
|
+
}
|