@armadra/agent 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +174 -1
- package/README.md +201 -82
- 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 +36203 -20499
- 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/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-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.js +5 -6
- 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 +7 -1
- package/dist/modes/commands-core.js +55 -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 +9 -1
- package/dist/modes/interactive/commands.js +19 -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 +134 -136
- package/dist/modes/interactive/key-dispatch.d.ts +16 -2
- package/dist/modes/interactive/key-dispatch.js +49 -1
- package/dist/modes/interactive/line/line-mode.d.ts +2 -1
- package/dist/modes/interactive/line/line-mode.js +22 -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/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/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 +5 -0
- package/dist/tui/keybindings.js +5 -0
- 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 +77 -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 +125 -6
- package/package.json +13 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 读 git HEAD(docs/rewind-plan.md §1.2、D6)。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* 不起 git 进程:从 cwd 向上找 `.git`(目录,或 worktree / 子模块的 `gitdir: <路径>` 文件),读 `HEAD`;
|
|
5
|
+
* 符号引用先查 gitdir 再查 `commondir`(worktree 的分支引用在主仓库),最后查 `packed-refs`。
|
|
6
|
+
* 读不到(不在仓库、未提交过的分支、格式不认识)返回 undefined。
|
|
7
|
+
*/
|
|
8
|
+
import { readFile, stat } from "node:fs/promises";
|
|
9
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
10
|
+
const SHA = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
11
|
+
async function readText(path) {
|
|
12
|
+
try {
|
|
13
|
+
return await readFile(path, "utf8");
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** 找到 cwd 所在仓库的 gitdir;不在仓库返回 undefined。 */
|
|
20
|
+
export async function findGitDir(cwd) {
|
|
21
|
+
let dir = resolve(cwd);
|
|
22
|
+
for (;;) {
|
|
23
|
+
const candidate = join(dir, ".git");
|
|
24
|
+
try {
|
|
25
|
+
const info = await stat(candidate);
|
|
26
|
+
if (info.isDirectory())
|
|
27
|
+
return candidate;
|
|
28
|
+
if (info.isFile()) {
|
|
29
|
+
const text = (await readText(candidate)) ?? "";
|
|
30
|
+
const match = /^gitdir:\s*(.+?)\s*$/m.exec(text);
|
|
31
|
+
if (match?.[1] !== undefined) {
|
|
32
|
+
return isAbsolute(match[1]) ? match[1] : resolve(dir, match[1]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// 没有 .git:继续向上
|
|
38
|
+
}
|
|
39
|
+
const parent = dirname(dir);
|
|
40
|
+
if (parent === dir)
|
|
41
|
+
return undefined;
|
|
42
|
+
dir = parent;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function resolveRef(gitDir, ref) {
|
|
46
|
+
const commonText = await readText(join(gitDir, "commondir"));
|
|
47
|
+
const commonDir = commonText === undefined
|
|
48
|
+
? gitDir
|
|
49
|
+
: isAbsolute(commonText.trim())
|
|
50
|
+
? commonText.trim()
|
|
51
|
+
: resolve(gitDir, commonText.trim());
|
|
52
|
+
const dirs = commonDir === gitDir ? [gitDir] : [gitDir, commonDir];
|
|
53
|
+
for (const dir of dirs) {
|
|
54
|
+
const text = (await readText(join(dir, ...ref.split("/"))))?.trim();
|
|
55
|
+
if (text !== undefined && SHA.test(text))
|
|
56
|
+
return text;
|
|
57
|
+
}
|
|
58
|
+
for (const dir of dirs) {
|
|
59
|
+
const packed = await readText(join(dir, "packed-refs"));
|
|
60
|
+
if (packed === undefined)
|
|
61
|
+
continue;
|
|
62
|
+
for (const line of packed.split("\n")) {
|
|
63
|
+
const [sha, name] = line.trim().split(/\s+/);
|
|
64
|
+
if (name === ref && sha !== undefined && SHA.test(sha))
|
|
65
|
+
return sha;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
/** cwd 所在仓库的 HEAD 提交与分支;detached 时没有 branch。 */
|
|
71
|
+
export async function readGitHead(cwd) {
|
|
72
|
+
const gitDir = await findGitDir(cwd);
|
|
73
|
+
if (gitDir === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
const head = (await readText(join(gitDir, "HEAD")))?.trim();
|
|
76
|
+
if (head === undefined)
|
|
77
|
+
return undefined;
|
|
78
|
+
if (SHA.test(head))
|
|
79
|
+
return { head };
|
|
80
|
+
const match = /^ref:\s*(\S+)$/.exec(head);
|
|
81
|
+
if (match?.[1] === undefined)
|
|
82
|
+
return undefined;
|
|
83
|
+
const ref = match[1];
|
|
84
|
+
const sha = await resolveRef(gitDir, ref);
|
|
85
|
+
if (sha === undefined)
|
|
86
|
+
return undefined;
|
|
87
|
+
const branch = ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : undefined;
|
|
88
|
+
return { head: sha, ...(branch !== undefined ? { branch } : {}) };
|
|
89
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 检查点(docs/rewind-plan.md)的对外入口。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* 会话接线(RW-B)用 `createCheckpointBackendFactory` + `resolveCheckpointSettings`;
|
|
5
|
+
* 其余导出给 CLI(GC、占用)与测试。
|
|
6
|
+
*/
|
|
7
|
+
export * from "./types.js";
|
|
8
|
+
export { createCheckpointBackendFactory, type CheckpointBackend, type CheckpointBackendContext, type CheckpointBackendFactory, type CheckpointBackendSettings, } from "./backend.js";
|
|
9
|
+
export { resolveCheckpointSettings, type CheckpointSettings } from "./settings.js";
|
|
10
|
+
export { CheckpointTracker, createCheckpointTracker, type CheckpointTrackerOptions, } from "./tracker.js";
|
|
11
|
+
export { isRewindable, latestCheckpoint, loadCheckpoints, rewindableIds, trackedKeys, type CheckpointEntryLike, type LoadedCheckpoints, } from "./replay.js";
|
|
12
|
+
export { countLineChanges, emptyCodeResult, gitHintFor, restoreCheckpoint, type RestoreOptions, type RestoreOutcome, } from "./restore.js";
|
|
13
|
+
export { collectBlobRefs, formatBytes, gcBlobs, readSessionRoots, registerSessionRoot, type GcOptions, type GcResult, } from "./gc.js";
|
|
14
|
+
export { blobPath, blobUsage, fileHistoryDir, hashBytes, keyToPath, pathKey, putBlob, readBlob, recordFile, type BlobUsage, } from "./blobs.js";
|
|
15
|
+
export { readGitHead, type GitHead } from "./git-head.js";
|
|
16
|
+
export { SHADOW_MAX_FILES, SHADOW_MAX_SNAPSHOT_MS, ShadowRepo, shadowRepoDir, shadowUsage, type ShadowGitOptions, } from "./shadow-git.js";
|
|
17
|
+
export { restoreFromShadow, type ShadowRestoreOptions } from "./shadow-restore.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 检查点(docs/rewind-plan.md)的对外入口。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* 会话接线(RW-B)用 `createCheckpointBackendFactory` + `resolveCheckpointSettings`;
|
|
5
|
+
* 其余导出给 CLI(GC、占用)与测试。
|
|
6
|
+
*/
|
|
7
|
+
export * from "./types.js";
|
|
8
|
+
export { createCheckpointBackendFactory, } from "./backend.js";
|
|
9
|
+
export { resolveCheckpointSettings } from "./settings.js";
|
|
10
|
+
export { CheckpointTracker, createCheckpointTracker, } from "./tracker.js";
|
|
11
|
+
export { isRewindable, latestCheckpoint, loadCheckpoints, rewindableIds, trackedKeys, } from "./replay.js";
|
|
12
|
+
export { countLineChanges, emptyCodeResult, gitHintFor, restoreCheckpoint, } from "./restore.js";
|
|
13
|
+
export { collectBlobRefs, formatBytes, gcBlobs, readSessionRoots, registerSessionRoot, } from "./gc.js";
|
|
14
|
+
export { blobPath, blobUsage, fileHistoryDir, hashBytes, keyToPath, pathKey, putBlob, readBlob, recordFile, } from "./blobs.js";
|
|
15
|
+
export { readGitHead } from "./git-head.js";
|
|
16
|
+
export { SHADOW_MAX_FILES, SHADOW_MAX_SNAPSHOT_MS, ShadowRepo, shadowRepoDir, shadowUsage, } from "./shadow-git.js";
|
|
17
|
+
export { restoreFromShadow } from "./shadow-restore.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从会话条目重建检查点(docs/rewind-plan.md §1.2)。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* - 按文件顺序重放全部条目(不限活动分支)里的 `ama.checkpoint` 与 `ama.checkpoint-track`;
|
|
5
|
+
* track 并入对应 userEntryId 的检查点(检查点里已有该文件时不覆盖)。
|
|
6
|
+
* - 每个文件的第一条记录永久作为回退(`earliest`);最近一条记录与其条目时间(`latest`)给快照沿用判断。
|
|
7
|
+
* - 形状不对的条目跳过(不抛):会话文件可能来自更新的版本。
|
|
8
|
+
*/
|
|
9
|
+
import { type CheckpointData, type CheckpointTrackData, type FileRecord } from "./types.js";
|
|
10
|
+
/** 重放需要的最小条目形状(SessionEntry 满足)。 */
|
|
11
|
+
export interface CheckpointEntryLike {
|
|
12
|
+
type: string;
|
|
13
|
+
customType?: string;
|
|
14
|
+
data?: unknown;
|
|
15
|
+
/** ISO 8601。 */
|
|
16
|
+
timestamp?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface LoadedCheckpoints {
|
|
19
|
+
/** userEntryId → 检查点(含并入的 track)。 */
|
|
20
|
+
byUserEntry: Map<string, CheckpointData>;
|
|
21
|
+
/** userEntryId,按第一次出现的顺序(从旧到新)。 */
|
|
22
|
+
order: string[];
|
|
23
|
+
/** 路径键 → 该文件最早的记录。 */
|
|
24
|
+
earliest: Map<string, FileRecord>;
|
|
25
|
+
/** 路径键 → 最近一条记录与其条目时间(毫秒;条目无时间时 0)。 */
|
|
26
|
+
latest: Map<string, {
|
|
27
|
+
record: FileRecord;
|
|
28
|
+
at: number;
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
export declare function emptyCheckpoints(): LoadedCheckpoints;
|
|
32
|
+
/** 校验并规整一条 FileRecord;不合法返回 undefined。 */
|
|
33
|
+
export declare function parseFileRecord(value: unknown): FileRecord | undefined;
|
|
34
|
+
/** 把一条检查点并进已加载状态(重放与运行时追加共用)。 */
|
|
35
|
+
export declare function applyCheckpoint(state: LoadedCheckpoints, data: CheckpointData, at: number): void;
|
|
36
|
+
/** 把一条 track 并进已加载状态。 */
|
|
37
|
+
export declare function applyTrack(state: LoadedCheckpoints, track: CheckpointTrackData, at: number): void;
|
|
38
|
+
/** 重放会话条目(全文件,不限活动分支)。 */
|
|
39
|
+
export declare function loadCheckpoints(entries: Iterable<CheckpointEntryLike>): LoadedCheckpoints;
|
|
40
|
+
export declare const DEFAULT_CHECKPOINT_KEEP = 100;
|
|
41
|
+
/** 仍可作为回滚点的 userEntryId(最近 `keep` 个检查点,§1.3)。 */
|
|
42
|
+
export declare function rewindableIds(state: LoadedCheckpoints, keep?: number): Set<string>;
|
|
43
|
+
/** 该用户消息是否有可用检查点(含 keep 上限)。 */
|
|
44
|
+
export declare function isRewindable(state: LoadedCheckpoints, userEntryId: string, keep?: number): boolean;
|
|
45
|
+
/** 文件顺序上最后一个检查点(冲突检测的「最近检查点」)。 */
|
|
46
|
+
export declare function latestCheckpoint(state: LoadedCheckpoints): CheckpointData | undefined;
|
|
47
|
+
/** 全部出现过的路径键(= 已跟踪文件)。 */
|
|
48
|
+
export declare function trackedKeys(state: LoadedCheckpoints): string[];
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从会话条目重建检查点(docs/rewind-plan.md §1.2)。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* - 按文件顺序重放全部条目(不限活动分支)里的 `ama.checkpoint` 与 `ama.checkpoint-track`;
|
|
5
|
+
* track 并入对应 userEntryId 的检查点(检查点里已有该文件时不覆盖)。
|
|
6
|
+
* - 每个文件的第一条记录永久作为回退(`earliest`);最近一条记录与其条目时间(`latest`)给快照沿用判断。
|
|
7
|
+
* - 形状不对的条目跳过(不抛):会话文件可能来自更新的版本。
|
|
8
|
+
*/
|
|
9
|
+
import { isBlobHash } from "./blobs.js";
|
|
10
|
+
import { CHECKPOINT_CUSTOM_TYPE, CHECKPOINT_TRACK_CUSTOM_TYPE, } from "./types.js";
|
|
11
|
+
export function emptyCheckpoints() {
|
|
12
|
+
return { byUserEntry: new Map(), order: [], earliest: new Map(), latest: new Map() };
|
|
13
|
+
}
|
|
14
|
+
const SKIP_REASONS = new Set(["too_large", "not_regular"]);
|
|
15
|
+
/** 校验并规整一条 FileRecord;不合法返回 undefined。 */
|
|
16
|
+
export function parseFileRecord(value) {
|
|
17
|
+
if (typeof value !== "object" || value === null)
|
|
18
|
+
return undefined;
|
|
19
|
+
const v = value;
|
|
20
|
+
const blob = v["blob"];
|
|
21
|
+
if (blob !== null && !isBlobHash(blob))
|
|
22
|
+
return undefined;
|
|
23
|
+
const record = { blob };
|
|
24
|
+
if (typeof v["mode"] === "number")
|
|
25
|
+
record.mode = v["mode"];
|
|
26
|
+
if (typeof v["size"] === "number")
|
|
27
|
+
record.size = v["size"];
|
|
28
|
+
if (typeof v["realParentDir"] === "string")
|
|
29
|
+
record.realParentDir = v["realParentDir"];
|
|
30
|
+
const skipped = v["skipped"];
|
|
31
|
+
if (typeof skipped === "string" && SKIP_REASONS.has(skipped)) {
|
|
32
|
+
record.skipped = skipped;
|
|
33
|
+
}
|
|
34
|
+
return record;
|
|
35
|
+
}
|
|
36
|
+
function parseCheckpoint(data) {
|
|
37
|
+
if (typeof data !== "object" || data === null)
|
|
38
|
+
return undefined;
|
|
39
|
+
const d = data;
|
|
40
|
+
if (d["v"] !== 1 || typeof d["userEntryId"] !== "string")
|
|
41
|
+
return undefined;
|
|
42
|
+
const files = {};
|
|
43
|
+
const rawFiles = d["files"];
|
|
44
|
+
if (typeof rawFiles === "object" && rawFiles !== null) {
|
|
45
|
+
for (const [key, raw] of Object.entries(rawFiles)) {
|
|
46
|
+
const record = parseFileRecord(raw);
|
|
47
|
+
if (record !== undefined)
|
|
48
|
+
files[key] = record;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const out = { v: 1, userEntryId: d["userEntryId"], files };
|
|
52
|
+
const git = d["git"];
|
|
53
|
+
if (typeof git === "object" && git !== null && typeof git["head"] === "string") {
|
|
54
|
+
out.git = {
|
|
55
|
+
head: git["head"],
|
|
56
|
+
...(typeof git["branch"] === "string" ? { branch: git["branch"] } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (typeof d["shadowCommit"] === "string")
|
|
60
|
+
out.shadowCommit = d["shadowCommit"];
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
function parseTrack(data) {
|
|
64
|
+
if (typeof data !== "object" || data === null)
|
|
65
|
+
return undefined;
|
|
66
|
+
const d = data;
|
|
67
|
+
if (d["v"] !== 1 || typeof d["userEntryId"] !== "string" || typeof d["path"] !== "string") {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const record = parseFileRecord(d["record"]);
|
|
71
|
+
if (record === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
return { v: 1, userEntryId: d["userEntryId"], path: d["path"], record };
|
|
74
|
+
}
|
|
75
|
+
function timeOf(entry) {
|
|
76
|
+
const t = entry.timestamp === undefined ? NaN : Date.parse(entry.timestamp);
|
|
77
|
+
return Number.isFinite(t) ? t : 0;
|
|
78
|
+
}
|
|
79
|
+
/** 把一条检查点并进已加载状态(重放与运行时追加共用)。 */
|
|
80
|
+
export function applyCheckpoint(state, data, at) {
|
|
81
|
+
const existing = state.byUserEntry.get(data.userEntryId);
|
|
82
|
+
if (existing === undefined) {
|
|
83
|
+
state.byUserEntry.set(data.userEntryId, { ...data, files: { ...data.files } });
|
|
84
|
+
state.order.push(data.userEntryId);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// 同一回合的检查点重复出现(重试快照等):以后来的为准,保留已并入的 track。
|
|
88
|
+
existing.files = { ...existing.files, ...data.files };
|
|
89
|
+
if (data.git !== undefined)
|
|
90
|
+
existing.git = data.git;
|
|
91
|
+
if (data.shadowCommit !== undefined)
|
|
92
|
+
existing.shadowCommit = data.shadowCommit;
|
|
93
|
+
}
|
|
94
|
+
for (const [key, record] of Object.entries(data.files))
|
|
95
|
+
noteRecord(state, key, record, at);
|
|
96
|
+
}
|
|
97
|
+
/** 把一条 track 并进已加载状态。 */
|
|
98
|
+
export function applyTrack(state, track, at) {
|
|
99
|
+
let checkpoint = state.byUserEntry.get(track.userEntryId);
|
|
100
|
+
if (checkpoint === undefined) {
|
|
101
|
+
checkpoint = { v: 1, userEntryId: track.userEntryId, files: {} };
|
|
102
|
+
state.byUserEntry.set(track.userEntryId, checkpoint);
|
|
103
|
+
state.order.push(track.userEntryId);
|
|
104
|
+
}
|
|
105
|
+
if (checkpoint.files[track.path] === undefined)
|
|
106
|
+
checkpoint.files[track.path] = track.record;
|
|
107
|
+
noteRecord(state, track.path, track.record, at);
|
|
108
|
+
}
|
|
109
|
+
function noteRecord(state, key, record, at) {
|
|
110
|
+
if (!state.earliest.has(key))
|
|
111
|
+
state.earliest.set(key, record);
|
|
112
|
+
state.latest.set(key, { record, at });
|
|
113
|
+
}
|
|
114
|
+
/** 重放会话条目(全文件,不限活动分支)。 */
|
|
115
|
+
export function loadCheckpoints(entries) {
|
|
116
|
+
const state = emptyCheckpoints();
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
if (entry.type !== "custom")
|
|
119
|
+
continue;
|
|
120
|
+
if (entry.customType === CHECKPOINT_CUSTOM_TYPE) {
|
|
121
|
+
const data = parseCheckpoint(entry.data);
|
|
122
|
+
if (data !== undefined)
|
|
123
|
+
applyCheckpoint(state, data, timeOf(entry));
|
|
124
|
+
}
|
|
125
|
+
else if (entry.customType === CHECKPOINT_TRACK_CUSTOM_TYPE) {
|
|
126
|
+
const track = parseTrack(entry.data);
|
|
127
|
+
if (track !== undefined)
|
|
128
|
+
applyTrack(state, track, timeOf(entry));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return state;
|
|
132
|
+
}
|
|
133
|
+
export const DEFAULT_CHECKPOINT_KEEP = 100;
|
|
134
|
+
/** 仍可作为回滚点的 userEntryId(最近 `keep` 个检查点,§1.3)。 */
|
|
135
|
+
export function rewindableIds(state, keep = DEFAULT_CHECKPOINT_KEEP) {
|
|
136
|
+
const n = Math.max(0, Math.floor(keep));
|
|
137
|
+
return new Set(n === 0 ? [] : state.order.slice(-n));
|
|
138
|
+
}
|
|
139
|
+
/** 该用户消息是否有可用检查点(含 keep 上限)。 */
|
|
140
|
+
export function isRewindable(state, userEntryId, keep = DEFAULT_CHECKPOINT_KEEP) {
|
|
141
|
+
if (!state.byUserEntry.has(userEntryId))
|
|
142
|
+
return false;
|
|
143
|
+
const index = state.order.lastIndexOf(userEntryId);
|
|
144
|
+
return index >= 0 && index >= state.order.length - Math.max(0, Math.floor(keep));
|
|
145
|
+
}
|
|
146
|
+
/** 文件顺序上最后一个检查点(冲突检测的「最近检查点」)。 */
|
|
147
|
+
export function latestCheckpoint(state) {
|
|
148
|
+
const id = state.order[state.order.length - 1];
|
|
149
|
+
return id === undefined ? undefined : state.byUserEntry.get(id);
|
|
150
|
+
}
|
|
151
|
+
/** 全部出现过的路径键(= 已跟踪文件)。 */
|
|
152
|
+
export function trackedKeys(state) {
|
|
153
|
+
return [...state.earliest.keys()];
|
|
154
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 恢复代码(docs/rewind-plan.md §3.2)。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* 对每个已跟踪文件,取目标检查点里的记录,没有则取该文件最早的记录:
|
|
5
|
+
* 1. 记录 `skipped` → 报告 too_large / not_regular;
|
|
6
|
+
* (目标当前是符号链接 / 非普通文件时无法比较内容,直接按第 4 步报告 symlink / not_regular)
|
|
7
|
+
* 2. 当前内容与记录相同 → 跳过(不计入);
|
|
8
|
+
* 3. 冲突:当前 sha256 ∉ {lastWritten,最近检查点的记录} → skip 时列入 conflicts 不动;
|
|
9
|
+
* 4. 安全检查:nlink > 1、父目录 realpath ≠ realParentDir → skipped;备份 blob 不在 → backup_missing;
|
|
10
|
+
* 5. `blob: null` → 删除(只删普通文件);
|
|
11
|
+
* 6. 写回:Windows 以外用 O_NOFOLLOW 打开,fstat 与 lstat 的 dev / ino 一致才写;清空后写入并恢复 mode;
|
|
12
|
+
* 目标不存在时建父目录并再次校验;
|
|
13
|
+
* 7. 被恢复 / 删除的绝对路径作为 `touched` 返回,由调用方从 readFiles 移除。
|
|
14
|
+
*
|
|
15
|
+
* 单文件的 1–6 是 `restoreFile`,结果汇总是 `settleFile`;影子 git(`shadow-restore.ts`)复用二者,
|
|
16
|
+
* 只换掉「目标内容从哪来」(`load`)与「当前内容算不算已知」(`isKnown`)。
|
|
17
|
+
*
|
|
18
|
+
* `dryRun` 只做 1–4,并用行级 diff 计 insertions / deletions(复用 tools/edit 的 unifiedDiff)。
|
|
19
|
+
*/
|
|
20
|
+
import { type LoadedCheckpoints } from "./replay.js";
|
|
21
|
+
import type { CheckpointData, CodeRestoreResult, FileRecord, RewindSkipReason } from "./types.js";
|
|
22
|
+
export interface RestoreOptions {
|
|
23
|
+
cwd: string;
|
|
24
|
+
dataDir: string;
|
|
25
|
+
/** 重放 / tracker 的状态(提供已跟踪文件、最早记录与最近检查点)。 */
|
|
26
|
+
state: LoadedCheckpoints;
|
|
27
|
+
/** 回滚目标。 */
|
|
28
|
+
target: CheckpointData;
|
|
29
|
+
/** ama 最后写入的内容哈希(绝对路径 → sha256)。 */
|
|
30
|
+
lastWritten?: ReadonlyMap<string, string>;
|
|
31
|
+
dryRun?: boolean;
|
|
32
|
+
onConflict?: "skip" | "overwrite";
|
|
33
|
+
/** 超过它的当前文件不读内容算行数(缺省 5 MiB)。 */
|
|
34
|
+
maxDiffBytes?: number;
|
|
35
|
+
/** 只处理这些路径键(缺省全部已跟踪文件)。 */
|
|
36
|
+
keys?: Iterable<string>;
|
|
37
|
+
}
|
|
38
|
+
export interface RestoreOutcome {
|
|
39
|
+
result: CodeRestoreResult;
|
|
40
|
+
/** 被恢复 / 删除的绝对路径(dryRun 时为空);调用方从 readFiles 移除。 */
|
|
41
|
+
touched: string[];
|
|
42
|
+
}
|
|
43
|
+
/** 单文件不恢复的原因(安全检查不通过等);`settleFile` 把它记进 `skipped`。 */
|
|
44
|
+
export declare class RestoreSkip extends Error {
|
|
45
|
+
readonly reason: RewindSkipReason;
|
|
46
|
+
constructor(reason: RewindSkipReason);
|
|
47
|
+
}
|
|
48
|
+
/** 没有可恢复内容时的结果。 */
|
|
49
|
+
export declare function emptyCodeResult(): CodeRestoreResult;
|
|
50
|
+
/** 行级增删数(current → target)。 */
|
|
51
|
+
export declare function countLineChanges(current: string, target: string): {
|
|
52
|
+
insertions: number;
|
|
53
|
+
deletions: number;
|
|
54
|
+
};
|
|
55
|
+
/** 恢复到目标检查点(§3.2)。 */
|
|
56
|
+
export declare function restoreCheckpoint(options: RestoreOptions): Promise<RestoreOutcome>;
|
|
57
|
+
/** 单文件恢复的环境。 */
|
|
58
|
+
export interface FileRestoreContext {
|
|
59
|
+
dataDir: string;
|
|
60
|
+
dryRun?: boolean;
|
|
61
|
+
onConflict?: "skip" | "overwrite";
|
|
62
|
+
/** 超过它的当前文件不读内容算行数。 */
|
|
63
|
+
maxDiff: number;
|
|
64
|
+
/** 当前内容(sha256;不存在为 null)是 ama 写的或最近检查点记录的 → 不算冲突(§3.2 第 3 步)。 */
|
|
65
|
+
isKnown(currentHash: string | null): boolean;
|
|
66
|
+
/** 取目标内容;缺省按 `record.blob` 读备份。返回 undefined → backup_missing。 */
|
|
67
|
+
load?(record: FileRecord): Promise<Buffer | undefined>;
|
|
68
|
+
}
|
|
69
|
+
export interface FileOutcome {
|
|
70
|
+
deleted: boolean;
|
|
71
|
+
conflict: boolean;
|
|
72
|
+
insertions: number;
|
|
73
|
+
deletions: number;
|
|
74
|
+
}
|
|
75
|
+
/** 执行单文件恢复并把结果并进 `result` / `touched`(RestoreSkip → skipped,其它错误 → failed)。 */
|
|
76
|
+
export declare function settleFile(result: CodeRestoreResult, touched: string[], key: string, abs: string, options: {
|
|
77
|
+
dryRun?: boolean;
|
|
78
|
+
onConflict?: "skip" | "overwrite";
|
|
79
|
+
}, work: () => Promise<FileOutcome | undefined>): Promise<void>;
|
|
80
|
+
/** §3.2 的 1–6(单个文件);内容相同返回 undefined。 */
|
|
81
|
+
export declare function restoreFile(abs: string, record: FileRecord, options: FileRestoreContext): Promise<FileOutcome | undefined>;
|
|
82
|
+
/** 记录的 HEAD 与当前不同时给出(只提示,不操作 git)。 */
|
|
83
|
+
export declare function gitHintFor(cwd: string, target: CheckpointData): Promise<{
|
|
84
|
+
recordedHead: string;
|
|
85
|
+
currentHead: string;
|
|
86
|
+
} | undefined>;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 恢复代码(docs/rewind-plan.md §3.2)。[RW-A]
|
|
3
|
+
*
|
|
4
|
+
* 对每个已跟踪文件,取目标检查点里的记录,没有则取该文件最早的记录:
|
|
5
|
+
* 1. 记录 `skipped` → 报告 too_large / not_regular;
|
|
6
|
+
* (目标当前是符号链接 / 非普通文件时无法比较内容,直接按第 4 步报告 symlink / not_regular)
|
|
7
|
+
* 2. 当前内容与记录相同 → 跳过(不计入);
|
|
8
|
+
* 3. 冲突:当前 sha256 ∉ {lastWritten,最近检查点的记录} → skip 时列入 conflicts 不动;
|
|
9
|
+
* 4. 安全检查:nlink > 1、父目录 realpath ≠ realParentDir → skipped;备份 blob 不在 → backup_missing;
|
|
10
|
+
* 5. `blob: null` → 删除(只删普通文件);
|
|
11
|
+
* 6. 写回:Windows 以外用 O_NOFOLLOW 打开,fstat 与 lstat 的 dev / ino 一致才写;清空后写入并恢复 mode;
|
|
12
|
+
* 目标不存在时建父目录并再次校验;
|
|
13
|
+
* 7. 被恢复 / 删除的绝对路径作为 `touched` 返回,由调用方从 readFiles 移除。
|
|
14
|
+
*
|
|
15
|
+
* 单文件的 1–6 是 `restoreFile`,结果汇总是 `settleFile`;影子 git(`shadow-restore.ts`)复用二者,
|
|
16
|
+
* 只换掉「目标内容从哪来」(`load`)与「当前内容算不算已知」(`isKnown`)。
|
|
17
|
+
*
|
|
18
|
+
* `dryRun` 只做 1–4,并用行级 diff 计 insertions / deletions(复用 tools/edit 的 unifiedDiff)。
|
|
19
|
+
*/
|
|
20
|
+
import { constants } from "node:fs";
|
|
21
|
+
import { lstat, mkdir, open, readFile, realpath, unlink } from "node:fs/promises";
|
|
22
|
+
import { dirname } from "node:path";
|
|
23
|
+
import { unifiedDiff } from "../tools/edit.js";
|
|
24
|
+
import { DEFAULT_MAX_FILE_BYTES, hashFile, isNotFound, keyToPath, readBlob, realParentOf, } from "./blobs.js";
|
|
25
|
+
import { readGitHead } from "./git-head.js";
|
|
26
|
+
import { latestCheckpoint } from "./replay.js";
|
|
27
|
+
/** 单文件不恢复的原因(安全检查不通过等);`settleFile` 把它记进 `skipped`。 */
|
|
28
|
+
export class RestoreSkip extends Error {
|
|
29
|
+
reason;
|
|
30
|
+
constructor(reason) {
|
|
31
|
+
super(reason);
|
|
32
|
+
this.reason = reason;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const Skip = RestoreSkip;
|
|
36
|
+
const IS_WINDOWS = process.platform === "win32";
|
|
37
|
+
/** Windows 没有 O_NOFOLLOW(为 undefined);只靠 lstat 与 fstat 的 dev / ino 比较。 */
|
|
38
|
+
const NOFOLLOW = IS_WINDOWS ? 0 : (constants.O_NOFOLLOW ?? 0);
|
|
39
|
+
/** 没有可恢复内容时的结果。 */
|
|
40
|
+
export function emptyCodeResult() {
|
|
41
|
+
return {
|
|
42
|
+
restored: [],
|
|
43
|
+
deleted: [],
|
|
44
|
+
conflicts: [],
|
|
45
|
+
skipped: [],
|
|
46
|
+
failed: [],
|
|
47
|
+
insertions: 0,
|
|
48
|
+
deletions: 0,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function lstatOrUndefined(path) {
|
|
52
|
+
try {
|
|
53
|
+
return await lstat(path);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (isNotFound(error))
|
|
57
|
+
return undefined;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function toText(bytes) {
|
|
62
|
+
return bytes === undefined ? "" : bytes.toString("utf8");
|
|
63
|
+
}
|
|
64
|
+
/** 行级增删数(current → target)。 */
|
|
65
|
+
export function countLineChanges(current, target) {
|
|
66
|
+
let insertions = 0;
|
|
67
|
+
let deletions = 0;
|
|
68
|
+
const diff = unifiedDiff(current, target, "f", 0);
|
|
69
|
+
const lines = diff === "" ? [] : diff.split("\n").slice(2);
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
if (line.startsWith("+"))
|
|
72
|
+
insertions++;
|
|
73
|
+
else if (line.startsWith("-"))
|
|
74
|
+
deletions++;
|
|
75
|
+
}
|
|
76
|
+
return { insertions, deletions };
|
|
77
|
+
}
|
|
78
|
+
/** 恢复到目标检查点(§3.2)。 */
|
|
79
|
+
export async function restoreCheckpoint(options) {
|
|
80
|
+
const result = emptyCodeResult();
|
|
81
|
+
const touched = [];
|
|
82
|
+
const latest = latestCheckpoint(options.state);
|
|
83
|
+
const maxDiff = options.maxDiffBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
84
|
+
const only = options.keys === undefined ? undefined : new Set(options.keys);
|
|
85
|
+
for (const [key, earliest] of options.state.earliest) {
|
|
86
|
+
if (only !== undefined && !only.has(key))
|
|
87
|
+
continue;
|
|
88
|
+
const record = options.target.files[key] ?? earliest;
|
|
89
|
+
const abs = keyToPath(options.cwd, key);
|
|
90
|
+
const known = new Set();
|
|
91
|
+
const written = options.lastWritten?.get(abs);
|
|
92
|
+
if (written !== undefined)
|
|
93
|
+
known.add(written);
|
|
94
|
+
const latestRecord = latest?.files[key];
|
|
95
|
+
if (latestRecord !== undefined && latestRecord.skipped === undefined) {
|
|
96
|
+
known.add(latestRecord.blob);
|
|
97
|
+
}
|
|
98
|
+
await settleFile(result, touched, key, abs, options, () => restoreFile(abs, record, {
|
|
99
|
+
dataDir: options.dataDir,
|
|
100
|
+
...(options.dryRun !== undefined ? { dryRun: options.dryRun } : {}),
|
|
101
|
+
...(options.onConflict !== undefined ? { onConflict: options.onConflict } : {}),
|
|
102
|
+
maxDiff,
|
|
103
|
+
isKnown: (hash) => known.has(hash),
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
return { result, touched };
|
|
107
|
+
}
|
|
108
|
+
/** 执行单文件恢复并把结果并进 `result` / `touched`(RestoreSkip → skipped,其它错误 → failed)。 */
|
|
109
|
+
export async function settleFile(result, touched, key, abs, options, work) {
|
|
110
|
+
try {
|
|
111
|
+
const outcome = await work();
|
|
112
|
+
if (outcome === undefined)
|
|
113
|
+
return;
|
|
114
|
+
if (outcome.conflict)
|
|
115
|
+
result.conflicts.push(key);
|
|
116
|
+
if (outcome.conflict && options.onConflict !== "overwrite")
|
|
117
|
+
return;
|
|
118
|
+
result.insertions += outcome.insertions;
|
|
119
|
+
result.deletions += outcome.deletions;
|
|
120
|
+
if (outcome.deleted)
|
|
121
|
+
result.deleted.push(key);
|
|
122
|
+
else
|
|
123
|
+
result.restored.push(key);
|
|
124
|
+
if (options.dryRun !== true)
|
|
125
|
+
touched.push(abs);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (error instanceof RestoreSkip)
|
|
129
|
+
result.skipped.push({ path: key, reason: error.reason });
|
|
130
|
+
else
|
|
131
|
+
result.failed.push({
|
|
132
|
+
path: key,
|
|
133
|
+
message: error instanceof Error ? error.message : String(error),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** §3.2 的 1–6(单个文件);内容相同返回 undefined。 */
|
|
138
|
+
export async function restoreFile(abs, record, options) {
|
|
139
|
+
// 1. 记录时就没备份
|
|
140
|
+
if (record.skipped !== undefined)
|
|
141
|
+
throw new Skip(record.skipped);
|
|
142
|
+
// 当前状态
|
|
143
|
+
const info = await lstatOrUndefined(abs);
|
|
144
|
+
if (info?.isSymbolicLink() === true)
|
|
145
|
+
throw new Skip("symlink");
|
|
146
|
+
if (info !== undefined && !info.isFile())
|
|
147
|
+
throw new Skip("not_regular");
|
|
148
|
+
const currentHash = info === undefined ? null : await hashFile(abs);
|
|
149
|
+
// 2. 内容相同
|
|
150
|
+
if (currentHash === record.blob)
|
|
151
|
+
return undefined;
|
|
152
|
+
// 3. 冲突
|
|
153
|
+
const conflict = !options.isKnown(currentHash);
|
|
154
|
+
if (conflict && options.onConflict !== "overwrite") {
|
|
155
|
+
return { deleted: record.blob === null, conflict, insertions: 0, deletions: 0 };
|
|
156
|
+
}
|
|
157
|
+
// 4. 安全检查
|
|
158
|
+
if (info !== undefined && info.nlink > 1)
|
|
159
|
+
throw new Skip("hardlink");
|
|
160
|
+
await checkParent(abs, record);
|
|
161
|
+
const targetBytes = record.blob === null
|
|
162
|
+
? undefined
|
|
163
|
+
: await (options.load ?? ((r) => readBlob(options.dataDir, r.blob)))(record);
|
|
164
|
+
if (record.blob !== null && targetBytes === undefined)
|
|
165
|
+
throw new Skip("backup_missing");
|
|
166
|
+
const currentBytes = info !== undefined && info.size <= options.maxDiff ? await readFile(abs) : undefined;
|
|
167
|
+
const counted = info !== undefined && currentBytes === undefined
|
|
168
|
+
? { insertions: 0, deletions: 0 }
|
|
169
|
+
: countLineChanges(toText(currentBytes), toText(targetBytes));
|
|
170
|
+
const outcome = { deleted: record.blob === null, conflict, ...counted };
|
|
171
|
+
if (options.dryRun === true)
|
|
172
|
+
return outcome;
|
|
173
|
+
if (record.blob === null) {
|
|
174
|
+
// 5. 删除(当前一定存在:不存在时第 2 步已跳过)
|
|
175
|
+
await removeRegular(abs, info);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
// 6. 写回
|
|
179
|
+
await writeBack(abs, targetBytes, record, info);
|
|
180
|
+
}
|
|
181
|
+
return outcome;
|
|
182
|
+
}
|
|
183
|
+
async function checkParent(abs, record) {
|
|
184
|
+
if (record.realParentDir === undefined)
|
|
185
|
+
return;
|
|
186
|
+
const real = await realParentOf(abs);
|
|
187
|
+
if (real !== undefined && real !== record.realParentDir)
|
|
188
|
+
throw new Skip("parent_moved");
|
|
189
|
+
}
|
|
190
|
+
async function removeRegular(abs, before) {
|
|
191
|
+
const now = await lstatOrUndefined(abs);
|
|
192
|
+
if (now === undefined)
|
|
193
|
+
return;
|
|
194
|
+
if (now.isSymbolicLink())
|
|
195
|
+
throw new Skip("symlink");
|
|
196
|
+
if (!now.isFile())
|
|
197
|
+
throw new Skip("not_regular");
|
|
198
|
+
if (now.nlink > 1)
|
|
199
|
+
throw new Skip("hardlink");
|
|
200
|
+
if (now.dev !== before.dev || now.ino !== before.ino)
|
|
201
|
+
throw new Error("文件在恢复期间被替换");
|
|
202
|
+
await unlink(abs);
|
|
203
|
+
}
|
|
204
|
+
async function writeBack(abs, bytes, record, before) {
|
|
205
|
+
if (before === undefined) {
|
|
206
|
+
const parent = dirname(abs);
|
|
207
|
+
await mkdir(parent, { recursive: true });
|
|
208
|
+
if (record.realParentDir !== undefined && (await realpath(parent)) !== record.realParentDir) {
|
|
209
|
+
throw new Skip("parent_moved");
|
|
210
|
+
}
|
|
211
|
+
const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | NOFOLLOW;
|
|
212
|
+
const handle = await open(abs, flags, record.mode ?? 0o644);
|
|
213
|
+
try {
|
|
214
|
+
await handle.writeFile(bytes);
|
|
215
|
+
if (record.mode !== undefined)
|
|
216
|
+
await handle.chmod(record.mode);
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
await handle.close();
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
let handle;
|
|
224
|
+
try {
|
|
225
|
+
handle = await open(abs, constants.O_WRONLY | NOFOLLOW);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
// O_NOFOLLOW 打开符号链接:ELOOP(Linux / macOS)或 EMLINK(FreeBSD)
|
|
229
|
+
const code = error.code;
|
|
230
|
+
if (code === "ELOOP" || code === "EMLINK")
|
|
231
|
+
throw new Skip("symlink");
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const opened = await handle.stat();
|
|
236
|
+
const now = await lstat(abs);
|
|
237
|
+
if (!opened.isFile() ||
|
|
238
|
+
opened.dev !== before.dev ||
|
|
239
|
+
opened.ino !== before.ino ||
|
|
240
|
+
now.dev !== opened.dev ||
|
|
241
|
+
now.ino !== opened.ino) {
|
|
242
|
+
throw new Error("文件在恢复期间被替换");
|
|
243
|
+
}
|
|
244
|
+
if (opened.nlink > 1)
|
|
245
|
+
throw new Skip("hardlink");
|
|
246
|
+
await handle.truncate(0);
|
|
247
|
+
await handle.write(bytes, 0, bytes.length, 0);
|
|
248
|
+
if (record.mode !== undefined)
|
|
249
|
+
await handle.chmod(record.mode);
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
await handle.close();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** 记录的 HEAD 与当前不同时给出(只提示,不操作 git)。 */
|
|
256
|
+
export async function gitHintFor(cwd, target) {
|
|
257
|
+
if (target.git === undefined)
|
|
258
|
+
return undefined;
|
|
259
|
+
const current = await readGitHead(cwd);
|
|
260
|
+
if (current === undefined || current.head === target.git.head)
|
|
261
|
+
return undefined;
|
|
262
|
+
return { recordedHead: target.git.head, currentHead: current.head };
|
|
263
|
+
}
|