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.
- package/LICENSE +21 -0
- package/README.md +95 -0
- package/README_en.md +95 -0
- package/bin/dsh-paoding.mjs +12 -0
- package/cordis.patch.yml +6 -0
- package/docs/architecture.md +292 -0
- package/docs/architecture_en.md +363 -0
- package/docs/configuration.md +551 -0
- package/docs/configuration_en.md +560 -0
- package/docs/images/1.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6.png +0 -0
- package/docs/installation.md +364 -0
- package/docs/installation_en.md +367 -0
- package/docs/orchestration.md +254 -0
- package/docs/orchestration_en.md +255 -0
- package/install.sh +31 -0
- package/package.json +29 -0
- package/plugins/paoding-config-ui/api-core.mjs +100 -0
- package/plugins/paoding-config-ui/index.mjs +327 -0
- package/plugins/paoding-config-ui/lib/base.css +615 -0
- package/plugins/paoding-config-ui/lib/client.js +1154 -0
- package/plugins/paoding-config-ui/package.json +23 -0
- package/plugins/paoding-config-ui/runtime-inventory.mjs +212 -0
- package/presets/orchestrator/agent.cordis.yml +323 -0
- package/presets/orchestrator/preset.yml +6 -0
- package/presets/orchestrator/restrict.mjs +191 -0
- package/tools/install.mjs +24 -0
- package/tools/lib/alloc.mjs +138 -0
- package/tools/lib/cli.mjs +522 -0
- package/tools/lib/compose.mjs +647 -0
- package/tools/lib/config.mjs +297 -0
- package/tools/lib/host.mjs +288 -0
- package/tools/lib/skills.mjs +146 -0
- package/tools/lib/spans.mjs +188 -0
- package/tools/lib/state.mjs +328 -0
- package/tools/lib/util.mjs +26 -0
- package/tools/lib/wizard.mjs +516 -0
- 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
|
+
}
|