@bushizhanghan/dsh-publish-gate 0.3.2 → 0.3.4
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 +2 -2
- package/lib/engine.js +54 -2
- package/lib/index.js +7 -1
- package/package.json +1 -1
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 只有这一条能看见。默认不调——有些工具会开程序或写文件,你点名才算同意。超时单独算 `tool-call-slow`(warn),不跟「真的抛了」混在一起:`of_env` 这种要起 WSL 的工具本来就慢。 |
|
|
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 #
|
|
186
|
+
node --test # 67 个引擎单测
|
|
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,29 @@ 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
|
-
|
|
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 budget = Number(process.env.PG_CALL_MS || 8000)
|
|
1083
|
+
const calls = []
|
|
1084
|
+
for (const entry of registered) {
|
|
1085
|
+
if (!wanted.has(entry.name) || !entry.run) continue
|
|
1086
|
+
try {
|
|
1087
|
+
await Promise.race([
|
|
1088
|
+
Promise.resolve(entry.run({}, { signal: undefined })),
|
|
1089
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timed out')), budget)),
|
|
1090
|
+
])
|
|
1091
|
+
calls.push({ name: entry.name, ok: true })
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
const message = String((error && error.message) || error)
|
|
1094
|
+
// A slow tool is not a broken tool: of_env shells out to WSL and takes
|
|
1095
|
+
// seconds by design. Only a real throw is a defect.
|
|
1096
|
+
calls.push({ name: entry.name, ok: false, timedOut: message === 'timed out', error: message })
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
emit({ ok: true, tools: registered.map(({ run, ...rest }) => rest), badParamTypes, calls })
|
|
1077
1100
|
} catch (error) {
|
|
1078
1101
|
emit({ ok: false, stage, error: String((error && error.message) || error) })
|
|
1079
1102
|
}
|
|
@@ -1088,6 +1111,11 @@ export async function checkToolRegistrations(dir, opts = {}) {
|
|
|
1088
1111
|
timeout: opts.timeoutMs ?? 20_000,
|
|
1089
1112
|
maxBuffer: 4 * 1024 * 1024,
|
|
1090
1113
|
windowsHide: true,
|
|
1114
|
+
env: {
|
|
1115
|
+
...process.env,
|
|
1116
|
+
PG_CALLS: JSON.stringify(Array.isArray(opts.calls) ? opts.calls : []),
|
|
1117
|
+
PG_CALL_MS: String(opts.callTimeoutMs ?? 8000),
|
|
1118
|
+
},
|
|
1091
1119
|
})
|
|
1092
1120
|
const line = String(stdout).split('\n').filter((l) => l.startsWith('@@PG@@')).pop()
|
|
1093
1121
|
if (!line) throw new Error('the child printed no result')
|
|
@@ -1132,6 +1160,30 @@ export async function checkToolRegistrations(dir, opts = {}) {
|
|
|
1132
1160
|
}
|
|
1133
1161
|
|
|
1134
1162
|
const seen = new Set()
|
|
1163
|
+
for (const c of Array.isArray(payload.calls) ? payload.calls : []) {
|
|
1164
|
+
if (c.ok) continue
|
|
1165
|
+
if (c.timedOut) {
|
|
1166
|
+
out.push(
|
|
1167
|
+
F(
|
|
1168
|
+
'tool-call-slow',
|
|
1169
|
+
'warn',
|
|
1170
|
+
`tool "${c.name}" did not answer within the call budget`,
|
|
1171
|
+
'A slow tool is not a broken one — this may shell out or wait on a service. Reported so a hang is not mistaken for a pass, and NOT counted as a defect.',
|
|
1172
|
+
'Raise callTimeoutMs if the tool is legitimately slow, or check that it returns at all.',
|
|
1173
|
+
),
|
|
1174
|
+
)
|
|
1175
|
+
continue
|
|
1176
|
+
}
|
|
1177
|
+
out.push(
|
|
1178
|
+
F(
|
|
1179
|
+
'tool-call-throws',
|
|
1180
|
+
'error',
|
|
1181
|
+
`tool "${c.name}" throws when called with no arguments`,
|
|
1182
|
+
`${c.error}. Registration never runs execute, so nothing else in this gate can see it — the consumer hits it on the first call.`,
|
|
1183
|
+
'Guard the missing input, or accept that the tool is unusable without arguments and say so in its description.',
|
|
1184
|
+
),
|
|
1185
|
+
)
|
|
1186
|
+
}
|
|
1135
1187
|
for (const t of tools) {
|
|
1136
1188
|
const label = t.name || '(unnamed)'
|
|
1137
1189
|
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.'))
|
|
@@ -1643,7 +1695,7 @@ const RULE_ORDER = [
|
|
|
1643
1695
|
'inject-missing-tools', 'inject-missing-web', 'inject-missing-webServer', 'inject-missing-timer',
|
|
1644
1696
|
'inject-unused-service',
|
|
1645
1697
|
'tool-missing-name', 'tool-duplicate-name', 'tool-missing-execute', 'tool-present-call-throws',
|
|
1646
|
-
'tool-bad-card-kind', 'tool-missing-output-schema', 'tool-none-registered', 'tool-bad-parameter-type',
|
|
1698
|
+
'tool-bad-card-kind', 'tool-missing-output-schema', 'tool-none-registered', 'tool-bad-parameter-type', 'tool-call-throws', 'tool-call-slow',
|
|
1647
1699
|
'lib-debugger', 'lib-console-log', 'lib-hardcoded-path', 'lib-url-pathname', 'node-too-old', 'stale-build', 'hand-edited-build',
|
|
1648
1700
|
'pack-omits-lib', 'pack-omits-patch', 'pack-omits-client', 'pack-oversize', 'pack-omits-readme', 'pack-omits-license', 'pack-ships-secret',
|
|
1649
1701
|
'pack-unexpected-dir', 'pack-script-build', 'pack-unavailable',
|
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.
|
|
3
|
+
"version": "0.3.4",
|
|
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",
|