aicomputer 0.1.22 → 0.2.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 +55 -136
- package/dist/index.d.ts +381 -1
- package/dist/index.js +1657 -4977
- package/package.json +24 -31
- package/dist/chunk-3ZF7JRBW.js +0 -270
- package/dist/chunk-5Y2NWK5I.js +0 -14
- package/dist/chunk-E7QD4MHI.js +0 -279
- package/dist/chunk-G7UQLVUZ.js +0 -75
- package/dist/chunk-GD42GHW3.js +0 -183
- package/dist/chunk-GGBVVRLL.js +0 -32
- package/dist/chunk-HDZTFK4U.js +0 -544
- package/dist/chunk-JMRAYXUO.js +0 -62
- package/dist/chunk-KXLTHWW3.js +0 -184
- package/dist/chunk-LGJN26BQ.js +0 -242
- package/dist/chunk-TPFE3CC6.js +0 -367
- package/dist/lib/autossh-runtime.d.ts +0 -21
- package/dist/lib/autossh-runtime.js +0 -23
- package/dist/lib/mount-config.d.ts +0 -79
- package/dist/lib/mount-config.js +0 -40
- package/dist/lib/mount-host.d.ts +0 -13
- package/dist/lib/mount-host.js +0 -10
- package/dist/lib/mount-mutagen.d.ts +0 -39
- package/dist/lib/mount-mutagen.js +0 -25
- package/dist/lib/mount-reconcile.d.ts +0 -30
- package/dist/lib/mount-reconcile.js +0 -17
- package/dist/lib/mutagen-runtime.d.ts +0 -20
- package/dist/lib/mutagen-runtime.js +0 -19
- package/dist/lib/ssh-access.d.ts +0 -74
- package/dist/lib/ssh-access.js +0 -25
- package/dist/lib/ssh-config.d.ts +0 -14
- package/dist/lib/ssh-config.js +0 -10
- package/dist/lib/upgrade-version.d.ts +0 -10
- package/dist/lib/upgrade-version.js +0 -8
- package/scripts/postinstall.mjs +0 -48
package/dist/chunk-HDZTFK4U.js
DELETED
|
@@ -1,544 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createHandleSession,
|
|
3
|
-
ensureMutagenSshEnvironment,
|
|
4
|
-
getHandleSessionName,
|
|
5
|
-
isAbortError,
|
|
6
|
-
listOwnedSessions,
|
|
7
|
-
selectPreferredSession,
|
|
8
|
-
terminateSession
|
|
9
|
-
} from "./chunk-TPFE3CC6.js";
|
|
10
|
-
import {
|
|
11
|
-
readMountHandleMeta,
|
|
12
|
-
readMountStatusSnapshot,
|
|
13
|
-
removeMountHandleState,
|
|
14
|
-
writeMountHandleMeta,
|
|
15
|
-
writeMountStatusSnapshot
|
|
16
|
-
} from "./chunk-KXLTHWW3.js";
|
|
17
|
-
import {
|
|
18
|
-
getConnectionInfo,
|
|
19
|
-
listComputers
|
|
20
|
-
} from "./chunk-E7QD4MHI.js";
|
|
21
|
-
|
|
22
|
-
// src/lib/mount-reconcile.ts
|
|
23
|
-
import { readdir, mkdir, rm } from "fs/promises";
|
|
24
|
-
import { join } from "path";
|
|
25
|
-
|
|
26
|
-
// src/lib/computer-picker.ts
|
|
27
|
-
import { select } from "@inquirer/prompts";
|
|
28
|
-
import chalk2 from "chalk";
|
|
29
|
-
|
|
30
|
-
// src/lib/format.ts
|
|
31
|
-
import chalk from "chalk";
|
|
32
|
-
function padEnd(str, len) {
|
|
33
|
-
const visible = str.replace(/\u001b\[[0-9;]*m/g, "");
|
|
34
|
-
return str + " ".repeat(Math.max(0, len - visible.length));
|
|
35
|
-
}
|
|
36
|
-
function timeAgo(dateStr) {
|
|
37
|
-
const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1e3);
|
|
38
|
-
if (seconds < 60) return `${seconds}s ago`;
|
|
39
|
-
const minutes = Math.floor(seconds / 60);
|
|
40
|
-
if (minutes < 60) return `${minutes}m ago`;
|
|
41
|
-
const hours = Math.floor(minutes / 60);
|
|
42
|
-
if (hours < 24) return `${hours}h ago`;
|
|
43
|
-
const days = Math.floor(hours / 24);
|
|
44
|
-
return `${days}d ago`;
|
|
45
|
-
}
|
|
46
|
-
function formatStatus(status) {
|
|
47
|
-
switch (status) {
|
|
48
|
-
case "running":
|
|
49
|
-
return chalk.green(status);
|
|
50
|
-
case "pending":
|
|
51
|
-
case "provisioning":
|
|
52
|
-
case "starting":
|
|
53
|
-
return chalk.yellow(status);
|
|
54
|
-
case "stopping":
|
|
55
|
-
case "stopped":
|
|
56
|
-
case "deleted":
|
|
57
|
-
return chalk.gray(status);
|
|
58
|
-
case "error":
|
|
59
|
-
return chalk.red(status);
|
|
60
|
-
default:
|
|
61
|
-
return status;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// src/lib/computer-picker.ts
|
|
66
|
-
async function promptForSSHComputer(computers, message) {
|
|
67
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
68
|
-
throw new Error("computer id or handle is required when not running interactively");
|
|
69
|
-
}
|
|
70
|
-
const available = computers.filter(isSSHSelectable);
|
|
71
|
-
if (available.length === 0) {
|
|
72
|
-
if (computers.length === 0) {
|
|
73
|
-
throw new Error("no computers found");
|
|
74
|
-
}
|
|
75
|
-
throw new Error("no running computers with SSH enabled");
|
|
76
|
-
}
|
|
77
|
-
const handleWidth = Math.max(6, ...available.map((computer) => computer.handle.length));
|
|
78
|
-
const selectedID = await select({
|
|
79
|
-
message,
|
|
80
|
-
pageSize: Math.min(available.length, 10),
|
|
81
|
-
choices: available.map((computer) => ({
|
|
82
|
-
name: `${padEnd(chalk2.white(computer.handle), handleWidth + 2)}${padEnd(formatStatus(computer.status), 12)}${chalk2.dim(describeSSHChoice(computer))}`,
|
|
83
|
-
value: computer.id
|
|
84
|
-
}))
|
|
85
|
-
});
|
|
86
|
-
return available.find((computer) => computer.id === selectedID) ?? available[0];
|
|
87
|
-
}
|
|
88
|
-
function isSSHSelectable(computer) {
|
|
89
|
-
return computer.ssh_enabled && computer.status === "running";
|
|
90
|
-
}
|
|
91
|
-
function describeSSHChoice(computer) {
|
|
92
|
-
const displayName = computer.display_name.trim();
|
|
93
|
-
if (displayName && displayName !== computer.handle) {
|
|
94
|
-
return `${displayName} ${timeAgo(computer.updated_at)}`;
|
|
95
|
-
}
|
|
96
|
-
return `${computer.runtime_family} ${timeAgo(computer.updated_at)}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// src/lib/mount-reconcile.ts
|
|
100
|
-
function computeMountPlan(input) {
|
|
101
|
-
const desiredHandles = new Set(input.desired.map((entry) => entry.handle));
|
|
102
|
-
const sessionGroups = /* @__PURE__ */ new Map();
|
|
103
|
-
for (const session of input.ownedSessions) {
|
|
104
|
-
const group = sessionGroups.get(session.handle);
|
|
105
|
-
if (group) {
|
|
106
|
-
group.push(session);
|
|
107
|
-
} else {
|
|
108
|
-
sessionGroups.set(session.handle, [session]);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
const toCreate = [];
|
|
112
|
-
const toInspect = [];
|
|
113
|
-
const toReset = /* @__PURE__ */ new Set();
|
|
114
|
-
for (const entry of input.desired) {
|
|
115
|
-
const sessions = sessionGroups.get(entry.handle) ?? [];
|
|
116
|
-
const reusable = sessions.length === 1 && !sessions[0].legacy;
|
|
117
|
-
if (reusable) {
|
|
118
|
-
toInspect.push(entry);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
if (sessions.length > 0) {
|
|
122
|
-
toReset.add(entry.handle);
|
|
123
|
-
}
|
|
124
|
-
toCreate.push(entry);
|
|
125
|
-
}
|
|
126
|
-
const toStop = /* @__PURE__ */ new Set();
|
|
127
|
-
for (const session of input.ownedSessions) {
|
|
128
|
-
if (!desiredHandles.has(session.handle)) {
|
|
129
|
-
toStop.add(session.handle);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
for (const entry of input.rootEntries) {
|
|
133
|
-
if (!desiredHandles.has(entry)) {
|
|
134
|
-
toStop.add(entry);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
for (const handle of toReset) {
|
|
138
|
-
toStop.delete(handle);
|
|
139
|
-
}
|
|
140
|
-
return {
|
|
141
|
-
toCreate,
|
|
142
|
-
toInspect,
|
|
143
|
-
toReset: Array.from(toReset).sort(),
|
|
144
|
-
toStop: Array.from(toStop).sort(),
|
|
145
|
-
pending: input.pending
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
async function planMountReconcile(config, paths, signal) {
|
|
149
|
-
const computers = await listComputers(signal);
|
|
150
|
-
const desired = [];
|
|
151
|
-
const pending = [];
|
|
152
|
-
for (const computer of computers.filter(isSSHSelectable)) {
|
|
153
|
-
const mountPath = join(paths.rootPath, computer.handle);
|
|
154
|
-
try {
|
|
155
|
-
const info = await getConnectionInfo(computer.id, signal);
|
|
156
|
-
if (!info.connection.ssh_available) {
|
|
157
|
-
pending.push({
|
|
158
|
-
handle: computer.handle,
|
|
159
|
-
mountPath,
|
|
160
|
-
ready: false,
|
|
161
|
-
message: "SSH is not ready yet"
|
|
162
|
-
});
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
} catch (error) {
|
|
166
|
-
pending.push({
|
|
167
|
-
handle: computer.handle,
|
|
168
|
-
mountPath,
|
|
169
|
-
ready: false,
|
|
170
|
-
message: error instanceof Error ? error.message : "Failed to confirm SSH readiness"
|
|
171
|
-
});
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
desired.push({
|
|
175
|
-
handle: computer.handle,
|
|
176
|
-
mountPath,
|
|
177
|
-
ready: true
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
throwIfAborted(signal);
|
|
181
|
-
const ownedSessions = await listOwnedSessions(config, paths, signal);
|
|
182
|
-
const rootEntries = await listRootHandleDirectories(paths.rootPath);
|
|
183
|
-
return {
|
|
184
|
-
plan: computeMountPlan({
|
|
185
|
-
desired,
|
|
186
|
-
pending,
|
|
187
|
-
ownedSessions,
|
|
188
|
-
rootEntries
|
|
189
|
-
}),
|
|
190
|
-
ownedSessions
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
async function reconcileMounts(config, paths, controllerPid = process.pid, signal) {
|
|
194
|
-
ensureMutagenSshEnvironment(config, paths);
|
|
195
|
-
throwIfAborted(signal);
|
|
196
|
-
const { plan, ownedSessions } = await planMountReconcile(config, paths, signal);
|
|
197
|
-
const mounts = [];
|
|
198
|
-
const issues = [];
|
|
199
|
-
const ownedByHandle = groupSessionsByHandle(ownedSessions);
|
|
200
|
-
for (const handle of [...plan.toStop, ...plan.toReset]) {
|
|
201
|
-
throwIfAborted(signal);
|
|
202
|
-
const sessions = ownedByHandle.get(handle) ?? [];
|
|
203
|
-
try {
|
|
204
|
-
await terminateOwnedSessions(sessions, config, paths, signal);
|
|
205
|
-
await removeHandleFromRoot(handle, paths);
|
|
206
|
-
removeMountHandleState(handle, config.rootPath);
|
|
207
|
-
} catch (error) {
|
|
208
|
-
if (isAbortError(error)) {
|
|
209
|
-
throw error;
|
|
210
|
-
}
|
|
211
|
-
issues.push(
|
|
212
|
-
error instanceof Error ? `${handle}: ${error.message}` : `${handle}: teardown failed`
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
for (const entry of plan.toCreate) {
|
|
217
|
-
throwIfAborted(signal);
|
|
218
|
-
try {
|
|
219
|
-
await ensureMountedHandle(entry, config, paths, signal);
|
|
220
|
-
const session = selectPreferredSession(
|
|
221
|
-
entry.handle,
|
|
222
|
-
(await listOwnedSessions(config, paths, signal)).filter(
|
|
223
|
-
(candidate) => candidate.handle === entry.handle
|
|
224
|
-
)
|
|
225
|
-
);
|
|
226
|
-
const inspected = inspectSnapshotEntry(session, entry.mountPath, config.rootPath);
|
|
227
|
-
mounts.push(inspected.snapshot);
|
|
228
|
-
if (inspected.issue) {
|
|
229
|
-
issues.push(inspected.issue);
|
|
230
|
-
}
|
|
231
|
-
} catch (error) {
|
|
232
|
-
if (isAbortError(error)) {
|
|
233
|
-
throw error;
|
|
234
|
-
}
|
|
235
|
-
const message = error instanceof Error ? error.message : "sync start failed";
|
|
236
|
-
mounts.push(snapshotEntry(entry.handle, entry.mountPath, "error", message));
|
|
237
|
-
issues.push(`${entry.handle}: ${message}`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
throwIfAborted(signal);
|
|
241
|
-
const postCreateSessions = groupSessionsByHandle(
|
|
242
|
-
await listOwnedSessions(config, paths, signal)
|
|
243
|
-
);
|
|
244
|
-
for (const entry of plan.toInspect) {
|
|
245
|
-
throwIfAborted(signal);
|
|
246
|
-
try {
|
|
247
|
-
const sessions = postCreateSessions.get(entry.handle) ?? [];
|
|
248
|
-
if (sessions.length === 0) {
|
|
249
|
-
await ensureMountedHandle(entry, config, paths, signal);
|
|
250
|
-
const createdSession = selectPreferredSession(
|
|
251
|
-
entry.handle,
|
|
252
|
-
(await listOwnedSessions(config, paths, signal)).filter(
|
|
253
|
-
(candidate) => candidate.handle === entry.handle
|
|
254
|
-
)
|
|
255
|
-
);
|
|
256
|
-
const inspected2 = inspectSnapshotEntry(
|
|
257
|
-
createdSession,
|
|
258
|
-
entry.mountPath,
|
|
259
|
-
config.rootPath
|
|
260
|
-
);
|
|
261
|
-
mounts.push(inspected2.snapshot);
|
|
262
|
-
if (inspected2.issue) {
|
|
263
|
-
issues.push(inspected2.issue);
|
|
264
|
-
}
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
const session = selectPreferredSession(entry.handle, sessions);
|
|
268
|
-
const inspected = inspectSnapshotEntry(session, entry.mountPath, config.rootPath);
|
|
269
|
-
mounts.push(inspected.snapshot);
|
|
270
|
-
if (inspected.issue) {
|
|
271
|
-
issues.push(inspected.issue);
|
|
272
|
-
}
|
|
273
|
-
} catch (error) {
|
|
274
|
-
if (isAbortError(error)) {
|
|
275
|
-
throw error;
|
|
276
|
-
}
|
|
277
|
-
const message = error instanceof Error ? error.message : "sync inspection failed";
|
|
278
|
-
mounts.push(snapshotEntry(entry.handle, entry.mountPath, "error", message));
|
|
279
|
-
issues.push(`${entry.handle}: ${message}`);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
for (const entry of plan.pending) {
|
|
283
|
-
mounts.push(snapshotEntry(entry.handle, entry.mountPath, "pending", entry.message));
|
|
284
|
-
}
|
|
285
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
286
|
-
const previousSnapshot = readMountStatusSnapshot(config.rootPath);
|
|
287
|
-
const healthy = mounts.every((mount) => mount.state === "mounted" || mount.state === "pending");
|
|
288
|
-
const startupInProgress = previousSnapshot?.startupPhase !== "ready" && mounts.some((mount) => mount.state === "syncing" || mount.state === "reconnecting");
|
|
289
|
-
const snapshot = {
|
|
290
|
-
updatedAt: now,
|
|
291
|
-
controllerPid,
|
|
292
|
-
running: true,
|
|
293
|
-
startupPhase: startupInProgress ? "syncing" : "ready",
|
|
294
|
-
startupMessage: startupInProgress ? createInitialSyncMessage(mounts) : void 0,
|
|
295
|
-
lastHealthySyncAt: healthy ? now : previousSnapshot?.lastHealthySyncAt,
|
|
296
|
-
lastSuccessfulSyncAt: healthy ? now : previousSnapshot?.lastSuccessfulSyncAt,
|
|
297
|
-
lastIssueAt: issues.length > 0 ? now : previousSnapshot?.lastIssueAt,
|
|
298
|
-
lastIssue: issues[0],
|
|
299
|
-
lastError: mounts.some((mount) => mount.state === "error") ? issues[0] : void 0,
|
|
300
|
-
mounts: sortSnapshots(mounts)
|
|
301
|
-
};
|
|
302
|
-
writeMountStatusSnapshot(snapshot, config.rootPath);
|
|
303
|
-
return snapshot;
|
|
304
|
-
}
|
|
305
|
-
async function teardownManagedSessions(config, paths, signal) {
|
|
306
|
-
throwIfAborted(signal);
|
|
307
|
-
const ownedSessions = await listOwnedSessions(config, paths, signal);
|
|
308
|
-
await terminateOwnedSessions(ownedSessions, config, paths, signal);
|
|
309
|
-
const handles = new Set(ownedSessions.map((session) => session.handle));
|
|
310
|
-
for (const handle of await listKnownHandleStates(paths)) {
|
|
311
|
-
handles.add(handle);
|
|
312
|
-
}
|
|
313
|
-
for (const handle of await listRootHandleDirectories(paths.rootPath)) {
|
|
314
|
-
handles.add(handle);
|
|
315
|
-
}
|
|
316
|
-
for (const handle of handles) {
|
|
317
|
-
removeMountHandleState(handle, config.rootPath);
|
|
318
|
-
}
|
|
319
|
-
await rm(paths.rootPath, { recursive: true, force: true });
|
|
320
|
-
}
|
|
321
|
-
async function ensureMountedHandle(entry, config, paths, signal) {
|
|
322
|
-
await mkdir(entry.mountPath, { recursive: true });
|
|
323
|
-
await createHandleSession(entry.handle, config, paths, signal);
|
|
324
|
-
writeMountHandleMeta(
|
|
325
|
-
entry.handle,
|
|
326
|
-
{
|
|
327
|
-
handle: entry.handle,
|
|
328
|
-
sessionName: getHandleSessionName(entry.handle),
|
|
329
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
330
|
-
lastStartedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
331
|
-
},
|
|
332
|
-
config.rootPath
|
|
333
|
-
);
|
|
334
|
-
}
|
|
335
|
-
function inspectSnapshotEntry(session, mountPath, rootPath) {
|
|
336
|
-
const status = (session.status ?? "").toLowerCase();
|
|
337
|
-
const progressDetails = buildSyncProgressDetails(session, rootPath);
|
|
338
|
-
const problemParts = [];
|
|
339
|
-
if (session.conflictCount > 0) {
|
|
340
|
-
problemParts.push(
|
|
341
|
-
`${session.conflictCount} conflict${session.conflictCount === 1 ? "" : "s"}`
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
if (session.scanProblemCount > 0) {
|
|
345
|
-
problemParts.push(
|
|
346
|
-
`${session.scanProblemCount} scan problem${session.scanProblemCount === 1 ? "" : "s"}`
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
if (problemParts.length > 0) {
|
|
350
|
-
const message = problemParts.join(", ");
|
|
351
|
-
return {
|
|
352
|
-
snapshot: snapshotEntry(session.handle, mountPath, "degraded", message),
|
|
353
|
-
issue: `${session.handle}: ${message}`
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
if (!session.alphaConnected) {
|
|
357
|
-
const message = session.lastError ?? "local sync endpoint disconnected";
|
|
358
|
-
return {
|
|
359
|
-
snapshot: snapshotEntry(session.handle, mountPath, "error", message, {
|
|
360
|
-
status: session.status,
|
|
361
|
-
progress: session.stagingProgress,
|
|
362
|
-
currentFile: session.currentFile
|
|
363
|
-
}),
|
|
364
|
-
issue: `${session.handle}: ${message}`
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
if (!session.betaConnected || status.includes("connecting") || status.includes("reconnect")) {
|
|
368
|
-
const message = session.lastError ?? progressDetails.message ?? "connecting to remote machine";
|
|
369
|
-
return {
|
|
370
|
-
snapshot: snapshotEntry(session.handle, mountPath, "reconnecting", message, {
|
|
371
|
-
status: session.status,
|
|
372
|
-
progress: session.stagingProgress,
|
|
373
|
-
currentFile: session.currentFile,
|
|
374
|
-
etaSeconds: progressDetails.etaSeconds
|
|
375
|
-
})
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
if (isMutagenSyncInProgress(status)) {
|
|
379
|
-
return {
|
|
380
|
-
snapshot: snapshotEntry(
|
|
381
|
-
session.handle,
|
|
382
|
-
mountPath,
|
|
383
|
-
"syncing",
|
|
384
|
-
progressDetails.message ?? session.status ?? "initial sync in progress",
|
|
385
|
-
{
|
|
386
|
-
status: session.status,
|
|
387
|
-
progress: session.stagingProgress,
|
|
388
|
-
currentFile: session.currentFile,
|
|
389
|
-
etaSeconds: progressDetails.etaSeconds
|
|
390
|
-
}
|
|
391
|
-
)
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
if (session.lastError && !status.includes("watching")) {
|
|
395
|
-
return {
|
|
396
|
-
snapshot: snapshotEntry(session.handle, mountPath, "degraded", session.lastError, {
|
|
397
|
-
status: session.status,
|
|
398
|
-
progress: session.stagingProgress,
|
|
399
|
-
currentFile: session.currentFile
|
|
400
|
-
}),
|
|
401
|
-
issue: `${session.handle}: ${session.lastError}`
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
return {
|
|
405
|
-
snapshot: snapshotEntry(session.handle, mountPath, "mounted", void 0, {
|
|
406
|
-
status: session.status
|
|
407
|
-
})
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
function snapshotEntry(handle, mountPath, state, message, extra = {}) {
|
|
411
|
-
return {
|
|
412
|
-
handle,
|
|
413
|
-
mountPath,
|
|
414
|
-
state,
|
|
415
|
-
message,
|
|
416
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
417
|
-
...extra
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
function isMutagenSyncInProgress(status) {
|
|
421
|
-
return [
|
|
422
|
-
"staging",
|
|
423
|
-
"scanning",
|
|
424
|
-
"reconciling",
|
|
425
|
-
"applying",
|
|
426
|
-
"saving",
|
|
427
|
-
"waiting"
|
|
428
|
-
].some((phase) => status.includes(phase));
|
|
429
|
-
}
|
|
430
|
-
function buildSyncProgressDetails(session, rootPath) {
|
|
431
|
-
const details = [];
|
|
432
|
-
if (session.status) {
|
|
433
|
-
details.push(session.status);
|
|
434
|
-
}
|
|
435
|
-
if (session.stagingProgress) {
|
|
436
|
-
details.push(session.stagingProgress);
|
|
437
|
-
}
|
|
438
|
-
if (session.currentFile) {
|
|
439
|
-
details.push(session.currentFile);
|
|
440
|
-
}
|
|
441
|
-
const startedAt = readMountHandleMeta(session.handle, rootPath)?.lastStartedAt;
|
|
442
|
-
const etaSeconds = estimateEtaSeconds(startedAt, session.stagingProgress);
|
|
443
|
-
if (etaSeconds !== void 0) {
|
|
444
|
-
details.push(`eta ~${formatSeconds(etaSeconds)}`);
|
|
445
|
-
}
|
|
446
|
-
return {
|
|
447
|
-
message: details.length > 0 ? details.join("; ") : void 0,
|
|
448
|
-
etaSeconds
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
function estimateEtaSeconds(startedAt, stagingProgress) {
|
|
452
|
-
if (!startedAt || !stagingProgress) {
|
|
453
|
-
return void 0;
|
|
454
|
-
}
|
|
455
|
-
const match = stagingProgress.match(/(\d+)%/);
|
|
456
|
-
if (!match) {
|
|
457
|
-
return void 0;
|
|
458
|
-
}
|
|
459
|
-
const percent = Number.parseInt(match[1] ?? "", 10);
|
|
460
|
-
if (!Number.isFinite(percent) || percent <= 0 || percent >= 100) {
|
|
461
|
-
return void 0;
|
|
462
|
-
}
|
|
463
|
-
const elapsedSeconds = Math.max(
|
|
464
|
-
0,
|
|
465
|
-
(Date.now() - new Date(startedAt).getTime()) / 1e3
|
|
466
|
-
);
|
|
467
|
-
if (!Number.isFinite(elapsedSeconds) || elapsedSeconds < 1) {
|
|
468
|
-
return void 0;
|
|
469
|
-
}
|
|
470
|
-
return Math.max(1, Math.round(elapsedSeconds * (100 - percent) / percent));
|
|
471
|
-
}
|
|
472
|
-
function formatSeconds(seconds) {
|
|
473
|
-
if (seconds < 60) {
|
|
474
|
-
return `${seconds}s`;
|
|
475
|
-
}
|
|
476
|
-
const minutes = Math.floor(seconds / 60);
|
|
477
|
-
const remainder = seconds % 60;
|
|
478
|
-
return remainder === 0 ? `${minutes}m` : `${minutes}m ${remainder}s`;
|
|
479
|
-
}
|
|
480
|
-
function createInitialSyncMessage(mounts) {
|
|
481
|
-
const actionable = mounts.filter((mount) => mount.state !== "pending");
|
|
482
|
-
const readyCount = actionable.filter((mount) => mount.state === "mounted").length;
|
|
483
|
-
const active = actionable.find((mount) => mount.state === "syncing") ?? actionable.find((mount) => mount.state === "reconnecting");
|
|
484
|
-
if (!active) {
|
|
485
|
-
return "Waiting for initial sync...";
|
|
486
|
-
}
|
|
487
|
-
const progressPrefix = actionable.length > 0 ? `${readyCount}/${actionable.length} ready; ` : "";
|
|
488
|
-
return `Waiting for initial sync... ${progressPrefix}${active.handle}: ${active.message ?? active.state}`;
|
|
489
|
-
}
|
|
490
|
-
function sortSnapshots(mounts) {
|
|
491
|
-
return mounts.slice().sort((left, right) => left.handle.localeCompare(right.handle));
|
|
492
|
-
}
|
|
493
|
-
function groupSessionsByHandle(sessions) {
|
|
494
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
495
|
-
for (const session of sessions) {
|
|
496
|
-
const group = grouped.get(session.handle);
|
|
497
|
-
if (group) {
|
|
498
|
-
group.push(session);
|
|
499
|
-
} else {
|
|
500
|
-
grouped.set(session.handle, [session]);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
return grouped;
|
|
504
|
-
}
|
|
505
|
-
function terminateOwnedSessions(sessions, config, paths, signal) {
|
|
506
|
-
return Promise.all(
|
|
507
|
-
sessions.map((session) => terminateSession(session, config, paths, signal))
|
|
508
|
-
).then(() => void 0);
|
|
509
|
-
}
|
|
510
|
-
async function removeHandleFromRoot(handle, paths) {
|
|
511
|
-
await rm(join(paths.rootPath, handle), { recursive: true, force: true });
|
|
512
|
-
}
|
|
513
|
-
async function listRootHandleDirectories(rootPath) {
|
|
514
|
-
try {
|
|
515
|
-
return (await readdir(rootPath, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
516
|
-
} catch {
|
|
517
|
-
return [];
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
async function listKnownHandleStates(paths) {
|
|
521
|
-
try {
|
|
522
|
-
return (await readdir(paths.handlesDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
523
|
-
} catch {
|
|
524
|
-
return [];
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
function throwIfAborted(signal) {
|
|
528
|
-
if (signal?.aborted) {
|
|
529
|
-
const error = new Error("mount operation cancelled");
|
|
530
|
-
error.name = "AbortError";
|
|
531
|
-
throw error;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
export {
|
|
536
|
-
padEnd,
|
|
537
|
-
timeAgo,
|
|
538
|
-
formatStatus,
|
|
539
|
-
promptForSSHComputer,
|
|
540
|
-
computeMountPlan,
|
|
541
|
-
planMountReconcile,
|
|
542
|
-
reconcileMounts,
|
|
543
|
-
teardownManagedSessions
|
|
544
|
-
};
|
package/dist/chunk-JMRAYXUO.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// src/lib/mount-host.ts
|
|
2
|
-
import { spawnSync } from "child_process";
|
|
3
|
-
function getMountHostValidationIssues() {
|
|
4
|
-
return evaluateMountHostValidation({
|
|
5
|
-
platform: process.platform,
|
|
6
|
-
hasSsh: hasCommand("ssh"),
|
|
7
|
-
hasScp: hasCommand("scp")
|
|
8
|
-
});
|
|
9
|
-
}
|
|
10
|
-
function formatMountHostInstallGuidance(issues) {
|
|
11
|
-
const lines = [];
|
|
12
|
-
for (const issue of issues) {
|
|
13
|
-
switch (issue.code) {
|
|
14
|
-
case "unsupported-platform":
|
|
15
|
-
lines.push(
|
|
16
|
-
"Supported today: macOS and Linux terminals with bundled Mutagen plus OpenSSH client tools available."
|
|
17
|
-
);
|
|
18
|
-
break;
|
|
19
|
-
case "missing-ssh":
|
|
20
|
-
case "missing-scp":
|
|
21
|
-
lines.push("Install OpenSSH client tools so both `ssh` and `scp` are available on PATH.");
|
|
22
|
-
break;
|
|
23
|
-
default:
|
|
24
|
-
lines.push(issue.message);
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return lines;
|
|
29
|
-
}
|
|
30
|
-
function evaluateMountHostValidation(input) {
|
|
31
|
-
const issues = [];
|
|
32
|
-
if (!["darwin", "linux"].includes(input.platform)) {
|
|
33
|
-
issues.push({
|
|
34
|
-
code: "unsupported-platform",
|
|
35
|
-
message: "computer mount currently supports macOS and Linux terminals only"
|
|
36
|
-
});
|
|
37
|
-
return issues;
|
|
38
|
-
}
|
|
39
|
-
if (!input.hasSsh) {
|
|
40
|
-
issues.push({
|
|
41
|
-
code: "missing-ssh",
|
|
42
|
-
message: "`ssh` is not installed or not available on PATH."
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
if (!input.hasScp) {
|
|
46
|
-
issues.push({
|
|
47
|
-
code: "missing-scp",
|
|
48
|
-
message: "`scp` is not installed or not available on PATH."
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
return issues;
|
|
52
|
-
}
|
|
53
|
-
function hasCommand(command) {
|
|
54
|
-
const result = spawnSync("which", [command], { stdio: "ignore" });
|
|
55
|
-
return result.status === 0;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export {
|
|
59
|
-
getMountHostValidationIssues,
|
|
60
|
-
formatMountHostInstallGuidance,
|
|
61
|
-
evaluateMountHostValidation
|
|
62
|
-
};
|