berry-agent 0.1.0-alpha.1

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 (401) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/.api-emit.stamp +1 -0
  4. package/dist/.build-meta.json +6 -0
  5. package/dist/agent/events.js +1 -0
  6. package/dist/agent/index.js +2 -0
  7. package/dist/agent/loop.js +139 -0
  8. package/dist/agent/queue.js +119 -0
  9. package/dist/agent/stream.js +62 -0
  10. package/dist/agent/tools-batch.js +221 -0
  11. package/dist/agent/types.js +8 -0
  12. package/dist/api/berry-agent-llm.d.ts +18 -0
  13. package/dist/api/berry-agent.d.ts +9 -0
  14. package/dist/api/surface.json +1632 -0
  15. package/dist/api/tsconfig.paths.json +12 -0
  16. package/dist/api/typebox-compile.d.ts +2 -0
  17. package/dist/api/typebox-value.d.ts +2 -0
  18. package/dist/api/typebox.d.ts +2 -0
  19. package/dist/browser/cdp.js +256 -0
  20. package/dist/browser/codes.js +31 -0
  21. package/dist/browser/discover.js +107 -0
  22. package/dist/browser/engine.js +169 -0
  23. package/dist/browser/index.js +17 -0
  24. package/dist/browser/install.js +342 -0
  25. package/dist/browser/page.js +321 -0
  26. package/dist/browser/service.js +158 -0
  27. package/dist/browser/tools.js +198 -0
  28. package/dist/browser/types.js +81 -0
  29. package/dist/channels/ask-queue.js +82 -0
  30. package/dist/channels/commands.js +129 -0
  31. package/dist/channels/engine/cell.js +143 -0
  32. package/dist/channels/engine/diff.js +143 -0
  33. package/dist/channels/engine/engine.js +416 -0
  34. package/dist/channels/engine/index.js +8 -0
  35. package/dist/channels/engine/input-keys.js +86 -0
  36. package/dist/channels/engine/input.js +596 -0
  37. package/dist/channels/engine/memory-io.js +71 -0
  38. package/dist/channels/engine/process-io.js +38 -0
  39. package/dist/channels/engine/types.js +17 -0
  40. package/dist/channels/engine/width.js +122 -0
  41. package/dist/channels/index.js +15 -0
  42. package/dist/channels/registry.js +56 -0
  43. package/dist/channels/sdk/admit.js +29 -0
  44. package/dist/channels/sdk/backend.js +102 -0
  45. package/dist/channels/sdk/cursor.js +56 -0
  46. package/dist/channels/sdk/index.js +20 -0
  47. package/dist/channels/sdk/jsonl.js +208 -0
  48. package/dist/channels/sdk/protocol.js +24 -0
  49. package/dist/channels/sdk/schema.js +91 -0
  50. package/dist/channels/sdk/wire-core.js +446 -0
  51. package/dist/channels/service.js +169 -0
  52. package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
  53. package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
  54. package/dist/channels/tui/autocomplete/popup.js +148 -0
  55. package/dist/channels/tui/autocomplete/provider.js +7 -0
  56. package/dist/channels/tui/autocomplete/token.js +68 -0
  57. package/dist/channels/tui/backend/ansi-rows.js +211 -0
  58. package/dist/channels/tui/backend/main-screen.js +207 -0
  59. package/dist/channels/tui/backend/osc.js +91 -0
  60. package/dist/channels/tui/backend/transcript.js +230 -0
  61. package/dist/channels/tui/backend/tui-backend.js +915 -0
  62. package/dist/channels/tui/editor/editor-model.js +543 -0
  63. package/dist/channels/tui/editor/editor-view.js +140 -0
  64. package/dist/channels/tui/editor/editor.js +275 -0
  65. package/dist/channels/tui/editor/undo-stack.js +35 -0
  66. package/dist/channels/tui/editor/visual-lines.js +126 -0
  67. package/dist/channels/tui/editor/word-nav.js +106 -0
  68. package/dist/channels/tui/history/history-viewer.js +403 -0
  69. package/dist/channels/tui/index.js +3 -0
  70. package/dist/channels/tui/layout.js +167 -0
  71. package/dist/channels/tui/markdown/blocks.js +111 -0
  72. package/dist/channels/tui/markdown/inline.js +75 -0
  73. package/dist/channels/tui/markdown/markdown.js +182 -0
  74. package/dist/channels/tui/memory/memory-viewer.js +540 -0
  75. package/dist/channels/tui/overlay/alt-screen.js +105 -0
  76. package/dist/channels/tui/overlay/overlay.js +57 -0
  77. package/dist/channels/tui/overlay/select-confirm.js +156 -0
  78. package/dist/channels/tui/panels/todo-panel.js +54 -0
  79. package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
  80. package/dist/channels/tui/scroll/scroll-view.js +254 -0
  81. package/dist/channels/tui/status/status-line.js +76 -0
  82. package/dist/channels/tui/text.js +38 -0
  83. package/dist/channels/tui/theme.js +41 -0
  84. package/dist/channels/types.js +11 -0
  85. package/dist/channels/ui-core.js +209 -0
  86. package/dist/checkpoint/capture.js +45 -0
  87. package/dist/checkpoint/codes.js +33 -0
  88. package/dist/checkpoint/command.js +82 -0
  89. package/dist/checkpoint/gate.js +48 -0
  90. package/dist/checkpoint/index.js +20 -0
  91. package/dist/checkpoint/restore.js +172 -0
  92. package/dist/checkpoint/store.js +247 -0
  93. package/dist/checkpoint/types.js +4 -0
  94. package/dist/checkpoint/walk.js +146 -0
  95. package/dist/compaction/ccr-tools.js +112 -0
  96. package/dist/compaction/ccr.js +84 -0
  97. package/dist/compaction/codes.js +21 -0
  98. package/dist/compaction/index.js +18 -0
  99. package/dist/compaction/policy.js +200 -0
  100. package/dist/compaction/service.js +422 -0
  101. package/dist/compaction/slots.js +121 -0
  102. package/dist/compaction/types.js +10 -0
  103. package/dist/context/codes.js +40 -0
  104. package/dist/context/events.js +160 -0
  105. package/dist/context/index.js +13 -0
  106. package/dist/context/logger.js +136 -0
  107. package/dist/context/scope.js +182 -0
  108. package/dist/context/workspace.js +104 -0
  109. package/dist/contracts/agent-events.d.ts +73 -0
  110. package/dist/contracts/agent-events.js +16 -0
  111. package/dist/contracts/api.d.ts +249 -0
  112. package/dist/contracts/api.js +299 -0
  113. package/dist/contracts/approval.d.ts +37 -0
  114. package/dist/contracts/approval.js +9 -0
  115. package/dist/contracts/env-ref.d.ts +41 -0
  116. package/dist/contracts/env-ref.js +32 -0
  117. package/dist/contracts/errors.d.ts +56 -0
  118. package/dist/contracts/errors.js +266 -0
  119. package/dist/contracts/events.d.ts +70 -0
  120. package/dist/contracts/events.js +329 -0
  121. package/dist/contracts/index.d.ts +32 -0
  122. package/dist/contracts/index.js +26 -0
  123. package/dist/contracts/llm.d.ts +275 -0
  124. package/dist/contracts/llm.js +12 -0
  125. package/dist/contracts/messages.d.ts +63 -0
  126. package/dist/contracts/messages.js +65 -0
  127. package/dist/contracts/redact.d.ts +46 -0
  128. package/dist/contracts/redact.js +223 -0
  129. package/dist/contracts/tools.d.ts +197 -0
  130. package/dist/contracts/tools.js +33 -0
  131. package/dist/contracts/types.d.ts +302 -0
  132. package/dist/contracts/types.js +50 -0
  133. package/dist/contracts/ui.d.ts +122 -0
  134. package/dist/contracts/ui.js +15 -0
  135. package/dist/conversation/agent-service.js +61 -0
  136. package/dist/conversation/approval-wiring.js +82 -0
  137. package/dist/conversation/backoff.js +38 -0
  138. package/dist/conversation/codes.js +47 -0
  139. package/dist/conversation/control-tools.js +102 -0
  140. package/dist/conversation/control.js +210 -0
  141. package/dist/conversation/driver.js +922 -0
  142. package/dist/conversation/index.js +12 -0
  143. package/dist/conversation/model-visible.js +169 -0
  144. package/dist/conversation/open-tools.js +126 -0
  145. package/dist/conversation/reseed.js +132 -0
  146. package/dist/conversation/sessions.js +215 -0
  147. package/dist/conversation/todo.js +203 -0
  148. package/dist/conversation/types.js +35 -0
  149. package/dist/conversation/wiring.js +204 -0
  150. package/dist/credentials/codes.js +59 -0
  151. package/dist/credentials/commands.js +186 -0
  152. package/dist/credentials/env-ref.js +34 -0
  153. package/dist/credentials/index.js +28 -0
  154. package/dist/credentials/migration.js +18 -0
  155. package/dist/credentials/oauth.js +266 -0
  156. package/dist/credentials/refresh.js +152 -0
  157. package/dist/credentials/secrets.js +118 -0
  158. package/dist/credentials/types.js +30 -0
  159. package/dist/exec/bash.js +279 -0
  160. package/dist/exec/codes.js +37 -0
  161. package/dist/exec/env.js +99 -0
  162. package/dist/exec/environment.js +39 -0
  163. package/dist/exec/git-guard.js +623 -0
  164. package/dist/exec/index.js +24 -0
  165. package/dist/exec/registry.js +120 -0
  166. package/dist/exec/spawn.js +281 -0
  167. package/dist/exec/tail.js +77 -0
  168. package/dist/exec/types.js +5 -0
  169. package/dist/goal/codes.js +33 -0
  170. package/dist/goal/command.js +175 -0
  171. package/dist/goal/fold.js +121 -0
  172. package/dist/goal/gates.js +122 -0
  173. package/dist/goal/index.js +22 -0
  174. package/dist/goal/migration.js +48 -0
  175. package/dist/goal/service.js +501 -0
  176. package/dist/goal/todo-tool.js +174 -0
  177. package/dist/goal/types.js +1 -0
  178. package/dist/goal/update-tool.js +46 -0
  179. package/dist/host/approval-cmd.js +276 -0
  180. package/dist/host/assembly.js +1146 -0
  181. package/dist/host/boot-failures.js +104 -0
  182. package/dist/host/budget-advisory.js +74 -0
  183. package/dist/host/budget-broadcast.js +50 -0
  184. package/dist/host/builtins.js +54 -0
  185. package/dist/host/cli.js +544 -0
  186. package/dist/host/codes.js +120 -0
  187. package/dist/host/config-schema.js +195 -0
  188. package/dist/host/conversation-stack.js +654 -0
  189. package/dist/host/core-plugins.js +1685 -0
  190. package/dist/host/credentials-cmd.js +42 -0
  191. package/dist/host/disclosure.js +30 -0
  192. package/dist/host/dispatch.js +105 -0
  193. package/dist/host/doors-cmd.js +154 -0
  194. package/dist/host/dump-config.js +44 -0
  195. package/dist/host/hook-dispatch-guard.js +27 -0
  196. package/dist/host/import-gate.js +153 -0
  197. package/dist/host/index.js +54 -0
  198. package/dist/host/issue-session.js +241 -0
  199. package/dist/host/loader.js +372 -0
  200. package/dist/host/main.js +161 -0
  201. package/dist/host/manifest.js +381 -0
  202. package/dist/host/mcp-entry.js +53 -0
  203. package/dist/host/plugin-boot.js +952 -0
  204. package/dist/host/plugin-context.js +672 -0
  205. package/dist/host/plugin-install.js +480 -0
  206. package/dist/host/plugin-reload.js +124 -0
  207. package/dist/host/plugin-store.js +462 -0
  208. package/dist/host/plugin-tools.js +359 -0
  209. package/dist/host/plugin-uninstall.js +257 -0
  210. package/dist/host/plugins-cmd.js +302 -0
  211. package/dist/host/plugins-command.js +130 -0
  212. package/dist/host/plugins-config.js +195 -0
  213. package/dist/host/prompt-sections.js +133 -0
  214. package/dist/host/run-entry.js +544 -0
  215. package/dist/host/runtime.js +209 -0
  216. package/dist/host/scheduler-tick.js +294 -0
  217. package/dist/host/serve-daemon.js +501 -0
  218. package/dist/host/serve-entry.js +279 -0
  219. package/dist/host/session-anchor.js +41 -0
  220. package/dist/host/sessions-cmd.js +219 -0
  221. package/dist/host/sessions-face.js +47 -0
  222. package/dist/host/settings-store.js +112 -0
  223. package/dist/host/signals.js +69 -0
  224. package/dist/host/single-instance.js +105 -0
  225. package/dist/host/subagent-factory.js +291 -0
  226. package/dist/host/testkit/harness.js +283 -0
  227. package/dist/host/testkit/index.js +14 -0
  228. package/dist/host/testkit/matrix.js +302 -0
  229. package/dist/host/tool-policy-store.js +177 -0
  230. package/dist/host/triggers.js +212 -0
  231. package/dist/host/tui-entry.js +216 -0
  232. package/dist/host/webui-bridge.js +165 -0
  233. package/dist/issue/codes.js +38 -0
  234. package/dist/issue/filter.js +146 -0
  235. package/dist/issue/github.js +116 -0
  236. package/dist/issue/index.js +17 -0
  237. package/dist/issue/mount.js +70 -0
  238. package/dist/issue/poll.js +58 -0
  239. package/dist/issue/service.js +341 -0
  240. package/dist/issue/tools.js +70 -0
  241. package/dist/issue/types.js +34 -0
  242. package/dist/issue/webhook.js +126 -0
  243. package/dist/llm/codes.js +77 -0
  244. package/dist/llm/complete.js +196 -0
  245. package/dist/llm/events.js +15 -0
  246. package/dist/llm/index.js +24 -0
  247. package/dist/llm/inflight.js +65 -0
  248. package/dist/llm/model-id.js +58 -0
  249. package/dist/llm/provider-face.d.ts +32 -0
  250. package/dist/llm/provider-face.js +34 -0
  251. package/dist/llm/recovery.js +146 -0
  252. package/dist/llm/runtime.js +46 -0
  253. package/dist/llm/stream-fn.js +147 -0
  254. package/dist/lsp/codes.js +31 -0
  255. package/dist/lsp/connection.js +147 -0
  256. package/dist/lsp/frame.js +110 -0
  257. package/dist/lsp/index.js +16 -0
  258. package/dist/lsp/inject.js +58 -0
  259. package/dist/lsp/instance.js +264 -0
  260. package/dist/lsp/service.js +522 -0
  261. package/dist/lsp/tools.js +53 -0
  262. package/dist/lsp/types.js +201 -0
  263. package/dist/mcp/bridge.js +222 -0
  264. package/dist/mcp/codes.js +21 -0
  265. package/dist/mcp/index.js +19 -0
  266. package/dist/mcp/jsonrpc.js +230 -0
  267. package/dist/mcp/service.js +125 -0
  268. package/dist/mcp/tools.js +145 -0
  269. package/dist/mcp/types.js +108 -0
  270. package/dist/memory/cite.js +131 -0
  271. package/dist/memory/codes.js +50 -0
  272. package/dist/memory/command.js +71 -0
  273. package/dist/memory/consolidate.js +219 -0
  274. package/dist/memory/cycle.js +109 -0
  275. package/dist/memory/dao.js +878 -0
  276. package/dist/memory/diff.js +238 -0
  277. package/dist/memory/extract.js +181 -0
  278. package/dist/memory/fts.js +58 -0
  279. package/dist/memory/index.js +42 -0
  280. package/dist/memory/inject.js +354 -0
  281. package/dist/memory/merge.js +178 -0
  282. package/dist/memory/migration.js +132 -0
  283. package/dist/memory/pollution.js +59 -0
  284. package/dist/memory/port.js +385 -0
  285. package/dist/memory/review.js +201 -0
  286. package/dist/memory/scan.js +89 -0
  287. package/dist/memory/tools.js +508 -0
  288. package/dist/memory/types.js +140 -0
  289. package/dist/obs/codes.js +38 -0
  290. package/dist/obs/db.js +115 -0
  291. package/dist/obs/index.js +20 -0
  292. package/dist/obs/rollup.js +61 -0
  293. package/dist/obs/service.js +363 -0
  294. package/dist/obs/session-tools.js +282 -0
  295. package/dist/obs/session-view.js +225 -0
  296. package/dist/obs/tool.js +97 -0
  297. package/dist/obs/types.js +1 -0
  298. package/dist/persist/audit.js +63 -0
  299. package/dist/persist/aux.js +39 -0
  300. package/dist/persist/codes.js +35 -0
  301. package/dist/persist/index.js +30 -0
  302. package/dist/persist/load-history.js +50 -0
  303. package/dist/persist/migrations.js +37 -0
  304. package/dist/persist/paths.js +108 -0
  305. package/dist/persist/persistence.js +229 -0
  306. package/dist/persist/schema.js +99 -0
  307. package/dist/persist/secret-box.js +95 -0
  308. package/dist/persist/store.js +706 -0
  309. package/dist/persist/write-behind.js +238 -0
  310. package/dist/safety/approval.js +171 -0
  311. package/dist/safety/bwrap.js +123 -0
  312. package/dist/safety/codes.js +57 -0
  313. package/dist/safety/danger.js +505 -0
  314. package/dist/safety/gate.js +226 -0
  315. package/dist/safety/index.js +24 -0
  316. package/dist/safety/presets.js +73 -0
  317. package/dist/safety/roots.js +139 -0
  318. package/dist/safety/sandbox.js +214 -0
  319. package/dist/safety/seatbelt.js +88 -0
  320. package/dist/safety/sensitive.js +34 -0
  321. package/dist/safety/tool-policy.js +203 -0
  322. package/dist/safety/types.js +13 -0
  323. package/dist/scheduler/codes.js +54 -0
  324. package/dist/scheduler/cron-backend.js +135 -0
  325. package/dist/scheduler/engine.js +280 -0
  326. package/dist/scheduler/gates.js +60 -0
  327. package/dist/scheduler/index.js +25 -0
  328. package/dist/scheduler/migration.js +22 -0
  329. package/dist/scheduler/runner.js +171 -0
  330. package/dist/scheduler/schedule.js +193 -0
  331. package/dist/scheduler/service.js +283 -0
  332. package/dist/scheduler/tick.js +139 -0
  333. package/dist/scheduler/types.js +1 -0
  334. package/dist/sdk/http.js +865 -0
  335. package/dist/sdk/index.js +22 -0
  336. package/dist/sdk/mcp.js +289 -0
  337. package/dist/sdk/plugin-route-registry.js +126 -0
  338. package/dist/sdk/plugin-routes.js +149 -0
  339. package/dist/sdk/security.js +116 -0
  340. package/dist/sdk/types.js +32 -0
  341. package/dist/session/budget.js +159 -0
  342. package/dist/session/codes.js +40 -0
  343. package/dist/session/derive.js +180 -0
  344. package/dist/session/event-data.js +1 -0
  345. package/dist/session/fork.js +72 -0
  346. package/dist/session/import-gates.js +148 -0
  347. package/dist/session/index.js +18 -0
  348. package/dist/session/recover.js +103 -0
  349. package/dist/session/session.js +279 -0
  350. package/dist/session/snapshot.js +107 -0
  351. package/dist/skills/agents.js +242 -0
  352. package/dist/skills/codes.js +51 -0
  353. package/dist/skills/discovery.js +266 -0
  354. package/dist/skills/frontmatter.js +215 -0
  355. package/dist/skills/index.js +28 -0
  356. package/dist/skills/load.js +118 -0
  357. package/dist/skills/manage.js +201 -0
  358. package/dist/skills/registry.js +138 -0
  359. package/dist/skills/render.js +124 -0
  360. package/dist/skills/sections.js +171 -0
  361. package/dist/skills/types.js +21 -0
  362. package/dist/subagent/codes.js +50 -0
  363. package/dist/subagent/declarative.js +55 -0
  364. package/dist/subagent/index.js +19 -0
  365. package/dist/subagent/notify.js +51 -0
  366. package/dist/subagent/provide.js +7 -0
  367. package/dist/subagent/registry.js +149 -0
  368. package/dist/subagent/service.js +338 -0
  369. package/dist/subagent/surface.js +44 -0
  370. package/dist/subagent/tool.js +153 -0
  371. package/dist/subagent/types.js +25 -0
  372. package/dist/tools/apply-patch.js +164 -0
  373. package/dist/tools/codes.js +109 -0
  374. package/dist/tools/fs.js +501 -0
  375. package/dist/tools/index.js +17 -0
  376. package/dist/tools/observed.js +99 -0
  377. package/dist/tools/pipeline.js +176 -0
  378. package/dist/tools/protected-read.js +54 -0
  379. package/dist/tools/registry.js +250 -0
  380. package/dist/tools/search.js +451 -0
  381. package/dist/tools/worktree.js +327 -0
  382. package/dist/web/codes.js +37 -0
  383. package/dist/web/gate.js +43 -0
  384. package/dist/web/hygiene.js +195 -0
  385. package/dist/web/index.js +16 -0
  386. package/dist/web/service.js +207 -0
  387. package/dist/web/ssrf-guard.js +44 -0
  388. package/dist/web/tool.js +76 -0
  389. package/dist/web/types.js +10 -0
  390. package/dist/webui/assets/index-B-t2O8gh.js +22 -0
  391. package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
  392. package/dist/webui/index.html +14 -0
  393. package/dist/webui/index.js +14 -0
  394. package/dist/webui/server.js +549 -0
  395. package/dist/webui/types.js +38 -0
  396. package/examples/README.md +26 -0
  397. package/examples/minimal-code-plugin/entry.js +43 -0
  398. package/examples/minimal-code-plugin/package.json +12 -0
  399. package/examples/pure-skill-pack/package.json +14 -0
  400. package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
  401. package/package.json +86 -0
