ralph-gate 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/config.ts","../src/init.ts","../src/runner.ts","../src/output.ts","../src/hook.ts"],"sourcesContent":["#!/usr/bin/env node\nimport path from 'node:path';\nimport type { Gate, GateRunSummary } from './types.js';\nimport { loadConfig } from './config.js';\nimport { initConfigFile } from './init.js';\nimport { runGates, type RunGatesOptions } from './runner.js';\nimport { formatConsoleOutput, writeResultsFile } from './output.js';\nimport { generateHookResponse, outputHookResponse } from './hook.js';\n\ninterface CliOptions {\n hook: boolean;\n dryRun: boolean;\n only?: string;\n verbose: boolean;\n}\n\ninterface InitCliOptions {\n force: boolean;\n print: boolean;\n}\n\nfunction parseArgs(args: string[]): { options: CliOptions; error?: string } {\n const options: CliOptions = {\n hook: false,\n dryRun: false,\n verbose: false,\n };\n\n for (let i = 0; i < args.length; i += 1) {\n const arg = args[i];\n switch (arg) {\n case '--hook':\n options.hook = true;\n break;\n case '--dry-run':\n options.dryRun = true;\n break;\n case '--verbose':\n options.verbose = true;\n break;\n case '--only': {\n const value = args[i + 1];\n if (!value) {\n return { options, error: 'Missing value for --only.' };\n }\n options.only = value;\n i += 1;\n break;\n }\n default:\n return { options, error: `Unknown argument: ${arg}` };\n }\n }\n\n return { options };\n}\n\nfunction parseInitArgs(args: string[]): {\n options: InitCliOptions;\n error?: string;\n} {\n const options: InitCliOptions = {\n force: false,\n print: false,\n };\n\n for (let i = 0; i < args.length; i += 1) {\n const arg = args[i];\n switch (arg) {\n case '--force':\n options.force = true;\n break;\n case '--print':\n options.print = true;\n break;\n default:\n return { options, error: `Unknown argument: ${arg}` };\n }\n }\n\n return { options };\n}\n\nfunction defaultOutputPath(): string {\n return `gate-results-${process.pid}.json`;\n}\n\nfunction createEmptySummary(passed: boolean): GateRunSummary {\n return {\n passed,\n timestamp: new Date().toISOString(),\n totalDurationMs: 0,\n results: [],\n firstFailure: null,\n warnings: [],\n };\n}\n\nfunction formatDryRun(gates: Gate[], shell: string): string {\n const lines = [`SHELL: ${shell}`];\n if (gates.length === 0) {\n lines.push('No gates to run.');\n return lines.join('\\n');\n }\n for (const gate of gates) {\n const order = typeof gate.order === 'number' ? gate.order : 100;\n lines.push(`- ${gate.name} (order ${order}): ${gate.command}`);\n }\n return lines.join('\\n');\n}\n\nfunction createHookProgressReporter(\n enabled: boolean,\n): Partial<RunGatesOptions> {\n if (!enabled) {\n return {};\n }\n\n const prefix = '[ralph-gate]';\n const writeLine = (line: string) => {\n process.stderr.write(`${prefix} ${line}\\n`);\n };\n\n return {\n onGateStart: (gate) => {\n writeLine(`running ${gate.name}: ${gate.command}`);\n },\n onGateOutput: (_gate, _stream, text) => {\n process.stderr.write(text);\n },\n onGateComplete: (result) => {\n if (result.skipped) {\n writeLine(`${result.name} skipped`);\n return;\n }\n const status = result.passed\n ? `passed in ${result.durationMs}ms`\n : `failed (exit ${result.exitCode ?? 'null'}) in ${result.durationMs}ms`;\n writeLine(`${result.name} ${status}`);\n },\n };\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2);\n\n if (argv[0] === 'init') {\n const { options, error } = parseInitArgs(argv.slice(1));\n if (error) {\n console.error(error);\n process.exitCode = 1;\n return;\n }\n\n const result = await initConfigFile({\n force: options.force,\n print: options.print,\n });\n if (result.error) {\n console.error(result.error);\n process.exitCode = 1;\n return;\n }\n\n for (const warning of result.warnings) {\n console.error(`Warning: ${warning}`);\n }\n\n if (options.print) {\n console.log(JSON.stringify(result.config, null, 2));\n return;\n }\n\n console.log(\n `Created ${path.basename(result.configPath)} with ${result.config.gates.length} gate(s).`,\n );\n if (result.gitignoreUpdated) {\n console.log('Updated .gitignore to exclude gate-results-*.json files.');\n }\n return;\n }\n\n const { options, error } = parseArgs(argv);\n if (error) {\n console.error(error);\n process.exitCode = 1;\n return;\n }\n\n const configResult = await loadConfig();\n if (configResult.error) {\n const summary = createEmptySummary(false);\n const outputPath = defaultOutputPath();\n await writeResultsFile(summary, outputPath);\n\n if (options.hook) {\n outputHookResponse({ decision: 'block', reason: configResult.error });\n process.exitCode = 0;\n return;\n }\n\n console.error(configResult.error);\n process.exitCode = 1;\n return;\n }\n\n if (!configResult.config) {\n if (options.dryRun) {\n const shellLabel = process.env.SHELL ?? '(default)';\n console.log(formatDryRun([], shellLabel));\n }\n if (options.hook) {\n outputHookResponse({});\n process.exitCode = 0;\n }\n return;\n }\n\n const config = configResult.config;\n const shellLabel = process.env.SHELL ?? '(default)';\n\n if (options.dryRun) {\n console.log(formatDryRun(config.gates, shellLabel));\n return;\n }\n\n let gates = config.gates;\n if (options.only) {\n const match = gates.find((gate) => gate.name === options.only);\n if (!match) {\n console.error(`Gate not found: ${options.only}`);\n process.exitCode = 1;\n return;\n }\n gates = [match];\n }\n\n const outputPath = config.outputPath ?? defaultOutputPath();\n\n if (gates.length === 0) {\n const summary = createEmptySummary(true);\n await writeResultsFile(summary, outputPath);\n if (options.hook) {\n outputHookResponse({});\n process.exitCode = 0;\n return;\n }\n console.log(formatConsoleOutput(summary));\n return;\n }\n\n const hookProgress = createHookProgressReporter(options.hook);\n const summary = await runGates(gates, {\n failFast: config.failFast,\n verbose: options.verbose && !options.hook,\n ...hookProgress,\n });\n\n await writeResultsFile(summary, outputPath);\n\n if (options.hook) {\n outputHookResponse(generateHookResponse(summary));\n process.exitCode = 0;\n return;\n }\n\n console.log(formatConsoleOutput(summary));\n if (!summary.passed) {\n process.exitCode = 1;\n }\n}\n\nmain().catch((error) => {\n console.error(error instanceof Error ? error.message : String(error));\n process.exitCode = 1;\n});\n","import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport type { Gate, GateConfig } from './types.js';\n\nconst CONFIG_FILES = ['gate.config.json', '.gaterc.json', '.gaterc'];\n\nexport interface LoadConfigResult {\n config: GateConfig | null;\n configPath?: string;\n error?: string;\n}\n\nfunction normalizeGate(gate: Gate): Gate {\n const order = typeof gate.order === 'number' ? gate.order : 100;\n const enabled = typeof gate.enabled === 'boolean' ? gate.enabled : true;\n const blocking = typeof gate.blocking === 'boolean' ? gate.blocking : true;\n\n return {\n ...gate,\n order,\n enabled,\n blocking,\n description:\n typeof gate.description === 'string' ? gate.description : undefined,\n name: gate.name,\n command: gate.command,\n } as Gate;\n}\n\nfunction validateGate(gate: unknown): string | null {\n if (!gate || typeof gate !== 'object') {\n return 'Gate entries must be objects.';\n }\n const maybeGate = gate as Gate;\n if (typeof maybeGate.name !== 'string' || maybeGate.name.trim() === '') {\n return 'Gate is missing required field: name.';\n }\n if (\n typeof maybeGate.command !== 'string' ||\n maybeGate.command.trim() === ''\n ) {\n return `Gate '${maybeGate.name}' is missing required field: command.`;\n }\n return null;\n}\n\nfunction sortGates(gates: Gate[]): Gate[] {\n const withIndex = gates.map((gate, index) => ({ gate, index }));\n withIndex.sort((a, b) => {\n const orderA = typeof a.gate.order === 'number' ? a.gate.order : 100;\n const orderB = typeof b.gate.order === 'number' ? b.gate.order : 100;\n if (orderA !== orderB) {\n return orderA - orderB;\n }\n return a.index - b.index;\n });\n return withIndex.map((entry) => entry.gate);\n}\n\nexport async function loadConfig(\n cwd: string = process.cwd(),\n): Promise<LoadConfigResult> {\n for (const filename of CONFIG_FILES) {\n const filePath = path.join(cwd, filename);\n try {\n await fs.access(filePath);\n } catch {\n continue;\n }\n\n let raw: string;\n try {\n raw = await fs.readFile(filePath, 'utf8');\n } catch (error) {\n return {\n config: null,\n configPath: filePath,\n error: `Invalid config: unable to read ${filename}: ${(error as Error).message}`,\n };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n return {\n config: null,\n configPath: filePath,\n error: `Invalid config: malformed JSON in ${filename}: ${(error as Error).message}`,\n };\n }\n\n if (!parsed || typeof parsed !== 'object') {\n return {\n config: null,\n configPath: filePath,\n error: `Invalid config: expected object in ${filename}.`,\n };\n }\n\n const config = parsed as GateConfig;\n if (!Array.isArray(config.gates)) {\n return {\n config: null,\n configPath: filePath,\n error: `Invalid config: missing required 'gates' array in ${filename}.`,\n };\n }\n\n const normalized: Gate[] = [];\n for (const gate of config.gates) {\n const gateError = validateGate(gate as Gate);\n if (gateError) {\n return {\n config: null,\n configPath: filePath,\n error: `Invalid config: ${gateError}`,\n };\n }\n const normalizedGate = normalizeGate(gate as Gate);\n if (normalizedGate.enabled === false) {\n continue;\n }\n normalized.push(normalizedGate);\n }\n\n const failFast =\n typeof config.failFast === 'boolean' ? config.failFast : true;\n const outputPath =\n typeof config.outputPath === 'string' ? config.outputPath : undefined;\n\n const sorted = sortGates(normalized);\n\n return {\n config: {\n gates: sorted,\n failFast,\n outputPath,\n },\n configPath: filePath,\n };\n }\n\n return { config: null };\n}\n","import { promises as fs } from 'node:fs';\nimport { execSync } from 'node:child_process';\nimport path from 'node:path';\nimport type { Gate, GateConfig } from './types.js';\n\nexport const DEFAULT_CONFIG_FILENAME = 'gate.config.json';\n\ntype PackageManager = 'npm' | 'yarn' | 'pnpm';\n\ntype DetectedProject =\n | {\n kind: 'node';\n packageManager: PackageManager;\n packageJson: Record<string, unknown>;\n }\n | { kind: 'python'; requirements: Set<string> }\n | { kind: 'unknown' };\n\nexport interface InitOptions {\n cwd?: string;\n filename?: string;\n force?: boolean;\n print?: boolean;\n}\n\nexport interface InitResult {\n config: GateConfig;\n configPath: string;\n created: boolean;\n projectKind: DetectedProject['kind'];\n warnings: string[];\n error?: string;\n gitignoreUpdated?: boolean;\n}\n\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function detectPackageManager(cwd: string): Promise<PackageManager> {\n if (await fileExists(path.join(cwd, 'pnpm-lock.yaml'))) {\n return 'pnpm';\n }\n if (await fileExists(path.join(cwd, 'yarn.lock'))) {\n return 'yarn';\n }\n return 'npm';\n}\n\nfunction runScriptCommand(\n packageManager: PackageManager,\n scriptName: string,\n): string {\n switch (packageManager) {\n case 'yarn':\n return `yarn ${scriptName}`;\n case 'pnpm':\n return `pnpm run ${scriptName}`;\n default:\n return `npm run ${scriptName}`;\n }\n}\n\nasync function readJsonFile(filePath: string): Promise<unknown> {\n const raw = await fs.readFile(filePath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction extractRequirementName(line: string): string | null {\n const trimmed = line.trim();\n if (trimmed.length === 0 || trimmed.startsWith('#')) {\n return null;\n }\n const noEnvMarker = trimmed.split(';', 1)[0]?.trim() ?? '';\n if (noEnvMarker.length === 0) {\n return null;\n }\n const name = noEnvMarker.split(/[<>=\\[]/, 1)[0]?.trim();\n if (!name) {\n return null;\n }\n return name.toLowerCase();\n}\n\nasync function detectProject(cwd: string): Promise<DetectedProject> {\n const packageJsonPath = path.join(cwd, 'package.json');\n if (await fileExists(packageJsonPath)) {\n const pm = await detectPackageManager(cwd);\n const parsed = await readJsonFile(packageJsonPath);\n if (!parsed || typeof parsed !== 'object') {\n throw new Error('package.json is not a JSON object.');\n }\n return {\n kind: 'node',\n packageManager: pm,\n packageJson: parsed as Record<string, unknown>,\n };\n }\n\n const requirementsPath = path.join(cwd, 'requirements.txt');\n const pyprojectPath = path.join(cwd, 'pyproject.toml');\n\n if (\n (await fileExists(requirementsPath)) ||\n (await fileExists(pyprojectPath))\n ) {\n const requirements = new Set<string>();\n if (await fileExists(requirementsPath)) {\n const raw = await fs.readFile(requirementsPath, 'utf8');\n for (const line of raw.split(/\\r?\\n/)) {\n const name = extractRequirementName(line);\n if (name) {\n requirements.add(name);\n }\n }\n }\n return { kind: 'python', requirements };\n }\n\n return { kind: 'unknown' };\n}\n\nfunction getPackageJsonScripts(\n packageJson: Record<string, unknown>,\n): Record<string, string> {\n const scripts = packageJson.scripts;\n if (!scripts || typeof scripts !== 'object') {\n return {};\n }\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(\n scripts as Record<string, unknown>,\n )) {\n if (typeof value === 'string') {\n result[key] = value;\n }\n }\n return result;\n}\n\nfunction getPackageJsonDeps(packageJson: Record<string, unknown>): Set<string> {\n const deps = new Set<string>();\n const sections = [\n 'dependencies',\n 'devDependencies',\n 'peerDependencies',\n 'optionalDependencies',\n ] as const;\n for (const section of sections) {\n const values = packageJson[section];\n if (!values || typeof values !== 'object') {\n continue;\n }\n for (const name of Object.keys(values as Record<string, unknown>)) {\n deps.add(name);\n }\n }\n return deps;\n}\n\nasync function inferNodeGates(\n cwd: string,\n packageManager: PackageManager,\n packageJson: Record<string, unknown>,\n warnings: string[],\n): Promise<Gate[]> {\n const gates: Gate[] = [];\n const scripts = getPackageJsonScripts(packageJson);\n const deps = getPackageJsonDeps(packageJson);\n\n const addIfScript = (name: string, order: number) => {\n if (!scripts[name]) {\n return;\n }\n gates.push({\n name,\n order,\n command: runScriptCommand(packageManager, name),\n });\n };\n\n addIfScript('lint', 10);\n addIfScript('typecheck', 20);\n addIfScript('test', 30);\n addIfScript('build', 40);\n\n const tsconfigPath = path.join(cwd, 'tsconfig.json');\n const hasTypescript = deps.has('typescript');\n if (hasTypescript && (await fileExists(tsconfigPath)) && !scripts.typecheck) {\n warnings.push(\n \"No 'typecheck' script found; generating a default tsc gate.\",\n );\n gates.splice(1, 0, {\n name: 'typecheck',\n order: 20,\n command: 'npx tsc -p tsconfig.json --noEmit',\n });\n }\n\n return gates;\n}\n\nfunction inferPythonGates(\n requirements: Set<string>,\n warnings: string[],\n): Gate[] {\n const gates: Gate[] = [];\n\n if (requirements.has('ruff')) {\n gates.push({ name: 'lint', order: 10, command: 'python -m ruff check .' });\n }\n\n if (requirements.has('mypy')) {\n gates.push({ name: 'typecheck', order: 20, command: 'python -m mypy .' });\n }\n\n if (requirements.has('pytest')) {\n gates.push({ name: 'test', order: 30, command: 'python -m pytest -q' });\n } else {\n warnings.push(\n 'pytest not detected in requirements; no test gate generated.',\n );\n }\n\n return gates;\n}\n\nasync function updateGitignore(cwd: string): Promise<boolean> {\n const gitignorePath = path.join(cwd, '.gitignore');\n const pattern = 'gate-results-*.json';\n\n try {\n // Use git check-ignore to test if the pattern would already cover gate result files\n // This handles all edge cases: broader patterns, multiple patterns, etc.\n execSync('git check-ignore -q gate-results-test.json', {\n cwd,\n stdio: 'ignore',\n });\n // Exit code 0 means the file would be ignored - pattern already covered\n return false;\n } catch {\n // Not ignored or git not available - need to add the pattern\n }\n\n try {\n // Check if .gitignore exists and read it\n const exists = await fileExists(gitignorePath);\n const content = exists ? await fs.readFile(gitignorePath, 'utf8') : '';\n\n // Append the pattern with proper newline handling\n const newline = content && !content.endsWith('\\n') ? '\\n' : '';\n await fs.appendFile(gitignorePath, `${newline}${pattern}\\n`, 'utf8');\n return true;\n } catch {\n // Silently fail if we can't update .gitignore\n return false;\n }\n}\n\nexport async function generateGateConfig(cwd: string): Promise<{\n config: GateConfig;\n projectKind: DetectedProject['kind'];\n warnings: string[];\n}> {\n const warnings: string[] = [];\n const detected = await detectProject(cwd);\n\n let gates: Gate[] = [];\n\n if (detected.kind === 'node') {\n gates = await inferNodeGates(\n cwd,\n detected.packageManager,\n detected.packageJson,\n warnings,\n );\n } else if (detected.kind === 'python') {\n gates = inferPythonGates(detected.requirements, warnings);\n } else {\n warnings.push(\n 'No supported project markers found; creating an empty gate config.',\n );\n }\n\n return {\n config: {\n gates,\n failFast: true,\n },\n projectKind: detected.kind,\n warnings,\n };\n}\n\nexport async function initConfigFile(\n options: InitOptions = {},\n): Promise<InitResult> {\n const cwd = options.cwd ?? process.cwd();\n const filename = options.filename ?? DEFAULT_CONFIG_FILENAME;\n const configPath = path.join(cwd, filename);\n\n const { config, projectKind, warnings } = await generateGateConfig(cwd);\n\n if (options.print) {\n return {\n config,\n configPath,\n created: false,\n projectKind,\n warnings,\n };\n }\n\n const writeFlag = options.force ? 'w' : 'wx';\n\n try {\n await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\\n', {\n encoding: 'utf8',\n flag: writeFlag,\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const code = (error as NodeJS.ErrnoException | null)?.code;\n if (!options.force && code === 'EEXIST') {\n return {\n config,\n configPath,\n created: false,\n projectKind,\n warnings,\n error: `Config already exists at ${configPath}. Use --force to overwrite.`,\n };\n }\n return {\n config,\n configPath,\n created: false,\n projectKind,\n warnings,\n error: `Unable to write config at ${configPath}: ${message}`,\n };\n }\n\n // Update .gitignore to include gate result files\n const gitignoreUpdated = await updateGitignore(cwd);\n\n return {\n config,\n configPath,\n created: true,\n projectKind,\n warnings,\n gitignoreUpdated,\n };\n}\n","import { spawn } from 'node:child_process';\nimport type { Gate, GateResult, GateRunSummary } from './types.js';\n\nexport interface RunGatesOptions {\n failFast?: boolean;\n verbose?: boolean;\n shell?: string;\n cwd?: string;\n onGateStart?: (gate: Gate) => void;\n onGateOutput?: (\n gate: Gate,\n stream: 'stdout' | 'stderr',\n text: string,\n ) => void;\n onGateComplete?: (result: GateResult) => void;\n}\n\nfunction resolveShell(shell?: string): string | boolean {\n if (typeof shell === 'string' && shell.length > 0) {\n return shell;\n }\n const envShell = process.env.SHELL;\n return envShell && envShell.length > 0 ? envShell : true;\n}\n\nasync function runGate(\n gate: Gate,\n options: RunGatesOptions,\n): Promise<GateResult> {\n const start = Date.now();\n const shell = resolveShell(options.shell);\n const env = process.env;\n\n return new Promise((resolve) => {\n options.onGateStart?.(gate);\n const child = spawn(gate.command, {\n shell,\n env,\n cwd: options.cwd,\n });\n\n let stdout = '';\n let stderr = '';\n let exitCode: number | null = null;\n let resolved = false;\n\n const finalize = () => {\n if (resolved) {\n return;\n }\n resolved = true;\n const durationMs = Date.now() - start;\n const result: GateResult = {\n name: gate.name,\n passed: exitCode === 0,\n exitCode,\n stdout,\n stderr,\n durationMs,\n skipped: false,\n blocking: gate.blocking !== false,\n timestamp: new Date().toISOString(),\n };\n options.onGateComplete?.(result);\n resolve(result);\n };\n\n child.stdout?.on('data', (chunk) => {\n const text = chunk.toString();\n stdout += text;\n if (options.verbose) {\n process.stdout.write(text);\n }\n options.onGateOutput?.(gate, 'stdout', text);\n });\n\n child.stderr?.on('data', (chunk) => {\n const text = chunk.toString();\n stderr += text;\n if (options.verbose) {\n process.stderr.write(text);\n }\n options.onGateOutput?.(gate, 'stderr', text);\n });\n\n child.on('error', (error) => {\n stderr += error.message;\n exitCode = null;\n finalize();\n });\n\n child.on('close', (code) => {\n exitCode = code;\n finalize();\n });\n });\n}\n\nexport async function runGates(\n gates: Gate[],\n options: RunGatesOptions = {},\n): Promise<GateRunSummary> {\n const results: GateResult[] = [];\n const warnings: string[] = [];\n const start = Date.now();\n let firstFailure: GateResult | null = null;\n\n for (const gate of gates) {\n const isBlocking = gate.blocking !== false;\n const shouldSkip =\n options.failFast !== false && firstFailure !== null && isBlocking;\n\n if (shouldSkip) {\n const skippedResult: GateResult = {\n name: gate.name,\n passed: true,\n exitCode: null,\n stdout: '',\n stderr: '',\n durationMs: 0,\n skipped: true,\n blocking: isBlocking,\n timestamp: new Date().toISOString(),\n };\n results.push(skippedResult);\n options.onGateComplete?.(skippedResult);\n continue;\n }\n\n const result = await runGate(gate, options);\n results.push(result);\n\n if (!result.passed) {\n if (result.blocking) {\n if (!firstFailure) {\n firstFailure = result;\n }\n } else {\n warnings.push(result.name);\n }\n }\n }\n\n const totalDurationMs = Date.now() - start;\n const passed = firstFailure === null;\n\n return {\n passed,\n timestamp: new Date().toISOString(),\n totalDurationMs,\n results,\n firstFailure,\n warnings,\n };\n}\n","import { promises as fs } from 'node:fs';\nimport type { GateResult, GateRunSummary } from './types.js';\n\nconst MAX_FAILURE_CHARS = 4000;\nconst HEAD_RATIO = 0.6;\n\nfunction truncateOutput(text: string, maxChars: number): string {\n const trimmed = text.trimEnd();\n if (maxChars <= 0 || trimmed.length === 0) {\n return '';\n }\n if (trimmed.length <= maxChars) {\n return trimmed;\n }\n const headChars = Math.max(1, Math.floor(maxChars * HEAD_RATIO));\n const tailChars = Math.max(0, maxChars - headChars);\n const omitted = trimmed.length - maxChars;\n const marker = `\\n...<${omitted} chars omitted>...\\n`;\n const head = trimmed.slice(0, headChars);\n const tail = tailChars > 0 ? trimmed.slice(-tailChars) : '';\n return `${head}${marker}${tail}`;\n}\n\nfunction splitBudget(\n stdoutLen: number,\n stderrLen: number,\n): {\n stdout: number;\n stderr: number;\n} {\n if (stdoutLen === 0 && stderrLen === 0) {\n return { stdout: 0, stderr: 0 };\n }\n if (stdoutLen === 0) {\n return { stdout: 0, stderr: MAX_FAILURE_CHARS };\n }\n if (stderrLen === 0) {\n return { stdout: MAX_FAILURE_CHARS, stderr: 0 };\n }\n\n const base = Math.floor(MAX_FAILURE_CHARS / 2);\n let stdout = Math.min(stdoutLen, base);\n let stderr = Math.min(stderrLen, base);\n let remaining = MAX_FAILURE_CHARS - stdout - stderr;\n\n if (remaining > 0) {\n const stdoutRemaining = stdoutLen - stdout;\n const stderrRemaining = stderrLen - stderr;\n if (stdoutRemaining === 0) {\n stderr += remaining;\n } else if (stderrRemaining === 0) {\n stdout += remaining;\n } else {\n const totalRemaining = stdoutRemaining + stderrRemaining;\n const stdoutExtra = Math.round(\n (stdoutRemaining / totalRemaining) * remaining,\n );\n stdout += stdoutExtra;\n stderr += remaining - stdoutExtra;\n }\n }\n\n return { stdout, stderr };\n}\n\nfunction colorize(text: string, code: string, enabled: boolean): string {\n if (!enabled) {\n return text;\n }\n return `\\u001b[${code}m${text}\\u001b[0m`;\n}\n\nfunction formatResultLine(result: GateResult, color: boolean): string {\n const statusSymbol = result.skipped ? '⊘' : result.passed ? '✓' : '✗';\n\n let symbolColor = '32';\n if (result.skipped) {\n symbolColor = '90';\n } else if (!result.passed) {\n symbolColor = '31';\n }\n\n const parts: string[] = [];\n if (result.skipped) {\n parts.push('skipped');\n } else if (!result.passed) {\n parts.push(`exit ${result.exitCode ?? 'null'}`);\n }\n parts.push(`${result.durationMs}ms`);\n\n const details = parts.length > 0 ? ` (${parts.join(', ')})` : '';\n const coloredSymbol = colorize(statusSymbol, symbolColor, color);\n return `${coloredSymbol} ${result.name}${details}`;\n}\n\nexport function formatFailureContext(stderr: string, stdout = ''): string {\n const trimmedStderr = stderr.trimEnd();\n const trimmedStdout = stdout.trimEnd();\n\n if (trimmedStderr.length === 0 && trimmedStdout.length === 0) {\n return 'No output captured.';\n }\n\n if (trimmedStderr.length > 0 && trimmedStdout.length > 0) {\n const budget = splitBudget(trimmedStdout.length, trimmedStderr.length);\n const stderrSection = truncateOutput(trimmedStderr, budget.stderr);\n const stdoutSection = truncateOutput(trimmedStdout, budget.stdout);\n return `STDERR:\\n${stderrSection}\\n\\nSTDOUT:\\n${stdoutSection}`;\n }\n\n if (trimmedStderr.length > 0) {\n return truncateOutput(trimmedStderr, MAX_FAILURE_CHARS);\n }\n\n return truncateOutput(trimmedStdout, MAX_FAILURE_CHARS);\n}\n\nexport function formatConsoleOutput(summary: GateRunSummary): string {\n const color = Boolean(process.stdout.isTTY);\n const lines: string[] = [];\n\n for (const result of summary.results) {\n lines.push(formatResultLine(result, color));\n }\n\n if (summary.warnings.length > 0) {\n const warningLine = `Warnings: ${summary.warnings.join(', ')}`;\n lines.push(colorize(warningLine, '33', color));\n }\n\n const finalLine = summary.passed\n ? colorize('All blocking gates passed.', '32', color)\n : colorize('Blocking gate failed.', '31', color);\n lines.push(finalLine);\n\n return lines.join('\\n');\n}\n\nexport async function writeResultsFile(\n summary: GateRunSummary,\n outputPath: string,\n): Promise<void> {\n const data = JSON.stringify(summary, null, 2);\n await fs.writeFile(outputPath, data, 'utf8');\n}\n","import type { GateRunSummary, HookOutput } from './types.js';\nimport { formatFailureContext } from './output.js';\n\nexport function generateHookResponse(summary: GateRunSummary): HookOutput {\n const warnings = summary.warnings.length > 0 ? summary.warnings : undefined;\n\n if (summary.passed) {\n return warnings ? { warnings } : {};\n }\n\n const failure = summary.firstFailure;\n if (failure) {\n const reason = `Gate '${failure.name}' failed (exit ${failure.exitCode ?? 'null'}):\\n${formatFailureContext(failure.stderr, failure.stdout)}`;\n return warnings\n ? { decision: 'block', reason, warnings }\n : { decision: 'block', reason };\n }\n\n const reason = 'Gate run failed without a blocking gate result.';\n return warnings\n ? { decision: 'block', reason, warnings }\n : { decision: 'block', reason };\n}\n\nexport function outputHookResponse(output: HookOutput): void {\n process.stdout.write(`${JSON.stringify(output)}\\n`);\n}\n"],"mappings":"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","names":["path","fs","path","fs","reason","options","error","path","summary","outputPath","shellLabel"]}
@@ -0,0 +1,62 @@
1
+ interface Gate {
2
+ name: string;
3
+ command: string;
4
+ description?: string;
5
+ order?: number;
6
+ enabled?: boolean;
7
+ blocking?: boolean;
8
+ }
9
+ interface GateResult {
10
+ name: string;
11
+ passed: boolean;
12
+ exitCode: number | null;
13
+ stdout: string;
14
+ stderr: string;
15
+ durationMs: number;
16
+ skipped: boolean;
17
+ blocking: boolean;
18
+ timestamp: string;
19
+ }
20
+ interface GateRunSummary {
21
+ passed: boolean;
22
+ timestamp: string;
23
+ totalDurationMs: number;
24
+ results: GateResult[];
25
+ firstFailure: GateResult | null;
26
+ warnings: string[];
27
+ }
28
+ interface GateConfig {
29
+ gates: Gate[];
30
+ outputPath?: string;
31
+ failFast?: boolean;
32
+ }
33
+ interface HookOutput {
34
+ decision?: 'block';
35
+ reason?: string;
36
+ warnings?: string[];
37
+ }
38
+
39
+ interface RunGatesOptions {
40
+ failFast?: boolean;
41
+ verbose?: boolean;
42
+ shell?: string;
43
+ cwd?: string;
44
+ onGateStart?: (gate: Gate) => void;
45
+ onGateOutput?: (gate: Gate, stream: 'stdout' | 'stderr', text: string) => void;
46
+ onGateComplete?: (result: GateResult) => void;
47
+ }
48
+ declare function runGates(gates: Gate[], options?: RunGatesOptions): Promise<GateRunSummary>;
49
+
50
+ interface LoadConfigResult {
51
+ config: GateConfig | null;
52
+ configPath?: string;
53
+ error?: string;
54
+ }
55
+ declare function loadConfig(cwd?: string): Promise<LoadConfigResult>;
56
+
57
+ declare function generateHookResponse(summary: GateRunSummary): HookOutput;
58
+
59
+ declare function formatFailureContext(stderr: string, stdout?: string): string;
60
+ declare function formatConsoleOutput(summary: GateRunSummary): string;
61
+
62
+ export { type Gate, type GateConfig, type GateResult, type GateRunSummary, type HookOutput, formatConsoleOutput, formatFailureContext, generateHookResponse, loadConfig, runGates };
package/dist/index.js ADDED
@@ -0,0 +1,374 @@
1
+ // src/runner.ts
2
+ import { spawn } from "child_process";
3
+ function resolveShell(shell) {
4
+ if (typeof shell === "string" && shell.length > 0) {
5
+ return shell;
6
+ }
7
+ const envShell = process.env.SHELL;
8
+ return envShell && envShell.length > 0 ? envShell : true;
9
+ }
10
+ async function runGate(gate, options) {
11
+ const start = Date.now();
12
+ const shell = resolveShell(options.shell);
13
+ const env = process.env;
14
+ return new Promise((resolve) => {
15
+ options.onGateStart?.(gate);
16
+ const child = spawn(gate.command, {
17
+ shell,
18
+ env,
19
+ cwd: options.cwd
20
+ });
21
+ let stdout = "";
22
+ let stderr = "";
23
+ let exitCode = null;
24
+ let resolved = false;
25
+ const finalize = () => {
26
+ if (resolved) {
27
+ return;
28
+ }
29
+ resolved = true;
30
+ const durationMs = Date.now() - start;
31
+ const result = {
32
+ name: gate.name,
33
+ passed: exitCode === 0,
34
+ exitCode,
35
+ stdout,
36
+ stderr,
37
+ durationMs,
38
+ skipped: false,
39
+ blocking: gate.blocking !== false,
40
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
41
+ };
42
+ options.onGateComplete?.(result);
43
+ resolve(result);
44
+ };
45
+ child.stdout?.on("data", (chunk) => {
46
+ const text = chunk.toString();
47
+ stdout += text;
48
+ if (options.verbose) {
49
+ process.stdout.write(text);
50
+ }
51
+ options.onGateOutput?.(gate, "stdout", text);
52
+ });
53
+ child.stderr?.on("data", (chunk) => {
54
+ const text = chunk.toString();
55
+ stderr += text;
56
+ if (options.verbose) {
57
+ process.stderr.write(text);
58
+ }
59
+ options.onGateOutput?.(gate, "stderr", text);
60
+ });
61
+ child.on("error", (error) => {
62
+ stderr += error.message;
63
+ exitCode = null;
64
+ finalize();
65
+ });
66
+ child.on("close", (code) => {
67
+ exitCode = code;
68
+ finalize();
69
+ });
70
+ });
71
+ }
72
+ async function runGates(gates, options = {}) {
73
+ const results = [];
74
+ const warnings = [];
75
+ const start = Date.now();
76
+ let firstFailure = null;
77
+ for (const gate of gates) {
78
+ const isBlocking = gate.blocking !== false;
79
+ const shouldSkip = options.failFast !== false && firstFailure !== null && isBlocking;
80
+ if (shouldSkip) {
81
+ const skippedResult = {
82
+ name: gate.name,
83
+ passed: true,
84
+ exitCode: null,
85
+ stdout: "",
86
+ stderr: "",
87
+ durationMs: 0,
88
+ skipped: true,
89
+ blocking: isBlocking,
90
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
91
+ };
92
+ results.push(skippedResult);
93
+ options.onGateComplete?.(skippedResult);
94
+ continue;
95
+ }
96
+ const result = await runGate(gate, options);
97
+ results.push(result);
98
+ if (!result.passed) {
99
+ if (result.blocking) {
100
+ if (!firstFailure) {
101
+ firstFailure = result;
102
+ }
103
+ } else {
104
+ warnings.push(result.name);
105
+ }
106
+ }
107
+ }
108
+ const totalDurationMs = Date.now() - start;
109
+ const passed = firstFailure === null;
110
+ return {
111
+ passed,
112
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
113
+ totalDurationMs,
114
+ results,
115
+ firstFailure,
116
+ warnings
117
+ };
118
+ }
119
+
120
+ // src/config.ts
121
+ import { promises as fs } from "fs";
122
+ import path from "path";
123
+ var CONFIG_FILES = ["gate.config.json", ".gaterc.json", ".gaterc"];
124
+ function normalizeGate(gate) {
125
+ const order = typeof gate.order === "number" ? gate.order : 100;
126
+ const enabled = typeof gate.enabled === "boolean" ? gate.enabled : true;
127
+ const blocking = typeof gate.blocking === "boolean" ? gate.blocking : true;
128
+ return {
129
+ ...gate,
130
+ order,
131
+ enabled,
132
+ blocking,
133
+ description: typeof gate.description === "string" ? gate.description : void 0,
134
+ name: gate.name,
135
+ command: gate.command
136
+ };
137
+ }
138
+ function validateGate(gate) {
139
+ if (!gate || typeof gate !== "object") {
140
+ return "Gate entries must be objects.";
141
+ }
142
+ const maybeGate = gate;
143
+ if (typeof maybeGate.name !== "string" || maybeGate.name.trim() === "") {
144
+ return "Gate is missing required field: name.";
145
+ }
146
+ if (typeof maybeGate.command !== "string" || maybeGate.command.trim() === "") {
147
+ return `Gate '${maybeGate.name}' is missing required field: command.`;
148
+ }
149
+ return null;
150
+ }
151
+ function sortGates(gates) {
152
+ const withIndex = gates.map((gate, index) => ({ gate, index }));
153
+ withIndex.sort((a, b) => {
154
+ const orderA = typeof a.gate.order === "number" ? a.gate.order : 100;
155
+ const orderB = typeof b.gate.order === "number" ? b.gate.order : 100;
156
+ if (orderA !== orderB) {
157
+ return orderA - orderB;
158
+ }
159
+ return a.index - b.index;
160
+ });
161
+ return withIndex.map((entry) => entry.gate);
162
+ }
163
+ async function loadConfig(cwd = process.cwd()) {
164
+ for (const filename of CONFIG_FILES) {
165
+ const filePath = path.join(cwd, filename);
166
+ try {
167
+ await fs.access(filePath);
168
+ } catch {
169
+ continue;
170
+ }
171
+ let raw;
172
+ try {
173
+ raw = await fs.readFile(filePath, "utf8");
174
+ } catch (error) {
175
+ return {
176
+ config: null,
177
+ configPath: filePath,
178
+ error: `Invalid config: unable to read ${filename}: ${error.message}`
179
+ };
180
+ }
181
+ let parsed;
182
+ try {
183
+ parsed = JSON.parse(raw);
184
+ } catch (error) {
185
+ return {
186
+ config: null,
187
+ configPath: filePath,
188
+ error: `Invalid config: malformed JSON in ${filename}: ${error.message}`
189
+ };
190
+ }
191
+ if (!parsed || typeof parsed !== "object") {
192
+ return {
193
+ config: null,
194
+ configPath: filePath,
195
+ error: `Invalid config: expected object in ${filename}.`
196
+ };
197
+ }
198
+ const config = parsed;
199
+ if (!Array.isArray(config.gates)) {
200
+ return {
201
+ config: null,
202
+ configPath: filePath,
203
+ error: `Invalid config: missing required 'gates' array in ${filename}.`
204
+ };
205
+ }
206
+ const normalized = [];
207
+ for (const gate of config.gates) {
208
+ const gateError = validateGate(gate);
209
+ if (gateError) {
210
+ return {
211
+ config: null,
212
+ configPath: filePath,
213
+ error: `Invalid config: ${gateError}`
214
+ };
215
+ }
216
+ const normalizedGate = normalizeGate(gate);
217
+ if (normalizedGate.enabled === false) {
218
+ continue;
219
+ }
220
+ normalized.push(normalizedGate);
221
+ }
222
+ const failFast = typeof config.failFast === "boolean" ? config.failFast : true;
223
+ const outputPath = typeof config.outputPath === "string" ? config.outputPath : void 0;
224
+ const sorted = sortGates(normalized);
225
+ return {
226
+ config: {
227
+ gates: sorted,
228
+ failFast,
229
+ outputPath
230
+ },
231
+ configPath: filePath
232
+ };
233
+ }
234
+ return { config: null };
235
+ }
236
+
237
+ // src/output.ts
238
+ import { promises as fs2 } from "fs";
239
+ var MAX_FAILURE_CHARS = 4e3;
240
+ var HEAD_RATIO = 0.6;
241
+ function truncateOutput(text, maxChars) {
242
+ const trimmed = text.trimEnd();
243
+ if (maxChars <= 0 || trimmed.length === 0) {
244
+ return "";
245
+ }
246
+ if (trimmed.length <= maxChars) {
247
+ return trimmed;
248
+ }
249
+ const headChars = Math.max(1, Math.floor(maxChars * HEAD_RATIO));
250
+ const tailChars = Math.max(0, maxChars - headChars);
251
+ const omitted = trimmed.length - maxChars;
252
+ const marker = `
253
+ ...<${omitted} chars omitted>...
254
+ `;
255
+ const head = trimmed.slice(0, headChars);
256
+ const tail = tailChars > 0 ? trimmed.slice(-tailChars) : "";
257
+ return `${head}${marker}${tail}`;
258
+ }
259
+ function splitBudget(stdoutLen, stderrLen) {
260
+ if (stdoutLen === 0 && stderrLen === 0) {
261
+ return { stdout: 0, stderr: 0 };
262
+ }
263
+ if (stdoutLen === 0) {
264
+ return { stdout: 0, stderr: MAX_FAILURE_CHARS };
265
+ }
266
+ if (stderrLen === 0) {
267
+ return { stdout: MAX_FAILURE_CHARS, stderr: 0 };
268
+ }
269
+ const base = Math.floor(MAX_FAILURE_CHARS / 2);
270
+ let stdout = Math.min(stdoutLen, base);
271
+ let stderr = Math.min(stderrLen, base);
272
+ let remaining = MAX_FAILURE_CHARS - stdout - stderr;
273
+ if (remaining > 0) {
274
+ const stdoutRemaining = stdoutLen - stdout;
275
+ const stderrRemaining = stderrLen - stderr;
276
+ if (stdoutRemaining === 0) {
277
+ stderr += remaining;
278
+ } else if (stderrRemaining === 0) {
279
+ stdout += remaining;
280
+ } else {
281
+ const totalRemaining = stdoutRemaining + stderrRemaining;
282
+ const stdoutExtra = Math.round(
283
+ stdoutRemaining / totalRemaining * remaining
284
+ );
285
+ stdout += stdoutExtra;
286
+ stderr += remaining - stdoutExtra;
287
+ }
288
+ }
289
+ return { stdout, stderr };
290
+ }
291
+ function colorize(text, code, enabled) {
292
+ if (!enabled) {
293
+ return text;
294
+ }
295
+ return `\x1B[${code}m${text}\x1B[0m`;
296
+ }
297
+ function formatResultLine(result, color) {
298
+ const statusSymbol = result.skipped ? "\u2298" : result.passed ? "\u2713" : "\u2717";
299
+ let symbolColor = "32";
300
+ if (result.skipped) {
301
+ symbolColor = "90";
302
+ } else if (!result.passed) {
303
+ symbolColor = "31";
304
+ }
305
+ const parts = [];
306
+ if (result.skipped) {
307
+ parts.push("skipped");
308
+ } else if (!result.passed) {
309
+ parts.push(`exit ${result.exitCode ?? "null"}`);
310
+ }
311
+ parts.push(`${result.durationMs}ms`);
312
+ const details = parts.length > 0 ? ` (${parts.join(", ")})` : "";
313
+ const coloredSymbol = colorize(statusSymbol, symbolColor, color);
314
+ return `${coloredSymbol} ${result.name}${details}`;
315
+ }
316
+ function formatFailureContext(stderr, stdout = "") {
317
+ const trimmedStderr = stderr.trimEnd();
318
+ const trimmedStdout = stdout.trimEnd();
319
+ if (trimmedStderr.length === 0 && trimmedStdout.length === 0) {
320
+ return "No output captured.";
321
+ }
322
+ if (trimmedStderr.length > 0 && trimmedStdout.length > 0) {
323
+ const budget = splitBudget(trimmedStdout.length, trimmedStderr.length);
324
+ const stderrSection = truncateOutput(trimmedStderr, budget.stderr);
325
+ const stdoutSection = truncateOutput(trimmedStdout, budget.stdout);
326
+ return `STDERR:
327
+ ${stderrSection}
328
+
329
+ STDOUT:
330
+ ${stdoutSection}`;
331
+ }
332
+ if (trimmedStderr.length > 0) {
333
+ return truncateOutput(trimmedStderr, MAX_FAILURE_CHARS);
334
+ }
335
+ return truncateOutput(trimmedStdout, MAX_FAILURE_CHARS);
336
+ }
337
+ function formatConsoleOutput(summary) {
338
+ const color = Boolean(process.stdout.isTTY);
339
+ const lines = [];
340
+ for (const result of summary.results) {
341
+ lines.push(formatResultLine(result, color));
342
+ }
343
+ if (summary.warnings.length > 0) {
344
+ const warningLine = `Warnings: ${summary.warnings.join(", ")}`;
345
+ lines.push(colorize(warningLine, "33", color));
346
+ }
347
+ const finalLine = summary.passed ? colorize("All blocking gates passed.", "32", color) : colorize("Blocking gate failed.", "31", color);
348
+ lines.push(finalLine);
349
+ return lines.join("\n");
350
+ }
351
+
352
+ // src/hook.ts
353
+ function generateHookResponse(summary) {
354
+ const warnings = summary.warnings.length > 0 ? summary.warnings : void 0;
355
+ if (summary.passed) {
356
+ return warnings ? { warnings } : {};
357
+ }
358
+ const failure = summary.firstFailure;
359
+ if (failure) {
360
+ const reason2 = `Gate '${failure.name}' failed (exit ${failure.exitCode ?? "null"}):
361
+ ${formatFailureContext(failure.stderr, failure.stdout)}`;
362
+ return warnings ? { decision: "block", reason: reason2, warnings } : { decision: "block", reason: reason2 };
363
+ }
364
+ const reason = "Gate run failed without a blocking gate result.";
365
+ return warnings ? { decision: "block", reason, warnings } : { decision: "block", reason };
366
+ }
367
+ export {
368
+ formatConsoleOutput,
369
+ formatFailureContext,
370
+ generateHookResponse,
371
+ loadConfig,
372
+ runGates
373
+ };
374
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runner.ts","../src/config.ts","../src/output.ts","../src/hook.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport type { Gate, GateResult, GateRunSummary } from './types.js';\n\nexport interface RunGatesOptions {\n failFast?: boolean;\n verbose?: boolean;\n shell?: string;\n cwd?: string;\n onGateStart?: (gate: Gate) => void;\n onGateOutput?: (\n gate: Gate,\n stream: 'stdout' | 'stderr',\n text: string,\n ) => void;\n onGateComplete?: (result: GateResult) => void;\n}\n\nfunction resolveShell(shell?: string): string | boolean {\n if (typeof shell === 'string' && shell.length > 0) {\n return shell;\n }\n const envShell = process.env.SHELL;\n return envShell && envShell.length > 0 ? envShell : true;\n}\n\nasync function runGate(\n gate: Gate,\n options: RunGatesOptions,\n): Promise<GateResult> {\n const start = Date.now();\n const shell = resolveShell(options.shell);\n const env = process.env;\n\n return new Promise((resolve) => {\n options.onGateStart?.(gate);\n const child = spawn(gate.command, {\n shell,\n env,\n cwd: options.cwd,\n });\n\n let stdout = '';\n let stderr = '';\n let exitCode: number | null = null;\n let resolved = false;\n\n const finalize = () => {\n if (resolved) {\n return;\n }\n resolved = true;\n const durationMs = Date.now() - start;\n const result: GateResult = {\n name: gate.name,\n passed: exitCode === 0,\n exitCode,\n stdout,\n stderr,\n durationMs,\n skipped: false,\n blocking: gate.blocking !== false,\n timestamp: new Date().toISOString(),\n };\n options.onGateComplete?.(result);\n resolve(result);\n };\n\n child.stdout?.on('data', (chunk) => {\n const text = chunk.toString();\n stdout += text;\n if (options.verbose) {\n process.stdout.write(text);\n }\n options.onGateOutput?.(gate, 'stdout', text);\n });\n\n child.stderr?.on('data', (chunk) => {\n const text = chunk.toString();\n stderr += text;\n if (options.verbose) {\n process.stderr.write(text);\n }\n options.onGateOutput?.(gate, 'stderr', text);\n });\n\n child.on('error', (error) => {\n stderr += error.message;\n exitCode = null;\n finalize();\n });\n\n child.on('close', (code) => {\n exitCode = code;\n finalize();\n });\n });\n}\n\nexport async function runGates(\n gates: Gate[],\n options: RunGatesOptions = {},\n): Promise<GateRunSummary> {\n const results: GateResult[] = [];\n const warnings: string[] = [];\n const start = Date.now();\n let firstFailure: GateResult | null = null;\n\n for (const gate of gates) {\n const isBlocking = gate.blocking !== false;\n const shouldSkip =\n options.failFast !== false && firstFailure !== null && isBlocking;\n\n if (shouldSkip) {\n const skippedResult: GateResult = {\n name: gate.name,\n passed: true,\n exitCode: null,\n stdout: '',\n stderr: '',\n durationMs: 0,\n skipped: true,\n blocking: isBlocking,\n timestamp: new Date().toISOString(),\n };\n results.push(skippedResult);\n options.onGateComplete?.(skippedResult);\n continue;\n }\n\n const result = await runGate(gate, options);\n results.push(result);\n\n if (!result.passed) {\n if (result.blocking) {\n if (!firstFailure) {\n firstFailure = result;\n }\n } else {\n warnings.push(result.name);\n }\n }\n }\n\n const totalDurationMs = Date.now() - start;\n const passed = firstFailure === null;\n\n return {\n passed,\n timestamp: new Date().toISOString(),\n totalDurationMs,\n results,\n firstFailure,\n warnings,\n };\n}\n","import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport type { Gate, GateConfig } from './types.js';\n\nconst CONFIG_FILES = ['gate.config.json', '.gaterc.json', '.gaterc'];\n\nexport interface LoadConfigResult {\n config: GateConfig | null;\n configPath?: string;\n error?: string;\n}\n\nfunction normalizeGate(gate: Gate): Gate {\n const order = typeof gate.order === 'number' ? gate.order : 100;\n const enabled = typeof gate.enabled === 'boolean' ? gate.enabled : true;\n const blocking = typeof gate.blocking === 'boolean' ? gate.blocking : true;\n\n return {\n ...gate,\n order,\n enabled,\n blocking,\n description:\n typeof gate.description === 'string' ? gate.description : undefined,\n name: gate.name,\n command: gate.command,\n } as Gate;\n}\n\nfunction validateGate(gate: unknown): string | null {\n if (!gate || typeof gate !== 'object') {\n return 'Gate entries must be objects.';\n }\n const maybeGate = gate as Gate;\n if (typeof maybeGate.name !== 'string' || maybeGate.name.trim() === '') {\n return 'Gate is missing required field: name.';\n }\n if (\n typeof maybeGate.command !== 'string' ||\n maybeGate.command.trim() === ''\n ) {\n return `Gate '${maybeGate.name}' is missing required field: command.`;\n }\n return null;\n}\n\nfunction sortGates(gates: Gate[]): Gate[] {\n const withIndex = gates.map((gate, index) => ({ gate, index }));\n withIndex.sort((a, b) => {\n const orderA = typeof a.gate.order === 'number' ? a.gate.order : 100;\n const orderB = typeof b.gate.order === 'number' ? b.gate.order : 100;\n if (orderA !== orderB) {\n return orderA - orderB;\n }\n return a.index - b.index;\n });\n return withIndex.map((entry) => entry.gate);\n}\n\nexport async function loadConfig(\n cwd: string = process.cwd(),\n): Promise<LoadConfigResult> {\n for (const filename of CONFIG_FILES) {\n const filePath = path.join(cwd, filename);\n try {\n await fs.access(filePath);\n } catch {\n continue;\n }\n\n let raw: string;\n try {\n raw = await fs.readFile(filePath, 'utf8');\n } catch (error) {\n return {\n config: null,\n configPath: filePath,\n error: `Invalid config: unable to read ${filename}: ${(error as Error).message}`,\n };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n return {\n config: null,\n configPath: filePath,\n error: `Invalid config: malformed JSON in ${filename}: ${(error as Error).message}`,\n };\n }\n\n if (!parsed || typeof parsed !== 'object') {\n return {\n config: null,\n configPath: filePath,\n error: `Invalid config: expected object in ${filename}.`,\n };\n }\n\n const config = parsed as GateConfig;\n if (!Array.isArray(config.gates)) {\n return {\n config: null,\n configPath: filePath,\n error: `Invalid config: missing required 'gates' array in ${filename}.`,\n };\n }\n\n const normalized: Gate[] = [];\n for (const gate of config.gates) {\n const gateError = validateGate(gate as Gate);\n if (gateError) {\n return {\n config: null,\n configPath: filePath,\n error: `Invalid config: ${gateError}`,\n };\n }\n const normalizedGate = normalizeGate(gate as Gate);\n if (normalizedGate.enabled === false) {\n continue;\n }\n normalized.push(normalizedGate);\n }\n\n const failFast =\n typeof config.failFast === 'boolean' ? config.failFast : true;\n const outputPath =\n typeof config.outputPath === 'string' ? config.outputPath : undefined;\n\n const sorted = sortGates(normalized);\n\n return {\n config: {\n gates: sorted,\n failFast,\n outputPath,\n },\n configPath: filePath,\n };\n }\n\n return { config: null };\n}\n","import { promises as fs } from 'node:fs';\nimport type { GateResult, GateRunSummary } from './types.js';\n\nconst MAX_FAILURE_CHARS = 4000;\nconst HEAD_RATIO = 0.6;\n\nfunction truncateOutput(text: string, maxChars: number): string {\n const trimmed = text.trimEnd();\n if (maxChars <= 0 || trimmed.length === 0) {\n return '';\n }\n if (trimmed.length <= maxChars) {\n return trimmed;\n }\n const headChars = Math.max(1, Math.floor(maxChars * HEAD_RATIO));\n const tailChars = Math.max(0, maxChars - headChars);\n const omitted = trimmed.length - maxChars;\n const marker = `\\n...<${omitted} chars omitted>...\\n`;\n const head = trimmed.slice(0, headChars);\n const tail = tailChars > 0 ? trimmed.slice(-tailChars) : '';\n return `${head}${marker}${tail}`;\n}\n\nfunction splitBudget(\n stdoutLen: number,\n stderrLen: number,\n): {\n stdout: number;\n stderr: number;\n} {\n if (stdoutLen === 0 && stderrLen === 0) {\n return { stdout: 0, stderr: 0 };\n }\n if (stdoutLen === 0) {\n return { stdout: 0, stderr: MAX_FAILURE_CHARS };\n }\n if (stderrLen === 0) {\n return { stdout: MAX_FAILURE_CHARS, stderr: 0 };\n }\n\n const base = Math.floor(MAX_FAILURE_CHARS / 2);\n let stdout = Math.min(stdoutLen, base);\n let stderr = Math.min(stderrLen, base);\n let remaining = MAX_FAILURE_CHARS - stdout - stderr;\n\n if (remaining > 0) {\n const stdoutRemaining = stdoutLen - stdout;\n const stderrRemaining = stderrLen - stderr;\n if (stdoutRemaining === 0) {\n stderr += remaining;\n } else if (stderrRemaining === 0) {\n stdout += remaining;\n } else {\n const totalRemaining = stdoutRemaining + stderrRemaining;\n const stdoutExtra = Math.round(\n (stdoutRemaining / totalRemaining) * remaining,\n );\n stdout += stdoutExtra;\n stderr += remaining - stdoutExtra;\n }\n }\n\n return { stdout, stderr };\n}\n\nfunction colorize(text: string, code: string, enabled: boolean): string {\n if (!enabled) {\n return text;\n }\n return `\\u001b[${code}m${text}\\u001b[0m`;\n}\n\nfunction formatResultLine(result: GateResult, color: boolean): string {\n const statusSymbol = result.skipped ? '⊘' : result.passed ? '✓' : '✗';\n\n let symbolColor = '32';\n if (result.skipped) {\n symbolColor = '90';\n } else if (!result.passed) {\n symbolColor = '31';\n }\n\n const parts: string[] = [];\n if (result.skipped) {\n parts.push('skipped');\n } else if (!result.passed) {\n parts.push(`exit ${result.exitCode ?? 'null'}`);\n }\n parts.push(`${result.durationMs}ms`);\n\n const details = parts.length > 0 ? ` (${parts.join(', ')})` : '';\n const coloredSymbol = colorize(statusSymbol, symbolColor, color);\n return `${coloredSymbol} ${result.name}${details}`;\n}\n\nexport function formatFailureContext(stderr: string, stdout = ''): string {\n const trimmedStderr = stderr.trimEnd();\n const trimmedStdout = stdout.trimEnd();\n\n if (trimmedStderr.length === 0 && trimmedStdout.length === 0) {\n return 'No output captured.';\n }\n\n if (trimmedStderr.length > 0 && trimmedStdout.length > 0) {\n const budget = splitBudget(trimmedStdout.length, trimmedStderr.length);\n const stderrSection = truncateOutput(trimmedStderr, budget.stderr);\n const stdoutSection = truncateOutput(trimmedStdout, budget.stdout);\n return `STDERR:\\n${stderrSection}\\n\\nSTDOUT:\\n${stdoutSection}`;\n }\n\n if (trimmedStderr.length > 0) {\n return truncateOutput(trimmedStderr, MAX_FAILURE_CHARS);\n }\n\n return truncateOutput(trimmedStdout, MAX_FAILURE_CHARS);\n}\n\nexport function formatConsoleOutput(summary: GateRunSummary): string {\n const color = Boolean(process.stdout.isTTY);\n const lines: string[] = [];\n\n for (const result of summary.results) {\n lines.push(formatResultLine(result, color));\n }\n\n if (summary.warnings.length > 0) {\n const warningLine = `Warnings: ${summary.warnings.join(', ')}`;\n lines.push(colorize(warningLine, '33', color));\n }\n\n const finalLine = summary.passed\n ? colorize('All blocking gates passed.', '32', color)\n : colorize('Blocking gate failed.', '31', color);\n lines.push(finalLine);\n\n return lines.join('\\n');\n}\n\nexport async function writeResultsFile(\n summary: GateRunSummary,\n outputPath: string,\n): Promise<void> {\n const data = JSON.stringify(summary, null, 2);\n await fs.writeFile(outputPath, data, 'utf8');\n}\n","import type { GateRunSummary, HookOutput } from './types.js';\nimport { formatFailureContext } from './output.js';\n\nexport function generateHookResponse(summary: GateRunSummary): HookOutput {\n const warnings = summary.warnings.length > 0 ? summary.warnings : undefined;\n\n if (summary.passed) {\n return warnings ? { warnings } : {};\n }\n\n const failure = summary.firstFailure;\n if (failure) {\n const reason = `Gate '${failure.name}' failed (exit ${failure.exitCode ?? 'null'}):\\n${formatFailureContext(failure.stderr, failure.stdout)}`;\n return warnings\n ? { decision: 'block', reason, warnings }\n : { decision: 'block', reason };\n }\n\n const reason = 'Gate run failed without a blocking gate result.';\n return warnings\n ? { decision: 'block', reason, warnings }\n : { decision: 'block', reason };\n}\n\nexport function outputHookResponse(output: HookOutput): void {\n process.stdout.write(`${JSON.stringify(output)}\\n`);\n}\n"],"mappings":"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","names":["fs","reason"]}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "ralph-gate",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "description": "Structured verification gates for Claude Code - prevent premature completion with ordered validation checks",
6
+ "keywords": [
7
+ "claude",
8
+ "claude-code",
9
+ "verification",
10
+ "gates",
11
+ "testing",
12
+ "cli",
13
+ "hooks"
14
+ ],
15
+ "homepage": "https://github.com/Uzair-akrum/ralph-gate",
16
+ "bugs": {
17
+ "url": "https://github.com/Uzair-akrum/ralph-gate/issues"
18
+ },
19
+ "author": "Uzair Akram <uzair.akrum@gmail.com> (https://github.com/Uzair-akrum)",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/Uzair-akrum/ralph-gate.git"
23
+ },
24
+ "license": "MIT",
25
+ "bin": {
26
+ "ralph-gate": "dist/cli.js"
27
+ },
28
+ "exports": {
29
+ ".": "./dist/index.js"
30
+ },
31
+ "main": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "test": "vitest run",
42
+ "dev": "vitest",
43
+ "ci": "npm run build && npm run check-format && npm run test",
44
+ "format": "prettier --write .",
45
+ "check-format": "prettier --check .",
46
+ "prepublishOnly": "npm run ci",
47
+ "local-release": "changeset version",
48
+ "release": "changeset version && changeset publish"
49
+ },
50
+ "devDependencies": {
51
+ "@changesets/cli": "^2.27.1",
52
+ "@types/node": "^20.11.0",
53
+ "prettier": "^3.2.4",
54
+ "tsup": "^8.0.1",
55
+ "typescript": "^5.3.3",
56
+ "vitest": "^1.2.2"
57
+ }
58
+ }