Browse Source

fix(release): preview.5 修复发行验收在托管 runner 上的路径授权 (#160)

preview.4 的发行构建在 Windows runner 的隔离加载步骤失败:Node 权限模型把临时目录规范化成长名,按 8.3 短名授予的读取权限不匹配即 ERR_ACCESS_DENIED。现按读写分别授予短名、规范名、尾分隔符与临时目录通配四种拼写。

完整版卸载验收此前必然失败:pnpm remove 没有 --registry 选项,移除 meta 包会重新解析依赖树而去 registry 找尚未发布的主包;改为在隔离 profile 的 npmrc 指向临时本地 registry。版本提升到 0.1.0-preview.5,产品代码与 preview.4 相同。
聆风 4 ngày trước cách đây
mục cha
commit
80f8fa5f1c

+ 9 - 0
CHANGELOG.md

@@ -6,6 +6,15 @@
 
 
 尚无未发布变更。
 尚无未发布变更。
 
 
+## [0.1.0-preview.5] — 2026-09-21
+
+preview.4 的发行构建在 Windows runner 上失败,本版只修发行链路,产品代码与 preview.4 相同。
+
+### Fixed
+
+- **发行验收的 Windows 权限**:隔离加载验收给 Node 的读取授权此前只有一拼写,托管 runner 的临时目录同时存在 8.3 短名(`RUNNER~1`)与规范长名,规范化后不匹配即被拒(`ERR_ACCESS_DENIED`)。现按读写分别授予短名、规范名、尾分隔符与临时目录通配四种拼写,普通权限与托管 runner 都能通过。
+- **完整版卸载验收**:`pnpm remove` 没有 `--registry` 选项,移除 meta 包会重新解析依赖树,于是去 registry 找尚未发布的主包而失败。改为在隔离 profile 的 npmrc 中指向临时本地 registry。
+
 ## [0.1.0-preview.4] — 2026-09-21
 ## [0.1.0-preview.4] — 2026-09-21
 
 
 准备首次 npm 发行的预览版,恢复完整版 meta 包发行并补齐安装验收。
 准备首次 npm 发行的预览版,恢复完整版 meta 包发行并补齐安装验收。

+ 4 - 4
README.md

@@ -2,7 +2,7 @@
 
 
 基于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的长篇小说写作工作台。把构想、设定、大纲、写章、审读、修改、定稿、记忆与导出放进一个能够暂停和继续的工作流程,作品以本地文件保存。
 基于 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的长篇小说写作工作台。把构想、设定、大纲、写章、审读、修改、定稿、记忆与导出放进一个能够暂停和继续的工作流程,作品以本地文件保存。
 
 
-**开发预览版:0.1.0-preview.4。** Windows 首发;重要作品使用前请备份。模型服务由你配置,调用可能产生费用。
+**开发预览版:0.1.0-preview.5。** Windows 首发;重要作品使用前请备份。模型服务由你配置,调用可能产生费用。
 
 
 DSH Scriptor is a local-first fiction writing workspace for DeepSeek Harness. This is the v8 developer preview. Runtime prompts and skills are included; model services are configured by the user.
 DSH Scriptor is a local-first fiction writing workspace for DeepSeek Harness. This is the v8 developer preview. Runtime prompts and skills are included; model services are configured by the user.
 
 
@@ -11,9 +11,9 @@ DSH Scriptor is a local-first fiction writing workspace for DeepSeek Harness. Th
 | 产品线 | 运行环境 | 入口 |
 | 产品线 | 运行环境 | 入口 |
 | --- | --- | --- |
 | --- | --- | --- |
 | v6 | Claude Code 插件 | [v6 文档与安装](https://github.com/lingfengQAQ/webnovel-writer/tree/master) |
 | v6 | Claude Code 插件 | [v6 文档与安装](https://github.com/lingfengQAQ/webnovel-writer/tree/master) |
-| v8(本分支) | DeepSeek Harness 工作台 | [下载预览版](https://github.com/lingfengQAQ/webnovel-writer/releases/tag/scriptor-v0.1.0-preview.4) · [安装教程](docs/user/install.md) |
+| v8(本分支) | DeepSeek Harness 工作台 | [下载预览版](https://github.com/lingfengQAQ/webnovel-writer/releases/tag/scriptor-v0.1.0-preview.5) · [安装教程](docs/user/install.md) |
 
 
-两个版本安装方式不同。当前没有经过验证的 v6/v7 书仓直接迁移方案。`v8` 是产品线名称,`0.1.0-preview.4` 是工作台安装包版本。
+两个版本安装方式不同。当前没有经过验证的 v6/v7 书仓直接迁移方案。`v8` 是产品线名称,`0.1.0-preview.5` 是工作台安装包版本。
 
 
 ## 能做什么
 ## 能做什么
 
 
@@ -33,7 +33,7 @@ DSH Scriptor is a local-first fiction writing workspace for DeepSeek Harness. Th
 3. 在宿主设置中配置主模型与凭据,阅读 [最小配置](docs/user/configuration.md)。
 3. 在宿主设置中配置主模型与凭据,阅读 [最小配置](docs/user/configuration.md)。
 4. 用 [合成练习素材](examples/first-book.md) 走一遍 [第一本书与第一章](docs/user/first-book.md),熟悉确认、定稿和恢复。
 4. 用 [合成练习素材](examples/first-book.md) 走一遍 [第一本书与第一章](docs/user/first-book.md),熟悉确认、定稿和恢复。
 
 
-安装包不需要在用户电脑上编译,也不要求 npm 账号。Release 中的 `linfengqaqtat-dsh-scriptor-0.1.0-preview.4.tgz` 是本地安装备选,使用前核对 SHA256;源码压缩包用于开发。
+安装包不需要在用户电脑上编译,也不要求 npm 账号。Release 中的 `linfengqaqtat-dsh-scriptor-0.1.0-preview.5.tgz` 是本地安装备选,使用前核对 SHA256;源码压缩包用于开发。
 
 
 
 
 ### 可选嵌入提供方
 ### 可选嵌入提供方

+ 4 - 4
docs/user/install.md

@@ -31,20 +31,20 @@ dsh --profile scriptor --dump-config
 
 
 需要主插件和嵌入提供方时,在新的 profile 中把上面的主包安装命令替换为 `dsh plugin --profile scriptor add @linfengqaqtat/dsh-scriptor-full@preview`。完整版默认不开启嵌入 API,也不包含模型额度。两种安装方式选一种,避免组合配置重复加载。
 需要主插件和嵌入提供方时,在新的 profile 中把上面的主包安装命令替换为 `dsh plugin --profile scriptor add @linfengqaqtat/dsh-scriptor-full@preview`。完整版默认不开启嵌入 API,也不包含模型额度。两种安装方式选一种,避免组合配置重复加载。
 
 
-本版精确版本为 `0.1.0-preview.4`;需要固定版本时用 `@0.1.0-preview.4` 替换 `@preview`。公开安装无需 npm 账号。首次创建必须使用 `web` 模板;已有同名 profile 时请换名或先确认用途。
+本版精确版本为 `0.1.0-preview.5`;需要固定版本时用 `@0.1.0-preview.5` 替换 `@preview`。公开安装无需 npm 账号。首次创建必须使用 `web` 模板;已有同名 profile 时请换名或先确认用途。
 
 
 配置应包含 `id: webnovel`;完整版还应包含 `id: webnovel-embeddings`,各一份。安装时宿主共享 peer 的提示不代表需要额外安装 Cordis/React;若实际启动报模块缺失,按排错教程核对宿主版本。完成后进入第 4 步启动。
 配置应包含 `id: webnovel`;完整版还应包含 `id: webnovel-embeddings`,各一份。安装时宿主共享 peer 的提示不代表需要额外安装 Cordis/React;若实际启动报模块缺失,按排错教程核对宿主版本。完成后进入第 4 步启动。
 
 
 ## 3. 离线安装包备选:下载并核对
 ## 3. 离线安装包备选:下载并核对
 
 
-打开 [本版 Release](https://github.com/lingfengQAQ/webnovel-writer/releases/tag/scriptor-v0.1.0-preview.4),下载主包 `linfengqaqtat-dsh-scriptor-0.1.0-preview.4.tgz` 和 `SHA256SUMS`。
+打开 [本版 Release](https://github.com/lingfengQAQ/webnovel-writer/releases/tag/scriptor-v0.1.0-preview.5),下载主包 `linfengqaqtat-dsh-scriptor-0.1.0-preview.5.tgz` 和 `SHA256SUMS`。
 
 
 这里的离线备选指本地插件安装包;首次安装宿主及普通依赖仍可能需要网络。
 这里的离线备选指本地插件安装包;首次安装宿主及普通依赖仍可能需要网络。
 
 
 把包放入你有写权限的**无空格目录**。下面以 `C:/scriptor-dist` 为例;可换成另一处无空格目录。当前宿主在 Windows 转发安装参数时存在空格路径限制。
 把包放入你有写权限的**无空格目录**。下面以 `C:/scriptor-dist` 为例;可换成另一处无空格目录。当前宿主在 Windows 转发安装参数时存在空格路径限制。
 
 
 ```powershell
 ```powershell
-Get-FileHash -Algorithm SHA256 C:/scriptor-dist/linfengqaqtat-dsh-scriptor-0.1.0-preview.4.tgz
+Get-FileHash -Algorithm SHA256 C:/scriptor-dist/linfengqaqtat-dsh-scriptor-0.1.0-preview.5.tgz
 ```
 ```
 
 
 把输出与 SHA256SUMS 对应文件行比较,大小写不影响判断;不一致时重新下载并停止安装。GitHub 自动生成的 Source code 压缩包用于开发,不是插件安装包。
 把输出与 SHA256SUMS 对应文件行比较,大小写不影响判断;不一致时重新下载并停止安装。GitHub 自动生成的 Source code 压缩包用于开发,不是插件安装包。
@@ -53,7 +53,7 @@ Get-FileHash -Algorithm SHA256 C:/scriptor-dist/linfengqaqtat-dsh-scriptor-0.1.0
 
 
 ```powershell
 ```powershell
 dsh --profile scriptor --from-default-profile web --dump-config
 dsh --profile scriptor --from-default-profile web --dump-config
-dsh plugin --profile scriptor add C:/scriptor-dist/linfengqaqtat-dsh-scriptor-0.1.0-preview.4.tgz
+dsh plugin --profile scriptor add C:/scriptor-dist/linfengqaqtat-dsh-scriptor-0.1.0-preview.5.tgz
 dsh --profile scriptor --dump-config
 dsh --profile scriptor --dump-config
 ```
 ```
 
 

+ 2 - 2
docs/user/upgrade-backup.md

@@ -17,13 +17,13 @@
 4. 检查书房入口、技能是否各一份,核对实际书仓状态;先用合成副本试写。
 4. 检查书房入口、技能是否各一份,核对实际书仓状态;先用合成副本试写。
 
 
 ```powershell
 ```powershell
-dsh plugin --profile scriptor add @linfengqaqtat/dsh-scriptor@0.1.0-preview.4
+dsh plugin --profile scriptor add @linfengqaqtat/dsh-scriptor@0.1.0-preview.5
 dsh --profile scriptor --dump-config
 dsh --profile scriptor --dump-config
 ```
 ```
 
 
 上面展示命令形状,更新时换成实际目标版本。不要用同版本不同内容的包覆盖,也不要同时保留开发 file patch。
 上面展示命令形状,更新时换成实际目标版本。不要用同版本不同内容的包覆盖,也不要同时保留开发 file patch。
 
 
-完整版安装对应使用 `@linfengqaqtat/dsh-scriptor-full@0.1.0-preview.4`。不要在保留完整版时另外安装主包;切换安装方式前先移除原组合包。
+完整版安装对应使用 `@linfengqaqtat/dsh-scriptor-full@0.1.0-preview.5`。不要在保留完整版时另外安装主包;切换安装方式前先移除原组合包。
 
 
 ## 回退
 ## 回退
 
 

+ 1 - 1
package.json

@@ -2,7 +2,7 @@
   "name": "webnovel-writer-v8",
   "name": "webnovel-writer-v8",
   "private": true,
   "private": true,
   "type": "module",
   "type": "module",
-  "version": "0.1.0-preview.4",
+  "version": "0.1.0-preview.5",
   "description": "DSH Scriptor: a long-form fiction writing workspace for DeepSeek Harness",
   "description": "DSH Scriptor: a long-form fiction writing workspace for DeepSeek Harness",
   "packageManager": "pnpm@11.27.1",
   "packageManager": "pnpm@11.27.1",
   "scripts": {
   "scripts": {

+ 3 - 3
packages/bundle/README.md

@@ -20,7 +20,7 @@ dsh --profile webnovel --host 127.0.0.1 --port 6104 --no-open
 
 
 ## 更新与卸载
 ## 更新与卸载
 
 
-先停止指定 profile 的运行实例,再安装目标精确版本并重启(本版为 `@linfengqaqtat/dsh-scriptor@0.1.0-preview.4`)。
+先停止指定 profile 的运行实例,再安装目标精确版本并重启(本版为 `@linfengqaqtat/dsh-scriptor@0.1.0-preview.5`)。
 卸载后可执行上面的安装命令重装:
 卸载后可执行上面的安装命令重装:
 
 
 ```powershell
 ```powershell
@@ -28,10 +28,10 @@ dsh plugin --profile webnovel remove @linfengqaqtat/dsh-scriptor
 dsh --profile webnovel --dump-config
 dsh --profile webnovel --dump-config
 ```
 ```
 
 
-卸载插件不会删除作品书仓。安装不需要源码、安装时构建或 npm 账号。Release 提供 `linfengqaqtat-dsh-scriptor-0.1.0-preview.4.tgz` 作为本地安装备选;放在无空格目录并核对 SHA256 后,将安装命令的包名替换为完整文件路径。首次下载宿主依赖仍可能需要网络。
+卸载插件不会删除作品书仓。安装不需要源码、安装时构建或 npm 账号。Release 提供 `linfengqaqtat-dsh-scriptor-0.1.0-preview.5.tgz` 作为本地安装备选;放在无空格目录并核对 SHA256 后,将安装命令的包名替换为完整文件路径。首次下载宿主依赖仍可能需要网络。
 
 
 ## 预览版与公开文档
 ## 预览版与公开文档
 
 
-当前主包 0.1.0-preview.4。完整的安装、配置、首章和备份教程见 [公开使用文档](https://github.com/lingfengQAQ/webnovel-writer/tree/v8/docs/user)。
+当前主包 0.1.0-preview.5。完整的安装、配置、首章和备份教程见 [公开使用文档](https://github.com/lingfengQAQ/webnovel-writer/tree/v8/docs/user)。
 
 
 第三方组件的原始声明见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md),许可证文本随包放在 licenses/。源码及校验和由对应的 GitHub Release 提供。
 第三方组件的原始声明见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md),许可证文本随包放在 licenses/。源码及校验和由对应的 GitHub Release 提供。

+ 1 - 1
packages/bundle/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@linfengqaqtat/dsh-scriptor",
   "name": "@linfengqaqtat/dsh-scriptor",
-  "version": "0.1.0-preview.4",
+  "version": "0.1.0-preview.5",
   "type": "module",
   "type": "module",
   "main": "./lib/index.js",
   "main": "./lib/index.js",
   "exports": {
   "exports": {

+ 1 - 1
packages/meta/README.md

@@ -12,7 +12,7 @@ dsh plugin --profile scriptor add @linfengqaqtat/dsh-scriptor-full@preview
 dsh --profile scriptor --host 127.0.0.1 --port 6104 --no-open
 dsh --profile scriptor --host 127.0.0.1 --port 6104 --no-open
 ```
 ```
 
 
-本版为 `0.1.0-preview.4`,固定版本可使用 `@linfengqaqtat/dsh-scriptor-full@0.1.0-preview.4`。
+本版为 `0.1.0-preview.5`,固定版本可使用 `@linfengqaqtat/dsh-scriptor-full@0.1.0-preview.5`。
 完整版与主包单独安装二选一。已经分别安装两个插件时,先停机并移除旧安装入口,再安装完整版,避免组合配置重复加载。
 完整版与主包单独安装二选一。已经分别安装两个插件时,先停机并移除旧安装入口,再安装完整版,避免组合配置重复加载。
 
 
 嵌入 API 默认关闭,需要在宿主设置中配置模型、维度和凭据。主聊天模型也由用户配置,包不包含模型额度。
 嵌入 API 默认关闭,需要在宿主设置中配置模型、维度和凭据。主聊天模型也由用户配置,包不包含模型额度。

+ 2 - 2
packages/meta/RELEASE_NOTES.md

@@ -1,6 +1,6 @@
-# 完整版 0.1.0-preview.4
+# 完整版 0.1.0-preview.5
 
 
-- 主插件:`@linfengqaqtat/dsh-scriptor@0.1.0-preview.4`。
+- 主插件:`@linfengqaqtat/dsh-scriptor@0.1.0-preview.5`。
 - 嵌入提供方:`webnovel-embedding-provider@0.0.8`。
 - 嵌入提供方:`webnovel-embedding-provider@0.0.8`。
 - `dsh.bundle` 组合配置同时启用两个插件;嵌入模型默认关闭。
 - `dsh.bundle` 组合配置同时启用两个插件;嵌入模型默认关闭。
 - 从 npm 安装:`dsh plugin --profile scriptor add @linfengqaqtat/dsh-scriptor-full@preview`。
 - 从 npm 安装:`dsh plugin --profile scriptor add @linfengqaqtat/dsh-scriptor-full@preview`。

+ 1 - 1
packages/meta/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@linfengqaqtat/dsh-scriptor-full",
   "name": "@linfengqaqtat/dsh-scriptor-full",
-  "version": "0.1.0-preview.4",
+  "version": "0.1.0-preview.5",
   "description": "网文写作工作台完整版(含可选嵌入提供方)",
   "description": "网文写作工作台完整版(含可选嵌入提供方)",
   "keywords": [
   "keywords": [
     "dsh-plugin",
     "dsh-plugin",

+ 13 - 2
scripts/release/install-smoke.mjs

@@ -29,6 +29,17 @@ fs.writeFileSync(path.join(root, 'npmrc'), '')
 const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^(npm_|pnpm_)|TOKEN|API_KEY|SECRET|DSH_|AGENTS_HOME|NODE_PATH/i.test(key)))
 const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !/^(npm_|pnpm_)|TOKEN|API_KEY|SECRET|DSH_|AGENTS_HOME|NODE_PATH/i.test(key)))
 Object.assign(env, { DSH_HOME: home, DSH_AGENTS_HOME: path.join(root, 'agents'), DSH_TELEMETRY_DISABLED: '1', NPM_CONFIG_USERCONFIG: path.join(root, 'npmrc') })
 Object.assign(env, { DSH_HOME: home, DSH_AGENTS_HOME: path.join(root, 'agents'), DSH_TELEMETRY_DISABLED: '1', NPM_CONFIG_USERCONFIG: path.join(root, 'npmrc') })
 const run = (entry, rest, cwd = workspace, timeout = 240000) => execFileSync(process.execPath, [entry, ...rest], { cwd, env, encoding: 'utf8', windowsHide: true, timeout, maxBuffer: 24 * 1024 * 1024 })
 const run = (entry, rest, cwd = workspace, timeout = 240000) => execFileSync(process.execPath, [entry, ...rest], { cwd, env, encoding: 'utf8', windowsHide: true, timeout, maxBuffer: 24 * 1024 * 1024 })
+// Node matches permission resources after canonicalizing them. On a Windows volume with 8.3
+// short names (hosted runners keep that setting) the temp root is also spelled RUNNER~1, so
+// one granted spelling is not enough; grant the short spelling, the canonical one and a
+// wildcard over the temp tree. Other platforms keep the single exact grant.
+const permissionGrants = access => {
+  if (process.platform !== 'win32') return [`--allow-fs-${access}=${root}`]
+  const canonical = (() => { try { return fs.realpathSync.native(root) } catch { return root } })()
+  const spellings = [...new Set([root, canonical, `${root}${path.sep}`, path.join(os.tmpdir(), '*')])]
+  // Node takes one resource per flag; a path-list form is not accepted.
+  return spellings.map(value => `--allow-fs-${access}=${value}`)
+}
 const npmCandidates = [process.env.NPM_CLI_ENTRY, path.join(path.dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js'), path.resolve(path.dirname(process.execPath), '../lib/node_modules/npm/bin/npm-cli.js')].filter(Boolean)
 const npmCandidates = [process.env.NPM_CLI_ENTRY, path.join(path.dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js'), path.resolve(path.dirname(process.execPath), '../lib/node_modules/npm/bin/npm-cli.js')].filter(Boolean)
 const npm = npmCandidates.find(file => fs.existsSync(file))
 const npm = npmCandidates.find(file => fs.existsSync(file))
 assert.ok(npm, 'npm-cli.js was not found; set NPM_CLI_ENTRY to the installed npm CLI')
 assert.ok(npm, 'npm-cli.js was not found; set NPM_CLI_ENTRY to the installed npm CLI')
@@ -72,7 +83,7 @@ try {
   fs.copyFileSync(path.join(sourceRoot, 'packages/bundle/tests/fixtures/packaging-isolation.mjs'), script)
   fs.copyFileSync(path.join(sourceRoot, 'packages/bundle/tests/fixtures/packaging-isolation.mjs'), script)
   const isolatedReport = path.join(root, 'source-isolation.json')
   const isolatedReport = path.join(root, 'source-isolation.json')
   const blocked = path.join(sourceRoot, 'packages/bundle/src/index.ts')
   const blocked = path.join(sourceRoot, 'packages/bundle/src/index.ts')
-  const output = execFileSync(process.execPath, ['--permission', `--allow-fs-read=${root}`, `--allow-fs-write=${root}`, `--allow-fs-read=${path.dirname(process.execPath)}`, script, profile, hostAnchor, blocked, isolatedReport], { cwd: workspace, env, encoding: 'utf8', windowsHide: true, timeout: 45000 })
+  const output = execFileSync(process.execPath, ['--permission', ...permissionGrants('read'), ...permissionGrants('write'), `--allow-fs-read=${path.dirname(process.execPath)}`, script, profile, hostAnchor, blocked, isolatedReport], { cwd: workspace, env, encoding: 'utf8', windowsHide: true, timeout: 45000 })
   log('source-isolation', output)
   log('source-isolation', output)
   assert.equal(JSON.parse(fs.readFileSync(isolatedReport, 'utf8')).ok, true)
   assert.equal(JSON.parse(fs.readFileSync(isolatedReport, 'utf8')).ok, true)
   report.checks.sourceDeniedRealLoader = true
   report.checks.sourceDeniedRealLoader = true
@@ -125,7 +136,7 @@ try {
       assert.equal((config.match(/id: webnovel-embeddings(?:\r?\n|$)/g) ?? []).length, 1)
       assert.equal((config.match(/id: webnovel-embeddings(?:\r?\n|$)/g) ?? []).length, 1)
       log(`${label}-prepare`, run(prepare, [fullProfile, hostAnchor]))
       log(`${label}-prepare`, run(prepare, [fullProfile, hostAnchor]))
       const fullReport = path.join(root, `${label}-isolation.json`)
       const fullReport = path.join(root, `${label}-isolation.json`)
-      log(`${label}-loader`, execFileSync(process.execPath, ['--permission', `--allow-fs-read=${root}`, `--allow-fs-write=${root}`, `--allow-fs-read=${path.dirname(process.execPath)}`, script, fullProfile, hostAnchor, blocked, fullReport, '--embedding'], { cwd: workspace, env, encoding: 'utf8', windowsHide: true, timeout: 45000 }))
+      log(`${label}-loader`, execFileSync(process.execPath, ['--permission', ...permissionGrants('read'), ...permissionGrants('write'), `--allow-fs-read=${path.dirname(process.execPath)}`, script, fullProfile, hostAnchor, blocked, fullReport, '--embedding'], { cwd: workspace, env, encoding: 'utf8', windowsHide: true, timeout: 45000 }))
       assert.equal(JSON.parse(fs.readFileSync(fullReport, 'utf8')).embeddingLoaded, true)
       assert.equal(JSON.parse(fs.readFileSync(fullReport, 'utf8')).embeddingLoaded, true)
     }
     }
     log('add-meta', run(cli, ['plugin', '--profile', fullName, 'add', fullSpec, ...registryArgs]))
     log('add-meta', run(cli, ['plugin', '--profile', fullName, 'add', fullSpec, ...registryArgs]))