@visual-z/xeno 0.1.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/README.md +498 -0
- package/docs/install.md +130 -0
- package/package.json +43 -0
- package/src/apps/approve.ts +49 -0
- package/src/apps/cli.ts +37 -0
- package/src/apps/commands.ts +226 -0
- package/src/apps/completions.ts +28 -0
- package/src/apps/input.ts +297 -0
- package/src/apps/render.ts +207 -0
- package/src/apps/shell.ts +2353 -0
- package/src/apps/startup.ts +1302 -0
- package/src/core/benchmark.ts +195 -0
- package/src/core/boundary.ts +174 -0
- package/src/core/churn.ts +59 -0
- package/src/core/completion-review.ts +66 -0
- package/src/core/context-policy.ts +60 -0
- package/src/core/context-window.ts +54 -0
- package/src/core/continuation.ts +114 -0
- package/src/core/cycle.ts +17 -0
- package/src/core/data-query.ts +201 -0
- package/src/core/deadline.ts +39 -0
- package/src/core/derive.ts +489 -0
- package/src/core/edit-plan.ts +200 -0
- package/src/core/environment.ts +29 -0
- package/src/core/event-validation.ts +99 -0
- package/src/core/events.ts +486 -0
- package/src/core/evidence-links.ts +71 -0
- package/src/core/evidence.ts +89 -0
- package/src/core/execution.ts +12 -0
- package/src/core/exhaustive.ts +18 -0
- package/src/core/experiment.ts +40 -0
- package/src/core/export.ts +169 -0
- package/src/core/file-references.ts +35 -0
- package/src/core/frontmatter.ts +213 -0
- package/src/core/handoff.ts +397 -0
- package/src/core/harness-comparison.ts +90 -0
- package/src/core/history-query.ts +103 -0
- package/src/core/history.ts +100 -0
- package/src/core/json-event.ts +135 -0
- package/src/core/json-pointer.ts +46 -0
- package/src/core/keymap.ts +241 -0
- package/src/core/observation-view.ts +87 -0
- package/src/core/outcome.ts +45 -0
- package/src/core/overflow.ts +59 -0
- package/src/core/permissions.ts +182 -0
- package/src/core/policy-evaluation.ts +55 -0
- package/src/core/progress.ts +101 -0
- package/src/core/project.ts +372 -0
- package/src/core/prompts.ts +83 -0
- package/src/core/queue.ts +60 -0
- package/src/core/read-bounds.ts +15 -0
- package/src/core/recall-analysis.ts +90 -0
- package/src/core/recall-trial.ts +153 -0
- package/src/core/response-recovery.ts +46 -0
- package/src/core/scheduling.ts +8 -0
- package/src/core/settings.ts +174 -0
- package/src/core/skills.ts +121 -0
- package/src/core/stall.ts +48 -0
- package/src/core/text-normalize.ts +81 -0
- package/src/core/theme.ts +179 -0
- package/src/core/tokens.ts +149 -0
- package/src/core/tool-contract.ts +102 -0
- package/src/core/tool-input.ts +38 -0
- package/src/core/transcript.ts +60 -0
- package/src/core/window-trial.ts +65 -0
- package/src/core/work-state.ts +103 -0
- package/src/core/yaml.ts +282 -0
- package/src/index.ts +10 -0
- package/src/runtime/auth.ts +255 -0
- package/src/runtime/benchmark.ts +187 -0
- package/src/runtime/catalog.ts +365 -0
- package/src/runtime/evidence-notes.ts +33 -0
- package/src/runtime/exec-sessions.ts +214 -0
- package/src/runtime/execution-control.ts +39 -0
- package/src/runtime/harness-comparison.ts +356 -0
- package/src/runtime/history-tools.ts +108 -0
- package/src/runtime/loop.ts +1983 -0
- package/src/runtime/model.ts +614 -0
- package/src/runtime/models-fetch.ts +155 -0
- package/src/runtime/oauth-device.ts +86 -0
- package/src/runtime/oauth.ts +759 -0
- package/src/runtime/openai-responses.ts +286 -0
- package/src/runtime/openai.ts +675 -0
- package/src/runtime/provider.ts +498 -0
- package/src/runtime/recall-trial.ts +226 -0
- package/src/runtime/resume.ts +66 -0
- package/src/runtime/retry.ts +297 -0
- package/src/runtime/tools.ts +1193 -0
- package/src/runtime/usage.ts +225 -0
- package/src/runtime/window-trial.ts +140 -0
- package/src/runtime/working-notes.ts +30 -0
- package/src/store/atomic.ts +31 -0
- package/src/store/auth-store.ts +50 -0
- package/src/store/blobs.ts +142 -0
- package/src/store/command.ts +75 -0
- package/src/store/config.ts +69 -0
- package/src/store/context-files.ts +356 -0
- package/src/store/file-index.ts +81 -0
- package/src/store/git-branch.ts +35 -0
- package/src/store/history-search.ts +19 -0
- package/src/store/log.ts +642 -0
- package/src/store/models.ts +41 -0
- package/src/store/observations.ts +181 -0
- package/src/store/query-source.ts +44 -0
- package/src/store/recovery.ts +39 -0
- package/src/store/replay.ts +63 -0
- package/src/store/resources.ts +361 -0
- package/src/store/restore.ts +302 -0
- package/src/store/search.ts +644 -0
- package/src/store/serial.ts +13 -0
- package/src/store/sessions.ts +463 -0
- package/src/store/trust.ts +69 -0
- package/src/store/workspace.ts +256 -0
- package/src/store/worktree.ts +233 -0
- package/src/tui/ansi.ts +67 -0
- package/src/tui/autocomplete.ts +29 -0
- package/src/tui/complete.ts +87 -0
- package/src/tui/diff.ts +160 -0
- package/src/tui/editor.ts +893 -0
- package/src/tui/exhaustive.ts +14 -0
- package/src/tui/fuzzy.ts +75 -0
- package/src/tui/host.ts +223 -0
- package/src/tui/input.ts +69 -0
- package/src/tui/keys.ts +378 -0
- package/src/tui/markdown.ts +17 -0
- package/src/tui/render.ts +153 -0
- package/src/tui/select.ts +240 -0
- package/src/tui/settings.ts +40 -0
- package/src/tui/statusline.ts +162 -0
- package/src/tui/style.ts +205 -0
- package/src/tui/terminal.ts +30 -0
- package/src/tui/text.ts +52 -0
- package/src/tui/toolview.ts +386 -0
- package/src/tui/transcript.ts +71 -0
- package/src/tui/tree.ts +405 -0
- package/src/tui/width.ts +567 -0
- package/src/vendor/pi-tui/LICENSE.upstream +21 -0
- package/src/vendor/pi-tui/VENDOR.md +23 -0
- package/src/vendor/pi-tui/alt-screen-search.ts +327 -0
- package/src/vendor/pi-tui/autocomplete.ts +835 -0
- package/src/vendor/pi-tui/components/alt-screen-flash.ts +51 -0
- package/src/vendor/pi-tui/components/box.ts +167 -0
- package/src/vendor/pi-tui/components/cancellable-loader.ts +40 -0
- package/src/vendor/pi-tui/components/editor.ts +2470 -0
- package/src/vendor/pi-tui/components/h-stack.ts +44 -0
- package/src/vendor/pi-tui/components/image.ts +127 -0
- package/src/vendor/pi-tui/components/input.ts +494 -0
- package/src/vendor/pi-tui/components/loader.ts +101 -0
- package/src/vendor/pi-tui/components/markdown.ts +1015 -0
- package/src/vendor/pi-tui/components/mouse-region.ts +33 -0
- package/src/vendor/pi-tui/components/scroll-view.ts +224 -0
- package/src/vendor/pi-tui/components/select-list.ts +273 -0
- package/src/vendor/pi-tui/components/settings-list.ts +328 -0
- package/src/vendor/pi-tui/components/spacer.ts +28 -0
- package/src/vendor/pi-tui/components/stack.ts +154 -0
- package/src/vendor/pi-tui/components/text.ts +107 -0
- package/src/vendor/pi-tui/components/truncated-text.ts +65 -0
- package/src/vendor/pi-tui/components/v-stack.ts +33 -0
- package/src/vendor/pi-tui/editor-component.ts +74 -0
- package/src/vendor/pi-tui/fuzzy.ts +138 -0
- package/src/vendor/pi-tui/index.ts +156 -0
- package/src/vendor/pi-tui/keybindings.ts +320 -0
- package/src/vendor/pi-tui/keys.ts +1401 -0
- package/src/vendor/pi-tui/kill-ring.ts +46 -0
- package/src/vendor/pi-tui/latex.ts +1506 -0
- package/src/vendor/pi-tui/layout-node.ts +51 -0
- package/src/vendor/pi-tui/layout.ts +449 -0
- package/src/vendor/pi-tui/native-modifiers.ts +13 -0
- package/src/vendor/pi-tui/native-module-path.ts +31 -0
- package/src/vendor/pi-tui/native-platform.ts +63 -0
- package/src/vendor/pi-tui/stdin-buffer.ts +444 -0
- package/src/vendor/pi-tui/terminal-colors.ts +73 -0
- package/src/vendor/pi-tui/terminal-image.ts +696 -0
- package/src/vendor/pi-tui/terminal.ts +547 -0
- package/src/vendor/pi-tui/tui-alt-screen.ts +1742 -0
- package/src/vendor/pi-tui/tui-main-screen.ts +655 -0
- package/src/vendor/pi-tui/tui.ts +1456 -0
- package/src/vendor/pi-tui/undo-stack.ts +28 -0
- package/src/vendor/pi-tui/utils.ts +1345 -0
- package/src/vendor/pi-tui/word-navigation.ts +117 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restoring the workspace to a node.
|
|
3
|
+
*
|
|
4
|
+
* This is where appendix H's argument becomes executable: reverting is not
|
|
5
|
+
* overwriting the present with the past, it is moving a cursor to another
|
|
6
|
+
* node. The nodes are untouched, so the cursor can move again -- which is
|
|
7
|
+
* why a restore here cannot produce the failure that made Zed's checkpoint
|
|
8
|
+
* restore an unanswered S1, where reverting destroyed both the files and the
|
|
9
|
+
* thread with no way back.
|
|
10
|
+
*
|
|
11
|
+
* WHY THIS DOES NOT REUSE readAncestry. That walk stops at a cut, because a
|
|
12
|
+
* handoff basis means the parent's events do not replay -- correct for
|
|
13
|
+
* context, catastrophic for files. A node whose ancestry crosses a cut would
|
|
14
|
+
* see only the snapshots after it, and applyRestore deletes whatever is not
|
|
15
|
+
* in its target, so restoring past a boundary would silently remove every
|
|
16
|
+
* file written before it. File history therefore always walks to the root,
|
|
17
|
+
* whatever each node declared about context.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readNode, readManifest, digestNode, MAX_ANCESTRY_DEPTH, type Manifest } from "./log.ts";
|
|
21
|
+
import type { LogEvent } from "../core/events.ts";
|
|
22
|
+
import {
|
|
23
|
+
planRestore, applyRestore, scanWorkspace, DEFAULT_IGNORE,
|
|
24
|
+
type ScanDelta, type FileSet, type RestoreResult,
|
|
25
|
+
} from "./workspace.ts";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Where to move the workspace to.
|
|
29
|
+
*
|
|
30
|
+
* A bare node means the end of that node. `atEvent` names a snapshot inside
|
|
31
|
+
* it, which is what makes an ordinary undo possible: a node accumulates many
|
|
32
|
+
* turns, so without it the earliest reachable state is the end of the node
|
|
33
|
+
* and nothing in the node being worked in can be taken back.
|
|
34
|
+
*/
|
|
35
|
+
export type RestoreTarget = number | { node: number; atEvent?: string };
|
|
36
|
+
|
|
37
|
+
/** One snapshot, kept separate from its neighbours so a turn can be addressed. */
|
|
38
|
+
export type FileDelta = ScanDelta & { node: number; event: string };
|
|
39
|
+
|
|
40
|
+
export type FileHistoryResult =
|
|
41
|
+
| { ok: true; deltas: FileDelta[] }
|
|
42
|
+
| { ok: false; error: string };
|
|
43
|
+
|
|
44
|
+
/** One ancestor's contribution: its events, bounded by any fork point below it. */
|
|
45
|
+
export interface NodeEvents {
|
|
46
|
+
node: number;
|
|
47
|
+
events: LogEvent[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type NodeHistoryResult =
|
|
51
|
+
| { ok: true; nodes: NodeEvents[] }
|
|
52
|
+
| { ok: false; error: string };
|
|
53
|
+
|
|
54
|
+
export type PlanResult =
|
|
55
|
+
| { ok: true; files: FileSet; willWrite: string[]; willDelete: string[] }
|
|
56
|
+
| { ok: false; error: string };
|
|
57
|
+
|
|
58
|
+
export type RestoreToNodeResult =
|
|
59
|
+
| ({ ok: true } & RestoreResult)
|
|
60
|
+
| { ok: false; error: string };
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Every snapshot delta from the root to `node`, in replay order.
|
|
64
|
+
*
|
|
65
|
+
* Two fields on the way up, treated oppositely, because they answer different
|
|
66
|
+
* questions:
|
|
67
|
+
*
|
|
68
|
+
* `basis` says what the MODEL should see, and is ignored here. A cut
|
|
69
|
+
* truncates the context walk; the files it wrote are still on disk.
|
|
70
|
+
*
|
|
71
|
+
* `at_event` says where this node's history BEGINS in its parent, which is
|
|
72
|
+
* a question about history, so it is obeyed. Ignoring it makes a rewind
|
|
73
|
+
* restore the very file it was asked to undo -- the parent's later turns
|
|
74
|
+
* would flow into a node that forked before them.
|
|
75
|
+
*/
|
|
76
|
+
/**
|
|
77
|
+
* Every ancestor's events, root-to-leaf, bounded by the fork points below.
|
|
78
|
+
*
|
|
79
|
+
* The one traversal both file history and turn history are built on. They
|
|
80
|
+
* have to agree about where a node's history begins, or a rewind would move
|
|
81
|
+
* the files to one point and the conversation to another.
|
|
82
|
+
*/
|
|
83
|
+
export async function readNodeHistory(
|
|
84
|
+
dir: string,
|
|
85
|
+
node: number,
|
|
86
|
+
manifest?: Manifest,
|
|
87
|
+
scope: "workspace" | "context" = "workspace",
|
|
88
|
+
): Promise<NodeHistoryResult> {
|
|
89
|
+
const index = manifest ?? await readManifest(dir);
|
|
90
|
+
const chain: NodeEvents[] = [];
|
|
91
|
+
const seen = new Set<number>();
|
|
92
|
+
let current: number | null = node;
|
|
93
|
+
// Set by a child's parent pointer: this ancestor contributes only the
|
|
94
|
+
// events up to and including that one.
|
|
95
|
+
let cutoff: string | undefined;
|
|
96
|
+
let depth = 0;
|
|
97
|
+
|
|
98
|
+
while (current !== null) {
|
|
99
|
+
if (seen.has(current)) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
error: `CYCLE: parent chain from node ${node} revisits node ${current}`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
seen.add(current);
|
|
106
|
+
if (depth++ >= MAX_ANCESTRY_DEPTH) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: `TOO_DEEP: history of node ${node} exceeded ${MAX_ANCESTRY_DEPTH} nodes`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const { header, events, corrupt } = await readNode(dir, current);
|
|
114
|
+
const sealed = index.nodes.find((n) => n.node === current);
|
|
115
|
+
if (sealed && sealed.sha256 !== await digestNode(dir, current)) {
|
|
116
|
+
return { ok: false, error: `INTEGRITY: node ${current} digest differs from its sealed record` };
|
|
117
|
+
}
|
|
118
|
+
if (header === null) {
|
|
119
|
+
return { ok: false, error: `MISSING_HEADER: node ${current} has no header line` };
|
|
120
|
+
}
|
|
121
|
+
if (corrupt.length > 0) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
error: `CORRUPT: node ${current} has ${corrupt.length} unparsable line(s)`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The fork point is an event id in the node's whole stream, not a
|
|
129
|
+
// snapshot index: a turn that changed nothing writes no snapshot, so
|
|
130
|
+
// counting snapshots would land on the wrong turn.
|
|
131
|
+
let contributed = events;
|
|
132
|
+
if (cutoff !== undefined) {
|
|
133
|
+
const at = events.findIndex((e) => e.id === cutoff);
|
|
134
|
+
if (at === -1) {
|
|
135
|
+
// Refused rather than falling back to the whole node. The fallback is
|
|
136
|
+
// the silent form of the bug the cutoff exists to prevent: it would
|
|
137
|
+
// restore exactly the work the fork was made to leave behind.
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
error: `NO_SUCH_EVENT: node ${current} has no event ${cutoff} (fork point reached from ${node})`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
contributed = events.slice(0, at + 1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
chain.push({ node: current, events: contributed });
|
|
147
|
+
cutoff = header.parent?.at_event;
|
|
148
|
+
// A fresh subagent inherits files, not the parent's observations. Context
|
|
149
|
+
// history crosses compaction boundaries but stops at an explicit new root.
|
|
150
|
+
current = scope === "context" && header.basis === "none" ? null : header.parent?.node ?? null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
chain.reverse();
|
|
154
|
+
return { ok: true, nodes: chain };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Every snapshot delta from the root to `node`, in replay order.
|
|
159
|
+
*
|
|
160
|
+
* Two fields on the way up, treated oppositely, because they answer different
|
|
161
|
+
* questions:
|
|
162
|
+
*
|
|
163
|
+
* `basis` says what the MODEL should see, and is ignored here. A cut
|
|
164
|
+
* truncates the context walk; the files it wrote are still on disk.
|
|
165
|
+
*
|
|
166
|
+
* `at_event` says where this node's history BEGINS in its parent, which is
|
|
167
|
+
* a question about history, so it is obeyed. Ignoring it makes a rewind
|
|
168
|
+
* restore the very file it was asked to undo -- the parent's later turns
|
|
169
|
+
* would flow into a node that forked before them.
|
|
170
|
+
*
|
|
171
|
+
* Kept one entry per snapshot rather than merged per node: merging is lossy
|
|
172
|
+
* in exactly the direction that matters, since the turns inside a node are
|
|
173
|
+
* the ones a person is most likely to want back.
|
|
174
|
+
*/
|
|
175
|
+
export async function readFileHistory(
|
|
176
|
+
dir: string,
|
|
177
|
+
node: number,
|
|
178
|
+
manifest?: Manifest,
|
|
179
|
+
): Promise<FileHistoryResult> {
|
|
180
|
+
const history = await readNodeHistory(dir, node, manifest);
|
|
181
|
+
if (!history.ok) return { ok: false, error: history.error };
|
|
182
|
+
|
|
183
|
+
const deltas: FileDelta[] = [];
|
|
184
|
+
for (const n of history.nodes) {
|
|
185
|
+
for (const e of n.events) {
|
|
186
|
+
if (e.t !== "snapshot") continue;
|
|
187
|
+
deltas.push({
|
|
188
|
+
node: n.node, event: e.id,
|
|
189
|
+
added: { ...e.added }, changed: { ...e.changed }, removed: [...e.removed],
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { ok: true, deltas };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* What a restore would do, without doing it.
|
|
198
|
+
*
|
|
199
|
+
* Separate from the restore because applyRestore deletes whatever is not in
|
|
200
|
+
* the target -- necessary, or the workspace would end up a mixture of two
|
|
201
|
+
* states, but destructive enough that a caller has to be able to look first.
|
|
202
|
+
*/
|
|
203
|
+
export async function planRestoreToNode(
|
|
204
|
+
dir: string,
|
|
205
|
+
target: RestoreTarget,
|
|
206
|
+
manifest?: Manifest,
|
|
207
|
+
root?: string,
|
|
208
|
+
ignore: readonly string[] = DEFAULT_IGNORE,
|
|
209
|
+
): Promise<PlanResult> {
|
|
210
|
+
const { node, atEvent } = typeof target === "number" ? { node: target, atEvent: undefined } : target;
|
|
211
|
+
const history = await readNodeHistory(dir, node, manifest);
|
|
212
|
+
if (!history.ok) return { ok: false, error: history.error };
|
|
213
|
+
|
|
214
|
+
const deltas: FileDelta[] = [];
|
|
215
|
+
for (const n of history.nodes) {
|
|
216
|
+
// The cutoff is an id in the EVENT stream, not in the snapshot stream: a
|
|
217
|
+
// fork point is where a conversation was whole, which is the end of an
|
|
218
|
+
// exchange and usually not a snapshot at all. Cutting the events first
|
|
219
|
+
// and collecting snapshots after is what keeps the files and the
|
|
220
|
+
// conversation landing on the same point.
|
|
221
|
+
let events = n.events;
|
|
222
|
+
if (atEvent !== undefined && n.node === node) {
|
|
223
|
+
const at = events.findIndex((e) => e.id === atEvent);
|
|
224
|
+
if (at === -1) {
|
|
225
|
+
return { ok: false, error: `NO_SUCH_EVENT: node ${node} has no event ${atEvent}` };
|
|
226
|
+
}
|
|
227
|
+
events = events.slice(0, at + 1);
|
|
228
|
+
}
|
|
229
|
+
for (const e of events) {
|
|
230
|
+
if (e.t !== "snapshot") continue;
|
|
231
|
+
deltas.push({
|
|
232
|
+
node: n.node, event: e.id,
|
|
233
|
+
added: { ...e.added }, changed: { ...e.changed }, removed: [...e.removed],
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const files = planRestore(deltas, node);
|
|
239
|
+
const willWrite = Object.keys(files).sort();
|
|
240
|
+
if (!root) return { ok: true, files, willWrite, willDelete: [] };
|
|
241
|
+
|
|
242
|
+
const current = await scanWorkspace(root, ignore);
|
|
243
|
+
if (!current.ok) return { ok: false, error: current.error };
|
|
244
|
+
const willDelete = Object.keys(current.files).filter((p) => !(p in files)).sort();
|
|
245
|
+
return { ok: true, files, willWrite, willDelete };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Moves the workspace to the state it had at `node`.
|
|
250
|
+
*
|
|
251
|
+
* Writes nothing to the log. The log is the truth and the workspace is a
|
|
252
|
+
* projection of it, so there is nothing to record: the nodes did not change,
|
|
253
|
+
* only which one is being projected. That is also what keeps this safe
|
|
254
|
+
* despite being irreversible in itself -- the previous cursor position is
|
|
255
|
+
* still a node, so the move can be made again in the other direction.
|
|
256
|
+
*/
|
|
257
|
+
export async function restoreToNode(
|
|
258
|
+
dir: string,
|
|
259
|
+
blobDir: string,
|
|
260
|
+
root: string,
|
|
261
|
+
target: RestoreTarget,
|
|
262
|
+
manifest?: Manifest,
|
|
263
|
+
ignore: readonly string[] = DEFAULT_IGNORE,
|
|
264
|
+
): Promise<RestoreToNodeResult> {
|
|
265
|
+
const plan = await planRestoreToNode(dir, target, manifest ?? (await readManifest(dir)), root, ignore);
|
|
266
|
+
if (!plan.ok) return { ok: false, error: plan.error };
|
|
267
|
+
return { ok: true, ...(await applyRestorePlan(root, blobDir, plan.files, ignore, describeTarget(target))) };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Applies a plan that has already been computed.
|
|
272
|
+
*
|
|
273
|
+
* Split out so a caller holding a plan does not have to compute it twice.
|
|
274
|
+
* That is not only wasted work: a caller that re-planned inside the apply
|
|
275
|
+
* could never see the apply fail on its own, which turns its own "did the
|
|
276
|
+
* files move?" guard into unreachable code.
|
|
277
|
+
*/
|
|
278
|
+
export async function applyRestorePlan(
|
|
279
|
+
root: string,
|
|
280
|
+
blobDir: string,
|
|
281
|
+
files: FileSet,
|
|
282
|
+
ignore: readonly string[],
|
|
283
|
+
label: string,
|
|
284
|
+
): Promise<RestoreResult> {
|
|
285
|
+
const result = await applyRestore(root, blobDir, files, ignore);
|
|
286
|
+
if (result.unrecoverable.length > 0) return result;
|
|
287
|
+
return {
|
|
288
|
+
...result,
|
|
289
|
+
note:
|
|
290
|
+
`Workspace moved to ${label}. ` +
|
|
291
|
+
result.note +
|
|
292
|
+
" Other logged snapshots remain available. File versions never " +
|
|
293
|
+
"snapshotted cannot be reconstructed from the log.",
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function describeTarget(target: RestoreTarget): string {
|
|
298
|
+
if (typeof target === "number") return `node ${target}`;
|
|
299
|
+
return target.atEvent === undefined
|
|
300
|
+
? `node ${target.node}`
|
|
301
|
+
: `node ${target.node} as of snapshot ${target.atEvent}`;
|
|
302
|
+
}
|