Repository navigation
Expand file tree
/
Copy pathcheck.mjs
More file actions
129 lines (119 loc) · 6.11 KB
/
Copy pathcheck.mjs
File metadata and controls
129 lines (119 loc) · 6.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// dsh-remote 部署前闸门(Pre-deploy gate)
//
// 在 ./sync.sh 部署前运行,拦截会"把自己搞崩"的源码问题。历史教训:
// v0.6.1 曾把 slash 命令命名为 `remote.forget-key`,而 dsh-commands 框架
// 硬校验命令名必须匹配 /^[a-z][a-z0-9_-]*$/u(不允许点号/大写/空格),
// 导致插件树加载失败、整个 DSH Desktop 无法启动,直到手工修复。
//
// 本脚本是纯文本静态检查(不 import 模块,无需依赖即可运行),只查那些
// 违反会让 boot 崩溃的"框架注册约束"。语法问题由 node --check 兜底,
// 真正的启动冒烟测试见 scripts/boot-smoke.sh。
import { readFileSync, readdirSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
const here = path.dirname(fileURLToPath(import.meta.url))
const src = readFileSync(path.join(here, 'lib', 'index.js'), 'utf8').replace(/\r\n/g, '\n')
const file = path.relative(process.cwd(), path.join(here, 'lib', 'index.js'))
const libSrc = readdirSync(path.join(here, 'lib'))
.filter((n) => n.endsWith('.js'))
.map((n) => ({
file: path.relative(process.cwd(), path.join(here, 'lib', n)),
src: readFileSync(path.join(here, 'lib', n), 'utf8').replace(/\r\n/g, '\n'),
}))
let fail = 0
// ── 1) slash 命令名必须匹配 dsh-commands 的约束 ───────────────────────────
// 依据:安装框架 @deepseek-ai/dsh-commands lib/index.js normalizeDefinition()
const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/u
const regRe = /commands\.register\(\s*\{[\s\S]*?\bname:\s*'([^']+)'/g
let m, total = 0
while ((m = regRe.exec(src))) {
total++
if (!COMMAND_NAME.test(m[1])) {
fail++
console.log(` ✗ ${file}:${lineOf(src, m.index)}: command name '${m[1]}' must match ${COMMAND_NAME} (no dots/uppercase/spaces)`)
}
}
console.log(` command-name lint: ${total} register(s) checked`)
// ── 2) 常见 boot 崩溃点:禁止点号注册名(其它 register 也套同正则,防类名误用)──
const otherRe = /\.register\(\{\s*name:\s*'([^']*)'[\s\S]*?\b(?:route|event|service)\b/g
while ((m = otherRe.exec(src))) {
if (!COMMAND_NAME.test(m[1])) {
fail++
console.log(` ✗ ${file}:${lineOf(src, m.index)}: suspicious dotted/illegal registration name '${m[1]}'`)
}
}
// ── 3) 工具名必须 rw_ 前缀(避免与内置工具命名空间冲突)──────────────────
const TOOL_NAME = /^rw_[a-z][a-z0-9_]*$/u
const toolRe = /defineTool\(\{\s*name:\s*'([^']+)'/g
let tTotal = 0
while ((m = toolRe.exec(src))) {
tTotal++
if (!TOOL_NAME.test(m[1])) {
fail++
console.log(` ✗ ${file}:${lineOf(src, m.index)}: tool name '${m[1]}' must match ${TOOL_NAME}`)
}
}
console.log(` tool-name lint: ${tTotal} defineTool(s) checked`)
// ── 4) JSON 路由必须挂在 /dsh-remote/ 前缀下(防路由命名漂移)─────────────
const ROUTE_PREFIX = /^\/dsh-remote\//u
const routeRe = /path:\s*'(\/[^']*)'/g
let rTotal = 0
for (const { file: rf, src: rs } of libSrc) {
if (!/index\.js$|routes-/.test(rf)) continue
while ((m = routeRe.exec(rs))) {
rTotal++
if (!ROUTE_PREFIX.test(m[1])) {
fail++
console.log(` ✗ ${rf}:${lineOf(rs, m.index)}: route path '${m[1]}' must start with /dsh-remote/`)
}
}
}
console.log(` route-prefix lint: ${rTotal} route(s) checked`)
// ── 5) defineTool 参数里 type:'object' 必须显式 additionalProperties ──────
// 依据:@deepseek-ai/dsh-tools runSchemaCompiler() case "object":
// 没有显式 boolean additionalProperties 直接 authorError,导致插件树加载失败
// (用户 dsh 0.1.0-rc.8 启动崩溃,issue #9)。只在 defineTool({...}) 参数块内检查。
const toolBlocks = [...src.matchAll(/defineTool\(\{[\s\S]*?\n \}\)/g)].map(m => m[0])
let sTotal = 0
for (const toolBlock of toolBlocks) {
const paramsMatch = toolBlock.match(/parameters:\s*\{([\s\S]*?)\n \},\n output:/)
if (!paramsMatch) continue
const paramsBody = paramsMatch[1]
for (const m of paramsBody.matchAll(/type:\s*'object'/g)) {
sTotal++
const lineNo = lineOf(src, m.index + src.indexOf(toolBlock))
if (!/additionalProperties:\s*(?:true|false)/.test(paramsBody)) {
fail++
console.log(` ✗ ${file}:${lineNo}: type:'object' param must declare additionalProperties: true|false`)
}
}
}
if (sTotal) console.log(` object-schema lint: ${sTotal} type:'object' param(s) checked`)
else console.log(' object-schema lint: no type:\'object\' params found')
// ── 6) 主题 token 配对:`*-fill` 是背景 token,绝不能当文字色 ─────────────
// 依据:宿主自己的主按钮就是这个配对
// ._primary{background:var(--dsw-alias-button-primary-fill);
// color:var(--dsw-alias-label-primary-foreground)}
// 0.8.20 把 `--dsw-alias-button-contrast-fill`(一个 *fill*)当成了文字色:
// 浅色主题 #61666b on #0f1115 = 3.26:1(:disabled 再降到 1.56:1),
// 深色主题两个 token 同为 #f9fafb ⇒ 文字完全不可见。
// 这里只查「文字色 / onPrimary」两处,`border-color:` 等不受影响。
const clientFile = path.relative(process.cwd(), path.join(here, 'lib', 'client.js'))
const clientSrc = readFileSync(path.join(here, 'lib', 'client.js'), 'utf8').replace(/\r\n/g, '\n')
const FILL_AS_TEXT = [
...clientSrc.matchAll(/(?<![-\w])color:\s*var\((--dsw-alias-[\w-]*fill)/g),
...clientSrc.matchAll(/onPrimary:\s*v\('(--dsw-alias-[\w-]*fill)'/g),
]
for (const mm of FILL_AS_TEXT) {
fail++
console.log(` ✗ ${clientFile}:${lineOf(clientSrc, mm.index)}: '${mm[1]}' is a *fill* token and must not be used as a text colour — pair --dsw-alias-button-primary-fill with --dsw-alias-label-primary-foreground`)
}
console.log(` theme-token lint: ${FILL_AS_TEXT.length} fill-as-text misuse(s) checked`)
function lineOf(text, idx) {
return text.slice(0, idx).split('\n').length
}
if (fail) {
console.error(`\ncheck.mjs FAILED: ${fail} problem(s). 修复后再部署。`)
process.exit(1)
}
console.log('check.mjs OK: no framework-constraint violations.')