@commitguard/cli
Advanced tools
+266
-285
@@ -6,8 +6,8 @@ #!/usr/bin/env node | ||
| import { execFileSync, execSync } from "node:child_process"; | ||
| import { createHash } from "node:crypto"; | ||
| import { cancel, confirm, intro, isCancel, log, multiselect, note, outro, text } from "@clack/prompts"; | ||
| import { Entry } from "@napi-rs/keyring"; | ||
| import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs"; | ||
| import { homedir } from "node:os"; | ||
| import { dirname, join } from "node:path"; | ||
| import { cancel, confirm, intro, isCancel, log, multiselect, note, outro, select, text } from "@clack/prompts"; | ||
| import { createHash } from "node:crypto"; | ||
| import { Entry } from "@napi-rs/keyring"; | ||
| import { fileURLToPath, pathToFileURL } from "node:url"; | ||
@@ -21,3 +21,3 @@ import { readFile } from "node:fs/promises"; | ||
| //#region package.json | ||
| var version = "0.0.14"; | ||
| var version = "0.0.15"; | ||
| var package_default = { | ||
@@ -77,273 +77,2 @@ name: "@commitguard/cli", | ||
| //#endregion | ||
| //#region src/utils/global.ts | ||
| function createDiffHash(diff) { | ||
| return createHash("md5").update(diff).digest("base64url"); | ||
| } | ||
| function addGitLineNumbers(diff) { | ||
| if (!diff.trim()) return diff; | ||
| const lines = diff.split("\n"); | ||
| const result = []; | ||
| let oldLine = 0; | ||
| let newLine = 0; | ||
| for (const line of lines) if (line.startsWith("@@")) { | ||
| const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/); | ||
| if (match) { | ||
| oldLine = Number.parseInt(match[1], 10); | ||
| newLine = Number.parseInt(match[2], 10); | ||
| } | ||
| result.push(line); | ||
| } else if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("diff ") || line.startsWith("index ")) result.push(line); | ||
| else if (line.startsWith("-")) { | ||
| result.push(`${oldLine}:${line}`); | ||
| oldLine++; | ||
| } else if (line.startsWith("+")) { | ||
| result.push(`${newLine}:${line}`); | ||
| newLine++; | ||
| } else { | ||
| result.push(`${newLine}:${line}`); | ||
| oldLine++; | ||
| newLine++; | ||
| } | ||
| return result.join("\n"); | ||
| } | ||
| const MESSAGES = { noGit: "No .git folder found. Run this inside a git repository." }; | ||
| //#endregion | ||
| //#region src/utils/config.ts | ||
| const MAX_CUSTOM_PROMPT_LENGTH = 500; | ||
| const CONFIG_DIR = join(homedir(), ".commitguard"); | ||
| const PROJECTS_CONFIG_PATH = join(CONFIG_DIR, "projects.json"); | ||
| let projectsConfigCache = null; | ||
| const GIT_DIR$1 = ".git"; | ||
| function ensureConfigDir() { | ||
| if (!existsSync(CONFIG_DIR)) try { | ||
| mkdirSync(CONFIG_DIR, { recursive: true }); | ||
| } catch (e) { | ||
| consola.error(`Failed to create config directory at ${CONFIG_DIR}: ${e.message}`); | ||
| } | ||
| } | ||
| function getDefaultConfig() { | ||
| return { | ||
| context: "normal", | ||
| checks: { | ||
| security: true, | ||
| performance: true, | ||
| codeQuality: true, | ||
| architecture: true | ||
| }, | ||
| severityLevels: { | ||
| critical: true, | ||
| warning: true, | ||
| suggestion: false | ||
| }, | ||
| customRule: "" | ||
| }; | ||
| } | ||
| let projectIdCache = null; | ||
| function getProjectId() { | ||
| if (projectIdCache) return projectIdCache; | ||
| try { | ||
| projectIdCache = execFileSync("git", [ | ||
| "rev-list", | ||
| "--max-parents=0", | ||
| "HEAD" | ||
| ], { | ||
| encoding: "utf8", | ||
| stdio: [ | ||
| "pipe", | ||
| "pipe", | ||
| "ignore" | ||
| ] | ||
| }).trim().split("\n")[0]; | ||
| return projectIdCache; | ||
| } catch { | ||
| consola.error("Warning: Unable to determine project ID. Using current working directory as fallback project ID."); | ||
| projectIdCache = process.cwd(); | ||
| return projectIdCache; | ||
| } | ||
| } | ||
| function loadProjectsConfig() { | ||
| if (projectsConfigCache) return projectsConfigCache; | ||
| if (existsSync(PROJECTS_CONFIG_PATH)) try { | ||
| const content = readFileSync(PROJECTS_CONFIG_PATH, "utf8"); | ||
| projectsConfigCache = JSON.parse(content); | ||
| return projectsConfigCache; | ||
| } catch { | ||
| consola.warn("Failed to parse projects config"); | ||
| } | ||
| projectsConfigCache = {}; | ||
| return projectsConfigCache; | ||
| } | ||
| function saveProjectsConfig(projects) { | ||
| try { | ||
| ensureConfigDir(); | ||
| writeFileSync(PROJECTS_CONFIG_PATH, JSON.stringify(projects, null, 2)); | ||
| projectsConfigCache = projects; | ||
| } catch (e) { | ||
| consola.error(`Failed to save projects config: ${e.message}`); | ||
| } | ||
| } | ||
| function loadConfig() { | ||
| const projectId = getProjectId(); | ||
| return loadProjectsConfig()[projectId] || getDefaultConfig(); | ||
| } | ||
| async function manageConfig() { | ||
| if (!existsSync(GIT_DIR$1)) { | ||
| cancel(MESSAGES.noGit); | ||
| return; | ||
| } | ||
| const projectId = getProjectId(); | ||
| const currentConfig = loadConfig(); | ||
| intro(`CommitGuard Configuration`); | ||
| const enabledChecks = await multiselect({ | ||
| message: "Select enabled checks for this project:", | ||
| options: [ | ||
| { | ||
| value: "security", | ||
| label: "Security" | ||
| }, | ||
| { | ||
| value: "performance", | ||
| label: "Performance" | ||
| }, | ||
| { | ||
| value: "codeQuality", | ||
| label: "Code Quality" | ||
| }, | ||
| { | ||
| value: "architecture", | ||
| label: "Architecture" | ||
| } | ||
| ], | ||
| initialValues: Object.entries(currentConfig.checks).filter(([_, enabled]) => enabled).map(([key]) => key) | ||
| }); | ||
| if (isCancel(enabledChecks)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| const enabledSeverity = await multiselect({ | ||
| message: "Select severity levels for enabled checks:", | ||
| options: [ | ||
| { | ||
| value: "suggestion", | ||
| label: "Suggestion" | ||
| }, | ||
| { | ||
| value: "warning", | ||
| label: "Warning" | ||
| }, | ||
| { | ||
| value: "critical", | ||
| label: "Critical" | ||
| } | ||
| ], | ||
| initialValues: Object.entries(currentConfig.severityLevels).filter(([_, enabled]) => enabled).map(([key]) => key) | ||
| }); | ||
| if (isCancel(enabledSeverity)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| const contextLevel = await select({ | ||
| message: "Select context level for analysis:", | ||
| options: [{ | ||
| value: "minimal", | ||
| label: "Minimal (Just Actual Changes)" | ||
| }, { | ||
| value: "normal", | ||
| label: "Normal (Actual Changes + Context Lines)" | ||
| }], | ||
| initialValue: currentConfig.context | ||
| }); | ||
| if (isCancel(contextLevel)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| let customRule = currentConfig.customRule; | ||
| if (currentConfig.customRule) { | ||
| log.info(`Current custom rule: ${currentConfig.customRule}`); | ||
| const editCustomRule = await confirm({ | ||
| message: "Would you like to edit the custom rule? (Currently only available to pro users)", | ||
| initialValue: false | ||
| }); | ||
| if (isCancel(editCustomRule)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| if (editCustomRule) { | ||
| const newCustomRule = await text({ | ||
| message: "Enter new custom rule (leave empty to remove):", | ||
| initialValue: currentConfig.customRule, | ||
| validate: (value) => { | ||
| const val = String(value).trim(); | ||
| if (!val) return void 0; | ||
| if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`; | ||
| } | ||
| }); | ||
| if (isCancel(newCustomRule)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| customRule = String(newCustomRule).trim(); | ||
| } | ||
| } else { | ||
| const addCustomRule = await confirm({ | ||
| message: "Would you like to add a custom rule for this project? (Currently only available to pro users)", | ||
| initialValue: false | ||
| }); | ||
| if (isCancel(addCustomRule)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| if (addCustomRule) { | ||
| const newCustomRule = await text({ | ||
| message: "Enter custom rule (leave empty to skip):", | ||
| placeholder: "e.g., Check for proper error handling in async functions", | ||
| validate: (value) => { | ||
| const val = String(value).trim(); | ||
| if (!val) return void 0; | ||
| if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`; | ||
| } | ||
| }); | ||
| if (isCancel(newCustomRule)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| customRule = String(newCustomRule).trim(); | ||
| } | ||
| } | ||
| const newConfig = { | ||
| context: contextLevel, | ||
| checks: { | ||
| security: enabledChecks.includes("security"), | ||
| performance: enabledChecks.includes("performance"), | ||
| codeQuality: enabledChecks.includes("codeQuality"), | ||
| architecture: enabledChecks.includes("architecture") | ||
| }, | ||
| severityLevels: { | ||
| suggestion: enabledSeverity.includes("suggestion"), | ||
| warning: enabledSeverity.includes("warning"), | ||
| critical: enabledSeverity.includes("critical") | ||
| }, | ||
| customRule | ||
| }; | ||
| if (JSON.stringify(newConfig) === JSON.stringify(currentConfig)) { | ||
| outro("No changes made to the configuration."); | ||
| return; | ||
| } | ||
| const confirmUpdate = await confirm({ message: "Save this configuration?" }); | ||
| if (isCancel(confirmUpdate)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| if (!confirmUpdate) { | ||
| outro("Configuration not saved."); | ||
| return; | ||
| } | ||
| const projects = loadProjectsConfig(); | ||
| projects[projectId] = newConfig; | ||
| saveProjectsConfig(projects); | ||
| outro("✓ Configuration updated for this project!"); | ||
| } | ||
| //#endregion | ||
| //#region src/data/ignore.json | ||
@@ -472,5 +201,38 @@ var ignore = [ | ||
| //#endregion | ||
| //#region src/utils/global.ts | ||
| function createDiffHash(diff) { | ||
| return createHash("md5").update(diff).digest("base64url"); | ||
| } | ||
| function addGitLineNumbers(diff) { | ||
| if (!diff.trim()) return diff; | ||
| const lines = diff.split("\n"); | ||
| const result = []; | ||
| let oldLine = 0; | ||
| let newLine = 0; | ||
| for (const line of lines) if (line.startsWith("@@")) { | ||
| const match = line.match(/@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/); | ||
| if (match) { | ||
| oldLine = Number.parseInt(match[1], 10); | ||
| newLine = Number.parseInt(match[2], 10); | ||
| } | ||
| result.push(line); | ||
| } else if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("diff ") || line.startsWith("index ")) result.push(line); | ||
| else if (line.startsWith("-")) { | ||
| result.push(`${oldLine}:${line}`); | ||
| oldLine++; | ||
| } else if (line.startsWith("+")) { | ||
| result.push(`${newLine}:${line}`); | ||
| newLine++; | ||
| } else { | ||
| result.push(`${newLine}:${line}`); | ||
| oldLine++; | ||
| newLine++; | ||
| } | ||
| return result.join("\n"); | ||
| } | ||
| const MESSAGES = { noGit: "No .git folder found. Run this inside a git repository." }; | ||
| //#endregion | ||
| //#region src/utils/git.ts | ||
| function getStagedDiff(context) { | ||
| const gitContextCommand = context === "minimal" ? [] : ["--function-context"]; | ||
| function getStagedDiff() { | ||
| try { | ||
@@ -481,3 +243,3 @@ return addGitLineNumbers(execFileSync("git", [ | ||
| "--no-color", | ||
| ...gitContextCommand, | ||
| "--function-context", | ||
| "--diff-algorithm=histogram", | ||
@@ -501,4 +263,3 @@ "--diff-filter=AMC", | ||
| } | ||
| function getLastDiff(context) { | ||
| const gitContextCommand = context === "minimal" ? [] : ["--function-context"]; | ||
| function getLastDiff() { | ||
| try { | ||
@@ -510,3 +271,3 @@ return execFileSync("git", [ | ||
| "--no-color", | ||
| ...gitContextCommand, | ||
| "--function-context", | ||
| "--diff-algorithm=histogram", | ||
@@ -640,3 +401,3 @@ "--diff-filter=AMC", | ||
| const apiUrl = process.env.COMMITGUARD_API_BYPASS_URL || "https://api.commitguard.ai/v1/bypass"; | ||
| const diff = getLastDiff(loadConfig().context); | ||
| const diff = getLastDiff(); | ||
| const controller = new AbortController(); | ||
@@ -669,2 +430,222 @@ const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT); | ||
| //#endregion | ||
| //#region src/utils/config.ts | ||
| const MAX_CUSTOM_PROMPT_LENGTH = 500; | ||
| const CONFIG_DIR = join(homedir(), ".commitguard"); | ||
| const PROJECTS_CONFIG_PATH = join(CONFIG_DIR, "projects.json"); | ||
| let projectsConfigCache = null; | ||
| const GIT_DIR$1 = ".git"; | ||
| function ensureConfigDir() { | ||
| if (!existsSync(CONFIG_DIR)) try { | ||
| mkdirSync(CONFIG_DIR, { recursive: true }); | ||
| } catch (e) { | ||
| consola.error(`Failed to create config directory at ${CONFIG_DIR}: ${e.message}`); | ||
| } | ||
| } | ||
| function getDefaultConfig() { | ||
| return { | ||
| checks: { | ||
| security: true, | ||
| performance: true, | ||
| codeQuality: true, | ||
| architecture: true | ||
| }, | ||
| severityLevels: { | ||
| critical: true, | ||
| warning: true, | ||
| suggestion: false | ||
| }, | ||
| customRule: "" | ||
| }; | ||
| } | ||
| let projectIdCache = null; | ||
| function getProjectId() { | ||
| if (projectIdCache) return projectIdCache; | ||
| try { | ||
| projectIdCache = execFileSync("git", [ | ||
| "rev-list", | ||
| "--max-parents=0", | ||
| "HEAD" | ||
| ], { | ||
| encoding: "utf8", | ||
| stdio: [ | ||
| "pipe", | ||
| "pipe", | ||
| "ignore" | ||
| ] | ||
| }).trim().split("\n")[0]; | ||
| return projectIdCache; | ||
| } catch { | ||
| consola.error("Warning: Unable to determine project ID. Using current working directory as fallback project ID."); | ||
| projectIdCache = process.cwd(); | ||
| return projectIdCache; | ||
| } | ||
| } | ||
| function loadProjectsConfig() { | ||
| if (projectsConfigCache) return projectsConfigCache; | ||
| if (existsSync(PROJECTS_CONFIG_PATH)) try { | ||
| const content = readFileSync(PROJECTS_CONFIG_PATH, "utf8"); | ||
| projectsConfigCache = JSON.parse(content); | ||
| return projectsConfigCache; | ||
| } catch { | ||
| consola.warn("Failed to parse projects config"); | ||
| } | ||
| projectsConfigCache = {}; | ||
| return projectsConfigCache; | ||
| } | ||
| function saveProjectsConfig(projects) { | ||
| try { | ||
| ensureConfigDir(); | ||
| writeFileSync(PROJECTS_CONFIG_PATH, JSON.stringify(projects, null, 2)); | ||
| projectsConfigCache = projects; | ||
| } catch (e) { | ||
| consola.error(`Failed to save projects config: ${e.message}`); | ||
| } | ||
| } | ||
| function loadConfig() { | ||
| const projectId = getProjectId(); | ||
| return loadProjectsConfig()[projectId] || getDefaultConfig(); | ||
| } | ||
| async function manageConfig() { | ||
| if (!existsSync(GIT_DIR$1)) { | ||
| cancel(MESSAGES.noGit); | ||
| return; | ||
| } | ||
| const projectId = getProjectId(); | ||
| const currentConfig = loadConfig(); | ||
| intro(`CommitGuard Configuration`); | ||
| const enabledChecks = await multiselect({ | ||
| message: "Select enabled checks for this project:", | ||
| options: [ | ||
| { | ||
| value: "security", | ||
| label: "Security" | ||
| }, | ||
| { | ||
| value: "performance", | ||
| label: "Performance" | ||
| }, | ||
| { | ||
| value: "codeQuality", | ||
| label: "Code Quality" | ||
| }, | ||
| { | ||
| value: "architecture", | ||
| label: "Architecture" | ||
| } | ||
| ], | ||
| initialValues: Object.entries(currentConfig.checks).filter(([_, enabled]) => enabled).map(([key]) => key) | ||
| }); | ||
| if (isCancel(enabledChecks)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| const enabledSeverity = await multiselect({ | ||
| message: "Select severity levels for enabled checks:", | ||
| options: [ | ||
| { | ||
| value: "suggestion", | ||
| label: "Suggestion" | ||
| }, | ||
| { | ||
| value: "warning", | ||
| label: "Warning" | ||
| }, | ||
| { | ||
| value: "critical", | ||
| label: "Critical" | ||
| } | ||
| ], | ||
| initialValues: Object.entries(currentConfig.severityLevels).filter(([_, enabled]) => enabled).map(([key]) => key) | ||
| }); | ||
| if (isCancel(enabledSeverity)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| let customRule = currentConfig.customRule; | ||
| if (currentConfig.customRule) { | ||
| log.info(`Current custom rule: ${currentConfig.customRule}`); | ||
| const editCustomRule = await confirm({ | ||
| message: "Would you like to edit the custom rule? (Currently only available to pro users)", | ||
| initialValue: false | ||
| }); | ||
| if (isCancel(editCustomRule)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| if (editCustomRule) { | ||
| const newCustomRule = await text({ | ||
| message: "Enter new custom rule (leave empty to remove):", | ||
| initialValue: currentConfig.customRule, | ||
| validate: (value) => { | ||
| const val = String(value).trim(); | ||
| if (!val) return void 0; | ||
| if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`; | ||
| } | ||
| }); | ||
| if (isCancel(newCustomRule)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| customRule = String(newCustomRule).trim(); | ||
| } | ||
| } else { | ||
| const addCustomRule = await confirm({ | ||
| message: "Would you like to add a custom rule for this project? (Currently only available to pro users)", | ||
| initialValue: false | ||
| }); | ||
| if (isCancel(addCustomRule)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| if (addCustomRule) { | ||
| const newCustomRule = await text({ | ||
| message: "Enter custom rule (leave empty to skip):", | ||
| placeholder: "e.g., Check for proper error handling in async functions", | ||
| validate: (value) => { | ||
| const val = String(value).trim(); | ||
| if (!val) return void 0; | ||
| if (val.length > MAX_CUSTOM_PROMPT_LENGTH) return `Custom rule must be ${MAX_CUSTOM_PROMPT_LENGTH} characters or less (current: ${val.length})`; | ||
| } | ||
| }); | ||
| if (isCancel(newCustomRule)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| customRule = String(newCustomRule).trim(); | ||
| } | ||
| } | ||
| const newConfig = { | ||
| checks: { | ||
| security: enabledChecks.includes("security"), | ||
| performance: enabledChecks.includes("performance"), | ||
| codeQuality: enabledChecks.includes("codeQuality"), | ||
| architecture: enabledChecks.includes("architecture") | ||
| }, | ||
| severityLevels: { | ||
| suggestion: enabledSeverity.includes("suggestion"), | ||
| warning: enabledSeverity.includes("warning"), | ||
| critical: enabledSeverity.includes("critical") | ||
| }, | ||
| customRule | ||
| }; | ||
| if (JSON.stringify(newConfig) === JSON.stringify(currentConfig)) { | ||
| outro("No changes made to the configuration."); | ||
| return; | ||
| } | ||
| const confirmUpdate = await confirm({ message: "Save this configuration?" }); | ||
| if (isCancel(confirmUpdate)) { | ||
| cancel("Configuration cancelled"); | ||
| return; | ||
| } | ||
| if (!confirmUpdate) { | ||
| outro("Configuration not saved."); | ||
| return; | ||
| } | ||
| const projects = loadProjectsConfig(); | ||
| projects[projectId] = newConfig; | ||
| saveProjectsConfig(projects); | ||
| outro("✓ Configuration updated for this project!"); | ||
| } | ||
| //#endregion | ||
| //#region src/utils/eslint.ts | ||
@@ -956,3 +937,3 @@ const cacheDir = join(homedir(), ".cache", "commitguard"); | ||
| async function onStaged() { | ||
| const diff = getStagedDiff(config.context); | ||
| const diff = getStagedDiff(); | ||
| if (!diff.trim()) { | ||
@@ -978,3 +959,3 @@ clearCache(); | ||
| function getCachedAnalysis(diff, diffHash) { | ||
| const effectiveDiff = diff ?? getStagedDiff(config.context); | ||
| const effectiveDiff = diff ?? getStagedDiff(); | ||
| if (!effectiveDiff.trim()) return { | ||
@@ -998,3 +979,3 @@ analysis: { | ||
| async function validateCommit() { | ||
| const diff = getStagedDiff(config.context); | ||
| const diff = getStagedDiff(); | ||
| const diffHash = diff.trim() ? createDiffHash(diff) : ""; | ||
@@ -1001,0 +982,0 @@ const cached = getCachedAnalysis(diff, diffHash); |
+1
-1
| { | ||
| "name": "@commitguard/cli", | ||
| "type": "module", | ||
| "version": "0.0.14", | ||
| "version": "0.0.15", | ||
| "description": "AI-powered git commit checker that blocks bad code before it ships", | ||
@@ -6,0 +6,0 @@ "license": "MIT", |
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 3 instances in 1 package
36087
-1.8%1061
-1.76%