dsh-app-deploy 0.2.0 → 0.3.1
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 +6 -2
- package/client.js +346 -117
- package/index.js +9 -2
- package/package.json +2 -2
- package/server/config.js +87 -28
- package/server/deployment.js +151 -36
- package/server/local-tools.js +71 -1
- package/server/rpc.js +11 -4
- package/server/tools.js +138 -0
package/server/rpc.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { promises as fsp } from 'node:fs'
|
|
2
2
|
import { getEnvs, saveEnvs, normalizeEnv, isNewSchema, templateEnv, validateEnv, frontEnvFile, getDefaultProjectPath } from './config.js'
|
|
3
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'
|
|
4
|
+
import { activeRecord, testSsh } from './deployment.js'
|
|
5
|
+
import { listJdks, listNodes, listPackageManagers, listWorktrees, readGitInfo, browseDirectory, which } from './local-tools.js'
|
|
6
6
|
|
|
7
7
|
// sshpass 无 Windows 版本;警告文案按平台给出可操作的下一步(保存/导入两处共用)
|
|
8
8
|
const SSHPASS_HINT = process.platform === 'win32'
|
|
@@ -31,7 +31,7 @@ const methods = {
|
|
|
31
31
|
if (i >= 0) envs[i] = env; else envs.push(env)
|
|
32
32
|
await saveEnvs()
|
|
33
33
|
let warning = ''
|
|
34
|
-
if (env.
|
|
34
|
+
if ((env.sshs || []).some(s => s.password) && !(await which('sshpass'))) {
|
|
35
35
|
warning = '已保存,' + SSHPASS_HINT
|
|
36
36
|
}
|
|
37
37
|
return { envs: getEnvs(), warning }
|
|
@@ -72,7 +72,7 @@ const methods = {
|
|
|
72
72
|
await saveEnvs(order.map(id => byId.get(id)))
|
|
73
73
|
const touched = new Set([...added, ...overwritten])
|
|
74
74
|
let warning = ''
|
|
75
|
-
if (getEnvs().some(e => touched.has(e.id) && e.
|
|
75
|
+
if (getEnvs().some(e => touched.has(e.id) && (e.sshs || []).some(s => s.password)) && !(await which('sshpass'))) {
|
|
76
76
|
warning = '已导入,' + SSHPASS_HINT
|
|
77
77
|
}
|
|
78
78
|
return { envs: getEnvs(), added, overwritten, warning }
|
|
@@ -89,6 +89,13 @@ const methods = {
|
|
|
89
89
|
try { return { text: await fsp.readFile(p, 'utf8') } } catch { return { text: '' } }
|
|
90
90
|
},
|
|
91
91
|
jdks: () => listJdks(),
|
|
92
|
+
nodes: () => listNodes(),
|
|
93
|
+
packageManagers: () => listPackageManagers(),
|
|
94
|
+
// 测试 SSH 连通性:target 直接用表单当前值(无需先保存环境)
|
|
95
|
+
sshTest: async ({ target } = {}) => {
|
|
96
|
+
if (!target || typeof target !== 'object') throw new Error('缺少测试目标')
|
|
97
|
+
return testSsh(target)
|
|
98
|
+
},
|
|
92
99
|
browse: (args) => browseDirectory(args ?? {}),
|
|
93
100
|
git: async ({ env: id, path: sel } = {}) => {
|
|
94
101
|
const e = getEnvs().find(x => x.id === id)
|
package/server/tools.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// AI 可调用的部署工具:列出环境、执行部署、查执行记录、测试 SSH 连接
|
|
2
|
+
// 密码绝不暴露给模型(只返回 hasPassword 标记);密钥路径是本机路径,可返回
|
|
3
|
+
import { getEnvs } from './config.js'
|
|
4
|
+
import { runDeploy, testSsh, activeRecord } from './deployment.js'
|
|
5
|
+
import { getHistory } from './history.js'
|
|
6
|
+
|
|
7
|
+
// 环境摘要:供模型决定部署目标;剥离 password
|
|
8
|
+
export function envSummaryForModel(e) {
|
|
9
|
+
return {
|
|
10
|
+
id: e.id, name: e.name, note: e.note, group: e.group,
|
|
11
|
+
projectPath: e.projectPath, jdkHome: e.jdkHome || '', nodeHome: e.nodeHome || '', pmPath: e.pmPath || '',
|
|
12
|
+
sshs: (e.sshs || []).map(s => ({
|
|
13
|
+
id: s.id, name: s.name, host: s.host, port: s.port, user: s.user,
|
|
14
|
+
hasPassword: !!s.password, keyFile: s.keyFile || '', options: s.options || '',
|
|
15
|
+
})),
|
|
16
|
+
modules: (e.modules || []).map(m => ({
|
|
17
|
+
id: m.id, name: m.name, kind: m.kind, enabled: m.enabled, buildCmd: m.buildCmd, sshId: m.sshId || '',
|
|
18
|
+
localPath: m.localPath, remotePath: m.remotePath, postCommand: m.postCommand, healthCheckUrl: m.healthCheckUrl || '',
|
|
19
|
+
})),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const jsonRender = (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }]
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 向 ctx.tools 注册 4 个工具。defineTool 由宿主解析(@deepseek-ai/dsh-tools),
|
|
27
|
+
* 插件自身不依赖该包,保证脱离 dsh 宿主时模块仍可加载(如单元测试)。
|
|
28
|
+
* @param {object} ctx cordis Context(需注入 tools 服务)
|
|
29
|
+
* @param {object} defineTool @deepseek-ai/dsh-tools 的 defineTool
|
|
30
|
+
* @param {() => Promise<void>} ensure 插件初始化(数据目录/配置/历史);工具调用不走 HTTP 路由,必须显式等待
|
|
31
|
+
*/
|
|
32
|
+
export function registerTools(ctx, defineTool, ensure) {
|
|
33
|
+
// 每个工具执行前确保初始化完成(幂等,首次才真正读盘)
|
|
34
|
+
const ready = () => (ensure ? ensure() : Promise.resolve())
|
|
35
|
+
ctx.tools.register(defineTool({
|
|
36
|
+
name: 'deploy_list_envs',
|
|
37
|
+
description: 'List deploy environments configured in the dsh-app-deploy plugin: SSH targets (passwords stripped), build modules with their build commands, artifact paths and publish targets. Use before deploy_run to pick env/module IDs.',
|
|
38
|
+
parameters: {},
|
|
39
|
+
output: {
|
|
40
|
+
schema: {
|
|
41
|
+
type: 'object', additionalProperties: false,
|
|
42
|
+
properties: {
|
|
43
|
+
active: { type: 'object', additionalProperties: true }, // 进行中的部署(无则不返回)
|
|
44
|
+
envs: { type: 'array', required: true, items: { type: 'object', additionalProperties: true } },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
render: jsonRender,
|
|
48
|
+
},
|
|
49
|
+
execute: async () => {
|
|
50
|
+
await ready()
|
|
51
|
+
const rec = activeRecord()
|
|
52
|
+
return {
|
|
53
|
+
...(rec ? { active: { id: rec.id, env: rec.envName, modules: rec.modules } } : {}),
|
|
54
|
+
envs: getEnvs().map(envSummaryForModel),
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
}))
|
|
58
|
+
|
|
59
|
+
ctx.tools.register(defineTool({
|
|
60
|
+
name: 'deploy_run',
|
|
61
|
+
description: 'Run a deployment for one environment: clean → build each module (generic command: gradlew/mvn/npm…) → rsync/rclone to the module\'s SSH target → remote command → health check. Blocks until finished and returns the exit code plus the full log tail. Fails if another deployment is running. Omit modules to deploy all enabled modules in config order.',
|
|
62
|
+
parameters: {
|
|
63
|
+
env: { type: 'string', required: true, description: 'Environment ID (from deploy_list_envs)' },
|
|
64
|
+
modules: { type: 'array', items: { type: 'string' }, description: 'Module IDs in desired execution order. Omit for all enabled modules.' },
|
|
65
|
+
},
|
|
66
|
+
output: {
|
|
67
|
+
schema: {
|
|
68
|
+
type: 'object', additionalProperties: false,
|
|
69
|
+
properties: {
|
|
70
|
+
code: { type: 'integer', required: true },
|
|
71
|
+
status: { type: 'string', required: true },
|
|
72
|
+
durationMs: { type: 'integer' },
|
|
73
|
+
failedModule: { type: 'string' },
|
|
74
|
+
failedStep: { type: 'string' },
|
|
75
|
+
logTail: { type: 'string', required: true },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
render: jsonRender,
|
|
79
|
+
},
|
|
80
|
+
execute: async (args) => {
|
|
81
|
+
await ready()
|
|
82
|
+
return runDeploy(args.env, args.modules || [])
|
|
83
|
+
},
|
|
84
|
+
}))
|
|
85
|
+
|
|
86
|
+
ctx.tools.register(defineTool({
|
|
87
|
+
name: 'deploy_history',
|
|
88
|
+
description: 'Recent deployment records (newest first): status, duration, per-module failure step, and a truncated log tail for failed runs. Filter by environment ID optionally.',
|
|
89
|
+
parameters: {
|
|
90
|
+
env: { type: 'string', description: 'Environment ID to filter by' },
|
|
91
|
+
limit: { type: 'integer', description: 'Max records, default 10' },
|
|
92
|
+
},
|
|
93
|
+
output: {
|
|
94
|
+
schema: {
|
|
95
|
+
type: 'object', additionalProperties: false,
|
|
96
|
+
properties: {
|
|
97
|
+
records: { type: 'array', required: true, items: { type: 'object', additionalProperties: true } },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
render: jsonRender,
|
|
101
|
+
},
|
|
102
|
+
execute: async (args) => {
|
|
103
|
+
await ready()
|
|
104
|
+
const limit = Math.min(Math.max(Number(args.limit) || 10, 1), 50)
|
|
105
|
+
let list = getHistory().slice()
|
|
106
|
+
if (args.env) list = list.filter(r => r.envId === args.env)
|
|
107
|
+
return { records: list.slice(-limit).reverse() }
|
|
108
|
+
},
|
|
109
|
+
}))
|
|
110
|
+
|
|
111
|
+
ctx.tools.register(defineTool({
|
|
112
|
+
name: 'ssh_test_connection',
|
|
113
|
+
description: 'Test an SSH connection (10s timeout): host key/agent auth via keyFile, or password via sshpass. Returns ok/latency or the server-reported error.',
|
|
114
|
+
parameters: {
|
|
115
|
+
host: { type: 'string', required: true },
|
|
116
|
+
port: { type: 'integer' },
|
|
117
|
+
user: { type: 'string', required: true },
|
|
118
|
+
password: { type: 'string', description: 'SSH password; omit to use keyFile or agent' },
|
|
119
|
+
keyFile: { type: 'string', description: 'Absolute private key path; takes precedence over options -i' },
|
|
120
|
+
options: { type: 'string', description: 'Extra ssh options, e.g. -o StrictHostKeyChecking=no' },
|
|
121
|
+
},
|
|
122
|
+
output: {
|
|
123
|
+
schema: {
|
|
124
|
+
type: 'object', additionalProperties: false,
|
|
125
|
+
properties: {
|
|
126
|
+
ok: { type: 'boolean', required: true },
|
|
127
|
+
ms: { type: 'integer' },
|
|
128
|
+
error: { type: 'string' },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
render: jsonRender,
|
|
132
|
+
},
|
|
133
|
+
execute: async (args) => {
|
|
134
|
+
await ready()
|
|
135
|
+
return testSsh({ host: args.host, port: args.port || 22, user: args.user, password: args.password || '', keyFile: args.keyFile || '', options: args.options || '' })
|
|
136
|
+
},
|
|
137
|
+
}))
|
|
138
|
+
}
|