handmux 0.23.0 → 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.
Files changed (252) hide show
  1. package/README.md +3 -3
  2. package/README.zh-CN.md +3 -3
  3. package/bin/handmux.js +3 -689
  4. package/dist/bin/handmux.js +850 -0
  5. package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
  6. package/{hooks → dist/hooks}/handmux-write.cjs +24 -3
  7. package/dist/package.json +126 -0
  8. package/dist/public/assets/index-CicdY9E9.css +32 -0
  9. package/dist/public/assets/index-D4VP5t8h.js +337 -0
  10. package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
  11. package/{public → dist/public}/index.html +5 -4
  12. package/{src → dist/src}/agents/claude.js +105 -83
  13. package/dist/src/agents/codex.js +217 -0
  14. package/{src → dist/src}/agents/index.js +7 -7
  15. package/dist/src/agents/processIdentity.js +80 -0
  16. package/dist/src/agents/scanUtils.js +262 -0
  17. package/dist/src/apiErrors.js +77 -0
  18. package/{src → dist/src}/appName.js +12 -13
  19. package/dist/src/asr/iflyConfig.js +7 -0
  20. package/dist/src/asr/iflySign.js +14 -0
  21. package/dist/src/auth.js +27 -0
  22. package/dist/src/browser/bootstrap.js +72 -0
  23. package/dist/src/browser/cookieProfiles.js +413 -0
  24. package/dist/src/browser/coordinator.js +51 -0
  25. package/dist/src/browser/hammerheadCompat.js +104 -0
  26. package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
  27. package/dist/src/browser/manager.js +560 -0
  28. package/dist/src/browser/originLabel.js +15 -0
  29. package/dist/src/browser/profilePersistence.js +235 -0
  30. package/dist/src/browser/publicProxy.js +176 -0
  31. package/dist/src/browser/routes.js +159 -0
  32. package/dist/src/browser/sessionStore.js +72 -0
  33. package/dist/src/browser/siteVersion.js +121 -0
  34. package/dist/src/browser/targetPolicy.js +163 -0
  35. package/{src → dist/src}/browser/worker.js +14 -18
  36. package/dist/src/browser/workerClient.js +286 -0
  37. package/dist/src/browser/workerServer.js +91 -0
  38. package/dist/src/captureBackground.js +95 -0
  39. package/dist/src/claudeEvents.js +499 -0
  40. package/{src → dist/src}/cli/claudeHooks.js +146 -108
  41. package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
  42. package/dist/src/cli/cloudflared.js +106 -0
  43. package/dist/src/cli/codexManaged.js +77 -0
  44. package/{src → dist/src}/cli/cpolarUrl.js +8 -9
  45. package/dist/src/cli/drivers.js +98 -0
  46. package/dist/src/cli/hookScaffold.js +36 -0
  47. package/dist/src/cli/i18n/en.js +366 -0
  48. package/{src → dist/src}/cli/i18n/index.js +27 -25
  49. package/dist/src/cli/i18n/zh.js +365 -0
  50. package/dist/src/cli/legacyCodexHooks.js +52 -0
  51. package/{src → dist/src}/cli/natappUrl.js +2 -3
  52. package/dist/src/cli/options.js +245 -0
  53. package/dist/src/cli/probe.js +16 -0
  54. package/{src → dist/src}/cli/prompt.js +4 -6
  55. package/dist/src/cli/pushCmd.js +102 -0
  56. package/{src → dist/src}/cli/qr.js +19 -19
  57. package/dist/src/cli/service.js +144 -0
  58. package/dist/src/cli/setupModel.js +194 -0
  59. package/dist/src/cli/setupWizard.js +647 -0
  60. package/dist/src/cli/shortcutEditor.js +351 -0
  61. package/dist/src/cli/sshTunnel.js +18 -0
  62. package/dist/src/cli/state.js +114 -0
  63. package/{src → dist/src}/cli/statusLine.js +63 -46
  64. package/dist/src/cli/supervisor.js +278 -0
  65. package/dist/src/cli/supervisorHealth.js +21 -0
  66. package/dist/src/cli/supervisorLaunch.js +148 -0
  67. package/dist/src/cli/supervisorProcesses.js +62 -0
  68. package/dist/src/cli/supervisorState.js +62 -0
  69. package/dist/src/cli/tmuxVersion.js +53 -0
  70. package/{src → dist/src}/cli/tunlite.js +7 -10
  71. package/dist/src/cli/tunnelClients.js +72 -0
  72. package/{src → dist/src}/cli/updateCheck.js +98 -69
  73. package/{src → dist/src}/cli/urlHost.js +8 -4
  74. package/dist/src/cli/workspaceCmd.js +347 -0
  75. package/dist/src/codexAppServer.js +2299 -0
  76. package/dist/src/codexConversationProjection.js +199 -0
  77. package/dist/src/codexDiff.js +118 -0
  78. package/dist/src/codexMessageIdentity.js +18 -0
  79. package/dist/src/codexPlan.js +30 -0
  80. package/dist/src/codexQueueProtocol.js +96 -0
  81. package/dist/src/codexStreamProtocol.js +207 -0
  82. package/dist/src/codexToolProtocol.js +68 -0
  83. package/dist/src/codexTranscriptParse.js +724 -0
  84. package/dist/src/codexUsageSnapshot.js +205 -0
  85. package/dist/src/config.js +8 -0
  86. package/{src → dist/src}/docPath.js +22 -24
  87. package/dist/src/docs.js +337 -0
  88. package/dist/src/git.js +272 -0
  89. package/dist/src/healthProtocol.js +97 -0
  90. package/dist/src/httpApi.js +68 -0
  91. package/dist/src/jsonStore.js +15 -0
  92. package/{src → dist/src}/keyNames.js +3 -3
  93. package/dist/src/notifications.js +111 -0
  94. package/dist/src/orphans.js +157 -0
  95. package/dist/src/pendingPrompt.js +194 -0
  96. package/dist/src/previewServer.js +202 -0
  97. package/dist/src/previews.js +179 -0
  98. package/dist/src/privateStateStore.js +89 -0
  99. package/dist/src/push.js +224 -0
  100. package/dist/src/routes/codex.js +606 -0
  101. package/dist/src/routes/files.js +230 -0
  102. package/dist/src/routes/git.js +78 -0
  103. package/dist/src/routes/health.js +16 -0
  104. package/dist/src/routes/notifications.js +16 -0
  105. package/dist/src/routes/previews.js +43 -0
  106. package/dist/src/routes/push.js +145 -0
  107. package/dist/src/routes/sessions.js +232 -0
  108. package/dist/src/routes/system.js +156 -0
  109. package/dist/src/routes/terminal.js +238 -0
  110. package/dist/src/routes/transcript.js +197 -0
  111. package/dist/src/routes/workspace.js +267 -0
  112. package/{src → dist/src}/server.js +100 -74
  113. package/dist/src/shortcutConfig.js +83 -0
  114. package/{src → dist/src}/staticCache.js +5 -3
  115. package/{src → dist/src}/staticCompression.js +5 -6
  116. package/dist/src/terminalStream.js +441 -0
  117. package/{src → dist/src}/tmux/commands.js +156 -171
  118. package/dist/src/tmux/format.js +47 -0
  119. package/dist/src/transcriptParse.js +240 -0
  120. package/dist/src/transcriptReader.js +113 -0
  121. package/{src → dist/src}/trimCapture.js +11 -11
  122. package/dist/src/uploadTypes.js +29 -0
  123. package/dist/src/urlPolicy.js +18 -0
  124. package/dist/src/usage.js +260 -0
  125. package/dist/src/workspace/agentRunner.js +204 -0
  126. package/dist/src/workspace/atomicJson.js +31 -0
  127. package/dist/src/workspace/capture.js +139 -0
  128. package/dist/src/workspace/checkpointer.js +228 -0
  129. package/dist/src/workspace/environment.js +71 -0
  130. package/dist/src/workspace/lock.js +137 -0
  131. package/dist/src/workspace/mapping.js +128 -0
  132. package/dist/src/workspace/operations.js +271 -0
  133. package/dist/src/workspace/paths.js +15 -0
  134. package/dist/src/workspace/planner.js +292 -0
  135. package/dist/src/workspace/restore.js +314 -0
  136. package/dist/src/workspace/runtime.js +318 -0
  137. package/dist/src/workspace/schema.js +156 -0
  138. package/dist/src/workspace/store.js +464 -0
  139. package/dist/src/workspace/tmuxAdapter.js +694 -0
  140. package/dist/src/workspaceProtocol.js +158 -0
  141. package/package.json +27 -15
  142. package/public/assets/index-C3N7OkyP.css +0 -32
  143. package/public/assets/index-D1DWcC5u.js +0 -337
  144. package/src/.gitkeep +0 -0
  145. package/src/agents/codex.js +0 -168
  146. package/src/agents/processIdentity.js +0 -72
  147. package/src/agents/scanUtils.js +0 -225
  148. package/src/asr/iflyConfig.js +0 -10
  149. package/src/asr/iflySign.js +0 -16
  150. package/src/auth.js +0 -30
  151. package/src/browser/bootstrap.js +0 -68
  152. package/src/browser/cookieProfiles.js +0 -403
  153. package/src/browser/coordinator.js +0 -47
  154. package/src/browser/hammerheadCompat.js +0 -86
  155. package/src/browser/hammerheadRedirectCompat.js +0 -38
  156. package/src/browser/manager.js +0 -556
  157. package/src/browser/originLabel.js +0 -17
  158. package/src/browser/profilePersistence.js +0 -241
  159. package/src/browser/publicProxy.js +0 -175
  160. package/src/browser/routes.js +0 -154
  161. package/src/browser/sessionStore.js +0 -78
  162. package/src/browser/siteVersion.js +0 -124
  163. package/src/browser/targetPolicy.js +0 -137
  164. package/src/browser/workerClient.js +0 -253
  165. package/src/browser/workerServer.js +0 -98
  166. package/src/captureBackground.js +0 -93
  167. package/src/claudeEvents.js +0 -399
  168. package/src/cli/cloudflared.js +0 -99
  169. package/src/cli/codexManaged.js +0 -77
  170. package/src/cli/drivers.js +0 -97
  171. package/src/cli/hookScaffold.js +0 -33
  172. package/src/cli/i18n/en.js +0 -385
  173. package/src/cli/i18n/zh.js +0 -384
  174. package/src/cli/legacyCodexHooks.js +0 -49
  175. package/src/cli/options.js +0 -211
  176. package/src/cli/probe.js +0 -16
  177. package/src/cli/pushCmd.js +0 -60
  178. package/src/cli/service.js +0 -126
  179. package/src/cli/setupModel.js +0 -154
  180. package/src/cli/setupWizard.js +0 -506
  181. package/src/cli/shortcutEditor.js +0 -288
  182. package/src/cli/sshTunnel.js +0 -12
  183. package/src/cli/state.js +0 -90
  184. package/src/cli/supervisor.js +0 -183
  185. package/src/cli/supervisorProcesses.js +0 -57
  186. package/src/cli/tmuxVersion.js +0 -49
  187. package/src/cli/tunnelClients.js +0 -62
  188. package/src/cli/workspaceCmd.js +0 -300
  189. package/src/codexAppServer.js +0 -1620
  190. package/src/codexDiff.js +0 -108
  191. package/src/codexTranscriptParse.js +0 -413
  192. package/src/codexUsageSnapshot.cjs +0 -129
  193. package/src/config.js +0 -9
  194. package/src/docs.js +0 -228
  195. package/src/git.js +0 -208
  196. package/src/httpApi.js +0 -72
  197. package/src/jsonStore.js +0 -24
  198. package/src/notifications.js +0 -68
  199. package/src/orphans.js +0 -152
  200. package/src/pendingPrompt.js +0 -163
  201. package/src/previewServer.js +0 -168
  202. package/src/previews.js +0 -116
  203. package/src/push.js +0 -183
  204. package/src/routes/codex.js +0 -473
  205. package/src/routes/files.js +0 -172
  206. package/src/routes/git.js +0 -57
  207. package/src/routes/notifications.js +0 -19
  208. package/src/routes/previews.js +0 -35
  209. package/src/routes/push.js +0 -119
  210. package/src/routes/sessions.js +0 -168
  211. package/src/routes/system.js +0 -132
  212. package/src/routes/terminal.js +0 -210
  213. package/src/routes/transcript.js +0 -171
  214. package/src/routes/workspace.js +0 -267
  215. package/src/shortcutConfig.js +0 -75
  216. package/src/terminalStream.js +0 -391
  217. package/src/tmux/format.js +0 -45
  218. package/src/transcriptParse.js +0 -180
  219. package/src/transcriptReader.js +0 -101
  220. package/src/uploadTypes.js +0 -30
  221. package/src/urlPolicy.js +0 -14
  222. package/src/usage.js +0 -260
  223. package/src/workspace/agentRunner.js +0 -188
  224. package/src/workspace/atomicJson.js +0 -32
  225. package/src/workspace/capture.js +0 -139
  226. package/src/workspace/checkpointer.js +0 -187
  227. package/src/workspace/environment.js +0 -75
  228. package/src/workspace/lock.js +0 -128
  229. package/src/workspace/mapping.js +0 -124
  230. package/src/workspace/operations.js +0 -252
  231. package/src/workspace/paths.js +0 -16
  232. package/src/workspace/planner.js +0 -247
  233. package/src/workspace/restore.js +0 -279
  234. package/src/workspace/runtime.js +0 -314
  235. package/src/workspace/schema.js +0 -136
  236. package/src/workspace/store.js +0 -382
  237. package/src/workspace/tmuxAdapter.js +0 -554
  238. /package/{hooks → dist/hooks}/handmux-notify.sh +0 -0
  239. /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  240. /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
  241. /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
  242. /package/{public → dist/public}/icons/badge-96.png +0 -0
  243. /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
  244. /package/{public → dist/public}/icons/icon-192.png +0 -0
  245. /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
  246. /package/{public → dist/public}/icons/icon-512.png +0 -0
  247. /package/{public → dist/public}/icons/logo.svg +0 -0
  248. /package/{public → dist/public}/manifest.webmanifest +0 -0
  249. /package/{public → dist/public}/offline.html +0 -0
  250. /package/{public → dist/public}/sw.js +0 -0
  251. /package/{src → dist/src}/browser/protocol.js +0 -0
  252. /package/{src → dist/src}/cli/cfNamed.js +0 -0
