dsh-paoding 0.3.3 → 0.3.5

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.
@@ -1,25 +1,28 @@
1
1
  /**
2
2
  * 公共状态管线:collectState(patch 分层扫描 / MCP 工具名解析 / 插件与技能检测 /
3
- * smart defaults,含可选 runtimeFacts 合并 / 已落盘 preset 目录扫描)与
4
- * generateAndInstall(compose + yaml 校验 + dst 目录落盘 + saveConfig,支持按
5
- * 工作区落到 orchestrator-<slug> 独立 preset 目录并把条目并进配置 workspaces 段;
6
- * 配置读取失败 / 状态不可信时拒绝破坏性落盘,成功后回收孤儿工作区 preset)。
3
+ * smart defaults,含可选 runtimeFacts 合并 / 预设安装轨道探测 / 已落盘 preset
4
+ * 目录扫描)与 generateAndInstall(compose + yaml 校验 + dst 目录落盘 +
5
+ * saveConfig,支持按工作区落到 orchestrator-<slug> 独立 preset 目录并把条目
6
+ * 并进配置 workspaces 段;配置读取失败 / 状态不可信时拒绝破坏性落盘,成功后
7
+ * 回收孤儿工作区 preset;并按 presetSystem 双轨收尾——0.1.7+ 声明行轨把声明行
8
+ * upsert 进 home patch 托管块,≤0.1.6 目录扫描轨撤掉残留声明块自愈)。
7
9
  * install.mjs 的公共导出(collectState / generateAndInstall)即来自本模块。
8
- * 依赖 util / yaml / config / host / skills / spans / compose / alloc / workspaces
9
- * 与 node:fs / node:path;被 cli 与 plugins/paoding-config-ui 引用。
10
+ * 依赖 util / yaml / config / host / skills / spans / compose / alloc /
11
+ * workspaces / preset-system 与 node:fs / node:path / node:url;被 cli 与
12
+ * plugins/paoding-config-ui 引用。
10
13
  */
11
14
  import {
12
15
  chmodSync,
13
16
  copyFileSync,
14
17
  existsSync,
15
18
  mkdirSync,
16
- readdirSync,
17
19
  readFileSync,
18
20
  rmSync,
19
21
  statSync,
20
22
  writeFileSync,
21
23
  } from 'node:fs'
22
24
  import path from 'node:path'
25
+ import { pathToFileURL } from 'node:url'
23
26
  import { DEFAULT_MAIN_AGENT_PERSONA_EXTRA, ROLES, SRC_DIR, warn } from './util.mjs'
24
27
  import { flattenEntries, loadYaml, parsePatchFile } from './yaml.mjs'
