dsh-app-deploy 0.0.0-stage → 0.2.0
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 +81 -2
- package/client.js +1472 -0
- package/cordis.patch.yml +3 -0
- package/icon.svg +6 -0
- package/index.js +102 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +40 -4
- package/server/config.js +212 -0
- package/server/deployment.js +392 -0
- package/server/health-check.js +39 -0
- package/server/history.js +40 -0
- package/server/local-tools.js +180 -0
- package/server/rpc.js +129 -0
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<path d="M12 3v10"/>
|
|
3
|
+
<path d="M8.5 6.5 12 3l3.5 3.5"/>
|
|
4
|
+
<path d="M4 14v4a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3v-4"/>
|
|
5
|
+
<path d="M8 17h8"/>
|
|
6
|
+
</svg>
|
package/index.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { realpathSync } from 'node:fs'
|
|
3
|
+
import { mkdir } from 'node:fs/promises'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { initConfig } from './server/config.js'
|
|
7
|
+
import { initHistory } from './server/history.js'
|
|
8
|
+
import { handleRpc } from './server/rpc.js'
|
|
9
|
+
import { handleDeploy, killActiveProcess } from './server/deployment.js'
|
|
10
|
+
|
|
11
|
+
// ponytail: linked workspace cannot resolve dsh's own packages; resolve from the dsh entry instead
|
|
12
|
+
function hostRequire() {
|
|
13
|
+
const local = createRequire(import.meta.url)
|
|
14
|
+
try {
|
|
15
|
+
local.resolve('@deepseek-ai/dsh-tools')
|
|
16
|
+
return local
|
|
17
|
+
} catch {}
|
|
18
|
+
if (!process.argv[1]) throw new Error('找不到 dsh 安装目录')
|
|
19
|
+
return createRequire(realpathSync(process.argv[1]))
|
|
20
|
+
}
|
|
21
|
+
const host = hostRequire()
|
|
22
|
+
const schema = host('@deepseek-ai/schemastery')
|
|
23
|
+
const z = schema.z ?? schema
|
|
24
|
+
|
|
25
|
+
export const inject = ['webServer']
|
|
26
|
+
export const Config = z.object({
|
|
27
|
+
dataDir: z.string().default(''),
|
|
28
|
+
defaultProjectPath: z.string().default(''),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const ROUTE = '/api/dsh-app-deploy'
|
|
32
|
+
const ROUTE_DEPLOY = `${ROUTE}/deploy`
|
|
33
|
+
|
|
34
|
+
function readJson(req) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const chunks = []
|
|
37
|
+
req.on('data', (chunk) => chunks.push(chunk))
|
|
38
|
+
req.on('end', () => {
|
|
39
|
+
try {
|
|
40
|
+
resolve(chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {})
|
|
41
|
+
} catch (error) {
|
|
42
|
+
reject(error)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
req.on('error', reject)
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function send(res, payload) {
|
|
50
|
+
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
51
|
+
res.end(JSON.stringify(payload))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveDataDir(config) {
|
|
55
|
+
const custom = typeof config?.dataDir === 'string' ? config.dataDir.trim() : ''
|
|
56
|
+
if (custom) return custom
|
|
57
|
+
return join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'dsh-app-deploy')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function apply(ctx, config) {
|
|
61
|
+
const dataDir = resolveDataDir(config)
|
|
62
|
+
// 延迟初始化:apply 内不 await,首个请求时才读数据目录/初始化配置
|
|
63
|
+
let ready
|
|
64
|
+
const ensure = () => (ready ??= (async () => {
|
|
65
|
+
await mkdir(dataDir, { recursive: true })
|
|
66
|
+
await initConfig({ dataDir, defaultProjectPath: config?.defaultProjectPath || '' })
|
|
67
|
+
await initHistory(dataDir)
|
|
68
|
+
})())
|
|
69
|
+
|
|
70
|
+
ctx.effect(() => {
|
|
71
|
+
const dispose = ctx.webServer.register({
|
|
72
|
+
kind: 'prefix',
|
|
73
|
+
path: ROUTE,
|
|
74
|
+
async handler(req, res) {
|
|
75
|
+
const url = new URL(req.url ?? '/', 'http://dsh.local')
|
|
76
|
+
try {
|
|
77
|
+
await ensure()
|
|
78
|
+
if (req.method === 'GET' && url.pathname === ROUTE_DEPLOY) {
|
|
79
|
+
await handleDeploy(req, res, url)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if (req.method !== 'POST') {
|
|
83
|
+
send(res, { ok: false, error: '只支持 POST' })
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
const body = await readJson(req).catch(() => null)
|
|
87
|
+
send(res, await handleRpc(body))
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (res.headersSent) {
|
|
90
|
+
try { res.end() } catch {}
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
send(res, { ok: false, error: error?.message || '请求失败' })
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
return () => {
|
|
98
|
+
dispose()
|
|
99
|
+
killActiveProcess()
|
|
100
|
+
}
|
|
101
|
+
}, 'dsh-app-deploy: routes')
|
|
102
|
+
}
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-app-deploy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "DSH 插件:按环境编排并执行 Gradle 构建、rsync 同步与 SSH 远程命令的部署面板,实时推送日志并保留执行记录。",
|
|
5
|
+
"repository": "btboys/dsh-app-deploy",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"icon": "./icon.svg",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.js",
|
|
10
|
+
"./client": "./client.js",
|
|
11
|
+
"./locale/*.json": "./locale/*.json",
|
|
12
|
+
"./icon": "./icon.svg",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"index.js",
|
|
17
|
+
"client.js",
|
|
18
|
+
"server",
|
|
19
|
+
"cordis.patch.yml",
|
|
20
|
+
"locale",
|
|
21
|
+
"icon.svg",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node --test",
|
|
26
|
+
"check": "node --check index.js && node --check client.js"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22",
|
|
30
|
+
"dsh": ">=0.2.1-rc.3 <0.3.0"
|
|
31
|
+
},
|
|
32
|
+
"dsh": {
|
|
33
|
+
"bundle": {
|
|
34
|
+
"patch": "./cordis.patch.yml"
|
|
35
|
+
},
|
|
36
|
+
"client": {
|
|
37
|
+
"platform": "web",
|
|
38
|
+
"immediately": true,
|
|
39
|
+
"inject": []
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
package/server/config.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { promises as fsp } from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
// ---------- 状态:数据目录由插件 Config/dataDir 决定,initConfig 时绑定 ----------
|
|
5
|
+
let dataDir = ''
|
|
6
|
+
let envsFile = ''
|
|
7
|
+
let defaultProjectPath = ''
|
|
8
|
+
let envs = []
|
|
9
|
+
|
|
10
|
+
// 固定 4 个模块的显示名与旧 record 键的映射
|
|
11
|
+
const MODULE_NAME = { bsFront: 'bs 前端', bsServer: 'bs 服务端', adminFront: 'admin 前端', adminServer: 'admin 服务端' }
|
|
12
|
+
const MODULE_ORDER = ['bsFront', 'bsServer', 'adminFront', 'adminServer']
|
|
13
|
+
|
|
14
|
+
function defaultModules() {
|
|
15
|
+
return [
|
|
16
|
+
{ id: 'bsFront', name: 'bs 前端', kind: 'front', enabled: true, gradleTask: 'client:client-front:dist', key: '', extraArgs: '', localPath: 'client/client-front/dist/', remotePath: '', postCommand: '', frontEnv: '' },
|
|
17
|
+
{ id: 'bsServer', name: 'bs 服务端', kind: 'server', enabled: true, gradleTask: 'client:client-server:bootJar', key: '', extraArgs: '', localPath: 'client/client-server/build/libs/financial-v5.jar', remotePath: '', postCommand: '', frontEnv: '' },
|
|
18
|
+
{ id: 'adminFront', name: 'admin 前端', kind: 'front', enabled: true, gradleTask: 'admin:admin-front:dist', key: '', extraArgs: '', localPath: 'admin/admin-front/dist/', remotePath: '', postCommand: '', frontEnv: '' },
|
|
19
|
+
{ id: 'adminServer', name: 'admin 服务端', kind: 'server', enabled: true, gradleTask: 'admin:admin-server:bootJar', key: '', extraArgs: '', localPath: 'admin/admin-server/build/libs/financial-admin-v5.jar', remotePath: '', postCommand: '', frontEnv: '' },
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function templateEnv() {
|
|
24
|
+
return { id: '', name: '', note: '', group: '', projectPath: defaultProjectPath, jdkHome: '', expTime: '', ssh: { host: '', port: 22, user: 'root', password: '', options: '' }, modules: defaultModules() }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 把 modules 归一化为数组;record 旧格式自动转数组(id 保持 bsFront 等,name 映射),frontEnv.bs/admin 迁移进对应 front 模块
|
|
28
|
+
function isNewSchema(e) {
|
|
29
|
+
return !!e && typeof e === 'object' && !!e.ssh && typeof e.ssh === 'object'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 把 modules 归一化为数组;record 旧格式自动转数组(id 保持 bsFront 等,name 映射),frontEnv.bs/admin 迁移进对应 front 模块
|
|
33
|
+
function toModArray(mods, fe) {
|
|
34
|
+
const out = []
|
|
35
|
+
if (Array.isArray(mods)) {
|
|
36
|
+
let i = 0
|
|
37
|
+
for (const raw of mods) {
|
|
38
|
+
const id = String(raw?.id || `mod${++i}`)
|
|
39
|
+
out.push({
|
|
40
|
+
id,
|
|
41
|
+
name: String(raw?.name || MODULE_NAME[id] || id),
|
|
42
|
+
kind: (raw?.kind === 'server' ? 'server' : raw?.kind === 'front' ? 'front' : (id.endsWith('Front') ? 'front' : 'server')),
|
|
43
|
+
enabled: raw?.enabled !== false,
|
|
44
|
+
gradleTask: String(raw?.gradleTask || ''),
|
|
45
|
+
key: String(raw?.key || ''),
|
|
46
|
+
extraArgs: String(raw?.extraArgs || ''),
|
|
47
|
+
localPath: String(raw?.localPath || ''),
|
|
48
|
+
remotePath: String(raw?.remotePath || ''),
|
|
49
|
+
postCommand: String(raw?.postCommand || ''),
|
|
50
|
+
healthCheckUrl: String(raw?.healthCheckUrl || '').trim(),
|
|
51
|
+
frontEnv: String(raw?.frontEnv || ''),
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
} else if (mods && typeof mods === 'object') {
|
|
55
|
+
for (const mk of MODULE_ORDER) {
|
|
56
|
+
const m = mods[mk]
|
|
57
|
+
if (!m) continue
|
|
58
|
+
const feText = mk === 'bsFront' ? (fe?.bs || '') : mk === 'adminFront' ? (fe?.admin || '') : ''
|
|
59
|
+
out.push({
|
|
60
|
+
id: mk, name: MODULE_NAME[mk] || mk, kind: mk.endsWith('Front') ? 'front' : 'server',
|
|
61
|
+
enabled: m.enabled !== false,
|
|
62
|
+
gradleTask: String(m.gradleTask || ''), key: String(m.key || ''), extraArgs: String(m.extraArgs || ''),
|
|
63
|
+
localPath: String(m.localPath || ''), remotePath: String(m.remotePath || ''), postCommand: String(m.postCommand || ''),
|
|
64
|
+
frontEnv: feText,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeEnv(e) {
|
|
72
|
+
const env = {
|
|
73
|
+
id: e.id, name: e.name, note: e.note ?? '',
|
|
74
|
+
group: e.group ?? '',
|
|
75
|
+
projectPath: e.projectPath || defaultProjectPath,
|
|
76
|
+
jdkHome: e.jdkHome ?? '',
|
|
77
|
+
expTime: e.expTime ?? '',
|
|
78
|
+
ssh: { host: e.ssh?.host ?? '', port: Number(e.ssh?.port) || 22, user: e.ssh?.user ?? 'root', password: e.ssh?.password ?? '', options: e.ssh?.options ?? '' },
|
|
79
|
+
modules: toModArray(e.modules, { bs: e.frontEnv?.bs, admin: e.frontEnv?.admin }),
|
|
80
|
+
}
|
|
81
|
+
// 迁移(幂等):把 server 模块 extraArgs 里的 -PexpTime=xxx 提取到 env.expTime 并从 extraArgs 移除
|
|
82
|
+
let extracted = ''
|
|
83
|
+
for (const m of env.modules) {
|
|
84
|
+
if (m.kind !== 'server' || !m.extraArgs) continue
|
|
85
|
+
const kept = m.extraArgs.trim().split(/\s+/).filter(p => {
|
|
86
|
+
if (p.startsWith('-PexpTime=')) { if (!extracted) extracted = p.slice(10); return false }
|
|
87
|
+
return true
|
|
88
|
+
})
|
|
89
|
+
m.extraArgs = kept.join(' ')
|
|
90
|
+
}
|
|
91
|
+
if (extracted && !env.expTime) env.expTime = extracted
|
|
92
|
+
return env
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function loadEnvs() {
|
|
96
|
+
let raw = null
|
|
97
|
+
try { raw = JSON.parse(await fsp.readFile(envsFile, 'utf8')) } catch {}
|
|
98
|
+
// 不播种:文件缺失/损坏时初始化为空配置
|
|
99
|
+
if (!Array.isArray(raw)) {
|
|
100
|
+
await fsp.writeFile(envsFile, '[]')
|
|
101
|
+
return []
|
|
102
|
+
}
|
|
103
|
+
let migrated = false
|
|
104
|
+
const out = []
|
|
105
|
+
for (const e of raw) {
|
|
106
|
+
if (isNewSchema(e)) {
|
|
107
|
+
const r = e
|
|
108
|
+
if (!Array.isArray(r.modules) || r.modules.some(m => m?.extraArgs?.toString().includes('-PexpTime='))) migrated = true
|
|
109
|
+
out.push(normalizeEnv(r))
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
migrated = true
|
|
113
|
+
// 旧 schema({id,name,script,...}):保留 id/name/note,其余字段用空模板兜底
|
|
114
|
+
const old = e
|
|
115
|
+
const base = templateEnv()
|
|
116
|
+
out.push(normalizeEnv({ ...base, id: String(old.id || ''), name: String(old.name || base.name), note: String(old.note ?? base.note ?? '') }))
|
|
117
|
+
}
|
|
118
|
+
if (migrated) await fsp.writeFile(envsFile, JSON.stringify(out, null, 2))
|
|
119
|
+
return out
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function initConfig({ dataDir: dir, defaultProjectPath: projectPath = '' } = {}) {
|
|
123
|
+
dataDir = dir || ''
|
|
124
|
+
envsFile = dataDir ? path.join(dataDir, 'envs.json') : ''
|
|
125
|
+
defaultProjectPath = projectPath || ''
|
|
126
|
+
if (!envsFile) throw new Error('未配置数据目录')
|
|
127
|
+
envs = await loadEnvs()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function getEnvs() { return envs }
|
|
131
|
+
|
|
132
|
+
export async function saveEnvs(next = envs) {
|
|
133
|
+
await fsp.writeFile(envsFile, JSON.stringify(next, null, 2))
|
|
134
|
+
envs = next
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getDefaultProjectPath() { return defaultProjectPath }
|
|
138
|
+
|
|
139
|
+
// ---------- 校验 ----------
|
|
140
|
+
const BAD_PATH = /[\s;"'`$&|<>()*?[\]{}!]/ // 禁止空格/引号/; & | ` $ 等
|
|
141
|
+
// 绝对路径:posix 以 / 开头,Windows 为盘符 + \ 或 /(如 C:\jdk、D:/proj)
|
|
142
|
+
const isAbsPath = (p) => /^(\/|[a-zA-Z]:[\\/])/.test(p)
|
|
143
|
+
const BAD_CMD = /["'`;&|<>$()*?[\]{}!]/ // 允许空格,其余同上
|
|
144
|
+
|
|
145
|
+
// 前端 .env 文本:只允许 KEY=VALUE(KEY 为 A-Z0-9_)、空行、# 注释
|
|
146
|
+
export function validateFrontEnv(s) {
|
|
147
|
+
for (const line of s.split('\n')) {
|
|
148
|
+
const t = line.trim()
|
|
149
|
+
if (!t || t.startsWith('#')) continue
|
|
150
|
+
if (!/^[A-Z0-9_]+\s*=/.test(t)) return '前端 .env 只允许 KEY=VALUE 行(KEY 为 A-Z0-9_)或空行/注释行'
|
|
151
|
+
}
|
|
152
|
+
return ''
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 前端 .env 文件路径(仅种子 bsFront/adminFront 有约定路径,供「配置为空时读文件作参考」)
|
|
156
|
+
export function frontEnvFile(e, m) {
|
|
157
|
+
const p = (e.projectPath || defaultProjectPath || '').replace(/[\\/]+$/, '')
|
|
158
|
+
if (!p) return null
|
|
159
|
+
const key = m.key ? '.' + m.key : ''
|
|
160
|
+
if (m.id === 'bsFront') return path.join(p, 'client/client-front', `.env${key}`)
|
|
161
|
+
if (m.id === 'adminFront') return path.join(p, 'admin/admin-front', `.env${key}`)
|
|
162
|
+
return null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function validateEnv(e) {
|
|
166
|
+
const errs = []
|
|
167
|
+
if (!/^[a-z0-9-]+$/.test(e.id)) errs.push('ID 只能是小写字母/数字/连字符')
|
|
168
|
+
if (!e.name.trim()) errs.push('名称必填')
|
|
169
|
+
if (e.group && !/^[\u4e00-\u9fa5A-Za-z0-9 -]{1,20}$/.test(e.group)) errs.push('分组只能含中文/字母/数字/空格/连字符,最长 20 字符')
|
|
170
|
+
if (!isAbsPath(e.projectPath)) errs.push('项目路径必须是绝对路径(如 /opt/proj 或 C:\\proj)')
|
|
171
|
+
if (BAD_PATH.test(e.projectPath)) errs.push('项目路径含非法字符(禁止空格/引号/; & | ` $ 等)')
|
|
172
|
+
if (e.jdkHome && !isAbsPath(e.jdkHome)) errs.push('JDK 路径必须是绝对路径')
|
|
173
|
+
if (e.jdkHome && BAD_PATH.test(e.jdkHome)) errs.push('JDK 路径含非法字符')
|
|
174
|
+
const s = e.ssh
|
|
175
|
+
if (!/^[a-zA-Z0-9.-]+$/.test(s.host)) errs.push('SSH host 非法(仅字母/数字/./-)')
|
|
176
|
+
if (!Number.isInteger(s.port) || s.port < 1 || s.port > 65535) errs.push('SSH port 必须是 1-65535 的整数')
|
|
177
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(s.user)) errs.push('SSH user 非法')
|
|
178
|
+
if (BAD_CMD.test(s.options)) errs.push('SSH 自定义选项含非法字符')
|
|
179
|
+
if (s.password.includes('\n')) errs.push('SSH 密码不能包含换行')
|
|
180
|
+
if (e.expTime && !/^\d{4}-\d{2}-\d{2}$/.test(e.expTime)) errs.push('过期时间必须是 YYYY-MM-DD')
|
|
181
|
+
if (!Array.isArray(e.modules) || !e.modules.length) errs.push('至少需要一个模块')
|
|
182
|
+
const seenId = {}
|
|
183
|
+
for (const m of (e.modules || [])) {
|
|
184
|
+
if (!/^[A-Za-z0-9-]+$/.test(m.id)) errs.push(`模块 ${m.name || m.id}: ID「${m.id}」只能包含字母/数字/连字符`)
|
|
185
|
+
else if (seenId[m.id]) errs.push(`模块 ${m.name || m.id}: ID「${m.id}」重复(环境内须唯一)`)
|
|
186
|
+
seenId[m.id] = true
|
|
187
|
+
if (!m.name.trim()) errs.push('模块显示名必填')
|
|
188
|
+
if (m.kind !== 'front' && m.kind !== 'server') errs.push(`模块 ${m.name || m.id}: kind 必须是 front 或 server`)
|
|
189
|
+
if (m.kind === 'front') {
|
|
190
|
+
const fe = validateFrontEnv(m.frontEnv || '')
|
|
191
|
+
if (fe) errs.push(`${m.name || m.id}: ${fe}`)
|
|
192
|
+
}
|
|
193
|
+
if (m.healthCheckUrl) {
|
|
194
|
+
try {
|
|
195
|
+
const url = new URL(m.healthCheckUrl)
|
|
196
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) throw new Error('invalid URL')
|
|
197
|
+
} catch { errs.push(`${m.name || m.id}: 健康检查 URL 必须是 HTTP/HTTPS 地址,且不能包含用户名或密码`) }
|
|
198
|
+
}
|
|
199
|
+
if (!m.enabled) continue
|
|
200
|
+
if (!/^[a-zA-Z0-9:_-]+$/.test(m.gradleTask)) errs.push(`${m.name || m.id}: gradle 任务非法`)
|
|
201
|
+
if (m.key && !/^[a-zA-Z0-9._-]+$/.test(m.key)) errs.push(`${m.name || m.id}: -Pkey 非法`)
|
|
202
|
+
if (m.extraArgs && BAD_CMD.test(m.extraArgs)) errs.push(`${m.name || m.id}: 额外参数含非法字符`)
|
|
203
|
+
if (!m.localPath) errs.push(`${m.name || m.id}: 本地产物路径必填`)
|
|
204
|
+
else if (BAD_PATH.test(m.localPath)) errs.push(`${m.name || m.id}: 本地产物路径含非法字符`)
|
|
205
|
+
if (!m.remotePath) errs.push(`${m.name || m.id}: 远程目标路径必填`)
|
|
206
|
+
else if (BAD_PATH.test(m.remotePath)) errs.push(`${m.name || m.id}: 远程目标路径含非法字符(禁止空格/引号/; & | \` $ 等)`)
|
|
207
|
+
if (m.postCommand && BAD_CMD.test(m.postCommand)) errs.push(`${m.name || m.id}: 远程命令含非法字符`)
|
|
208
|
+
}
|
|
209
|
+
return errs
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export { MODULE_NAME, MODULE_ORDER, templateEnv, isNewSchema, normalizeEnv }
|