handmux 0.22.1 → 0.24.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 +3 -3
- package/README.zh-CN.md +3 -3
- package/bin/handmux.js +3 -688
- package/dist/bin/handmux.js +850 -0
- package/{hooks → dist/hooks}/handmux-notify.sh +2 -4
- package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
- package/{hooks → dist/hooks}/handmux-write.cjs +25 -26
- package/dist/package.json +126 -0
- package/dist/public/assets/index-CicdY9E9.css +32 -0
- package/dist/public/assets/index-D4VP5t8h.js +337 -0
- package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
- package/{public → dist/public}/index.html +5 -4
- package/{src → dist/src}/agents/claude.js +106 -82
- package/dist/src/agents/codex.js +217 -0
- package/{src → dist/src}/agents/index.js +9 -10
- package/dist/src/agents/processIdentity.js +80 -0
- package/dist/src/agents/scanUtils.js +262 -0
- package/dist/src/apiErrors.js +77 -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 +499 -0
- package/{src → dist/src}/cli/claudeHooks.js +146 -108
- package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
- package/dist/src/cli/cloudflared.js +106 -0
- package/dist/src/cli/codexManaged.js +77 -0
- package/{src → dist/src}/cli/cpolarUrl.js +8 -9
- package/dist/src/cli/drivers.js +98 -0
- package/dist/src/cli/hookScaffold.js +36 -0
- package/dist/src/cli/i18n/en.js +366 -0
- package/{src → dist/src}/cli/i18n/index.js +27 -25
- package/dist/src/cli/i18n/zh.js +365 -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/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 +144 -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 +114 -0
- package/{src → dist/src}/cli/statusLine.js +63 -46
- 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 +2299 -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 +96 -0
- package/dist/src/codexStreamProtocol.js +207 -0
- package/dist/src/codexToolProtocol.js +68 -0
- package/dist/src/codexTranscriptParse.js +724 -0
- package/dist/src/codexUsageSnapshot.js +205 -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 +272 -0
- package/dist/src/healthProtocol.js +97 -0
- package/dist/src/httpApi.js +68 -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 +157 -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 +89 -0
- package/dist/src/push.js +224 -0
- package/dist/src/routes/codex.js +606 -0
- package/dist/src/routes/files.js +230 -0
- package/dist/src/routes/git.js +78 -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 +232 -0
- package/dist/src/routes/system.js +156 -0
- package/dist/src/routes/terminal.js +238 -0
- package/dist/src/routes/transcript.js +197 -0
- package/dist/src/routes/workspace.js +267 -0
- package/{src → dist/src}/server.js +110 -76
- 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 +160 -167
- package/dist/src/tmux/format.js +47 -0
- package/dist/src/transcriptParse.js +240 -0
- package/dist/src/transcriptReader.js +113 -0
- package/{src → dist/src}/trimCapture.js +11 -11
- package/dist/src/uploadTypes.js +29 -0
- package/dist/src/urlPolicy.js +18 -0
- package/dist/src/usage.js +260 -0
- package/dist/src/workspace/agentRunner.js +204 -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 +314 -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 +694 -0
- package/dist/src/workspaceProtocol.js +158 -0
- package/package.json +28 -16
- package/hooks/handmux-codex-usage.cjs +0 -141
- package/public/assets/index-ClOByNCn.js +0 -335
- package/public/assets/index-rjrtUwoJ.css +0 -32
- package/src/.gitkeep +0 -0
- package/src/agents/codex.js +0 -121
- 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 -356
- package/src/cli/cloudflared.js +0 -99
- package/src/cli/codexHooks.js +0 -123
- package/src/cli/drivers.js +0 -97
- package/src/cli/hookScaffold.js +0 -35
- package/src/cli/i18n/en.js +0 -388
- package/src/cli/i18n/zh.js +0 -387
- 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/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/config.js +0 -9
- package/src/docs.js +0 -228
- package/src/git.js +0 -208
- package/src/httpApi.js +0 -69
- package/src/jsonStore.js +0 -24
- package/src/notifications.js +0 -68
- package/src/orphans.js +0 -150
- 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/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 -163
- package/src/routes/system.js +0 -145
- package/src/routes/terminal.js +0 -210
- package/src/routes/transcript.js +0 -115
- package/src/routes/workspace.js +0 -267
- 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 -261
- package/src/workspace/agentRunner.js +0 -185
- package/src/workspace/atomicJson.js +0 -32
- package/src/workspace/capture.js +0 -87
- package/src/workspace/checkpointer.js +0 -183
- 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
package/src/workspace/restore.js
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import fsp from 'node:fs/promises';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import { AGENTS } from '../agents/index.js';
|
|
4
|
-
|
|
5
|
-
function message(error) {
|
|
6
|
-
return error instanceof Error ? error.message : String(error);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function windowMap(checkpoint) {
|
|
10
|
-
return new Map(checkpoint.windows.map((window) => [window.id, window]));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function sessionMap(checkpoint) {
|
|
14
|
-
return new Map(checkpoint.sessions.map((session) => [session.id, session]));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function emptyMapping() {
|
|
18
|
-
return {
|
|
19
|
-
names: {},
|
|
20
|
-
runtime: { sessions: {}, windows: {}, panes: {} },
|
|
21
|
-
logical: { sessions: {}, windows: {}, panes: {} },
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function mergeMapping(target, source) {
|
|
26
|
-
Object.assign(target.names, source.names);
|
|
27
|
-
for (const kind of ['sessions', 'windows', 'panes']) {
|
|
28
|
-
Object.assign(target.runtime[kind], source.runtime[kind]);
|
|
29
|
-
Object.assign(target.logical[kind], source.logical[kind]);
|
|
30
|
-
}
|
|
31
|
-
return target;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function mapRuntime(mapping, kind, source, logical, actual) {
|
|
35
|
-
if (typeof source === 'string' && source) mapping.runtime[kind][source] = actual;
|
|
36
|
-
if (typeof logical === 'string' && logical) mapping.logical[kind][logical] = actual;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function usableCwd(cwd, { access, home, warnings }) {
|
|
40
|
-
if (typeof cwd === 'string' && cwd) {
|
|
41
|
-
try {
|
|
42
|
-
await access(cwd);
|
|
43
|
-
return cwd;
|
|
44
|
-
} catch { /* fall through */ }
|
|
45
|
-
}
|
|
46
|
-
warnings.push(`cwd ${typeof cwd === 'string' && cwd ? cwd : '(missing)'} is unavailable; restored in ${home}`);
|
|
47
|
-
return home;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function sortedPanes(window) {
|
|
51
|
-
return [...window.panes].sort((a, b) => a.index - b.index);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function dispositionMap(plan) {
|
|
55
|
-
return new Map((plan.windows || []).map((window) => [window.logicalId, window]));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function notify(onProgress, results, result, total) {
|
|
59
|
-
if (typeof onProgress !== 'function') return;
|
|
60
|
-
await onProgress({ completed: results.length, total, result });
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function restoreOneSession({
|
|
64
|
-
item, checkpoint, tmux, agents, access, home, dispositions, restoredWindows,
|
|
65
|
-
}) {
|
|
66
|
-
const sessions = sessionMap(checkpoint);
|
|
67
|
-
const windows = windowMap(checkpoint);
|
|
68
|
-
const source = sessions.get(item.logicalId);
|
|
69
|
-
if (!source) throw Object.assign(new Error('checkpoint session is missing'), { stage: 'plan' });
|
|
70
|
-
|
|
71
|
-
const warnings = [];
|
|
72
|
-
const mapping = emptyMapping();
|
|
73
|
-
const localWindows = new Map();
|
|
74
|
-
const localPanes = new Map();
|
|
75
|
-
const links = [...item.windowLinks].sort((a, b) => a.index - b.index);
|
|
76
|
-
const seedLink = links.find((link) => {
|
|
77
|
-
if (restoredWindows.has(link.windowId)) return false;
|
|
78
|
-
return dispositions.get(link.windowId)?.action !== 'reuse';
|
|
79
|
-
});
|
|
80
|
-
let temp = null;
|
|
81
|
-
let topologyComplete = false;
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
if (seedLink) {
|
|
85
|
-
const seedWindow = windows.get(seedLink.windowId);
|
|
86
|
-
const seedPane = sortedPanes(seedWindow)[0];
|
|
87
|
-
const cwd = await usableCwd(seedPane.cwd, { access, home, warnings });
|
|
88
|
-
temp = await tmux.createTemporarySession({
|
|
89
|
-
cwd,
|
|
90
|
-
sessionLogicalId: item.logicalId,
|
|
91
|
-
windowLogicalId: seedWindow.id,
|
|
92
|
-
paneLogicalId: seedPane.id,
|
|
93
|
-
windowName: seedWindow.name,
|
|
94
|
-
windowIndex: seedLink.index,
|
|
95
|
-
});
|
|
96
|
-
localWindows.set(seedWindow.id, temp.windowId);
|
|
97
|
-
localPanes.set(seedPane.id, temp.paneId);
|
|
98
|
-
mapRuntime(mapping, 'windows', seedWindow.runtimeId, seedWindow.id, temp.windowId);
|
|
99
|
-
mapRuntime(mapping, 'panes', seedPane.runtimeId, seedPane.id, temp.paneId);
|
|
100
|
-
} else {
|
|
101
|
-
const fallbackWindow = windows.get(links[0]?.windowId);
|
|
102
|
-
const cwd = await usableCwd(sortedPanes(fallbackWindow)[0].cwd, { access, home, warnings });
|
|
103
|
-
temp = await tmux.createTemporarySession({ cwd, sessionLogicalId: item.logicalId });
|
|
104
|
-
}
|
|
105
|
-
mapRuntime(mapping, 'sessions', source.runtimeId, source.id, temp.sessionId);
|
|
106
|
-
|
|
107
|
-
for (const link of links) {
|
|
108
|
-
const window = windows.get(link.windowId);
|
|
109
|
-
if (!window) throw new Error(`checkpoint window ${link.windowId} is missing`);
|
|
110
|
-
if (localWindows.has(window.id)) continue;
|
|
111
|
-
|
|
112
|
-
const sharedRuntime = restoredWindows.get(window.id);
|
|
113
|
-
const disposition = dispositions.get(window.id);
|
|
114
|
-
if (sharedRuntime || disposition?.action === 'reuse') {
|
|
115
|
-
const windowId = sharedRuntime || disposition.runtimeId;
|
|
116
|
-
await tmux.linkWindow(windowId, temp.sessionId, link.index, { existing: !sharedRuntime });
|
|
117
|
-
if (!sharedRuntime) mapRuntime(mapping, 'windows', window.runtimeId, window.id, windowId);
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const firstPane = sortedPanes(window)[0];
|
|
122
|
-
const cwd = await usableCwd(firstPane.cwd, { access, home, warnings });
|
|
123
|
-
const created = await tmux.createWindow(temp.sessionId, {
|
|
124
|
-
name: window.name,
|
|
125
|
-
index: link.index,
|
|
126
|
-
cwd,
|
|
127
|
-
windowLogicalId: window.id,
|
|
128
|
-
paneLogicalId: firstPane.id,
|
|
129
|
-
});
|
|
130
|
-
localWindows.set(window.id, created.windowId);
|
|
131
|
-
localPanes.set(firstPane.id, created.paneId);
|
|
132
|
-
mapRuntime(mapping, 'windows', window.runtimeId, window.id, created.windowId);
|
|
133
|
-
mapRuntime(mapping, 'panes', firstPane.runtimeId, firstPane.id, created.paneId);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
for (const [logicalId, runtimeId] of localWindows) {
|
|
137
|
-
const window = windows.get(logicalId);
|
|
138
|
-
const panes = sortedPanes(window);
|
|
139
|
-
const seedPaneId = localPanes.get(panes[0].id);
|
|
140
|
-
for (const pane of panes.slice(1)) {
|
|
141
|
-
const cwd = await usableCwd(pane.cwd, { access, home, warnings });
|
|
142
|
-
const paneId = await tmux.splitPane(seedPaneId, { cwd, paneLogicalId: pane.id });
|
|
143
|
-
localPanes.set(pane.id, paneId);
|
|
144
|
-
mapRuntime(mapping, 'panes', pane.runtimeId, pane.id, paneId);
|
|
145
|
-
}
|
|
146
|
-
try {
|
|
147
|
-
await tmux.applyLayout(runtimeId, window.layout);
|
|
148
|
-
} catch (error) {
|
|
149
|
-
warnings.push(`layout for ${window.name} was unavailable; kept the default layout (${message(error)})`);
|
|
150
|
-
}
|
|
151
|
-
const activePane = localPanes.get(window.activePaneId);
|
|
152
|
-
if (activePane) await tmux.selectPane(activePane);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const activeLink = links.find((link) => link.windowId === item.activeWindowId);
|
|
156
|
-
if (activeLink) {
|
|
157
|
-
if (typeof tmux.selectWindowInSession === 'function') await tmux.selectWindowInSession(temp.sessionId, activeLink.index);
|
|
158
|
-
else {
|
|
159
|
-
const activeWindow = localWindows.get(activeLink.windowId) || restoredWindows.get(activeLink.windowId);
|
|
160
|
-
if (activeWindow) await tmux.selectWindow(activeWindow);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (!seedLink && temp.windowId) await tmux.killCreatedWindow(temp.windowId);
|
|
165
|
-
await tmux.renameCreatedSession(temp.sessionId, item.targetName);
|
|
166
|
-
topologyComplete = true;
|
|
167
|
-
mapping.names[item.sourceName] = item.targetName;
|
|
168
|
-
for (const [logicalId, runtimeId] of localWindows) restoredWindows.set(logicalId, runtimeId);
|
|
169
|
-
|
|
170
|
-
for (const [logicalId, paneId] of localPanes) {
|
|
171
|
-
const pane = [...windows.values()].flatMap((window) => window.panes).find((candidate) => candidate.id === logicalId);
|
|
172
|
-
if (!pane?.agent) continue;
|
|
173
|
-
const driver = agents.find((candidate) => candidate.id === pane.agent.id);
|
|
174
|
-
if (!driver) {
|
|
175
|
-
warnings.push(`agent ${pane.agent.id || '(missing)'} is unsupported; shell was restored`);
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
if (!driver.sessions.isId(pane.agent.sessionId)) {
|
|
179
|
-
warnings.push(`agent ${driver.id} session id is invalid; shell was restored`);
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
try {
|
|
183
|
-
await access(pane.agent.transcriptPath);
|
|
184
|
-
} catch {
|
|
185
|
-
warnings.push(`agent ${driver.id} context is unavailable; shell was restored`);
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
const [cmd, ...args] = driver.sessions.resumeArgs(pane.agent.sessionId);
|
|
189
|
-
try {
|
|
190
|
-
await tmux.startAgent(paneId, cmd, args);
|
|
191
|
-
} catch (error) {
|
|
192
|
-
warnings.push(`agent ${driver.id} resume failed; shell was restored (${message(error)})`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
logicalId: item.logicalId,
|
|
198
|
-
sourceName: item.sourceName,
|
|
199
|
-
targetName: item.targetName,
|
|
200
|
-
status: 'restored',
|
|
201
|
-
warnings,
|
|
202
|
-
mapping,
|
|
203
|
-
};
|
|
204
|
-
} catch (error) {
|
|
205
|
-
let cleanupError;
|
|
206
|
-
if (temp && !topologyComplete) {
|
|
207
|
-
try { await tmux.killCreatedSession(temp.sessionId); } catch (failure) { cleanupError = failure; }
|
|
208
|
-
}
|
|
209
|
-
const failure = cleanupError
|
|
210
|
-
? new Error(`${message(error)}; cleanup failed: ${message(cleanupError)}`)
|
|
211
|
-
: error;
|
|
212
|
-
failure.stage ||= error?.stage || 'topology';
|
|
213
|
-
throw failure;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function restoredTopologySummary(checkpoint, results) {
|
|
218
|
-
const restoredSessions = new Set(results
|
|
219
|
-
.filter((result) => result.status === 'restored')
|
|
220
|
-
.map((result) => result.logicalId));
|
|
221
|
-
const windowIds = new Set((checkpoint?.sessions || [])
|
|
222
|
-
.filter((session) => restoredSessions.has(session.id))
|
|
223
|
-
.flatMap((session) => (session.windowLinks || []).map((link) => link.windowId)));
|
|
224
|
-
const paneIds = new Set((checkpoint?.windows || [])
|
|
225
|
-
.filter((window) => windowIds.has(window.id))
|
|
226
|
-
.flatMap((window) => (window.panes || []).map((pane) => pane.id)));
|
|
227
|
-
return { sessions: restoredSessions.size, windows: windowIds.size, panes: paneIds.size };
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export function summarizeRestore(results, checkpoint = null) {
|
|
231
|
-
const mapping = emptyMapping();
|
|
232
|
-
for (const result of results) if (result.mapping) mergeMapping(mapping, result.mapping);
|
|
233
|
-
const restored = results.filter((result) => result.status === 'restored').length;
|
|
234
|
-
const alreadyPresent = results.filter((result) => result.status === 'already-present').length;
|
|
235
|
-
const failed = results.filter((result) => result.status === 'failed').length;
|
|
236
|
-
const status = failed === 0 ? 'succeeded' : restored + alreadyPresent > 0 ? 'partial' : 'failed';
|
|
237
|
-
return { status, restored, alreadyPresent, failed, results, mapping, summary: restoredTopologySummary(checkpoint, results) };
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export async function executeRestore({
|
|
241
|
-
plan,
|
|
242
|
-
checkpoint,
|
|
243
|
-
tmux,
|
|
244
|
-
agents = AGENTS,
|
|
245
|
-
onProgress,
|
|
246
|
-
access = fsp.access,
|
|
247
|
-
home = os.homedir(),
|
|
248
|
-
} = {}) {
|
|
249
|
-
const results = [];
|
|
250
|
-
const dispositions = dispositionMap(plan);
|
|
251
|
-
const restoredWindows = new Map();
|
|
252
|
-
try {
|
|
253
|
-
for (const item of plan.sessions) {
|
|
254
|
-
let result;
|
|
255
|
-
if (item.action === 'already-present') {
|
|
256
|
-
result = { logicalId: item.logicalId, sourceName: item.sourceName, status: 'already-present' };
|
|
257
|
-
} else if (item.action === 'unsupported') {
|
|
258
|
-
result = { logicalId: item.logicalId, sourceName: item.sourceName, status: 'failed', stage: 'plan', error: item.reason };
|
|
259
|
-
} else {
|
|
260
|
-
try {
|
|
261
|
-
result = await restoreOneSession({ item, checkpoint, tmux, agents, access, home, dispositions, restoredWindows });
|
|
262
|
-
} catch (error) {
|
|
263
|
-
result = {
|
|
264
|
-
logicalId: item.logicalId,
|
|
265
|
-
sourceName: item.sourceName,
|
|
266
|
-
status: 'failed',
|
|
267
|
-
stage: error.stage || 'topology',
|
|
268
|
-
error: message(error),
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
results.push(result);
|
|
273
|
-
await notify(onProgress, results, result, plan.sessions.length);
|
|
274
|
-
}
|
|
275
|
-
return summarizeRestore(results, checkpoint);
|
|
276
|
-
} finally {
|
|
277
|
-
tmux.revokeCreatedTargets?.();
|
|
278
|
-
}
|
|
279
|
-
}
|
package/src/workspace/runtime.js
DELETED
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import { buildRestorePlan } from './planner.js';
|
|
3
|
-
import { executeRestore } from './restore.js';
|
|
4
|
-
import { createOperationManager, normalizeRestoreRequest } from './operations.js';
|
|
5
|
-
import { buildRecoveryMapping } from './mapping.js';
|
|
6
|
-
|
|
7
|
-
function unwrapCheckpoint(result) {
|
|
8
|
-
if (result?.status !== 'ok' || !result.value) {
|
|
9
|
-
throw new Error(result?.error || `checkpoint is ${result?.status || 'unavailable'}`);
|
|
10
|
-
}
|
|
11
|
-
return { checkpoint: result.value, warnings: [result.warning, ...(result.warnings || [])].filter(Boolean) };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function readCheckpoint(store, checkpointId) {
|
|
15
|
-
return unwrapCheckpoint(checkpointId === 'latest'
|
|
16
|
-
? await store.readLatestCheckpoint()
|
|
17
|
-
: await store.readCheckpoint(checkpointId));
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function readRecovery(store, checkpointId, historical) {
|
|
21
|
-
const result = await store.readRecovery(checkpointId);
|
|
22
|
-
if (result.status === 'ok') return result.value;
|
|
23
|
-
if (historical && result.status === 'missing') return null;
|
|
24
|
-
throw new Error(result.error || `recovery state is ${result.status}`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function captureLive(tmux) {
|
|
28
|
-
const live = await tmux.captureTopology({ readOnly: true });
|
|
29
|
-
if (live?.status === 'ok') return live;
|
|
30
|
-
if (live?.status === 'empty') return { ...live, sessions: [], windows: [] };
|
|
31
|
-
throw new Error(live?.error || 'current tmux topology is unavailable');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function blankMapping() {
|
|
35
|
-
return {
|
|
36
|
-
names: {},
|
|
37
|
-
runtime: { sessions: {}, windows: {}, panes: {} },
|
|
38
|
-
logical: { sessions: {}, windows: {}, panes: {} },
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function mapRuntime(mapping, kind, source, logical, runtime) {
|
|
43
|
-
if (typeof source === 'string' && source) mapping.runtime[kind][source] = runtime;
|
|
44
|
-
if (typeof logical === 'string' && logical) mapping.logical[kind][logical] = runtime;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function mappingForAlreadyPresent(checkpoint, live, results) {
|
|
48
|
-
const mapping = blankMapping();
|
|
49
|
-
const resolved = new Set(results.filter((row) => row.status === 'already-present').map((row) => row.logicalId));
|
|
50
|
-
const liveSessions = new Map(live.sessions.map((session) => [session.id, session]));
|
|
51
|
-
const liveWindows = new Map(live.windows.map((window) => [window.id, window]));
|
|
52
|
-
const sourceWindows = new Map(checkpoint.windows.map((window) => [window.id, window]));
|
|
53
|
-
for (const source of checkpoint.sessions) {
|
|
54
|
-
if (!resolved.has(source.id)) continue;
|
|
55
|
-
const actual = liveSessions.get(source.id);
|
|
56
|
-
if (!actual) continue;
|
|
57
|
-
mapping.names[source.name] = actual.name;
|
|
58
|
-
mapRuntime(mapping, 'sessions', source.runtimeId, source.id, actual.runtimeId);
|
|
59
|
-
for (const link of source.windowLinks) {
|
|
60
|
-
const sourceWindow = sourceWindows.get(link.windowId);
|
|
61
|
-
const actualWindow = liveWindows.get(link.windowId);
|
|
62
|
-
if (!sourceWindow || !actualWindow) continue;
|
|
63
|
-
mapRuntime(mapping, 'windows', sourceWindow.runtimeId, sourceWindow.id, actualWindow.runtimeId);
|
|
64
|
-
const actualPanes = new Map(actualWindow.panes.map((pane) => [pane.id, pane]));
|
|
65
|
-
for (const sourcePane of sourceWindow.panes) {
|
|
66
|
-
const actualPane = actualPanes.get(sourcePane.id);
|
|
67
|
-
if (actualPane) mapRuntime(mapping, 'panes', sourcePane.runtimeId, sourcePane.id, actualPane.runtimeId);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
return mapping;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function sorted(value) {
|
|
75
|
-
if (Array.isArray(value)) return value.map(sorted);
|
|
76
|
-
if (!value || typeof value !== 'object') return value;
|
|
77
|
-
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sorted(value[key])]));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function restoreGuard(live) {
|
|
81
|
-
const topology = {
|
|
82
|
-
status: live.status,
|
|
83
|
-
tmuxVersion: live.tmuxVersion || null,
|
|
84
|
-
active: live.active || null,
|
|
85
|
-
sessions: live.sessions || [],
|
|
86
|
-
windows: live.windows || [],
|
|
87
|
-
};
|
|
88
|
-
return {
|
|
89
|
-
topologyFingerprint: crypto.createHash('sha256').update(JSON.stringify(sorted(topology))).digest('hex'),
|
|
90
|
-
identities: {
|
|
91
|
-
sessions: (live.sessions || []).map(({ id, runtimeId, name }) => [id, runtimeId, name]).sort(),
|
|
92
|
-
windows: (live.windows || []).map(({ id, runtimeId, name }) => [id, runtimeId, name]).sort(),
|
|
93
|
-
panes: (live.windows || []).flatMap((window) => (window.panes || []).map(({ id, runtimeId }) => [id, runtimeId])).sort(),
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function sameRestoreGuard(left, right) {
|
|
99
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function createWorkspaceRuntime({
|
|
103
|
-
store,
|
|
104
|
-
tmux,
|
|
105
|
-
lock,
|
|
106
|
-
checkpointer,
|
|
107
|
-
now = Date.now,
|
|
108
|
-
randomUUID = crypto.randomUUID,
|
|
109
|
-
planner = buildRestorePlan,
|
|
110
|
-
executor = executeRestore,
|
|
111
|
-
agents,
|
|
112
|
-
access,
|
|
113
|
-
home,
|
|
114
|
-
restoreGuardAttempts = 3,
|
|
115
|
-
} = {}) {
|
|
116
|
-
const operations = createOperationManager({
|
|
117
|
-
store,
|
|
118
|
-
now,
|
|
119
|
-
randomUUID,
|
|
120
|
-
tryAcquireOperationLock: (owner) => lock.tryAcquire(owner),
|
|
121
|
-
});
|
|
122
|
-
let startPromise = null;
|
|
123
|
-
|
|
124
|
-
async function resolveOperationRequest(requestInput = {}) {
|
|
125
|
-
const request = normalizeRestoreRequest(requestInput);
|
|
126
|
-
if (request.checkpointId !== 'latest') return request;
|
|
127
|
-
const { checkpoint } = unwrapCheckpoint(await store.readLatestCheckpoint());
|
|
128
|
-
return { ...request, checkpointId: checkpoint.id };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function planRestore(requestInput = {}) {
|
|
132
|
-
const request = normalizeRestoreRequest(requestInput);
|
|
133
|
-
const { checkpoint, warnings } = await readCheckpoint(store, request.checkpointId);
|
|
134
|
-
const recovery = await readRecovery(store, checkpoint.id, request.historical);
|
|
135
|
-
const live = await captureLive(tmux);
|
|
136
|
-
const plan = planner(checkpoint, live, {
|
|
137
|
-
sessionNames: request.sessions,
|
|
138
|
-
recovery,
|
|
139
|
-
historical: request.historical,
|
|
140
|
-
warnings,
|
|
141
|
-
});
|
|
142
|
-
return { request, checkpoint, recovery, live, plan };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function guardedPlanRestore(requestInput = {}) {
|
|
146
|
-
const request = normalizeRestoreRequest(requestInput);
|
|
147
|
-
const { checkpoint, warnings } = await readCheckpoint(store, request.checkpointId);
|
|
148
|
-
const recovery = await readRecovery(store, checkpoint.id, request.historical);
|
|
149
|
-
let live = await captureLive(tmux);
|
|
150
|
-
for (let attempt = 0; attempt < restoreGuardAttempts; attempt += 1) {
|
|
151
|
-
const plan = planner(checkpoint, live, {
|
|
152
|
-
sessionNames: request.sessions,
|
|
153
|
-
recovery,
|
|
154
|
-
historical: request.historical,
|
|
155
|
-
warnings,
|
|
156
|
-
});
|
|
157
|
-
const before = restoreGuard(live);
|
|
158
|
-
const verified = await captureLive(tmux);
|
|
159
|
-
if (sameRestoreGuard(before, restoreGuard(verified))) {
|
|
160
|
-
return { request, checkpoint, recovery, live: verified, plan };
|
|
161
|
-
}
|
|
162
|
-
live = verified;
|
|
163
|
-
}
|
|
164
|
-
throw new Error(`tmux topology changed during restore planning ${restoreGuardAttempts} times; retry restore`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function getRestorePlan(request = {}) {
|
|
168
|
-
await start();
|
|
169
|
-
const state = await planRestore(request);
|
|
170
|
-
const serverNow = new Date(now()).toISOString();
|
|
171
|
-
const promptEligible = Boolean(
|
|
172
|
-
!state.request.historical
|
|
173
|
-
&& state.recovery
|
|
174
|
-
&& state.recovery.resolvedAt === null
|
|
175
|
-
&& state.recovery.pendingSessionIds.length > 0
|
|
176
|
-
&& Date.parse(state.recovery.expiresAt) > now(),
|
|
177
|
-
);
|
|
178
|
-
return Object.freeze({ ...state.plan, mapping: state.recovery?.mapping || null, serverNow, promptEligible });
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function getProtectionStatus() {
|
|
182
|
-
let live;
|
|
183
|
-
try {
|
|
184
|
-
live = await store.readLive();
|
|
185
|
-
} catch {
|
|
186
|
-
return { status: 'degraded', lastSuccessfulCaptureAt: null, errorCode: 'live-unavailable' };
|
|
187
|
-
}
|
|
188
|
-
if (live?.status === 'ok') {
|
|
189
|
-
return { status: 'protected', lastSuccessfulCaptureAt: live.value?.capturedAt || null, errorCode: null };
|
|
190
|
-
}
|
|
191
|
-
if (live?.status === 'empty') {
|
|
192
|
-
return { status: 'unprotected', lastSuccessfulCaptureAt: null, errorCode: null };
|
|
193
|
-
}
|
|
194
|
-
return {
|
|
195
|
-
status: 'degraded',
|
|
196
|
-
lastSuccessfulCaptureAt: null,
|
|
197
|
-
errorCode: live?.status === 'corrupt' ? 'live-corrupt' : 'live-unavailable',
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async function performRestore(operationId, request, onProgress, onRunning, onTerminal) {
|
|
202
|
-
let result;
|
|
203
|
-
let restoreError;
|
|
204
|
-
try {
|
|
205
|
-
result = await lock.withLock({ operationId }, async () => {
|
|
206
|
-
try {
|
|
207
|
-
await onRunning();
|
|
208
|
-
// The preview may be stale by now. Capture and plan again while holding the writer lock so a name
|
|
209
|
-
// created after preview receives the next non-destructive `-restored` suffix.
|
|
210
|
-
const state = await guardedPlanRestore(request);
|
|
211
|
-
const restored = await executor({
|
|
212
|
-
plan: state.plan,
|
|
213
|
-
checkpoint: state.checkpoint,
|
|
214
|
-
tmux,
|
|
215
|
-
agents,
|
|
216
|
-
onProgress,
|
|
217
|
-
access,
|
|
218
|
-
home,
|
|
219
|
-
});
|
|
220
|
-
const resolvedIds = restored.results
|
|
221
|
-
.filter((row) => row.status === 'restored' || row.status === 'already-present')
|
|
222
|
-
.map((row) => row.logicalId);
|
|
223
|
-
const mapping = buildRecoveryMapping(state.checkpoint.id, state.recovery?.mapping, [
|
|
224
|
-
restored.mapping,
|
|
225
|
-
mappingForAlreadyPresent(state.checkpoint, state.live, restored.results),
|
|
226
|
-
], now);
|
|
227
|
-
if (mapping && typeof store.mergeRecoveryMapping === 'function') {
|
|
228
|
-
await store.mergeRecoveryMapping(state.checkpoint.id, mapping);
|
|
229
|
-
}
|
|
230
|
-
// Persist migration data before resolving pending ids. If the second write fails, retry planning
|
|
231
|
-
// still sees the session and will recognize it as already-present instead of losing its mapping.
|
|
232
|
-
if (resolvedIds.length > 0) await store.resolveSessions(state.checkpoint.id, resolvedIds);
|
|
233
|
-
const lockedResult = { ...restored, mapping };
|
|
234
|
-
await onTerminal(lockedResult);
|
|
235
|
-
return lockedResult;
|
|
236
|
-
} catch (error) {
|
|
237
|
-
await onTerminal(error);
|
|
238
|
-
throw error;
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
} catch (error) {
|
|
242
|
-
restoreError = error;
|
|
243
|
-
}
|
|
244
|
-
let reconcileWarning;
|
|
245
|
-
try {
|
|
246
|
-
const reconciled = await checkpointer.reconcile('restore-complete');
|
|
247
|
-
if (!['written', 'unchanged'].includes(reconciled?.status)) {
|
|
248
|
-
reconcileWarning = `live reconcile ${reconciled?.status || 'unknown'}; workspace protection may be degraded`;
|
|
249
|
-
}
|
|
250
|
-
} catch (error) {
|
|
251
|
-
reconcileWarning = `live reconcile failed: ${error?.message || String(error)}`;
|
|
252
|
-
}
|
|
253
|
-
if (restoreError) throw restoreError;
|
|
254
|
-
if (reconcileWarning) {
|
|
255
|
-
result = {
|
|
256
|
-
...result,
|
|
257
|
-
warnings: [...(result.warnings || []), reconcileWarning],
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
return result;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function startOnce() {
|
|
264
|
-
let interrupted;
|
|
265
|
-
let sweepError;
|
|
266
|
-
try {
|
|
267
|
-
interrupted = await operations.interruptOrphans();
|
|
268
|
-
} catch (error) {
|
|
269
|
-
sweepError = error;
|
|
270
|
-
}
|
|
271
|
-
let started;
|
|
272
|
-
let startError;
|
|
273
|
-
try {
|
|
274
|
-
started = await checkpointer.start?.();
|
|
275
|
-
} catch (error) {
|
|
276
|
-
startError = error;
|
|
277
|
-
}
|
|
278
|
-
if (sweepError && startError) {
|
|
279
|
-
const sweepMessage = sweepError instanceof Error ? sweepError.message : String(sweepError);
|
|
280
|
-
const startMessage = startError instanceof Error ? startError.message : String(startError);
|
|
281
|
-
throw new AggregateError([sweepError, startError], `${sweepMessage}; workspace protection start failed: ${startMessage}`);
|
|
282
|
-
}
|
|
283
|
-
if (sweepError) throw sweepError;
|
|
284
|
-
if (startError) throw startError;
|
|
285
|
-
return [interrupted, started];
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function start() {
|
|
289
|
-
if (!startPromise) {
|
|
290
|
-
startPromise = startOnce().catch((error) => {
|
|
291
|
-
startPromise = null;
|
|
292
|
-
throw error;
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
return startPromise;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return {
|
|
299
|
-
start,
|
|
300
|
-
stop: () => checkpointer.stop(),
|
|
301
|
-
requestReconcile: () => checkpointer.requestReconcile(),
|
|
302
|
-
confirmEmpty: () => checkpointer.confirmEmpty(),
|
|
303
|
-
listCheckpoints: () => store.listCheckpoints(),
|
|
304
|
-
getProtectionStatus,
|
|
305
|
-
getRestorePlan,
|
|
306
|
-
startRestore: async (request = {}) => operations.start(await resolveOperationRequest(request), ({ operationId, request: normalized, onProgress, onRunning, onTerminal }) => (
|
|
307
|
-
performRestore(operationId, normalized, onProgress, onRunning, onTerminal)
|
|
308
|
-
), { deferRunning: true }),
|
|
309
|
-
getOperation: (id) => operations.get(id),
|
|
310
|
-
restoreNow: async (request = {}) => operations.run(await resolveOperationRequest(request), ({ operationId, request: normalized, onProgress, onRunning, onTerminal }) => (
|
|
311
|
-
performRestore(operationId, normalized, onProgress, onRunning, onTerminal)
|
|
312
|
-
), { deferRunning: true }),
|
|
313
|
-
};
|
|
314
|
-
}
|