feat: vibe-registry M1 — 万物皆注册框架骨架
- core: registerLayout/Widget/Action(泛型)/Schema + 校验闸门 - definePage: 页面只管业务,结构来自注册表 - presets: admin / app-shell 预设布局(vr add 物化) - cli: vr list/add/update 防覆盖 - examples/qa-orchestrator: 实战编排验证(workspace:* 依赖)
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* vr —— vibe-registry CLI(M1)
|
||||
*
|
||||
* 用法:
|
||||
* vr list 列出包内可用预设布局
|
||||
* vr add <layout> [--force] 物化预设到项目 registry/layouts/(防覆盖)
|
||||
* vr update <layout> [--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 <layout> [--force]')
|
||||
process.exit(1)
|
||||
}
|
||||
cmdAdd(arg, force)
|
||||
break
|
||||
case 'update':
|
||||
if (!arg || arg === '--force') {
|
||||
console.error('用法:vr update <layout> [--force]')
|
||||
process.exit(1)
|
||||
}
|
||||
cmdUpdate(arg, force)
|
||||
break
|
||||
default:
|
||||
console.error('用法:vr list | vr add <layout> [--force] | vr update <layout> [--force]')
|
||||
process.exit(1)
|
||||
}
|
||||
Reference in New Issue
Block a user