25
28
  import {
@@ -43,6 +46,16 @@ import { detectSkills, findSkillMeta } from './skills.mjs'
43
46
  import { locateAllowBlocks, locatePersonaBlocks } from './spans.mjs'
44
47
  import { composeGenerated, extractMainAgentAllow } from './compose.mjs'
45
48
  import { smartDefaults } from './alloc.mjs'
49
+ import {
50
+ buildPresetRowText,
51
+ detectPresetSystem,
52
+ listOrchestratorPresetIds,
53
+ presetRowId,
54
+ readHomePatchRows,
55
+ readPresetDirMeta,
56
+ stripHomePatchRows,
57
+ upsertHomePatchRows,
58
+ } from './preset-system.mjs'
46
59
 
47
60
  /**
48
61
  * Reusable state collection for the CLI and the visual config UI.
@@ -51,11 +64,13 @@ import { smartDefaults } from './alloc.mjs'
51
64
  * without writing anything or prompting. Throws on hard errors.
52
65
  *
53
66
  * `runtimeFacts`(可选,仅 Web 配置器经 ctx 传入)是运行时优先的补充事实:
54
- * { ok, error?, pluginEntries, toolNames },来自 plugins/paoding-config-ui/
55
- * runtime-inventory.mjs。不传时整段合并逻辑跳过,行为与旧版完全一致;传入时
56
- * 把 preset 未覆盖的运行时工具并进 inventory / mcpReports / pluginReports,
57
- * 让 bundle 形态加载的插件(@hyzyn/dsh-codegraph、dsh-mnemon 等)也能被识别。
58
- * 文件扫描路径(KNOWN_HOST_PLUGINS 白名单等)始终保持为兜底,不动。
67
+ * { ok, error?, pluginEntries, toolNames, presetSystem },来自
68
+ * plugins/paoding-config-ui/runtime-inventory.mjs。不传时整段合并逻辑跳过,
69
+ * 行为与旧版完全一致;传入时把 preset 未覆盖的运行时工具并进 inventory /
70
+ * mcpReports / pluginReports,让 bundle 形态加载的插件(@hyzyn/dsh-codegraph、
71
+ * dsh-mnemon 等)也能被识别;presetSystem(预设安装轨道)直接采信,免去
72
+ * dsh --version 子进程探测。文件扫描路径(KNOWN_HOST_PLUGINS 白名单等)
73
+ * 始终保持为兜底,不动。
59
74
  */
60
75
  export async function collectState({ dshHome, configFile, profile = null, patches = [], cwd = process.cwd(), runtimeFacts = null }) {
61
76
  const dstDir = path.join(dshHome, '.agent-presets', 'orchestrator')
@@ -211,18 +226,18 @@ export async function collectState({ dshHome, configFile, profile = null, patche
211
226
  }
212
227
  }
213
228
 
229
+ // 预设安装轨道探测(declarative = 0.1.7+ 声明行轨 / directory = ≤0.1.6 目录
230
+ // 扫描轨)。运行时事实里带 presetSystem(Web 插件对宿主服务的实时探测)时
231
+ // 直接采信零开销;CLI / 首装链路没有运行时事实,走 dsh --version + 文件探测
232
+ //(见 preset-system.detectPresetSystem 的降级链)。
233
+ const presetSystem = await detectPresetSystem({ dshHome, runtimeSystem: runtimeFacts?.presetSystem ?? null })
234
+
214
235
  // 已落盘 preset 目录扫描(编排类):.agent-presets 下的 orchestrator 基础
215
236
  // preset 与 orchestrator-<slug> 工作区专属 preset。扫描失败(目录尚不存在、
216
237
  // 无读权限等)一律降级为空列表,绝不影响检测结果本体;generateAndInstall
217
- // 成功落盘后以它为对账清单做孤儿工作区 preset 回收。
218
- let installedPresets = []
219
- try {
220
- installedPresets = readdirSync(path.join(dshHome, '.agent-presets'))
221
- .filter((name) => /^orchestrator(-[a-z0-9-]+)?$/.test(name))
222
- .sort()
223
- } catch {
224
- installedPresets = []
225
- }
238
+ // 成功落盘后以它为对账清单做孤儿工作区 preset 回收。两轨的产物目录同名同
239
+ // 位置(.agent-presets/<presetId>/),这一扫描天然覆盖两轨的 installed 判定。
240
+ const installedPresets = listOrchestratorPresetIds(dshHome)
226
241
 
227
242
  return {
228
243
  dshHome,
@@ -248,6 +263,8 @@ export async function collectState({ dshHome, configFile, profile = null, patche
248
263
  // 已落盘的编排类 preset 目录名列表(含基础 orchestrator 与各工作区专属),
249
264
  // 供 UI 的 workspaceMeta 对账「该工作区 preset 是否已生成」。
250
265
  installedPresets,
266
+ // 预设安装轨道(generateAndInstall 据此双轨收尾;serializeState 透传给面板)
267
+ presetSystem,
251
268
  // 主 persona 尾部追加的默认常量(配置键 main_agent_persona_extra 缺省值):
252
269
  // 供 UI state(serializeState → /api/paoding/state)展示与「恢复默认」用。
253
270
  mainPersonaExtraDefault: DEFAULT_MAIN_AGENT_PERSONA_EXTRA,
@@ -259,16 +276,34 @@ export async function collectState({ dshHome, configFile, profile = null, patche
259
276
  }
260
277
  }
261
278
 
279
+ /**
280
+ * SRC preset.yml 显示元数据(name / description / order):声明行轨把这些值
281
+ * 写进 home patch 声明行的 config,取值口径与目录轨写 preset.yml 完全同源
282
+ * (name 可被 main_agent_display_name / 工作区派生名覆盖,见 generateAndInstall
283
+ * 调用处)。解析实现已沉到 preset-system.readPresetDirMeta(迁移对账同源复用)。
284
+ */
285
+ function readPresetMeta() {
286
+ return readPresetDirMeta(SRC_DIR)
287
+ }
288
+
262
289
  /**
263
290
  * Compose, validate and (unless dry-run) install the generated preset from an
264
291
  * assignments object. Shared by the CLI and the visual config UI.
265
- * Returns { text, roleResults, staleNote, wrote, presetId }.
292
+ * Returns { text, roleResults, staleNote, wrote, presetId, presetSystem }.
266
293
  *
267
294
  * 破坏性落盘护栏:配置读取失败直接抛错中止(不静默降级 prev=null 继续装);
268
295
  * 配置缺失(prev=null)而目标 preset 目录已存在时同样拒绝重建。落盘成功后对
269
296
  * state.installedPresets 做孤儿工作区 preset 回收(不被 workspaces 引用的
270
297
  * orchestrator-<slug> 目录 rmSync + warn;基础 preset 永不清)。
271
298
  *
299
+ * 预设安装双轨收尾(wrote=true 才动,dry-run 零副作用):
300
+ * - declarative(0.1.7+,目录扫描机制已删):把本 preset 以一条 `- insert:`
301
+ * 声明行 upsert 进 $DSH_HOME/cordis.patch.yml 托管块(restrict.mjs 用绝对
302
+ * file: URL);孤儿工作区回收处同步撤对应声明行——目录与行同生同灭。
303
+ * - directory(≤0.1.6):上面落盘即完成;home patch 里若有残留托管声明块
304
+ * (宿主刚从 0.1.7 降级等场景)整块撤下自愈——残留声明行会让 profile 启动
305
+ * 失败。
306
+ *
272
307
  * workspacePath(可选):传工作区绝对路径时生成落到 orchestrator-<slug> 独立
273
308
  * preset 目录(slug 由磁盘配置现有 workspaces 键 ∪ 本路径统一分配,同名目录
274
309
  * 冲突自动加 hash 后缀),preset 显示名按 main_agent_display_name >
@@ -283,6 +318,10 @@ export function generateAndInstall(state, assignments, { dryRun = false, saveCon
283
318
  // 一律丢弃,防其混进生成的 preset 或覆盖配置文件里对应的段。
284
319
  assignments = stripNonTargetKeys(assignments)
285
320
 
321
+ // 预设安装轨道(collectState 已探测;缺省按目录轨兜底——安全侧,理由见
322
+ // preset-system 模块头注释)
323
+ const presetSystem = state.presetSystem ?? 'directory'
324
+
286
325
  // 工作区定位:路径归一(容忍尾斜杠 / 相对写法),空值一律视为全局。
287
326
  const wsPath = workspacePath === null || workspacePath === undefined || workspacePath === ''
288
327
  ? null
@@ -452,9 +491,45 @@ export function generateAndInstall(state, assignments, { dryRun = false, saveCon
452
491
  warn(`孤儿工作区 preset 已回收: ${dir}(配置 workspaces 已无引用)`)
453
492
  } catch (err) {
454
493
  warn(`孤儿工作区 preset 回收失败(已保留): ${dir}: ${err?.message ?? err}`)
494
+ continue
495
+ }
496
+ if (presetSystem === 'declarative') {
497
+ // 声明行轨:目录回收了,home patch 托管块里的对应声明行同步撤下——
498
+ // 目录与行必须同生同灭,否则 0.1.7 宿主会挂出一个指向已删目录的 preset。
499
+ try {
500
+ stripHomePatchRows(dshHome, [presetRowId(name)])
501
+ } catch (err) {
502
+ warn(`孤儿 preset 的声明行撤除失败(${presetRowId(name)}): ${err?.message ?? err}`)
503
+ }
455
504
  }
456
505
  }
457
506
  }
458
507
 
459
- return { text: generatedText, roleResults, staleNote, wrote, presetId }
508
+ // ── 预设安装双轨收尾(wrote=true 才动;dry-run 零副作用)────────────────
509
+ // declarative(0.1.7+):目录扫描机制已删,必须把本 preset 以一条 `- insert:`
510
+ // 声明行写进 home patch 托管块才算安装完成(preset 元信息与写 preset.yml 同
511
+ // 源:显示名取 displayName ?? SRC preset.yml 的 name,description / order 取
512
+ // SRC;restrict.mjs 用绝对 file: URL——声明行没有「相对 preset 目录」语义)。
513
+ // directory(≤0.1.6):上面落盘即完成;home patch 里若有残留托管声明块
514
+ //(宿主刚从 0.1.7 降级等场景)整块撤下自愈——残留声明行会让 profile 启动失败。
515
+ if (wrote) {
516
+ if (presetSystem === 'declarative') {
517
+ const meta = readPresetMeta()
518
+ upsertHomePatchRows(dshHome, [
519
+ buildPresetRowText({
520
+ presetId,
521
+ name: displayName ?? meta.name,
522
+ description: meta.description,
523
+ order: meta.order,
524
+ agentYmlText: generatedText,
525
+ restrictFileUrl: pathToFileURL(path.join(dstDir, 'restrict.mjs')).href,
526
+ }),
527
+ ])
528
+ } else if (readHomePatchRows(dshHome).length > 0) {
529
+ stripHomePatchRows(dshHome, null)
530
+ warn('cordis.patch.yml 检出 dsh-paoding 声明块,但当前宿主是目录扫描轨(DSH ≤ 0.1.6),已整块撤下(残留声明行会导致 profile 启动失败)')
531
+ }
532
+ }
533
+
534
+ return { text: generatedText, roleResults, staleNote, wrote, presetId, presetSystem }
460
535
  }