@bushizhanghan/dsh-publish-gate 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,7 +30,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
30
30
  | 工具 | 作用 |
31
31
  |---|---|
32
32
  | `publish_gate` | 全量门禁:清单 + 产物 + 运行时 + 浏览器半边 + 网络,返回 `safeToPublish`(`skipPack` / `skipTools` / `skipClient` / `skipNetwork` 可逐层跳过) |
33
- | `publish_gate_tools` | 只跑运行时层:在子进程里 `apply(ctx)`,看注册了哪些工具、有没有缺 `execute`、重名、`presentCall` 抛错、`card` 用了宿主不认识的值 |
33
+ | `publish_gate_tools` | 只跑运行时层:在子进程里 `apply(ctx)`,看注册了哪些工具、有没有缺 `execute`、重名、`presentCall` 抛错、`card` 用了宿主不认识的值 | 传 `call: ["工具名"]` 可以**真的调一次**(空参数):注册永远不会执行 `execute`,所以「一调就抛 ReferenceError」这类 bug 只有这一条能看见。默认不调——有些工具会开程序或写文件,你点名才算同意。 |
34
34
  | `publish_gate_client` | 只跑浏览器半边:在子进程里假造 `window.__ModuleLoader__`,看 bundle 会不会加载、`apply` 会不会抛、到底往哪个服务注册了东西、`inject` 有没有漏 |
35
35
  | `publish_gate_verify` | 发后校验:`npm pack --dry-run` 的 shasum 和 registry 上的 `dist.shasum` 逐字节比,比版本号猜准 |
36
36
  | `publish_gate_patch` | 只查 `cordis.patch.yml`——「插件装了但工具没出现」的第一个怀疑对象 |
@@ -183,7 +183,7 @@ dsh plugin --profile desktop add @bushizhanghan/dsh-publish-gate
183
183
  ## 开发
184
184
 
185
185
  ```sh
186
- node --test # 65 个引擎单测
186
+ node --test # 66 个引擎单测
187
187
  node test/integration.test.mjs # 26 条集成断言(9 个真实插件作 fixture)
188
188
  ```
189
189
 
package/lib/engine.js CHANGED
@@ -1062,6 +1062,7 @@ ctx.tools = {
1062
1062
  hasExecute: typeof def?.execute === 'function',
1063
1063
  hasOutputSchema: Boolean(def && def.output && def.output.schema),
1064
1064
  cardKind: card,
1065
+ run: typeof def?.execute === 'function' ? def.execute : null,
1065
1066
  })
1066
1067
  walkParams(def?.parameters, typeof def?.name === 'string' ? def.name : '(unnamed)')
1067
1068
  },
@@ -1073,7 +1074,25 @@ try {
1073
1074
  stage = 'apply'
1074
1075
  if (typeof mod.apply !== 'function') throw new Error('lib/index.js does not export apply')
1075
1076
  await mod.apply(ctx, {})
1076
- emit({ ok: true, tools: registered, badParamTypes })
1077
+ // Calling a tool is opt-in: some tools launch applications or write files, so
1078
+ // the caller names the ones it considers safe. A tool that throws here is a
1079
+ // bug the author cannot see any other way - registration never runs execute.
1080
+ stage = 'call'
1081
+ const wanted = new Set(JSON.parse(process.env.PG_CALLS || '[]'))
1082
+ const calls = []
1083
+ for (const entry of registered) {
1084
+ if (!wanted.has(entry.name) || !entry.run) continue
1085
+ try {
1086
+ await Promise.race([
1087
+ Promise.resolve(entry.run({}, { signal: undefined })),
1088
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timed out after 8s')), 8000)),
1089
+ ])
1090
+ calls.push({ name: entry.name, ok: true })
1091
+ } catch (error) {
1092
+ calls.push({ name: entry.name, ok: false, error: String((error && error.message) || error) })
1093
+ }
1094
+ }
1095
+ emit({ ok: true, tools: registered.map(({ run, ...rest }) => rest), badParamTypes, calls })
1077
1096
  } catch (error) {
1078
1097
  emit({ ok: false, stage, error: String((error && error.message) || error) })
1079
1098
  }
