dsh-paoding 0.4.4 → 0.4.6
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/README.md +5 -4
- package/README_en.md +5 -4
- package/bin/dsh-paoding.mjs +22 -10
- package/docs/architecture.md +6 -5
- package/docs/architecture_en.md +8 -7
- package/docs/configuration.md +9 -1
- package/docs/configuration_en.md +9 -1
- package/docs/installation.md +17 -7
- package/docs/installation_en.md +14 -6
- package/docs/orchestration.md +2 -2
- package/docs/orchestration_en.md +2 -2
- package/package.json +3 -2
- package/plugins/paoding-config-ui/api-core.mjs +171 -2
- package/plugins/paoding-config-ui/icon.svg +8 -0
- package/plugins/paoding-config-ui/index.mjs +88 -6
- package/plugins/paoding-config-ui/lib/base.css +244 -0
- package/plugins/paoding-config-ui/lib/client.js +517 -46
- package/plugins/paoding-config-ui/package.json +7 -2
- package/plugins/paoding-config-ui/runtime-inventory.mjs +22 -0
- package/plugins/paoding-config-ui/stats-core.mjs +362 -0
- package/presets/orchestrator/agent.cordis.yml +2 -1
- package/presets/orchestrator/restrict.mjs +1 -1
- package/tools/lib/cli.mjs +298 -8
- package/tools/lib/config.mjs +117 -0
- package/tools/lib/doctor.mjs +3 -13
- package/tools/lib/lifecycle.mjs +264 -0
package/tools/lib/cli.mjs
CHANGED
|
@@ -11,18 +11,31 @@
|
|
|
11
11
|
* host(removedReason)、state(PRESET_ASIS_FILES / collectState /
|
|
12
12
|
* generateAndInstall)、alloc(autoAssignments,--auto 语义与插件首装共享)、
|
|
13
13
|
* health(captureWarnings / healthCheck,--dry-run 报告的结构化体检)、
|
|
14
|
-
* doctor(runDoctor,doctor 子命令的只读体检)、
|
|
15
|
-
*
|
|
14
|
+
* doctor(runDoctor,doctor 子命令的只读体检)、lifecycle(uninstall 与
|
|
15
|
+
* doctor --fix 共用的清理原语与判据)、version(getVersionInfo,main 收尾的
|
|
16
|
+
* 新版提示)、yaml(loadYaml,--fix 的配置对账读法)以及 node:os / node:path /
|
|
17
|
+
* node:readline/promises。
|
|
16
18
|
*/
|
|
17
19
|
import { homedir } from 'node:os'
|
|
18
20
|
import path from 'node:path'
|
|
21
|
+
import readline from 'node:readline/promises'
|
|
19
22
|
import { PROFILE_NAME_RE, ROLES, SRC_DIR } from './util.mjs'
|
|
20
23
|
import { normalizeRoleName } from './config.mjs'
|
|
24
|
+
import { loadYaml } from './yaml.mjs'
|
|
21
25
|
import { removedReason } from './host.mjs'
|
|
22
26
|
import { PRESET_ASIS_FILES, collectState, generateAndInstall } from './state.mjs'
|
|
23
27
|
import { autoAssignments } from './alloc.mjs'
|
|
24
28
|
import { captureWarnings, healthCheck } from './health.mjs'
|
|
25
29
|
import { runDoctor } from './doctor.mjs'
|
|
30
|
+
import {
|
|
31
|
+
cleanupPaodingArtifacts,
|
|
32
|
+
foreignPresetDirsRemaining,
|
|
33
|
+
loadConfigForReconcile,
|
|
34
|
+
orphanManagedRows,
|
|
35
|
+
planUninstall,
|
|
36
|
+
removeConfigAndBackups,
|
|
37
|
+
workspacePresetOrphans,
|
|
38
|
+
} from './lifecycle.mjs'
|
|
26
39
|
import { getVersionInfo } from './version.mjs'
|
|
27
40
|
|
|
28
41
|
// ── CLI ─────────────────────────────────────────────────────────────────────
|
|
@@ -40,6 +53,7 @@ export function printHelp() {
|
|
|
40
53
|
Usage:
|
|
41
54
|
node tools/install.mjs [options]
|
|
42
55
|
node tools/install.mjs doctor [options]
|
|
56
|
+
node tools/install.mjs uninstall [options]
|
|
43
57
|
|
|
44
58
|
Options:
|
|
45
59
|
--profile <name> Profile whose patch layer is scanned (default: web).
|
|
@@ -57,6 +71,11 @@ Options:
|
|
|
57
71
|
--suggest Fresh install without a config file: seed smart defaults
|
|
58
72
|
from the detected host/MCP tools instead of the base
|
|
59
73
|
template (host tools are folded in automatically).
|
|
74
|
+
--purge uninstall 专用:连配置文件与其 .bak* 备份一起删(默认保留)。
|
|
75
|
+
--yes uninstall / doctor --fix 专用:跳过逐类确认直接执行;
|
|
76
|
+
非交互终端(非 TTY)下没带 --yes 时只列计划不落盘,退出码 2。
|
|
77
|
+
--fix doctor 专用:体检后自动修两类可安全处理的问题(孤儿工作区
|
|
78
|
+
预设目录、孤儿/过期声明行),其余发现只给处理建议不代做。
|
|
60
79
|
--help Show this help and exit.
|
|
61
80
|
|
|
62
81
|
Subcommands:
|
|
@@ -64,6 +83,13 @@ Subcommands:
|
|
|
64
83
|
preset track vs host version, managed patch rows, orphan
|
|
65
84
|
workspace presets, config drift, version marker) and a
|
|
66
85
|
summary; exit 1 only on error-level findings.
|
|
86
|
+
doctor --fix 体检并清理:只自动修 --fix 所述两类,输出逐项标注
|
|
87
|
+
「已修 / 需手动」;托管块清理写盘前自动备份 .bak-1。
|
|
88
|
+
uninstall 清理庖丁写进宿主目录的东西:cordis.patch.yml 托管声明块、
|
|
89
|
+
.agent-presets 下的 orchestrator* 预设目录、.generator-version
|
|
90
|
+
标记。默认保留 dsh-paoding.config.yml 及其备份(用户数据),
|
|
91
|
+
--purge 才连配置一起删。不碰插件包自身——包本体用
|
|
92
|
+
dsh plugin remove dsh-paoding 卸载。
|
|
67
93
|
|
|
68
94
|
Environment:
|
|
69
95
|
DSH_HOME Directory holding cordis.patch.yml etc. (default: $HOME/.dsh)
|
|
@@ -84,14 +110,17 @@ function assertProfileName(value) {
|
|
|
84
110
|
}
|
|
85
111
|
|
|
86
112
|
export function parseArgs(argv) {
|
|
87
|
-
const opts = { command: 'install', profile: null, patches: [], dryRun: false, help: false, auto: false, config: undefined, suggest: false }
|
|
113
|
+
const opts = { command: 'install', profile: null, patches: [], dryRun: false, help: false, auto: false, config: undefined, suggest: false, purge: false, yes: false, fix: false }
|
|
88
114
|
for (let i = 0; i < argv.length; i++) {
|
|
89
115
|
const arg = argv[i]
|
|
90
116
|
if (arg === 'doctor') {
|
|
91
|
-
// 子命令:doctor =
|
|
92
|
-
|
|
93
|
-
//
|
|
117
|
+
// 子命令:doctor = 只读体检(--fix 升级为「体检并修」);缺省 install
|
|
118
|
+
//(原 installer 主流程)。位置参数只认 doctor / uninstall 两个,其余仍按
|
|
119
|
+
// 未知选项报错;--help / --profile / --config 等照常解析,子命令分支共用。
|
|
94
120
|
opts.command = 'doctor'
|
|
121
|
+
} else if (arg === 'uninstall') {
|
|
122
|
+
// 子命令:uninstall = 清理庖丁写进宿主目录的东西(不动插件包自身)。
|
|
123
|
+
opts.command = 'uninstall'
|
|
95
124
|
} else if (arg === '--help' || arg === '-h') {
|
|
96
125
|
opts.help = true
|
|
97
126
|
} else if (arg === '--dry-run') {
|
|
@@ -101,6 +130,16 @@ export function parseArgs(argv) {
|
|
|
101
130
|
} else if (arg === '--suggest') {
|
|
102
131
|
// fresh 安装时用智能默认(自动纳入检测到的 host 工具)而非基础模板。
|
|
103
132
|
opts.suggest = true
|
|
133
|
+
} else if (arg === '--purge') {
|
|
134
|
+
// 仅 uninstall:连配置文件与其备份一起删(默认保留用户配置)。
|
|
135
|
+
opts.purge = true
|
|
136
|
+
} else if (arg === '--yes') {
|
|
137
|
+
// uninstall / doctor --fix:跳过逐类确认直接执行;非 TTY 无 --yes 时
|
|
138
|
+
// 只列计划不落盘(防呆)。
|
|
139
|
+
opts.yes = true
|
|
140
|
+
} else if (arg === '--fix') {
|
|
141
|
+
// 仅 doctor:对体检发现里可安全自动修的两类动手(孤儿目录、孤儿声明行)。
|
|
142
|
+
opts.fix = true
|
|
104
143
|
} else if (arg === '--config') {
|
|
105
144
|
const value = argv[++i]
|
|
106
145
|
if (value === undefined || value.startsWith('--')) {
|
|
@@ -266,6 +305,230 @@ export function printDoctorReport(report, { dshHome, configFile }) {
|
|
|
266
305
|
}
|
|
267
306
|
}
|
|
268
307
|
|
|
308
|
+
// ── 生命周期清理(uninstall / doctor --fix)────────────────────────────────
|
|
309
|
+
//
|
|
310
|
+
// 「怎么删」全部在 lib/lifecycle.mjs(判据与 doctor 同源);这里只做三件事:
|
|
311
|
+
// 计划打印、确认口径(--yes 直干 / TTY 逐类问 / 非 TTY 防呆拒做)、结果汇报。
|
|
312
|
+
|
|
313
|
+
/** 非 TTY(管道 / 重定向)下逐类确认问不了,只能列计划等显式 --yes。 */
|
|
314
|
+
function stdinIsTTY() {
|
|
315
|
+
return Boolean(process.stdin?.isTTY)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** 逐类确认(仅 TTY 调用):回车 / 非 y 一律视为拒绝——删东西默认说不。 */
|
|
319
|
+
async function confirm(question) {
|
|
320
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
321
|
+
try {
|
|
322
|
+
const answer = (await rl.question(`${question} [y/N] `)).trim().toLowerCase()
|
|
323
|
+
return answer === 'y' || answer === 'yes'
|
|
324
|
+
} finally {
|
|
325
|
+
rl.close()
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** 包本体卸载提示(uninstall 收尾专用):插件包的卸载通道只有 dsh plugin
|
|
330
|
+
* remove,本命令绝不碰自己的包目录。 */
|
|
331
|
+
function printPluginRemovalHint() {
|
|
332
|
+
console.log()
|
|
333
|
+
console.log('收尾:插件包本体走插件通道卸载——')
|
|
334
|
+
console.log(' dsh plugin remove dsh-paoding')
|
|
335
|
+
console.log('(desktop profile 由 Electron 守卫拦着,请在 DSH 面板 / 宿主侧处理)')
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** uninstall 的计划打印(防呆拒做时同一份,见 runUninstall)。 */
|
|
339
|
+
function printUninstallPlan(plan, { purge }) {
|
|
340
|
+
console.log('dsh-paoding uninstall — 清理庖丁写进宿主目录的东西(不碰插件包自身)')
|
|
341
|
+
console.log(` DSH_HOME : ${plan.dshHome}`)
|
|
342
|
+
console.log(` 配置文件 : ${plan.configFile}`)
|
|
343
|
+
console.log()
|
|
344
|
+
console.log('将清理:')
|
|
345
|
+
if (plan.blockPresent && !plan.blockCorrupt) {
|
|
346
|
+
console.log(` - ${plan.patchFile} 里的托管声明块(${plan.blockRowCount} 条声明行;写前备份 ${path.basename(plan.patchFile)}.bak-1)`)
|
|
347
|
+
}
|
|
348
|
+
if (plan.blockCorrupt) {
|
|
349
|
+
console.log(' - 托管声明块已损坏(有起始标记无结束标记):自动清理做不了,需手工整块删除或补回结束标记')
|
|
350
|
+
}
|
|
351
|
+
if (plan.presetDirs.length > 0 || plan.markerExists) {
|
|
352
|
+
const markerNote = plan.markerExists ? '(含版本标记 .generator-version)' : ''
|
|
353
|
+
console.log(` - 预设目录 ${plan.presetDirs.join('、') || '(无)'}${markerNote}`)
|
|
354
|
+
}
|
|
355
|
+
if (purge && (plan.configExists || plan.configBackups.length > 0)) {
|
|
356
|
+
const backups = plan.configBackups.length > 0 ? ` 及 ${plan.configBackups.length} 个 .bak* 备份` : ''
|
|
357
|
+
console.log(` - 配置文件 ${plan.configFile}${backups}(--purge)`)
|
|
358
|
+
}
|
|
359
|
+
console.log('将保留:')
|
|
360
|
+
if (!purge && (plan.configExists || plan.configBackups.length > 0)) {
|
|
361
|
+
console.log(` - 配置文件 ${plan.configFile}${plan.configBackups.length > 0 ? ` 及 ${plan.configBackups.length} 个 .bak* 备份` : ''}(用户数据;--purge 才删)`)
|
|
362
|
+
}
|
|
363
|
+
const foreign = foreignPresetDirsRemaining(plan.dshHome)
|
|
364
|
+
if (foreign.length > 0) {
|
|
365
|
+
console.log(` - .agent-presets 下非庖丁目录:${foreign.join('、')}`)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** uninstall 主流程,返回退出码。 */
|
|
370
|
+
async function runUninstall({ dshHome, configFile, purge, yes }) {
|
|
371
|
+
const plan = planUninstall({ dshHome, configFile })
|
|
372
|
+
|
|
373
|
+
// 无事可做:不打扰确认(反正一个字节都不会动),直接交代现状。
|
|
374
|
+
const hasWork =
|
|
375
|
+
plan.blockPresent ||
|
|
376
|
+
plan.presetDirs.length > 0 ||
|
|
377
|
+
plan.markerExists ||
|
|
378
|
+
(purge && (plan.configExists || plan.configBackups.length > 0))
|
|
379
|
+
if (!hasWork) {
|
|
380
|
+
console.log('dsh-paoding uninstall — 宿主目录里没有庖丁的安装产物(托管声明块、orchestrator* 预设目录、版本标记均不在场),无事可做。')
|
|
381
|
+
if (plan.configExists) console.log(`配置已保留在 ${configFile},确认不要可手动删(连同 .bak* 备份)。`)
|
|
382
|
+
printPluginRemovalHint()
|
|
383
|
+
return 0
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
printUninstallPlan(plan, { purge })
|
|
387
|
+
|
|
388
|
+
// 防呆:非交互终端问不了,宁可不做——列出计划,等显式 --yes。
|
|
389
|
+
if (!yes && !stdinIsTTY()) {
|
|
390
|
+
console.error()
|
|
391
|
+
console.error('当前不是交互终端且未加 --yes:只列计划不执行,重跑时加 --yes 确认。')
|
|
392
|
+
return 2
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 逐类确认(--yes 跳过;损坏块不进确认——自动路径反正删不动,按需手工报)。
|
|
396
|
+
let doBlock = plan.blockPresent && !plan.blockCorrupt
|
|
397
|
+
let doDirs = plan.presetDirs.length > 0
|
|
398
|
+
let doConfig = purge && (plan.configExists || plan.configBackups.length > 0)
|
|
399
|
+
if (!yes) {
|
|
400
|
+
if (doBlock) doBlock = await confirm(`移除 ${plan.patchFile} 里的庖丁托管声明块?`)
|
|
401
|
+
if (doDirs) doDirs = await confirm(`删除预设目录 ${plan.presetDirs.join('、')}?`)
|
|
402
|
+
if (doConfig) {
|
|
403
|
+
doConfig = await confirm(
|
|
404
|
+
`删除配置文件及其备份(${configFile}${plan.configBackups.length > 0 ? ` + ${plan.configBackups.length} 个 .bak*` : ''})?`,
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const cleanup = cleanupPaodingArtifacts(dshHome, {
|
|
410
|
+
stripBlock: doBlock,
|
|
411
|
+
removeDirIds: doDirs ? plan.presetDirs : [],
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
console.log()
|
|
415
|
+
if (plan.blockCorrupt) {
|
|
416
|
+
console.log(`⚠ 托管声明块损坏(有起始标记无结束标记),自动清理已跳过——请手工补回结束标记或整块删除 ${plan.patchFile}`)
|
|
417
|
+
} else if (doBlock) {
|
|
418
|
+
if (cleanup.blockRemoved) {
|
|
419
|
+
console.log(`✓ 已移除托管声明块${cleanup.backupPath ? `(原文件备份:${cleanup.backupPath})` : ''}`)
|
|
420
|
+
} else {
|
|
421
|
+
console.log(`⚠ 托管声明块未能移除,请检查 ${plan.patchFile}`)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (doDirs && cleanup.removedDirs.length > 0) {
|
|
425
|
+
console.log(`✓ 已删除预设目录:${cleanup.removedDirs.join('、')}${plan.markerExists ? ',版本标记 .generator-version 随目录一并删除' : ''}`)
|
|
426
|
+
}
|
|
427
|
+
if (cleanup.markerRemoved) {
|
|
428
|
+
console.log('✓ 已删除版本标记 .generator-version')
|
|
429
|
+
}
|
|
430
|
+
for (const failure of cleanup.dirFailures) {
|
|
431
|
+
console.log(`⚠ 预设目录删除失败:${failure.id}(${failure.error})——请手工处理`)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (purge) {
|
|
435
|
+
if (doConfig) {
|
|
436
|
+
const removed = removeConfigAndBackups(configFile)
|
|
437
|
+
if (removed.length > 0) {
|
|
438
|
+
console.log(`✓ 已删除配置文件及其备份(${removed.length} 个文件)`)
|
|
439
|
+
}
|
|
440
|
+
} else if (plan.configExists || plan.configBackups.length > 0) {
|
|
441
|
+
console.log(`ℹ 配置未删(未确认):${configFile}`)
|
|
442
|
+
}
|
|
443
|
+
} else if (plan.configExists || plan.configBackups.length > 0) {
|
|
444
|
+
console.log(`ℹ 配置已保留在 ${configFile},确认不要可手动删(连同 .bak* 备份)。`)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
printPluginRemovalHint()
|
|
448
|
+
|
|
449
|
+
const failed =
|
|
450
|
+
cleanup.dirFailures.length > 0 ||
|
|
451
|
+
plan.blockCorrupt ||
|
|
452
|
+
(doBlock && !cleanup.blockRemoved)
|
|
453
|
+
return failed ? 1 : 0
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** doctor --fix 对「预设漂移」这类体检行文的补充建议:各行 detail 多数自带
|
|
457
|
+
* 处理建议,这里只补体检行文没点到的 --auto 路径,不逐行复读。 */
|
|
458
|
+
const DOCTOR_FIX_HINTS = {
|
|
459
|
+
预设漂移: '(或运行 dsh-paoding --auto 重新生成)',
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** doctor --fix 主流程,返回退出码:全量只读体检(runDoctor,与不带 --fix 的
|
|
463
|
+
* doctor 同一份诊断)→ 可修两类动手 → 逐项标注「已修 / 需手动」。可修判据
|
|
464
|
+
* 与 doctor 同源(lifecycle 的孤儿目录 / 孤儿声明行两个函数),不另立标准。 */
|
|
465
|
+
async function runDoctorFix({ dshHome, configFile, profile, yes }) {
|
|
466
|
+
const report = await runDoctor({ dshHome, configFile, profile })
|
|
467
|
+
|
|
468
|
+
// 配置存在但不可解析时目录对账不可信(doctor 该项同样降级 skip)——只报不修。
|
|
469
|
+
const reconcile = loadConfigForReconcile(configFile, loadYaml(dshHome))
|
|
470
|
+
const dirsReconcilable = !reconcile.exists || reconcile.usable
|
|
471
|
+
const orphanDirIds = dirsReconcilable ? workspacePresetOrphans(dshHome, reconcile.config).orphans : []
|
|
472
|
+
const orphanRowIds = orphanManagedRows(dshHome).map((row) => row.rowId)
|
|
473
|
+
|
|
474
|
+
let doDirs = orphanDirIds.length > 0
|
|
475
|
+
let doRows = orphanRowIds.length > 0
|
|
476
|
+
|
|
477
|
+
// 防呆:非交互终端问不了——只列将修什么,等显式 --yes(退出码 2)。
|
|
478
|
+
const planOnly = !yes && !stdinIsTTY()
|
|
479
|
+
if (!planOnly && !yes) {
|
|
480
|
+
if (doDirs) doDirs = await confirm(`删除孤儿预设目录 ${orphanDirIds.join('、')}?`)
|
|
481
|
+
if (doRows) doRows = await confirm(`撤下孤儿声明行 ${orphanRowIds.join('、')}?`)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const cleanup = planOnly
|
|
485
|
+
? null
|
|
486
|
+
: cleanupPaodingArtifacts(dshHome, {
|
|
487
|
+
pruneRowIds: doRows ? orphanRowIds : [],
|
|
488
|
+
removeDirIds: doDirs ? orphanDirIds : [],
|
|
489
|
+
})
|
|
490
|
+
const dirsFixed = cleanup !== null && doDirs && cleanup.removedDirs.length > 0
|
|
491
|
+
const rowsFixed = cleanup !== null && doRows && cleanup.prunedRowIds.length > 0
|
|
492
|
+
|
|
493
|
+
const marks = { ok: '✔', error: '✘', warn: '⚠', skip: '–' }
|
|
494
|
+
console.log(planOnly
|
|
495
|
+
? 'dsh-paoding doctor --fix — 体检完成,以下为可自动修复项(未执行)'
|
|
496
|
+
: 'dsh-paoding doctor --fix — 体检并清理可自动修复的问题')
|
|
497
|
+
console.log(` DSH_HOME : ${dshHome}`)
|
|
498
|
+
console.log(` 配置文件 : ${configFile}`)
|
|
499
|
+
console.log()
|
|
500
|
+
let fixedCount = 0
|
|
501
|
+
let manualCount = 0
|
|
502
|
+
for (const item of report.items) {
|
|
503
|
+
const dirsHit = item.label === '工作区预设目录' && /孤儿目录|全部视为孤儿/.test(item.detail) && (dirsFixed || (planOnly && doDirs))
|
|
504
|
+
const rowsHit = item.label === '声明行托管块' && /孤儿声明行/.test(item.detail) && (rowsFixed || (planOnly && doRows))
|
|
505
|
+
if (dirsHit) {
|
|
506
|
+
fixedCount++
|
|
507
|
+
const what = planOnly ? orphanDirIds.join('、') : cleanup.removedDirs.join('、')
|
|
508
|
+
const rowNote = planOnly ? '' : ';受影响声明行已同步撤下'
|
|
509
|
+
console.log(`✓ ${planOnly ? '将修' : '已修'} ${item.label} — ${planOnly ? '将删除' : '已删除'}孤儿目录 ${what}${rowNote}`)
|
|
510
|
+
} else if (rowsHit) {
|
|
511
|
+
fixedCount++
|
|
512
|
+
const what = planOnly ? orphanRowIds.join('、') : cleanup.prunedRowIds.join('、')
|
|
513
|
+
const backupNote = planOnly ? '' : `(patch 原文件备份:${cleanup.backupPath ?? '无'})`
|
|
514
|
+
console.log(`✓ ${planOnly ? '将修' : '已修'} ${item.label} — ${planOnly ? '将撤下' : '已撤下'}孤儿声明行 ${what}${backupNote}`)
|
|
515
|
+
} else if (item.status === 'warn' || item.status === 'error') {
|
|
516
|
+
manualCount++
|
|
517
|
+
const hint = DOCTOR_FIX_HINTS[item.label] ?? ''
|
|
518
|
+
console.log(`${marks[item.status] ?? '·'} 需手动 ${item.label} — ${item.detail}${hint}`)
|
|
519
|
+
} else {
|
|
520
|
+
console.log(`${marks[item.status] ?? '·'} ${item.label} — ${item.detail}`)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
console.log()
|
|
524
|
+
if (planOnly) {
|
|
525
|
+
console.error('当前不是交互终端且未加 --yes:只列将修什么不执行,重跑时加 --yes 确认。')
|
|
526
|
+
return 2
|
|
527
|
+
}
|
|
528
|
+
console.log(`汇总: ${report.items.length} 项检查,已修 ${fixedCount} 项${manualCount > 0 ? `,需手动 ${manualCount} 项(见 ⚠/✘ 行)` : ''}`)
|
|
529
|
+
return report.hasErrors ? 1 : 0
|
|
530
|
+
}
|
|
531
|
+
|
|
269
532
|
// ── main ────────────────────────────────────────────────────────────────────
|
|
270
533
|
|
|
271
534
|
/**
|
|
@@ -287,16 +550,43 @@ export async function main({ argv = process.argv.slice(2) } = {}) {
|
|
|
287
550
|
return 0
|
|
288
551
|
}
|
|
289
552
|
|
|
290
|
-
//
|
|
291
|
-
//
|
|
553
|
+
// 旗标与子命令的配属校验:装错地方的旗标直接报错(与未知选项同款 exit 1),
|
|
554
|
+
// 不静默忽略——静默会让用户以为 --purge / --fix 生效了。
|
|
555
|
+
if (opts.fix && opts.command !== 'doctor') {
|
|
556
|
+
console.error('error: --fix 仅 doctor 子命令支持')
|
|
557
|
+
return 1
|
|
558
|
+
}
|
|
559
|
+
if (opts.purge && opts.command !== 'uninstall') {
|
|
560
|
+
console.error('error: --purge 仅 uninstall 子命令支持')
|
|
561
|
+
return 1
|
|
562
|
+
}
|
|
563
|
+
if (opts.yes && !(opts.command === 'uninstall' || (opts.command === 'doctor' && opts.fix))) {
|
|
564
|
+
console.error('error: --yes 仅 uninstall / doctor --fix 支持')
|
|
565
|
+
return 1
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// doctor 子命令:只读体检(--fix 升级为「体检并修」),不走 --auto 门禁
|
|
569
|
+
//(门禁仅拦会动盘的安装主流程),也不用 collectState 的全量检测——runDoctor
|
|
570
|
+
// 内部按项自行探测与降级。
|
|
292
571
|
if (opts.command === 'doctor') {
|
|
293
572
|
const dshHome = process.env.DSH_HOME || path.join(homedir(), '.dsh')
|
|
294
573
|
const configFile = opts.config || path.join(dshHome, 'dsh-paoding.config.yml')
|
|
574
|
+
if (opts.fix) {
|
|
575
|
+
return runDoctorFix({ dshHome, configFile, profile: opts.profile, yes: opts.yes })
|
|
576
|
+
}
|
|
295
577
|
const report = await runDoctor({ dshHome, configFile, profile: opts.profile })
|
|
296
578
|
printDoctorReport(report, { dshHome, configFile })
|
|
297
579
|
return report.hasErrors ? 1 : 0
|
|
298
580
|
}
|
|
299
581
|
|
|
582
|
+
// uninstall 子命令:清理庖丁写进宿主目录的东西(托管块 / 预设目录 / 标记),
|
|
583
|
+
// 确认口径与退出码见 runUninstall。
|
|
584
|
+
if (opts.command === 'uninstall') {
|
|
585
|
+
const dshHome = process.env.DSH_HOME || path.join(homedir(), '.dsh')
|
|
586
|
+
const configFile = opts.config || path.join(dshHome, 'dsh-paoding.config.yml')
|
|
587
|
+
return runUninstall({ dshHome, configFile, purge: opts.purge, yes: opts.yes })
|
|
588
|
+
}
|
|
589
|
+
|
|
300
590
|
// 无 --auto 直接拒绝(交互模式已删):--dry-run 只读不动盘,放行作检测预览。
|
|
301
591
|
// 门禁放在 collectState 之前:被拒路径不做任何检测 / 握手,快速失败。
|
|
302
592
|
if (!opts.auto && !opts.dryRun) {
|
package/tools/lib/config.mjs
CHANGED
|
@@ -359,6 +359,123 @@ export function stripNonTargetKeys(assignments) {
|
|
|
359
359
|
return out
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
+
// ── 目标字段差分(面板 preview / apply 响应的 diff 字段)──────────────────────
|
|
363
|
+
|
|
364
|
+
/** 序列差集:added = 新表按新序取不在旧表的项;removed = 旧表按旧序取不在新表的项。 */
|
|
365
|
+
function listDiff(oldList, newList) {
|
|
366
|
+
const oldSet = new Set(oldList)
|
|
367
|
+
const newSet = new Set(newList)
|
|
368
|
+
return {
|
|
369
|
+
added: newList.filter((n) => !oldSet.has(n)),
|
|
370
|
+
removed: oldList.filter((n) => !newSet.has(n)),
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** 合并去重保序(toolsAdded / toolsRemoved 由 extra 与 remove 两路合并而来)。 */
|
|
375
|
+
function dedupKeepOrder(list) {
|
|
376
|
+
return [...new Set(list)]
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** 技能映射等值:键集合一致且各键下角色列表逐项一致(顺序敏感,与落盘序一致)。 */
|
|
380
|
+
function skillsMapEq(a, b) {
|
|
381
|
+
const ak = Object.keys(a)
|
|
382
|
+
if (ak.length !== Object.keys(b).length || !ak.every((k) => k in b)) return false
|
|
383
|
+
return ak.every((k) => a[k].length === b[k].length && a[k].every((v, i) => v === b[k][i]))
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* 目标字段集合差分(纯函数,零文案——返回纯结构化结果,展示串一律由客户端
|
|
388
|
+
* i18n 渲染)。两侧先各过一次 normalizeTargetFields(未知键 / profile /
|
|
389
|
+
* workspaces 段天然被忽略,手写未规范化的输入也得到与落盘一致的口径),再按
|
|
390
|
+
* 维度对比:
|
|
391
|
+
* - mainExtra:main_agent_extra 列表的增删;
|
|
392
|
+
* - mainAgent:主 agent 四维——工具增删(extra 增删 ∪ remove 增删的合并视图:
|
|
393
|
+
* 「新增 remove 项」即工具减少、「撤销 remove 项」即工具恢复)、persona
|
|
394
|
+
* (main_agent_persona_extra,null 与 '' 语义不同:默认常量 vs 追加空串);
|
|
395
|
+
* model 恒 false(主 agent 无专用模型配置键,字段只为与角色条目同形方便
|
|
396
|
+
* 客户端统一渲染);skills(main_agent_skills / main_agent_skills_inline /
|
|
397
|
+
* skills 映射三者任一变化——角色级技能分配没有别的位置可报,并入此布尔);
|
|
398
|
+
* - roles:按角色名(键)对齐,新增 / 删除 / 变更三态;变更条目带六个子维度
|
|
399
|
+
* (tools 增删——null 与空表同视、persona、model(含 provider 路由)、
|
|
400
|
+
* start_mode、background_mode)。roles.<x>.name(角色显示名)不在对比维度。
|
|
401
|
+
* - rolesRemove:内置角色删除名单的增删。
|
|
402
|
+
* changed = 任一子维度有变化。oldAssign 传 null(磁盘无配置)按全空目标字段算,
|
|
403
|
+
* 即首次保存时一切都算「新增」。
|
|
404
|
+
*/
|
|
405
|
+
export function diffAssignments(oldAssign, newAssign) {
|
|
406
|
+
const oldT = normalizeTargetFields(oldAssign ?? {})
|
|
407
|
+
const newT = normalizeTargetFields(newAssign ?? {})
|
|
408
|
+
|
|
409
|
+
// 主 agent:extra 增删 + remove 增删合并成同一对工具增删视图
|
|
410
|
+
const extraDiff = listDiff(oldT.main_agent_extra, newT.main_agent_extra)
|
|
411
|
+
const removeDiff = listDiff(oldT.main_agent_remove, newT.main_agent_remove)
|
|
412
|
+
const toolsAdded = dedupKeepOrder([...extraDiff.added, ...removeDiff.removed])
|
|
413
|
+
const toolsRemoved = dedupKeepOrder([...extraDiff.removed, ...removeDiff.added])
|
|
414
|
+
const personaChanged = (oldT.main_agent_persona_extra ?? null) !== (newT.main_agent_persona_extra ?? null)
|
|
415
|
+
const softSkillDiff = listDiff(oldT.main_agent_skills, newT.main_agent_skills)
|
|
416
|
+
const hardSkillDiff = listDiff(oldT.main_agent_skills_inline, newT.main_agent_skills_inline)
|
|
417
|
+
const skillsChanged =
|
|
418
|
+
!skillsMapEq(oldT.skills, newT.skills) ||
|
|
419
|
+
softSkillDiff.added.length > 0 || softSkillDiff.removed.length > 0 ||
|
|
420
|
+
hardSkillDiff.added.length > 0 || hardSkillDiff.removed.length > 0
|
|
421
|
+
const mainAgent = { toolsAdded, toolsRemoved, personaChanged, modelChanged: false, skillsChanged }
|
|
422
|
+
|
|
423
|
+
// 角色按名对齐:旧键序在前、新独有键序在后(新增角色在结果里的顺序可预期)
|
|
424
|
+
const roles = []
|
|
425
|
+
const roleNames = [
|
|
426
|
+
...Object.keys(oldT.roles),
|
|
427
|
+
...Object.keys(newT.roles).filter((n) => !(n in oldT.roles)),
|
|
428
|
+
]
|
|
429
|
+
for (const name of roleNames) {
|
|
430
|
+
const o = oldT.roles[name]
|
|
431
|
+
const n = newT.roles[name]
|
|
432
|
+
if (o && !n) { roles.push({ name, op: 'removed' }); continue }
|
|
433
|
+
if (!o && n) { roles.push({ name, op: 'added' }); continue }
|
|
434
|
+
const tools = listDiff(o.tools, n.tools)
|
|
435
|
+
const changedRole = {
|
|
436
|
+
name,
|
|
437
|
+
op: 'changed',
|
|
438
|
+
toolsAdded: tools.added,
|
|
439
|
+
toolsRemoved: tools.removed,
|
|
440
|
+
// provider 与 model 同属「角色专用模型」维度(LLM 路由成对生效)
|
|
441
|
+
personaChanged: (o.persona ?? null) !== (n.persona ?? null),
|
|
442
|
+
modelChanged: (o.model ?? null) !== (n.model ?? null) || (o.provider ?? null) !== (n.provider ?? null),
|
|
443
|
+
startModeChanged: o.start_mode !== n.start_mode,
|
|
444
|
+
backgroundModeChanged: o.background_mode !== n.background_mode,
|
|
445
|
+
}
|
|
446
|
+
if (
|
|
447
|
+
changedRole.toolsAdded.length > 0 ||
|
|
448
|
+
changedRole.toolsRemoved.length > 0 ||
|
|
449
|
+
changedRole.personaChanged ||
|
|
450
|
+
changedRole.modelChanged ||
|
|
451
|
+
changedRole.startModeChanged ||
|
|
452
|
+
changedRole.backgroundModeChanged
|
|
453
|
+
) {
|
|
454
|
+
roles.push(changedRole)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const rolesRemove = listDiff(oldT.roles_remove, newT.roles_remove)
|
|
459
|
+
const changed =
|
|
460
|
+
toolsAdded.length > 0 ||
|
|
461
|
+
toolsRemoved.length > 0 ||
|
|
462
|
+
personaChanged ||
|
|
463
|
+
skillsChanged ||
|
|
464
|
+
roles.length > 0 ||
|
|
465
|
+
rolesRemove.added.length > 0 ||
|
|
466
|
+
rolesRemove.removed.length > 0 ||
|
|
467
|
+
extraDiff.added.length > 0 ||
|
|
468
|
+
extraDiff.removed.length > 0
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
changed,
|
|
472
|
+
mainAgent,
|
|
473
|
+
roles,
|
|
474
|
+
rolesRemove: { added: rolesRemove.added, removed: rolesRemove.removed },
|
|
475
|
+
mainExtra: { added: extraDiff.added, removed: extraDiff.removed },
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
362
479
|
/**
|
|
363
480
|
* 目标字段集合(roles → skills,除
|
|
364
481
|
* profile / workspaces 外的全部顶层键)写出体:pad 为该层缩进前缀(顶层 ''、workspaces.<path> 条目里
|
package/tools/lib/doctor.mjs
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* healthCheck 的 preset_drift 判定(同一份 dry-run 对比,不另立标准)。
|
|
13
13
|
*
|
|
14
14
|
* 依赖 config(loadConfig)/ yaml(loadYaml)/ alloc(autoAssignments)/
|
|
15
|
-
* state(collectState)/
|
|
15
|
+
* state(collectState)/ lifecycle(workspacePresetOrphans,孤儿目录判据与
|
|
16
|
+
* 清理侧同源)/
|
|
16
17
|
* preset-system(轨道、托管块、标记)与 health(healthCheck);被 cli 引用。
|
|
17
18
|
* 零运行时依赖(node:fs / node:path / node:url)。
|
|
18
19
|
*/
|
|
@@ -22,7 +23,7 @@ import { loadConfig } from './config.mjs'
|
|
|
22
23
|
import { loadYaml } from './yaml.mjs'
|
|
23
24
|
import { autoAssignments } from './alloc.mjs'
|
|
24
25
|
import { collectState } from './state.mjs'
|
|
25
|
-
import {
|
|
26
|
+
import { workspacePresetOrphans } from './lifecycle.mjs'
|
|
26
27
|
import {
|
|
27
28
|
MANAGED_BEGIN,
|
|
28
29
|
MANAGED_END,
|
|
@@ -31,7 +32,6 @@ import {
|
|
|
31
32
|
detectPresetSystem,
|
|
32
33
|
isDeclarativeTriple,
|
|
33
34
|
isPtcWorkerHost,
|
|
34
|
-
listOrchestratorPresetIds,
|
|
35
35
|
parseGeneratorMarker,
|
|
36
36
|
presetRolesFlavorOf,
|
|
37
37
|
readHomePatchRows,
|
|
@@ -59,16 +59,6 @@ function managedMarkerLines(text) {
|
|
|
59
59
|
)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/** 孤儿工作区预设目录对账:配置 workspaces 键组统一过 assignSlugs(与生成层
|
|
63
|
-
* 同源,slug 稳定),得到「配置在册」的 presetId 集合;目录有、名册无 → 孤儿。 */
|
|
64
|
-
function workspacePresetOrphans(dshHome, config) {
|
|
65
|
-
const dirs = listOrchestratorPresetIds(dshHome).filter((id) => id !== 'orchestrator')
|
|
66
|
-
if (dirs.length === 0) return { dirs, orphans: [] }
|
|
67
|
-
const wsPaths = Object.keys(config?.workspaces ?? {})
|
|
68
|
-
const inRegister = new Set([...assignSlugs(wsPaths).values()].map(presetIdOf))
|
|
69
|
-
return { dirs, orphans: dirs.filter((id) => !inRegister.has(id)) }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
62
|
/**
|
|
73
63
|
* 跑全套 doctor 检查。参数(全可缺省,干净机器上逐项降级):
|
|
74
64
|
* - dshHome / configFile:DSH 家目录与配置文件路径。
|