UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

153 lines (152 loc) 6.92 kB
/** * FileProtection — 非 AutoSnippet 独有文件的写入保护 * * 保护逻辑: * - 如果目标文件不存在 → 直接写入(首次生成) * - 如果目标文件存在且包含 AutoSnippet 签名 → 允许覆盖(我们生成的) * - 如果目标文件存在但不包含 AutoSnippet 签名 → 拒绝覆盖(用户原有文件) * * 签名标记(任意一个匹配即视为 AutoSnippet 所有): * - "Auto-generated by AutoSnippet" * - "Auto-generated by [AutoSnippet]" * - "auto-generated by autosnippet" (case-insensitive) */ import fs from 'node:fs'; import path from 'node:path'; /** * AutoSnippet 文件签名模式(case-insensitive) * 检查文件前 1024 字节即可——签名总在文件头部 * 匹配以下任一标记: * - "Auto-generated by AutoSnippet" / "Auto-generated by [AutoSnippet]" * - "<!-- autosnippet:begin -->" (模板起始标记) */ const SIGNATURE_PATTERN = /auto-generated by (?:\[)?autosnippet(?:\])?|autosnippet:begin/i; /** * 检查文件是否可以被 AutoSnippet 安全写入 * * @param filePath 目标文件绝对路径 * @returns } * - canWrite: true → 可以安全写入 * - canWrite: false → 文件存在且非 AutoSnippet 生成,不应覆盖 */ export function checkWriteSafety(filePath) { if (!fs.existsSync(filePath)) { return { canWrite: true, reason: 'file-not-exist' }; } try { // 只读前 1024 字节检测签名,避免大文件全量读取 const fd = fs.openSync(filePath, 'r'); const buf = Buffer.alloc(1024); const bytesRead = fs.readSync(fd, buf, 0, 1024, 0); fs.closeSync(fd); const header = buf.toString('utf8', 0, bytesRead); if (SIGNATURE_PATTERN.test(header)) { return { canWrite: true, reason: 'autosnippet-owned' }; } return { canWrite: false, reason: 'user-owned' }; } catch { // 读取失败(权限等问题),保守处理:不覆盖 return { canWrite: false, reason: 'read-error' }; } } /** * 安全写入文件 — 带保护机制 * * @param filePath 目标文件绝对路径 * @param content 文件内容 * @param [options.force=false] 强制覆盖(忽略保护) * @param [options.logger] 日志器 * @returns } */ export function safeWriteFile(filePath, content, options = {}) { const { force = false, logger } = options; if (force) { fs.writeFileSync(filePath, content, 'utf8'); return { written: true, reason: 'force', filePath }; } const { canWrite, reason } = checkWriteSafety(filePath); if (canWrite) { fs.writeFileSync(filePath, content, 'utf8'); return { written: true, reason, filePath }; } // 用户原有文件 → 跳过 logger?.info?.(`[FileProtection] Skipped "${filePath}" — ${reason} (file exists and is not AutoSnippet-generated)`); return { written: false, reason, filePath }; } /** * 安全复制文件 — 带保护机制 * * @param srcPath 源文件路径 * @param destPath 目标文件路径 * @param [options.force=false] 强制覆盖 * @param [options.logger] 日志器 * @returns } */ export function safeCopyFile(srcPath, destPath, options = {}) { const { force = false, logger } = options; if (force) { fs.copyFileSync(srcPath, destPath); return { written: true, reason: 'force', filePath: destPath }; } const { canWrite, reason } = checkWriteSafety(destPath); if (canWrite) { fs.copyFileSync(srcPath, destPath); return { written: true, reason, filePath: destPath }; } logger?.info?.(`[FileProtection] Skipped "${destPath}" — ${reason} (file exists and is not AutoSnippet-generated)`); return { written: false, reason, filePath: destPath }; } // ─── Section Merge ────────────────────────────────── const SECTION_BEGIN = '<!-- autosnippet:begin -->'; const SECTION_END = '<!-- autosnippet:end -->'; const SECTION_PATTERN = /<!-- autosnippet:begin -->[\s\S]*?<!-- autosnippet:end -->/; /** * 智能合并 AutoSnippet 管理区段到目标文件 * * 四种场景: * 1. 文件不存在 → 创建完整文件(header + markers) * 2. 文件有 begin/end 标记 → 仅替换标记区段(增量更新) * 3. 文件有旧版 AutoSnippet 签名但无标记 → 全量重写并加标记(旧版迁移) * 4. 文件无签名无标记(用户文件)→ 追加标记区段到末尾(共存) */ export function mergeSection(filePath, section, options = {}) { const { header = '', logger } = options; const fileName = path.basename(filePath); // 确保 section 包含 begin/end 标记 const wrappedSection = section.includes(SECTION_BEGIN) ? section : `${SECTION_BEGIN}\n\n${section}\n${SECTION_END}`; // Case 1: 文件不存在 → 创建完整文件 if (!fs.existsSync(filePath)) { const content = header ? `${header}\n${wrappedSection}\n` : `${wrappedSection}\n`; fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, content, 'utf8'); logger?.info?.(`[FileProtection] Created "${fileName}" (new file)`); return { written: true, strategy: 'create', filePath }; } const existing = fs.readFileSync(filePath, 'utf8'); // Case 2: 文件已有 begin/end 标记 → 仅替换标记区段 if (SECTION_PATTERN.test(existing)) { const updated = existing.replace(SECTION_PATTERN, wrappedSection); if (updated === existing) { logger?.info?.(`[FileProtection] "${fileName}" — section unchanged, skipped write`); return { written: false, strategy: 'replace-section', filePath }; } fs.writeFileSync(filePath, updated, 'utf8'); logger?.info?.(`[FileProtection] Updated "${fileName}" (replaced marker section)`); return { written: true, strategy: 'replace-section', filePath }; } // Case 3: 文件有 AutoSnippet 签名但无标记 → 旧版格式,重写并加标记 if (SIGNATURE_PATTERN.test(existing.slice(0, 1024))) { const content = header ? `${header}\n${wrappedSection}\n` : `${wrappedSection}\n`; fs.writeFileSync(filePath, content, 'utf8'); logger?.info?.(`[FileProtection] Rewrote "${fileName}" (legacy → marker format)`); return { written: true, strategy: 'rewrite-legacy', filePath }; } // Case 4: 用户自有文件 → 追加标记区段到末尾 const separator = existing.endsWith('\n') ? '\n' : '\n\n'; fs.writeFileSync(filePath, `${existing}${separator}${wrappedSection}\n`, 'utf8'); logger?.info?.(`[FileProtection] Appended AutoSnippet section to "${fileName}" (user-owned file preserved)`); return { written: true, strategy: 'append-section', filePath }; }