dsh-paoding 0.2.2 → 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.
Files changed (42) hide show
  1. package/README.md +27 -32
  2. package/README_en.md +27 -32
  3. package/bin/dsh-paoding.mjs +73 -11
  4. package/docs/architecture.md +41 -34
  5. package/docs/architecture_en.md +65 -42
  6. package/docs/configuration.md +159 -65
  7. package/docs/configuration_en.md +161 -66
  8. package/docs/images/1.png +0 -0
  9. package/docs/installation.md +100 -237
  10. package/docs/installation_en.md +112 -252
  11. package/docs/orchestration.md +34 -38
  12. package/docs/orchestration_en.md +34 -38
  13. package/package.json +8 -5
  14. package/plugins/paoding-config-ui/api-core.mjs +215 -18
  15. package/plugins/paoding-config-ui/index.mjs +414 -154
  16. package/plugins/paoding-config-ui/lib/base.css +1083 -42
  17. package/plugins/paoding-config-ui/lib/client.js +2266 -252
  18. package/plugins/paoding-config-ui/package.json +2 -2
  19. package/plugins/paoding-config-ui/runtime-inventory.mjs +27 -26
  20. package/presets/orchestrator/agent.cordis.yml +19 -9
  21. package/presets/orchestrator/preset.yml +3 -2
  22. package/presets/orchestrator/restrict.mjs +2 -1
  23. package/tools/install.mjs +9 -4
  24. package/tools/lib/alloc.mjs +60 -29
  25. package/tools/lib/cli.mjs +127 -278
  26. package/tools/lib/compose.mjs +261 -95
  27. package/tools/lib/config.mjs +266 -86
  28. package/tools/lib/host.mjs +55 -24
  29. package/tools/lib/skills.mjs +72 -45
  30. package/tools/lib/state.mjs +172 -40
  31. package/tools/lib/upgrade.mjs +170 -0
  32. package/tools/lib/util.mjs +7 -0
  33. package/tools/lib/version.mjs +233 -0
  34. package/tools/lib/workspaces.mjs +74 -0
  35. package/tools/lib/yaml.mjs +92 -15
  36. package/docs/images/2.png +0 -0
  37. package/docs/images/3.png +0 -0
  38. package/docs/images/4.png +0 -0
  39. package/docs/images/5.png +0 -0
  40. package/docs/images/6.png +0 -0
  41. package/install.sh +0 -31
  42. package/tools/lib/wizard.mjs +0 -516
package/tools/lib/cli.mjs CHANGED
@@ -1,32 +1,40 @@
1
1
  /**
2
- * CLI 层:参数解析与帮助(parseArgs / printHelp)、检测报告(printReport)、
3
- * --config-ui 挂载(ensureConfigUiMount,含 npx 分布式载荷自愈)与主流程
4
- * (main,供 tools/install.mjs 直接运行与 bin/dsh-paoding.mjs(npx 入口)调用)。
5
- * 依赖 util(REPO_ROOT / ROLES / SRC_DIR / warn)、config(normalizeMainAgentName)、
6
- * host(removedReason)、state(collectState / generateAndInstall)、wizard
7
- * (runWizard)与 alloc(resolveAssignments / baseAssignments)以及
8
- * node:fs / node:os / node:path。
2
+ * CLI 层:参数解析与帮助(parseArgs / printHelp)、检测报告(printReport)与
3
+ * 主流程(main,供 tools/install.mjs 直接运行)。
4
+ *
5
+ * 官方安装通道是 DSH 插件通道(dsh plugin --profile web add dsh-paoding),
6
+ * npm 包不再携带 CLI 入口;本 CLI 仅作仓库内不宣传的开发兜底(--auto /
7
+ * --dry-run / --config / --help)。交互模式已整块移除:可视化配置走插件通道
8
+ * 自带的 Web 配置器(侧栏底部「庖丁配置」入口)。
9
+ *
10
+ * 依赖 util(ROLES / SRC_DIR / PROFILE_NAME_RE)、config(normalizeRoleName)、
11
+ * host(removedReason)、state(collectState /
12
+ * generateAndInstall)、alloc(autoAssignments,--auto 语义与插件首装共享)、
13
+ * version(getVersionInfo,main 收尾的新版提示)以及 node:os / node:path。
9
14
  */
10
- import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs'
11
15
  import { homedir } from 'node:os'
