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:
hellow-da-sb
2026-09-03 00:48:10 +08:00
commit 8c93bde5f6
30 changed files with 1289 additions and 0 deletions
+146
View File
@@ -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)
}