@commitguard/cli 0.0.14 → 0.0.16
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.mjs +269 -287
- 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.
|
|
20
|
+
var version = "0.0.16";
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
265
|
+
"--function-context",
|
|
505
266
|
"--diff-algorithm=histogram",
|
|
506
267
|
"--diff-filter=AMC",
|
|
507
268
|
"--",
|
|
@@ -613,6 +374,7 @@ async function sendToCommitGuard(diff, eslint, config$1) {
|
|
|
613
374
|
const errorText = await response.text();
|
|
614
375
|
let errorMessage = "Failed to analyze commit";
|
|
615
376
|
if (response.status === 401) errorMessage = "Invalid API key. Check your key with \"commitguard keys\" or get a new one at https://commitguard.ai";
|
|
377
|
+
else if (response.status === 402) errorMessage = "You’ve reached your plan limit. Upgrade now with 20% off using code SAVE20 at checkout: https://commitguard.ai/dashboard";
|
|
616
378
|
else if (response.status === 429) errorMessage = "Rate limit exceeded. Please try again later";
|
|
617
379
|
else if (response.status === 500) errorMessage = "CommitGuard service error. Please try again later";
|
|
618
380
|
else if (response.status >= 400 && response.status < 500) errorMessage = `Request error: ${errorText || "Invalid request"}`;
|
|
@@ -630,7 +392,7 @@ async function bypassCommitGuard() {
|
|
|
630
392
|
const apiKey = process.env.COMMITGUARD_API_KEY || getGlobalKey() || null;
|
|
631
393
|
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
394
|
const apiUrl = process.env.COMMITGUARD_API_BYPASS_URL || "https://api.commitguard.ai/v1/bypass";
|
|
633
|
-
const diff = getLastDiff(
|
|
395
|
+
const diff = getLastDiff();
|
|
634
396
|
const controller = new AbortController();
|
|
635
397
|
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
|
|
636
398
|
try {
|
|
@@ -657,6 +419,226 @@ async function bypassCommitGuard() {
|
|
|
657
419
|
}
|
|
658
420
|
}
|
|
659
421
|
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/utils/config.ts
|
|
424
|
+
const MAX_CUSTOM_PROMPT_LENGTH = 500;
|
|
425
|
+
const CONFIG_DIR = join(homedir(), ".commitguard");
|
|
426
|
+
const PROJECTS_CONFIG_PATH = join(CONFIG_DIR, "projects.json");
|
|
427
|
+
let projectsConfigCache = null;
|
|
428
|
+
const GIT_DIR$1 = ".git";
|
|
429
|
+
function ensureConfigDir() {
|
|
430
|
+
if (!existsSync(CONFIG_DIR)) try {
|
|
431
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
432
|
+
} catch (e) {
|
|
433
|
+
consola.error(`Failed to create config directory at ${CONFIG_DIR}: ${e.message}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function getDefaultConfig() {
|
|
437
|
+
return {
|
|
438
|
+
checks: {
|
|
439
|
+
security: true,
|
|
440
|
+
performance: true,
|
|
441
|
+
codeQuality: true,
|
|
442
|
+
architecture: true
|
|
443
|
+
},
|
|
444
|
+
severityLevels: {
|
|
445
|
+
critical: true,
|
|
446
|
+
warning: true,
|
|
447
|
+
suggestion: false
|
|
448
|
+
},
|
|
449
|
+
customRule: ""
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
let projectIdCache = null;
|
|
453
|
+
function getProjectId() {
|
|
454
|
+
if (projectIdCache) return projectIdCache;
|
|
455
|
+
try {
|
|
456
|
+
projectIdCache = execFileSync("git", [
|
|
457
|
+
"rev-list",
|
|
458
|
+
"--max-parents=0",
|
|
459
|
+
"HEAD"
|
|
460
|
+
], {
|
|
461
|
+
encoding: "utf8",
|
|
462
|
+
stdio: [
|
|
463
|
+
"pipe",
|
|
464
|
+
"pipe",
|
|
465
|
+
"ignore"
|
|
466
|
+
]
|
|
467
|
+
}).trim().split("\n")[0];
|
|
468
|
+
return projectIdCache;
|
|
469
|
+
} catch {
|
|
470
|
+
consola.error("Warning: Unable to determine project ID. Using current working directory as fallback project ID.");
|
|
471
|
+
projectIdCache = process.cwd();
|
|
472
|
+
return projectIdCache;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function loadProjectsConfig() {
|
|
476
|
+
if (projectsConfigCache) return projectsConfigCache;
|
|
477
|
+
if (existsSync(PROJECTS_CONFIG_PATH)) try {
|
|
478
|
+
const content = readFileSync(PROJECTS_CONFIG_PATH, "utf8");
|
|
479
|
+
projectsConfigCache = JSON.parse(content);
|
|
480
|
+
return projectsConfigCache;
|
|
481
|
+
} catch {
|
|
482
|
+
consola.warn("Failed to parse projects config");
|
|
483
|
+
}
|
|
484
|
+
projectsConfigCache = {};
|
|
485
|
+
return projectsConfigCache;
|
|
486
|
+
}
|
|
487
|
+
function saveProjectsConfig(projects) {
|
|
488
|
+
try {
|
|
489
|
+
ensureConfigDir();
|
|
490
|
+
writeFileSync(PROJECTS_CONFIG_PATH, JSON.stringify(projects, null, 2));
|
|
491
|
+
projectsConfigCache = projects;
|
|
492
|
+
} catch (e) {
|
|
493
|
+
consola.error(`Failed to save projects config: ${e.message}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function loadConfig() {
|
|
497
|
+
const projectId = getProjectId();
|
|
498
|
+
return loadProjectsConfig()[projectId] || getDefaultConfig();
|
|
499
|
+
}
|
|
500
|
+
async function manageConfig() {
|
|
501
|
+
if (!existsSync(GIT_DIR$1)) {
|
|
502
|
+
cancel(MESSAGES.noGit);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const projectId = getProjectId();
|
|
506
|
+
const currentConfig = loadConfig();
|
|
507
|
+
intro(`CommitGuard Configuration`);
|
|
508
|
+
const enabledChecks = await multiselect({
|
|
509
|
+
message: "Select enabled checks for this project:",
|
|
510
|
+
options: [
|
|
511
|
+
{
|
|
512
|
+
value: "security",
|
|
513
|
+
label: "Security"
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
value: "performance",
|
|
517
|
+
label: "Performance"
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
value: "codeQuality",
|
|
521
|
+
label: "Code Quality"
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
value: "architecture",
|
|
525
|
+
label: "Architecture"
|
|
526
|
+
}
|
|
527
|
+
],
|
|
528
|
+
initialValues: Object.entries(currentConfig.checks).filter(([_, enabled]) => enabled).map(([key]) => key)
|
|
529
|
+
});
|
|
530
|
+
if (isCancel(enabledChecks)) {
|
|
531
|
+
cancel("Configuration cancelled");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const enabledSeverity = await multiselect({
|
|
535
|
+
message: "Select severity levels for enabled checks:",
|
|
536
|
+
options: [
|
|
537
|
+
{
|
|
538
|
+
value: "suggestion",
|
|
539
|
+
label: "Suggestion"
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
value: "warning",
|
|
543
|
+
label: "Warning"
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
value: "critical",
|
|
547
|
+
label: "Critical"
|
|
548
|
+
}
|
|
549
|
+
],
|
|
550
|
+
initialValues: Object.entries(currentConfig.severityLevels).filter(([_, enabled]) => enabled).map(([key]) => key)
|
|
551
|
+
});
|
|
552
|
+
if (isCancel(enabledSeverity)) {
|
|
553
|
+
cancel("Configuration cancelled");
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
let customRule = currentConfig.customRule;
|
|
557
|
+
if (currentConfig.customRule) {
|
|
558
|
+
log.info(`Current custom rule: ${currentConfig.customRule}`);
|
|
559
|
+
const editCustomRule = await confirm({
|
|
560
|
+
message: "Would you like to edit the custom rule? (Currently only available to pro users)",
|
|
561
|
+
initialValue: false
|
|
562
|
+
});
|
|
563
|
+
if (isCancel(editCustomRule)) {
|
|
564
|
+
cancel("Configuration cancelled");
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (editCustomRule) {
|
|
568
|
+
const newCustomRule = await text({
|
|
569
|
+
message: "Enter new custom rule (leave empty to remove):",
|
|
570
|
+
initialValue: currentConfig.customRule,
|
|
571
|
+
validate: (value) => {
|
|
572
|
+
const val = String(value).trim();
|
|
573
|
+
if (!val) return void 0;
|
|
574
|
+
if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`;
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
if (isCancel(newCustomRule)) {
|
|
578
|
+
cancel("Configuration cancelled");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
customRule = String(newCustomRule).trim();
|
|
582
|
+
}
|
|
583
|
+
} else {
|
|
584
|
+
const addCustomRule = await confirm({
|
|
585
|
+
message: "Would you like to add a custom rule for this project? (Currently only available to pro users)",
|
|
586
|
+
initialValue: false
|
|
587
|
+
});
|
|
588
|
+
if (isCancel(addCustomRule)) {
|
|
589
|
+
cancel("Configuration cancelled");
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (addCustomRule) {
|
|
593
|
+
const newCustomRule = await text({
|
|
594
|
+
message: "Enter custom rule (leave empty to skip):",
|
|
595
|
+
placeholder: "e.g., Check for proper error handling in async functions",
|
|
596
|
+
validate: (value) => {
|
|
597
|
+
const val = String(value).trim();
|
|
598
|
+
if (!val) return void 0;
|
|
599
|
+
if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`;
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
if (isCancel(newCustomRule)) {
|
|
603
|
+
cancel("Configuration cancelled");
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
customRule = String(newCustomRule).trim();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const newConfig = {
|
|
610
|
+
checks: {
|
|
611
|
+
security: enabledChecks.includes("security"),
|
|
612
|
+
performance: enabledChecks.includes("performance"),
|
|
613
|
+
codeQuality: enabledChecks.includes("codeQuality"),
|
|
614
|
+
architecture: enabledChecks.includes("architecture")
|
|
615
|
+
},
|
|
616
|
+
severityLevels: {
|
|
617
|
+
suggestion: enabledSeverity.includes("suggestion"),
|
|
618
|
+
warning: enabledSeverity.includes("warning"),
|
|
619
|
+
critical: enabledSeverity.includes("critical")
|
|
620
|
+
},
|
|
621
|
+
customRule
|
|
622
|
+
};
|
|
623
|
+
if (JSON.stringify(newConfig) === JSON.stringify(currentConfig)) {
|
|
624
|
+
outro("No changes made to the configuration.");
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const confirmUpdate = await confirm({ message: "Save this configuration?" });
|
|
628
|
+
if (isCancel(confirmUpdate)) {
|
|
629
|
+
cancel("Configuration cancelled");
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (!confirmUpdate) {
|
|
633
|
+
outro("Configuration not saved.");
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const projects = loadProjectsConfig();
|
|
637
|
+
projects[projectId] = newConfig;
|
|
638
|
+
saveProjectsConfig(projects);
|
|
639
|
+
outro("✓ Configuration updated for this project!");
|
|
640
|
+
}
|
|
641
|
+
|
|
660
642
|
//#endregion
|
|
661
643
|
//#region src/utils/eslint.ts
|
|
662
644
|
const cacheDir = join(homedir(), ".cache", "commitguard");
|
|
@@ -944,7 +926,7 @@ function groupIssuesByFile(issues = []) {
|
|
|
944
926
|
};
|
|
945
927
|
}
|
|
946
928
|
async function onStaged() {
|
|
947
|
-
const diff = getStagedDiff(
|
|
929
|
+
const diff = getStagedDiff();
|
|
948
930
|
if (!diff.trim()) {
|
|
949
931
|
clearCache();
|
|
950
932
|
return;
|
|
@@ -965,7 +947,7 @@ async function onStaged() {
|
|
|
965
947
|
}
|
|
966
948
|
}
|
|
967
949
|
function getCachedAnalysis(diff, diffHash) {
|
|
968
|
-
const effectiveDiff = diff ?? getStagedDiff(
|
|
950
|
+
const effectiveDiff = diff ?? getStagedDiff();
|
|
969
951
|
if (!effectiveDiff.trim()) return {
|
|
970
952
|
analysis: {
|
|
971
953
|
status: "pass",
|
|
@@ -984,7 +966,7 @@ function getCachedAnalysis(diff, diffHash) {
|
|
|
984
966
|
};
|
|
985
967
|
}
|
|
986
968
|
async function validateCommit() {
|
|
987
|
-
const diff = getStagedDiff(
|
|
969
|
+
const diff = getStagedDiff();
|
|
988
970
|
const diffHash = diff.trim() ? createDiffHash(diff) : "";
|
|
989
971
|
const cached = getCachedAnalysis(diff, diffHash);
|
|
990
972
|
if (!cached) {
|