@@ -1,252 +0,0 @@
1
- import crypto from 'node:crypto';
2
-
3
- const ACTIVE = new Set(['pending', 'running']);
4
- const TERMINAL = new Set(['succeeded', 'partial', 'failed']);
5
-
6
- function iso(now) {
7
- return new Date(typeof now === 'function' ? now() : now).toISOString();
8
- }
9
-
10
- function errorMessage(error) {
11
- return error instanceof Error ? error.message : String(error);
12
- }
13
-
14
- const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
15
- const unavailableLock = async () => { throw new Error('workspace operation lock is unavailable'); };
16
- const INACTIVE_OPERATION = 'WORKSPACE_OPERATION_INACTIVE';
17
-
18
- export function normalizeRestoreRequest(request = {}) {
19
- const checkpointId = typeof request.checkpointId === 'string' && request.checkpointId ? request.checkpointId : 'latest';
20
- const rawSessions = Array.isArray(request.sessions) ? request.sessions : request.sessions ? [request.sessions] : [];
21
- const sessions = [...new Set(rawSessions.filter((name) => typeof name === 'string' && name))].sort();
22
- return { checkpointId, sessions, historical: request.historical === true };
23
- }
24
-
25
- export function restoreRequestHash(request) {
26
- return crypto.createHash('sha256').update(JSON.stringify(normalizeRestoreRequest(request))).digest('hex');
27
- }
28
-
29
- export function createOperationManager({
30
- store,
31
- now = Date.now,
32
- randomUUID = crypto.randomUUID,
33
- pid = process.pid,
34
- tryAcquireOperationLock = unavailableLock,
35
- pendingGraceMs = 250,
36
- wait = delay,
37
- } = {}) {
38
- const values = new Map();
39
- const activeByHash = new Map();
40
- const localIds = new Set();
41
-
42
- async function persist(operation) {
43
- if (localIds.has(operation.id)) values.set(operation.id, operation);
44
- await store.writeOperation(operation);
45
- return operation;
46
- }
47
-
48
- async function readFresh(id) {
49
- const result = await store.readOperation(id);
50
- return result.status === 'ok' ? result.value : null;
51
- }
52
-
53
- async function assessExternal(operation, { allowPendingGrace = true } = {}) {
54
- let current = await readFresh(operation.id);
55
- if (!current) return { active: false, operation: null };
56
- if (!ACTIVE.has(current?.status)) return { active: false, operation: current };
57
- if (current.status === 'pending' && allowPendingGrace) {
58
- const createdAt = Date.parse(current.createdAt);
59
- const age = Number.isFinite(createdAt) ? Math.max(0, now() - createdAt) : Number.POSITIVE_INFINITY;
60
- const remaining = pendingGraceMs - age;
61
- if (remaining > 0) {
62
- await wait(remaining);
63
- current = await readFresh(current.id);
64
- if (!current || !ACTIVE.has(current.status)) return { active: false, operation: current };
65
- }
66
- }
67
- let handle;
68
- try {
69
- handle = await tryAcquireOperationLock({ operationId: `assess-${current.id}` });
70
- } catch {
71
- return { active: true, operation: current };
72
- }
73
- if (!handle) return { active: true, operation: current };
74
- try {
75
- current = await readFresh(current.id);
76
- if (!current || !ACTIVE.has(current.status)) return { active: false, operation: current };
77
- const interrupted = {
78
- ...current,
79
- status: 'interrupted',
80
- error: 'restore interrupted by process restart; retry the restore',
81
- completedAt: iso(now),
82
- updatedAt: iso(now),
83
- };
84
- await store.writeOperation(interrupted);
85
- return { active: false, operation: interrupted, interrupted: true };
86
- } finally {
87
- await handle.release();
88
- }
89
- }
90
-
91
- async function findExternalActive(requestHash) {
92
- const rows = typeof store.listOperations === 'function' ? await store.listOperations() : [];
93
- for (const row of rows) {
94
- if (row.status !== 'ok' || localIds.has(row.value?.id) || !ACTIVE.has(row.value?.status)) continue;
95
- if (row.value.requestHash !== requestHash || row.value.requestHash !== restoreRequestHash(row.value.request)) continue;
96
- const assessed = await assessExternal(row.value);
97
- if (assessed.active) return assessed.operation;
98
- }
99
- return null;
100
- }
101
-
102
- async function execute(operation, runner, { deferRunning = false } = {}) {
103
- let current = operation;
104
- let running = false;
105
- let terminalStatus = null;
106
- const onRunning = async () => {
107
- if (running) return current;
108
- const persisted = await readFresh(current.id);
109
- if (!persisted || !ACTIVE.has(persisted.status)) {
110
- if (persisted) {
111
- current = persisted;
112
- if (localIds.has(current.id)) values.set(current.id, current);
113
- } else {
114
- localIds.delete(current.id);
115
- values.delete(current.id);
116
- }
117
- const error = new Error(`restore operation ${current.id} is no longer active`);
118
- error.code = INACTIVE_OPERATION;
119
- error.operation = persisted;
120
- throw error;
121
- }
122
- current = persisted;
123
- current = await persist({
124
- ...current,
125
- status: 'running',
126
- startedAt: iso(now),
127
- updatedAt: iso(now),
128
- });
129
- running = true;
130
- return current;
131
- };
132
- const onTerminal = async (value) => {
133
- await onRunning();
134
- const result = value instanceof Error
135
- ? { status: 'failed', error: errorMessage(value) }
136
- : value;
137
- const requestedStatus = TERMINAL.has(result?.status) ? result.status : 'failed';
138
- if (terminalStatus && requestedStatus !== terminalStatus) return current;
139
- const status = terminalStatus || requestedStatus;
140
- const candidate = {
141
- ...current,
142
- ...result,
143
- status,
144
- progress: { completed: result?.results?.length ?? current.progress.completed, total: current.progress.total },
145
- completedAt: current.completedAt || iso(now),
146
- };
147
- if (terminalStatus && JSON.stringify(candidate) === JSON.stringify(current)) return current;
148
- current = await persist({ ...candidate, updatedAt: iso(now) });
149
- terminalStatus = status;
150
- return current;
151
- };
152
- try {
153
- if (!deferRunning) await onRunning();
154
- const result = await runner({
155
- operationId: current.id,
156
- request: current.request,
157
- onRunning,
158
- onTerminal,
159
- onProgress: async ({ completed, total, result: row }) => {
160
- await onRunning();
161
- const results = row ? [...(current.results || []), row] : (current.results || []);
162
- current = await persist({
163
- ...current,
164
- progress: { completed, total },
165
- results,
166
- updatedAt: iso(now),
167
- });
168
- },
169
- });
170
- await onTerminal(result);
171
- } catch (error) {
172
- if (error?.code === INACTIVE_OPERATION) {
173
- if (error.operation) current = error.operation;
174
- } else {
175
- try { await onTerminal(error); } catch { /* pending/running file is interrupted on restart */ }
176
- }
177
- } finally {
178
- if (activeByHash.get(operation.requestHash) === operation.id) activeByHash.delete(operation.requestHash);
179
- }
180
- return current;
181
- }
182
-
183
- async function createPending(request) {
184
- const normalized = normalizeRestoreRequest(request);
185
- const requestHash = restoreRequestHash(normalized);
186
- const existingId = activeByHash.get(requestHash);
187
- if (existingId) return { reused: true, operation: values.get(existingId) };
188
- const external = await findExternalActive(requestHash);
189
- if (external) return { reused: true, operation: external };
190
- const id = randomUUID();
191
- const operation = {
192
- id,
193
- kind: 'workspace-restore',
194
- status: 'pending',
195
- request: normalized,
196
- requestHash,
197
- ownerPid: pid,
198
- createdAt: iso(now),
199
- updatedAt: iso(now),
200
- startedAt: null,
201
- completedAt: null,
202
- progress: { completed: 0, total: 0 },
203
- results: [],
204
- mapping: null,
205
- error: null,
206
- };
207
- localIds.add(id);
208
- activeByHash.set(requestHash, id);
209
- try {
210
- await persist(operation);
211
- } catch (error) {
212
- activeByHash.delete(requestHash);
213
- localIds.delete(id);
214
- values.delete(id);
215
- throw error;
216
- }
217
- return { reused: false, operation };
218
- }
219
-
220
- async function start(request, runner, options) {
221
- const pending = await createPending(request);
222
- if (pending.reused) {
223
- return { operationId: pending.operation.id, status: pending.operation.status, reused: true };
224
- }
225
- Promise.resolve().then(() => execute(pending.operation, runner, options)).catch(() => {});
226
- return { operationId: pending.operation.id, status: 'pending', reused: false };
227
- }
228
-
229
- async function run(request, runner, options) {
230
- const pending = await createPending(request);
231
- if (pending.reused) return pending.operation;
232
- return execute(pending.operation, runner, options);
233
- }
234
-
235
- async function get(id) {
236
- if (localIds.has(id) && values.has(id)) return values.get(id);
237
- return readFresh(id);
238
- }
239
-
240
- async function interruptOrphans() {
241
- const rows = typeof store.listOperations === 'function' ? await store.listOperations() : [];
242
- let interrupted = 0;
243
- for (const row of rows) {
244
- if (row.status !== 'ok' || localIds.has(row.value?.id) || !ACTIVE.has(row.value?.status)) continue;
245
- const assessed = await assessExternal(row.value);
246
- if (assessed.interrupted) interrupted += 1;
247
- }
248
- return interrupted;
249
- }
250
-
251
- return { start, run, get, interruptOrphans };
252
- }
@@ -1,16 +0,0 @@
1
- import path from 'node:path';
2
-
3
- export function workspacePaths(home) {
4
- const root = path.join(home, '.handmux', 'workspaces');
5
- return {
6
- root,
7
- liveDir: path.join(root, 'live'),
8
- liveCurrent: path.join(root, 'live', 'current.json'),
9
- liveMirror: path.join(root, 'live', 'mirror.json'),
10
- checkpointsDir: path.join(root, 'checkpoints'),
11
- recoveryDir: path.join(root, 'recovery'),
12
- operationsDir: path.join(root, 'operations'),
13
- latest: path.join(root, 'latest.json'),
14
- lockDir: path.join(root, 'restore.lock'),
15
- };
16
- }
@@ -1,247 +0,0 @@
1
- function deepFreeze(value, seen = new Set()) {
2
- if (!value || typeof value !== 'object' || seen.has(value)) return value;
3
- seen.add(value);
4
- for (const nested of Object.values(value)) deepFreeze(nested, seen);
5
- return Object.freeze(value);
6
- }
7
-
8
- function uniqueStrings(values) {
9
- return [...new Set(values.filter((value) => typeof value === 'string' && value))];
10
- }
11
-
12
- function stringOrNull(value) {
13
- return typeof value === 'string' ? value : null;
14
- }
15
-
16
- function nonEmptyStringOrNull(value) {
17
- return typeof value === 'string' && value ? value : null;
18
- }
19
-
20
- function indexOrNull(value) {
21
- return Number.isInteger(value) && value >= 0 ? value : null;
22
- }
23
-
24
- function restoredName(sourceName, names) {
25
- if (!names.has(sourceName)) return sourceName;
26
- const restored = `${sourceName}-restored`;
27
- if (!names.has(restored)) return restored;
28
- for (let suffix = 2; ; suffix += 1) {
29
- const candidate = `${restored}-${suffix}`;
30
- if (!names.has(candidate)) return candidate;
31
- }
32
- }
33
-
34
- function runtimeIdList(items, kind) {
35
- return items.map((item) => {
36
- const runtimeId = nonEmptyStringOrNull(item?.runtimeId);
37
- if (runtimeId === null) throw new Error(`invalid live ${kind} runtime id`);
38
- return runtimeId;
39
- });
40
- }
41
-
42
- function runtimeIds(live) {
43
- const sessions = Array.isArray(live?.sessions) ? live.sessions : [];
44
- const windows = Array.isArray(live?.windows) ? live.windows : [];
45
- const panes = windows.flatMap((window) => Array.isArray(window?.panes) ? window.panes : []);
46
- return {
47
- sessions: runtimeIdList(sessions, 'session'),
48
- windows: runtimeIdList(windows, 'window'),
49
- panes: runtimeIdList(panes, 'pane'),
50
- };
51
- }
52
-
53
- function countTopology(windows) {
54
- const panes = windows.flatMap((window) => Array.isArray(window.panes) ? window.panes : []);
55
- return {
56
- windows: windows.length,
57
- panes: panes.length,
58
- agents: panes.filter((pane) => pane.agent !== null && pane.agent !== undefined).length,
59
- };
60
- }
61
-
62
- function warningList(...sources) {
63
- return uniqueStrings(sources.flatMap((source) => Array.isArray(source) ? source : [source]));
64
- }
65
-
66
- function unwrapCheckpoint(input) {
67
- if (input?.status === 'ok' && input.value) {
68
- return { checkpoint: input.value, warnings: warningList(input.warnings, input.warning) };
69
- }
70
- return { checkpoint: input, warnings: warningList(input?.warnings, input?.warning) };
71
- }
72
-
73
- function linkedOwners(sessions) {
74
- const owners = new Map();
75
- for (const session of sessions) {
76
- if (!Array.isArray(session?.windowLinks)) continue;
77
- for (const link of session.windowLinks) {
78
- if (typeof link?.windowId !== 'string') continue;
79
- if (!owners.has(link.windowId)) owners.set(link.windowId, new Set());
80
- owners.get(link.windowId).add(session.id);
81
- }
82
- }
83
- return owners;
84
- }
85
-
86
- function topologyIssue(session, windowsById, owners, supportsLinkedWindows) {
87
- if (!Array.isArray(session.windowLinks) || session.windowLinks.length === 0) return 'missing-window-links';
88
- const ids = session.windowLinks.map((link) => link?.windowId);
89
- const indexes = session.windowLinks.map((link) => link?.index);
90
- if (ids.some((id) => typeof id !== 'string' || !id)) return 'invalid-window-link';
91
- if (indexes.some((index) => !Number.isInteger(index) || index < 0)) return 'invalid-window-link-index';
92
- if (new Set(ids).size !== ids.length) return 'duplicate-window-link';
93
- if (new Set(indexes).size !== indexes.length) return 'duplicate-window-link-index';
94
- if (ids.some((id) => !windowsById.has(id))) return 'dangling-window-link';
95
- if (!ids.includes(session.activeWindowId)) return 'dangling-active-window';
96
- if (!supportsLinkedWindows && ids.some((id) => owners.get(id)?.size > 1)) return 'linked-windows-unsupported';
97
-
98
- for (const id of ids) {
99
- const window = windowsById.get(id);
100
- if (!Array.isArray(window.panes) || window.panes.length === 0) return 'missing-window-panes';
101
- const paneIds = window.panes.map((pane) => pane?.id);
102
- if (paneIds.some((paneId) => typeof paneId !== 'string' || !paneId)) return 'invalid-pane';
103
- if (new Set(paneIds).size !== paneIds.length) return 'duplicate-pane';
104
- if (!paneIds.includes(window.activePaneId)) return 'dangling-active-pane';
105
- }
106
- return null;
107
- }
108
-
109
- function sourceProjection(session) {
110
- const validSession = session && typeof session === 'object' && !Array.isArray(session);
111
- const rawLinks = validSession && Array.isArray(session.windowLinks) ? session.windowLinks : [];
112
- const source = {
113
- logicalId: nonEmptyStringOrNull(validSession ? session.id : null),
114
- sourceName: nonEmptyStringOrNull(validSession ? session.name : null),
115
- activeWindowId: nonEmptyStringOrNull(validSession ? session.activeWindowId : null),
116
- windowLinks: rawLinks.map((link) => ({
117
- windowId: nonEmptyStringOrNull(link && typeof link === 'object' && !Array.isArray(link) ? link.windowId : null),
118
- index: indexOrNull(link && typeof link === 'object' && !Array.isArray(link) ? link.index : null),
119
- })),
120
- };
121
- let issue = null;
122
- if (!validSession) issue = 'invalid-session';
123
- else if (source.logicalId === null) issue = 'invalid-session-id';
124
- else if (source.sourceName === null) issue = 'invalid-session-name';
125
- else if (source.activeWindowId === null) issue = 'invalid-active-window-id';
126
- else if (!Array.isArray(session.windowLinks)) issue = 'invalid-window-links';
127
- else if (source.windowLinks.some((link) => link.windowId === null)) issue = 'invalid-window-link';
128
- else if (source.windowLinks.some((link) => link.index === null)) issue = 'invalid-window-link-index';
129
- return { source, issue };
130
- }
131
-
132
- function activeProjection(active) {
133
- if (active === null || active === undefined) return null;
134
- return {
135
- sessionId: stringOrNull(active.sessionId),
136
- windowId: stringOrNull(active.windowId),
137
- paneId: stringOrNull(active.paneId),
138
- };
139
- }
140
-
141
- function selectedTopology(items, windowsById) {
142
- const windowIds = new Set();
143
- for (const item of items) {
144
- if (item.action !== 'create' && item.action !== 'create-renamed') continue;
145
- for (const link of item.windowLinks) windowIds.add(link.windowId);
146
- }
147
- return [...windowIds].map((id) => windowsById.get(id)).filter(Boolean);
148
- }
149
-
150
- function windowDispositions(items, live) {
151
- const liveWindows = new Map((Array.isArray(live?.windows) ? live.windows : []).map((window) => [window.id, window.runtimeId]));
152
- const dispositions = new Map();
153
- for (const item of items) {
154
- if (item.action !== 'create' && item.action !== 'create-renamed') continue;
155
- for (const { windowId } of item.windowLinks) {
156
- if (dispositions.has(windowId)) continue;
157
- if (liveWindows.has(windowId)) {
158
- const runtimeId = nonEmptyStringOrNull(liveWindows.get(windowId));
159
- if (runtimeId === null) throw new Error('invalid live window runtime id');
160
- dispositions.set(windowId, { logicalId: nonEmptyStringOrNull(windowId), action: 'reuse', runtimeId });
161
- } else {
162
- dispositions.set(windowId, {
163
- logicalId: nonEmptyStringOrNull(windowId),
164
- action: 'create',
165
- ownerSessionId: nonEmptyStringOrNull(item.logicalId),
166
- });
167
- }
168
- }
169
- }
170
- return [...dispositions.values()];
171
- }
172
-
173
- export function buildRestorePlan(checkpointInput, live, {
174
- sessionNames = [],
175
- recovery = null,
176
- historical = false,
177
- supportsLinkedWindows = true,
178
- warnings = [],
179
- warning = null,
180
- } = {}) {
181
- const unwrapped = unwrapCheckpoint(checkpointInput);
182
- const checkpoint = unwrapped.checkpoint;
183
- if (!checkpoint || !Array.isArray(checkpoint.sessions) || !Array.isArray(checkpoint.windows)) {
184
- throw new Error('invalid checkpoint for restore planning');
185
- }
186
- if (recovery !== null && recovery?.checkpointId !== checkpoint.id) {
187
- throw new Error('recovery checkpoint id mismatch');
188
- }
189
-
190
- const windowsById = new Map(checkpoint.windows.map((window) => [window.id, window]));
191
- const owners = linkedOwners(checkpoint.sessions);
192
- const pendingIds = recovery && Array.isArray(recovery.pendingSessionIds)
193
- ? new Set(recovery.pendingSessionIds)
194
- : null;
195
- const requestedNames = new Set(Array.isArray(sessionNames) ? sessionNames : [sessionNames]);
196
- const selected = checkpoint.sessions.filter((session) => {
197
- if (!historical && pendingIds && !pendingIds.has(session?.id)) return false;
198
- return requestedNames.size === 0 || requestedNames.has(session?.name);
199
- });
200
-
201
- const currentSessions = Array.isArray(live?.sessions) ? live.sessions : [];
202
- const logicalIds = new Set(currentSessions.map((session) => session.id));
203
- const names = new Set(currentSessions.map((session) => session.name));
204
- const sessions = selected.map((session) => {
205
- const { source, issue } = sourceProjection(session);
206
- if (issue) return { ...source, action: 'unsupported', reason: issue };
207
- if (logicalIds.has(source.logicalId)) return { ...source, action: 'already-present' };
208
- const reason = topologyIssue(source, windowsById, owners, supportsLinkedWindows);
209
- if (reason) return { ...source, action: 'unsupported', reason };
210
- const targetName = restoredName(source.sourceName, names);
211
- names.add(targetName);
212
- return {
213
- ...source,
214
- targetName,
215
- action: targetName === source.sourceName ? 'create' : 'create-renamed',
216
- };
217
- });
218
-
219
- const actions = { create: 0, renamed: 0, alreadyPresent: 0, unsupported: 0 };
220
- for (const session of sessions) {
221
- if (session.action === 'create') actions.create += 1;
222
- else if (session.action === 'create-renamed') actions.renamed += 1;
223
- else if (session.action === 'already-present') actions.alreadyPresent += 1;
224
- else if (session.action === 'unsupported') actions.unsupported += 1;
225
- }
226
-
227
- const checkpointTopology = countTopology(checkpoint.windows);
228
- const planTopology = countTopology(selectedTopology(sessions, windowsById));
229
- const plan = {
230
- checkpointId: stringOrNull(checkpoint.id),
231
- capturedAt: stringOrNull(checkpoint.capturedAt),
232
- archivedAt: stringOrNull(checkpoint.archivedAt),
233
- changeReason: stringOrNull(checkpoint.environment?.endedReason),
234
- detectedAt: stringOrNull(recovery?.detectedAt),
235
- expiresAt: stringOrNull(recovery?.expiresAt),
236
- resolved: Boolean(recovery?.resolvedAt),
237
- pendingCount: pendingIds?.size ?? null,
238
- summary: { sessions: checkpoint.sessions.length, ...checkpointTopology },
239
- planSummary: { ...actions, ...planTopology },
240
- sessions,
241
- windows: windowDispositions(sessions, live),
242
- active: activeProjection(checkpoint.active),
243
- preExistingRuntimeIds: runtimeIds(live),
244
- warnings: warningList(unwrapped.warnings, checkpoint.warnings, checkpoint.warning, warnings, warning),
245
- };
246
- return deepFreeze(plan);
247
- }