handmux 0.23.0 → 0.25.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 +31 -8
- package/README.zh-CN.md +31 -8
- package/bin/handmux.js +14 -688
- package/bin/node-version.js +20 -0
- package/dist/bin/handmux.js +1015 -0
- package/dist/connectors/bridgeClient.js +385 -0
- package/dist/connectors/claude/index.js +476 -0
- package/dist/connectors/pi/bridgeClient.js +8 -0
- package/dist/connectors/pi/index.js +851 -0
- package/dist/hooks/handmux-notify.sh +64 -0
- package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
- package/dist/hooks/handmux-write.cjs +264 -0
- package/dist/package.json +127 -0
- package/dist/public/assets/index-CTrqTqtT.css +32 -0
- package/dist/public/assets/index-DmjeMSQA.js +346 -0
- package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
- package/{public → dist/public}/index.html +5 -4
- package/dist/src/agent-runtime/adapter.js +204 -0
- package/dist/src/agent-runtime/bridge.js +1006 -0
- package/dist/src/agent-runtime/bridgeStore.js +25 -0
- package/dist/src/agent-runtime/bridgeTransport.js +619 -0
- package/dist/src/agent-runtime/bridgeTypes.js +13 -0
- package/dist/src/agent-runtime/builtinRuntime.js +193 -0
- package/dist/src/agent-runtime/conversation.js +2179 -0
- package/dist/src/agent-runtime/conversationActivation.js +75 -0
- package/dist/src/agent-runtime/conversationActivity.js +69 -0
- package/dist/src/agent-runtime/conversationControls.js +206 -0
- package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
- package/dist/src/agent-runtime/conversationStore.js +352 -0
- package/dist/src/agent-runtime/conversationTypes.js +1 -0
- package/dist/src/agent-runtime/conversationValidation.js +339 -0
- package/dist/src/agent-runtime/inbox.js +973 -0
- package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
- package/dist/src/agent-runtime/inboxStore.js +25 -0
- package/dist/src/agent-runtime/inboxTypes.js +1 -0
- package/dist/src/agent-runtime/interaction.js +829 -0
- package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
- package/dist/src/agent-runtime/interactionStore.js +18 -0
- package/dist/src/agent-runtime/interactionTypes.js +1 -0
- package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
- package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
- package/dist/src/agent-runtime/resourceTypes.js +1 -0
- package/dist/src/agent-runtime/resources.js +245 -0
- package/dist/src/agent-runtime/run.js +328 -0
- package/dist/src/agent-runtime/runtime.js +793 -0
- package/dist/src/agent-runtime/sessionControl.js +164 -0
- package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
- package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
- package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
- package/dist/src/agents/claude.js +203 -0
- package/dist/src/agents/claudeConversation.js +362 -0
- package/dist/src/agents/claudeInteraction.js +134 -0
- package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
- package/dist/src/agents/codex.js +262 -0
- package/dist/src/agents/codexConversation.js +1103 -0
- package/dist/src/agents/codexConversationActivation.js +121 -0
- package/dist/src/agents/codexConversationControls.js +256 -0
- package/dist/src/agents/codexInteraction.js +306 -0
- package/dist/src/agents/codexSessionControl.js +118 -0
- package/dist/src/agents/codexSubscriptionUsage.js +313 -0
- package/dist/src/agents/codexTerminalControl.js +23 -0
- package/dist/src/agents/conversationProjectionSafety.js +416 -0
- package/dist/src/agents/index.js +39 -0
- package/dist/src/agents/nativeInbox.js +264 -0
- package/dist/src/agents/nativeInboxSources.js +115 -0
- package/dist/src/agents/pi.js +28 -0
- package/dist/src/agents/piConversation.js +523 -0
- package/dist/src/agents/piConversationHistory.js +502 -0
- package/dist/src/agents/piInboxBridge.js +368 -0
- package/dist/src/agents/piSessionControl.js +32 -0
- package/dist/src/agents/processIdentity.js +94 -0
- package/dist/src/agents/scanUtils.js +262 -0
- package/dist/src/apiAccountProviders.js +194 -0
- package/dist/src/apiAccounts.js +592 -0
- package/dist/src/apiErrors.js +86 -0
- package/{src → dist/src}/appName.js +12 -13
- package/dist/src/asr/iflyConfig.js +7 -0
- package/dist/src/asr/iflySign.js +14 -0
- package/dist/src/auth.js +27 -0
- package/dist/src/browser/bootstrap.js +72 -0
- package/dist/src/browser/cookieProfiles.js +413 -0
- package/dist/src/browser/coordinator.js +51 -0
- package/dist/src/browser/hammerheadCompat.js +104 -0
- package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
- package/dist/src/browser/manager.js +560 -0
- package/dist/src/browser/originLabel.js +15 -0
- package/dist/src/browser/profilePersistence.js +235 -0
- package/dist/src/browser/publicProxy.js +176 -0
- package/dist/src/browser/routes.js +159 -0
- package/dist/src/browser/sessionStore.js +72 -0
- package/dist/src/browser/siteVersion.js +121 -0
- package/dist/src/browser/targetPolicy.js +163 -0
- package/{src → dist/src}/browser/worker.js +14 -18
- package/dist/src/browser/workerClient.js +286 -0
- package/dist/src/browser/workerServer.js +91 -0
- package/dist/src/captureBackground.js +95 -0
- package/dist/src/claudeEvents.js +525 -0
- package/dist/src/cli/agentIntegration.js +131 -0
- package/dist/src/cli/agentLauncher.js +26 -0
- package/dist/src/cli/claudeHooks.js +290 -0
- package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
- package/dist/src/cli/cloudflared.js +106 -0
- package/dist/src/cli/codexManaged.js +80 -0
- package/{src → dist/src}/cli/cpolarUrl.js +8 -9
- package/dist/src/cli/drivers.js +98 -0
- package/dist/src/cli/hookScaffold.js +39 -0
- package/dist/src/cli/i18n/en.js +392 -0
- package/{src → dist/src}/cli/i18n/index.js +27 -25
- package/dist/src/cli/i18n/zh.js +391 -0
- package/dist/src/cli/legacyCodexHooks.js +52 -0
- package/{src → dist/src}/cli/natappUrl.js +2 -3
- package/dist/src/cli/options.js +245 -0
- package/dist/src/cli/piExtension.js +155 -0
- package/dist/src/cli/probe.js +16 -0
- package/{src → dist/src}/cli/prompt.js +4 -6
- package/dist/src/cli/pushCmd.js +102 -0
- package/{src → dist/src}/cli/qr.js +19 -19
- package/dist/src/cli/service.js +154 -0
- package/dist/src/cli/setupModel.js +194 -0
- package/dist/src/cli/setupWizard.js +647 -0
- package/dist/src/cli/shortcutEditor.js +351 -0
- package/dist/src/cli/sshTunnel.js +18 -0
- package/dist/src/cli/state.js +117 -0
- package/dist/src/cli/statusLine.js +115 -0
- package/dist/src/cli/supervisor.js +278 -0
- package/dist/src/cli/supervisorHealth.js +21 -0
- package/dist/src/cli/supervisorLaunch.js +148 -0
- package/dist/src/cli/supervisorProcesses.js +62 -0
- package/dist/src/cli/supervisorState.js +62 -0
- package/dist/src/cli/tmuxVersion.js +53 -0
- package/{src → dist/src}/cli/tunlite.js +7 -10
- package/dist/src/cli/tunnelClients.js +72 -0
- package/{src → dist/src}/cli/updateCheck.js +98 -69
- package/{src → dist/src}/cli/urlHost.js +8 -4
- package/dist/src/cli/workspaceCmd.js +347 -0
- package/dist/src/codexAppServer.js +3436 -0
- package/dist/src/codexConversationProjection.js +199 -0
- package/dist/src/codexDiff.js +118 -0
- package/dist/src/codexMessageIdentity.js +18 -0
- package/dist/src/codexPlan.js +30 -0
- package/dist/src/codexQueueProtocol.js +137 -0
- package/dist/src/codexStreamProtocol.js +207 -0
- package/dist/src/codexToolProtocol.js +68 -0
- package/dist/src/codexTranscriptParse.js +835 -0
- package/dist/src/codexUsageSnapshot.js +228 -0
- package/dist/src/config.js +8 -0
- package/{src → dist/src}/docPath.js +22 -24
- package/dist/src/docs.js +337 -0
- package/dist/src/git.js +284 -0
- package/dist/src/healthProtocol.js +97 -0
- package/dist/src/httpApi.js +96 -0
- package/dist/src/jsonStore.js +15 -0
- package/{src → dist/src}/keyNames.js +3 -3
- package/dist/src/notifications.js +111 -0
- package/dist/src/orphans.js +167 -0
- package/dist/src/paneInput.js +42 -0
- package/dist/src/pendingPrompt.js +194 -0
- package/dist/src/previewServer.js +202 -0
- package/dist/src/previews.js +179 -0
- package/dist/src/privateStateStore.js +124 -0
- package/dist/src/projectTask/backup.js +224 -0
- package/dist/src/projectTask/lock.js +108 -0
- package/dist/src/projectTask/migrations.js +90 -0
- package/dist/src/projectTask/routes.js +155 -0
- package/dist/src/projectTask/runtime.js +182 -0
- package/dist/src/projectTask/schema.js +56 -0
- package/dist/src/projectTask/store.js +457 -0
- package/dist/src/push.js +224 -0
- package/dist/src/routes/agents.js +631 -0
- package/dist/src/routes/apiAccounts.js +82 -0
- package/dist/src/routes/files.js +230 -0
- package/dist/src/routes/git.js +89 -0
- package/dist/src/routes/health.js +16 -0
- package/dist/src/routes/notifications.js +16 -0
- package/dist/src/routes/previews.js +43 -0
- package/dist/src/routes/push.js +145 -0
- package/dist/src/routes/sessions.js +244 -0
- package/dist/src/routes/system.js +220 -0
- package/dist/src/routes/terminal.js +244 -0
- package/dist/src/routes/workspace.js +267 -0
- package/dist/src/server.js +315 -0
- package/dist/src/shortcutConfig.js +83 -0
- package/{src → dist/src}/staticCache.js +5 -3
- package/{src → dist/src}/staticCompression.js +5 -6
- package/dist/src/terminalStream.js +441 -0
- package/{src → dist/src}/tmux/commands.js +185 -172
- package/dist/src/tmux/format.js +47 -0
- package/dist/src/transcriptParse.js +261 -0
- package/dist/src/transcriptReader.js +209 -0
- package/{src → dist/src}/trimCapture.js +11 -11
- package/dist/src/uploadTypes.js +31 -0
- package/dist/src/urlPolicy.js +18 -0
- package/dist/src/usagePaths.js +15 -0
- package/dist/src/workspace/agentRunner.js +301 -0
- package/dist/src/workspace/atomicJson.js +31 -0
- package/dist/src/workspace/capture.js +139 -0
- package/dist/src/workspace/checkpointer.js +228 -0
- package/dist/src/workspace/environment.js +71 -0
- package/dist/src/workspace/lock.js +137 -0
- package/dist/src/workspace/mapping.js +128 -0
- package/dist/src/workspace/operations.js +271 -0
- package/dist/src/workspace/paths.js +15 -0
- package/dist/src/workspace/planner.js +292 -0
- package/dist/src/workspace/restore.js +363 -0
- package/dist/src/workspace/runtime.js +318 -0
- package/dist/src/workspace/schema.js +156 -0
- package/dist/src/workspace/store.js +464 -0
- package/dist/src/workspace/tmuxAdapter.js +704 -0
- package/dist/src/workspaceProtocol.js +158 -0
- package/package.json +30 -17
- package/hooks/handmux-notify.sh +0 -22
- package/hooks/handmux-write.cjs +0 -78
- package/public/assets/index-C3N7OkyP.css +0 -32
- package/public/assets/index-D1DWcC5u.js +0 -337
- package/src/.gitkeep +0 -0
- package/src/agents/claude.js +0 -159
- package/src/agents/codex.js +0 -168
- package/src/agents/index.js +0 -21
- package/src/agents/processIdentity.js +0 -72
- package/src/agents/scanUtils.js +0 -225
- package/src/asr/iflyConfig.js +0 -10
- package/src/asr/iflySign.js +0 -16
- package/src/auth.js +0 -30
- package/src/browser/bootstrap.js +0 -68
- package/src/browser/cookieProfiles.js +0 -403
- package/src/browser/coordinator.js +0 -47
- package/src/browser/hammerheadCompat.js +0 -86
- package/src/browser/hammerheadRedirectCompat.js +0 -38
- package/src/browser/manager.js +0 -556
- package/src/browser/originLabel.js +0 -17
- package/src/browser/profilePersistence.js +0 -241
- package/src/browser/publicProxy.js +0 -175
- package/src/browser/routes.js +0 -154
- package/src/browser/sessionStore.js +0 -78
- package/src/browser/siteVersion.js +0 -124
- package/src/browser/targetPolicy.js +0 -137
- package/src/browser/workerClient.js +0 -253
- package/src/browser/workerServer.js +0 -98
- package/src/captureBackground.js +0 -93
- package/src/claudeEvents.js +0 -399
- package/src/cli/claudeHooks.js +0 -206
- package/src/cli/cloudflared.js +0 -99
- package/src/cli/codexManaged.js +0 -77
- package/src/cli/drivers.js +0 -97
- package/src/cli/hookScaffold.js +0 -33
- package/src/cli/i18n/en.js +0 -385
- package/src/cli/i18n/zh.js +0 -384
- package/src/cli/legacyCodexHooks.js +0 -49
- package/src/cli/options.js +0 -211
- package/src/cli/probe.js +0 -16
- package/src/cli/pushCmd.js +0 -60
- package/src/cli/service.js +0 -126
- package/src/cli/setupModel.js +0 -154
- package/src/cli/setupWizard.js +0 -506
- package/src/cli/shortcutEditor.js +0 -288
- package/src/cli/sshTunnel.js +0 -12
- package/src/cli/state.js +0 -90
- package/src/cli/statusLine.js +0 -90
- package/src/cli/supervisor.js +0 -183
- package/src/cli/supervisorProcesses.js +0 -57
- package/src/cli/tmuxVersion.js +0 -49
- package/src/cli/tunnelClients.js +0 -62
- package/src/cli/workspaceCmd.js +0 -300
- package/src/codexAppServer.js +0 -1620
- package/src/codexDiff.js +0 -108
- package/src/codexTranscriptParse.js +0 -413
- package/src/codexUsageSnapshot.cjs +0 -129
- package/src/config.js +0 -9
- package/src/docs.js +0 -228
- package/src/git.js +0 -208
- package/src/httpApi.js +0 -72
- package/src/jsonStore.js +0 -24
- package/src/notifications.js +0 -68
- package/src/orphans.js +0 -152
- package/src/pendingPrompt.js +0 -163
- package/src/previewServer.js +0 -168
- package/src/previews.js +0 -116
- package/src/push.js +0 -183
- package/src/routes/codex.js +0 -473
- package/src/routes/files.js +0 -172
- package/src/routes/git.js +0 -57
- package/src/routes/notifications.js +0 -19
- package/src/routes/previews.js +0 -35
- package/src/routes/push.js +0 -119
- package/src/routes/sessions.js +0 -168
- package/src/routes/system.js +0 -132
- package/src/routes/terminal.js +0 -210
- package/src/routes/transcript.js +0 -171
- package/src/routes/workspace.js +0 -267
- package/src/server.js +0 -186
- package/src/shortcutConfig.js +0 -75
- package/src/terminalStream.js +0 -391
- package/src/tmux/format.js +0 -45
- package/src/transcriptParse.js +0 -180
- package/src/transcriptReader.js +0 -101
- package/src/uploadTypes.js +0 -30
- package/src/urlPolicy.js +0 -14
- package/src/usage.js +0 -260
- package/src/workspace/agentRunner.js +0 -188
- package/src/workspace/atomicJson.js +0 -32
- package/src/workspace/capture.js +0 -139
- package/src/workspace/checkpointer.js +0 -187
- package/src/workspace/environment.js +0 -75
- package/src/workspace/lock.js +0 -128
- package/src/workspace/mapping.js +0 -124
- package/src/workspace/operations.js +0 -252
- package/src/workspace/paths.js +0 -16
- package/src/workspace/planner.js +0 -247
- package/src/workspace/restore.js +0 -279
- package/src/workspace/runtime.js +0 -314
- package/src/workspace/schema.js +0 -136
- package/src/workspace/store.js +0 -382
- package/src/workspace/tmuxAdapter.js +0 -554
- /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
- /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
- /package/{public → dist/public}/icons/badge-96.png +0 -0
- /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-192.png +0 -0
- /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-512.png +0 -0
- /package/{public → dist/public}/icons/logo.svg +0 -0
- /package/{public → dist/public}/manifest.webmanifest +0 -0
- /package/{public → dist/public}/offline.html +0 -0
- /package/{public → dist/public}/sw.js +0 -0
- /package/{src → dist/src}/browser/protocol.js +0 -0
- /package/{src → dist/src}/cli/cfNamed.js +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { captureWorkspace } from './capture.js';
|
|
2
|
+
import { detectEnvironmentChange } from './environment.js';
|
|
3
|
+
import { fingerprintSnapshot } from './schema.js';
|
|
4
|
+
function sameSnapshot(left, right) {
|
|
5
|
+
return fingerprintSnapshot(left) === fingerprintSnapshot(right);
|
|
6
|
+
}
|
|
7
|
+
function snapshotEnvironment(observed) {
|
|
8
|
+
return {
|
|
9
|
+
id: observed.id,
|
|
10
|
+
bootIdentity: observed.bootIdentity,
|
|
11
|
+
tmuxServerId: observed.tmuxServerId,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
async function reconcileOnce(deps, cause) {
|
|
15
|
+
const handle = await deps.lock.tryAcquire({ operationId: `checkpointer:${cause}` });
|
|
16
|
+
if (!handle)
|
|
17
|
+
return { status: 'locked' };
|
|
18
|
+
try {
|
|
19
|
+
const observed = await deps.observeEnvironment();
|
|
20
|
+
if (!observed || observed.status === 'unknown')
|
|
21
|
+
return { status: 'unknown' };
|
|
22
|
+
const live = await deps.store.readLive();
|
|
23
|
+
if (live.status === 'corrupt')
|
|
24
|
+
return live;
|
|
25
|
+
const previous = live.status === 'ok' ? live.value.environment : null;
|
|
26
|
+
let change = detectEnvironmentChange(previous, observed);
|
|
27
|
+
if (cause === 'confirmed-empty' && observed.status === 'absent') {
|
|
28
|
+
change = { status: 'same', reason: 'same', current: observed };
|
|
29
|
+
}
|
|
30
|
+
if (change.status === 'unknown')
|
|
31
|
+
return change;
|
|
32
|
+
// An absent tmux server only ends a known live generation. An already-empty generation remains
|
|
33
|
+
// unknown unless a Handmux deletion explicitly confirms it.
|
|
34
|
+
if (observed.status === 'absent' && cause !== 'confirmed-empty' && change.status !== 'changed') {
|
|
35
|
+
return { status: 'unknown' };
|
|
36
|
+
}
|
|
37
|
+
const current = 'current' in change ? change.current : observed;
|
|
38
|
+
const captured = await deps.capture(snapshotEnvironment(current));
|
|
39
|
+
if (captured.status === 'changed-during-capture') {
|
|
40
|
+
deps.scheduleRetry?.();
|
|
41
|
+
return captured;
|
|
42
|
+
}
|
|
43
|
+
if (captured.status !== 'ok' && captured.status !== 'empty')
|
|
44
|
+
return captured;
|
|
45
|
+
if (captured.status === 'empty' && cause !== 'confirmed-empty' && observed.status !== 'present' && change.status !== 'changed') {
|
|
46
|
+
return { status: 'unknown' };
|
|
47
|
+
}
|
|
48
|
+
if (change.status === 'changed') {
|
|
49
|
+
const archived = await deps.store.archiveEnvironment({
|
|
50
|
+
endedReason: change.reason,
|
|
51
|
+
detectedAt: new Date(deps.now()).toISOString(),
|
|
52
|
+
});
|
|
53
|
+
if (archived.status !== 'ok' && archived.status !== 'empty')
|
|
54
|
+
return archived;
|
|
55
|
+
}
|
|
56
|
+
if (live.status === 'ok' && sameSnapshot(live.value, captured.snapshot))
|
|
57
|
+
return { status: 'unchanged' };
|
|
58
|
+
await deps.store.writeLive(captured.snapshot);
|
|
59
|
+
return { status: 'written', snapshot: captured.snapshot };
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await handle.release();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function createCheckpointer({ setInterval = globalThis.setInterval, clearInterval = globalThis.clearInterval, setTimeout = globalThis.setTimeout, clearTimeout = globalThis.clearTimeout, now = Date.now, ...rest }) {
|
|
66
|
+
const deps = {
|
|
67
|
+
...rest, now, setInterval, clearInterval, setTimeout, clearTimeout,
|
|
68
|
+
};
|
|
69
|
+
let interval = null;
|
|
70
|
+
let debounce = null;
|
|
71
|
+
let running = null;
|
|
72
|
+
let pendingConfirmation = null;
|
|
73
|
+
let stopping = false;
|
|
74
|
+
let stopped = false;
|
|
75
|
+
let stopPromise = null;
|
|
76
|
+
let lastResult = null;
|
|
77
|
+
let lastError = null;
|
|
78
|
+
function launch(cause) {
|
|
79
|
+
const current = reconcileOnce(deps, cause);
|
|
80
|
+
running = current;
|
|
81
|
+
const settled = () => {
|
|
82
|
+
if (running !== current)
|
|
83
|
+
return;
|
|
84
|
+
running = null;
|
|
85
|
+
if (pendingConfirmation) {
|
|
86
|
+
const pending = pendingConfirmation;
|
|
87
|
+
pendingConfirmation = null;
|
|
88
|
+
launch('confirmed-empty').then(pending.resolve, pending.reject);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
current.then((result) => {
|
|
92
|
+
lastResult = result;
|
|
93
|
+
lastError = null;
|
|
94
|
+
settled();
|
|
95
|
+
}, () => {
|
|
96
|
+
lastError = 'workspace-reconcile-failed';
|
|
97
|
+
settled();
|
|
98
|
+
});
|
|
99
|
+
return current;
|
|
100
|
+
}
|
|
101
|
+
function confirmEmpty() {
|
|
102
|
+
if (stopping || stopped)
|
|
103
|
+
return Promise.resolve({ status: 'stopped' });
|
|
104
|
+
if (!running)
|
|
105
|
+
return launch('confirmed-empty');
|
|
106
|
+
if (!pendingConfirmation) {
|
|
107
|
+
let resolve;
|
|
108
|
+
let reject;
|
|
109
|
+
const promise = new Promise((onResolve, onReject) => { resolve = onResolve; reject = onReject; });
|
|
110
|
+
pendingConfirmation = { promise, resolve, reject };
|
|
111
|
+
}
|
|
112
|
+
return pendingConfirmation.promise;
|
|
113
|
+
}
|
|
114
|
+
function reconcile(cause = 'timer') {
|
|
115
|
+
if (cause === 'confirmed-empty')
|
|
116
|
+
return confirmEmpty();
|
|
117
|
+
if (stopping || stopped)
|
|
118
|
+
return Promise.resolve({ status: 'stopped' });
|
|
119
|
+
return running || launch(cause);
|
|
120
|
+
}
|
|
121
|
+
function requestReconcile() {
|
|
122
|
+
if (stopping || stopped)
|
|
123
|
+
return;
|
|
124
|
+
clearTimeout(debounce);
|
|
125
|
+
debounce = setTimeout(() => {
|
|
126
|
+
debounce = null;
|
|
127
|
+
reconcile('event').catch(() => { });
|
|
128
|
+
}, 2_000);
|
|
129
|
+
}
|
|
130
|
+
deps.scheduleRetry = requestReconcile;
|
|
131
|
+
const outstanding = () => (running ?? pendingConfirmation?.promise ?? null);
|
|
132
|
+
return {
|
|
133
|
+
reconcile,
|
|
134
|
+
health() {
|
|
135
|
+
if (stopping || stopped)
|
|
136
|
+
return { status: 'degraded', detail: 'workspace-stopped' };
|
|
137
|
+
if (lastError)
|
|
138
|
+
return { status: 'degraded', detail: lastError };
|
|
139
|
+
if (!lastResult)
|
|
140
|
+
return { status: 'starting', detail: 'workspace-reconcile-starting' };
|
|
141
|
+
if (lastResult.status === 'written' || lastResult.status === 'unchanged') {
|
|
142
|
+
return { status: 'ready', detail: null };
|
|
143
|
+
}
|
|
144
|
+
if (lastResult.status === 'locked') {
|
|
145
|
+
return { status: 'ready', detail: 'workspace-writer-locked' };
|
|
146
|
+
}
|
|
147
|
+
return { status: 'degraded', detail: `workspace-${lastResult.status || 'unknown'}` };
|
|
148
|
+
},
|
|
149
|
+
start() {
|
|
150
|
+
if (stopping || stopped)
|
|
151
|
+
return Promise.resolve({ status: 'stopped' });
|
|
152
|
+
if (!interval)
|
|
153
|
+
interval = setInterval(() => { reconcile('timer').catch(() => { }); }, 60_000);
|
|
154
|
+
return reconcile('start');
|
|
155
|
+
},
|
|
156
|
+
requestReconcile,
|
|
157
|
+
confirmEmpty,
|
|
158
|
+
stop() {
|
|
159
|
+
if (stopPromise)
|
|
160
|
+
return stopPromise;
|
|
161
|
+
stopping = true;
|
|
162
|
+
stopPromise = (async () => {
|
|
163
|
+
if (interval)
|
|
164
|
+
clearInterval(interval);
|
|
165
|
+
if (debounce)
|
|
166
|
+
clearTimeout(debounce);
|
|
167
|
+
interval = null;
|
|
168
|
+
debounce = null;
|
|
169
|
+
let pending = outstanding();
|
|
170
|
+
while (pending) {
|
|
171
|
+
await pending.catch(() => { });
|
|
172
|
+
pending = outstanding();
|
|
173
|
+
}
|
|
174
|
+
await launch('shutdown').catch(() => { });
|
|
175
|
+
pending = outstanding();
|
|
176
|
+
while (pending) {
|
|
177
|
+
await pending.catch(() => { });
|
|
178
|
+
pending = outstanding();
|
|
179
|
+
}
|
|
180
|
+
stopped = true;
|
|
181
|
+
})();
|
|
182
|
+
return stopPromise;
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export function createWorkspaceBackground({ store, tmux, observeEnvironment, lock, stateFile, getCodexApp = () => null, codexSessions, now = Date.now, }) {
|
|
187
|
+
return createCheckpointer({
|
|
188
|
+
store,
|
|
189
|
+
observeEnvironment,
|
|
190
|
+
lock,
|
|
191
|
+
now,
|
|
192
|
+
capture: (environment) => {
|
|
193
|
+
const codexApp = getCodexApp();
|
|
194
|
+
return captureWorkspace({
|
|
195
|
+
tmux, stateFile, environment,
|
|
196
|
+
...(codexApp ? { codexApp } : {}),
|
|
197
|
+
...(codexSessions ? { codexSessions } : {}),
|
|
198
|
+
now,
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
export function createGracefulShutdown({ events, workspace, browser, server }) {
|
|
204
|
+
let closing = null;
|
|
205
|
+
return function shutdown() {
|
|
206
|
+
if (!closing) {
|
|
207
|
+
closing = (async () => {
|
|
208
|
+
try {
|
|
209
|
+
await events.stop();
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
try {
|
|
213
|
+
await workspace.stop();
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
try {
|
|
217
|
+
await browser?.close();
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
server.close();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
})();
|
|
225
|
+
}
|
|
226
|
+
return closing;
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
function normalizeMacBootTime(value) {
|
|
7
|
+
const seconds = String(value).match(/\bsec\s*=\s*(\d+)\b/)?.[1];
|
|
8
|
+
return seconds && seconds !== '0' ? seconds : null;
|
|
9
|
+
}
|
|
10
|
+
function environmentId(bootIdentity, tmuxServerId) {
|
|
11
|
+
return crypto
|
|
12
|
+
.createHash('sha256')
|
|
13
|
+
.update(`${bootIdentity}\0${tmuxServerId || 'no-tmux'}`)
|
|
14
|
+
.digest('hex');
|
|
15
|
+
}
|
|
16
|
+
export function detectEnvironmentChange(previous, observed) {
|
|
17
|
+
if (!observed || observed.status === 'unknown')
|
|
18
|
+
return { status: 'unknown' };
|
|
19
|
+
if (!previous)
|
|
20
|
+
return { status: 'initial', current: observed };
|
|
21
|
+
if (previous.bootIdentity !== observed.bootIdentity)
|
|
22
|
+
return { status: 'changed', reason: 'boot-changed', current: observed };
|
|
23
|
+
if (observed.status === 'absent') {
|
|
24
|
+
return previous.tmuxServerId
|
|
25
|
+
? { status: 'changed', reason: 'tmux-changed', current: observed }
|
|
26
|
+
: { status: 'unknown' };
|
|
27
|
+
}
|
|
28
|
+
if (!previous.tmuxServerId)
|
|
29
|
+
return { status: 'attached', reason: 'same', current: observed };
|
|
30
|
+
if (previous.tmuxServerId !== observed.tmuxServerId)
|
|
31
|
+
return { status: 'changed', reason: 'tmux-changed', current: observed };
|
|
32
|
+
return { status: 'same', reason: 'same', current: observed };
|
|
33
|
+
}
|
|
34
|
+
export function createBootIdentityProvider({ platform = process.platform, readFile = fsp.readFile, exec = execFileAsync, } = {}) {
|
|
35
|
+
return async () => {
|
|
36
|
+
try {
|
|
37
|
+
if (platform === 'linux') {
|
|
38
|
+
const identity = String(await readFile('/proc/sys/kernel/random/boot_id', 'utf8')).trim();
|
|
39
|
+
return identity || null;
|
|
40
|
+
}
|
|
41
|
+
if (platform === 'darwin') {
|
|
42
|
+
return normalizeMacBootTime((await exec('sysctl', ['-n', 'kern.boottime'])).stdout);
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function createEnvironmentProvider({ bootIdentityProvider = createBootIdentityProvider(), tmuxServerIdProvider, } = {}) {
|
|
52
|
+
return async () => {
|
|
53
|
+
try {
|
|
54
|
+
const bootIdentity = await bootIdentityProvider();
|
|
55
|
+
if (typeof bootIdentity !== 'string' || !bootIdentity)
|
|
56
|
+
return { status: 'unknown' };
|
|
57
|
+
const tmuxServerId = await tmuxServerIdProvider?.();
|
|
58
|
+
if (tmuxServerId !== null && (typeof tmuxServerId !== 'string' || !tmuxServerId))
|
|
59
|
+
return { status: 'unknown' };
|
|
60
|
+
return {
|
|
61
|
+
status: tmuxServerId === null ? 'absent' : 'present',
|
|
62
|
+
id: environmentId(bootIdentity, tmuxServerId),
|
|
63
|
+
bootIdentity,
|
|
64
|
+
tmuxServerId,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return { status: 'unknown' };
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
const OWNER_FILE = 'owner.json';
|
|
5
|
+
const SAFE_TOKEN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
6
|
+
const errorCode = (error) => (error && typeof error === 'object' && 'code' in error && typeof error.code === 'string'
|
|
7
|
+
? error.code : undefined);
|
|
8
|
+
const asRecord = (value) => (value && typeof value === 'object' && !Array.isArray(value)
|
|
9
|
+
? value : null);
|
|
10
|
+
function defaultProcessAlive(pid) {
|
|
11
|
+
try {
|
|
12
|
+
process.kill(pid, 0);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
return errorCode(error) === 'EPERM';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
+
function ownerLabel(value) {
|
|
21
|
+
const owner = asRecord(value);
|
|
22
|
+
if (!owner)
|
|
23
|
+
return 'unknown owner';
|
|
24
|
+
const operation = typeof owner.operationId === 'string' && owner.operationId ? owner.operationId : 'unknown operation';
|
|
25
|
+
const pid = Number.isInteger(owner.pid) ? owner.pid : 'unknown';
|
|
26
|
+
return `${operation} (pid ${pid})`;
|
|
27
|
+
}
|
|
28
|
+
export function createWorkspaceLock({ dir, fs = fsp, pid = process.pid, now = Date.now, isProcessAlive = defaultProcessAlive, wait = delay, timeoutMs = 5_000, retryMs = 50, staleGraceMs = 5_000, randomUUID = crypto.randomUUID, }) {
|
|
29
|
+
if (typeof dir !== 'string' || !dir)
|
|
30
|
+
throw new Error('workspace lock directory is required');
|
|
31
|
+
async function readOwner() {
|
|
32
|
+
let stat = null;
|
|
33
|
+
try {
|
|
34
|
+
stat = await fs.stat(dir);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (errorCode(error) === 'ENOENT')
|
|
38
|
+
return null;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const value = JSON.parse(await fs.readFile(path.join(dir, OWNER_FILE), 'utf8'));
|
|
43
|
+
return { value, mtimeMs: stat.mtimeMs };
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return { value: null, mtimeMs: stat.mtimeMs };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function reclaimIfStale(record) {
|
|
50
|
+
if (!record)
|
|
51
|
+
return true;
|
|
52
|
+
const owner = asRecord(record.value);
|
|
53
|
+
if (!owner || typeof owner.token !== 'string' || !SAFE_TOKEN.test(owner.token))
|
|
54
|
+
return false;
|
|
55
|
+
const startedAt = typeof owner.startedAt === 'string' ? Date.parse(owner.startedAt) : Number.NaN;
|
|
56
|
+
const age = now() - (Number.isFinite(startedAt) ? startedAt : record.mtimeMs);
|
|
57
|
+
if (age < staleGraceMs)
|
|
58
|
+
return false;
|
|
59
|
+
if (typeof owner.pid === 'number' && Number.isInteger(owner.pid) && await isProcessAlive(owner.pid))
|
|
60
|
+
return false;
|
|
61
|
+
// Every contender that observed this owner uses the SAME destination. The winner leaves the renamed
|
|
62
|
+
// directory as a tombstone, so a loser cannot later rename a newly-created lock out of the way.
|
|
63
|
+
const staleDir = `${dir}.stale.${owner.token}`;
|
|
64
|
+
try {
|
|
65
|
+
await fs.rename(dir, staleDir);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (errorCode(error) === 'ENOENT')
|
|
69
|
+
return true;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
async function tryAcquire({ operationId = 'workspace-writer' } = {}) {
|
|
75
|
+
await fs.mkdir(path.dirname(dir), { recursive: true, mode: 0o700 });
|
|
76
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
77
|
+
const token = randomUUID();
|
|
78
|
+
if (!SAFE_TOKEN.test(token))
|
|
79
|
+
throw new Error('workspace lock token must be a UUID');
|
|
80
|
+
const owner = { pid, startedAt: new Date(now()).toISOString(), operationId, token };
|
|
81
|
+
try {
|
|
82
|
+
await fs.mkdir(dir, { mode: 0o700 });
|
|
83
|
+
try {
|
|
84
|
+
await fs.writeFile(path.join(dir, OWNER_FILE), `${JSON.stringify(owner)}\n`, { mode: 0o600, flag: 'wx' });
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
let released = false;
|
|
91
|
+
return {
|
|
92
|
+
owner,
|
|
93
|
+
async release() {
|
|
94
|
+
if (released)
|
|
95
|
+
return;
|
|
96
|
+
released = true;
|
|
97
|
+
const current = await readOwner();
|
|
98
|
+
if (asRecord(current?.value)?.token === token)
|
|
99
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (errorCode(error) !== 'EEXIST')
|
|
105
|
+
throw error;
|
|
106
|
+
const record = await readOwner();
|
|
107
|
+
if (!await reclaimIfStale(record))
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
async function acquire(owner = {}, options = {}) {
|
|
114
|
+
const limit = options.timeoutMs ?? timeoutMs;
|
|
115
|
+
const started = now();
|
|
116
|
+
while (true) {
|
|
117
|
+
const handle = await tryAcquire(owner);
|
|
118
|
+
if (handle)
|
|
119
|
+
return handle;
|
|
120
|
+
const current = await readOwner();
|
|
121
|
+
if (now() - started >= limit) {
|
|
122
|
+
throw new Error(`workspace writer lock timed out; held by ${ownerLabel(current?.value)}`);
|
|
123
|
+
}
|
|
124
|
+
await wait(options.retryMs ?? retryMs);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function withLock(owner, fn, options) {
|
|
128
|
+
const handle = await acquire(owner, options);
|
|
129
|
+
try {
|
|
130
|
+
return await fn(handle.owner);
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
await handle.release();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { tryAcquire, acquire, withLock, readOwner: async () => (await readOwner())?.value ?? null };
|
|
137
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
const HASH = /^[0-9a-f]{64}$/;
|
|
3
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
const KINDS = ['sessions', 'windows', 'panes'];
|
|
5
|
+
const RUNTIME_ID = {
|
|
6
|
+
sessions: /^\$\d+$/,
|
|
7
|
+
windows: /^@\d+$/,
|
|
8
|
+
panes: /^%\d+$/,
|
|
9
|
+
};
|
|
10
|
+
function fail(message) {
|
|
11
|
+
throw new Error(`invalid recovery mapping: ${message}`);
|
|
12
|
+
}
|
|
13
|
+
function requireExactObject(value, fields, label) {
|
|
14
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
15
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
|
|
16
|
+
fail(`${label} must be a plain object`);
|
|
17
|
+
const keys = Object.keys(value);
|
|
18
|
+
if (keys.length !== fields.length || fields.some((field) => !keys.includes(field)))
|
|
19
|
+
fail(`${label} fields are invalid`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function validateRecord(value, label, keyPattern, valuePattern) {
|
|
23
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
24
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value)))
|
|
25
|
+
fail(`${label} must be a plain object`);
|
|
26
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
27
|
+
if (!keyPattern.test(key) || typeof entry !== 'string' || !valuePattern.test(entry))
|
|
28
|
+
fail(`${label} entry is invalid`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function sorted(value) {
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
return value.map(sorted);
|
|
34
|
+
if (!value || typeof value !== 'object')
|
|
35
|
+
return value;
|
|
36
|
+
const record = value;
|
|
37
|
+
return Object.fromEntries(Object.keys(record).sort().map((key) => [key, sorted(record[key])]));
|
|
38
|
+
}
|
|
39
|
+
function payload(mapping) {
|
|
40
|
+
return {
|
|
41
|
+
checkpointId: mapping.checkpointId,
|
|
42
|
+
names: mapping.names,
|
|
43
|
+
runtime: mapping.runtime,
|
|
44
|
+
logical: mapping.logical,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function mappingId(mapping) {
|
|
48
|
+
return crypto.createHash('sha256').update(JSON.stringify(sorted(payload(mapping)))).digest('hex');
|
|
49
|
+
}
|
|
50
|
+
function blankMapping(checkpointId) {
|
|
51
|
+
return {
|
|
52
|
+
checkpointId,
|
|
53
|
+
names: {},
|
|
54
|
+
runtime: { sessions: {}, windows: {}, panes: {} },
|
|
55
|
+
logical: { sessions: {}, windows: {}, panes: {} },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function validatePayload(mapping, checkpointId) {
|
|
59
|
+
if (typeof mapping.checkpointId !== 'string' || !mapping.checkpointId || mapping.checkpointId !== checkpointId) {
|
|
60
|
+
fail('checkpoint id mismatch');
|
|
61
|
+
}
|
|
62
|
+
if (!mapping.names || typeof mapping.names !== 'object' || Array.isArray(mapping.names)
|
|
63
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(mapping.names)))
|
|
64
|
+
fail('names must be a plain object');
|
|
65
|
+
for (const [source, target] of Object.entries(mapping.names)) {
|
|
66
|
+
if (!source || ['__proto__', 'constructor', 'prototype'].includes(source) || typeof target !== 'string' || !target)
|
|
67
|
+
fail('name entry is invalid');
|
|
68
|
+
}
|
|
69
|
+
for (const group of ['runtime', 'logical']) {
|
|
70
|
+
requireExactObject(mapping[group], ['sessions', 'windows', 'panes'], group);
|
|
71
|
+
}
|
|
72
|
+
const runtime = mapping.runtime;
|
|
73
|
+
const logical = mapping.logical;
|
|
74
|
+
for (const kind of KINDS) {
|
|
75
|
+
validateRecord(runtime[kind], `runtime.${kind}`, RUNTIME_ID[kind], RUNTIME_ID[kind]);
|
|
76
|
+
validateRecord(logical[kind], `logical.${kind}`, UUID, RUNTIME_ID[kind]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function validateRecoveryMapping(mapping, checkpointId) {
|
|
80
|
+
const record = requireExactObject(mapping, ['id', 'checkpointId', 'restoredAt', 'names', 'runtime', 'logical'], 'mapping');
|
|
81
|
+
if (typeof record.id !== 'string' || !HASH.test(record.id))
|
|
82
|
+
fail('id is invalid');
|
|
83
|
+
if (typeof record.restoredAt !== 'string' || Number.isNaN(Date.parse(record.restoredAt)))
|
|
84
|
+
fail('restoredAt is invalid');
|
|
85
|
+
validatePayload(record, checkpointId);
|
|
86
|
+
if (record.id !== mappingId(record))
|
|
87
|
+
fail('hash mismatch');
|
|
88
|
+
return record;
|
|
89
|
+
}
|
|
90
|
+
function merge(target, source) {
|
|
91
|
+
Object.assign(target.names, source.names);
|
|
92
|
+
for (const kind of KINDS) {
|
|
93
|
+
Object.assign(target.runtime[kind], source.runtime[kind]);
|
|
94
|
+
Object.assign(target.logical[kind], source.logical[kind]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function validateAddition(value, checkpointId) {
|
|
98
|
+
const record = requireExactObject(value, ['names', 'runtime', 'logical'], 'mapping addition');
|
|
99
|
+
const candidate = { checkpointId, ...record };
|
|
100
|
+
validatePayload(candidate, checkpointId);
|
|
101
|
+
return candidate;
|
|
102
|
+
}
|
|
103
|
+
function hasValues(mapping) {
|
|
104
|
+
return Object.keys(mapping.names).length > 0
|
|
105
|
+
|| KINDS.some((kind) => (Object.keys(mapping.runtime[kind]).length > 0 || Object.keys(mapping.logical[kind]).length > 0));
|
|
106
|
+
}
|
|
107
|
+
export function buildRecoveryMapping(checkpointId, previous, additions, now = Date.now) {
|
|
108
|
+
const combined = blankMapping(checkpointId);
|
|
109
|
+
if (previous)
|
|
110
|
+
merge(combined, validateRecoveryMapping(previous, checkpointId));
|
|
111
|
+
if (!Array.isArray(additions))
|
|
112
|
+
fail('additions must be an array');
|
|
113
|
+
for (const addition of additions) {
|
|
114
|
+
if (addition !== null && addition !== undefined)
|
|
115
|
+
merge(combined, validateAddition(addition, checkpointId));
|
|
116
|
+
}
|
|
117
|
+
if (!hasValues(combined))
|
|
118
|
+
return null;
|
|
119
|
+
const mapping = {
|
|
120
|
+
id: mappingId(combined),
|
|
121
|
+
checkpointId,
|
|
122
|
+
restoredAt: new Date(now()).toISOString(),
|
|
123
|
+
names: combined.names,
|
|
124
|
+
runtime: combined.runtime,
|
|
125
|
+
logical: combined.logical,
|
|
126
|
+
};
|
|
127
|
+
return validateRecoveryMapping(mapping, checkpointId);
|
|
128
|
+
}
|