@xxxyz/dsh-mcp-manager 2.0.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/LICENSE +21 -0
- package/README.md +122 -0
- package/README.zh-CN.md +173 -0
- package/install.mjs +369 -0
- package/install.ps1 +43 -0
- package/install.sh +33 -0
- package/lib/client.js +303 -0
- package/lib/index.js +967 -0
- package/package.json +78 -0
- package/uninstall.mjs +161 -0
- package/uninstall.ps1 +33 -0
- package/uninstall.sh +28 -0
- package//345/256/211/350/243/205/346/226/271/345/274/217.md +131 -0
package/install.mjs
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// dsh-mcp-manager install.mjs — the single cross-platform installer core.
|
|
3
|
+
// Runs on Windows / macOS / Linux with any Node.js >= 18 (no dependencies).
|
|
4
|
+
//
|
|
5
|
+
// install.ps1 (Windows) and install.sh (macOS / Linux) are thin wrappers that
|
|
6
|
+
// just forward their arguments to this file.
|
|
7
|
+
//
|
|
8
|
+
// What it does (idempotent — safe to run repeatedly):
|
|
9
|
+
// 1. copy this package to <dshHome>/local-packages/dsh-mcp-manager
|
|
10
|
+
// (source of record, kept outside node_modules so DSH upgrades never touch it)
|
|
11
|
+
// 2. copy it to <dshHome>/profiles/node_modules/dsh-mcp-manager
|
|
12
|
+
// (a PLAIN copy on purpose — a symlink would make Node ESM resolve the
|
|
13
|
+
// plugin's realpath where @deepseek-ai/dsh-tools cannot be found)
|
|
14
|
+
// 3. append the loader row (`id: mcp-manager`) to
|
|
15
|
+
// <dshHome>/profiles/<profile>/cordis.patch.yml
|
|
16
|
+
// (idempotent; keeps the patch a valid top-level YAML array)
|
|
17
|
+
//
|
|
18
|
+
// --repair additionally bumps the loader row's config.version to force an
|
|
19
|
+
// HMR re-apply, then polls POST /dsh-mcp-manager/api until it
|
|
20
|
+
// answers {ok:true} (default 30 s).
|
|
21
|
+
import fs from 'node:fs'
|
|
22
|
+
import path from 'node:path'
|
|
23
|
+
import os from 'node:os'
|
|
24
|
+
import { fileURLToPath } from 'node:url'
|
|
25
|
+
|
|
26
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
27
|
+
const PKG_NAME = 'dsh-mcp-manager'
|
|
28
|
+
const LOADER_ID = 'mcp-manager'
|
|
29
|
+
const DEFAULT_PROFILE = 'web'
|
|
30
|
+
const DEFAULT_PORT = 3080
|
|
31
|
+
const REPAIR_TIMEOUT_MS = 30000
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// small helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
const log = (msg) => console.log(msg)
|
|
37
|
+
const warn = (msg) => console.warn('[警告] ' + msg)
|
|
38
|
+
const fail = (msg) => { console.error('[错误] ' + msg); process.exit(1) }
|
|
39
|
+
|
|
40
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
41
|
+
|
|
42
|
+
function timeoutSignal(ms) {
|
|
43
|
+
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function')
|
|
44
|
+
return AbortSignal.timeout(ms)
|
|
45
|
+
const ac = new AbortController()
|
|
46
|
+
setTimeout(() => ac.abort(), ms)
|
|
47
|
+
return ac.signal
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Recursive copy that skips heavyweight / irrelevant dirs (node_modules, .git).
|
|
51
|
+
// The destination is wiped first so stale files never survive an upgrade.
|
|
52
|
+
function copyDir(src, dest) {
|
|
53
|
+
fs.rmSync(dest, { recursive: true, force: true })
|
|
54
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
55
|
+
fs.cpSync(src, dest, {
|
|
56
|
+
recursive: true,
|
|
57
|
+
force: true,
|
|
58
|
+
filter: (p) => {
|
|
59
|
+
const base = path.basename(p)
|
|
60
|
+
return base !== 'node_modules' && base !== '.git'
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// argument parsing
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
function parseArgs(argv) {
|
|
69
|
+
const opts = {
|
|
70
|
+
dshHome: process.env.DSH_HOME || null,
|
|
71
|
+
profile: DEFAULT_PROFILE,
|
|
72
|
+
port: DEFAULT_PORT,
|
|
73
|
+
repair: false,
|
|
74
|
+
skipPatch: false,
|
|
75
|
+
help: false,
|
|
76
|
+
}
|
|
77
|
+
for (let i = 0; i < argv.length; i++) {
|
|
78
|
+
const a = argv[i]
|
|
79
|
+
const next = () => {
|
|
80
|
+
i++
|
|
81
|
+
if (i >= argv.length) fail('缺少参数值: ' + a + '(用 --help 查看用法)')
|
|
82
|
+
return argv[i]
|
|
83
|
+
}
|
|
84
|
+
switch (a) {
|
|
85
|
+
case '--dsh-home': opts.dshHome = next(); break
|
|
86
|
+
case '--profile': opts.profile = next(); break
|
|
87
|
+
case '--port':
|
|
88
|
+
opts.port = parseInt(next(), 10)
|
|
89
|
+
if (Number.isNaN(opts.port)) fail('--port 需为数字')
|
|
90
|
+
break
|
|
91
|
+
case '--repair': opts.repair = true; break
|
|
92
|
+
case '--skip-patch': opts.skipPatch = true; break
|
|
93
|
+
case '-h':
|
|
94
|
+
case '--help': opts.help = true; break
|
|
95
|
+
default: fail('未知参数: ' + a + '(用 --help 查看用法)')
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!opts.dshHome) opts.dshHome = path.join(os.homedir(), '.dsh')
|
|
99
|
+
return opts
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function printUsage() {
|
|
103
|
+
log(`dsh-mcp-manager 安装脚本(跨平台:Windows / macOS / Linux,需要 Node.js >= 18)
|
|
104
|
+
|
|
105
|
+
用法:
|
|
106
|
+
node install.mjs [选项]
|
|
107
|
+
|
|
108
|
+
选项:
|
|
109
|
+
--dsh-home <path> DSH 主目录(含 profiles/、settings.yaml 的目录)。
|
|
110
|
+
默认取 $DSH_HOME 环境变量,否则 ~/.dsh
|
|
111
|
+
--profile <name> 要安装到的 profile 名(默认 ${DEFAULT_PROFILE})
|
|
112
|
+
--port <n> 修复模式下探测 API 的端口,即 DSH Web 端口(默认 ${DEFAULT_PORT})
|
|
113
|
+
--repair 修复模式:重新部署 + 递增 loader 行 config.version 触发
|
|
114
|
+
HMR 重应用 + 轮询 API 直到返回 {ok:true}
|
|
115
|
+
--skip-patch 只复制包文件,不修改 cordis.patch.yml
|
|
116
|
+
-h, --help 显示本帮助
|
|
117
|
+
|
|
118
|
+
示例:
|
|
119
|
+
node install.mjs
|
|
120
|
+
node install.mjs --dsh-home D:\\path\\.dsh --profile web
|
|
121
|
+
node install.mjs --repair --port 3080`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// loader-row patch editing (line based, keeps the patch a valid YAML array)
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// IMPORTANT: in the DSH loader patch dialect (applyEntryPatches), a plain
|
|
128
|
+
// `- id: x` row is an OVERRIDE of an already-existing entry and is SKIPPED with
|
|
129
|
+
// a warning when no such entry exists — it can never ADD a new plugin. Adding
|
|
130
|
+
// entries requires the `insert` form (same shape the plugin itself uses for
|
|
131
|
+
// MCP server rows). This is the whole plugin's loader row:
|
|
132
|
+
//
|
|
133
|
+
// - insert:
|
|
134
|
+
// - id: mcp-manager
|
|
135
|
+
// name: dsh-mcp-manager
|
|
136
|
+
// config:
|
|
137
|
+
// version: 1
|
|
138
|
+
function toLoaderBlock(version) {
|
|
139
|
+
return [
|
|
140
|
+
'- insert:',
|
|
141
|
+
' - id: ' + LOADER_ID,
|
|
142
|
+
' name: ' + PKG_NAME,
|
|
143
|
+
' config:',
|
|
144
|
+
' version: ' + version,
|
|
145
|
+
].join('\n')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const LOADER_BLOCK = toLoaderBlock(1)
|
|
149
|
+
|
|
150
|
+
const reLoaderChild = () => new RegExp("^(\\s+)- id:\\s*'?" + LOADER_ID + "'?\\s*$")
|
|
151
|
+
const reLoaderPlain = () => new RegExp("^- id:\\s*'?" + LOADER_ID + "'?\\s*$")
|
|
152
|
+
|
|
153
|
+
// Find the loader entry: either inside a top-level `- insert:` block (current
|
|
154
|
+
// form) or as a legacy plain top-level row (old form, converted on write).
|
|
155
|
+
// Returns the whole owning block's line range plus its form.
|
|
156
|
+
function findLoaderEntry(content) {
|
|
157
|
+
const lines = content.split(/\r?\n/)
|
|
158
|
+
const n = lines.length
|
|
159
|
+
for (let i = 0; i < n; i++) {
|
|
160
|
+
if (/^- insert:/.test(lines[i])) {
|
|
161
|
+
let end = i + 1
|
|
162
|
+
while (end < n && !/^- /.test(lines[end])) end++
|
|
163
|
+
if (lines.slice(i, end).some((l) => reLoaderChild().test(l)))
|
|
164
|
+
return { start: i, end, lines: lines.slice(i, end), form: 'insert' }
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
if (reLoaderPlain().test(lines[i])) {
|
|
168
|
+
let end = i + 1
|
|
169
|
+
while (end < n && !/^- /.test(lines[end])) end++
|
|
170
|
+
return { start: i, end, lines: lines.slice(i, end), form: 'plain' }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function versionOf(lines) {
|
|
177
|
+
for (const line of lines) {
|
|
178
|
+
const m = line.match(/^(\s*)version:\s*(\d+)\s*$/)
|
|
179
|
+
if (m) return parseInt(m[2], 10)
|
|
180
|
+
}
|
|
181
|
+
return 1
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Bump config.version in place; inserts `version: 1` when missing. Returns the
|
|
185
|
+
// new version number.
|
|
186
|
+
function bumpVersion(lines) {
|
|
187
|
+
let configIdx = -1
|
|
188
|
+
let configIndent = 0
|
|
189
|
+
for (let i = 0; i < lines.length; i++) {
|
|
190
|
+
const m = lines[i].match(/^(\s*)config:\s*$/)
|
|
191
|
+
if (m) { configIdx = i; configIndent = m[1].length; break }
|
|
192
|
+
}
|
|
193
|
+
if (configIdx < 0) {
|
|
194
|
+
lines.push(' config:', ' version: 2')
|
|
195
|
+
return 2
|
|
196
|
+
}
|
|
197
|
+
for (let i = configIdx + 1; i < lines.length; i++) {
|
|
198
|
+
const line = lines[i]
|
|
199
|
+
if (/^-\s/.test(line)) break // next top-level entry
|
|
200
|
+
if (!line.trim() || /^\s*#/.test(line)) continue
|
|
201
|
+
const indent = line.match(/^\s*/)[0].length
|
|
202
|
+
if (indent <= configIndent) break // config block ended
|
|
203
|
+
const m = line.match(/^(\s*)version:\s*(\d+)\s*$/)
|
|
204
|
+
if (m) {
|
|
205
|
+
const v = parseInt(m[2], 10) + 1
|
|
206
|
+
lines[i] = m[1] + 'version: ' + v
|
|
207
|
+
return v
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
lines.splice(configIdx + 1, 0, ' '.repeat(configIndent + 2) + 'version: 1')
|
|
211
|
+
return 1
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function appendLoaderBlock(content, block) {
|
|
215
|
+
if (/^\[\]\s*$/m.test(content)) return content.replace(/^\[\]\s*$/m, block + '\n')
|
|
216
|
+
let c = content
|
|
217
|
+
if (!c.trim()) return block + '\n'
|
|
218
|
+
if (!/\n$/.test(c)) c += '\n'
|
|
219
|
+
return c + block + '\n'
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function replaceLines(content, start, end, newLines) {
|
|
223
|
+
const all = content.split(/\r?\n/)
|
|
224
|
+
all.splice(start, end - start, ...newLines)
|
|
225
|
+
return all.join('\n')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Ensure the loader entry exists (append the `insert` block if missing) and
|
|
229
|
+
// optionally bump its config.version. A legacy plain `- id: mcp-manager` row is
|
|
230
|
+
// converted to the working insert form on the fly. Returns the resulting
|
|
231
|
+
// version for reporting.
|
|
232
|
+
function patchLoader(patchPath, bump) {
|
|
233
|
+
let content = ''
|
|
234
|
+
if (fs.existsSync(patchPath)) content = fs.readFileSync(patchPath, 'utf8')
|
|
235
|
+
|
|
236
|
+
const entry = findLoaderEntry(content)
|
|
237
|
+
if (entry) {
|
|
238
|
+
if (entry.form === 'plain') {
|
|
239
|
+
// Legacy row that the DSH loader silently skips — convert it to an
|
|
240
|
+
// insert block (bumping when asked) so the plugin actually mounts.
|
|
241
|
+
const v = bump ? versionOf(entry.lines) + 1 : versionOf(entry.lines)
|
|
242
|
+
content = replaceLines(content, entry.start, entry.end, toLoaderBlock(v).split('\n'))
|
|
243
|
+
fs.mkdirSync(path.dirname(patchPath), { recursive: true })
|
|
244
|
+
fs.writeFileSync(patchPath, content, 'utf8')
|
|
245
|
+
return v
|
|
246
|
+
}
|
|
247
|
+
if (bump) {
|
|
248
|
+
const lines = entry.lines.slice()
|
|
249
|
+
const v = bumpVersion(lines)
|
|
250
|
+
content = replaceLines(content, entry.start, entry.end, lines)
|
|
251
|
+
fs.mkdirSync(path.dirname(patchPath), { recursive: true })
|
|
252
|
+
fs.writeFileSync(patchPath, content, 'utf8')
|
|
253
|
+
return v
|
|
254
|
+
}
|
|
255
|
+
return versionOf(entry.lines)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const updated = appendLoaderBlock(content, LOADER_BLOCK)
|
|
259
|
+
fs.mkdirSync(path.dirname(patchPath), { recursive: true })
|
|
260
|
+
fs.writeFileSync(patchPath, updated, 'utf8')
|
|
261
|
+
return 1
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// --repair: poll the plugin API until it answers {ok:true}
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
async function pollApi(port, timeoutMs) {
|
|
268
|
+
const url = `http://127.0.0.1:${port}/dsh-mcp-manager/api`
|
|
269
|
+
const deadline = Date.now() + timeoutMs
|
|
270
|
+
let attempt = 0
|
|
271
|
+
while (Date.now() < deadline) {
|
|
272
|
+
attempt++
|
|
273
|
+
try {
|
|
274
|
+
const res = await fetch(url, {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
headers: { 'content-type': 'application/json' },
|
|
277
|
+
body: JSON.stringify({ op: 'mcpm-list', args: {} }),
|
|
278
|
+
signal: timeoutSignal(2000),
|
|
279
|
+
})
|
|
280
|
+
if (res.ok) {
|
|
281
|
+
const data = await res.json().catch(() => null)
|
|
282
|
+
if (data && data.ok) return true
|
|
283
|
+
}
|
|
284
|
+
} catch (e) { /* DSH not answering yet */ }
|
|
285
|
+
if (attempt === 1 || attempt % 5 === 0)
|
|
286
|
+
log(` 已等待 ${Math.round((Date.now() - (deadline - timeoutMs)) / 1000)} 秒…(${attempt} 次探测)`)
|
|
287
|
+
await sleep(1000)
|
|
288
|
+
}
|
|
289
|
+
return false
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// main
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
async function main() {
|
|
296
|
+
const opts = parseArgs(process.argv.slice(2))
|
|
297
|
+
if (opts.help) { printUsage(); return }
|
|
298
|
+
|
|
299
|
+
// Sanity-check the shipped artifacts we are about to deploy.
|
|
300
|
+
for (const f of ['package.json', 'lib/index.js', 'lib/client.js']) {
|
|
301
|
+
if (!fs.existsSync(path.join(SCRIPT_DIR, f)))
|
|
302
|
+
fail(`未找到 ${f}:请确认从 dsh-mcp-manager 包目录运行,或先执行 npm run build`)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const dshHome = opts.dshHome
|
|
306
|
+
const localPkg = path.join(dshHome, 'local-packages', PKG_NAME)
|
|
307
|
+
const deployDir = path.join(dshHome, 'profiles', 'node_modules', PKG_NAME)
|
|
308
|
+
const profileDir = path.join(dshHome, 'profiles', opts.profile)
|
|
309
|
+
const projectPatch = path.join(profileDir, 'cordis.patch.yml')
|
|
310
|
+
|
|
311
|
+
// When run through npx (npm cache or a `node_modules` install), the package
|
|
312
|
+
// lives in a temp directory — everything still works (the source is copied
|
|
313
|
+
// from SCRIPT_DIR), we just say so for clarity.
|
|
314
|
+
const runViaNpx = /node_modules[\\/]/.test(SCRIPT_DIR)
|
|
315
|
+
|
|
316
|
+
log(`操作系统: ${process.platform === 'win32' ? 'Windows' : process.platform === 'darwin' ? 'macOS' : 'Linux'}`)
|
|
317
|
+
if (runViaNpx) log('运行来源: npx(npm 缓存 / GitHub 直拉的临时安装)')
|
|
318
|
+
log(`DSH 主目录: ${dshHome}`)
|
|
319
|
+
log(`profile: ${opts.profile}`)
|
|
320
|
+
if (!fs.existsSync(path.join(dshHome, 'profiles')))
|
|
321
|
+
warn(`未在 ${dshHome} 下找到 profiles 目录,可能不是正确的 DSH 主目录;如需指定请用 --dsh-home`)
|
|
322
|
+
log('')
|
|
323
|
+
|
|
324
|
+
log('1/3 复制包到 local-packages(真源,DSH 升级不会动它)…')
|
|
325
|
+
try {
|
|
326
|
+
copyDir(SCRIPT_DIR, localPkg)
|
|
327
|
+
} catch (e) {
|
|
328
|
+
fail(`复制到 local-packages 失败:${e.message}(DSH 正在运行?请先退出 DSH 再重试)`)
|
|
329
|
+
}
|
|
330
|
+
log(' → ' + localPkg)
|
|
331
|
+
|
|
332
|
+
log('2/3 复制包到 profiles/node_modules(普通复制,不用软链接)…')
|
|
333
|
+
try {
|
|
334
|
+
copyDir(SCRIPT_DIR, deployDir)
|
|
335
|
+
} catch (e) {
|
|
336
|
+
fail(`复制到 profiles/node_modules 失败:${e.message}(DSH 正在运行?请先退出 DSH 再重试)`)
|
|
337
|
+
}
|
|
338
|
+
log(' → ' + deployDir)
|
|
339
|
+
|
|
340
|
+
let version = null
|
|
341
|
+
if (opts.skipPatch) {
|
|
342
|
+
log('3/3 已跳过补丁修改(--skip-patch)')
|
|
343
|
+
} else {
|
|
344
|
+
log('3/3 更新 loader 行 → ' + projectPatch)
|
|
345
|
+
version = patchLoader(projectPatch, opts.repair)
|
|
346
|
+
log(` loader 行已就绪:id=${LOADER_ID}, config.version=${version}` +
|
|
347
|
+
(opts.repair ? '(已递增,触发 HMR 重应用)' : '(幂等,重复运行不会重复添加)'))
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (opts.repair) {
|
|
351
|
+
log('修复模式:轮询 API 等待插件重新加载(默认 30 秒)…')
|
|
352
|
+
const ok = await pollApi(opts.port, REPAIR_TIMEOUT_MS)
|
|
353
|
+
if (!ok) {
|
|
354
|
+
warn(`30 秒内 POST http://127.0.0.1:${opts.port}/dsh-mcp-manager/api 未返回 {ok:true}`)
|
|
355
|
+
warn('请重启一次 DSH(loader 会在启动时重新导入),然后打开 设置 → MCP 管理 验证。')
|
|
356
|
+
process.exit(1)
|
|
357
|
+
}
|
|
358
|
+
log('API 已恢复:{ok:true},插件已重新加载。')
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
log('')
|
|
362
|
+
log('✔ 安装完成。')
|
|
363
|
+
if (!opts.repair)
|
|
364
|
+
log(' 请重启 DSH,然后打开 设置 → MCP 管理(4 个 mcp_manager_* 工具将在重启后注册)。')
|
|
365
|
+
else
|
|
366
|
+
log(' 打开 设置 → MCP 管理 即可使用。')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
main().catch((e) => { console.error(e); process.exit(1) })
|
package/install.ps1
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# dsh-mcp-manager install.ps1 — Windows installer (thin wrapper around install.mjs)
|
|
2
|
+
#
|
|
3
|
+
# .\dsh-mcp-manager\install.ps1 # default: ~/.dsh, web profile
|
|
4
|
+
# .\install.ps1 -DshHome D:\path\.dsh -Profile web
|
|
5
|
+
# .\install.ps1 -Repair -Port 3080
|
|
6
|
+
#
|
|
7
|
+
# See install.mjs for the full cross-platform logic and options.
|
|
8
|
+
[CmdletBinding()]
|
|
9
|
+
param(
|
|
10
|
+
[string]$DshHome,
|
|
11
|
+
[string]$Profile = '',
|
|
12
|
+
[int]$Port = 0,
|
|
13
|
+
[switch]$Repair,
|
|
14
|
+
[switch]$SkipPatch,
|
|
15
|
+
[switch]$Help
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
$ErrorActionPreference = 'Stop'
|
|
19
|
+
|
|
20
|
+
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
21
|
+
$mjs = Join-Path $scriptDir 'install.mjs'
|
|
22
|
+
|
|
23
|
+
$node = Get-Command node -ErrorAction SilentlyContinue
|
|
24
|
+
if (-not $node) {
|
|
25
|
+
Write-Error '未找到 node 命令,请先安装 Node.js 18+(https://nodejs.org)'
|
|
26
|
+
exit 1
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if ($Help) {
|
|
30
|
+
& $node $mjs '--help'
|
|
31
|
+
exit $LASTEXITCODE
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Build the argument list as separate elements so paths with spaces survive.
|
|
35
|
+
$argsList = [System.Collections.Generic.List[string]]::new()
|
|
36
|
+
if ($DshHome) { $argsList.Add('--dsh-home'); $argsList.Add($DshHome) }
|
|
37
|
+
if ($Profile) { $argsList.Add('--profile'); $argsList.Add($Profile) }
|
|
38
|
+
if ($Port -gt 0) { $argsList.Add('--port'); $argsList.Add([string]$Port) }
|
|
39
|
+
if ($Repair) { $argsList.Add('--repair') }
|
|
40
|
+
if ($SkipPatch){ $argsList.Add('--skip-patch') }
|
|
41
|
+
|
|
42
|
+
& $node $mjs @argsList
|
|
43
|
+
exit $LASTEXITCODE
|
package/install.sh
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dsh-mcp-manager install.sh — macOS / Linux installer (thin wrapper around install.mjs)
|
|
3
|
+
#
|
|
4
|
+
# ./dsh-mcp-manager/install.sh # default: ~/.dsh, web profile
|
|
5
|
+
# ./install.sh --dsh-home /path/.dsh --profile web
|
|
6
|
+
# ./install.sh --repair --port 3080
|
|
7
|
+
#
|
|
8
|
+
# If "permission denied", run: chmod +x dsh-mcp-manager/install.sh
|
|
9
|
+
# See install.mjs for the full cross-platform logic and options.
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
MJS="$SCRIPT_DIR/install.mjs"
|
|
14
|
+
|
|
15
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
16
|
+
echo "未找到 node 命令,请先安装 Node.js 18+(https://nodejs.org)" >&2
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
declare -a ARGS=()
|
|
21
|
+
while [[ $# -gt 0 ]]; do
|
|
22
|
+
case "$1" in
|
|
23
|
+
--dsh-home) ARGS+=("--dsh-home" "${2:?--dsh-home 需要路径参数}"); shift 2 ;;
|
|
24
|
+
--profile) ARGS+=("--profile" "${2:?--profile 需要名称参数}"); shift 2 ;;
|
|
25
|
+
--port) ARGS+=("--port" "${2:?--port 需要数字参数}"); shift 2 ;;
|
|
26
|
+
--repair) ARGS+=("--repair"); shift ;;
|
|
27
|
+
--skip-patch) ARGS+=("--skip-patch"); shift ;;
|
|
28
|
+
-h|--help) ARGS+=("--help"); shift ;;
|
|
29
|
+
*) echo "未知参数: $1(用 --help 查看用法)" >&2; exit 2 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
|
|
33
|
+
exec node "$MJS" "${ARGS[@]}"
|