import { existsSync } from 'node:fs'; import path from 'node:path'; import type { EnvPolicyInput, RiskScanInput } from '../types.js'; import { tryExecFile } from '../core/exec.js'; import { detectRepoType } from '../core/project.js'; import { readTextIfExists, resolveProject, toPosix, workspaceRoot } from '../core/workspace.js'; export function buildEnvPolicy(input: EnvPolicyInput): Record { const project = resolveProject(input.project); const envFiles = ['.env.development', '.env.production', '.env.example'].map((file) => { const absolutePath = path.join(project.path, file); return { exists: existsSync(absolutePath), file, relativePath: toPosix(path.relative(workspaceRoot, absolutePath)), }; }); const gitignore = readTextIfExists(path.join(project.path, '.gitignore')); const ignoredRules = gitignore .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); return { envFiles, ignoredRules: ignoredRules.filter((line) => line.includes('.env')), policy: [ '真实开发和生产配置只保留在本地,不提交。', '.env.example 用于维护变量名和示例值。', '接口、数据库、WordPress、MinIO 等真实账号密码不得写入文档或提交记录。', ], project, warnings: envFiles .filter((file) => file.file !== '.env.example' && file.exists) .map((file) => `${file.file} 存在,提交前确认已被 .gitignore 忽略。`), }; } export async function scanTaskRisk(input: RiskScanInput): Promise> { const project = resolveProject(input.project); let changedFiles = input.changedFiles || []; if (changedFiles.length === 0 && detectRepoType(project.path) === 'git') { const status = await tryExecFile('git', ['status', '--short'], project.path); if (status.ok) { changedFiles = status.stdout .split(/\r?\n/) .map((line) => line.slice(3).trim()) .filter(Boolean); } } const risks: Array<{ file: string; level: 'high' | 'medium'; reason: string; suggestion: string; }> = []; const addRisk = ( level: 'high' | 'medium', file: string, reason: string, suggestion: string, ) => { risks.push({ file, level, reason, suggestion }); }; for (const file of changedFiles) { const normalized = file.replaceAll('\\', '/'); if (/\.env\.(development|production)$/.test(normalized)) { addRisk('high', file, '真实环境配置文件有改动。', '确认不会提交真实密钥和密码。'); } if (/package\.json$|pnpm-lock\.yaml$|package-lock\.json$|yarn\.lock$/.test(normalized)) { addRisk('medium', file, '依赖或脚本发生变化。', '确认锁文件与 package.json 同步,并运行对应轻量验证。'); } if (normalized.includes('/components/ktTable/')) { addRisk( 'medium', file, 'KtTable 是高频核心组件,布局和滚动性能容易回归。', '保持原生 antdv-next 表格能力,避免监听 scroll/resize 做高频状态更新。', ); } if (/src\/admin\/|src\/.*controller|src\/.*service|src\/.*dto/.test(normalized)) { addRisk('medium', file, '后端接口或业务逻辑改动。', '本地启动或复用服务后真实调用对应接口。'); } if (/Jenkinsfile|deploy\/|k8s\/|nginx.*\.conf/.test(normalized)) { addRisk('medium', file, '部署链路配置改动。', '只验证配置语法或生成产物,不主动触发远程部署。'); } } return { changedFiles, project, risks, safeDefaults: [ '不覆盖用户已有改动。', '只运行必要验证。', '提交前重新检查 status 和 diff。', ], }; }