@@ -1088,6 +1107,7 @@ export async function checkToolRegistrations(dir, opts = {}) {
1088
1107
  timeout: opts.timeoutMs ?? 20_000,
1089
1108
  maxBuffer: 4 * 1024 * 1024,
1090
1109
  windowsHide: true,
1110
+ env: { ...process.env, PG_CALLS: JSON.stringify(Array.isArray(opts.calls) ? opts.calls : []) },
1091
1111
  })
1092
1112
  const line = String(stdout).split('\n').filter((l) => l.startsWith('@@PG@@')).pop()
1093
1113
  if (!line) throw new Error('the child printed no result')
@@ -1132,6 +1152,18 @@ export async function checkToolRegistrations(dir, opts = {}) {
1132
1152
  }
1133
1153
 
1134
1154
  const seen = new Set()
1155
+ for (const c of Array.isArray(payload.calls) ? payload.calls : []) {
1156
+ if (c.ok) continue
1157
+ out.push(
1158
+ F(
1159
+ 'tool-call-throws',
1160
+ 'error',
1161
+ `tool "${c.name}" throws when called with no arguments`,
1162
+ `${c.error}. Registration never runs execute, so nothing else in this gate can see it — the consumer hits it on the first call.`,
1163
+ 'Guard the missing input, or accept that the tool is unusable without arguments and say so in its description.',
1164
+ ),
1165
+ )
1166
+ }
1135
1167
  for (const t of tools) {
1136
1168
  const label = t.name || '(unnamed)'
1137
1169
  if (!t.name) out.push(F('tool-missing-name', 'error', 'a registered tool has no name', 'The host cannot address a tool without a name.', 'Give it a name.'))
package/lib/index.js CHANGED
@@ -136,12 +136,18 @@ export function apply(ctx, config = {}) {
136
136
  properties: {
137
137
  dir: { type: 'string', description: 'Plugin directory.' },
138
138
  timeoutMs: { type: 'number', description: 'Kill the child after this long (default 20000).' },
139
+ call: {
140
+ type: 'array',
141
+ items: { type: 'string' },
142
+ description:
143
+ 'Tool names to actually call with empty arguments, e.g. ["page_watch_list"]. Off by default: some tools open applications or write files, so you name the ones you consider safe. A tool that throws on {} is a bug no other check can see, because registration never runs execute.',
144
+ },
139
145
  },
140
146
  },
141
147
  async execute(args) {
142
148
  const dir = resolveDir(args.dir)
143
149
  if (!dir) return { ok: false, error: 'no plugin directory given' }
144
- const { findings, tools } = await checkToolRegistrations(dir, { timeoutMs: args.timeoutMs })
150
+ const { findings, tools } = await checkToolRegistrations(dir, { timeoutMs: args.timeoutMs, calls: args.call })
145
151
  return { ok: true, tools, findings, report: renderReport(findings, { title: `runtime gate · ${dir}` }) }
146
152
  },
147
153
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bushizhanghan/dsh-publish-gate",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Helps DSH plugin AUTHORS publish their plugins: a gate that turns every silent failure in the publish path into a deterministic check. Manifest layer — package.json completeness, BOM, YAML/JSON parseability, entry paths that point at no file, a patch the manifest never declares, patch insert correctness, a build whose lib/ is older than src/, inject services that are missing or misspelled, npm scope permissions, GitHub repo and token state, market entry format, LICENSE/README, tests. Artifact layer — runs `npm pack --dry-run` and reads what npm will really ship: code missing from the tarball, an unpacked patch, oversize packages, dev directories. Runtime layer — imports the entry in an isolated child process, calls apply(ctx), and reports tools that never register, missing execute, duplicate names, throwing presentCall and card kinds the host does not render. Browser half — loads the client bundle against a fake window.__ModuleLoader__ and reports a bundle that never loads, returns no apply, throws, registers nothing, or uses a service its own inject array omits, because a panel that never appears looks like a UI bug, not a packaging one. Then GitHub + npm + market, and a post-publish shasum check that proves the registry holds these exact bytes. 帮助插件作者一键发布:写完插件后跑一遍门禁(清单 / 产物 / 运行时 / 浏览器半边),把发布链路上的每个静默坑变成确定性检查,然后 GitHub + npm + 市场一键三连。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",