dsh-paoding 0.2.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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +95 -0
  3. package/README_en.md +95 -0
  4. package/bin/dsh-paoding.mjs +12 -0
  5. package/cordis.patch.yml +6 -0
  6. package/docs/architecture.md +292 -0
  7. package/docs/architecture_en.md +363 -0
  8. package/docs/configuration.md +551 -0
  9. package/docs/configuration_en.md +560 -0
  10. package/docs/images/1.png +0 -0
  11. package/docs/images/2.png +0 -0
  12. package/docs/images/3.png +0 -0
  13. package/docs/images/4.png +0 -0
  14. package/docs/images/5.png +0 -0
  15. package/docs/images/6.png +0 -0
  16. package/docs/installation.md +364 -0
  17. package/docs/installation_en.md +367 -0
  18. package/docs/orchestration.md +254 -0
  19. package/docs/orchestration_en.md +255 -0
  20. package/install.sh +31 -0
  21. package/package.json +29 -0
  22. package/plugins/paoding-config-ui/api-core.mjs +100 -0
  23. package/plugins/paoding-config-ui/index.mjs +327 -0
  24. package/plugins/paoding-config-ui/lib/base.css +615 -0
  25. package/plugins/paoding-config-ui/lib/client.js +1154 -0
  26. package/plugins/paoding-config-ui/package.json +23 -0
  27. package/plugins/paoding-config-ui/runtime-inventory.mjs +212 -0
  28. package/presets/orchestrator/agent.cordis.yml +323 -0
  29. package/presets/orchestrator/preset.yml +6 -0
  30. package/presets/orchestrator/restrict.mjs +191 -0
  31. package/tools/install.mjs +24 -0
  32. package/tools/lib/alloc.mjs +138 -0
  33. package/tools/lib/cli.mjs +522 -0
  34. package/tools/lib/compose.mjs +647 -0
  35. package/tools/lib/config.mjs +297 -0
  36. package/tools/lib/host.mjs +288 -0
  37. package/tools/lib/skills.mjs +146 -0
  38. package/tools/lib/spans.mjs +188 -0
  39. package/tools/lib/state.mjs +328 -0
  40. package/tools/lib/util.mjs +26 -0
  41. package/tools/lib/wizard.mjs +516 -0
  42. package/tools/lib/yaml.mjs +266 -0
