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