@@ -0,0 +1,302 @@
1
+ /**
2
+ * host/plugins-cmd — `plugins` 子命令族 CLI 入口(03 §5.8 三面同源之 CLI 面;
3
+ * 07 §5 命令族;批 12f-3 骨架 → 成熟度缺口 #10 装机面落码批写真身)。
4
+ *
5
+ * 子动词分账:
6
+ * - **list**:装载态清单三分区(启用/失败/禁用)。走与 dump-config **同一
7
+ * 合成代码路径**(真数据目录读侧 + 主库零落盘 + 不占标记)——装载面必须
8
+ * 跑(报告语义)。
9
+ * - **check**:纯只读零装配(07 §5——不走装载器直读装机账本)。api 块三色
10
+ * 裁决挂账 API 治理批(03 §8.4/§8.9)。
11
+ * - **install <ref>**:三源装机(plugin-install 执行器——ref 单参自含源前缀
12
+ * 与账本同词法;--min-release-age 旗标逐次覆盖 env 静置窗)。
13
+ * - **uninstall <id>**:双相(无 --confirm = inspect 只读报告 / 加 = execute
14
+ * 四段清算——plugin-uninstall 编舞;--data keep|purge 缺省 keep,--data
15
+ * 单独在场即拒——execute 载荷不静默猜)。开库走 Persistence 直开 +
16
+ * HOST_MIGRATION_TAIL 全链(零装配直开库纪律——sessions/credentials 同形)。
17
+ * - **mount/unmount/toggle <id>**:enabled.yaml 行编辑(plugin-store 行编辑
18
+ * 面;mount 前置查账本——core: id 或已装机 id 才可挂)。
19
+ * - **update <id>**:按源分派(plugin-install updatePlugin)。
20
+ *
21
+ * 写动词生效时点:CLI 短命进程不装配装载器——写侧动作**下次启动装载生效**
22
+ * (「装机零生效」语义的 CLI 面呈现;TUI 侧 /reload 排队编舞已落——assembly
23
+ * 编舞器 + 'reload' 命令注册,/reload 批收官)。
24
+ *
25
+ * 退出码:0 成功 / 1 执行失败(结算文本含原因)/ 用法错 2 归解析层。
26
+ */
27
+ import { readFileSync } from 'node:fs';
28
+ import { join } from 'node:path';
29
+ import { stdout as processStdout, stderr as processStderr } from 'node:process';
30
+ import { Persistence, createAuditFace, resolveDataDir } from '../persist/index.js';
31
+ import { assembleHostStack } from './assembly.js';
32
+ import { formatPluginFailureText } from './boot-failures.js';
33
+ import { checkPluginId } from './manifest.js';
34
+ import { installPlugin, updatePlugin, createDefaultSpawnRunner } from './plugin-install.js';
35
+ import { executeUninstall, inspectUninstall } from './plugin-uninstall.js';
36
+ import { mountRow, readLedger, toggleRow, unmountRow, createPluginStoreFs } from './plugin-store.js';
37
+ import { HOST_MIGRATION_TAIL } from './runtime.js';
38
+ /** plugins 子命令族主入口。返回进程退出码(0/1;用法错 2 归解析层) */
39
+ export async function runPluginsEntry(sub, options) {
40
+ switch (sub.sub) {
41
+ case 'list':
42
+ return runList(options);
43
+ case 'check':
44
+ return runCheck(options);
45
+ case 'install':
46
+ return runInstall(sub.ref, sub.minReleaseAge, options);
47
+ case 'uninstall':
48
+ return runUninstall(sub.id, sub.confirm, sub.dataAction, options);
49
+ case 'mount':
50
+ case 'unmount':
51
+ case 'toggle':
52
+ return runRowVerb(sub.sub, sub.id, options);
53
+ case 'update':
54
+ return runUpdate(sub.id, options);
55
+ }
56
+ }
57
+ /** list:同一合成代码路径装载 → 三分区人读输出(07 §5「装载态清单」) */
58
+ async function runList(options) {
59
+ const writeOut = options.writeOut ?? ((text) => processStdout.write(`${text}\n`));
60
+ const writeErr = options.writeErr ?? ((text) => processStderr.write(`${text}\n`));
61
+ // dataDir 解析与其他五动词同律(?? resolveDataDir()——env 梯子 BERRY_AGENT_DATA_DIR
62
+ // 由此接通;2026-09-13 可观测性批 obs-c 修:修前条件展开在 undefined 时省略
63
+ // dataDir ⇒ memory 诊断形 dataDir=null ⇒ enabled.yaml/账本整跳 ⇒ 磁盘装机
64
+ // 行三区皆隐〔真模型六轮实机实证:装+mount 后 list 仍只呈 16 core 件〕)
65
+ const dataDir = options.dataDir ?? resolveDataDir();
66
+ const assembly = await assembleHostStack({
67
+ runtime: {
68
+ dataDir,
69
+ memory: true, // 同构诊断形(与 dump-config 同形——真盘读侧 + 主库零落盘)
70
+ },
71
+ noPlugins: false, // list 语义 = 报告装载态——装载面必须跑(无 --no-plugins 旗标面)
72
+ debug: false,
73
+ version: options.version,
74
+ ...(options.env !== undefined ? { env: options.env } : {}),
75
+ ...(options.onRuntime !== undefined ? { onRuntime: options.onRuntime } : {}),
76
+ ...(options.corePlugins !== undefined ? { corePlugins: options.corePlugins } : {}),
77
+ });
78
+ if (!assembly.ok) {
79
+ writeErr(assembly.message);
80
+ return assembly.exitCode;
81
+ }
82
+ try {
83
+ const { activated, failed, skipped } = assembly.boot.report;
84
+ const lines = [];
85
+ lines.push(`启用(${activated.length}):`);
86
+ for (const a of activated)
87
+ lines.push(` ${a.id}` +
88
+ `${a.skillDirs.length > 0 ? ` 技能目录:${a.skillDirs.join('、')}` : ''}` +
89
+ `${a.agentDirs.length > 0 ? ` 子代理目录:${a.agentDirs.join('、')}` : ''}`);
90
+ lines.push(`失败(${failed.length}):`);
91
+ for (const f of failed)
92
+ lines.push(` ${f.id} ${formatPluginFailureText(f)}`);
93
+ lines.push(`禁用(${skipped.length}):`);
94
+ for (const s of skipped)
95
+ lines.push(` ${s.id} ${s.reason}`);
96
+ writeOut(lines.join('\n'));
97
+ return 0;
98
+ }
99
+ finally {
100
+ await assembly.runtime.shutdown();
101
+ }
102
+ }
103
+ /**
104
+ * check:纯只读零装配骨架(07 §5——数据面纯只读零装配;不走装载器)。
105
+ * 账本缺席/为空 = 零装机无可体检项(exit 0——「无断裂」成立);非空账本的
106
+ * apiVersion 三色裁决挂账 API 治理批(03 §8.4/§8.9——api 块回填同批)。
107
+ */
108
+ function runCheck(options) {
109
+ const writeOut = options.writeOut ?? ((text) => processStdout.write(`${text}\n`));
110
+ const dataDir = options.dataDir ?? resolveDataDir();
111
+ let raw = null;
112
+ try {
113
+ raw = readFileSync(join(dataDir, 'plugins', 'ledger.json'), 'utf8');
114
+ }
115
+ catch {
116
+ raw = null; // 缺席 = 零装机(ENOENT 同义——首启零文件零负担)
117
+ }
118
+ if (raw === null || raw.trim() === '' || raw.trim() === '{}') {
119
+ writeOut('装机账本缺席或为空——无可体检项(通过)');
120
+ return 0;
121
+ }
122
+ writeOut('装机账本非空,但 apiVersion 三色体检面尚未装配(归 API 治理批——03 §8.4/§8.9)——本命令本批不覆盖此形态');
123
+ return 1;
124
+ }
125
+ /* ---------------- 写侧六动词(装机面落码批 #10 真身) ---------------- */
126
+ /**
127
+ * 生命周期归因账真身(05 §1.1 生命周期归因面行——audit 落账批):惰性开库
128
+ * sink——动词成功尾首调才开 Persistence(只读路径 list/check 与失败路径零
129
+ * 开库零负担);Persistence.close 是 async,故配对 close 由调用层 finally
130
+ * await(库件回调是同步面——close 责任在 CLI 编排层)。落账失败 warn 不
131
+ * 阻塞主流程(行编辑已生效不回滚——与 llm/usage append 异常 warn 同哲学)。
132
+ */
133
+ function lifecycleAuditOf(options, writeErr) {
134
+ let persistence;
135
+ return {
136
+ sink: (type, data) => {
137
+ try {
138
+ if (persistence === undefined) {
139
+ // dbPath 显式随 dataDir(Persistence 的 dbPath/dataDir 分立解析——
140
+ // 缺省 dbPath 走 env 梯子不随 dataDir 选项;CLI 语义 = --data-dir
141
+ // 指到哪库就在哪〔三级梯子第二级本意〕)
142
+ persistence = Persistence.open({
143
+ ...(options.dataDir !== undefined
144
+ ? { dataDir: options.dataDir, dbPath: join(options.dataDir, 'sessions.db') }
145
+ : {}),
146
+ migrations: HOST_MIGRATION_TAIL,
147
+ warn: (message) => writeErr(`warn:${message}`),
148
+ });
149
+ }
150
+ createAuditFace(persistence.store.sqlite()).append(type, data);
151
+ }
152
+ catch (err) {
153
+ writeErr(`warn:生命周期审计落账失败(${type}):${err instanceof Error ? err.message : String(err)}——主流程不受影响(行编辑已生效)`);
154
+ }
155
+ },
156
+ close: async () => {
157
+ if (persistence !== undefined) {
158
+ const closing = persistence;
159
+ persistence = undefined;
160
+ await closing.close();
161
+ }
162
+ },
163
+ };
164
+ }
165
+ /** 装机执行器 deps 构造(CLI 面——真盘真 spawn;env 缺省 process.env) */
166
+ function installDepsOf(options, minReleaseAge, onLifecycleAudit) {
167
+ return {
168
+ dataDir: options.dataDir ?? resolveDataDir(),
169
+ fs: createPluginStoreFs(),
170
+ spawn: createDefaultSpawnRunner(),
171
+ env: options.env ?? process.env,
172
+ ...(minReleaseAge !== undefined ? { minReleaseAgeOverride: minReleaseAge } : {}),
173
+ ...(onLifecycleAudit !== undefined ? { onLifecycleAudit } : {}),
174
+ };
175
+ }
176
+ /** install <ref>:三源装机(§5.4——ref 词法坏形/撞名/收割失败各档结算文本直呈) */
177
+ async function runInstall(ref, minReleaseAge, options) {
178
+ const writeOut = options.writeOut ?? ((text) => processStdout.write(`${text}\n`));
179
+ const writeErr = options.writeErr ?? ((text) => processStderr.write(`${text}\n`));
180
+ const audit = lifecycleAuditOf(options, writeErr);
181
+ try {
182
+ const outcome = await installPlugin(installDepsOf(options, minReleaseAge, audit.sink), ref);
183
+ if (!outcome.ok) {
184
+ writeErr(outcome.message);
185
+ return 1;
186
+ }
187
+ writeOut(outcome.text);
188
+ return 0;
189
+ }
190
+ finally {
191
+ await audit.close();
192
+ }
193
+ }
194
+ /** update <id>:按源分派(§5.4——npm 重装/git 重克隆/local no-op) */
195
+ async function runUpdate(id, options) {
196
+ const writeOut = options.writeOut ?? ((text) => processStdout.write(`${text}\n`));
197
+ const writeErr = options.writeErr ?? ((text) => processStderr.write(`${text}\n`));
198
+ const audit = lifecycleAuditOf(options, writeErr);
199
+ try {
200
+ const outcome = await updatePlugin(installDepsOf(options, undefined, audit.sink), id);
201
+ if (!outcome.ok) {
202
+ writeErr(outcome.message);
203
+ return 1;
204
+ }
205
+ writeOut(outcome.text);
206
+ return 0;
207
+ }
208
+ finally {
209
+ await audit.close();
210
+ }
211
+ }
212
+ /**
213
+ * uninstall <id>:双相映射(§5.5——无 --confirm = inspect / 加 = execute)。
214
+ * 开库走 Persistence 直开 + 迁移链尾单源(域表 DROP/审计落账走同实例窄面);
215
+ * --data 单独在场即拒(execute 载荷不静默猜——双相语义混淆当响亮指出)。
216
+ */
217
+ async function runUninstall(id, confirm, dataAction, options) {
218
+ const writeOut = options.writeOut ?? ((text) => processStdout.write(`${text}\n`));
219
+ const writeErr = options.writeErr ?? ((text) => processStderr.write(`${text}\n`));
220
+ if (dataAction !== undefined && !confirm) {
221
+ writeErr('--data 是 execute 载荷——须与 --confirm 同场(无 --confirm 即 inspect 只读报告,不携带数据处置)');
222
+ return 1;
223
+ }
224
+ const dataDir = options.dataDir ?? resolveDataDir();
225
+ // dbPath 显式随 dataDir(同 lifecycleAuditOf——Persistence 的 dbPath/dataDir
226
+ // 分立解析,缺省 dbPath 走 env 梯子不随 dataDir 选项;CLI 语义 = --data-dir
227
+ // 指到哪库就在哪。装机面落码批遗留真 bug——审计落账批写测试抓获,修前
228
+ // 库恒开 env 梯子位、--data-dir 旗标形同虚设)
229
+ const persistence = Persistence.open({
230
+ ...(options.dataDir !== undefined
231
+ ? { dataDir: options.dataDir, dbPath: join(options.dataDir, 'sessions.db') }
232
+ : {}),
233
+ migrations: HOST_MIGRATION_TAIL,
234
+ warn: (message) => writeErr(`warn:${message}`),
235
+ });
236
+ try {
237
+ const deps = { dataDir, fs: createPluginStoreFs(), db: persistence.store.sqlite() };
238
+ const outcome = confirm
239
+ ? executeUninstall(deps, id, dataAction ?? 'keep') // 缺省 keep——execute 不静默猜 purge
240
+ : inspectUninstall(deps, id);
241
+ if (!outcome.ok) {
242
+ writeErr(outcome.message);
243
+ return 1;
244
+ }
245
+ writeOut(outcome.text);
246
+ return 0;
247
+ }
248
+ finally {
249
+ await persistence.close();
250
+ }
251
+ }
252
+ /**
253
+ * mount/unmount/toggle <id>:enabled.yaml 行编辑(§5.3/§5.2——下次启动装载
254
+ * 生效)。mount 前置两查:id 词法(enabled 行 id 空间——core: 前缀或用户
255
+ * 词法)+ 装机在场(core: id 豁免查账——内置态天然在场;用户 id 未装机拒,
256
+ * 防写死行 brick 下次 boot 读侧)。成功尾落生命周期归因账(unmount 幂等
257
+ * 跳过腿由库件零调用——无变更不造账)。
258
+ */
259
+ async function runRowVerb(verb, id, options) {
260
+ const writeOut = options.writeOut ?? ((text) => processStdout.write(`${text}\n`));
261
+ const writeErr = options.writeErr ?? ((text) => processStderr.write(`${text}\n`));
262
+ const dataDir = options.dataDir ?? resolveDataDir();
263
+ const fs = createPluginStoreFs();
264
+ if (verb === 'mount') {
265
+ if (!checkPluginId(id, { official: true })) {
266
+ writeErr(`插件 id 词法违例(${id}——小写字母数字连字符,官方件 core: 前缀同律)`);
267
+ return 1;
268
+ }
269
+ if (!id.startsWith('core:')) {
270
+ const ledgerRead = readLedger(dataDir, fs);
271
+ if (!ledgerRead.ok) {
272
+ writeErr(`装机账本损坏:${ledgerRead.reason}——拒写防覆盖(03 §5.4)`);
273
+ return 1;
274
+ }
275
+ if (!ledgerRead.entries.some((e) => e.id === id)) {
276
+ writeErr(`插件 ${id} 未装机——mount 先走 install(未装机挂行会在下次启动读侧降级,03 §5.3)`);
277
+ return 1;
278
+ }
279
+ }
280
+ }
281
+ const audit = lifecycleAuditOf(options, writeErr);
282
+ try {
283
+ const result = verb === 'mount'
284
+ ? mountRow(dataDir, id, undefined, fs, audit.sink)
285
+ : verb === 'unmount'
286
+ ? unmountRow(dataDir, id, fs, audit.sink)
287
+ : toggleRow(dataDir, id, fs, audit.sink);
288
+ if (!result.ok) {
289
+ writeErr(result.message);
290
+ return 1;
291
+ }
292
+ writeOut(verb === 'mount'
293
+ ? `已挂载:${id}——下次启动装载生效(CLI 短命进程不装配装载器;改行时点生效归 TUI /reload,03 §5.2)`
294
+ : verb === 'unmount'
295
+ ? `已卸下:${id}(装机保留)——下次启动生效(03 §5.3)`
296
+ : `已切换:${id} 禁用态翻转——下次启动生效(03 §5.2 三态语义)`);
297
+ return 0;
298
+ }
299
+ finally {
300
+ await audit.close();
301
+ }
302
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * host/plugins-command — TUI `/plugins` 命令面纯逻辑件(03 §5.8 三面同源之
3
+ * TUI 面;task #88 笔二)。
4
+ *
5
+ * argv → `{ok, text}` 结算形(c-5 credentials/commands 同款底座形态):动词语义
6
+ * 与回执文本在此单源,TUI 装配面只消费 text 经 notify 归因 'plugins' 投递。
7
+ * 与 CLI 面(plugins-cmd.ts runRowVerb)的分立点有三:
8
+ * 1. **成功尾自动链 /reload**(03 §5.2——mount/unmount/toggle 经 TUI 命令面
9
+ * 自动链;CLI 短命进程不装配装载器故不链,改行下次启动生效);
10
+ * 2. **审计 sink = 进程内 audit face**(assembly 单写者实例——与 CLI 惰性
11
+ * 开库形分立;落账失败 warn 不阻塞主流程,行编辑已生效不回滚);
12
+ * 3. **回执文案指向自动链**(CLI 文案指「下次启动生效」——两面对同一行编辑
13
+ * 动词的生效时点陈述各自诚实)。
14
+ *
15
+ * 子动词分账:
16
+ * - `list`:装载态清单三分区(启用/失败/禁用)——读内存 boot 报告投影(换血
17
+ * 取值器闭包——/reload 换代后即见新代),零磁盘零开库。
18
+ * - `mount <id>` / `unmount <id>` / `toggle <id>`:enabled.yaml 行编辑
19
+ * (plugin-store 行编辑面同源)。mount 前置两查与 CLI 同律:id 词法 +
20
+ * 装机在场(core: id 豁免查账——内置态天然在场)。
21
+ *
22
+ * 纯 memory 诊断形(dataDir null)无数据目录——写动词诚实拒(行编辑无落点),
23
+ * list 仍可用(读内存投影)。
24
+ */
25
+ import { formatPluginFailureText } from './boot-failures.js';
26
+ import { checkPluginId } from './manifest.js';
27
+ import { mountRow, readLedger, toggleRow, unmountRow } from './plugin-store.js';
28
+ /** 用法说明(命令描述位 + 未知动词回执共用单源) */
29
+ export const PLUGINS_CMD_USAGE = `/plugins list | mount <id> | unmount <id> | toggle <id> | config <id>
30
+ list 装载态清单三分区(启用/失败/禁用——内存读面零磁盘)
31
+ mount <id> 挂载已装机插件(成功尾自动链 /reload)
32
+ unmount <id> 卸下(装机保留——成功尾自动链 /reload)
33
+ toggle <id> 禁用态翻转(成功尾自动链 /reload)
34
+ config <id> 配置表单(configSchema 逐字段问答——secret 入凭证盒)
35
+ (install/uninstall/update 走 CLI:berry-agent plugins <sub>——03 §5.8 三面同源)`;
36
+ /** list 三分区渲染(CLI list 同构输出——两面同源人读形) */
37
+ function renderList(report) {
38
+ if (report === undefined) {
39
+ return '装载面未装配(noPlugins 诊断形)——无装载态可列。';
40
+ }
41
+ const lines = [];
42
+ lines.push(`启用(${report.activated.length}):`);
43
+ for (const a of report.activated)
44
+ lines.push(` ${a.id}${a.skillDirs.length > 0 ? ` 技能目录:${a.skillDirs.join('、')}` : ''}`);
45
+ lines.push(`失败(${report.failed.length}):`);
46
+ for (const f of report.failed)
47
+ lines.push(` ${f.id} ${formatPluginFailureText(f)}`);
48
+ lines.push(`禁用(${report.skipped.length}):`);
49
+ for (const s of report.skipped)
50
+ lines.push(` ${s.id} ${s.reason}`);
51
+ return lines.join('\n');
52
+ }
53
+ /** mount 前置两查(CLI runRowVerb 同律单源复述——id 词法 + 装机在场) */
54
+ function mountPreflight(id, deps) {
55
+ if (!checkPluginId(id, { official: true })) {
56
+ return `插件 id 词法违例(${id}——小写字母数字连字符,官方件 core: 前缀同律)`;
57
+ }
58
+ if (!id.startsWith('core:')) {
59
+ const ledgerRead = readLedger(deps.dataDir, deps.fs);
60
+ if (!ledgerRead.ok) {
61
+ return `装机账本损坏:${ledgerRead.reason}——拒写防覆盖(03 §5.4)`;
62
+ }
63
+ if (!ledgerRead.entries.some((e) => e.id === id)) {
64
+ return `插件 ${id} 未装机——mount 先走 install(未装机挂行会在下次启动读侧降级,03 §5.3)`;
65
+ }
66
+ }
67
+ return undefined; // 两查全过
68
+ }
69
+ /**
70
+ * `/plugins` 命令面主入口(argv → 结算形;async——config 表单腿问询在途)。
71
+ * 行编辑失败零副作用(无变更不造账不链 reload);成功尾序 = audit 落账 →
72
+ * 自动链 /reload → 回执。
73
+ */
74
+ export async function runPluginsCommand(argv, deps) {
75
+ const verb = argv[0];
76
+ // list:读内存投影——dataDir null 与 noPlugins 形均诚实呈现(零写面)
77
+ if (verb === 'list') {
78
+ return { ok: true, text: renderList(deps.report()) };
79
+ }
80
+ if (verb === 'config') {
81
+ const id = argv[1];
82
+ if (id === undefined || id === '') {
83
+ return { ok: false, text: `用法错——缺 <id>。\n${PLUGINS_CMD_USAGE}` };
84
+ }
85
+ if (deps.dataDir === null) {
86
+ return { ok: false, text: '纯 memory 诊断形无数据目录——写动词不可用(行编辑无落点)' };
87
+ }
88
+ if (deps.configForm === undefined) {
89
+ return {
90
+ ok: false,
91
+ text: 'config 表单腿未装配(此命令面无发起会话/问询通道)——TUI 会话内 /plugins config 或手编 enabled.yaml',
92
+ };
93
+ }
94
+ return deps.configForm(id);
95
+ }
96
+ if (verb === 'mount' || verb === 'unmount' || verb === 'toggle') {
97
+ if (deps.dataDir === null) {
98
+ return { ok: false, text: '纯 memory 诊断形无数据目录——写动词不可用(行编辑无落点)' };
99
+ }
100
+ const id = argv[1];
101
+ if (id === undefined || id === '') {
102
+ return { ok: false, text: `用法错——缺 <id>。\n${PLUGINS_CMD_USAGE}` };
103
+ }
104
+ if (verb === 'mount') {
105
+ const rejected = mountPreflight(id, deps);
106
+ if (rejected !== undefined)
107
+ return { ok: false, text: rejected };
108
+ }
109
+ const result = verb === 'mount'
110
+ ? mountRow(deps.dataDir, id, undefined, deps.fs, deps.auditSink)
111
+ : verb === 'unmount'
112
+ ? unmountRow(deps.dataDir, id, deps.fs, deps.auditSink)
113
+ : toggleRow(deps.dataDir, id, deps.fs, deps.auditSink);
114
+ if (!result.ok) {
115
+ return { ok: false, text: result.message }; // 行编辑失败——零副作用零链
116
+ }
117
+ // 成功尾:自动链 /reload(fire-and-forget——busy 期 reloader 自排队)
118
+ deps.requestReload();
119
+ const tail = '已自动链 /reload(会话运行中自动排队,run 收场后执行)';
120
+ return {
121
+ ok: true,
122
+ text: verb === 'mount'
123
+ ? `已挂载:${id}——${tail}`
124
+ : verb === 'unmount'
125
+ ? `已卸下:${id}(装机保留)——${tail}`
126
+ : `已切换:${id} 禁用态翻转——${tail}`,
127
+ };
128
+ }
129
+ return { ok: false, text: `未知动词或用法错。\n${PLUGINS_CMD_USAGE}` };
130
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * host/plugins-config — `/plugins config <id>` 表单腿编排(ix-3b/c——03 §1.2
3
+ * 表单条款 + 07 §4.3 提问队列消费)。
4
+ *
5
+ * fields 驱动逐字段问答:text/secret 用 input(secret 空答 = 保留现值)、
6
+ * boolean 用 select 两值(开/关——s-9 修)、select 用 select;问询经 ask
7
+ * seam(sessionId 由装配面绑定——表单需会话锚,无发起会话的面装配缺席)。写盘分两落点:
8
+ * - 非_secret 字段 + 未声明键透传 → enabled.yaml 行 config 整值替换
9
+ * (setRowConfig——保留 disabled/opens/doors 原样);
10
+ * - secret 字段 → 凭证盒 plugin:<id>/config:<key>(meta source 'manual',
11
+ * credentials/changed seam 归因——值恒不入生命周期面)。
12
+ *
13
+ * 取消语义(03 §1.2「取消 = 整次放弃」):任一 ask 拒绝(通道取消/abort)
14
+ * → 捕获为诚实回执,零写盘零凭证写;select 面永不 reject——取消/降级链
15
+ * 出值域答复的保守值 '' 是取消唯一信使(select 与 boolean 两值形同律;
16
+ * options 值域结构性非空,合法答复恒非 ''),按取消语义整次放弃——'' 落行
17
+ * 即写坏插件(不在值域,下次装载
18
+ * PLUGIN_CONFIG_INVALID 拒载)。落盘期异常按进度分档诚实回执(已写事实
19
+ * 不谎称零写盘)。缺省烘焙回避:值等于缺省源(行缺席
20
+ * 时宿主默认/字段 default)的字段不落行——行是覆盖仓,表单全默认直存会把
21
+ * 清单缺省烙进用户行,作者后续改 default 不再传播。
22
+ *
23
+ * 成功尾序:行写盘 → secret 入盒 → requestReload(与 mount 同律 03 §5.2
24
+ * 自动链)→ 回执(maskConfigSecrets 呈现纪律——secret 值恒遮蔽)。
25
+ */
26
+ import { pluginNamespace } from '../credentials/index.js';
27
+ import { readEnabledRowsForEdit, setRowConfig } from './plugin-store.js';
28
+ /** boolean 呈现词(人读形) */
29
+ const onOff = (v) => (v ? '开' : '关');
30
+ /**
31
+ * 表单腿主入口(id → 结算形)。异常问询(取消/abort/通道断)整次放弃——
32
+ * 捕获为诚实回执,不向上抛(TUI 装配面只消费 text)。
33
+ */
34
+ export async function runPluginConfigForm(pluginId, deps) {
35
+ const face = deps.configFaceOf(pluginId);
36
+ if (face === undefined) {
37
+ return { ok: false, text: `插件 ${pluginId} 无声明配置面(configSchema 缺席)——无可编辑字段。` };
38
+ }
39
+ // 现行行读(未声明键透传源 + 行内现值);损坏 = fail-loud 诚实拒(不写防覆盖)
40
+ const read = readEnabledRowsForEdit(deps.dataDir, deps.fs);
41
+ if (!read.ok)
42
+ return { ok: false, text: read.message };
43
+ const row = read.rows.find((r) => r.id === pluginId);
44
+ const rowConfig = typeof row?.config === 'object' && row.config !== null && !Array.isArray(row.config) ? { ...row.config } : {};
45
+ const defaults = typeof face.hostDefaults === 'object' && face.hostDefaults !== null && !Array.isArray(face.hostDefaults)
46
+ ? { ...face.hostDefaults }
47
+ : {};
48
+ let rowWritten = false; // 行 config 已写盘旗(落盘期异常分档回执判据——try/catch 两面共见,故置 try 外)
49
+ let writtenSecrets = 0; // 已入盒 secret 计数(同上——审计 seam 失败不折抵已写事实)
50
+ try {
51
+ // —— 逐字段问答(03 §1.2 表单条款——text/secret→input、boolean→select
52
+ // 两值〔s-9 修〕、select→select)——
53
+ const next = {}; // 落行值(非 secret)
54
+ const secrets = []; // 入盒值
55
+ const receipt = []; // 回执逐字段行(呈现纪律:secret 恒遮蔽)
56
+ for (const field of face.fields) {
57
+ const label = field.label ?? field.key;
58
+ const hint = field.description === undefined ? '' : `\n ${field.description}`;
59
+ // 字段 default 的型外安全读(secret 型无 default 位——判别联合外共取须窄化)
60
+ const fieldDefault = 'default' in field ? field.default : undefined;
61
+ // 生效现值 = 行 config ?? 宿主默认 ?? 字段 default(装载合成序同源——三级)
62
+ const currentRaw = field.key in rowConfig ? rowConfig[field.key] : (defaults[field.key] ?? fieldDefault);
63
+ const fromRow = field.key in rowConfig;
64
+ const defaultSource = fromRow ? rowConfig[field.key] : (defaults[field.key] ?? fieldDefault);
65
+ if (field.type === 'text') {
66
+ const placeholder = typeof currentRaw === 'string' ? currentRaw : undefined;
67
+ const answer = await deps.ask.input(`${label}:${hint}`, placeholder === undefined ? undefined : { placeholder });
68
+ // 空答 = 保留现值(清除值不可达——v1 诚实面:清值走手编 enabled.yaml)
69
+ const value = answer !== '' ? answer : typeof currentRaw === 'string' ? currentRaw : field.default;
70
+ if (value === undefined) {
71
+ if (field.required === true) {
72
+ return { ok: false, text: `必填字段 ${field.key}(text)无值——整次放弃(零写盘)。` };
73
+ }
74
+ receipt.push(` ${field.key} =(缺席)`);
75
+ continue;
76
+ }
77
+ if (fromRow || value !== defaultSource)
78
+ next[field.key] = value; // 缺省烘焙回避
79
+ receipt.push(` ${field.key} = ${value}`);
80
+ continue;
81
+ }
82
+ if (field.type === 'secret') {
83
+ const ns = pluginNamespace(pluginId);
84
+ const name = `config:${field.key}`;
85
+ const exists = deps.getCredential(ns, name) !== undefined;
86
+ const answer = await deps.ask.input(`${label}(敏感——入凭证盒不落 yaml):${hint}`, {
87
+ placeholder: exists ? '留空保留现值' : '未设置——输入新值',
88
+ });
89
+ if (answer !== '') {
90
+ secrets.push({ key: field.key, value: answer });
91
+ receipt.push(` ${field.key} = ***(凭证盒,已更新)`);
92
+ }
93
+ else if (exists) {
94
+ receipt.push(` ${field.key} = ***(凭证盒,保留现值)`);
95
+ }
96
+ else if (field.required === true) {
97
+ return { ok: false, text: `必填 secret 字段 ${field.key} 未提供且凭证盒无现值——整次放弃(零写盘)。` };
98
+ }
99
+ else {
100
+ receipt.push(` ${field.key} =(未设置)`);
101
+ }
102
+ continue;
103
+ }
104
+ if (field.type === 'boolean') {
105
+ // boolean 走 select 两值(开/关)不走 confirm——s-9 修(03 §1.2
106
+ // 2026-09-13 定形注):07 §4.3 confirm 后端 Esc 折保守值 false、与
107
+ // 显式「否」结构性不可辨,「取消 = 整次放弃」经 confirm 面不可表达
108
+ // (旧码取消折「关」落盘违取消条款);'' 取消信使同 select 律
109
+ const current = typeof currentRaw === 'boolean' ? currentRaw : (field.default ?? false);
110
+ const answer = await deps.ask.select(`${label}(当前 ${onOff(current)}):${hint}`, [
111
+ { value: 'true', label: '开' },
112
+ { value: 'false', label: '关' },
113
+ ]);
114
+ if (answer === '') {
115
+ return {
116
+ ok: false,
117
+ text: `已取消——${pluginId} config 编辑整次放弃(boolean 问询取消——零写盘零凭证写)。`,
118
+ };
119
+ }
120
+ const value = answer === 'true';
121
+ if (fromRow || value !== defaultSource)
122
+ next[field.key] = value;
123
+ receipt.push(` ${field.key} = ${onOff(value)}`);
124
+ continue;
125
+ }
126
+ // select:choices 即声明 options(值域单源);'' = 取消/降级链保守值
127
+ // (select 面永不 reject——ui-core 保守值恒 '';options 值域结构性非空
128
+ // 〔config-schema 深校验拒空 value〕,合法答复恒非 ''——'' 是取消唯一
129
+ // 信使,按取消语义整次放弃,绝不落行〔落行即写坏插件:'' 不在值域,
130
+ // 下次装载 PLUGIN_CONFIG_INVALID 拒载〕)
131
+ const choices = field.options;
132
+ const current = typeof currentRaw === 'string' ? currentRaw : field.default;
133
+ const value = await deps.ask.select(`${label}${current === undefined ? '' : `(当前 ${current})`}:${hint}`, choices);
134
+ if (value === '') {
135
+ return {
136
+ ok: false,
137
+ text: `已取消——${pluginId} config 编辑整次放弃(select 问询取消——零写盘零凭证写)。`,
138
+ };
139
+ }
140
+ if (fromRow || value !== defaultSource)
141
+ next[field.key] = value;
142
+ receipt.push(` ${field.key} = ${value}`);
143
+ }
144
+ // —— 落盘序:行 config 整值替换(未声明键透传)→ secret 入盒 → reload ——
145
+ const undeclared = Object.keys(rowConfig).filter((k) => !face.fields.some((f) => f.key === k));
146
+ const newRowConfig = {};
147
+ for (const key of undeclared)
148
+ newRowConfig[key] = rowConfig[key]; // 未声明键原样透传
149
+ for (const [key, value] of Object.entries(next))
150
+ newRowConfig[key] = value;
151
+ // 行缺席且新 config 为空 = 零覆盖需求(全随缺省)——不造 overlay 空行;
152
+ // 行在场则照写(幂等归一,未声明键清面场景亦达)
153
+ const rowWriteNeeded = row !== undefined || Object.keys(newRowConfig).length > 0;
154
+ if (rowWriteNeeded) {
155
+ const write = setRowConfig(deps.dataDir, pluginId, newRowConfig, deps.fs);
156
+ if (!write.ok)
157
+ return { ok: false, text: write.message };
158
+ rowWritten = true;
159
+ }
160
+ const ns = pluginNamespace(pluginId);
161
+ for (const { key, value } of secrets) {
162
+ deps.setCredential(ns, `config:${key}`, { apiKey: value, meta: { source: 'manual' } });
163
+ writtenSecrets += 1; // 入盒即已写(onCredentialChanged 审计 seam 失败不折抵)
164
+ deps.onCredentialChanged?.({ namespace: ns, name: `config:${key}`, action: 'add', origin: 'human' });
165
+ }
166
+ const changed = rowWriteNeeded || secrets.length > 0;
167
+ if (changed)
168
+ deps.requestReload();
169
+ // 呈现纪律内嵌于回执逐字段行(secret 恒 '***'——secret 值结构性不落行,
170
+ // maskConfigSecrets 的装载呈现位在 dump-config/list 面,此面无需再过一道)
171
+ const undeclaredLine = undeclared.length === 0 ? '' : `\n未声明键透传 ${undeclared.length} 个:${undeclared.join('、')}`;
172
+ const head = changed
173
+ ? `已更新 ${pluginId} 配置(行 config 整值替换 + secret 入凭证盒)——已自动链 /reload(会话运行中自动排队,run 收场后执行)`
174
+ : `无变更——全随缺省(值等于缺省源不落行,行是覆盖仓不烙缺省)`;
175
+ return {
176
+ ok: true,
177
+ text: `${head}\n${receipt.join('\n')}${undeclaredLine}`,
178
+ };
179
+ }
180
+ catch (err) {
181
+ const reason = err instanceof Error ? err.message : String(err);
182
+ // 问询期取消(落盘未达)= 整次放弃;落盘期异常按进度分档诚实回执——
183
+ // 已写事实不谎称零写盘(行写盘在全部问询之后,问询期异常恒走首档)
184
+ if (!rowWritten) {
185
+ return {
186
+ ok: false,
187
+ text: `已取消——${pluginId} config 编辑整次放弃(零写盘零凭证写):${reason}`,
188
+ };
189
+ }
190
+ return {
191
+ ok: false,
192
+ text: `已写入 ${pluginId} 行 config(${writtenSecrets} 个 secret 已入凭证盒、未回滚):${reason}——表单未完整收口,可重跑 /plugins config 覆盖修复或手编 enabled.yaml`,
193
+ };
194
+ }
195
+ }