@@ -0,0 +1,522 @@
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。
9
+ */
10
+ import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs'
11
+ import { homedir } from 'node:os'
12
+ import path from 'node:path'
13
+ import { REPO_ROOT, ROLES, SRC_DIR, warn } from './util.mjs'
14
+ import { normalizeMainAgentName } from './config.mjs'
15
+ import { removedReason } from './host.mjs'
16
+ import { collectState, generateAndInstall } from './state.mjs'
17
+ import { runWizard } from './wizard.mjs'
18
+ import { baseAssignments, resolveAssignments } from './alloc.mjs'
19
+
20
+ // ── CLI ─────────────────────────────────────────────────────────────────────
21
+
22
+ export function printHelp() {
23
+ console.log(`dsh-paoding installer — installs the orchestrator preset with host-aware tool filtering
24
+
25
+ Usage:
26
+ node tools/install.mjs [options]
27
+ ./install.sh [options]
28
+ npx dsh-paoding [options]
29
+ npm exec dsh-paoding -- [options]
30
+
31
+ Options:
32
+ --profile <name> Profile whose patch layer is scanned (default: web).
33
+ Reads $DSH_HOME/profiles/<name>/cordis.patch.yml; skipped
34
+ when the file does not exist.
35
+ --patch <file> Extra patch file(s) to scan, in addition to the home and
36
+ profile layers (repeatable).
37
+ --config <file> Configuration file with role/tool assignments
38
+ (default: $DSH_HOME/dsh-paoding.config.yml).
39
+ --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).
45
+ --dry-run Print the detection report and the generated allow lists
46
+ without writing anything; exit 0.
47
+ --suggest Fresh install without a config file: seed smart defaults
48
+ from the detected host/MCP tools instead of the base
49
+ 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
+ --help Show this help and exit.
59
+
60
+ Environment:
61
+ DSH_HOME Directory holding cordis.patch.yml etc. (default: $HOME/.dsh)
62
+
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).
70
+ Pass --suggest to fall back to the smart defaults that fold detected host
71
+ tools in automatically.`)
72
+ }
73
+
74
+ 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
+ for (let i = 0; i < argv.length; i++) {
77
+ const arg = argv[i]
78
+ if (arg === '--help' || arg === '-h') {
79
+ opts.help = true
80
+ } else if (arg === '--dry-run') {
81
+ opts.dryRun = true
82
+ } else if (arg === '--auto') {
83
+ 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
+ } else if (arg === '--suggest') {
93
+ // fresh 安装时用智能默认(自动纳入检测到的 host 工具)而非基础模板。
94
+ opts.suggest = true
95
+ } else if (arg === '--config') {
96
+ const value = argv[++i]
97
+ if (value === undefined || value.startsWith('--')) {
98
+ throw new Error('error: --config requires a file path')
99
+ }
100
+ opts.config = value
101
+ } else if (arg.startsWith('--config=')) {
102
+ opts.config = arg.slice('--config='.length)
103
+ } else if (arg === '--profile') {
104
+ const value = argv[++i]
105
+ if (value === undefined || value.startsWith('--')) {
106
+ throw new Error('error: --profile requires a value')
107
+ }
108
+ opts.profile = value
109
+ } else if (arg.startsWith('--profile=')) {
110
+ opts.profile = arg.slice('--profile='.length)
111
+ } else if (arg === '--patch') {
112
+ const value = argv[++i]
113
+ if (value === undefined || value.startsWith('--')) {
114
+ throw new Error('error: --patch requires a file path')
115
+ }
116
+ opts.patches.push(value)
117
+ } else if (arg.startsWith('--patch=')) {
118
+ opts.patches.push(arg.slice('--patch='.length))
119
+ } else {
120
+ throw new Error(`error: unknown option: ${arg}\nrun with --help for usage`)
121
+ }
122
+ }
123
+ return opts
124
+ }
125
+
126
+ // ── report & install ────────────────────────────────────────────────────────
127
+
128
+ export function printReport({ scanned, mcpReports, pluginReports, roleResults, dstDir, dryRun, staleNote, mainAgent }) {
129
+ const pad = (s, n) => String(s).padEnd(n)
130
+ console.log('dsh-paoding installer — orchestrator preset (host-aware tool filtering)')
131
+ console.log(` preset source : ${SRC_DIR}`)
132
+ console.log(` target : ${dstDir}${dryRun ? ' [dry-run: nothing will be written]' : ''}`)
133
+ console.log()
134
+ console.log('patch layers scanned:')
135
+ for (const s of scanned) {
136
+ const status =
137
+ s.status === 'parsed' ? '' : s.status === 'missing' ? ' (skipped: not found)' : ' (skipped: unparseable)'
138
+ console.log(` - ${s.path}${status}`)
139
+ }
140
+ console.log()
141
+ console.log('detected host tools:')
142
+ if (mcpReports.length > 0) {
143
+ console.log(' mcp servers:')
144
+ for (const r of mcpReports) {
145
+ const sourceLabel =
146
+ r.source === 'static' ? 'static table' : r.source === 'handshake' ? 'live handshake' : 'handshake failed'
147
+ const count = r.error ? `0 tools (${r.error})` : `${r.count} tool${r.count === 1 ? '' : 's'}`
148
+ console.log(` ${pad(r.serverName, 16)}${pad(r.transport ?? '-', 20)}${count} (${sourceLabel})`)
149
+ }
150
+ } else {
151
+ console.log(' mcp servers: none enabled')
152
+ }
153
+ if (pluginReports.length > 0) {
154
+ console.log(' local plugins:')
155
+ for (const p of pluginReports) {
156
+ console.log(` ${pad(p.label, 16)}${pad('plugin', 20)}${p.tools.join(', ')} (enabled)`)
157
+ }
158
+ } else {
159
+ console.log(' local plugins: none detected')
160
+ }
161
+ console.log()
162
+ console.log('role allow lists (intent -> kept):')
163
+ for (const rr of roleResults) {
164
+ console.log(` ${pad(rr.role, 16)} ${rr.intent.length} -> ${rr.kept.length}`)
165
+ }
166
+ if (mainAgent) {
167
+ console.log(` ${pad('主 agent', 16)} ${mainAgent.baseCount} 基础 + ${mainAgent.extra.length} 追加 − ${mainAgent.removed.length} 移除`)
168
+ if (mainAgent.removed.length > 0) console.log(` removed: ${mainAgent.removed.join(', ')}`)
169
+ }
170
+ const removed = roleResults.flatMap((rr) =>
171
+ rr.removed.map((name) => ({ role: rr.role, name, reason: removedReason(name, mcpReports) })),
172
+ )
173
+ if (removed.length > 0) {
174
+ console.log(' removed (would fail tools.restrict() at runtime if left in):')
175
+ for (const r of removed) {
176
+ console.log(` - [${r.role}] ${r.name} (${r.reason})`)
177
+ }
178
+ } else {
179
+ console.log(' removed: none — every host-dependent allow entry is enabled')
180
+ }
181
+ if (staleNote) {
182
+ console.log('\nnote: 检测到 patch 配置在本次安装后有过修改,改动后请重跑 ./install.sh')
183
+ }
184
+ console.log()
185
+ console.log(
186
+ `${dryRun ? '[dry-run] would write' : 'wrote'} ${path.join(dstDir, 'agent.cordis.yml')}` +
187
+ (dryRun ? '' : ' (allow lists rewritten)'),
188
+ )
189
+ console.log(`${dryRun ? '[dry-run] would copy' : 'copied'} preset.yml, restrict.mjs as-is`)
190
+ if (!dryRun) {
191
+ console.log()
192
+ console.log('next steps:')
193
+ console.log(' 1. restart the dsh host (or create a new session)')
194
+ console.log(' 2. pick 「编排模式 (Orchestrator)」 in the new-session preset selector')
195
+ console.log(' (or set it as default in Settings → Agent Presets)')
196
+ console.log(' 3. ask the main agent a task; it will self-answer internal file')
197
+ console.log(' searches and delegate web research / design / implementation via')
198
+ console.log(' the search_external / design / implement tools')
199
+ }
200
+ console.log()
201
+ console.log(`uninstall: rm -rf "${dstDir}"`)
202
+ }
203
+
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
+ // ── main ────────────────────────────────────────────────────────────────────
350
+
351
+ /**
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 不覆盖)。
356
+ */
357
+ export async function main({ argv = process.argv.slice(2), bin = false } = {}) {
358
+ let opts
359
+ try {
360
+ opts = parseArgs(argv)
361
+ } catch (err) {
362
+ console.error(err.message)
363
+ return 1
364
+ }
365
+ if (opts.help) {
366
+ printHelp()
367
+ return 0
368
+ }
369
+ if (bin) {
370
+ if (!opts.wizard) opts.auto ||= true
371
+ if (!opts.noUi) opts.configUi = true
372
+ }
373
+
374
+ const dshHome = process.env.DSH_HOME || path.join(homedir(), '.dsh')
375
+ const configFile = opts.config || path.join(dshHome, 'dsh-paoding.config.yml')
376
+
377
+ // 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
+ })
386
+ const {
387
+ srcText,
388
+ blocks,
389
+ personaBlocks,
390
+ restrictBase,
391
+ existing,
392
+ effectiveProfile,
393
+ scanned,
394
+ mcpReports,
395
+ pluginReports,
396
+ inventory,
397
+ skills,
398
+ staticBase,
399
+ suggested,
400
+ dstDir,
401
+ dstAgentFile,
402
+ } = 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
+ }
441
+
442
+ // Compose, validate and (unless dry-run) install via the shared generator.
443
+ // saveConfigOnWrite:wizard 路径写用户选择(原行为);fresh 非交互安装把基础
444
+ // 模板落进配置文件(显式 main_agent_extra: [] 锚定「未开启 host 工具」);
445
+ // existing 路径绝不覆写用户配置文件。
446
+ let composed
447
+ try {
448
+ composed = generateAndInstall(state, assignments, {
449
+ dryRun: opts.dryRun,
450
+ saveConfigOnWrite: wizardRan || (!existing && !opts.dryRun),
451
+ })
452
+ } catch (err) {
453
+ console.error(`error: ${err.message}`)
454
+ return 1
455
+ }
456
+ const generatedText = composed.text
457
+ const roleResults = composed.roleResults
458
+ const staleNote = composed.staleNote
459
+
460
+ printReport({
461
+ scanned,
462
+ mcpReports,
463
+ pluginReports,
464
+ roleResults,
465
+ dstDir,
466
+ dryRun: opts.dryRun,
467
+ staleNote,
468
+ mainAgent: {
469
+ baseCount: restrictBase.length,
470
+ extra: assignments.main_agent_extra ?? [],
471
+ removed: assignments.main_agent_remove ?? [],
472
+ },
473
+ })
474
+ if (wizardRan) {
475
+ console.log(`\n配置已写入 ${configFile}(改后重跑 ./install.sh --auto 应用)`)
476
+ } else if (!existing && !opts.dryRun) {
477
+ // fresh 非交互安装:提示基础模板已落盘、host/MCP 工具去哪补(--suggest 可
478
+ // 恢复智能默认自动纳入)。
479
+ console.log(
480
+ `\n基础模板已写入 ${configFile}:默认仅 dsh 基础工具,重启后在 设置 → 庖丁配置` +
481
+ `(或 npx dsh-paoding --wizard)里自定义 host/MCP 工具。`,
482
+ )
483
+ }
484
+ const customRoles = Object.keys(assignments.roles).filter((n) => !ROLES.includes(n))
485
+ if (customRoles.length > 0) {
486
+ console.log(`自定义角色: ${customRoles.join(', ')}(toolName 已注入主 agent restrict config.allow)`)
487
+ }
488
+ const skillAssignments = Object.entries(assignments.skills ?? {}).filter(([, roleList]) => roleList.length > 0)
489
+ if (skillAssignments.length > 0) {
490
+ console.log(`技能引导: ${skillAssignments.map(([s, r]) => `${s} → ${r.join(', ')}`).join(';')}`)
491
+ }
492
+ const mainAgentSkills = (assignments.main_agent_skills ?? []).filter((n) => typeof n === 'string' && n !== '')
493
+ if (mainAgentSkills.length > 0) {
494
+ console.log(`主 agent 技能(read 按需): ${mainAgentSkills.join(', ')}`)
495
+ }
496
+ const mainAgentInlineSkills = (assignments.main_agent_skills_inline ?? []).filter((n) => typeof n === 'string' && n !== '')
497
+ if (mainAgentInlineSkills.length > 0) {
498
+ console.log(`主 agent 技能(内联): ${mainAgentInlineSkills.join(', ')}`)
499
+ }
500
+ const removedBuiltins = (assignments.roles_remove ?? []).filter((role) => ROLES.includes(role))
501
+ if (removedBuiltins.length > 0) {
502
+ console.log(`内置角色已删除: ${removedBuiltins.join(', ')}(委派行、主 persona 引用与 restrict allow 已一并移除)`)
503
+ }
504
+ const namedMainAgent = normalizeMainAgentName(assignments.main_agent_name ?? null)
505
+ if (namedMainAgent !== null) {
506
+ console.log(`主 agent 名称: ${namedMainAgent}`)
507
+ }
508
+
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
519
+ }
520
+ }
521
+ return 0
522
+ }