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/server/rpc.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { promises as fsp } from 'node:fs'
|
|
2
|
+
import { getEnvs, saveEnvs, normalizeEnv, isNewSchema, templateEnv, validateEnv, frontEnvFile, getDefaultProjectPath } from './config.js'
|
|
3
|
+
import { getHistory, replaceHistory, HIST_LIMIT } from './history.js'
|
|
4
|
+
import { activeRecord } from './deployment.js'
|
|
5
|
+
import { listJdks, listWorktrees, readGitInfo, browseDirectory, which } from './local-tools.js'
|
|
6
|
+
|
|
7
|
+
// sshpass 无 Windows 版本;警告文案按平台给出可操作的下一步(保存/导入两处共用)
|
|
8
|
+
const SSHPASS_HINT = process.platform === 'win32'
|
|
9
|
+
? '但本机没有 sshpass(Windows 无此工具),部署时无法使用密码认证;请改用密钥认证(SSH 自定义选项填 -i 私钥路径,并清空密码)'
|
|
10
|
+
: '但本机未安装 sshpass,部署时无法使用密码认证。请先执行: brew install hudochenkov/sshpass/sshpass'
|
|
11
|
+
|
|
12
|
+
// JSON-RPC 方法表:与旧 server.ts 的 /api/* 逐一对齐(字段名不变)
|
|
13
|
+
const methods = {
|
|
14
|
+
config: () => {
|
|
15
|
+
// lastBuild: 每个环境最后一次部署的时间与状态(history 按开始时间正序追加,遍历覆盖即为最新)
|
|
16
|
+
const lastBuild = {}
|
|
17
|
+
for (const r of getHistory()) lastBuild[r.envId] = { time: r.startTime, status: r.status }
|
|
18
|
+
return { envs: getEnvs(), defaultProjectPath: getDefaultProjectPath(), lastBuild }
|
|
19
|
+
},
|
|
20
|
+
saveEnv: async (body) => {
|
|
21
|
+
const b = body && typeof body === 'object' ? body : {}
|
|
22
|
+
const env = normalizeEnv(
|
|
23
|
+
isNewSchema(b)
|
|
24
|
+
? b
|
|
25
|
+
: { ...templateEnv(), id: String(b.id || ''), name: String(b.name || ''), note: String(b.note || '') },
|
|
26
|
+
)
|
|
27
|
+
const errs = validateEnv(env)
|
|
28
|
+
if (errs.length) throw new Error(errs.join(';'))
|
|
29
|
+
const envs = getEnvs()
|
|
30
|
+
const i = envs.findIndex(x => x.id === env.id)
|
|
31
|
+
if (i >= 0) envs[i] = env; else envs.push(env)
|
|
32
|
+
await saveEnvs()
|
|
33
|
+
let warning = ''
|
|
34
|
+
if (env.ssh.password && !(await which('sshpass'))) {
|
|
35
|
+
warning = '已保存,' + SSHPASS_HINT
|
|
36
|
+
}
|
|
37
|
+
return { envs: getEnvs(), warning }
|
|
38
|
+
},
|
|
39
|
+
deleteEnv: async ({ id } = {}) => {
|
|
40
|
+
await saveEnvs(getEnvs().filter(x => x.id !== id))
|
|
41
|
+
return { envs: getEnvs() }
|
|
42
|
+
},
|
|
43
|
+
// 配置文件导入:同 ID 覆盖本地,其余追加;任一环境校验失败则整体拒绝(不写盘)
|
|
44
|
+
importEnvs: async ({ content } = {}) => {
|
|
45
|
+
if (typeof content !== 'string' || !content.trim()) throw new Error('配置文件内容为空')
|
|
46
|
+
let data
|
|
47
|
+
try { data = JSON.parse(content) } catch { throw new Error('文件不是合法的 JSON') }
|
|
48
|
+
if (data && !Array.isArray(data) && Array.isArray(data.envs)) data = data.envs
|
|
49
|
+
if (!Array.isArray(data) || !data.length) throw new Error('配置文件中没有环境(应为环境数组)')
|
|
50
|
+
const envs = getEnvs()
|
|
51
|
+
const byId = new Map(envs.map(x => [x.id, x]))
|
|
52
|
+
const order = envs.map(x => x.id)
|
|
53
|
+
const localIds = new Set(order)
|
|
54
|
+
const added = []
|
|
55
|
+
const overwritten = []
|
|
56
|
+
const errs = []
|
|
57
|
+
data.forEach((raw, index) => {
|
|
58
|
+
const b = raw && typeof raw === 'object' ? raw : {}
|
|
59
|
+
const env = normalizeEnv(
|
|
60
|
+
isNewSchema(b)
|
|
61
|
+
? b
|
|
62
|
+
: { ...templateEnv(), id: String(b.id || ''), name: String(b.name || ''), note: String(b.note || '') },
|
|
63
|
+
)
|
|
64
|
+
const list = validateEnv(env)
|
|
65
|
+
if (list.length) { errs.push(`环境 ${env.id || '#' + (index + 1)}: ${list.join(';')}`); return }
|
|
66
|
+
if (!byId.has(env.id)) order.push(env.id)
|
|
67
|
+
const bucket = localIds.has(env.id) ? overwritten : added
|
|
68
|
+
if (!bucket.includes(env.id)) bucket.push(env.id)
|
|
69
|
+
byId.set(env.id, env)
|
|
70
|
+
})
|
|
71
|
+
if (errs.length) throw new Error(errs.join('\n'))
|
|
72
|
+
await saveEnvs(order.map(id => byId.get(id)))
|
|
73
|
+
const touched = new Set([...added, ...overwritten])
|
|
74
|
+
let warning = ''
|
|
75
|
+
if (getEnvs().some(e => touched.has(e.id) && e.ssh.password) && !(await which('sshpass'))) {
|
|
76
|
+
warning = '已导入,' + SSHPASS_HINT
|
|
77
|
+
}
|
|
78
|
+
return { envs: getEnvs(), added, overwritten, warning }
|
|
79
|
+
},
|
|
80
|
+
frontEnv: async ({ env: envId, module: mid } = {}) => {
|
|
81
|
+
const e = getEnvs().find(x => x.id === envId)
|
|
82
|
+
if (!e) throw new Error('未知环境')
|
|
83
|
+
const m = e.modules.find(x => x.id === mid)
|
|
84
|
+
if (!m || m.kind !== 'front') return { text: '' }
|
|
85
|
+
// frontEnv 来源为模块配置;若为空,读对应 .env 文件一次作初始参考展示(绝不写回)
|
|
86
|
+
if (m.frontEnv) return { text: m.frontEnv }
|
|
87
|
+
const p = frontEnvFile(e, m)
|
|
88
|
+
if (!p) return { text: '' }
|
|
89
|
+
try { return { text: await fsp.readFile(p, 'utf8') } } catch { return { text: '' } }
|
|
90
|
+
},
|
|
91
|
+
jdks: () => listJdks(),
|
|
92
|
+
browse: (args) => browseDirectory(args ?? {}),
|
|
93
|
+
git: async ({ env: id, path: sel } = {}) => {
|
|
94
|
+
const e = getEnvs().find(x => x.id === id)
|
|
95
|
+
if (!e || !e.projectPath) return { ok: true, isGit: false, worktrees: [] }
|
|
96
|
+
const worktrees = await listWorktrees(e.projectPath)
|
|
97
|
+
let dir = e.projectPath
|
|
98
|
+
if (sel) {
|
|
99
|
+
if (!worktrees.some(w => w.path === sel)) throw new Error('所选路径不属于该仓库的 worktree')
|
|
100
|
+
dir = sel
|
|
101
|
+
}
|
|
102
|
+
return { ...(await readGitInfo(dir)), worktrees }
|
|
103
|
+
},
|
|
104
|
+
// 执行记录查询:env 按环境过滤(空=全部),limit 条数(默认 50,上限 500),按时间倒序
|
|
105
|
+
history: ({ env: envId, limit } = {}) => {
|
|
106
|
+
const n = Math.min(Math.max(Number(limit) || 50, 1), HIST_LIMIT)
|
|
107
|
+
const list = envId ? getHistory().filter(r => r.envId === envId) : getHistory()
|
|
108
|
+
return list.slice(-n).reverse()
|
|
109
|
+
},
|
|
110
|
+
clearHistory: async () => {
|
|
111
|
+
// 进行中的部署记录保留,其余清空
|
|
112
|
+
const record = activeRecord()
|
|
113
|
+
await replaceHistory(record ? [record] : [])
|
|
114
|
+
return { ok: true }
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function handleRpc(body) {
|
|
119
|
+
try {
|
|
120
|
+
if (!body || typeof body !== 'object') throw new Error('非法请求')
|
|
121
|
+
const method = methods[body.method]
|
|
122
|
+
if (!method) throw new Error('未知方法')
|
|
123
|
+
return { ok: true, value: await method(body.args) }
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return { ok: false, error: error?.message || '请求失败' }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export { methods }
|