12
16
  import path from 'node:path'
13
- import { REPO_ROOT, ROLES, SRC_DIR, warn } from './util.mjs'
14
- import { normalizeMainAgentName } from './config.mjs'
17
+ import { PROFILE_NAME_RE, ROLES, SRC_DIR } from './util.mjs'
18
+ import { normalizeRoleName } from './config.mjs'
15
19
  import { removedReason } from './host.mjs'
16
20
  import { collectState, generateAndInstall } from './state.mjs'
17
- import { runWizard } from './wizard.mjs'
18
- import { baseAssignments, resolveAssignments } from './alloc.mjs'
21
+ import { autoAssignments } from './alloc.mjs'
22
+ import { getVersionInfo } from './version.mjs'
19
23
 
20
24
  // ── CLI ─────────────────────────────────────────────────────────────────────
21
25
 
22
26
  export function printHelp() {
23
- console.log(`dsh-paoding installer — installs the orchestrator preset with host-aware tool filtering
27
+ console.log(`dsh-paoding installer — dev fallback installer for the orchestrator preset
28
+ (host-aware tool filtering)
29
+
30
+ 官方安装通道是 DSH 插件通道(本 CLI 仅作仓库内开发兜底,不作宣传):
31
+
32
+ dsh plugin --profile web add dsh-paoding
33
+
34
+ 可视化配置随插件通道自带:DSH Web 侧栏底部「庖丁配置」入口。
24
35
 
25
36
  Usage:
26
37
  node tools/install.mjs [options]
27
- ./install.sh [options]
28
- npx dsh-paoding [options]
29
- npm exec dsh-paoding -- [options]
30
38
 
31
39
  Options:
32
40
  --profile <name> Profile whose patch layer is scanned (default: web).
@@ -37,42 +45,35 @@ Options:
37
45
  --config <file> Configuration file with role/tool assignments
38
46
  (default: $DSH_HOME/dsh-paoding.config.yml).
39
47
  --auto Non-interactive: apply the config file when present,
40
- otherwise apply the base template (or --suggest smart
41
- defaults on a fresh install). Required when stdin is
42
- not a TTY.
43
- --wizard Force the interactive wizard even when stdin is not a TTY
44
- (reads answers from stdin).
48
+ otherwise apply the base template (--suggest seeds smart
49
+ defaults on a fresh install instead).
45
50
  --dry-run Print the detection report and the generated allow lists
46
51
  without writing anything; exit 0.
47
52
  --suggest Fresh install without a config file: seed smart defaults
48
53
  from the detected host/MCP tools instead of the base
49
54
  template (host tools are folded in automatically).
50
- --no-ui npx entry only: skip mounting the config UI (the npx
51
- entry mounts it by default; --config-ui wins when both
52
- are given).
53
- --config-ui Also mount the visual config UI into the DSH Settings
54
- page: symlink plugins/paoding-config-ui into
55
- $DSH_HOME/node_modules and ensure the cordis.patch.yml
56
- row (id: paoding-config-ui) is enabled. Requires a DSH
57
- restart to take effect.
58
55
  --help Show this help and exit.
59
56
 
60
57
  Environment:
61
58
  DSH_HOME Directory holding cordis.patch.yml etc. (default: $HOME/.dsh)
62
59
 
63
- Without --auto, an interactive wizard runs (when stdin is a TTY): it detects
64
- enabled MCP servers / plugins / skills, lets you assign tools to the default
65
- role agents or create custom sub-agents, and writes the config file before
66
- installing. The npx entry (bin/dsh-paoding.mjs) defaults to
67
- --auto --config-ui for a quick non-interactive install. With --auto and no
68
- config file, a base template is written: only the dsh built-in tools stay
69
- enabled — add host/MCP tools afterwards in 设置 → 庖丁配置 (or the wizard).
60
+ With --auto and no config file, a base template is written: only the dsh
61
+ built-in tools stay enabled — add host/MCP tools afterwards via the 庖丁配置
62
+ entry at the sidebar bottom (visual config UI from the plugin channel).
70
63
  Pass --suggest to fall back to the smart defaults that fold detected host
71
64
  tools in automatically.`)
72
65
  }
73
66
 
67
+ // --profile 值白名单:与 bin/dsh-paoding.mjs 共用同一份 PROFILE_NAME_RE(抽在
68
+ // util 防两处漂移)。profile 名会被拼进 dsh 子进程命令行,解析源头只收常规名称。
69
+ function assertProfileName(value) {
70
+ if (!PROFILE_NAME_RE.test(value)) {
71
+ throw new Error(`error: --profile 只收常规名称(字母数字开头,仅限 [A-Za-z0-9._-]): ${value}`)
72
+ }
73
+ }
74
+
74
75
  export function parseArgs(argv) {
75
- const opts = { profile: null, patches: [], dryRun: false, help: false, auto: false, wizard: false, config: undefined, configUi: false, noUi: false, suggest: false }
76
+ const opts = { profile: null, patches: [], dryRun: false, help: false, auto: false, config: undefined, suggest: false }
76
77
  for (let i = 0; i < argv.length; i++) {
77
78
  const arg = argv[i]
78
79
  if (arg === '--help' || arg === '-h') {
@@ -81,14 +82,6 @@ export function parseArgs(argv) {
81
82
  opts.dryRun = true
82
83
  } else if (arg === '--auto') {
83
84
  opts.auto = true
84
- } else if (arg === '--wizard') {
85
- opts.wizard = true
86
- } else if (arg === '--config-ui') {
87
- opts.configUi = true
88
- } else if (arg === '--no-ui') {
89
- // 仅 bin(npx 入口)有意义:跳过 bin 默认注入的 --config-ui;与
90
- // --config-ui 同传时后者优先(configUi 已为 true,注入逻辑自然跳过)。
91
- opts.noUi = true
92
85
  } else if (arg === '--suggest') {
93
86
  // fresh 安装时用智能默认(自动纳入检测到的 host 工具)而非基础模板。
94
87
  opts.suggest = true
@@ -99,15 +92,27 @@ export function parseArgs(argv) {
99
92
  }
100
93
  opts.config = value
101
94
  } else if (arg.startsWith('--config=')) {
102
- opts.config = arg.slice('--config='.length)
95
+ // 等号形式同样校验空串(--config= 与缺值同款报错)。
96
+ const value = arg.slice('--config='.length)
97
+ if (value === '') {
98
+ throw new Error('error: --config requires a file path')
99
+ }
100
+ opts.config = value
103
101
  } else if (arg === '--profile') {
104
102
  const value = argv[++i]
105
103
  if (value === undefined || value.startsWith('--')) {
106
104
  throw new Error('error: --profile requires a value')
107
105
  }
106
+ assertProfileName(value)
108
107
  opts.profile = value
109
108
  } else if (arg.startsWith('--profile=')) {
110
- opts.profile = arg.slice('--profile='.length)
109
+ // 等号形式:空串与缺值同款报错;值同样过 PROFILE_NAME_RE 白名单。
110
+ const value = arg.slice('--profile='.length)
111
+ if (value === '') {
112
+ throw new Error('error: --profile requires a value')
113
+ }
114
+ assertProfileName(value)
115
+ opts.profile = value
111
116
  } else if (arg === '--patch') {
112
117
  const value = argv[++i]
113
118
  if (value === undefined || value.startsWith('--')) {
@@ -142,8 +147,13 @@ export function printReport({ scanned, mcpReports, pluginReports, roleResults, d
142
147
  if (mcpReports.length > 0) {
143
148
  console.log(' mcp servers:')
144
149
  for (const r of mcpReports) {
150
+ // 来源标签:runtime(运行时注册表合并,见 state.collectState 的 runtimeFacts
151
+ // 段)排最前,与握手 / 静态表并列展示,不再误落「handshake failed」档。
145
152
  const sourceLabel =
146
- r.source === 'static' ? 'static table' : r.source === 'handshake' ? 'live handshake' : 'handshake failed'
153
+ r.source === 'runtime' ? 'runtime registry'
154
+ : r.source === 'static' ? 'static table'
155
+ : r.source === 'handshake' ? 'live handshake'
156
+ : 'handshake failed'
147
157
  const count = r.error ? `0 tools (${r.error})` : `${r.count} tool${r.count === 1 ? '' : 's'}`
148
158
  console.log(` ${pad(r.serverName, 16)}${pad(r.transport ?? '-', 20)}${count} (${sourceLabel})`)
149
159
  }
@@ -179,7 +189,7 @@ export function printReport({ scanned, mcpReports, pluginReports, roleResults, d
179
189
  console.log(' removed: none — every host-dependent allow entry is enabled')
180
190
  }
181
191
  if (staleNote) {
182
- console.log('\nnote: 检测到 patch 配置在本次安装后有过修改,改动后请重跑 ./install.sh')
192
+ console.log('\nnote: 检测到 patch 配置在本次安装后有过修改,改动后请重跑 node tools/install.mjs --auto')
183
193
  }
184
194
  console.log()
185
195
  console.log(
@@ -201,160 +211,15 @@ export function printReport({ scanned, mcpReports, pluginReports, roleResults, d
201
211
  console.log(`uninstall: rm -rf "${dstDir}"`)
202
212
  }
203
213
 
204
- // ── config UI mount (--config-ui) ──────────────────────────────────────────
205
- //
206
- // 借鉴 dsh-better-sidebar 的设置页整合机制(DSH 0.1.1-rc.1 实测通过):
207
- // - 插件以包名安装进 $DSH_HOME/node_modules,patch 行 `name: <包名>` 即可被
208
- // cordis Loader 解析;dsh-client-modules 节点半扫描该包 package.json 的
209
- // `dsh.client` 声明 + exports["./client"],把 client bundle 编入
210
- // __DSH_BOOT__ 并服务 /plugins/<包名>/client.js;
211
- // - 浏览器内核为 manifest 里每个插件创建 loader 条目并激活(不依赖谁 require),
212
- // bundle 以 window.__ModuleLoader__.load({id: <包名>, factory}) 注册;
213
- // - 客户端 apply() 经 ctx.slots.inject("settings.section") + slots.register()
214
- // 把分区挂进 DSH 设置壳(id/order/label 驱动导航),与 ui-settings-general
215
- // 的 general 分区同款模式;
216
- // - Node 半路由带信任围栏(/api/paoding 长于 /api 前缀会绕开网关围栏)。
217
-
218
- /** 幂等挂载配置 UI:symlink 进 $DSH_HOME/node_modules + 启用 patch 行。 */
219
- export function ensureConfigUiMount(dshHome, dryRun) {
220
- // 仓库检出(含 .git)直接 symlink 仓库内插件目录;npm 包 / tarball / git 依赖
221
- // 安装不含 .git,运行目录随时可能被 npx 缓存回收 —— 先把整包载荷复制到
222
- // $DSH_HOME/dsh-paoding/(稳定位置)再挂载,缓存清掉后 symlink 也不悬空。
223
- // 插件 api-core.mjs 以相对路径 import '../../tools/install.mjs',故必须整包
224
- // 复制(tools/ + presets/ + plugins/ + package.json),不能只拷 plugins/。
225
- const devCheckout = existsSync(path.join(REPO_ROOT, '.git'))
226
- const payloadDir = path.join(dshHome, 'dsh-paoding')
227
- // 自愈载荷内再运行(REPO_ROOT 已在 payload 里):源即目标,跳过复制,仅保证
228
- // symlink 指向 payload —— 否则 rmSync 会先删掉正在运行的包。比较走 realpath:
229
- // REPO_ROOT 可能带 /private 前缀(macOS /var 符号链接),字符串比对会失配。
230
- let selfHosted = false
231
- if (!devCheckout) {
232
- try {
233
- const realPayload = path.join(realpathSync(dshHome), 'dsh-paoding')
234
- const realRoot = realpathSync(REPO_ROOT)
235
- selfHosted = realRoot === realPayload || realRoot.startsWith(realPayload + path.sep)
236
- } catch {
237
- /* dshHome 尚不存在等:按非 selfHosted 处理 */
238
- }
239
- }
240
- const pluginDir = devCheckout || selfHosted
241
- ? path.join(REPO_ROOT, 'plugins', 'paoding-config-ui')
242
- : path.join(payloadDir, 'plugins', 'paoding-config-ui')
243
- const linkDir = path.join(dshHome, 'node_modules')
244
- const linkTarget = path.join(linkDir, 'paoding-config-ui')
245
- const patchFile = path.join(dshHome, 'cordis.patch.yml')
246
-
247
- if (dryRun) {
248
- if (!devCheckout && !selfHosted) {
249
- console.log(`[config-ui] 将复制到 ${payloadDir} 再挂载(npx/npm 包形态:整包载荷进稳定位置,防缓存清理悬空)`)
250
- }
251
- console.log(`[config-ui] 将挂载: ${linkTarget} -> ${pluginDir}`)
252
- console.log(`[config-ui] 将启用 patch 行: id: paoding-config-ui (${patchFile})`)
253
- return
254
- }
255
-
256
- if (!devCheckout && !selfHosted) {
257
- // 整包复制(先删后建 = 幂等刷新载荷;bin 缺失跳过)。
258
- console.log(`[config-ui] distributed copy: ${REPO_ROOT} → ${payloadDir}`)
259
- rmSync(payloadDir, { recursive: true, force: true })
260
- mkdirSync(payloadDir, { recursive: true })
261
- for (const dir of ['tools', 'presets', 'bin', 'plugins']) {
262
- const from = path.join(REPO_ROOT, dir)
263
- if (existsSync(from)) cpSync(from, path.join(payloadDir, dir), { recursive: true })
264
- }
265
- copyFileSync(path.join(REPO_ROOT, 'package.json'), path.join(payloadDir, 'package.json'))
266
- }
267
-
268
- // 1) symlink(已存在且指向正确则跳过)
269
- if (!existsSync(pluginDir)) {
270
- throw new Error(`config-ui 插件目录不存在: ${pluginDir}`)
271
- }
272
- let linkStat = null
273
- try {
274
- linkStat = lstatSync(linkTarget)
275
- } catch {
276
- /* 不存在:首次挂载 */
277
- }
278
- if (linkStat !== null && !linkStat.isSymbolicLink()) {
279
- // 真实目录(用户自装/解包安装)不动,保持原有报错文案。
280
- const real = realpathSync(linkTarget)
281
- if (real !== realpathSync(pluginDir)) {
282
- throw new Error(`config-ui 已安装为其它来源(${real});删除 ${linkTarget} 后重试`)
283
- }
284
- } else if (linkStat !== null) {
285
- // 符号链接:指向正确 → 幂等跳过;指向别处或悬空(lstat 命中但 realpath
286
- // 抛错,典型为旧 npx 缓存被清)→ 删除重建(迁移自愈)。
287
- let real = null
288
- try {
289
- real = realpathSync(linkTarget)
290
- } catch {
291
- real = null
292
- }
293
- if (real !== realpathSync(pluginDir)) {
294
- unlinkSync(linkTarget)
295
- mkdirSync(linkDir, { recursive: true })
296
- symlinkSync(pluginDir, linkTarget, 'dir')
297
- warn(`config-ui 旧 symlink 指向 ${real ?? '<已失效>'},已重建 -> ${pluginDir}`)
298
- }
299
- } else {
300
- mkdirSync(linkDir, { recursive: true })
301
- symlinkSync(pluginDir, linkTarget, 'dir')
302
- console.log(`[config-ui] symlink: ${linkTarget} -> ${pluginDir}`)
303
- }
304
-
305
- // 2) patch 行(幂等:已启用跳过;注释块则取消注释;缺失则追加)
306
- if (!existsSync(patchFile)) {
307
- writeFileSync(patchFile, '', 'utf8')
308
- }
309
- let patch = readFileSync(patchFile, 'utf8')
310
-
311
- // 已启用的挂载行存在?
312
- const enabledRow = /\n- insert:\n\s+- id: paoding-config-ui\s*\n\s+name: \S+\s*\n?$/.test('\n' + patch)
313
- const hasAnyRow = /(^|\n)\s*#?\s*- insert:\s*\n\s*#?\s+- id: paoding-config-ui/.test(patch)
314
-
315
- if (!hasAnyRow) {
316
- const block =
317
- '\n# ============================================================\n' +
318
- '# paoding-config-ui — DSH Web 设置页内嵌可视化配置器\n' +
319
- '# (install.sh --config-ui 自动挂载;停用:整段注释后重启 DSH)\n' +
320
- '# ============================================================\n' +
321
- '- insert:\n' +
322
- ' - id: paoding-config-ui\n' +
323
- ' name: paoding-config-ui\n'
324
- patch += block
325
- writeFileSync(patchFile, patch, 'utf8')
326
- console.log(`[config-ui] patch 已追加: ${patchFile}`)
327
- return
328
- }
329
-
330
- if (!enabledRow) {
331
- // 取消注释三行挂载(保留其上的注释块)
332
- const uncomment = (text) =>
333
- text
334
- .replace(/(^|\n)#- insert:/g, '$1- insert:')
335
- .replace(/(^|\n)#\s+- id: paoding-config-ui/g, '$1 - id: paoding-config-ui')
336
- .replace(/(^|\n)#\s+name: paoding-config-ui/g, '$1 name: paoding-config-ui')
337
- const next = uncomment(patch)
338
- if (next === patch) {
339
- throw new Error('config-ui patch 行存在但无法自动启用,请手动编辑 ' + patchFile)
340
- }
341
- writeFileSync(patchFile, next, 'utf8')
342
- console.log(`[config-ui] patch 行已启用: ${patchFile}`)
343
- return
344
- }
345
-
346
- console.log('[config-ui] 已挂载(幂等跳过)')
347
- }
348
-
349
214
  // ── main ────────────────────────────────────────────────────────────────────
350
215
 
351
216
  /**
352
- * 主流程。argv 缺省取 process.argv.slice(2)(tools/install.mjs 直接运行,行为
353
- * 与旧版逐字一致);bin=true 由 npx 入口(bin/dsh-paoding.mjs)传入 —— 注入
354
- * 快速安装默认值:非交互(TTY 也不进 wizard,先装完基础版,自定义留给 Web
355
- * 配置器)+ 默认挂载配置 UI。用户显式旗标优先(--wizard / --no-ui 不覆盖)。
217
+ * 主流程。argv 缺省取 process.argv.slice(2)(tools/install.mjs 直接运行)。
218
+ * 交互模式已随安装通道收敛整块移除:无 --auto(且非只读的 --dry-run)在参数
219
+ * 解析后、collectState 之前直接拒绝并指路(免做无谓的 MCP 握手 / patch 扫描)
220
+ * —— 非交互装走 --auto,可视化配置走插件通道的 Web 配置器。
356
221
  */
357
- export async function main({ argv = process.argv.slice(2), bin = false } = {}) {
222
+ export async function main({ argv = process.argv.slice(2) } = {}) {
358
223
  let opts
359
224
  try {
360
225
  opts = parseArgs(argv)
@@ -366,94 +231,66 @@ export async function main({ argv = process.argv.slice(2), bin = false } = {}) {
366
231
  printHelp()
367
232
  return 0
368
233
  }
369
- if (bin) {
370
- if (!opts.wizard) opts.auto ||= true
371
- if (!opts.noUi) opts.configUi = true
234
+
235
+ // 无 --auto 直接拒绝(交互模式已删):--dry-run 只读不动盘,放行作检测预览。
236
+ // 门禁放在 collectState 之前:被拒路径不做任何检测 / 握手,快速失败。
237
+ if (!opts.auto && !opts.dryRun) {
238
+ console.error(
239
+ 'error: 交互安装已移除——请用 --auto 非交互安装;可视化配置请安装插件:' +
240
+ 'dsh plugin --profile web add dsh-paoding',
241
+ )
242
+ return 1
372
243
  }
373
244
 
374
245
  const dshHome = process.env.DSH_HOME || path.join(homedir(), '.dsh')
375
246
  const configFile = opts.config || path.join(dshHome, 'dsh-paoding.config.yml')
376
247
 
377
248
  // Run the shared detection pipeline (patch scan, MCP handshake, plugins,
378
- // skills, smart defaults). Failures are hard errors.
379
- const state = await collectState({
380
- dshHome,
381
- configFile,
382
- profile: opts.profile,
383
- patches: opts.patches,
384
- cwd: process.cwd(),
385
- })
249
+ // skills, smart defaults). Failures are hard errors: 与下方 generateAndInstall
250
+ // 同款 try/catch 收敛(error: ... 到 stderr、exit 1),不再裸堆栈。
251
+ let state
252
+ try {
253
+ state = await collectState({
254
+ dshHome,
255
+ configFile,
256
+ profile: opts.profile,
257
+ patches: opts.patches,
258
+ cwd: process.cwd(),
259
+ })
260
+ } catch (err) {
261
+ console.error(`error: ${err.message}`)
262
+ return 1
263
+ }
386
264
  const {
387
- srcText,
388
- blocks,
389
- personaBlocks,
390
265
  restrictBase,
391
266
  existing,
392
- effectiveProfile,
393
267
  scanned,
394
268
  mcpReports,
395
269
  pluginReports,
396
- inventory,
397
- skills,
398
270
  staticBase,
399
271
  suggested,
400
272
  dstDir,
401
- dstAgentFile,
402
273
  } = state
403
-
404
- // Decide the interaction mode: the wizard runs whenever stdin is a TTY and
405
- // the user did not ask for --auto (it seeds from the existing config when
406
- // present); otherwise apply the config / smart defaults non-interactively.
407
- const interactive = opts.wizard || (!opts.auto && process.stdin.isTTY)
408
- let assignments = null
409
- let wizardRan = false
410
- if (interactive) {
411
- assignments = await runWizard({
412
- inventory,
413
- mcpReports,
414
- pluginReports,
415
- skills,
416
- staticBase,
417
- suggested,
418
- existingConfig: existing,
419
- personaBlocks,
420
- srcText,
421
- restrictBase,
422
- })
423
- if (assignments === null) return 0
424
- wizardRan = true
425
- } else if (opts.auto || existing || opts.dryRun) {
426
- // 既有配置 → 按配置合成(不变);fresh(无配置)→ 默认基础模板(仅 dsh
427
- // 基础工具,host/MCP 工具留给 Web 配置器 / 向导显式开启),--suggest 恢复
428
- // 智能默认自动纳入。--dry-run fresh 同样按此计算但不落盘。
429
- assignments = existing
430
- ? resolveAssignments(existing, suggested, staticBase)
431
- : opts.suggest
432
- ? suggested
433
- : baseAssignments(staticBase)
434
- } else {
435
- console.error(
436
- 'error: interactive wizard requires a terminal. Run with --auto for a non-interactive install, ' +
437
- 'or --config <file> to apply an existing config file. (--dry-run works without a TTY.)',
438
- )
439
- return 1
440
- }
274
+ // --auto 语义与插件首装 ensurePresetInstalled 共用同一构造(alloc.autoAssignments,
275
+ // 防两处逻辑漂移):既有配置 → 按配置合成(不变);fresh(无配置)→ 默认基础
276
+ // 模板(仅 dsh 基础工具),--suggest 恢复智能默认自动纳入。--dry-run fresh
277
+ // 同样按此计算但不落盘。
278
+ const assignments = autoAssignments({ existing, suggested, staticBase, suggest: opts.suggest })
441
279
 
442
280
  // Compose, validate and (unless dry-run) install via the shared generator.
443
- // saveConfigOnWrite:wizard 路径写用户选择(原行为);fresh 非交互安装把基础
444
- // 模板落进配置文件(显式 main_agent_extra: [] 锚定「未开启 host 工具」);
445
- // existing 路径绝不覆写用户配置文件。
281
+ // saveConfigOnWrite:fresh 非交互安装把基础模板落进配置文件(显式
282
+ // main_agent_extra: [] 锚定「未开启 host 工具」);existing 路径绝不覆写
283
+ // 用户配置文件。
446
284
  let composed
447
285
  try {
448
286
  composed = generateAndInstall(state, assignments, {
449
287
  dryRun: opts.dryRun,
450
- saveConfigOnWrite: wizardRan || (!existing && !opts.dryRun),
288
+ saveConfigOnWrite: !existing && !opts.dryRun,
451
289
  })
452
290
  } catch (err) {
453
291
  console.error(`error: ${err.message}`)
454
292
  return 1
455
293
  }
456
- const generatedText = composed.text
457
294
  const roleResults = composed.roleResults
458
295
  const staleNote = composed.staleNote
459
296
 
@@ -471,20 +308,28 @@ export async function main({ argv = process.argv.slice(2), bin = false } = {}) {
471
308
  removed: assignments.main_agent_remove ?? [],
472
309
  },
473
310
  })
474
- if (wizardRan) {
475
- console.log(`\n配置已写入 ${configFile}(改后重跑 ./install.sh --auto 应用)`)
476
- } else if (!existing && !opts.dryRun) {
311
+ if (!existing && !opts.dryRun) {
477
312
  // fresh 非交互安装:提示基础模板已落盘、host/MCP 工具去哪补(--suggest 可
478
313
  // 恢复智能默认自动纳入)。
479
314
  console.log(
480
- `\n基础模板已写入 ${configFile}:默认仅 dsh 基础工具,重启后在 设置 → 庖丁配置` +
481
- `(或 npx dsh-paoding --wizard)里自定义 host/MCP 工具。`,
315
+ `\n基础模板已写入 ${configFile}:默认仅 dsh 基础工具,重启后在侧栏底部「庖丁配置」入口` +
316
+ `里自定义 host/MCP 工具。`,
482
317
  )
483
318
  }
484
319
  const customRoles = Object.keys(assignments.roles).filter((n) => !ROLES.includes(n))
485
320
  if (customRoles.length > 0) {
486
321
  console.log(`自定义角色: ${customRoles.join(', ')}(toolName 已注入主 agent restrict config.allow)`)
487
322
  }
323
+ // 可续角色摘要(roles.<toolName>.background_mode = 'continuable',内置与自定义
324
+ // 角色一并列出;roles_remove 命中的角色其 background_mode 已被生成层忽略,不列)。
325
+ // 全部缺省(one-shot)时不打行,保持输出安静。
326
+ const removedRoleSet = new Set(assignments.roles_remove ?? [])
327
+ const continuableRoles = Object.entries(assignments.roles ?? {})
328
+ .filter(([toolName, role]) => role?.background_mode === 'continuable' && !removedRoleSet.has(toolName))
329
+ .map(([toolName]) => toolName)
330
+ if (continuableRoles.length > 0) {
331
+ console.log(`可续角色(continuable): ${continuableRoles.join(', ')}(委派失败先用 send_message 就地续修)`)
332
+ }
488
333
  const skillAssignments = Object.entries(assignments.skills ?? {}).filter(([, roleList]) => roleList.length > 0)
489
334
  if (skillAssignments.length > 0) {
490
335
  console.log(`技能引导: ${skillAssignments.map(([s, r]) => `${s} → ${r.join(', ')}`).join(';')}`)
@@ -501,22 +346,26 @@ export async function main({ argv = process.argv.slice(2), bin = false } = {}) {
501
346
  if (removedBuiltins.length > 0) {
502
347
  console.log(`内置角色已删除: ${removedBuiltins.join(', ')}(委派行、主 persona 引用与 restrict allow 已一并移除)`)
503
348
  }
504
- const namedMainAgent = normalizeMainAgentName(assignments.main_agent_name ?? null)
505
- if (namedMainAgent !== null) {
506
- console.log(`主 agent 名称: ${namedMainAgent}`)
349
+ // 主 agent 显示名(main_agent_display_name):仅 preset 显示名,persona 身份行不可改名。
350
+ const namedMainAgentDisplay = normalizeRoleName(assignments.main_agent_display_name ?? null, 'main_agent_display_name')
351
+ if (namedMainAgentDisplay !== null) {
352
+ console.log(`主 agent 显示名: ${namedMainAgentDisplay}(仅 preset 显示名,不动 persona 身份行)`)
507
353
  }
508
354
 
509
- // Optional: mount the visual config UI into the DSH Settings page.
510
- if (opts.configUi) {
511
- try {
512
- ensureConfigUiMount(dshHome, opts.dryRun)
513
- if (!opts.dryRun) {
514
- console.log('[config-ui] 完成。重启 DSH(dsh web)后在 设置 → 庖丁配置 中打开。')
515
- }
516
- } catch (err) {
517
- console.error(`error: config-ui 挂载失败: ${err.message}`)
518
- return 1
355
+ // 版本提示(best-effort,收尾最后一行、在既有输出之后,不改变退出码与输出
356
+ // 顺序):对比 GitHub latest release,有新版才打印;超时 2 秒,检测失败
357
+ // (断网 / 限流 / 解析异常)完全静默。--help / 参数错误等早退路径不检测,
358
+ // 此处已是安装 / 应用完成之后。
359
+ try {
360
+ const info = await getVersionInfo({ timeoutMs: 2000 })
361
+ if (info.updateAvailable && info.latest) {
362
+ console.log(
363
+ `\n💡 发现新版本 ${info.latest}(当前 v${info.current}):` +
364
+ `${info.releaseUrl || 'https://github.com/lifangjin/dsh-paoding/releases/latest'}`,
365
+ )
519
366
  }
367
+ } catch {
368
+ /* 静默:版本检测的任何意外都不影响安装结果 */
520
369
  }
521
370
  return 0
522
371
  }