@avantf/dsh-envinit
v0.1.3
Published
Plugin environment initialisation at startup: declare → probe → fetch → verify → report
Downloads
436
Maintainers
Readme
@avantf/dsh-envinit
插件启动期的环境初始化框架:插件声明它需要什么,框架负责把它装到盘上并报告。
插件在用户机器上启动时往往缺东西——运行期要用的 npm 包、要调用的命令行工具、要读的模型文件。
@avantf/dsh-envinit 让插件只写一份清单,其余交给框架:
声明 → 探测 → 获取 → 校验 → 报告- 已经在盘上 / PATH 上的直接用,不重复下载;
- 需要安装的下载、校验完整性、原子落盘;
- 每一项给出
present / installed / skipped / failed,失败带稳定code。
运行期零依赖,要求 Node.js >= 22。
核心概念
| 概念 | 含义 |
| --- | --- |
| item | 插件声明的一条资源:id / kind / spec / target.root / onMissing / startup / needs |
| kind | 资源种类。内置 npm-package、binary-archive、model-cache;自定义 kind 由 provider 认领 |
| provider | 认领一种 kind、负责这类资源获取与校验的 npm 包 |
| home | 受管族根,默认 ~/.avantf/env;所有受管资源都在它下面 |
| key | 资源身份 kind + name,多个插件声明同一个资源时只落一份 |
怎么用
1. 声明依赖
插件把框架声明为 peer(不要放 dependencies,否则会装出多份框架副本):
{
"peerDependencies": { "@avantf/dsh-envinit": "^0.1.3" },
"devDependencies": { "@avantf/dsh-envinit": "^0.1.3" }
}2. 内联 bootstrap
唯一需要进入插件产物的是框架的零依赖 bootstrap:它只把这份框架解析出来并校验版本,不安装任何东西。
有打包步骤:用框架的构建预设,它会强制内联
./bootstrap、让框架本包保持外部,并在产物写出后断言这两条:import { envinitPreset, assertEnvinitArtifacts, assertEnvinitPresetChecks } from '@avantf/dsh-envinit/preset' const preset = envinitPreset({ external: [/* 自己的外部名白名单 */] }) assertEnvinitPresetChecks(preset.checks) // bundler:external 用 preset.external,内联 preset.noExternal // 产物写出后: assertEnvinitPresetChecks(assertEnvinitArtifacts({ artifacts: ['dist/plugin.js'], clientArtifacts: ['dist/client.js'] }))tsc直出:把框架发布包里的dist/bootstrap.js拷进自己的产物,按相对路径 import。
3. 启动接线
import { fileURLToPath } from 'node:url'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { loadFramework, readDependencyRange } from './bootstrap.js' // 内联进来的那份
import type { Provisioner } from '@avantf/dsh-envinit' // 只类型,会被擦除
export async function mount(logger: { warn(message: string): void }): Promise<void> {
const home = join(homedir(), '.avantf', 'env')
const range = await readDependencyRange(fileURLToPath(import.meta.url)) // 自己的 peerDependencies
const framework = await loadFramework<typeof import('@avantf/dsh-envinit')>({ logger })
if (framework === undefined) {
logger.warn('预装设施不可用,降级挂载') // 拿不到框架也照常挂载
return
}
const provisioner: Provisioner = framework.createProvisioner({ home, logger, envinitRange: range })
provisioner.register(framework.npmPackageProvider())
provisioner.register(framework.binaryArchiveProvider())
provisioner.register(framework.modelCacheProvider())
provisioner.declare(manifest)
// 挂载前必须就绪的项:阻塞等待
await provisioner.ensure({ only: ['my-plugin:compat'], deadlineMs: 30_000 })
// 其余项(含 background)后台预装,完成时回调
void provisioner.ensure({
onSettled: entry => {
if (entry.id === 'my-plugin:model' && entry.action === 'installed') useModel()
if (entry.action === 'failed') logger.warn(`${entry.id}: ${entry.code}`)
},
})
}4. 写清单
{
"plugin": "my-plugin",
"items": [
{
"id": "my-plugin:compat",
"kind": "npm-package",
"spec": { "name": "@scope/compat", "range": "^1.0.0" },
"target": { "root": "runtime" },
"startup": "blocking",
"schemaVersion": 1
},
{
"id": "my-plugin:tool",
"kind": "binary-archive",
"spec": {
"id": "tool", "version": "1.2.3",
"packs": { "linux-x64": { "url": "https://…/tool.tar.gz", "sha256": "<hex>" } }
},
"target": { "root": "tools" },
"onMissing": { "atStartup": "degrade", "atUse": "error" },
"schemaVersion": 1
},
{
"id": "my-plugin:model",
"kind": "model-cache",
"spec": { "repo": "org/name", "revision": "main", "layout": "flat", "files": ["config.json"] },
"target": { "root": "models" },
"startup": "background",
"schemaVersion": 1
}
]
}startup:blocking(缺省,ensure等它)或background(派发后不占预算)。onMissing.atStartup:degrade(缺了也挂载)/refuse;onMissing.atUse:degrade/error。needs:同一插件内的其它 item id。
model-cache 的 spec.layout 决定文件怎么落盘,两种取值:
| layout | 落盘形态 | 谁读 |
| --- | --- | --- |
| hub(缺省) | <root>/models--<org>--<name>/{blobs,refs,snapshots/<sha>/<file>} 的内容寻址快照树 | 认这套缓存形状的客户端 |
| flat | <root>/<org>/<name>/<file>,文件直接可读;revision 记录与 blob 在 <root>/.envinit/ 侧车目录 | 直接按 <repo>/<file> 路径读取的运行时 |
两种布局共用同一套 revision 解析、文件列表、模型 hub 端点、内容寻址 blobs 与原子落盘;config.json 在两种布局里都始终必需,spec.requiredFiles 在其上追加。已经落位但内容不对的条目(旧残留、指向错误 blob 的链接、同名目录)会在下次落位时被换成指向正确 blob 的链接。
probe 只做廉价的存在性判断(存在、不是目录、大小 > 0),不联网、不改盘;verify 才证明内容:
hub:snapshot 里的每一项都指向blobs/<sha256>,逐项核对链接目标、blob 名与内容哈希一致;flat:侧车记录里带每个文件的 sha256,逐项核对;只有文件名的旧记录仍然接受,不会把已经装好的树判成未安装。
落位与完成标记(hub 的 refs/<revision>、flat 的 record.json)在同一把族根锁内完成,所以多个进程安装不同 revision 时,记录与盘上的字节始终一致。
只增不减:受管的模型数据不会回收。
blobs与snapshots永不删除,换一个 revision 就会再存一份;experimental().prune()只是打一条告警的 no-op。
endpoint 默认是内置的模型 hub;policy.mirrors.model 可以给一组镜像端点(按序尝试,内置端点兜底),spec.endpoint 优先于镜像。显式给出空白的 endpoint、非法的 revision(空、.、..、绝对路径、反斜杠、控制字符)都会报 invalid-option;端点返回的 sha 必须是 40/64 位小写十六进制,内容长度与声明不符的下载会报 fetch/failed。例如:
framework.createProvisioner({
home,
logger,
policy: { mirrors: { archive: [], model: ['https://mirror.example'] } },
})清单可以离线检查:
provision lint manifest.json5. 取用就绪资源
const state = provisioner.resolve('my-plugin:tool')
if (state.state === 'ready') {
const entry = state.handle.dir // 可用入口目录(归档:可执行文件所在目录)
const env = state.handle.env // PATH 等环境增量
}resolve() 的五个状态:ready / pending / failed / skipped / missing。
ensure() 返回的报告里 ok 不把"还在后台装"算成失败。
6. 写 provider(可选)
新增资源种类 = 新增一个 provider(不动核心)。provider 是实现五个动作的 npm 包:
import type { Provider } from '@avantf/dsh-envinit'
export const myProvider: Provider = {
id: '@scope/my-provider',
kinds: ['@scope/my-kind'],
identify: item => ({ name: '…', range: '…' }),
probe: async (item, ctx) => ({ found: false }), // 只读、不联网
plan: () => ({ action: 'install' }), // 纯函数
targetDir: (_item, ref) => `${ref.name}/${ref.segment}`, // 安全相对路径
install: async (item, ctx) => ctx.publish(await ctx.stage(), { name: '…', version: '…' }),
verify: async (item, resolved, ctx) => { /* 证明能用 */ },
}用一致性套件跑一遍再发布:
import { assertProviderConformance, runProviderConformance } from '@avantf/dsh-envinit/conformance'
const report = await runProviderConformance({ provider: myProvider, items: [...], packageName: '@scope/my-provider' })
assertProviderConformance(report)边界(不做的事)
- 不做包管理器:不推导传递依赖、不生成 lockfile、不做版本求解;每个 item 自包含。
- 不接管原生模块:
binding.gyp/*.node/ 依赖postinstall的包会被识别并拒绝。 - 不跳过校验:npm 包核对
dist.integrity,归档核对sha256。 - 不做进程内预热:让文件在盘上是框架的事,读进内存是插件自己的事。
- 框架自身零运行期依赖:
dependencies与peerDependencies均为空。
开发
pnpm check # 类型检查 + 测试
pnpm build # tsc → dist/
pnpm pack # 打包到 release/ 并断言产物
pnpm release:check # 发布门禁:typecheck → build → test → pack