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.
- package/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/.api-emit.stamp +1 -0
- package/dist/.build-meta.json +6 -0
- package/dist/agent/events.js +1 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/loop.js +139 -0
- package/dist/agent/queue.js +119 -0
- package/dist/agent/stream.js +62 -0
- package/dist/agent/tools-batch.js +221 -0
- package/dist/agent/types.js +8 -0
- package/dist/api/berry-agent-llm.d.ts +18 -0
- package/dist/api/berry-agent.d.ts +9 -0
- package/dist/api/surface.json +1632 -0
- package/dist/api/tsconfig.paths.json +12 -0
- package/dist/api/typebox-compile.d.ts +2 -0
- package/dist/api/typebox-value.d.ts +2 -0
- package/dist/api/typebox.d.ts +2 -0
- package/dist/browser/cdp.js +256 -0
- package/dist/browser/codes.js +31 -0
- package/dist/browser/discover.js +107 -0
- package/dist/browser/engine.js +169 -0
- package/dist/browser/index.js +17 -0
- package/dist/browser/install.js +342 -0
- package/dist/browser/page.js +321 -0
- package/dist/browser/service.js +158 -0
- package/dist/browser/tools.js +198 -0
- package/dist/browser/types.js +81 -0
- package/dist/channels/ask-queue.js +82 -0
- package/dist/channels/commands.js +129 -0
- package/dist/channels/engine/cell.js +143 -0
- package/dist/channels/engine/diff.js +143 -0
- package/dist/channels/engine/engine.js +416 -0
- package/dist/channels/engine/index.js +8 -0
- package/dist/channels/engine/input-keys.js +86 -0
- package/dist/channels/engine/input.js +596 -0
- package/dist/channels/engine/memory-io.js +71 -0
- package/dist/channels/engine/process-io.js +38 -0
- package/dist/channels/engine/types.js +17 -0
- package/dist/channels/engine/width.js +122 -0
- package/dist/channels/index.js +15 -0
- package/dist/channels/registry.js +56 -0
- package/dist/channels/sdk/admit.js +29 -0
- package/dist/channels/sdk/backend.js +102 -0
- package/dist/channels/sdk/cursor.js +56 -0
- package/dist/channels/sdk/index.js +20 -0
- package/dist/channels/sdk/jsonl.js +208 -0
- package/dist/channels/sdk/protocol.js +24 -0
- package/dist/channels/sdk/schema.js +91 -0
- package/dist/channels/sdk/wire-core.js +446 -0
- package/dist/channels/service.js +169 -0
- package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
- package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
- package/dist/channels/tui/autocomplete/popup.js +148 -0
- package/dist/channels/tui/autocomplete/provider.js +7 -0
- package/dist/channels/tui/autocomplete/token.js +68 -0
- package/dist/channels/tui/backend/ansi-rows.js +211 -0
- package/dist/channels/tui/backend/main-screen.js +207 -0
- package/dist/channels/tui/backend/osc.js +91 -0
- package/dist/channels/tui/backend/transcript.js +230 -0
- package/dist/channels/tui/backend/tui-backend.js +915 -0
- package/dist/channels/tui/editor/editor-model.js +543 -0
- package/dist/channels/tui/editor/editor-view.js +140 -0
- package/dist/channels/tui/editor/editor.js +275 -0
- package/dist/channels/tui/editor/undo-stack.js +35 -0
- package/dist/channels/tui/editor/visual-lines.js +126 -0
- package/dist/channels/tui/editor/word-nav.js +106 -0
- package/dist/channels/tui/history/history-viewer.js +403 -0
- package/dist/channels/tui/index.js +3 -0
- package/dist/channels/tui/layout.js +167 -0
- package/dist/channels/tui/markdown/blocks.js +111 -0
- package/dist/channels/tui/markdown/inline.js +75 -0
- package/dist/channels/tui/markdown/markdown.js +182 -0
- package/dist/channels/tui/memory/memory-viewer.js +540 -0
- package/dist/channels/tui/overlay/alt-screen.js +105 -0
- package/dist/channels/tui/overlay/overlay.js +57 -0
- package/dist/channels/tui/overlay/select-confirm.js +156 -0
- package/dist/channels/tui/panels/todo-panel.js +54 -0
- package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
- package/dist/channels/tui/scroll/scroll-view.js +254 -0
- package/dist/channels/tui/status/status-line.js +76 -0
- package/dist/channels/tui/text.js +38 -0
- package/dist/channels/tui/theme.js +41 -0
- package/dist/channels/types.js +11 -0
- package/dist/channels/ui-core.js +209 -0
- package/dist/checkpoint/capture.js +45 -0
- package/dist/checkpoint/codes.js +33 -0
- package/dist/checkpoint/command.js +82 -0
- package/dist/checkpoint/gate.js +48 -0
- package/dist/checkpoint/index.js +20 -0
- package/dist/checkpoint/restore.js +172 -0
- package/dist/checkpoint/store.js +247 -0
- package/dist/checkpoint/types.js +4 -0
- package/dist/checkpoint/walk.js +146 -0
- package/dist/compaction/ccr-tools.js +112 -0
- package/dist/compaction/ccr.js +84 -0
- package/dist/compaction/codes.js +21 -0
- package/dist/compaction/index.js +18 -0
- package/dist/compaction/policy.js +200 -0
- package/dist/compaction/service.js +422 -0
- package/dist/compaction/slots.js +121 -0
- package/dist/compaction/types.js +10 -0
- package/dist/context/codes.js +40 -0
- package/dist/context/events.js +160 -0
- package/dist/context/index.js +13 -0
- package/dist/context/logger.js +136 -0
- package/dist/context/scope.js +182 -0
- package/dist/context/workspace.js +104 -0
- package/dist/contracts/agent-events.d.ts +73 -0
- package/dist/contracts/agent-events.js +16 -0
- package/dist/contracts/api.d.ts +249 -0
- package/dist/contracts/api.js +299 -0
- package/dist/contracts/approval.d.ts +37 -0
- package/dist/contracts/approval.js +9 -0
- package/dist/contracts/env-ref.d.ts +41 -0
- package/dist/contracts/env-ref.js +32 -0
- package/dist/contracts/errors.d.ts +56 -0
- package/dist/contracts/errors.js +266 -0
- package/dist/contracts/events.d.ts +70 -0
- package/dist/contracts/events.js +329 -0
- package/dist/contracts/index.d.ts +32 -0
- package/dist/contracts/index.js +26 -0
- package/dist/contracts/llm.d.ts +275 -0
- package/dist/contracts/llm.js +12 -0
- package/dist/contracts/messages.d.ts +63 -0
- package/dist/contracts/messages.js +65 -0
- package/dist/contracts/redact.d.ts +46 -0
- package/dist/contracts/redact.js +223 -0
- package/dist/contracts/tools.d.ts +197 -0
- package/dist/contracts/tools.js +33 -0
- package/dist/contracts/types.d.ts +302 -0
- package/dist/contracts/types.js +50 -0
- package/dist/contracts/ui.d.ts +122 -0
- package/dist/contracts/ui.js +15 -0
- package/dist/conversation/agent-service.js +61 -0
- package/dist/conversation/approval-wiring.js +82 -0
- package/dist/conversation/backoff.js +38 -0
- package/dist/conversation/codes.js +47 -0
- package/dist/conversation/control-tools.js +102 -0
- package/dist/conversation/control.js +210 -0
- package/dist/conversation/driver.js +922 -0
- package/dist/conversation/index.js +12 -0
- package/dist/conversation/model-visible.js +169 -0
- package/dist/conversation/open-tools.js +126 -0
- package/dist/conversation/reseed.js +132 -0
- package/dist/conversation/sessions.js +215 -0
- package/dist/conversation/todo.js +203 -0
- package/dist/conversation/types.js +35 -0
- package/dist/conversation/wiring.js +204 -0
- package/dist/credentials/codes.js +59 -0
- package/dist/credentials/commands.js +186 -0
- package/dist/credentials/env-ref.js +34 -0
- package/dist/credentials/index.js +28 -0
- package/dist/credentials/migration.js +18 -0
- package/dist/credentials/oauth.js +266 -0
- package/dist/credentials/refresh.js +152 -0
- package/dist/credentials/secrets.js +118 -0
- package/dist/credentials/types.js +30 -0
- package/dist/exec/bash.js +279 -0
- package/dist/exec/codes.js +37 -0
- package/dist/exec/env.js +99 -0
- package/dist/exec/environment.js +39 -0
- package/dist/exec/git-guard.js +623 -0
- package/dist/exec/index.js +24 -0
- package/dist/exec/registry.js +120 -0
- package/dist/exec/spawn.js +281 -0
- package/dist/exec/tail.js +77 -0
- package/dist/exec/types.js +5 -0
- package/dist/goal/codes.js +33 -0
- package/dist/goal/command.js +175 -0
- package/dist/goal/fold.js +121 -0
- package/dist/goal/gates.js +122 -0
- package/dist/goal/index.js +22 -0
- package/dist/goal/migration.js +48 -0
- package/dist/goal/service.js +501 -0
- package/dist/goal/todo-tool.js +174 -0
- package/dist/goal/types.js +1 -0
- package/dist/goal/update-tool.js +46 -0
- package/dist/host/approval-cmd.js +276 -0
- package/dist/host/assembly.js +1146 -0
- package/dist/host/boot-failures.js +104 -0
- package/dist/host/budget-advisory.js +74 -0
- package/dist/host/budget-broadcast.js +50 -0
- package/dist/host/builtins.js +54 -0
- package/dist/host/cli.js +544 -0
- package/dist/host/codes.js +120 -0
- package/dist/host/config-schema.js +195 -0
- package/dist/host/conversation-stack.js +654 -0
- package/dist/host/core-plugins.js +1685 -0
- package/dist/host/credentials-cmd.js +42 -0
- package/dist/host/disclosure.js +30 -0
- package/dist/host/dispatch.js +105 -0
- package/dist/host/doors-cmd.js +154 -0
- package/dist/host/dump-config.js +44 -0
- package/dist/host/hook-dispatch-guard.js +27 -0
- package/dist/host/import-gate.js +153 -0
- package/dist/host/index.js +54 -0
- package/dist/host/issue-session.js +241 -0
- package/dist/host/loader.js +372 -0
- package/dist/host/main.js +161 -0
- package/dist/host/manifest.js +381 -0
- package/dist/host/mcp-entry.js +53 -0
- package/dist/host/plugin-boot.js +952 -0
- package/dist/host/plugin-context.js +672 -0
- package/dist/host/plugin-install.js +480 -0
- package/dist/host/plugin-reload.js +124 -0
- package/dist/host/plugin-store.js +462 -0
- package/dist/host/plugin-tools.js +359 -0
- package/dist/host/plugin-uninstall.js +257 -0
- package/dist/host/plugins-cmd.js +302 -0
- package/dist/host/plugins-command.js +130 -0
- package/dist/host/plugins-config.js +195 -0
- package/dist/host/prompt-sections.js +133 -0
- package/dist/host/run-entry.js +544 -0
- package/dist/host/runtime.js +209 -0
- package/dist/host/scheduler-tick.js +294 -0
- package/dist/host/serve-daemon.js +501 -0
- package/dist/host/serve-entry.js +279 -0
- package/dist/host/session-anchor.js +41 -0
- package/dist/host/sessions-cmd.js +219 -0
- package/dist/host/sessions-face.js +47 -0
- package/dist/host/settings-store.js +112 -0
- package/dist/host/signals.js +69 -0
- package/dist/host/single-instance.js +105 -0
- package/dist/host/subagent-factory.js +291 -0
- package/dist/host/testkit/harness.js +283 -0
- package/dist/host/testkit/index.js +14 -0
- package/dist/host/testkit/matrix.js +302 -0
- package/dist/host/tool-policy-store.js +177 -0
- package/dist/host/triggers.js +212 -0
- package/dist/host/tui-entry.js +216 -0
- package/dist/host/webui-bridge.js +165 -0
- package/dist/issue/codes.js +38 -0
- package/dist/issue/filter.js +146 -0
- package/dist/issue/github.js +116 -0
- package/dist/issue/index.js +17 -0
- package/dist/issue/mount.js +70 -0
- package/dist/issue/poll.js +58 -0
- package/dist/issue/service.js +341 -0
- package/dist/issue/tools.js +70 -0
- package/dist/issue/types.js +34 -0
- package/dist/issue/webhook.js +126 -0
- package/dist/llm/codes.js +77 -0
- package/dist/llm/complete.js +196 -0
- package/dist/llm/events.js +15 -0
- package/dist/llm/index.js +24 -0
- package/dist/llm/inflight.js +65 -0
- package/dist/llm/model-id.js +58 -0
- package/dist/llm/provider-face.d.ts +32 -0
- package/dist/llm/provider-face.js +34 -0
- package/dist/llm/recovery.js +146 -0
- package/dist/llm/runtime.js +46 -0
- package/dist/llm/stream-fn.js +147 -0
- package/dist/lsp/codes.js +31 -0
- package/dist/lsp/connection.js +147 -0
- package/dist/lsp/frame.js +110 -0
- package/dist/lsp/index.js +16 -0
- package/dist/lsp/inject.js +58 -0
- package/dist/lsp/instance.js +264 -0
- package/dist/lsp/service.js +522 -0
- package/dist/lsp/tools.js +53 -0
- package/dist/lsp/types.js +201 -0
- package/dist/mcp/bridge.js +222 -0
- package/dist/mcp/codes.js +21 -0
- package/dist/mcp/index.js +19 -0
- package/dist/mcp/jsonrpc.js +230 -0
- package/dist/mcp/service.js +125 -0
- package/dist/mcp/tools.js +145 -0
- package/dist/mcp/types.js +108 -0
- package/dist/memory/cite.js +131 -0
- package/dist/memory/codes.js +50 -0
- package/dist/memory/command.js +71 -0
- package/dist/memory/consolidate.js +219 -0
- package/dist/memory/cycle.js +109 -0
- package/dist/memory/dao.js +878 -0
- package/dist/memory/diff.js +238 -0
- package/dist/memory/extract.js +181 -0
- package/dist/memory/fts.js +58 -0
- package/dist/memory/index.js +42 -0
- package/dist/memory/inject.js +354 -0
- package/dist/memory/merge.js +178 -0
- package/dist/memory/migration.js +132 -0
- package/dist/memory/pollution.js +59 -0
- package/dist/memory/port.js +385 -0
- package/dist/memory/review.js +201 -0
- package/dist/memory/scan.js +89 -0
- package/dist/memory/tools.js +508 -0
- package/dist/memory/types.js +140 -0
- package/dist/obs/codes.js +38 -0
- package/dist/obs/db.js +115 -0
- package/dist/obs/index.js +20 -0
- package/dist/obs/rollup.js +61 -0
- package/dist/obs/service.js +363 -0
- package/dist/obs/session-tools.js +282 -0
- package/dist/obs/session-view.js +225 -0
- package/dist/obs/tool.js +97 -0
- package/dist/obs/types.js +1 -0
- package/dist/persist/audit.js +63 -0
- package/dist/persist/aux.js +39 -0
- package/dist/persist/codes.js +35 -0
- package/dist/persist/index.js +30 -0
- package/dist/persist/load-history.js +50 -0
- package/dist/persist/migrations.js +37 -0
- package/dist/persist/paths.js +108 -0
- package/dist/persist/persistence.js +229 -0
- package/dist/persist/schema.js +99 -0
- package/dist/persist/secret-box.js +95 -0
- package/dist/persist/store.js +706 -0
- package/dist/persist/write-behind.js +238 -0
- package/dist/safety/approval.js +171 -0
- package/dist/safety/bwrap.js +123 -0
- package/dist/safety/codes.js +57 -0
- package/dist/safety/danger.js +505 -0
- package/dist/safety/gate.js +226 -0
- package/dist/safety/index.js +24 -0
- package/dist/safety/presets.js +73 -0
- package/dist/safety/roots.js +139 -0
- package/dist/safety/sandbox.js +214 -0
- package/dist/safety/seatbelt.js +88 -0
- package/dist/safety/sensitive.js +34 -0
- package/dist/safety/tool-policy.js +203 -0
- package/dist/safety/types.js +13 -0
- package/dist/scheduler/codes.js +54 -0
- package/dist/scheduler/cron-backend.js +135 -0
- package/dist/scheduler/engine.js +280 -0
- package/dist/scheduler/gates.js +60 -0
- package/dist/scheduler/index.js +25 -0
- package/dist/scheduler/migration.js +22 -0
- package/dist/scheduler/runner.js +171 -0
- package/dist/scheduler/schedule.js +193 -0
- package/dist/scheduler/service.js +283 -0
- package/dist/scheduler/tick.js +139 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/sdk/http.js +865 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/mcp.js +289 -0
- package/dist/sdk/plugin-route-registry.js +126 -0
- package/dist/sdk/plugin-routes.js +149 -0
- package/dist/sdk/security.js +116 -0
- package/dist/sdk/types.js +32 -0
- package/dist/session/budget.js +159 -0
- package/dist/session/codes.js +40 -0
- package/dist/session/derive.js +180 -0
- package/dist/session/event-data.js +1 -0
- package/dist/session/fork.js +72 -0
- package/dist/session/import-gates.js +148 -0
- package/dist/session/index.js +18 -0
- package/dist/session/recover.js +103 -0
- package/dist/session/session.js +279 -0
- package/dist/session/snapshot.js +107 -0
- package/dist/skills/agents.js +242 -0
- package/dist/skills/codes.js +51 -0
- package/dist/skills/discovery.js +266 -0
- package/dist/skills/frontmatter.js +215 -0
- package/dist/skills/index.js +28 -0
- package/dist/skills/load.js +118 -0
- package/dist/skills/manage.js +201 -0
- package/dist/skills/registry.js +138 -0
- package/dist/skills/render.js +124 -0
- package/dist/skills/sections.js +171 -0
- package/dist/skills/types.js +21 -0
- package/dist/subagent/codes.js +50 -0
- package/dist/subagent/declarative.js +55 -0
- package/dist/subagent/index.js +19 -0
- package/dist/subagent/notify.js +51 -0
- package/dist/subagent/provide.js +7 -0
- package/dist/subagent/registry.js +149 -0
- package/dist/subagent/service.js +338 -0
- package/dist/subagent/surface.js +44 -0
- package/dist/subagent/tool.js +153 -0
- package/dist/subagent/types.js +25 -0
- package/dist/tools/apply-patch.js +164 -0
- package/dist/tools/codes.js +109 -0
- package/dist/tools/fs.js +501 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/observed.js +99 -0
- package/dist/tools/pipeline.js +176 -0
- package/dist/tools/protected-read.js +54 -0
- package/dist/tools/registry.js +250 -0
- package/dist/tools/search.js +451 -0
- package/dist/tools/worktree.js +327 -0
- package/dist/web/codes.js +37 -0
- package/dist/web/gate.js +43 -0
- package/dist/web/hygiene.js +195 -0
- package/dist/web/index.js +16 -0
- package/dist/web/service.js +207 -0
- package/dist/web/ssrf-guard.js +44 -0
- package/dist/web/tool.js +76 -0
- package/dist/web/types.js +10 -0
- package/dist/webui/assets/index-B-t2O8gh.js +22 -0
- package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
- package/dist/webui/index.html +14 -0
- package/dist/webui/index.js +14 -0
- package/dist/webui/server.js +549 -0
- package/dist/webui/types.js +38 -0
- package/examples/README.md +26 -0
- package/examples/minimal-code-plugin/entry.js +43 -0
- package/examples/minimal-code-plugin/package.json +12 -0
- package/examples/pure-skill-pack/package.json +14 -0
- package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
- package/package.json +86 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host/plugin-install — 三源装机执行器 + 收割 + update 分派(03 §5.4——
|
|
3
|
+
* 成熟度缺口 #10 装机面落码批 10b)。
|
|
4
|
+
*
|
|
5
|
+
* 三源(§5.4 物理布局/凭证纪律):
|
|
6
|
+
* - **npm**:`npm install --prefix <dataDir>/plugins` 进程外 spawn(07 §5
|
|
7
|
+
* 「用户显式动作族」——不进 exec 沙箱体系);供应链四件套执法——
|
|
8
|
+
* `--save-exact`(依赖记录钉版落锚)/`--omit=dev`/`--min-release-age`
|
|
9
|
+
* (静置窗,缺省 1440 分钟)/`--ignore-scripts`(安装期脚本缺省禁跑)
|
|
10
|
+
* + `--omit=peer --legacy-peer-deps`(承 berry 实践条款);装机树锚
|
|
11
|
+
* package.json(缺锚 npm 会向上爬找父 package.json 污染用户文件——锚
|
|
12
|
+
* 是防线非装饰);integrity 从 `<prefix>/.package-lock.json` 收割落账
|
|
13
|
+
* (tarball 凭证——§5.4「dist-tag 不算凭证」)。
|
|
14
|
+
* - **git**:tmp 克隆 → checkout ref → HEAD commit 收割落账(精确 commit
|
|
15
|
+
* 锁定)→ rename 落 `plugins/git/<host>/<首段>/<repo>/`(分层防撞名)。
|
|
16
|
+
* - **local**:直引不拷贝(installPath = canonical 绝对;§5.5 不删用户
|
|
17
|
+
* 目录)——无凭证字段。
|
|
18
|
+
*
|
|
19
|
+
* 收割(§5.4 词表账本):jiti 求值入口模块读 `events` 导出——「装机零生效」
|
|
20
|
+
* 的唯一例外(仅模块求值,零 apply 零注册);jiti 参数与装载器同形(虚拟面
|
|
21
|
+
* 四键直注 + import 门禁 transform + fsCache 关——同律不因装机时点放宽)。
|
|
22
|
+
* 纯声明包(declared-payload 态)零码收割(declaredEvents = [])。
|
|
23
|
+
*
|
|
24
|
+
* 失败回滚:装机物落位后清单校验/收割失败 = rm 装机物再拒(不留半装机残影
|
|
25
|
+
* ——账本未落,回滚后两账一致空态)。
|
|
26
|
+
*
|
|
27
|
+
* spawn 注入面:测试注假件零真网络(npm/git 两执行器的 argv 断言 + 落账
|
|
28
|
+
* 编舞可测);local 源与收割真跑(本地 fixture 零网络)。
|
|
29
|
+
*/
|
|
30
|
+
import { execFile } from 'node:child_process';
|
|
31
|
+
import { mkdtempSync } from 'node:fs';
|
|
32
|
+
import { tmpdir } from 'node:os';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
import { promisify } from 'node:util';
|
|
35
|
+
import { createJiti } from 'jiti';
|
|
36
|
+
import { BaseError } from '../contracts/index.js';
|
|
37
|
+
import { installPathForGit, installPathForLocal, installPathForNpm, ledgerPath, parentDir, readLedger, resolveInstallPath, upsertLedgerEntry, } from './plugin-store.js';
|
|
38
|
+
import { parseManifest } from './manifest.js';
|
|
39
|
+
import { createGateTransform } from './import-gate.js';
|
|
40
|
+
import { loadDefaultVirtualFaces, resolvePkgMain } from './loader.js';
|
|
41
|
+
/**
|
|
42
|
+
* ref 单源词法(CLI `plugins install <ref>` 与账本 ref 字段同一表示——
|
|
43
|
+
* 无变换直通):`npm:<pkg>[@<version>]` / `git:<url>[#<ref>]` /
|
|
44
|
+
* `local:<abs-path>`。前缀三选一强制(无前缀 = 用法错——不猜默认源)。
|
|
45
|
+
*/
|
|
46
|
+
export function parsePluginRef(ref) {
|
|
47
|
+
if (ref.startsWith('npm:')) {
|
|
48
|
+
const spec = ref.slice('npm:'.length);
|
|
49
|
+
if (spec.length === 0)
|
|
50
|
+
return { ok: false, message: 'npm ref 缺包名(形如 npm:acme-widgets@1.2.0)' };
|
|
51
|
+
const at = spec.lastIndexOf('@');
|
|
52
|
+
// scoped 包 @scope/pkg:@ 在位 0 是 scope 前缀非版本分隔——只认 >0 的 @
|
|
53
|
+
if (at > 0) {
|
|
54
|
+
return { ok: true, parsed: { source: 'npm', pkg: spec.slice(0, at), version: spec.slice(at + 1) } };
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, parsed: { source: 'npm', pkg: spec } };
|
|
57
|
+
}
|
|
58
|
+
if (ref.startsWith('git:')) {
|
|
59
|
+
const spec = ref.slice('git:'.length);
|
|
60
|
+
if (spec.length === 0)
|
|
61
|
+
return { ok: false, message: 'git ref 缺 url(形如 git:https://github.com/o/r.git#v1.2.0)' };
|
|
62
|
+
const hash = spec.lastIndexOf('#');
|
|
63
|
+
if (hash >= 0) {
|
|
64
|
+
const gitRef = spec.slice(hash + 1);
|
|
65
|
+
if (gitRef.length === 0)
|
|
66
|
+
return { ok: false, message: 'git ref 的 # 后为空(分支/tag/commit)' };
|
|
67
|
+
return { ok: true, parsed: { source: 'git', url: spec.slice(0, hash), gitRef } };
|
|
68
|
+
}
|
|
69
|
+
return { ok: true, parsed: { source: 'git', url: spec } };
|
|
70
|
+
}
|
|
71
|
+
if (ref.startsWith('local:')) {
|
|
72
|
+
const spec = ref.slice('local:'.length);
|
|
73
|
+
if (spec.length === 0)
|
|
74
|
+
return { ok: false, message: 'local ref 缺路径(形如 local:/abs/path/to/plugin)' };
|
|
75
|
+
return { ok: true, parsed: { source: 'local', path: spec } };
|
|
76
|
+
}
|
|
77
|
+
return { ok: false, message: `ref 须自含源前缀 npm:/git:/local:(得 "${ref}")——与装机账本 ref 字段同形` };
|
|
78
|
+
}
|
|
79
|
+
/* ---------------- min-release-age 配置解析(§5.4 供应链③) ---------------- */
|
|
80
|
+
/** 静置窗缺省(分钟)——24h,03 §5.4 装机面落码批定形 */
|
|
81
|
+
export const DEFAULT_MIN_RELEASE_AGE_MINUTES = 1440;
|
|
82
|
+
/** env 名(07 §2.1 名册在册位——BERRY_AGENT_PLUGIN_MIN_RELEASE_AGE) */
|
|
83
|
+
export const MIN_RELEASE_AGE_ENV = 'BERRY_AGENT_PLUGIN_MIN_RELEASE_AGE';
|
|
84
|
+
/**
|
|
85
|
+
* 静置窗解析(CLI 旗标 > env > 缺省 1440)。无效值(非数字/负数/NaN)=
|
|
86
|
+
* fail-loud(短命装机动作当场红优于静默降级——与 LOG_LEVEL 长跑让位律
|
|
87
|
+
* 分立两律)。
|
|
88
|
+
*/
|
|
89
|
+
export function resolveMinReleaseAge(input) {
|
|
90
|
+
if (input.cliFlag !== undefined)
|
|
91
|
+
return input.cliFlag; // CLI 解析层已验正整数
|
|
92
|
+
const raw = input.env?.[MIN_RELEASE_AGE_ENV];
|
|
93
|
+
if (raw !== undefined && raw !== '') {
|
|
94
|
+
const n = Number(raw);
|
|
95
|
+
if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n)) {
|
|
96
|
+
throw new BaseError('PLUGIN_INSTALL_FAILED', `${MIN_RELEASE_AGE_ENV}="${raw}" 坏形——须为非负整数分钟数(0 = 显式关窗)`);
|
|
97
|
+
}
|
|
98
|
+
return n;
|
|
99
|
+
}
|
|
100
|
+
return DEFAULT_MIN_RELEASE_AGE_MINUTES;
|
|
101
|
+
}
|
|
102
|
+
/** 缺省真身(maxBuffer 放大——npm 安装输出可观;非零退出 = reject 带 stderr) */
|
|
103
|
+
export function createDefaultSpawnRunner() {
|
|
104
|
+
const runFile = promisify(execFile);
|
|
105
|
+
return {
|
|
106
|
+
run: (cmd, args, options) => runFile(cmd, [...args], { cwd: options.cwd, maxBuffer: 64 * 1024 * 1024 }),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/* ---------------- 装机编舞主入口 ---------------- */
|
|
110
|
+
/**
|
|
111
|
+
* install 编舞(§5.4):撞名拒(同 id 已装指路 update)→ 按源执行 → 清单
|
|
112
|
+
* 校验 → 收割 → 落账。执行器产出 installPath + 凭证字段;收割失败/清单坏形
|
|
113
|
+
* = 回滚装机物再拒(不留半装机残影)。core: 前缀 id 拒(官方件身份——非
|
|
114
|
+
* 装机物)。`replacingId` = update 分派的换装豁免位(npm 重装腿复用本编舞
|
|
115
|
+
* 时旧条目尚在场——恰是被替换者,不构成撞名;其余 id 照拒)。
|
|
116
|
+
*/
|
|
117
|
+
export async function installPlugin(deps, ref, opts = {}) {
|
|
118
|
+
const parsed = parsePluginRef(ref);
|
|
119
|
+
if (!parsed.ok)
|
|
120
|
+
return parsed;
|
|
121
|
+
// 账本前置读(坏账本拒写防覆盖——与 upsertLedgerEntry 内防线双检)
|
|
122
|
+
const ledgerRead = readLedger(deps.dataDir, deps.fs);
|
|
123
|
+
if (!ledgerRead.ok) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
message: `装机账本损坏(${ledgerPath(deps.dataDir)}):${ledgerRead.reason}——拒写防覆盖(03 §5.4)`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// 执行器先行(装机物落位 + manifest 读——失败红即拒,账面零动作)
|
|
130
|
+
let product;
|
|
131
|
+
try {
|
|
132
|
+
product =
|
|
133
|
+
parsed.parsed.source === 'npm'
|
|
134
|
+
? await runNpmInstall(deps, parsed.parsed)
|
|
135
|
+
: parsed.parsed.source === 'git'
|
|
136
|
+
? await runGitInstall(deps, parsed.parsed)
|
|
137
|
+
: runLocalInstall(deps, parsed.parsed);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
return { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
141
|
+
}
|
|
142
|
+
// 清单校验(§1.2 拒绝式——装机时点红优于装载时点)
|
|
143
|
+
if (!product.manifest.ok) {
|
|
144
|
+
rollbackInstall(deps, product.installPath);
|
|
145
|
+
return { ok: false, message: `清单校验失败(${ref}):${product.manifest.message}` };
|
|
146
|
+
}
|
|
147
|
+
const manifest = product.manifest.manifest;
|
|
148
|
+
if (manifest.id.startsWith('core:')) {
|
|
149
|
+
rollbackInstall(deps, product.installPath);
|
|
150
|
+
return { ok: false, message: `清单 id "${manifest.id}" 带官方前缀——core: 为官方插件保留(03 §1.2),装机拒` };
|
|
151
|
+
}
|
|
152
|
+
if (ledgerRead.entries.some((e) => e.id === manifest.id && e.id !== opts.replacingId)) {
|
|
153
|
+
rollbackInstall(deps, product.installPath);
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
message: `插件 ${manifest.id} 已装机(源 ${ledgerRead.entries.find((e) => e.id === manifest.id).source})——换版本走 update,先卸走 uninstall(03 §5.4)`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// 收割(§5.4 词表账本——装机零生效唯一例外:仅模块求值读 events 导出)
|
|
160
|
+
let declaredEvents;
|
|
161
|
+
try {
|
|
162
|
+
declaredEvents = await harvestEvents(deps, resolveInstallPath(deps.dataDir, product.installPath), product.installPath, manifest);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
rollbackInstall(deps, product.installPath);
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
message: `收割失败(${ref})——装机回滚:${err instanceof Error ? err.message : String(err)}`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// 落账(条目数组形原子写——plugin-store 执法)
|
|
172
|
+
const now = deps.now ?? (() => new Date()); // Date 构造子裸调用返 string(JS 遗留)——显式闭包归一
|
|
173
|
+
const entry = {
|
|
174
|
+
id: manifest.id,
|
|
175
|
+
source: parsed.parsed.source,
|
|
176
|
+
ref,
|
|
177
|
+
version: manifest.version ?? product.version,
|
|
178
|
+
...(product.integrity !== undefined ? { integrity: product.integrity } : {}),
|
|
179
|
+
...(product.commit !== undefined ? { commit: product.commit } : {}),
|
|
180
|
+
installedAt: now().toISOString(),
|
|
181
|
+
installPath: product.installPath,
|
|
182
|
+
declaredEvents,
|
|
183
|
+
};
|
|
184
|
+
upsertLedgerEntry(deps.dataDir, entry, deps.fs);
|
|
185
|
+
// 生命周期归因账(05 §1.1):装机成功事实落 audit_events——词形
|
|
186
|
+
// {id, source, version};version 位缺席不落键(git/local 源清单可能无
|
|
187
|
+
// 版本位)。replacingId 场景(update npm 重装腿)已在调用侧剥 sink,
|
|
188
|
+
// 到达此处的恒为首次装机
|
|
189
|
+
deps.onLifecycleAudit?.('plugin/installed', {
|
|
190
|
+
id: manifest.id,
|
|
191
|
+
source: parsed.parsed.source,
|
|
192
|
+
...(entry.version !== undefined ? { version: entry.version } : {}),
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
ok: true,
|
|
196
|
+
entry,
|
|
197
|
+
text: `已装机:${manifest.id}(源 ${parsed.parsed.source},${product.installPath})——装机零生效,启用走 mount(下次启动装载生效)`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** 装机物回滚(清单/收割失败腿——local 源不删〔直引非装机物〕) */
|
|
201
|
+
function rollbackInstall(deps, installPath) {
|
|
202
|
+
if (installPath === '' || installPath.startsWith('/'))
|
|
203
|
+
return; // local 绝对表示 = 用户源不删
|
|
204
|
+
try {
|
|
205
|
+
deps.fs.rm(resolveInstallPath(deps.dataDir, installPath), { recursive: true, force: true });
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// 回滚失败不吞装机油——主错误照呈(残影目录下次 install 前置 rm 收敛)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/* ---------------- npm 源执行器 ---------------- */
|
|
212
|
+
/**
|
|
213
|
+
* npm 源执行(§5.4 四件套执法)。锚 package.json 缺席即写最小锚(防 npm
|
|
214
|
+
* 向上爬父目录污染用户文件);`--prefix` 装进装机树;integrity/version 从
|
|
215
|
+
* `.package-lock.json` 收割(tarball 凭证真源)。
|
|
216
|
+
*/
|
|
217
|
+
async function runNpmInstall(deps, parsed) {
|
|
218
|
+
const pluginsDir = join(deps.dataDir, 'plugins');
|
|
219
|
+
deps.fs.mkdir(pluginsDir, { recursive: true });
|
|
220
|
+
// 锚 package.json(缺席才写——在场保用户/前次装机产物,不覆盖)
|
|
221
|
+
if (deps.fs.read(join(pluginsDir, 'package.json')) === null) {
|
|
222
|
+
deps.fs.write(join(pluginsDir, 'package.json'), `${JSON.stringify({ name: 'berry-agent-plugin-tree', private: true }, null, 2)}\n`);
|
|
223
|
+
}
|
|
224
|
+
const argv = [
|
|
225
|
+
'install',
|
|
226
|
+
'--prefix',
|
|
227
|
+
pluginsDir,
|
|
228
|
+
'--save-exact', // ① 直接依赖钉版(依赖记录 exact 无 ranges)
|
|
229
|
+
'--omit=dev', // ② devDependencies 不装
|
|
230
|
+
'--omit=peer',
|
|
231
|
+
'--legacy-peer-deps',
|
|
232
|
+
'--ignore-scripts', // ④ 安装期脚本禁跑(白名单开面归点火日)
|
|
233
|
+
...buildMinReleaseAgeFlags(deps),
|
|
234
|
+
parsed.version !== undefined ? `${parsed.pkg}@${parsed.version}` : parsed.pkg,
|
|
235
|
+
];
|
|
236
|
+
try {
|
|
237
|
+
await deps.spawn.run('npm', argv, {});
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
throw new BaseError('PLUGIN_INSTALL_FAILED', npmFailureMessage(err, buildMinReleaseAgeFlags(deps).join(' ')), {
|
|
241
|
+
cause: err,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
// 凭证收割:.package-lock.json 的 packages[<装机目录>].integrity + version
|
|
245
|
+
const installPath = installPathForNpm(parsed.pkg);
|
|
246
|
+
const lockText = deps.fs.read(join(pluginsDir, '.package-lock.json'));
|
|
247
|
+
const lock = lockText === null
|
|
248
|
+
? undefined
|
|
249
|
+
: JSON.parse(lockText);
|
|
250
|
+
const lockEntry = lock?.packages?.[`node_modules/${parsed.pkg}`];
|
|
251
|
+
return {
|
|
252
|
+
installPath,
|
|
253
|
+
manifest: readManifestAt(deps, installPath),
|
|
254
|
+
version: lockEntry?.version,
|
|
255
|
+
integrity: lockEntry?.integrity,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/** min-release-age 旗标族(0 = 显式关窗 → 不传旗标——npm 无「关闭」值形) */
|
|
259
|
+
function buildMinReleaseAgeFlags(deps) {
|
|
260
|
+
const minutes = resolveMinReleaseAge({
|
|
261
|
+
...(deps.env !== undefined ? { env: deps.env } : {}),
|
|
262
|
+
...(deps.minReleaseAgeOverride !== undefined ? { cliFlag: deps.minReleaseAgeOverride } : {}),
|
|
263
|
+
});
|
|
264
|
+
return minutes <= 0 ? [] : ['--min-release-age', String(minutes)];
|
|
265
|
+
}
|
|
266
|
+
/** npm 失败增补指路(--min-release-age 旧版不识 → 指路升级) */
|
|
267
|
+
function npmFailureMessage(err, flagText) {
|
|
268
|
+
const stderr = err instanceof Error ? (err.stderr ?? '') : '';
|
|
269
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
270
|
+
const ageHint = stderr.includes('min-release-age') || stderr.includes('Unknown cli flag') || stderr.includes('unknown option')
|
|
271
|
+
? `——npm 不识 ${flagText}:--min-release-age 须 npm ≥11.5(npm -v 自查),或设 ${MIN_RELEASE_AGE_ENV}=0 关窗`
|
|
272
|
+
: '';
|
|
273
|
+
return `npm install 失败:${message}${ageHint}`;
|
|
274
|
+
}
|
|
275
|
+
/* ---------------- git 源执行器 ---------------- */
|
|
276
|
+
/**
|
|
277
|
+
* git 源执行:tmp 克隆(默认 branch)→ checkout gitRef(detach——branch/
|
|
278
|
+
* tag/commit 皆可)→ HEAD commit 收割 → 落位 `plugins/git/<host>/<首段>/
|
|
279
|
+
* <repo>/`(目标在场先 rm——幂等重装/撞名目录收敛)。克隆浅化不启用
|
|
280
|
+
* (checkout 任意历史 commit 需全史——--depth 与 commit ref 冲突)。
|
|
281
|
+
*/
|
|
282
|
+
async function runGitInstall(deps, parsed) {
|
|
283
|
+
const tmp = mkdtempSync(join(deps.tmpRoot ?? tmpdir(), 'berry-git-install-'));
|
|
284
|
+
try {
|
|
285
|
+
await deps.spawn.run('git', ['clone', parsed.url, tmp], {});
|
|
286
|
+
if (parsed.gitRef !== undefined) {
|
|
287
|
+
await deps.spawn.run('git', ['-C', tmp, 'checkout', '--detach', parsed.gitRef], {});
|
|
288
|
+
}
|
|
289
|
+
const head = await deps.spawn.run('git', ['-C', tmp, 'rev-parse', 'HEAD'], {});
|
|
290
|
+
const commit = head.stdout.trim();
|
|
291
|
+
const installPath = installPathForGit(parsed.url); // 坏 url 抛 PLUGIN_INSTALL_FAILED——ref 前置已校,保底
|
|
292
|
+
const target = resolveInstallPath(deps.dataDir, installPath);
|
|
293
|
+
deps.fs.mkdir(parentDir(target), { recursive: true });
|
|
294
|
+
deps.fs.rm(target, { recursive: true, force: true }); // 幂等(在场旧树先清)
|
|
295
|
+
deps.fs.rename(tmp, target);
|
|
296
|
+
return {
|
|
297
|
+
installPath,
|
|
298
|
+
manifest: readManifestAt(deps, installPath),
|
|
299
|
+
commit,
|
|
300
|
+
version: undefined,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
// rename 成功后 tmp 已不存在——force rm 幂等收尾
|
|
305
|
+
deps.fs.rm(tmp, { recursive: true, force: true });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/* ---------------- local 源执行器 ---------------- */
|
|
309
|
+
/** local 源执行:直引不拷贝(installPath = canonical 绝对;§5.4 表示法) */
|
|
310
|
+
function runLocalInstall(deps, parsed) {
|
|
311
|
+
let installPath;
|
|
312
|
+
try {
|
|
313
|
+
installPath = installPathForLocal(parsed.path);
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
throw new BaseError('PLUGIN_INSTALL_FAILED', `local 路径坏形(${parsed.path}):${err instanceof Error ? err.message : String(err)}`, {
|
|
317
|
+
cause: err,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return { installPath, manifest: readManifestAt(deps, installPath) };
|
|
321
|
+
}
|
|
322
|
+
/* ---------------- 清单读取(三源共用) ---------------- */
|
|
323
|
+
/** 装机目录读 package.json 过清单校验(§1.2 拒绝式——官方位 false) */
|
|
324
|
+
function readManifestAt(deps, installPath) {
|
|
325
|
+
const pluginDir = resolveInstallPath(deps.dataDir, installPath);
|
|
326
|
+
const text = deps.fs.read(join(pluginDir, 'package.json'));
|
|
327
|
+
if (text === null) {
|
|
328
|
+
return {
|
|
329
|
+
ok: false,
|
|
330
|
+
code: 'PLUGIN_SHAPE_INVALID',
|
|
331
|
+
message: `装机目录无 package.json(${pluginDir})——三源执行产物异常`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
return parseManifest(JSON.parse(text));
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
code: 'PLUGIN_SHAPE_INVALID',
|
|
341
|
+
message: `package.json 坏 JSON(${pluginDir}):${err instanceof Error ? err.message : String(err)}`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/* ---------------- 收割(§5.4 词表账本) ---------------- */
|
|
346
|
+
/**
|
|
347
|
+
* install 收割:jiti 求值入口模块读 `events` 导出(§1.3——装机零生效唯一
|
|
348
|
+
* 例外)。与装载器同形:虚拟面四键直注(顶层 import 'berry-agent' 不炸)+
|
|
349
|
+
* import 门禁 transform(03 §3.3 同律——装机时点不放宽)+ fsCache 关(门
|
|
350
|
+
* 禁身份不入缓存键)。纯声明包(declared-payload)零码收割。非字符串数组
|
|
351
|
+
* events 导出 = 收割失败(词表账本容不得坏形)。
|
|
352
|
+
*/
|
|
353
|
+
async function harvestEvents(deps, pluginDir, installPath, manifest) {
|
|
354
|
+
if (manifest.entryPlan.kind === 'declared-payload')
|
|
355
|
+
return []; // 纯声明包零码
|
|
356
|
+
const entryFile = manifest.entryPlan.kind === 'entry-file' ? manifest.entryPlan.entry : resolvePkgMain(pluginDir);
|
|
357
|
+
const entryId = join(pluginDir, entryFile);
|
|
358
|
+
const jiti = deps.jitiFactory?.(pluginDir, manifest.id) ??
|
|
359
|
+
createJiti(join(pluginDir, 'package.json'), {
|
|
360
|
+
transform: createGateTransform({ pluginId: manifest.id, pluginDir }),
|
|
361
|
+
interopDefault: true,
|
|
362
|
+
moduleCache: true,
|
|
363
|
+
fsCache: false, // 与装载器同律(门禁身份不入缓存键)
|
|
364
|
+
virtualModules: await loadDefaultVirtualFaces(),
|
|
365
|
+
});
|
|
366
|
+
let namespace;
|
|
367
|
+
try {
|
|
368
|
+
namespace = (await jiti.import(entryId));
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
throw new BaseError('PLUGIN_INSTALL_FAILED', `收割求值入口模块失败(${installPath}/${entryFile}):${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
372
|
+
}
|
|
373
|
+
const events = namespace['events'];
|
|
374
|
+
if (events === undefined)
|
|
375
|
+
return [];
|
|
376
|
+
if (!Array.isArray(events) || events.some((e) => typeof e !== 'string' || e.length === 0)) {
|
|
377
|
+
throw new BaseError('PLUGIN_SHAPE_INVALID', `入口 events 导出须字符串数组(插件 ${manifest.id})——词表账本容不得坏形`);
|
|
378
|
+
}
|
|
379
|
+
return events;
|
|
380
|
+
}
|
|
381
|
+
/* ---------------- update 分派(§5.4 按源) ---------------- */
|
|
382
|
+
/**
|
|
383
|
+
* update 编舞(§5.4 按源分派):npm 重装(拉最新满足窗龄版——`--save-exact`
|
|
384
|
+
* 锚树 + 无版本 spec = latest 受 min-release-age 窗检;新 integrity/version
|
|
385
|
+
* 落账);git 删目录按原 ref 重克隆(新 commit);local no-op(源动了下次
|
|
386
|
+
* 装载即新——§5.4 既有条款)。enabled 行不动(装机物换血、行引用 id 不变)。
|
|
387
|
+
*/
|
|
388
|
+
export async function updatePlugin(deps, id) {
|
|
389
|
+
const ledgerRead = readLedger(deps.dataDir, deps.fs);
|
|
390
|
+
if (!ledgerRead.ok) {
|
|
391
|
+
return { ok: false, message: `装机账本损坏:${ledgerRead.reason}——拒写防覆盖(03 §5.4)` };
|
|
392
|
+
}
|
|
393
|
+
const current = ledgerRead.entries.find((e) => e.id === id);
|
|
394
|
+
if (current === undefined) {
|
|
395
|
+
return { ok: false, message: `插件 ${id} 未装机——无可更新(装机清单见 plugins list)` };
|
|
396
|
+
}
|
|
397
|
+
if (current.source === 'local') {
|
|
398
|
+
return { ok: true, entry: current, text: `local 源直引不拷贝——源目录变更下次装载即生效(03 §5.4 no-op 分派)` };
|
|
399
|
+
}
|
|
400
|
+
// local 之外的 ref 重解析(账本 ref 与 CLI 同词法——单源往返)
|
|
401
|
+
const parsed = parsePluginRef(current.ref);
|
|
402
|
+
if (!parsed.ok) {
|
|
403
|
+
return { ok: false, message: `账本 ref 坏形(${current.ref}):${parsed.message}——重装可重建` };
|
|
404
|
+
}
|
|
405
|
+
if (parsed.parsed.source === 'local') {
|
|
406
|
+
// 病理态保底(local 已在上方 no-op 返回——账本 source 与 ref 前缀不一致)
|
|
407
|
+
return { ok: false, message: `账本 source 与 ref 前缀不一致(${current.ref})——重装可重建` };
|
|
408
|
+
}
|
|
409
|
+
if (parsed.parsed.source === 'npm') {
|
|
410
|
+
// 重装:旧装机物先清(spec 无版本 = 拉最新满足窗龄;upsert 后见胜出)。
|
|
411
|
+
// 换装豁免位传被替换 id——旧条目恰是替换目标不构成撞名(缺此豁免则
|
|
412
|
+
// installPlugin 撞名检查恒拒自家重装腿)。sink 剥离后再传——防
|
|
413
|
+
// installPlugin 成功尾落错词 installed(本腿语义是 updated)
|
|
414
|
+
const { onLifecycleAudit, ...rest } = deps;
|
|
415
|
+
const installPath = installPathForNpm(parsed.parsed.pkg);
|
|
416
|
+
rest.fs.rm(resolveInstallPath(rest.dataDir, installPath), { recursive: true, force: true });
|
|
417
|
+
const outcome = await installPlugin(rest, current.ref, { replacingId: id });
|
|
418
|
+
if (outcome.ok && onLifecycleAudit !== undefined) {
|
|
419
|
+
// 换装成功事实(05 §1.1):from 缺席容许(旧账本无版本位);npm 腿
|
|
420
|
+
// version 恒在(lock 收割),to 缺席不设防(兜底与 git 腿同形)
|
|
421
|
+
onLifecycleAudit('plugin/updated', {
|
|
422
|
+
id,
|
|
423
|
+
...(current.version !== undefined ? { from: current.version } : {}),
|
|
424
|
+
...(outcome.entry.version !== undefined ? { to: outcome.entry.version } : {}),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return outcome;
|
|
428
|
+
}
|
|
429
|
+
// git 重克隆(runGitInstall 目标在场先 rm——幂等腿复用)
|
|
430
|
+
let product;
|
|
431
|
+
try {
|
|
432
|
+
product = await runGitInstall(deps, parsed.parsed);
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
return { ok: false, message: err instanceof Error ? err.message : String(err) };
|
|
436
|
+
}
|
|
437
|
+
if (!product.manifest.ok) {
|
|
438
|
+
rollbackInstall(deps, product.installPath);
|
|
439
|
+
return { ok: false, message: `更新后清单校验失败(${id}):${product.manifest.message}` };
|
|
440
|
+
}
|
|
441
|
+
const manifest = product.manifest.manifest;
|
|
442
|
+
if (manifest.id !== id) {
|
|
443
|
+
rollbackInstall(deps, product.installPath);
|
|
444
|
+
return { ok: false, message: `更新后清单 id 变更(${id} → ${manifest.id})——装机身份漂移拒(卸后重装走两步)` };
|
|
445
|
+
}
|
|
446
|
+
let declaredEvents;
|
|
447
|
+
try {
|
|
448
|
+
declaredEvents = await harvestEvents(deps, resolveInstallPath(deps.dataDir, product.installPath), product.installPath, manifest);
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
rollbackInstall(deps, product.installPath);
|
|
452
|
+
return {
|
|
453
|
+
ok: false,
|
|
454
|
+
message: `更新收割失败(${id})——装机回滚:${err instanceof Error ? err.message : String(err)}`,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const entry = {
|
|
458
|
+
...current,
|
|
459
|
+
commit: product.commit,
|
|
460
|
+
installedAt: (deps.now ?? (() => new Date()))().toISOString(),
|
|
461
|
+
installPath: product.installPath,
|
|
462
|
+
declaredEvents,
|
|
463
|
+
};
|
|
464
|
+
upsertLedgerEntry(deps.dataDir, entry, deps.fs);
|
|
465
|
+
// 换装成功事实(05 §1.1):git 溢词形 {id, from?, to}——但 git 源
|
|
466
|
+
// entry.version 继承旧账恒不变(变更实质在 commit 位),落 version 会造
|
|
467
|
+
// 「版本换血」误读;from/to 落 commit 值(git 源的版本标识即 commit——
|
|
468
|
+
// 03 §5.4 精确锁定语义;npm 腿同键落 semver,源别可回 installed 词
|
|
469
|
+
// source 位对拍)。旧 commit 缺席(早期账本)from 不落
|
|
470
|
+
deps.onLifecycleAudit?.('plugin/updated', {
|
|
471
|
+
id,
|
|
472
|
+
...(current.commit !== undefined ? { from: current.commit } : {}),
|
|
473
|
+
...(product.commit !== undefined ? { to: product.commit } : {}),
|
|
474
|
+
});
|
|
475
|
+
return {
|
|
476
|
+
ok: true,
|
|
477
|
+
entry,
|
|
478
|
+
text: `已更新:${id}(源 git,commit ${product.commit?.slice(0, 7) ?? '?'})——下次启动装载生效`,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host/plugin-reload — /reload 排队编舞件(03 §5.7——热重载的编舞与失败三档)。
|
|
3
|
+
*
|
|
4
|
+
* 职责 = 把「重跑装载」变成一条**可排队、可串行、失败分档**的编舞:
|
|
5
|
+
* - **busy 判据** = 任一 run 在飞(03 §5.7「会话运行中」——单对话本体);
|
|
6
|
+
* busy 期 /reload 进 **pending 单槽**——连发并槽 coalesce(reload 读盘为
|
|
7
|
+
* 准、无请求参数,「最后态」即当前盘面,单槽布尔即完备表达);
|
|
8
|
+
* - **run 收场即执行**:onRunSettled 订阅驱动终态边界(04 §2 三值——settled
|
|
9
|
+
* 即非 busy 判定点),pending 在场且此刻不 busy → 出槽入链;
|
|
10
|
+
* - **失败三档**(由轻到重,03 §5.7):
|
|
11
|
+
* ① 启用清单校验失败 = **拒换**——preflight 前置于一切回卷(旧装载态
|
|
12
|
+
* 原封不动继续跑,报告行级错误与修复指引);
|
|
13
|
+
* ② 行级装载失败 = **换入 + 报告**——bootPlugins 行级隔离降级内置
|
|
14
|
+
* (boot-failures.json 点名 + warn 横幅 + failed 面汇总),本件只回执
|
|
15
|
+
* 汇总计数,不重复执法;
|
|
16
|
+
* ③ disposer 回卷失败 = **降级存活**——回卷抛错不阻断换入(宿主进程
|
|
17
|
+
* 不死),聚合报告回卷失败清单,旧代残迹随下一轮全量重载收口;
|
|
18
|
+
* - **reloadChain 串行**:in-flight promise 链——并发 /reload 恒串行不
|
|
19
|
+
* 交错(链尾吞错自愈,后续请求不受前次失败连坐)。
|
|
20
|
+
*
|
|
21
|
+
* 换入语义边界(本件不执法、由闭包真身承载):reapply 成功 = 装配根重跑
|
|
22
|
+
* bootPlugins 并把新代句柄写回闭包晚绑定位(boot 取值器/换代槽/skills 层
|
|
23
|
+
* 重同步随 reapply 闭包内完成——assembly 接线位见彼处注释)。
|
|
24
|
+
*
|
|
25
|
+
* 救援环律(07 §5 --no-plugins 条款):「/reload 读盘不受旗标影响」——
|
|
26
|
+
* reapply 闭包在装配侧以 noPlugins: false 形构造(旗标只管启动期短路,
|
|
27
|
+
* 人面显式 reload 即显式装载请求),本件零旗标知识。
|
|
28
|
+
*/
|
|
29
|
+
/** 编舞注入面(全部闭包真身——本件零 db/fs/驱动知识,词面独立律) */
|
|
30
|
+
import { formatPluginFailureText } from './boot-failures.js';
|
|
31
|
+
/**
|
|
32
|
+
* 组装 /reload 编舞器。订阅生命周期:onRunSettled 订阅随编舞器终身(进程
|
|
33
|
+
* 级单例——TUI 入口构造一次;不设 dispose 面,进程退出即回收)。
|
|
34
|
+
*/
|
|
35
|
+
export function createPluginReloader(options) {
|
|
36
|
+
const warn = options.warn ?? ((message) => process.stderr.write(`${message}\n`));
|
|
37
|
+
/** pending 单槽(coalesce 判据位——true = busy 期有请求待执行) */
|
|
38
|
+
let pending = false;
|
|
39
|
+
/** reloadChain(in-flight promise 链尾——串行执法位) */
|
|
40
|
+
let chain = Promise.resolve();
|
|
41
|
+
/** 单次重载编舞(失败三档执法序:①preflight → ③rollback → ②reapply) */
|
|
42
|
+
const runOnce = async () => {
|
|
43
|
+
// ① 清单校验失败 = 拒换——旧装载态原封不动(回卷零调用)
|
|
44
|
+
try {
|
|
45
|
+
options.preflight();
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
options.report(`重载已拒(启用清单校验失败——旧装载态继续运行):${err instanceof Error ? err.message : String(err)}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// ③ 回卷旧代——失败降级存活:聚合报告后继续换入(残迹随下轮重载收口)
|
|
52
|
+
let rollbackFailedIds = [];
|
|
53
|
+
try {
|
|
54
|
+
const receipt = await options.rollback();
|
|
55
|
+
rollbackFailedIds = receipt.failed;
|
|
56
|
+
if (receipt.failed.length > 0) {
|
|
57
|
+
warn(`插件回卷部分失败(${receipt.failed.join('、')})——残迹随下一轮重载收口`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
warn(`插件回卷失败(降级存活,继续换入):${err instanceof Error ? err.message : String(err)}`);
|
|
62
|
+
rollbackFailedIds = ['<rollback>'];
|
|
63
|
+
}
|
|
64
|
+
// ② 换入新代——行级失败已由装载序内置隔离(boot-failures 记账 + 横幅),
|
|
65
|
+
// 本层只回执汇总;抛错(core: fail-loud 等罕见尾路径)= 降级存活报告
|
|
66
|
+
try {
|
|
67
|
+
const receipt = await options.reapply();
|
|
68
|
+
const lines = [`插件已重载:启用 ${receipt.enabled}/${receipt.total}`];
|
|
69
|
+
if (receipt.addedTools.length > 0) {
|
|
70
|
+
// 新代工具面 diff 呈现(03 §2.8——新增工具名逐插件;无新增不加行不造噪声)
|
|
71
|
+
lines.push(`新增工具面:${receipt.addedTools.map((a) => `${a.pluginId} → ${a.tools.join('、')}`).join(';')}`);
|
|
72
|
+
}
|
|
73
|
+
if (receipt.failures.length > 0) {
|
|
74
|
+
// 行级失败附错误文本(03 §5.7② obs-a 呈现三面之③——与 plugins list
|
|
75
|
+
// 失败分区同形行;boot-failures.json 已由装载序记账)
|
|
76
|
+
lines.push(`行级失败(隔离降级,已记 boot-failures):\n${receipt.failures.map((f) => ` ${f.id} ${formatPluginFailureText(f)}`).join('\n')}`);
|
|
77
|
+
}
|
|
78
|
+
if (rollbackFailedIds.length > 0) {
|
|
79
|
+
lines.push(`上代回卷残留(下轮重载收口):${rollbackFailedIds.join('、')}`);
|
|
80
|
+
}
|
|
81
|
+
options.report(lines.join('\n'));
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
options.report(`重载失败(旧装载面已回卷、新代未立):${err instanceof Error ? err.message : String(err)}——修复后可再 /reload;仍失败可用 --no-plugins 启动排查`);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
/** 入链(串行执法——链尾吞错自愈,后续请求不受前次失败连坐) */
|
|
88
|
+
const enqueue = () => {
|
|
89
|
+
chain = chain.then(runOnce).catch((err) => {
|
|
90
|
+
// runOnce 内部已分档吞错,此处是防御性兜底(编舞自身异常不反噬链)
|
|
91
|
+
warn(`/reload 编舞异常:${err instanceof Error ? err.message : String(err)}`);
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
// run 收场即执行:pending 在场且此刻不 busy → 出槽入链(settled 边界后
|
|
95
|
+
// 仍 busy = 多会话交错——保持排队等下一个终态边界)
|
|
96
|
+
options.onRunSettled(() => {
|
|
97
|
+
if (pending && !options.isBusy()) {
|
|
98
|
+
pending = false;
|
|
99
|
+
enqueue();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
request: () => {
|
|
104
|
+
if (options.isBusy()) {
|
|
105
|
+
// busy 期并槽 coalesce——连发只跑最后态(读盘为准,单槽即完备)
|
|
106
|
+
if (!pending)
|
|
107
|
+
options.report('会话运行中——/reload 已排队,本轮收场后执行');
|
|
108
|
+
pending = true;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
enqueue();
|
|
112
|
+
},
|
|
113
|
+
hasPending: () => pending,
|
|
114
|
+
settle: () => chain,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** RollbackReceipt 构造速记(rollback 闭包真身侧使用——空代 no-op 形) */
|
|
118
|
+
export function emptyRollbackReceipt() {
|
|
119
|
+
return { disposed: [], failed: [] };
|
|
120
|
+
}
|
|
121
|
+
/** LoadReport.unload 回执 → RollbackReceipt 适配(failed 对象面 → id 清单——呈现形) */
|
|
122
|
+
export function rollbackFromReport(receipt) {
|
|
123
|
+
return { disposed: receipt.disposed, failed: receipt.failed.map((f) => f.id) };
|
|
124
|
+
}
|