8c93bde5f6
- core: registerLayout/Widget/Action(泛型)/Schema + 校验闸门 - definePage: 页面只管业务,结构来自注册表 - presets: admin / app-shell 预设布局(vr add 物化) - cli: vr list/add/update 防覆盖 - examples/qa-orchestrator: 实战编排验证(workspace:* 依赖)
147 lines
4.7 KiB
JavaScript
147 lines
4.7 KiB
JavaScript
#!/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)
|
|
}
|