Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 178 additions & 30 deletions src/cli/commands/init.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { Command } from 'commander';
import chalk from 'chalk';
import type { Severity } from '../../types/common.js';

const DEFAULT_CONFIG = `# g0 Configuration
# See: https://github.com/guard0-ai/g0
Expand Down Expand Up @@ -30,13 +30,51 @@ min_score: 70
# rules_dir: ./rules
`;

type HookType = 'pre-commit' | 'pre-push';
type HookManager = 'husky' | 'lefthook' | 'standalone';

interface InitOptions {
force?: boolean;
hook?: boolean;
hooks?: boolean;
hookType?: string;
hookManager?: string;
minSeverity?: string;
}

const VALID_HOOK_TYPES: readonly string[] = ['pre-commit', 'pre-push'] as const;
const VALID_HOOK_MANAGERS: readonly string[] = ['husky', 'lefthook', 'standalone'] as const;
const VALID_SEVERITIES: readonly string[] = ['critical', 'high', 'medium', 'low', 'info'] as const;

export const initCommand = new Command('init')
.description('Initialize g0 configuration file')
.option('-f, --force', 'Overwrite existing config')
.option('--hooks', 'Install git pre-commit hook')
.action((options: { force?: boolean; hooks?: boolean }) => {
if (options.hooks) {
installPreCommitHook();
.option('--hook', 'Install git hook for g0 scan')
.option('--hooks', 'Install git hook (alias for --hook)')
.option('--hook-type <type>', 'Hook type: pre-commit or pre-push (default: pre-commit)')
.option('--hook-manager <manager>', 'Hook manager: husky, lefthook, or standalone (default: auto-detect)')
.option('--min-severity <severity>', 'Minimum severity to block on (default: high)')
.action((options: InitOptions) => {
if (options.hook || options.hooks) {
const hookType = (options.hookType ?? 'pre-commit') as HookType;
if (!VALID_HOOK_TYPES.includes(hookType)) {
console.error(chalk.red(`Invalid hook type: ${hookType}. Must be one of: ${VALID_HOOK_TYPES.join(', ')}`));
process.exit(1);
}

const minSeverity = (options.minSeverity ?? 'high') as Severity;
if (!VALID_SEVERITIES.includes(minSeverity)) {
console.error(chalk.red(`Invalid severity: ${minSeverity}. Must be one of: ${VALID_SEVERITIES.join(', ')}`));
process.exit(1);
}

const manager = (options.hookManager ?? detectHookManager()) as HookManager;
if (!VALID_HOOK_MANAGERS.includes(manager)) {
console.error(chalk.red(`Invalid hook manager: ${manager}. Must be one of: ${VALID_HOOK_MANAGERS.join(', ')}`));
process.exit(1);
}

installHook({ hookType, manager, minSeverity, force: options.force });
return;
}

Expand All @@ -53,7 +91,114 @@ export const initCommand = new Command('init')
console.log(chalk.dim('Run `g0 scan` to scan your project'));
});

function installPreCommitHook(): void {
/**
* Detect which hook manager is in use in the current project.
*/
export function detectHookManager(): HookManager {
const cwd = process.cwd();

// Check for Husky directory
if (fs.existsSync(path.join(cwd, '.husky'))) {
return 'husky';
}

// Check for lefthook config
if (
fs.existsSync(path.join(cwd, 'lefthook.yml')) ||
fs.existsSync(path.join(cwd, 'lefthook.yaml'))
) {
return 'lefthook';
}

// Check package.json for husky/lefthook deps
const pkgPath = path.join(cwd, 'package.json');
if (fs.existsSync(pkgPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as Record<string, Record<string, unknown>>;
const allDeps: Record<string, unknown> = {
...(pkg.dependencies as Record<string, unknown> | undefined),
...(pkg.devDependencies as Record<string, unknown> | undefined),
};
if (allDeps.husky) return 'husky';
if (allDeps.lefthook) return 'lefthook';
} catch {
// ignore parse errors
}
}

return 'standalone';
}

function generateHookScript(hookType: HookType, minSeverity: Severity): string {
const scanCmd = `g0 scan --ci --severity ${minSeverity}`;
return `#!/bin/sh
# g0 ${hookType} hook — auto-generated by \`g0 init --hook\`
# To bypass: git commit --no-verify (use with caution!)
set -e

echo "[g0] Running security scan (${hookType})..."
${scanCmd}
echo "[g0] Security scan passed."
`;
}

interface InstallHookOptions {
hookType: HookType;
manager: HookManager;
minSeverity: Severity;
force?: boolean;
}

function installHookHusky(hookType: HookType, minSeverity: Severity, force?: boolean): void {
const huskyDir = path.join(process.cwd(), '.husky');
if (!fs.existsSync(huskyDir)) {
fs.mkdirSync(huskyDir, { recursive: true });
}

const hookPath = path.join(huskyDir, hookType);
if (fs.existsSync(hookPath) && !force) {
console.log(chalk.yellow(`⚠ Husky hook already exists: .husky/${hookType}`));
console.log(chalk.dim('Use --force to overwrite'));
return;
}

const script = generateHookScript(hookType, minSeverity);
fs.writeFileSync(hookPath, script, { mode: 0o755 });
console.log(chalk.green(`✓ Created .husky/${hookType}`));
console.log(chalk.dim(` Runs: g0 scan --ci --severity ${minSeverity}`));
console.log(chalk.dim(' Bypass with: git commit --no-verify'));
}

function installHookLefthook(hookType: HookType, minSeverity: Severity, force?: boolean): void {
const configName = fs.existsSync(path.join(process.cwd(), 'lefthook.yaml'))
? 'lefthook.yaml'
: 'lefthook.yml';
const configPath = path.join(process.cwd(), configName);

let existing = '';
if (fs.existsSync(configPath)) {
existing = fs.readFileSync(configPath, 'utf-8');
if (existing.includes('g0 scan') && !force) {
console.log(chalk.yellow('⚠ lefthook config already contains g0 scan'));
console.log(chalk.dim('Use --force to overwrite'));
return;
}
}

const block = `\n${hookType}:\n commands:\n g0-scan:\n run: g0 scan --ci --severity ${minSeverity}\n`;

if (existing && !force) {
fs.appendFileSync(configPath, block, 'utf-8');
} else {
const content = existing ? existing + block : block.trimStart();
fs.writeFileSync(configPath, content, 'utf-8');
}

console.log(chalk.green(`✓ Added g0 scan to ${configName} (${hookType})`));
console.log(chalk.dim(` Runs: g0 scan --ci --severity ${minSeverity}`));
}

function installHookStandalone(hookType: HookType, minSeverity: Severity, force?: boolean): void {
const gitDir = path.join(process.cwd(), '.git');
if (!fs.existsSync(gitDir)) {
console.error(chalk.red('Not a git repository. Run `git init` first.'));
Expand All @@ -65,32 +210,35 @@ function installPreCommitHook(): void {
fs.mkdirSync(hooksDir, { recursive: true });
}

const hookPath = path.join(hooksDir, 'pre-commit');
if (fs.existsSync(hookPath)) {
console.log(chalk.yellow('Pre-commit hook already exists.'));
console.log(chalk.dim(` ${hookPath}`));
console.log(chalk.dim('Remove it manually and re-run to replace.'));
const hookPath = path.join(hooksDir, hookType);
if (fs.existsSync(hookPath) && !force) {
console.log(chalk.yellow(`⚠ Hook already exists: .git/hooks/${hookType}`));
console.log(chalk.dim('Use --force to overwrite, or remove it manually.'));
return;
}

// Read the template from the package
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const templatePath = path.join(__dirname, '..', '..', 'templates', 'pre-commit-hook.sh');
let hookContent: string;
if (fs.existsSync(templatePath)) {
hookContent = fs.readFileSync(templatePath, 'utf-8');
} else {
// Fallback inline template
hookContent = `#!/bin/sh
set -e
echo "[g0] Running security scan..."
g0 gate . --min-score 70 --no-critical
echo "[g0] Security scan passed."
`;
}

fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
console.log(chalk.green('Installed pre-commit hook'));
const script = generateHookScript(hookType, minSeverity);
fs.writeFileSync(hookPath, script, { mode: 0o755 });
console.log(chalk.green(`✓ Installed ${hookType} hook`));
console.log(chalk.dim(` ${hookPath}`));
console.log(chalk.dim(` Runs: g0 scan --ci --severity ${minSeverity}`));
console.log(chalk.dim(' Bypass with: git commit --no-verify'));
}

function installHook(opts: InstallHookOptions): void {
const { hookType, manager, minSeverity, force } = opts;

console.log(chalk.dim(`Hook manager: ${manager}`));

switch (manager) {
case 'husky':
installHookHusky(hookType, minSeverity, force);
break;
case 'lefthook':
installHookLefthook(hookType, minSeverity, force);
break;
case 'standalone':
installHookStandalone(hookType, minSeverity, force);
break;
}
}
Loading