autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
153 lines (152 loc) • 6.92 kB
JavaScript
/**
* 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 };
}