@noob-stupid/dsh-plugin-console 0.5.30 → 0.5.32

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 (45) hide show
  1. package/README.md +39 -7
  2. package/README.zh.md +40 -8
  3. package/SECURITY.md +2 -2
  4. package/lib/client.js +17 -3
  5. package/lib/index.js +141 -141
  6. package/lib/server/domain/ai-run.js +1 -1
  7. package/lib/server/domain/ai.js +39 -11
  8. package/lib/server/domain/auto-disable.js +226 -0
  9. package/lib/server/domain/auto-preflight-run.js +184 -0
  10. package/lib/server/domain/auto-preflight.js +345 -0
  11. package/lib/server/domain/compat-state.js +57 -8
  12. package/lib/server/domain/compat.js +7 -3
  13. package/lib/server/domain/components.js +1 -1
  14. package/lib/server/domain/dep-source.js +1 -1
  15. package/lib/server/domain/framework.js +65 -6
  16. package/lib/server/domain/install-job.js +1 -1
  17. package/lib/server/domain/install.js +1 -1
  18. package/lib/server/domain/jobs.js +1 -1
  19. package/lib/server/domain/market.js +1 -1
  20. package/lib/server/domain/patch-yaml-check.js +151 -0
  21. package/lib/server/domain/patch.js +1 -1
  22. package/lib/server/domain/presets.js +1 -1
  23. package/lib/server/domain/release-source.js +3 -3
  24. package/lib/server/domain/repoland.js +1 -1
  25. package/lib/server/domain/runtime.js +1 -1
  26. package/lib/server/domain/safe-boot.js +520 -0
  27. package/lib/server/domain/skills.js +1 -1
  28. package/lib/server/domain/sources.js +1 -1
  29. package/lib/server/domain/suite.js +1 -1
  30. package/lib/server/infra/exec.js +2 -2
  31. package/lib/server/infra/fsx.js +3 -3
  32. package/lib/server/infra/http.js +1 -1
  33. package/lib/server/infra/httpd.js +1 -1
  34. package/lib/server/infra/mask.js +1 -1
  35. package/lib/server/infra/paths.js +1 -1
  36. package/lib/server/infra/semver.js +1 -1
  37. package/lib/server/routes/compat.js +240 -11
  38. package/lib/server/routes/components.js +1 -1
  39. package/lib/server/routes/framework-preflight.js +14 -1
  40. package/lib/server/routes/framework-upgrade.js +16 -17
  41. package/lib/server/routes/framework.js +18 -2
  42. package/lib/server/routes/github-login.js +2 -2
  43. package/lib/server/routes/index.js +8 -1
  44. package/lib/server/routes/safe-boot.js +139 -0
  45. package/package.json +1 -1
