dsh-app-deploy 0.1.0 → 0.3.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.
@@ -16,13 +16,22 @@ function run(cmd, args, options = {}) {
16
16
  })
17
17
  }
18
18
 
19
- // 等价旧 Bun.which:找到返回绝对路径,找不到返回 null
19
+ const IS_WIN = process.platform === 'win32'
20
+
21
+ // 等价旧 Bun.which:找到返回绝对路径,找不到返回 null(Windows 用 where)
20
22
  export async function which(command) {
21
- const { code, stdout } = await run('which', [command])
22
- const found = stdout.trim()
23
+ const { code, stdout } = await run(IS_WIN ? 'where' : 'which', [command])
24
+ const found = stdout.split('\n')[0].trim()
23
25
  return code === 0 && found ? found : null
24
26
  }
25
27
 
28
+ // rclone obscure 密码混淆(--sftp-pass 只接受混淆值);失败返回 null
29
+ export async function rcloneObscure(bin, password) {
30
+ const { code, stdout } = await run(bin, ['obscure', password])
31
+ const v = stdout.trim()
32
+ return code === 0 && v ? v : null
33
+ }
34
+
26
35
  // 列出目录所在 git 仓库的全部 worktree(含主工作区);非 git 仓库/失败返回空数组
27
36
  export async function listWorktrees(dir) {
28
37
  const worktrees = []
@@ -98,16 +107,32 @@ export async function listJdks() {
98
107
  for (const v of vers) found.add(path.join(vdir, v, 'libexec/openjdk.jdk/Contents/Home'))
99
108
  }
100
109
  } catch {}
110
+ // Windows:JAVA_HOME + 常见厂商安装目录(Oracle / Temurin / Microsoft / Zulu / Corretto)
111
+ if (IS_WIN) {
112
+ if (process.env.JAVA_HOME) found.add(process.env.JAVA_HOME)
113
+ const roots = [process.env.ProgramFiles, process.env['ProgramFiles(x86)']].filter(Boolean)
114
+ const vendors = ['Java', 'Eclipse Adoptium', 'Eclipse Foundation', 'Microsoft', 'Zulu', 'Amazon Corretto']
115
+ for (const root of roots) {
116
+ for (const vendor of vendors) {
117
+ try { for (const n of await fsp.readdir(path.join(root, vendor))) found.add(path.join(root, vendor, n)) } catch {}
118
+ }
119
+ }
120
+ }
101
121
  // 只保留 bin/java 真实存在者,生成友好短名
102
122
  const jdks = []
