@armadra/agent 0.4.0 → 0.5.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/CHANGELOG.md +184 -1
- package/README.md +202 -83
- package/THIRD_PARTY_NOTICES.md +34 -0
- package/dist/acp.d.ts +18 -0
- package/dist/acp.js +19 -0
- package/dist/agent/limits.d.ts +38 -0
- package/dist/agent/limits.js +127 -0
- package/dist/agent/loop-guard.d.ts +38 -0
- package/dist/agent/loop-guard.js +95 -0
- package/dist/agent/loop.d.ts +4 -0
- package/dist/agent/loop.js +7 -0
- package/dist/agent/prompt-rules.d.ts +12 -0
- package/dist/agent/prompt-rules.js +18 -0
- package/dist/agent/reminders.d.ts +59 -0
- package/dist/agent/reminders.js +327 -0
- package/dist/agent/session-cache.d.ts +5 -0
- package/dist/agent/session-cache.js +17 -1
- package/dist/agent/session-classifier.d.ts +1 -1
- package/dist/agent/session-classifier.js +3 -1
- package/dist/agent/session-compaction.d.ts +35 -6
- package/dist/agent/session-compaction.js +157 -31
- package/dist/agent/session-core.d.ts +27 -1
- package/dist/agent/session-extensions.d.ts +61 -0
- package/dist/agent/session-extensions.js +129 -0
- package/dist/agent/session-images.d.ts +23 -0
- package/dist/agent/session-images.js +97 -0
- package/dist/agent/session-plan.d.ts +21 -0
- package/dist/agent/session-plan.js +524 -0
- package/dist/agent/session-rewind.d.ts +81 -0
- package/dist/agent/session-rewind.js +354 -0
- package/dist/agent/session-run.d.ts +28 -1
- package/dist/agent/session-run.js +157 -6
- package/dist/agent/session-settings.d.ts +49 -0
- package/dist/agent/session-settings.js +95 -0
- package/dist/agent/session-subagent.d.ts +36 -24
- package/dist/agent/session-subagent.js +259 -129
- package/dist/agent/session-sync.d.ts +10 -3
- package/dist/agent/session-sync.js +37 -8
- package/dist/agent/session-telemetry.d.ts +39 -0
- package/dist/agent/session-telemetry.js +231 -0
- package/dist/agent/session-tools.js +40 -1
- package/dist/agent/session.d.ts +30 -29
- package/dist/agent/session.js +79 -123
- package/dist/agent/subagent-registry.d.ts +91 -0
- package/dist/agent/subagent-registry.js +494 -0
- package/dist/agent/system-prompt.d.ts +6 -3
- package/dist/agent/system-prompt.js +7 -2
- package/dist/agent/tool-runner.d.ts +8 -1
- package/dist/agent/tool-runner.js +57 -4
- package/dist/agent/types-w5.d.ts +192 -0
- package/dist/agent/types-w5.js +10 -0
- package/dist/agent/types.d.ts +47 -4
- package/dist/agent/types.js +2 -0
- package/dist/agent/worktree.d.ts +36 -0
- package/dist/agent/worktree.js +86 -0
- package/dist/agents/builtin.d.ts +14 -0
- package/dist/agents/builtin.js +39 -0
- package/dist/agents/catalog.d.ts +43 -0
- package/dist/agents/catalog.js +82 -0
- package/dist/agents/discover.d.ts +35 -0
- package/dist/agents/discover.js +83 -0
- package/dist/agents/external.d.ts +82 -0
- package/dist/agents/external.js +326 -0
- package/dist/agents/parse.d.ts +18 -0
- package/dist/agents/parse.js +116 -0
- package/dist/agents/result.d.ts +32 -0
- package/dist/agents/result.js +67 -0
- package/dist/agents/task-control.d.ts +17 -0
- package/dist/agents/task-control.js +17 -0
- package/dist/agents/task-record.d.ts +78 -0
- package/dist/agents/task-record.js +141 -0
- package/dist/agents/types.d.ts +46 -0
- package/dist/agents/types.js +8 -0
- package/dist/ai/apis/anthropic-compat.d.ts +36 -0
- package/dist/ai/apis/anthropic-compat.js +92 -0
- package/dist/ai/apis/anthropic-request.d.ts +7 -6
- package/dist/ai/apis/anthropic-request.js +10 -28
- package/dist/ai/apis/cache-params.d.ts +11 -2
- package/dist/ai/apis/cache-params.js +25 -6
- package/dist/ai/apis/openai-compat.d.ts +4 -1
- package/dist/ai/apis/openai-compat.js +8 -1
- package/dist/ai/image-limits.d.ts +45 -0
- package/dist/ai/image-limits.js +68 -0
- package/dist/ai/providers/builtin.d.ts +18 -6
- package/dist/ai/providers/builtin.js +135 -6
- package/dist/ai/providers/catalog-data.js +17 -13
- package/dist/ai/providers/catalog.d.ts +60 -7
- package/dist/ai/providers/catalog.js +197 -38
- package/dist/ai/providers/channels.d.ts +33 -3
- package/dist/ai/providers/channels.js +108 -7
- package/dist/ai/providers/enrich.d.ts +6 -3
- package/dist/ai/providers/enrich.js +19 -4
- package/dist/ai/providers/models-dev-cache.d.ts +28 -24
- package/dist/ai/providers/models-dev-cache.js +92 -78
- package/dist/ai/providers/models-dev-data.d.ts +7 -0
- package/dist/ai/providers/models-dev-data.js +32 -0
- package/dist/ai/providers/models-dev-snapshot.d.ts +50 -0
- package/dist/ai/providers/models-dev-snapshot.js +137 -0
- package/dist/ai/providers/models-dev.d.ts +51 -10
- package/dist/ai/providers/models-dev.js +132 -39
- package/dist/ai/providers/registry.d.ts +20 -10
- package/dist/ai/providers/registry.js +82 -43
- package/dist/ai/types.d.ts +28 -2
- package/dist/ai/types.js +5 -0
- package/dist/bundle/ama.cjs +37605 -21619
- package/dist/checkpoints/backend.d.ts +61 -0
- package/dist/checkpoints/backend.js +135 -0
- package/dist/checkpoints/blobs.d.ts +60 -0
- package/dist/checkpoints/blobs.js +203 -0
- package/dist/checkpoints/gc.d.ts +47 -0
- package/dist/checkpoints/gc.js +144 -0
- package/dist/checkpoints/git-head.d.ts +15 -0
- package/dist/checkpoints/git-head.js +89 -0
- package/dist/checkpoints/index.d.ts +17 -0
- package/dist/checkpoints/index.js +17 -0
- package/dist/checkpoints/replay.d.ts +48 -0
- package/dist/checkpoints/replay.js +154 -0
- package/dist/checkpoints/restore.d.ts +86 -0
- package/dist/checkpoints/restore.js +263 -0
- package/dist/checkpoints/settings.d.ts +14 -0
- package/dist/checkpoints/settings.js +23 -0
- package/dist/checkpoints/shadow-git.d.ts +108 -0
- package/dist/checkpoints/shadow-git.js +491 -0
- package/dist/checkpoints/shadow-restore.d.ts +25 -0
- package/dist/checkpoints/shadow-restore.js +128 -0
- package/dist/checkpoints/tracker.d.ts +64 -0
- package/dist/checkpoints/tracker.js +192 -0
- package/dist/checkpoints/types.d.ts +99 -0
- package/dist/checkpoints/types.js +12 -0
- package/dist/cli/args.d.ts +8 -4
- package/dist/cli/args.js +17 -97
- package/dist/cli/bootstrap.js +13 -1
- package/dist/cli/choice-prompt.d.ts +58 -0
- package/dist/cli/choice-prompt.js +140 -0
- package/dist/cli/codemode-notice.d.ts +3 -2
- package/dist/cli/codemode-notice.js +3 -2
- package/dist/cli/compose-agents.d.ts +27 -0
- package/dist/cli/compose-agents.js +111 -0
- package/dist/cli/compose-extensions.d.ts +29 -0
- package/dist/cli/compose-extensions.js +44 -0
- package/dist/cli/compose-session.d.ts +3 -0
- package/dist/cli/compose-session.js +47 -4
- package/dist/cli/compose.d.ts +3 -0
- package/dist/cli/compose.js +33 -6
- package/dist/cli/deps.d.ts +6 -0
- package/dist/cli/exit-codes.d.ts +10 -0
- package/dist/cli/exit-codes.js +12 -0
- package/dist/cli/help-text.d.ts +4 -0
- package/dist/cli/help-text.js +102 -0
- package/dist/cli/startup-screen.js +5 -3
- package/dist/cli/startup-steps.js +7 -1
- package/dist/cli/subcommands/config.d.ts +4 -1
- package/dist/cli/subcommands/config.js +17 -6
- package/dist/cli/subcommands/doctor.js +21 -0
- package/dist/cli/subcommands/models-cache-probe.d.ts +1 -1
- package/dist/cli/subcommands/models-cache-probe.js +3 -12
- package/dist/cli/subcommands/models-discover.js +5 -6
- package/dist/cli/subcommands/models.d.ts +2 -0
- package/dist/cli/subcommands/models.js +33 -14
- package/dist/cli/subcommands/providers.d.ts +1 -1
- package/dist/cli/subcommands/providers.js +8 -15
- package/dist/cli/subcommands/sessions.d.ts +3 -1
- package/dist/cli/subcommands/sessions.js +36 -2
- package/dist/codemode/capability.d.ts +24 -7
- package/dist/codemode/capability.js +38 -10
- package/dist/codemode/host-side.d.ts +14 -0
- package/dist/codemode/host-side.js +24 -1
- package/dist/codemode/tool.d.ts +1 -1
- package/dist/codemode/tool.js +6 -4
- package/dist/compaction/breaker.d.ts +25 -9
- package/dist/compaction/breaker.js +47 -17
- package/dist/compaction/estimate.d.ts +11 -3
- package/dist/compaction/estimate.js +64 -22
- package/dist/compaction/image-budget.d.ts +45 -0
- package/dist/compaction/image-budget.js +115 -0
- package/dist/compaction/post-compact.d.ts +38 -0
- package/dist/compaction/post-compact.js +152 -0
- package/dist/compaction/protect.d.ts +27 -0
- package/dist/compaction/protect.js +56 -0
- package/dist/compaction/prune-tier.d.ts +58 -12
- package/dist/compaction/prune-tier.js +120 -54
- package/dist/compaction/summarize-tier.d.ts +23 -4
- package/dist/compaction/summarize-tier.js +100 -29
- package/dist/config/checker.d.ts +28 -0
- package/dist/config/checker.js +97 -0
- package/dist/config/json-schema.js +63 -2
- package/dist/config/key-docs.js +72 -3
- package/dist/config/merge.d.ts +5 -3
- package/dist/config/merge.js +81 -7
- package/dist/config/profile.d.ts +2 -0
- package/dist/config/profile.js +2 -0
- package/dist/config/schema-w5.d.ts +18 -0
- package/dist/config/schema-w5.js +109 -0
- package/dist/config/schema.d.ts +5 -9
- package/dist/config/schema.js +68 -114
- package/dist/config/types-w5.d.ts +118 -0
- package/dist/config/types-w5.js +20 -0
- package/dist/config/types.d.ts +50 -1
- package/dist/config/types.js +12 -0
- package/dist/drivers/acp/client.d.ts +57 -0
- package/dist/drivers/acp/client.js +160 -0
- package/dist/drivers/acp/driver.d.ts +31 -0
- package/dist/drivers/acp/driver.js +274 -0
- package/dist/drivers/acp/testing/fake-agent-main.d.ts +5 -0
- package/dist/drivers/acp/testing/fake-agent-main.js +7 -0
- package/dist/drivers/acp/testing/fake-agent.d.ts +27 -0
- package/dist/drivers/acp/testing/fake-agent.js +211 -0
- package/dist/drivers/acp/types.d.ts +285 -0
- package/dist/drivers/acp/types.js +36 -0
- package/dist/drivers/agents.d.ts +58 -0
- package/dist/drivers/agents.js +142 -0
- package/dist/drivers/base.d.ts +35 -0
- package/dist/drivers/base.js +59 -0
- package/dist/drivers/catalog.d.ts +46 -0
- package/dist/drivers/catalog.js +167 -0
- package/dist/drivers/env.d.ts +20 -0
- package/dist/drivers/env.js +46 -0
- package/dist/drivers/host-runners.d.ts +18 -0
- package/dist/drivers/host-runners.js +45 -0
- package/dist/drivers/jsonrpc.d.ts +58 -0
- package/dist/drivers/jsonrpc.js +180 -0
- package/dist/drivers/native/claude-normalize.d.ts +46 -0
- package/dist/drivers/native/claude-normalize.js +106 -0
- package/dist/drivers/native/claude-stream.d.ts +29 -0
- package/dist/drivers/native/claude-stream.js +456 -0
- package/dist/drivers/native/codex-app-server.d.ts +26 -0
- package/dist/drivers/native/codex-app-server.js +407 -0
- package/dist/drivers/native/codex-normalize.d.ts +38 -0
- package/dist/drivers/native/codex-normalize.js +124 -0
- package/dist/drivers/native/oneshot.d.ts +28 -0
- package/dist/drivers/native/oneshot.js +312 -0
- package/dist/drivers/permissions.d.ts +40 -0
- package/dist/drivers/permissions.js +92 -0
- package/dist/drivers/pids.d.ts +32 -0
- package/dist/drivers/pids.js +93 -0
- package/dist/drivers/pool.d.ts +28 -0
- package/dist/drivers/pool.js +96 -0
- package/dist/drivers/probe.d.ts +47 -0
- package/dist/drivers/probe.js +172 -0
- package/dist/drivers/process.d.ts +48 -0
- package/dist/drivers/process.js +100 -0
- package/dist/drivers/runner.d.ts +61 -0
- package/dist/drivers/runner.js +388 -0
- package/dist/drivers/store.d.ts +49 -0
- package/dist/drivers/store.js +91 -0
- package/dist/drivers/turn.d.ts +41 -0
- package/dist/drivers/turn.js +99 -0
- package/dist/drivers/types.d.ts +122 -0
- package/dist/drivers/types.js +11 -0
- package/dist/git/info.d.ts +75 -0
- package/dist/git/info.js +191 -0
- package/dist/hooks/protocol.js +6 -1
- package/dist/hooks/types.d.ts +10 -1
- package/dist/hooks/types.js +2 -0
- package/dist/host/api-impl.d.ts +7 -0
- package/dist/host/api-impl.js +15 -0
- package/dist/host/types.d.ts +24 -2
- package/dist/host/types.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/modes/acp/acp-events.d.ts +46 -0
- package/dist/modes/acp/acp-events.js +233 -0
- package/dist/modes/acp/acp-mode.d.ts +17 -0
- package/dist/modes/acp/acp-mode.js +47 -0
- package/dist/modes/acp/acp-server.d.ts +79 -0
- package/dist/modes/acp/acp-server.js +315 -0
- package/dist/modes/commands-core.d.ts +13 -1
- package/dist/modes/commands-core.js +64 -0
- package/dist/modes/image-input.d.ts +12 -4
- package/dist/modes/image-input.js +22 -5
- package/dist/modes/interactive/agent-panels.d.ts +14 -0
- package/dist/modes/interactive/agent-panels.js +103 -0
- package/dist/modes/interactive/agent-ui.d.ts +73 -0
- package/dist/modes/interactive/agent-ui.js +234 -0
- package/dist/modes/interactive/approval-dialog.d.ts +21 -4
- package/dist/modes/interactive/approval-dialog.js +98 -20
- package/dist/modes/interactive/approval-merge.d.ts +41 -0
- package/dist/modes/interactive/approval-merge.js +89 -0
- package/dist/modes/interactive/clipboard-paste.d.ts +18 -0
- package/dist/modes/interactive/clipboard-paste.js +28 -0
- package/dist/modes/interactive/commands.d.ts +12 -1
- package/dist/modes/interactive/commands.js +31 -1
- package/dist/modes/interactive/confirm-dialog.d.ts +49 -0
- package/dist/modes/interactive/confirm-dialog.js +121 -0
- package/dist/modes/interactive/double-esc.d.ts +20 -0
- package/dist/modes/interactive/double-esc.js +34 -0
- package/dist/modes/interactive/event-notices.d.ts +32 -0
- package/dist/modes/interactive/event-notices.js +79 -0
- package/dist/modes/interactive/external-editor.d.ts +16 -0
- package/dist/modes/interactive/external-editor.js +39 -0
- package/dist/modes/interactive/interactive-mode.d.ts +14 -1
- package/dist/modes/interactive/interactive-mode.js +140 -136
- package/dist/modes/interactive/key-dispatch.d.ts +22 -2
- package/dist/modes/interactive/key-dispatch.js +66 -7
- package/dist/modes/interactive/line/line-mode.d.ts +4 -1
- package/dist/modes/interactive/line/line-mode.js +31 -5
- package/dist/modes/interactive/line/line-render.d.ts +2 -1
- package/dist/modes/interactive/line/line-render.js +40 -4
- package/dist/modes/interactive/message-view.d.ts +1 -1
- package/dist/modes/interactive/message-view.js +24 -2
- package/dist/modes/interactive/panels.d.ts +18 -1
- package/dist/modes/interactive/panels.js +19 -5
- package/dist/modes/interactive/plan-command.d.ts +40 -0
- package/dist/modes/interactive/plan-command.js +129 -0
- package/dist/modes/interactive/plan-dialog.d.ts +98 -0
- package/dist/modes/interactive/plan-dialog.js +290 -0
- package/dist/modes/interactive/plan-flow.d.ts +34 -0
- package/dist/modes/interactive/plan-flow.js +98 -0
- package/dist/modes/interactive/rewind-command.d.ts +31 -0
- package/dist/modes/interactive/rewind-command.js +106 -0
- package/dist/modes/interactive/rewind-flow.d.ts +37 -0
- package/dist/modes/interactive/rewind-flow.js +151 -0
- package/dist/modes/interactive/rewind-list.d.ts +36 -0
- package/dist/modes/interactive/rewind-list.js +123 -0
- package/dist/modes/interactive/rewind-panel.d.ts +77 -0
- package/dist/modes/interactive/rewind-panel.js +309 -0
- package/dist/modes/interactive/rewind-text.d.ts +40 -0
- package/dist/modes/interactive/rewind-text.js +139 -0
- package/dist/modes/interactive/session-events.d.ts +24 -0
- package/dist/modes/interactive/session-events.js +96 -0
- package/dist/modes/interactive/startup-ui.js +5 -1
- package/dist/modes/interactive/status-area.d.ts +73 -0
- package/dist/modes/interactive/status-area.js +190 -0
- package/dist/modes/interactive/status-bar.d.ts +99 -11
- package/dist/modes/interactive/status-bar.js +273 -90
- package/dist/modes/interactive/status-line.d.ts +33 -0
- package/dist/modes/interactive/status-line.js +113 -0
- package/dist/modes/interactive/subagent-view.d.ts +56 -0
- package/dist/modes/interactive/subagent-view.js +154 -0
- package/dist/modes/interactive/tasks-report.d.ts +27 -0
- package/dist/modes/interactive/tasks-report.js +114 -0
- package/dist/modes/interactive/tool-view.d.ts +12 -2
- package/dist/modes/interactive/tool-view.js +34 -4
- package/dist/modes/print/print-mode.d.ts +13 -4
- package/dist/modes/print/print-mode.js +56 -12
- package/dist/modes/rpc/commands.d.ts +9 -0
- package/dist/modes/rpc/commands.js +44 -0
- package/dist/modes/rpc/rpc-mode.js +7 -1
- package/dist/modes/session-report.d.ts +11 -0
- package/dist/modes/session-report.js +48 -1
- package/dist/permissions/auto-safe.d.ts +5 -0
- package/dist/permissions/auto-safe.js +18 -0
- package/dist/permissions/bypass.d.ts +32 -0
- package/dist/permissions/bypass.js +36 -0
- package/dist/permissions/classifier.d.ts +4 -0
- package/dist/permissions/classifier.js +2 -0
- package/dist/permissions/modes.js +1 -1
- package/dist/permissions/pipeline.d.ts +43 -4
- package/dist/permissions/pipeline.js +114 -9
- package/dist/permissions/readonly-bash.d.ts +30 -0
- package/dist/permissions/readonly-bash.js +118 -0
- package/dist/permissions/types.d.ts +42 -0
- package/dist/plan/compose.d.ts +9 -0
- package/dist/plan/compose.js +19 -0
- package/dist/plan/controller.d.ts +71 -0
- package/dist/plan/controller.js +27 -0
- package/dist/plan/done-markers.d.ts +17 -0
- package/dist/plan/done-markers.js +51 -0
- package/dist/plan/extract.d.ts +29 -0
- package/dist/plan/extract.js +160 -0
- package/dist/plan/prompts.d.ts +36 -0
- package/dist/plan/prompts.js +89 -0
- package/dist/plan/store.d.ts +37 -0
- package/dist/plan/store.js +90 -0
- package/dist/rpc.d.ts +58 -2
- package/dist/rpc.js +7 -0
- package/dist/sandbox/bash.d.ts +74 -0
- package/dist/sandbox/bash.js +126 -0
- package/dist/sandbox/detect.d.ts +48 -0
- package/dist/sandbox/detect.js +149 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/profile.d.ts +33 -0
- package/dist/sandbox/profile.js +76 -0
- package/dist/sandbox/wrap.d.ts +29 -0
- package/dist/sandbox/wrap.js +117 -0
- package/dist/sdk.d.ts +33 -2
- package/dist/sdk.js +24 -2
- package/dist/session/types.d.ts +3 -1
- package/dist/session/types.js +1 -0
- package/dist/skills/builtin.d.ts +31 -0
- package/dist/skills/builtin.js +109 -0
- package/dist/skills/discover.d.ts +2 -1
- package/dist/skills/index-prompt.js +5 -8
- package/dist/tools/background-jobs.d.ts +71 -0
- package/dist/tools/background-jobs.js +198 -0
- package/dist/tools/bash.d.ts +53 -1
- package/dist/tools/bash.js +158 -7
- package/dist/tools/clipboard-image.d.ts +44 -0
- package/dist/tools/clipboard-image.js +148 -0
- package/dist/tools/edit.d.ts +2 -0
- package/dist/tools/edit.js +18 -3
- package/dist/tools/image-file.d.ts +38 -3
- package/dist/tools/image-file.js +81 -10
- package/dist/tools/image-resize.d.ts +58 -0
- package/dist/tools/image-resize.js +97 -0
- package/dist/tools/presets.d.ts +7 -2
- package/dist/tools/presets.js +11 -4
- package/dist/tools/read.d.ts +5 -2
- package/dist/tools/read.js +13 -10
- package/dist/tools/registry.d.ts +7 -1
- package/dist/tools/registry.js +27 -4
- package/dist/tools/task-ctl.d.ts +22 -0
- package/dist/tools/task-ctl.js +116 -0
- package/dist/tools/task.d.ts +22 -10
- package/dist/tools/task.js +66 -26
- package/dist/tools/todo.d.ts +22 -2
- package/dist/tools/todo.js +58 -14
- package/dist/tools/truncate.d.ts +15 -0
- package/dist/tools/truncate.js +25 -0
- package/dist/tools/types.d.ts +113 -0
- package/dist/tools/types.js +5 -0
- package/dist/tools/write.js +3 -0
- package/dist/tui/glyphs.d.ts +4 -0
- package/dist/tui/glyphs.js +4 -0
- package/dist/tui/keybindings.d.ts +6 -1
- package/dist/tui/keybindings.js +6 -1
- package/docs/acp.md +78 -0
- package/docs/agents.md +250 -0
- package/docs/codemode.md +12 -8
- package/docs/hooks.md +26 -22
- package/docs/permissions.md +78 -13
- package/docs/plan.md +107 -0
- package/docs/providers.md +228 -52
- package/docs/rewind-plan.md +181 -0
- package/docs/rpc.md +92 -5
- package/docs/sandbox.md +215 -0
- package/docs/session-format.md +41 -21
- package/docs/sessions.md +41 -1
- package/docs/tui-design.md +751 -0
- package/docs/tui.md +179 -21
- package/package.json +13 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 检查点配置解析(docs/rewind-plan.md §5)。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* `AMA_CHECKPOINTS`(tools | shadow-git | off)> config `checkpoints.mode` > 缺省 tools;
|
|
5
|
+
* 无效的环境变量值 warn 后忽略。`maxFileBytes` / `keep` 取配置,缺省 5 MiB / 100。
|
|
6
|
+
*/
|
|
7
|
+
import { type AmaConfig } from "../config/types.js";
|
|
8
|
+
import type { CheckpointMode } from "./types.js";
|
|
9
|
+
export interface CheckpointSettings {
|
|
10
|
+
mode: CheckpointMode;
|
|
11
|
+
maxFileBytes: number;
|
|
12
|
+
keep: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function resolveCheckpointSettings(config: Pick<AmaConfig, "checkpoints"> | undefined, env?: Readonly<Record<string, string | undefined>>, warn?: (message: string) => void): CheckpointSettings;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 检查点配置解析(docs/rewind-plan.md §5)。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* `AMA_CHECKPOINTS`(tools | shadow-git | off)> config `checkpoints.mode` > 缺省 tools;
|
|
5
|
+
* 无效的环境变量值 warn 后忽略。`maxFileBytes` / `keep` 取配置,缺省 5 MiB / 100。
|
|
6
|
+
*/
|
|
7
|
+
import { CHECKPOINT_MODES, DEFAULT_CHECKPOINTS_CONFIG } from "../config/types.js";
|
|
8
|
+
export function resolveCheckpointSettings(config, env = process.env, warn = () => { }) {
|
|
9
|
+
const section = config?.checkpoints ?? {};
|
|
10
|
+
let mode = section.mode ?? DEFAULT_CHECKPOINTS_CONFIG.mode;
|
|
11
|
+
const raw = env["AMA_CHECKPOINTS"]?.trim();
|
|
12
|
+
if (raw !== undefined && raw !== "") {
|
|
13
|
+
if (CHECKPOINT_MODES.includes(raw))
|
|
14
|
+
mode = raw;
|
|
15
|
+
else
|
|
16
|
+
warn(`AMA_CHECKPOINTS=${raw} 无效(${CHECKPOINT_MODES.join(" | ")}),已忽略`);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
mode,
|
|
20
|
+
maxFileBytes: section.maxFileBytes ?? DEFAULT_CHECKPOINTS_CONFIG.maxFileBytes,
|
|
21
|
+
keep: section.keep ?? DEFAULT_CHECKPOINTS_CONFIG.keep,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 影子 git 仓库(docs/rewind-plan.md §6)。[RW-D]
|
|
3
|
+
*
|
|
4
|
+
* - 位置 `<dataDir>/file-history/shadow/<sha256(cwd) 前 16>/`,用 `--git-dir` 指向它、`--work-tree` 指向 cwd;
|
|
5
|
+
* 不碰用户仓库的对象、索引与引用。git 以子进程调用(零运行时依赖)。
|
|
6
|
+
* - 隔离:清掉继承的 `GIT_*` 环境变量;全局 / 系统配置指向空文件(不读用户的签名、钩子、过滤器、模板);
|
|
7
|
+
* 本地配置写固定身份、`core.autocrlf=false`、`core.hooksPath` 指向空目录、`gc.auto=0` 等;
|
|
8
|
+
* `info/attributes` 关掉换行转换与过滤器,存进去的就是磁盘原字节。
|
|
9
|
+
* - 忽略:工作区里的 `.gitignore` 由 git 自己读;cwd 在用户仓库里时,再把用户仓库判定为忽略的路径
|
|
10
|
+
* (含上级目录的 `.gitignore`、`info/exclude`、全局忽略文件)逐条写进影子仓库的 `info/exclude`;`.git` 一律排除。
|
|
11
|
+
* - 索引:每个进程一个(`GIT_INDEX_FILE=ama-index-<pid>`),同进程内对同一影子仓库的操作串行;
|
|
12
|
+
* 已退出进程留下的索引在初始化时清掉。
|
|
13
|
+
* - 快照:`add -A` + `write-tree` + `commit-tree`(父为上一个影子提交)。提交不挂引用,`gc.auto=0` 且
|
|
14
|
+
* `gc.pruneExpire=never`,不会被自动回收。
|
|
15
|
+
*/
|
|
16
|
+
export declare const SHADOW_DIR = "shadow";
|
|
17
|
+
/** cwd 下(不含忽略的)文件数上限,超出本会话降级为 tools。 */
|
|
18
|
+
export declare const SHADOW_MAX_FILES = 20000;
|
|
19
|
+
/** 单次快照耗时上限(毫秒),超出本会话降级为 tools。 */
|
|
20
|
+
export declare const SHADOW_MAX_SNAPSHOT_MS = 3000;
|
|
21
|
+
export declare function shadowRoot(dataDir: string): string;
|
|
22
|
+
/** cwd 对应的影子仓库目录。 */
|
|
23
|
+
export declare function shadowRepoDir(dataDir: string, cwd: string): string;
|
|
24
|
+
/** cwd 是家目录或文件系统根时不启用影子 git,返回原因。 */
|
|
25
|
+
export declare function unsafeShadowCwd(cwd: string, home?: string): string | undefined;
|
|
26
|
+
/** git 不在 PATH(或指定的可执行文件不存在)。 */
|
|
27
|
+
export declare class GitMissingError extends Error {
|
|
28
|
+
constructor();
|
|
29
|
+
}
|
|
30
|
+
export interface ShadowGitOptions {
|
|
31
|
+
/** git 可执行文件(缺省 `git`,按 PATH 查找)。 */
|
|
32
|
+
git?: string;
|
|
33
|
+
/** 子进程环境(缺省 process.env;`GIT_*` 一律去掉)。 */
|
|
34
|
+
env?: NodeJS.ProcessEnv;
|
|
35
|
+
/** 缺省 `SHADOW_MAX_FILES`。 */
|
|
36
|
+
maxFiles?: number;
|
|
37
|
+
/** 缺省 `SHADOW_MAX_SNAPSHOT_MS`。 */
|
|
38
|
+
maxSnapshotMs?: number;
|
|
39
|
+
/** 测试用:时钟。 */
|
|
40
|
+
now?(): number;
|
|
41
|
+
}
|
|
42
|
+
export type ShadowDisabledReason = "too_many_files" | "too_slow";
|
|
43
|
+
export interface ShadowSnapshot {
|
|
44
|
+
/** 提交 id;文件数超限时没有提交。 */
|
|
45
|
+
commit?: string;
|
|
46
|
+
/** 本次快照后应降级的原因。 */
|
|
47
|
+
degrade?: {
|
|
48
|
+
reason: ShadowDisabledReason;
|
|
49
|
+
message: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** diff-tree 的一项(old = 第一个树,new = 第二个树)。 */
|
|
53
|
+
export interface ShadowChange {
|
|
54
|
+
path: string;
|
|
55
|
+
oldMode: string;
|
|
56
|
+
newMode: string;
|
|
57
|
+
oldOid: string;
|
|
58
|
+
newOid: string;
|
|
59
|
+
status: string;
|
|
60
|
+
}
|
|
61
|
+
export declare const MODE_ABSENT = "000000";
|
|
62
|
+
export declare const MODE_SYMLINK = "120000";
|
|
63
|
+
export declare const MODE_GITLINK = "160000";
|
|
64
|
+
export declare const MODE_EXECUTABLE = "100755";
|
|
65
|
+
export declare class ShadowRepo {
|
|
66
|
+
readonly dir: string;
|
|
67
|
+
readonly cwd: string;
|
|
68
|
+
private readonly git;
|
|
69
|
+
private readonly baseEnv;
|
|
70
|
+
private readonly maxFiles;
|
|
71
|
+
private readonly maxSnapshotMs;
|
|
72
|
+
private readonly options;
|
|
73
|
+
private readonly indexFile;
|
|
74
|
+
private initialized;
|
|
75
|
+
private counted;
|
|
76
|
+
constructor(dataDir: string, cwd: string, options?: ShadowGitOptions);
|
|
77
|
+
private now;
|
|
78
|
+
private env;
|
|
79
|
+
/** 在影子仓库上执行 git(stdout 原字节)。 */
|
|
80
|
+
private run;
|
|
81
|
+
private text;
|
|
82
|
+
/** 建仓库与本地配置(每个实例一次;已是当前版本则只清理残留索引)。 */
|
|
83
|
+
init(): Promise<void>;
|
|
84
|
+
private doInit;
|
|
85
|
+
/** 把用户仓库判定为忽略的路径写进影子仓库的 info/exclude(cwd 不在仓库里时只排除 .git)。 */
|
|
86
|
+
private writeExcludes;
|
|
87
|
+
/** 把工作区收进索引并写树,返回树 id(调用前已 writeExcludes)。 */
|
|
88
|
+
private stageTree;
|
|
89
|
+
/** 新回合快照:返回提交 id 与是否该降级。 */
|
|
90
|
+
snapshot(parent: string | undefined, message: string): Promise<ShadowSnapshot>;
|
|
91
|
+
/** 当前工作区的树(恢复时与目标提交比较)。 */
|
|
92
|
+
currentTree(): Promise<string>;
|
|
93
|
+
private hasCommitUnlocked;
|
|
94
|
+
hasCommit(id: string): Promise<boolean>;
|
|
95
|
+
/** 两个树(提交或树 id)之间的差异,不识别重命名。 */
|
|
96
|
+
diff(from: string, to: string): Promise<ShadowChange[]>;
|
|
97
|
+
/** 树里的全部文件路径。 */
|
|
98
|
+
paths(treeish: string): Promise<Set<string>>;
|
|
99
|
+
/** 读一批 blob;超过 maxBytes 或不存在的不返回。 */
|
|
100
|
+
readBlobs(oids: readonly string[], maxBytes: number): Promise<Map<string, Buffer>>;
|
|
101
|
+
}
|
|
102
|
+
/** 影子仓库占用(doctor 用)。 */
|
|
103
|
+
export declare function shadowUsage(dataDir: string): Promise<{
|
|
104
|
+
repos: number;
|
|
105
|
+
bytes: number;
|
|
106
|
+
}>;
|
|
107
|
+
/** 用户仓库给出的忽略路径 → 锚定到根的 exclude 行(通配符转义;带换行的名字放弃)。 */
|
|
108
|
+
export declare function excludeLine(path: string): string | undefined;
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 影子 git 仓库(docs/rewind-plan.md §6)。[RW-D]
|
|
3
|
+
*
|
|
4
|
+
* - 位置 `<dataDir>/file-history/shadow/<sha256(cwd) 前 16>/`,用 `--git-dir` 指向它、`--work-tree` 指向 cwd;
|
|
5
|
+
* 不碰用户仓库的对象、索引与引用。git 以子进程调用(零运行时依赖)。
|
|
6
|
+
* - 隔离:清掉继承的 `GIT_*` 环境变量;全局 / 系统配置指向空文件(不读用户的签名、钩子、过滤器、模板);
|
|
7
|
+
* 本地配置写固定身份、`core.autocrlf=false`、`core.hooksPath` 指向空目录、`gc.auto=0` 等;
|
|
8
|
+
* `info/attributes` 关掉换行转换与过滤器,存进去的就是磁盘原字节。
|
|
9
|
+
* - 忽略:工作区里的 `.gitignore` 由 git 自己读;cwd 在用户仓库里时,再把用户仓库判定为忽略的路径
|
|
10
|
+
* (含上级目录的 `.gitignore`、`info/exclude`、全局忽略文件)逐条写进影子仓库的 `info/exclude`;`.git` 一律排除。
|
|
11
|
+
* - 索引:每个进程一个(`GIT_INDEX_FILE=ama-index-<pid>`),同进程内对同一影子仓库的操作串行;
|
|
12
|
+
* 已退出进程留下的索引在初始化时清掉。
|
|
13
|
+
* - 快照:`add -A` + `write-tree` + `commit-tree`(父为上一个影子提交)。提交不挂引用,`gc.auto=0` 且
|
|
14
|
+
* `gc.pruneExpire=never`,不会被自动回收。
|
|
15
|
+
*/
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { join, parse, resolve } from "node:path";
|
|
21
|
+
import { fileHistoryDir } from "./blobs.js";
|
|
22
|
+
export const SHADOW_DIR = "shadow";
|
|
23
|
+
/** cwd 下(不含忽略的)文件数上限,超出本会话降级为 tools。 */
|
|
24
|
+
export const SHADOW_MAX_FILES = 20_000;
|
|
25
|
+
/** 单次快照耗时上限(毫秒),超出本会话降级为 tools。 */
|
|
26
|
+
export const SHADOW_MAX_SNAPSHOT_MS = 3_000;
|
|
27
|
+
/** 单个 git 进程的硬上限,防止卡死。 */
|
|
28
|
+
const GIT_TIMEOUT_MS = 60_000;
|
|
29
|
+
/** 本地配置的版本;改了配置项就加一,旧仓库会补写。 */
|
|
30
|
+
const CONFIG_VERSION = "1";
|
|
31
|
+
const IDENTITY = { name: "ama", email: "ama@localhost" };
|
|
32
|
+
const ATTRIBUTES = "* -text -eol -filter -ident -working-tree-encoding\n";
|
|
33
|
+
export function shadowRoot(dataDir) {
|
|
34
|
+
return join(fileHistoryDir(dataDir), SHADOW_DIR);
|
|
35
|
+
}
|
|
36
|
+
/** cwd 对应的影子仓库目录。 */
|
|
37
|
+
export function shadowRepoDir(dataDir, cwd) {
|
|
38
|
+
const digest = createHash("sha256").update(resolve(cwd)).digest("hex");
|
|
39
|
+
return join(shadowRoot(dataDir), digest.slice(0, 16));
|
|
40
|
+
}
|
|
41
|
+
/** cwd 是家目录或文件系统根时不启用影子 git,返回原因。 */
|
|
42
|
+
export function unsafeShadowCwd(cwd, home = homedir()) {
|
|
43
|
+
const abs = resolve(cwd);
|
|
44
|
+
if (parse(abs).root === abs)
|
|
45
|
+
return "工作目录是文件系统根目录";
|
|
46
|
+
if (home !== "" && sameDir(abs, resolve(home)))
|
|
47
|
+
return "工作目录是家目录";
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
function sameDir(a, b) {
|
|
51
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
52
|
+
}
|
|
53
|
+
/** git 不在 PATH(或指定的可执行文件不存在)。 */
|
|
54
|
+
export class GitMissingError extends Error {
|
|
55
|
+
constructor() {
|
|
56
|
+
super("找不到 git");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export const MODE_ABSENT = "000000";
|
|
60
|
+
export const MODE_SYMLINK = "120000";
|
|
61
|
+
export const MODE_GITLINK = "160000";
|
|
62
|
+
export const MODE_EXECUTABLE = "100755";
|
|
63
|
+
const queues = new Map();
|
|
64
|
+
/** 同进程内对同一影子仓库的操作串行(共用一个索引文件)。 */
|
|
65
|
+
function exclusive(key, work) {
|
|
66
|
+
const previous = queues.get(key) ?? Promise.resolve();
|
|
67
|
+
const next = previous.then(work, work);
|
|
68
|
+
const settled = next.catch(() => undefined);
|
|
69
|
+
queues.set(key, settled);
|
|
70
|
+
void settled.then(() => {
|
|
71
|
+
if (queues.get(key) === settled)
|
|
72
|
+
queues.delete(key);
|
|
73
|
+
});
|
|
74
|
+
return next;
|
|
75
|
+
}
|
|
76
|
+
export class ShadowRepo {
|
|
77
|
+
dir;
|
|
78
|
+
cwd;
|
|
79
|
+
git;
|
|
80
|
+
baseEnv;
|
|
81
|
+
maxFiles;
|
|
82
|
+
maxSnapshotMs;
|
|
83
|
+
options;
|
|
84
|
+
indexFile;
|
|
85
|
+
initialized;
|
|
86
|
+
counted = false;
|
|
87
|
+
constructor(dataDir, cwd, options = {}) {
|
|
88
|
+
this.cwd = resolve(cwd);
|
|
89
|
+
this.dir = shadowRepoDir(dataDir, this.cwd);
|
|
90
|
+
this.options = options;
|
|
91
|
+
this.git = options.git ?? "git";
|
|
92
|
+
this.baseEnv = stripGitEnv(options.env ?? process.env);
|
|
93
|
+
this.maxFiles = options.maxFiles ?? SHADOW_MAX_FILES;
|
|
94
|
+
this.maxSnapshotMs = options.maxSnapshotMs ?? SHADOW_MAX_SNAPSHOT_MS;
|
|
95
|
+
this.indexFile = join(this.dir, `ama-index-${process.pid}`);
|
|
96
|
+
}
|
|
97
|
+
now() {
|
|
98
|
+
return this.options.now?.() ?? Date.now();
|
|
99
|
+
}
|
|
100
|
+
env() {
|
|
101
|
+
const empty = join(this.dir, "ama-empty");
|
|
102
|
+
return {
|
|
103
|
+
...this.baseEnv,
|
|
104
|
+
GIT_INDEX_FILE: this.indexFile,
|
|
105
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
106
|
+
GIT_CONFIG_GLOBAL: empty,
|
|
107
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
108
|
+
LC_ALL: "C",
|
|
109
|
+
GIT_AUTHOR_NAME: IDENTITY.name,
|
|
110
|
+
GIT_AUTHOR_EMAIL: IDENTITY.email,
|
|
111
|
+
GIT_COMMITTER_NAME: IDENTITY.name,
|
|
112
|
+
GIT_COMMITTER_EMAIL: IDENTITY.email,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** 在影子仓库上执行 git(stdout 原字节)。 */
|
|
116
|
+
run(args, options = {}) {
|
|
117
|
+
const full = options.userRepo === true
|
|
118
|
+
? [...args]
|
|
119
|
+
: [`--git-dir=${this.dir}`, `--work-tree=${this.cwd}`, ...args];
|
|
120
|
+
return runGit(this.git, full, {
|
|
121
|
+
cwd: this.cwd,
|
|
122
|
+
env: options.userRepo === true ? this.baseEnv : this.env(),
|
|
123
|
+
...(options.input !== undefined ? { input: options.input } : {}),
|
|
124
|
+
...(options.allowFailure !== undefined ? { allowFailure: options.allowFailure } : {}),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async text(args) {
|
|
128
|
+
return (await this.run(args)).toString("utf8").trim();
|
|
129
|
+
}
|
|
130
|
+
/** 建仓库与本地配置(每个实例一次;已是当前版本则只清理残留索引)。 */
|
|
131
|
+
init() {
|
|
132
|
+
this.initialized ??= this.doInit().catch((error) => {
|
|
133
|
+
this.initialized = undefined;
|
|
134
|
+
throw error;
|
|
135
|
+
});
|
|
136
|
+
return this.initialized;
|
|
137
|
+
}
|
|
138
|
+
async doInit() {
|
|
139
|
+
const hooks = join(this.dir, "ama-hooks");
|
|
140
|
+
await mkdir(hooks, { recursive: true, mode: 0o700 });
|
|
141
|
+
await writeFile(join(this.dir, "ama-empty"), "", { flag: "a", mode: 0o600 });
|
|
142
|
+
await writeFile(join(this.dir, "ama-cwd"), `${this.cwd}\n`, { mode: 0o600 });
|
|
143
|
+
const version = await readSmall(join(this.dir, "ama-config-version"));
|
|
144
|
+
if (version !== CONFIG_VERSION) {
|
|
145
|
+
if (!(await exists(join(this.dir, "HEAD")))) {
|
|
146
|
+
await runGit(this.git, ["init", "-q", "--bare", `--template=${hooks}`, this.dir], {
|
|
147
|
+
cwd: this.cwd,
|
|
148
|
+
env: this.env(),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const config = [
|
|
152
|
+
["core.bare", "false"],
|
|
153
|
+
["core.autocrlf", "false"],
|
|
154
|
+
["core.safecrlf", "false"],
|
|
155
|
+
["core.hooksPath", hooks.split("\\").join("/")],
|
|
156
|
+
["core.fsmonitor", "false"],
|
|
157
|
+
["core.untrackedCache", "false"],
|
|
158
|
+
["core.quotePath", "false"],
|
|
159
|
+
["core.longpaths", "true"],
|
|
160
|
+
["core.excludesFile", join(this.dir, "ama-empty").split("\\").join("/")],
|
|
161
|
+
["core.attributesFile", join(this.dir, "ama-empty").split("\\").join("/")],
|
|
162
|
+
["gc.auto", "0"],
|
|
163
|
+
["gc.autoDetach", "false"],
|
|
164
|
+
["gc.pruneExpire", "never"],
|
|
165
|
+
["maintenance.auto", "false"],
|
|
166
|
+
["commit.gpgSign", "false"],
|
|
167
|
+
["user.name", IDENTITY.name],
|
|
168
|
+
["user.email", IDENTITY.email],
|
|
169
|
+
["user.useConfigOnly", "true"],
|
|
170
|
+
];
|
|
171
|
+
for (const [key, value] of config)
|
|
172
|
+
await this.run(["config", key, value]);
|
|
173
|
+
await mkdir(join(this.dir, "info"), { recursive: true });
|
|
174
|
+
await writeFile(join(this.dir, "info", "attributes"), ATTRIBUTES);
|
|
175
|
+
await writeFile(join(this.dir, "ama-config-version"), CONFIG_VERSION);
|
|
176
|
+
}
|
|
177
|
+
await removeStaleIndexes(this.dir);
|
|
178
|
+
}
|
|
179
|
+
/** 把用户仓库判定为忽略的路径写进影子仓库的 info/exclude(cwd 不在仓库里时只排除 .git)。 */
|
|
180
|
+
async writeExcludes() {
|
|
181
|
+
const lines = ["/.git", ".git"];
|
|
182
|
+
try {
|
|
183
|
+
const out = await this.run(["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--directory"], { userRepo: true });
|
|
184
|
+
for (const path of splitZ(out)) {
|
|
185
|
+
const line = excludeLine(path);
|
|
186
|
+
if (line !== undefined)
|
|
187
|
+
lines.push(line);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (error instanceof GitMissingError)
|
|
192
|
+
throw error;
|
|
193
|
+
// 不在 git 仓库里:只靠工作区里的 .gitignore
|
|
194
|
+
}
|
|
195
|
+
await mkdir(join(this.dir, "info"), { recursive: true });
|
|
196
|
+
await writeFile(join(this.dir, "info", "exclude"), `${lines.join("\n")}\n`);
|
|
197
|
+
}
|
|
198
|
+
/** 把工作区收进索引并写树,返回树 id(调用前已 writeExcludes)。 */
|
|
199
|
+
async stageTree() {
|
|
200
|
+
await this.run(["add", "-A", "--ignore-errors"], { allowFailure: true });
|
|
201
|
+
return this.text(["write-tree"]);
|
|
202
|
+
}
|
|
203
|
+
/** 新回合快照:返回提交 id 与是否该降级。 */
|
|
204
|
+
snapshot(parent, message) {
|
|
205
|
+
return exclusive(this.dir, async () => {
|
|
206
|
+
const started = this.now();
|
|
207
|
+
await this.init();
|
|
208
|
+
await this.writeExcludes();
|
|
209
|
+
if (!this.counted) {
|
|
210
|
+
const out = await this.run([
|
|
211
|
+
"ls-files",
|
|
212
|
+
"-z",
|
|
213
|
+
"--others",
|
|
214
|
+
"--cached",
|
|
215
|
+
"--exclude-standard",
|
|
216
|
+
]);
|
|
217
|
+
const files = countZ(out);
|
|
218
|
+
this.counted = true;
|
|
219
|
+
if (files > this.maxFiles) {
|
|
220
|
+
return {
|
|
221
|
+
degrade: {
|
|
222
|
+
reason: "too_many_files",
|
|
223
|
+
message: `工作目录有 ${files} 个文件(上限 ${this.maxFiles})`,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const tree = await this.stageTree();
|
|
229
|
+
const args = ["commit-tree", tree, "-m", message];
|
|
230
|
+
if (parent !== undefined && (await this.hasCommitUnlocked(parent)))
|
|
231
|
+
args.push("-p", parent);
|
|
232
|
+
const commit = await this.text(args);
|
|
233
|
+
const elapsed = this.now() - started;
|
|
234
|
+
if (elapsed > this.maxSnapshotMs) {
|
|
235
|
+
return {
|
|
236
|
+
commit,
|
|
237
|
+
degrade: {
|
|
238
|
+
reason: "too_slow",
|
|
239
|
+
message: `快照用了 ${(elapsed / 1000).toFixed(1)} 秒(上限 ${this.maxSnapshotMs / 1000} 秒)`,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return { commit };
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/** 当前工作区的树(恢复时与目标提交比较)。 */
|
|
247
|
+
currentTree() {
|
|
248
|
+
return exclusive(this.dir, async () => {
|
|
249
|
+
await this.init();
|
|
250
|
+
await this.writeExcludes();
|
|
251
|
+
return this.stageTree();
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async hasCommitUnlocked(id) {
|
|
255
|
+
if (!/^[0-9a-f]{40,64}$/.test(id))
|
|
256
|
+
return false;
|
|
257
|
+
try {
|
|
258
|
+
await this.run(["cat-file", "-e", `${id}^{commit}`]);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (error instanceof GitMissingError)
|
|
263
|
+
throw error;
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
hasCommit(id) {
|
|
268
|
+
return exclusive(this.dir, async () => {
|
|
269
|
+
await this.init();
|
|
270
|
+
return this.hasCommitUnlocked(id);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/** 两个树(提交或树 id)之间的差异,不识别重命名。 */
|
|
274
|
+
async diff(from, to) {
|
|
275
|
+
const out = await this.run(["diff-tree", "-r", "-z", "--no-renames", "--raw", from, to]);
|
|
276
|
+
const parts = splitZ(out);
|
|
277
|
+
const changes = [];
|
|
278
|
+
for (let i = 0; i + 1 < parts.length; i += 2) {
|
|
279
|
+
const meta = /^:(\d{6}) (\d{6}) ([0-9a-f]+) ([0-9a-f]+) ([A-Z])/.exec(parts[i]);
|
|
280
|
+
if (meta === null)
|
|
281
|
+
continue;
|
|
282
|
+
changes.push({
|
|
283
|
+
oldMode: meta[1],
|
|
284
|
+
newMode: meta[2],
|
|
285
|
+
oldOid: meta[3],
|
|
286
|
+
newOid: meta[4],
|
|
287
|
+
status: meta[5],
|
|
288
|
+
path: parts[i + 1],
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return changes;
|
|
292
|
+
}
|
|
293
|
+
/** 树里的全部文件路径。 */
|
|
294
|
+
async paths(treeish) {
|
|
295
|
+
return new Set(splitZ(await this.run(["ls-tree", "-r", "-z", "--name-only", treeish])));
|
|
296
|
+
}
|
|
297
|
+
/** 读一批 blob;超过 maxBytes 或不存在的不返回。 */
|
|
298
|
+
async readBlobs(oids, maxBytes) {
|
|
299
|
+
const out = new Map();
|
|
300
|
+
const unique = [...new Set(oids)];
|
|
301
|
+
if (unique.length === 0)
|
|
302
|
+
return out;
|
|
303
|
+
const sizes = (await this.run(["cat-file", "--batch-check"], { input: `${unique.join("\n")}\n` }))
|
|
304
|
+
.toString("utf8")
|
|
305
|
+
.split("\n");
|
|
306
|
+
const wanted = [];
|
|
307
|
+
for (const line of sizes) {
|
|
308
|
+
const [oid, type, size] = line.split(" ");
|
|
309
|
+
if (oid !== undefined && type === "blob" && Number(size) <= maxBytes)
|
|
310
|
+
wanted.push(oid);
|
|
311
|
+
}
|
|
312
|
+
if (wanted.length === 0)
|
|
313
|
+
return out;
|
|
314
|
+
const data = await this.run(["cat-file", "--batch"], { input: `${wanted.join("\n")}\n` });
|
|
315
|
+
let offset = 0;
|
|
316
|
+
while (offset < data.length) {
|
|
317
|
+
const newline = data.indexOf(0x0a, offset);
|
|
318
|
+
if (newline < 0)
|
|
319
|
+
break;
|
|
320
|
+
const [oid, type, size] = data.subarray(offset, newline).toString("utf8").split(" ");
|
|
321
|
+
offset = newline + 1;
|
|
322
|
+
if (type === "missing" || oid === undefined)
|
|
323
|
+
continue;
|
|
324
|
+
const length = Number(size);
|
|
325
|
+
out.set(oid, Buffer.from(data.subarray(offset, offset + length)));
|
|
326
|
+
offset += length + 1;
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/** 影子仓库占用(doctor 用)。 */
|
|
332
|
+
export async function shadowUsage(dataDir) {
|
|
333
|
+
const usage = { repos: 0, bytes: 0 };
|
|
334
|
+
let names;
|
|
335
|
+
try {
|
|
336
|
+
names = await readdir(shadowRoot(dataDir));
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return usage;
|
|
340
|
+
}
|
|
341
|
+
for (const name of names) {
|
|
342
|
+
const dir = join(shadowRoot(dataDir), name);
|
|
343
|
+
if (!/^[0-9a-f]{16}$/.test(name) || !(await exists(join(dir, "HEAD"))))
|
|
344
|
+
continue;
|
|
345
|
+
usage.repos++;
|
|
346
|
+
usage.bytes += await duBytes(dir);
|
|
347
|
+
}
|
|
348
|
+
return usage;
|
|
349
|
+
}
|
|
350
|
+
async function duBytes(dir) {
|
|
351
|
+
let total = 0;
|
|
352
|
+
let entries;
|
|
353
|
+
try {
|
|
354
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return 0;
|
|
358
|
+
}
|
|
359
|
+
for (const entry of entries) {
|
|
360
|
+
const full = join(dir, entry.name);
|
|
361
|
+
if (entry.isDirectory())
|
|
362
|
+
total += await duBytes(full);
|
|
363
|
+
else if (entry.isFile()) {
|
|
364
|
+
try {
|
|
365
|
+
total += (await stat(full)).size;
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// 并发删除
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return total;
|
|
373
|
+
}
|
|
374
|
+
function stripGitEnv(env) {
|
|
375
|
+
const out = {};
|
|
376
|
+
for (const [key, value] of Object.entries(env)) {
|
|
377
|
+
if (!key.toUpperCase().startsWith("GIT_"))
|
|
378
|
+
out[key] = value;
|
|
379
|
+
}
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
function runGit(git, args, options) {
|
|
383
|
+
return new Promise((resolvePromise, reject) => {
|
|
384
|
+
let child;
|
|
385
|
+
try {
|
|
386
|
+
child = spawn(git, args, {
|
|
387
|
+
cwd: options.cwd,
|
|
388
|
+
env: options.env,
|
|
389
|
+
windowsHide: true,
|
|
390
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
reject(isMissing(error) ? new GitMissingError() : error);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const stdout = [];
|
|
398
|
+
const stderr = [];
|
|
399
|
+
const timer = setTimeout(() => child.kill(), GIT_TIMEOUT_MS);
|
|
400
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
401
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
402
|
+
child.on("error", (error) => {
|
|
403
|
+
clearTimeout(timer);
|
|
404
|
+
reject(isMissing(error) ? new GitMissingError() : error);
|
|
405
|
+
});
|
|
406
|
+
child.on("close", (code, signal) => {
|
|
407
|
+
clearTimeout(timer);
|
|
408
|
+
const errText = Buffer.concat(stderr).toString("utf8");
|
|
409
|
+
const tolerated = options.allowFailure === true && signal === null && !/^fatal:/m.test(errText);
|
|
410
|
+
if (code === 0 || tolerated) {
|
|
411
|
+
resolvePromise(Buffer.concat(stdout));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const detail = errText.trim().split("\n").slice(-2).join("; ");
|
|
415
|
+
reject(new Error(`git ${args.find((a) => !a.startsWith("--")) ?? ""} 失败(${signal ?? code})${detail === "" ? "" : `:${detail}`}`));
|
|
416
|
+
});
|
|
417
|
+
child.stdin.on("error", () => undefined);
|
|
418
|
+
child.stdin.end(options.input ?? "");
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function isMissing(error) {
|
|
422
|
+
const code = error?.code;
|
|
423
|
+
return code === "ENOENT" || code === "EACCES";
|
|
424
|
+
}
|
|
425
|
+
function splitZ(out) {
|
|
426
|
+
const text = out.toString("utf8");
|
|
427
|
+
if (text === "")
|
|
428
|
+
return [];
|
|
429
|
+
const parts = text.split("\0");
|
|
430
|
+
if (parts[parts.length - 1] === "")
|
|
431
|
+
parts.pop();
|
|
432
|
+
return parts;
|
|
433
|
+
}
|
|
434
|
+
function countZ(out) {
|
|
435
|
+
let n = 0;
|
|
436
|
+
for (const byte of out)
|
|
437
|
+
if (byte === 0)
|
|
438
|
+
n++;
|
|
439
|
+
return n;
|
|
440
|
+
}
|
|
441
|
+
/** 用户仓库给出的忽略路径 → 锚定到根的 exclude 行(通配符转义;带换行的名字放弃)。 */
|
|
442
|
+
export function excludeLine(path) {
|
|
443
|
+
if (path === "" || path.includes("\n") || path.includes("\r"))
|
|
444
|
+
return undefined;
|
|
445
|
+
const escaped = path.replace(/[\\*?[\]!#]/g, (c) => `\\${c}`).replace(/ $/, "\\ ");
|
|
446
|
+
return `/${escaped}`;
|
|
447
|
+
}
|
|
448
|
+
async function removeStaleIndexes(dir) {
|
|
449
|
+
let names;
|
|
450
|
+
try {
|
|
451
|
+
names = await readdir(dir);
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
for (const name of names) {
|
|
457
|
+
const match = /^ama-index-(\d+)(\.lock)?$/.exec(name);
|
|
458
|
+
if (match === null)
|
|
459
|
+
continue;
|
|
460
|
+
const pid = Number(match[1]);
|
|
461
|
+
if (pid === process.pid || isAlive(pid))
|
|
462
|
+
continue;
|
|
463
|
+
await rm(join(dir, name), { force: true }).catch(() => undefined);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function isAlive(pid) {
|
|
467
|
+
try {
|
|
468
|
+
process.kill(pid, 0);
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
return error.code === "EPERM";
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async function exists(path) {
|
|
476
|
+
try {
|
|
477
|
+
await stat(path);
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async function readSmall(path) {
|
|
485
|
+
try {
|
|
486
|
+
return (await readFile(path, "utf8")).trim();
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 按影子提交恢复代码(docs/rewind-plan.md §6)。[RW-D]
|
|
3
|
+
*
|
|
4
|
+
* 1. 把当前工作区写成树,与目标提交做 `diff-tree`:只处理有差异的文件,被忽略的文件不在两边的树里,不会被碰。
|
|
5
|
+
* 目标里有、当前树里没有但磁盘上还在的文件(现在被忽略了)跳过。
|
|
6
|
+
* 2. 每个差异文件交给 `restore.ts` 的 `restoreFile`,冲突与安全检查语义与 tools 模式相同:
|
|
7
|
+
* - 「已知」= 当前内容等于最近一个影子提交里的内容,或等于 ama 最后写入的、最近检查点记录的;
|
|
8
|
+
* 其余视为回合外的改动(冲突),缺省跳过。
|
|
9
|
+
* - 父目录应为 `realpath(cwd)/<相对目录>`(路径上有符号链接目录 → parent_moved)。
|
|
10
|
+
* - 目标是符号链接 / 子模块 → not_regular;目标 blob 超过 maxFileBytes → too_large。
|
|
11
|
+
* - 权限只在可执行位与目标不同时调整(git 只记 644 / 755),新建文件按目标的可执行位。
|
|
12
|
+
* 3. edit / write 跟踪的文件不在影子树里(cwd 外、被忽略)时,按 tools 的记录恢复。
|
|
13
|
+
*/
|
|
14
|
+
import { type LoadedCheckpoints } from "./replay.js";
|
|
15
|
+
import { type RestoreOptions, type RestoreOutcome } from "./restore.js";
|
|
16
|
+
import { type ShadowRepo } from "./shadow-git.js";
|
|
17
|
+
export interface ShadowRestoreOptions extends RestoreOptions {
|
|
18
|
+
repo: ShadowRepo;
|
|
19
|
+
/** 目标提交里超过它的文件报告 too_large(缺省 5 MiB)。 */
|
|
20
|
+
maxFileBytes?: number;
|
|
21
|
+
}
|
|
22
|
+
/** 文件顺序上最后一个带影子提交的检查点。 */
|
|
23
|
+
export declare function latestShadowCommit(state: LoadedCheckpoints): string | undefined;
|
|
24
|
+
/** 目标检查点没有影子提交、或提交已不在影子仓库里时抛错(调用方回退到 tools)。 */
|
|
25
|
+
export declare function restoreFromShadow(options: ShadowRestoreOptions): Promise<RestoreOutcome>;
|