@@ -0,0 +1,151 @@
1
+ // L1 · domain —— patch-yaml-check.js(`cordis.patch.yml` 的**结构校验**;0.5.32 加法 D3)
2
+ //
3
+ // 为什么不能直接复用 `domain/preset-yaml.js#validateAgentConfig`(**真机实测**,2026-09-29):
4
+ // 那把尺子是按 **agent 预设文件**(顶层 `- id:` 序列)校准的,拿它量真机补丁会**整片误杀**:
5
+ // 两个 live profile 的 `cordis.patch.yml`(web 56 KB / desktop 55 KB)实测各被判 17 / 12 条问题,
6
+ // 全部是**合法写法**:
7
+ // · `description: '第一行…` + 后续行 ` 续行…'` —— YAML 的**多行引号标量**(跨行合法),
8
+ // 逐行判「引号未闭合」→ 假红;
9
+ // · 续行缩进比父级深 → 逐行判「缩进跳级/文件被截断」→ 假红。
10
+ // 误杀的代价在这里同样是反向的:D3 的恢复入口拿它当写盘前闸门 → **一恢复就被自家校验拒掉**。
11
+ //
12
+ // 所以这里只钉**能确证损坏**的形态(全部离线可判定,且对真机补丁 0 误报):
13
+ // ① 行首缩进里有 **tab** —— YAML 明确禁止用 tab 缩进(框架解析器必然报错);
14
+ // ② **顶层空数组占位符 `[]`**(issue #7 事故形态):模板初始化的 `[]` 后面再追加 `- id:` 条目,
15
+ // 同一文档流里「数组结束符 + 后续项」= 非法 YAML → dsh 启动直接崩;
16
+ // ③ 顶层(缩进 0)出现**不是** `- ` 序列项的行(既不是注释/空行/占位符)—— 补丁文件的形状是
17
+ // 「序列项 + 其子键」,顶层键值对说明文件被拼坏/截断;
18
+ // ④ **多行引号标量未闭合**(真正的截断):跨行扫描引号状态,直到文件末尾仍未闭合才判红
19
+ // —— 这一条恰好是②之外最常见的"写坏"形态;
20
+ // ⑤ 块标量(`|` / `>`)整段跳过判据(其内容是散文,不是 YAML 结构)。
21
+ //
22
+ // 分层:L1 domain —— 纯函数、无 IO、永不抛(解析失败是正常结局,调用方按 ok=false 处理)。
23
+
24
+ /** 缩进宽度(空格数;tab 会在 ① 里单独判掉,不参与计数)。 */
25
+ function indentWidth(line) {
26
+ const m = String(line).match(/^ */u)
27
+ return m === null ? 0 : m[0].length
28
+ }
29
+
30
+ /** 行首缩进里有没有 tab(YAML 禁止;框架解析器会直接报错)。 */
31
+ function hasTabIndent(line) {
32
+ return /^[ \t]*\t/u.test(String(line))
33
+ }
34
+
35
+ /** 剥掉行尾注释(只认**不在引号里**且 `#` 前有空白或行首的 `#`)。 */
36
+ function stripTrailingComment(line) {
37
+ const text = String(line)
38
+ let quote = null
39
+ for (let i = 0; i < text.length; i += 1) {
40
+ const ch = text[i]
41
+ if (quote !== null) {
42
+ if (quote === '"' && ch === '\\') { i += 1; continue }
43
+ if (ch === quote) quote = null
44
+ continue
45
+ }
46
+ if (ch === "'" || ch === '"') { quote = ch; continue }
47
+ if (ch === '#' && (i === 0 || /\s/u.test(text[i - 1]))) return text.slice(0, i)
48
+ }
49
+ return text
50
+ }
51
+
52
+ /** 块标量指示符(`|`、`>`、`|-`、`>+2`…):只有**整个值**就是指示符时才算(`> foo` 是普通标量)。 */
53
+ function blockScalarIndicator(value) {
54
+ const m = String(value).trim().match(/^([|>])([+-]?\d*|\d*[+-]?)$/u)
55
+ return m === null ? null : m[0]
56
+ }
57
+
58
+ /**
59
+ * 从一行里取出「值起点之后的引号状态」:
60
+ * · 值不以引号开头 → `{ opens: false, quote: null }`(引号在标量中间不算跨行开始);
61
+ * · 以单/双引号开头 → 逐字符推进(双引号吃 `\\` 转义,单引号吃 `''`),返回是否仍未闭合。
62
+ */
63
+ function scanValueQuotes(value, initialQuote = null) {
64
+ const text = String(value)
65
+ let quote = initialQuote
66
+ for (let i = 0; i < text.length; i += 1) {
67
+ const ch = text[i]
68
+ if (quote === null) {
69
+ if (ch === "'" || ch === '"') quote = ch
70
+ continue
71
+ }
72
+ if (quote === '"' && ch === '\\') { i += 1; continue }
73
+ if (ch === quote) {
74
+ if (quote === "'" && text[i + 1] === "'") { i += 1; continue }
75
+ quote = null
76
+ }
77
+ }
78
+ return { quote }
79
+ }
80
+
81
+ /**
82
+ * 结构校验(**纯函数**,永不抛)。返回 `{ ok, problems, summary }`。
83
+ * `summary` 只放"看得懂的事实"(行数 / 顶层项数 / 多行标量数),供报告与面板短句使用。
84
+ */
85
+ function validatePatchYaml(text) {
86
+ const source = typeof text === 'string' ? text : String(text ?? '')
87
+ const lines = source.replace(/\r\n/gu, '\n').split('\n')
88
+ const problems = []
89
+ let topItems = 0
90
+ let multilineScalars = 0
91
+ let placeholderLines = 0
92
+ let inBlockScalar = false
93
+ let blockIndent = 0
94
+ let openQuote = null
95
+
96
+ for (let i = 0; i < lines.length; i += 1) {
97
+ const raw = lines[i]
98
+ const lineNo = i + 1
99
+ if (raw.trim() === '') continue
100
+ if (/^\s*#/u.test(raw)) continue
101
+ // ① tab 缩进(无论在哪一层都不合法)
102
+ if (hasTabIndent(raw)) { problems.push(`第 ${lineNo} 行的缩进里有 tab(YAML 禁止用 tab 缩进)`); continue }
103
+ const indent = indentWidth(raw)
104
+ // 多行引号标量的续行:内容不参与结构判定,只继续找闭合
105
+ if (openQuote !== null) {
106
+ multilineScalars += 1
107
+ const st = scanValueQuotes(raw, openQuote)
108
+ openQuote = st.quote
109
+ continue
110
+ }
111
+ // 块标量内容:整段跳过(缩进 ≤ 块首行缩进的非空行 = 块结束)
112
+ if (inBlockScalar) {
113
+ if (indent > blockIndent) continue
114
+ inBlockScalar = false
115
+ }
116
+ const content = stripTrailingComment(raw)
117
+ if (content.trim() === '') continue
118
+ // ② 顶层空数组占位符(issue #7:`[]` 后面再跟条目 = 非法 YAML,启动直接崩)
119
+ if (/^\s*\[\s*\]\s*$/u.test(content)) { placeholderLines += 1; continue }
120
+ // ③ 顶层必须是序列项(补丁文件的形状:`- id:` / `- insert:` + 其子键)
121
+ if (indent === 0) {
122
+ if (!/^-\s/u.test(content) && content.trim() !== '-') {
123
+ problems.push(`第 ${lineNo} 行是顶层键值对(补丁文件顶层必须是 \`- \` 序列项):${content.trim().slice(0, 60)}`)
124
+ continue
125
+ }
126
+ topItems += 1
127
+ }
128
+ // ④ / ⑤ 值里的块标量与跨行引号
129
+ const kv = content.match(/^(\s*)(?:-\s+)?[^\s:#][^:]*:\s*(.*)$/u)
130
+ if (kv === null) continue
131
+ const value = kv[2].trim()
132
+ if (blockScalarIndicator(value) !== null) { inBlockScalar = true; blockIndent = indent; continue }
133
+ const st = scanValueQuotes(value, null)
134
+ if (st.quote !== null) openQuote = st.quote
135
+ }
136
+ // ④ 收尾:跨行引号到文件末尾仍未闭合 = 真截断
137
+ if (openQuote !== null) {
138
+ problems.push(`文件末尾仍有未闭合的引号(${openQuote === "'" ? '单引号' : '双引号'})—— 文件被截断或写坏`)
139
+ }
140
+ // ② 收尾:占位符与真实条目共存(真实事故形态)
141
+ if (placeholderLines > 0 && topItems > 0) {
142
+ problems.push(`顶层空数组占位符 \`[]\` 与 ${topItems} 个序列项共存(同一文档流非法,框架解析会崩)—— 写入前必须移除占位符`)
143
+ }
144
+ return {
145
+ ok: problems.length === 0,
146
+ problems,
147
+ summary: { lines: lines.length, topItems, multilineScalars, placeholderLines },
148
+ }
149
+ }
150
+
151
+ export { blockScalarIndicator, hasTabIndent, indentWidth, scanValueQuotes, stripTrailingComment, validatePatchYaml }
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— patch.js(由 Step 2 从 lib/index.js 原样切出,只搬移未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md §三 L1 · domain
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFile, writeFile } from 'node:fs/promises'
5
5
  import { dirname } from 'node:path'
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— presets.js(预设/agent 配置迁移:config 文件收集、persona 迁移、升级期迁移;分层 Step 6 从 lib/index.js 搬出,只搬移未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFileSync, writeFileSync, existsSync, readdirSync, copyFileSync } from 'node:fs'
5
5
  import { join } from 'node:path'
@@ -443,9 +443,9 @@ async function downloadReleaseArtifact(url, dest, options = {}) {
443
443
  * node_modules(包根本身由 paths.js 的 pluginRoot() 解析——全仓库只有那一处算包根)。
444
444
  * 返回 `<root>/<packageName>`。
445
445
  * ★ 特判的判据必须包含"自身确实住在某个 node_modules 里"(dirname 的 basename 为 node_modules):
446
- * 旧判据只有 `existsSync(<pkg>/package.json)`,而任何**开发检出**(D:\dsh\dsh-plugin-hub-refactor 这种
447
- * 不在 node_modules 下的目录)都满足它 → 目标会被算成检出的**父目录**,release 通道装一次插件就往
448
- * `D:\dsh\<包名>` 写一份。生产布局不变:宿主根层 `<host>/node_modules/<pkg>` 仍然命中特判。 */
446
+ * 旧判据只有 `existsSync(<pkg>/package.json)`,而任何**开发检出**(不在 node_modules 下的目录,
447
+ * 例如 `~/dev/<包名>`)都满足它 → 目标会被算成检出的**父目录**,release 通道装一次插件就往
448
+ * `~/dev/<包名>` 写一份。生产布局不变:宿主根层 `<host>/node_modules/<pkg>` 仍然命中特判。 */
449
449
  function releaseInstallTarget(profileDir, packageName) {
450
450
  let targetRoot = join(profileDir, 'node_modules')
451
451
  try {
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— repoland.js(仓库落地:落地目录配置 / 已落地列表 / 克隆;分层 Step 4 从 lib/index.js 搬出,只搬移未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, renameSync, statSync } from 'node:fs'
5
5
  import { spawn } from 'node:child_process'
@@ -1,6 +1,6 @@
1
1
  // L1 · domain —— runtime.js(从宿主运行时读取信息:loader 条目 / webServer 端口)。
2
2
  // 这两个函数原本吃 cordis 的 ctx;Step 8c 起它们的形参改名为 ports(= 调用方注入的窄接口),domain 层不再出现 ctx 标识符。
3
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
3
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
4
4
 
5
5
  import { FIBER_PHASE } from './compat.js'
6
6
  import { rowIdOf } from '../infra/paths.js'