103
- for (const p of found) if (await fileExists(`${p}/bin/java`)) {
104
- const m = p.match(/\/JavaVirtualMachines\/([^/]+)\.jdk\/Contents\/Home$/)
123
+ for (const p of found) if (await fileExists(path.join(p, 'bin', IS_WIN ? 'java.exe' : 'java'))) {
124
+ // 标签推导统一用正斜杠视图(Windows 反斜杠路径也能复用同一套正则)
125
+ const pv = p.replace(/\\/g, '/')
126
+ const m = pv.match(/\/JavaVirtualMachines\/([^/]+)\.jdk\/Contents\/Home$/)
105
127
  let label
106
128
  if (m) label = (m[1].match(/\d+(?:\.\d+)*/) || [])[1] || m[1]
107
- else if (p.includes('/opt/homebrew/')) {
108
- const v = (p.match(/openjdk@?(\d+)/) || [])[1] || (p.match(/\/(\d+(?:\.\d+)+)\/libexec/) || [])[1]
129
+ else if (pv.includes('/opt/homebrew/')) {
130
+ const v = (pv.match(/openjdk@?(\d+)/) || [])[1] || (pv.match(/\/(\d+(?:\.\d+)+)\/libexec/) || [])[1]
109
131
  label = v ? 'homebrew-' + v : 'homebrew'
110
- } else label = p.split('/').filter(Boolean).pop()
132
+ } else if (/^[a-zA-Z]:\//.test(pv)) {
133
+ const segs = pv.split('/').filter(Boolean)
134
+ label = segs.pop() || pv
135
+ } else label = pv.split('/').filter(Boolean).pop()
111
136
  jdks.push({ label, path: p })
112
137
  }
113
138
  // 短名去重:同名者追加版本/opt-cellar 段区分
@@ -135,11 +160,78 @@ export async function listJdks() {
135
160
  return out.sort((a, b) => a.label.localeCompare(b.label))
136
161
  }
137
162
 
163
+ // 探测本机包管理器可执行文件(npm/pnpm/yarn),返回 [{label, path}],path 为可执行文件路径
164
+ export async function listPackageManagers() {
165
+ const home = process.env.HOME || process.env.USERPROFILE || ''
166
+ const exts = IS_WIN ? ['.exe', '.cmd', '.ps1', ''] : ['']
167
+ const candidates = []
168
+ const tryBin = async (dir, name) => {
169
+ for (const ext of exts) {
170
+ const p = path.join(dir, name + ext)
171
+ if (await fileExists(p)) { candidates.push({ label: name, path: p }); return }
172
+ }
173
+ }
174
+ // PATH 中的(which 返回可执行文件全路径)
175
+ for (const name of ['npm', 'pnpm', 'yarn']) {
176
+ const w = await which(name).catch(() => '')
177
+ if (w) candidates.push({ label: name, path: w })
178
+ }
179
+ // pnpm/yarn 全局 bin 的常见固定位置(不经 PATH)
180
+ if (home) {
181
+ await tryBin(path.join(home, '.local/share/pnpm'), 'pnpm')
182
+ await tryBin(path.join(home, '.volta/bin'), 'pnpm')
183
+ await tryBin(path.join(home, '.volta/bin'), 'yarn')
184
+ }
185
+ await tryBin('/opt/homebrew/bin', 'pnpm')
186
+ await tryBin('/usr/local/bin', 'pnpm')
187
+ await tryBin('/opt/homebrew/bin', 'yarn')
188
+ // 去重(同路径只留一个)
189
+ const seen = new Set()
190
+ return candidates.filter((c) => (seen.has(c.path) ? false : (seen.add(c.path), true)))
191
+ }
192
+
193
+ // 探测本机已装 Node(nvm / n / fnm / volta / nvm-windows),返回 [{label, path}],path 为版本目录(含 bin/node)
194
+ export async function listNodes() {
195
+ const found = new Set()
196
+ const home = process.env.HOME || process.env.USERPROFILE || ''
197
+ const scan = async (base) => {
198
+ try { for (const n of await fsp.readdir(base)) found.add(path.join(base, n)) } catch {}
199
+ }
200
+ if (home) {
201
+ await scan(path.join(home, '.nvm/versions/node'))
202
+ await scan(path.join(home, '.local/share/fnm/node-versions'))
203
+ await scan(path.join(home, '.volta/tools/image/node'))
204
+ }
205
+ await scan('/usr/local/n/versions/node')
206
+ await scan('/usr/lib/n/versions/node')
207
+ if (IS_WIN && process.env.NVM_HOME) await scan(process.env.NVM_HOME)
208
+ const binName = IS_WIN ? 'node.exe' : 'node'
209
+ const out = []
210
+ for (const p of found) {
211
+ // 支持 <dir>/bin/node、<dir>/node.exe(nvm-windows)、<dir>/installation/bin/node(fnm)
212
+ for (const cand of [p, path.join(p, 'installation')]) {
213
+ if (await fileExists(path.join(cand, 'bin', binName)) || await fileExists(path.join(cand, binName))) {
214
+ const label = path.basename(cand).replace(/^v/, '')
215
+ out.push({ label, path: cand })
216
+ break
217
+ }
218
+ }
219
+ }
220
+ // 版本号倒序,新者在前
221
+ out.sort((a, b) => b.label.localeCompare(a.label, undefined, { numeric: true }))
222
+ return out
223
+ }
224
+
138
225
  // 目录浏览:非法/不可读抛出错误,由 RPC 层转成 ok:false
139
226
  export async function browseDirectory(args = {}) {
140
227
  let p = typeof args.path === 'string' ? args.path : ''
141
228
  const showHidden = args.hidden === true || args.hidden === '1' || args.hidden === 1
142
- if (!p) p = process.env.HOME || '/'
229
+ const pickFile = args.file === true || args.file === '1' || args.file === 1
230
+ // 展开 ~/(私钥选择等场景直接从 ~/.ssh 起步)
231
+ if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) {
232
+ p = path.join(process.env.HOME || process.env.USERPROFILE || '/', p.slice(2))
233
+ }
234
+ if (!p) p = process.env.HOME || (IS_WIN ? (process.env.USERPROFILE || 'C:\\') : '/')
143
235
  if (!path.isAbsolute(p)) throw new Error('必须为绝对路径')
144
236
  p = path.resolve(p)
145
237
  try {
@@ -148,7 +240,10 @@ export async function browseDirectory(args = {}) {
148
240
  const dirs = entries.filter(d => d.isDirectory() && (showHidden || !d.name.startsWith('.'))).map(d => d.name).sort().slice(0, 200)
149
241
  // 若当前目录属于 git 仓库(含子目录/任意 worktree),列出全部 worktree 供前端切换
150
242
  const worktrees = await listWorktrees(p)
151
- return { path: p, parent, dirs, worktrees }
243
+ const out = { path: p, parent, dirs, worktrees }
244
+ // 文件选择模式(如私钥):额外列出普通文件
245
+ if (pickFile) out.files = entries.filter(d => d.isFile() && (showHidden || !d.name.startsWith('.'))).map(d => d.name).sort().slice(0, 200)
246
+ return out
152
247
  } catch {
153
248
  throw new Error('目录不可读')
154
249
  }
package/server/rpc.js CHANGED
@@ -1,8 +1,13 @@
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
+
7
+ // sshpass 无 Windows 版本;警告文案按平台给出可操作的下一步(保存/导入两处共用)
8
+ const SSHPASS_HINT = process.platform === 'win32'
9
+ ? '但本机没有 sshpass(Windows 无此工具),部署时无法使用密码认证;请改用密钥认证(SSH 自定义选项填 -i 私钥路径,并清空密码)'
10
+ : '但本机未安装 sshpass,部署时无法使用密码认证。请先执行: brew install hudochenkov/sshpass/sshpass'
6
11
 
7
12
  // JSON-RPC 方法表:与旧 server.ts 的 /api/* 逐一对齐(字段名不变)
8
13
  const methods = {
@@ -26,8 +31,8 @@ const methods = {
26
31
  if (i >= 0) envs[i] = env; else envs.push(env)
27
32
  await saveEnvs()
28
33
  let warning = ''
29
- if (env.ssh.password && !(await which('sshpass'))) {
30
- warning = '已保存,但本机未安装 sshpass,部署时无法使用密码认证。请先执行: brew install hudochenkov/sshpass/sshpass'
34
+ if ((env.sshs || []).some(s => s.password) && !(await which('sshpass'))) {
35
+ warning = '已保存,' + SSHPASS_HINT
31
36
  }
32
37
  return { envs: getEnvs(), warning }
33
38
  },
@@ -67,8 +72,8 @@ const methods = {
67
72
  await saveEnvs(order.map(id => byId.get(id)))
68
73
  const touched = new Set([...added, ...overwritten])
69
74
  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'
75
+ if (getEnvs().some(e => touched.has(e.id) && (e.sshs || []).some(s => s.password)) && !(await which('sshpass'))) {
76
+ warning = '已导入,' + SSHPASS_HINT
72
77
  }
73
78
  return { envs: getEnvs(), added, overwritten, warning }
74
79
  },
@@ -84,6 +89,13 @@ const methods = {
84
89
  try { return { text: await fsp.readFile(p, 'utf8') } } catch { return { text: '' } }
85
90
  },
86
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
+ },
87
99
  browse: (args) => browseDirectory(args ?? {}),
88
100
  git: async ({ env: id, path: sel } = {}) => {
89
101
  const e = getEnvs().find(x => x.id === id)