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,266 @@
1
+ /**
2
+ * YAML 装载与 patch 解析:loadYaml(经 createRequire 从 $DSH_HOME/node_modules
3
+ * 惰性加载 yaml 包)、preprocessPatchText(中和 cordis 的 !!js 表达式)与内置
4
+ * YAML subset 回退解析器(parsePatchFile / parseYamlSubset 家族,块标量 /
5
+ * insert 包裹等由 consumeBlockScalar / flattenEntries 等支撑)。
6
+ * 依赖 util(warn)与 node:fs / node:module / node:path;被 config / state 引用。
7
+ */
8
+ import { readFileSync } from 'node:fs'
9
+ import { createRequire } from 'node:module'
10
+ import path from 'node:path'
11
+ import { warn } from './util.mjs'
12
+
13
+ // ── yaml loading ────────────────────────────────────────────────────────────
14
+
15
+ /**
16
+ * Resolve the `yaml` package from $DSH_HOME/node_modules via createRequire.
17
+ * The directory is typically a symlink into dsh's npx node_modules, which
18
+ * ships yaml v2. Returns null when unavailable — callers then use the
19
+ * built-in fallback parser.
20
+ */
21
+ export function loadYaml(dshHome) {
22
+ try {
23
+ const requireFromDsh = createRequire(path.join(dshHome, 'node_modules', 'package.json'))
24
+ const mod = requireFromDsh('yaml')
25
+ if (mod && typeof mod.parse === 'function' && typeof mod.parseDocument === 'function') {
26
+ return mod
27
+ }
28
+ } catch {
29
+ /* yaml missing or unreadable — fall back to the subset parser */
30
+ }
31
+ return null
32
+ }
33
+
34
+ // ── patch parsing ───────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Neutralize the cordis `!!js` expressions found in real patch files before
38
+ * any parser sees them (yaml would otherwise raise unresolved-tag warnings).
39
+ */
40
+ export function preprocessPatchText(text) {
41
+ const isWin = process.platform === 'win32'
42
+ return text
43
+ .replaceAll("!!js process.platform === 'win32'", isWin ? 'true' : 'false')
44
+ .replaceAll("!!js process.platform !== 'win32'", isWin ? 'false' : 'true')
45
+ .replaceAll('!!js process.cwd()', JSON.stringify(process.cwd()))
46
+ }
47
+
48
+ /** Parse one patch file. Returns the top-level array, or null (warned) on failure. */
49
+ export function parsePatchFile(file, yamlMod) {
50
+ let text
51
+ try {
52
+ text = readFileSync(file, 'utf8')
53
+ } catch (err) {
54
+ warn(`${file}: unreadable (${err.message}) — skipped`)
55
+ return null
56
+ }
57
+ const processed = preprocessPatchText(text)
58
+
59
+ if (yamlMod) {
60
+ try {
61
+ const data = yamlMod.parse(processed)
62
+ if (Array.isArray(data)) return data
63
+ warn(`${file}: parsed but the top level is not a list — skipped`)
64
+ return null
65
+ } catch (yamlErr) {
66
+ warn(`${file}: yaml parse failed (${yamlErr.message}) — trying the built-in fallback parser`)
67
+ }
68
+ }
69
+
70
+ try {
71
+ const data = parseYamlSubset(processed)
72
+ if (Array.isArray(data)) return data
73
+ warn(`${file}: fallback parser produced a non-list result — skipped`)
74
+ return null
75
+ } catch (err) {
76
+ warn(`${file}: could not be parsed (fallback: ${err.message}) — skipped`)
77
+ return null
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Minimal YAML subset parser, used only for patch files when `yaml` is
83
+ * unavailable. Supports: top-level list items, nested maps, scalar values
84
+ * (single/double-quoted strings, numbers, true/false), `#` comments and
85
+ * `- insert:` wrapping.
86
+ */
87
+ export function parseYamlSubset(text) {
88
+ const lines = []
89
+ for (const raw of text.split(/\r?\n/)) {
90
+ const line = stripInlineComment(raw).replace(/[ \t]+$/, '')
91
+ if (line.trim() === '') continue
92
+ const indent = line.match(/^[ \t]*/)[0].length
93
+ lines.push({ indent, text: line.slice(indent) })
94
+ }
95
+ if (lines.length === 0) return []
96
+ const [value] = parseBlock(lines, 0, lines[0].indent)
97
+ return value
98
+ }
99
+
100
+ export function stripInlineComment(line) {
101
+ let inSingle = false
102
+ let inDouble = false
103
+ for (let i = 0; i < line.length; i++) {
104
+ const c = line[i]
105
+ if (inSingle) {
106
+ if (c === "'") inSingle = false
107
+ continue
108
+ }
109
+ if (inDouble) {
110
+ if (c === '"') inDouble = false
111
+ continue
112
+ }
113
+ if (c === "'") inSingle = true
114
+ else if (c === '"') inDouble = true
115
+ // YAML comments start with '#' only at line start or after whitespace.
116
+ else if (c === '#' && (i === 0 || /\s/.test(line[i - 1]))) return line.slice(0, i)
117
+ }
118
+ return line
119
+ }
120
+
121
+ /** YAML 块标量(| > 及其折叠/裁剪变体)值标记:后续更深的行全部属该标量正文,
122
+ * 直到遇到缩进 <= 键缩进的行(同级键/父级结束)。返回拼接正文与下一行下标。
123
+ * 说明:parseYamlSubset 是 yaml 包缺失时的回退解析器;此前块标量(如角色
124
+ * persona: |-)之后的同级键/顶层键(roles_remove / main_agent_extra 等)会被
125
+ * 误判为无法解析而静默丢弃,导致回读配置丢键。 */
126
+ export function consumeBlockScalar(lines, i, keyIndent) {
127
+ const parts = []
128
+ while (i < lines.length && lines[i].indent > keyIndent) {
129
+ parts.push(lines[i].text)
130
+ i++
131
+ }
132
+ return { value: parts.join('\n'), next: i }
133
+ }
134
+
135
+ export function parseBlock(lines, i, indent) {
136
+ if (lines[i].text.startsWith('- ')) return parseList(lines, i, indent)
137
+ return parseMap(lines, i, indent)
138
+ }
139
+
140
+ export function parseList(lines, i, indent) {
141
+ const out = []
142
+ while (i < lines.length && lines[i].indent === indent && lines[i].text.startsWith('- ')) {
143
+ const rest = lines[i].text.slice(2).trim()
144
+ i++
145
+ if (rest === '') {
146
+ // `- ` followed by a nested block (e.g. `- insert:`)
147
+ if (i < lines.length && lines[i].indent > indent) {
148
+ const [value, next] = parseBlock(lines, i, lines[i].indent)
149
+ out.push(value)
150
+ i = next
151
+ } else {
152
+ out.push(null)
153
+ }
154
+ } else if (looksLikeKeyValue(rest)) {
155
+ const [key, value] = splitKeyValue(rest)
156
+ const item = {}
157
+ if (value === undefined) {
158
+ if (i < lines.length && lines[i].indent > indent) {
159
+ const [nested, next] = parseBlock(lines, i, lines[i].indent)
160
+ item[key] = nested
161
+ i = next
162
+ } else {
163
+ item[key] = null
164
+ }
165
+ } else {
166
+ let parsed = parseScalar(value)
167
+ if (/^[|>][-+]?$/.test(String(value).trim()) && i < lines.length && lines[i].indent > indent) {
168
+ const blk = consumeBlockScalar(lines, i, indent)
169
+ parsed = blk.value
170
+ i = blk.next
171
+ }
172
+ item[key] = parsed
173
+ }
174
+ // Remaining keys of this map item continue at a deeper indent.
175
+ if (i < lines.length && lines[i].indent > indent) {
176
+ const [more, next] = parseMap(lines, i, lines[i].indent)
177
+ Object.assign(item, more)
178
+ i = next
179
+ }
180
+ out.push(item)
181
+ } else {
182
+ out.push(parseScalar(rest))
183
+ }
184
+ }
185
+ return [out, i]
186
+ }
187
+
188
+ export function parseMap(lines, i, indent) {
189
+ const out = {}
190
+ while (i < lines.length && lines[i].indent === indent && !lines[i].text.startsWith('- ')) {
191
+ const line = lines[i].text
192
+ if (!looksLikeKeyValue(line)) break
193
+ const [key, value] = splitKeyValue(line)
194
+ i++
195
+ if (value === undefined) {
196
+ if (i < lines.length && lines[i].indent > indent) {
197
+ const [nested, next] = parseBlock(lines, i, lines[i].indent)
198
+ out[key] = nested
199
+ i = next
200
+ } else {
201
+ out[key] = null
202
+ }
203
+ } else {
204
+ let parsed = parseScalar(value)
205
+ if (/^[|>][-+]?$/.test(String(value).trim()) && i < lines.length && lines[i].indent > indent) {
206
+ const blk = consumeBlockScalar(lines, i, indent)
207
+ parsed = blk.value
208
+ i = blk.next
209
+ }
210
+ out[key] = parsed
211
+ }
212
+ }
213
+ return [out, i]
214
+ }
215
+
216
+ export function looksLikeKeyValue(rest) {
217
+ const trimmed = rest.trim()
218
+ if (trimmed.endsWith(':') && trimmed.length > 1) return true
219
+ return trimmed.includes(': ') || trimmed.includes(':\t')
220
+ }
221
+
222
+ /** Split `key: value` at the first colon; a trailing `key:` yields undefined. */
223
+ export function splitKeyValue(rest) {
224
+ const trimmed = rest.trim()
225
+ if (trimmed.endsWith(':')) return [trimmed.slice(0, -1).trim(), undefined]
226
+ const idx = trimmed.indexOf(': ')
227
+ if (idx === -1) return [trimmed, undefined]
228
+ return [trimmed.slice(0, idx).trim(), trimmed.slice(idx + 2).trim()]
229
+ }
230
+
231
+ export function parseScalar(raw) {
232
+ const s = raw.trim()
233
+ if (s.length >= 2 && s.startsWith("'") && s.endsWith("'")) {
234
+ return s.slice(1, -1).replace(/''/g, "'")
235
+ }
236
+ if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
237
+ try {
238
+ return JSON.parse(s)
239
+ } catch {
240
+ return s.slice(1, -1)
241
+ }
242
+ }
243
+ if (s === 'true') return true
244
+ if (s === 'false') return false
245
+ if (s === 'null' || s === '~') return null
246
+ // 空流式序列 []:serializeConfig 对显式空 main_agent_extra 落盘 `[]`,回读时
247
+ // 必须还原为数组而非字符串,否则 has_main_agent_extra 判否、显式空语义丢失。
248
+ // (非空流式 [a, b] 仍不支持 —— 现有消费键均走块列表或 yaml 包路径。)
249
+ if (s === '[]') return []
250
+ if (/^-?\d+$/.test(s) || /^-?\d*\.\d+$/.test(s)) return Number(s)
251
+ return s
252
+ }
253
+
254
+ /** Recursively expand `- insert:` wrappers into plain entries. */
255
+ export function flattenEntries(list, out = []) {
256
+ for (const item of list ?? []) {
257
+ if (!item || typeof item !== 'object' || Array.isArray(item)) continue
258
+ if (item.insert !== undefined) {
259
+ if (Array.isArray(item.insert)) flattenEntries(item.insert, out)
260
+ else if (item.insert && typeof item.insert === 'object') flattenEntries([item.insert], out)
261
+ } else {
262
+ out.push(item)
263
+ }
264
+ }
265
+ return out
266
+ }