#!/usr/bin/env node /** * vr —— vibe-registry CLI(M1) * * 用法: * vr list 列出包内可用预设布局 * vr add [--force] 物化预设到项目 registry/layouts/(防覆盖) * vr update [--force] 对比包内新版,拉 diff(防覆盖,不静默改你的代码) * * 防覆盖原则(v0.5 决策): * - 目标文件已存在且与预设不同(被用户改过)→ 拒绝,绝不静默覆盖 * - 内容相同 → 提示 already up-to-date * - 显式 --force 才允许覆盖(用户自己确认) */ import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'node:fs' import { fileURLToPath } from 'node:url' import { dirname, join, relative } from 'node:path' const root = join(dirname(fileURLToPath(import.meta.url)), '..') const presetsDir = join(root, 'presets', 'layouts') const targetDir = join(process.cwd(), 'registry', 'layouts') function listPresets(): string[] { return readdirSync(presetsDir) .filter((f) => f.endsWith('.ts')) .map((f) => f.replace(/\.ts$/, '')) .sort() } function presetFile(id: string): string | undefined { const f = join(presetsDir, `${id}.ts`) return existsSync(f) && statSync(f).isFile() ? f : undefined } function targetFile(id: string): string { return join(targetDir, `${id}.ts`) } function showDiff(target: string, preset: string, id: string): void { const tLines = target.split(/\r?\n/) const pLines = preset.split(/\r?\n/) const max = Math.max(tLines.length, pLines.length) const diffs: string[] = [] for (let i = 0; i < max; i++) { const a = tLines[i] ?? '' const b = pLines[i] ?? '' if (a !== b) diffs.push(` L${i + 1}: 你的 "${a.slice(0, 60)}" → 新版 "${b.slice(0, 60)}"`) if (diffs.length >= 8) break } console.log(` 差异预览(最多 8 行,${id}.ts):`) diffs.forEach((d) => console.log(d)) } function cmdList(): void { const presets = listPresets() console.log(`可用预设布局(${presets.length}):`) presets.forEach((id) => { const f = presetFile(id)! const head = readFileSync(f, 'utf8').split(/\r?\n/).slice(0, 3).join(' ') console.log(` - ${id}`) console.log(` ${head.replace(/^\/\*\*?\s?/, '').slice(0, 80)}`) }) } function cmdAdd(id: string, force: boolean): void { const src = presetFile(id) if (!src) { console.error(`✗ 未知预设布局:'${id}'`) console.error(` 可用:${listPresets().join(', ')}`) process.exit(1) } const content = readFileSync(src, 'utf8') const dest = targetFile(id) if (existsSync(dest)) { const existing = readFileSync(dest, 'utf8') if (existing === content) { console.log(`• '${id}' 已物化且与预设一致(already up-to-date)`) return } if (!force) { console.error(`✗ 防覆盖:registry/layouts/${id}.ts 已存在且被修改过`) console.error(` 不静默覆盖。选项:`) console.error(` 1) 看差异:vr update ${id} (仅预览,不写文件)`) console.error(` 2) 确认覆盖:vr add ${id} --force`) process.exit(2) } console.log(`⚠ 强制覆盖 registry/layouts/${id}.ts`) } mkdirSync(targetDir, { recursive: true }) writeFileSync(dest, content, 'utf8') console.log(`✓ 已物化预设 '${id}' → ${relative(process.cwd(), dest)}`) console.log(` 从此它是你的源码,可直接修改;升级用 vr update ${id}`) } function cmdUpdate(id: string, force: boolean): void { const src = presetFile(id) if (!src) { console.error(`✗ 未知预设布局:'${id}'`) process.exit(1) } const dest = targetFile(id) if (!existsSync(dest)) { console.log(`• '${id}' 尚未物化,执行 vr add ${id}`) return } const existing = readFileSync(dest, 'utf8') const preset = readFileSync(src, 'utf8') if (existing === preset) { console.log(`• '${id}' 已是最新版`) return } showDiff(existing, preset, id) if (force) { writeFileSync(dest, preset, 'utf8') console.log(`✓ 已更新 ${id}(--force)`) } else { console.log(` 未写文件。确认升级请:vr update ${id} --force`) } } const [cmd, arg, ...rest] = process.argv.slice(2) const force = rest.includes('--force') || arg === '--force' switch (cmd) { case 'list': cmdList() break case 'add': if (!arg || arg === '--force') { console.error('用法:vr add [--force]') process.exit(1) } cmdAdd(arg, force) break case 'update': if (!arg || arg === '--force') { console.error('用法:vr update [--force]') process.exit(1) } cmdUpdate(arg, force) break default: console.error('用法:vr list | vr add [--force] | vr update [--force]') process.exit(1) }