aicomputer 0.1.19 → 0.1.21
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 +28 -0
- package/dist/{chunk-5JYMQXKN.js → chunk-36BZXAAX.js} +129 -258
- package/dist/chunk-3ZF7JRBW.js +270 -0
- package/dist/chunk-4TE5XTYE.js +242 -0
- package/dist/chunk-5Y2NWK5I.js +14 -0
- package/dist/chunk-D3SAFNSI.js +75 -0
- package/dist/{chunk-MDSPJ57B.js → chunk-GD42GHW3.js} +4 -9
- package/dist/chunk-LOGK7YYJ.js +255 -0
- package/dist/{chunk-NN4GECN6.js → chunk-TPFE3CC6.js} +14 -1
- package/dist/index.js +664 -406
- package/dist/lib/autossh-runtime.d.ts +21 -0
- package/dist/lib/autossh-runtime.js +23 -0
- package/dist/lib/mount-config.d.ts +5 -1
- package/dist/lib/mount-mutagen.d.ts +2 -0
- package/dist/lib/mount-mutagen.js +3 -2
- package/dist/lib/mount-reconcile.js +5 -3
- package/dist/lib/mutagen-runtime.d.ts +1 -2
- package/dist/lib/mutagen-runtime.js +4 -5
- package/dist/lib/ssh-access.d.ts +74 -0
- package/dist/lib/ssh-access.js +25 -0
- package/dist/lib/ssh-config.d.ts +14 -0
- package/dist/lib/ssh-config.js +10 -0
- package/package.json +3 -3
- package/scripts/postinstall.mjs +36 -23
package/README.md
CHANGED
|
@@ -93,10 +93,14 @@ computer claude-login
|
|
|
93
93
|
computer codex-login
|
|
94
94
|
computer whoami
|
|
95
95
|
computer create my-box
|
|
96
|
+
computer power-off my-box
|
|
97
|
+
computer power-on my-box
|
|
96
98
|
computer open my-box
|
|
97
99
|
computer ssh
|
|
98
100
|
computer ssh my-box
|
|
101
|
+
computer ssh my-box --tmux
|
|
99
102
|
computer ssh --setup
|
|
103
|
+
computer ssh my-box -N -L 3000:localhost:3000
|
|
100
104
|
computer mount
|
|
101
105
|
computer mount --background
|
|
102
106
|
computer mount status
|
|
@@ -112,6 +116,10 @@ Codex credentials onto a machine after the CLI is already logged in. Use
|
|
|
112
116
|
`computer upgrade` to update a global npm install or the matching Nix profile
|
|
113
117
|
entry.
|
|
114
118
|
|
|
119
|
+
Use `computer power-off` to stop a managed worker without deleting its durable
|
|
120
|
+
home volume. Use `computer power-on` to recreate the runtime against the same
|
|
121
|
+
stored machine and home state.
|
|
122
|
+
|
|
115
123
|
Run `computer ssh` without a handle in an interactive terminal to pick from your available machines.
|
|
116
124
|
|
|
117
125
|
Run `computer ssh --setup` once to register your SSH key and add a global alias:
|
|
@@ -121,6 +129,26 @@ ssh agentcomputer.ai
|
|
|
121
129
|
ssh my-box@agentcomputer.ai
|
|
122
130
|
```
|
|
123
131
|
|
|
132
|
+
Use `computer ssh <handle> <ssh args>` when you want the CLI to resolve
|
|
133
|
+
the machine and identity while still passing standard SSH tunnel flags through
|
|
134
|
+
to the underlying client. Long-running SSH sessions and forwarded tunnels stay
|
|
135
|
+
on the same resilient path:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
computer ssh my-box -N -L 3000:localhost:3000
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`computer ssh` now launches through `autossh` by default. Nix installs package
|
|
142
|
+
`ssh`, `scp`, and `autossh` together. Global npm installs on macOS and Linux
|
|
143
|
+
provision Agent Computer's bundled `autossh` copy during postinstall and retry
|
|
144
|
+
that install on first SSH use if npm scripts were skipped. npm users still need
|
|
145
|
+
local OpenSSH client tools available.
|
|
146
|
+
|
|
147
|
+
Use `computer ssh <handle> --tmux` when you want reconnects to reattach to the
|
|
148
|
+
same remote shell session instead of dropping you into a fresh shell. The flag
|
|
149
|
+
attaches to `tmux new-session -A -s agentcomputer`, so it is meant for
|
|
150
|
+
interactive shells, not `-N` tunnels or other non-interactive SSH flows.
|
|
151
|
+
|
|
124
152
|
Run `computer mount` to start the mount controller in the foreground and mirror
|
|
125
153
|
all SSH-ready machine homes under `~/agentcomputer/<handle>`.
|
|
126
154
|
On macOS, the foreground command also opens Finder to `~/agentcomputer` after
|
|
@@ -6,235 +6,22 @@ import {
|
|
|
6
6
|
listOwnedSessions,
|
|
7
7
|
selectPreferredSession,
|
|
8
8
|
terminateSession
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-TPFE3CC6.js";
|
|
10
10
|
import {
|
|
11
|
+
readMountHandleMeta,
|
|
11
12
|
readMountStatusSnapshot,
|
|
12
13
|
removeMountHandleState,
|
|
13
14
|
writeMountHandleMeta,
|
|
14
15
|
writeMountStatusSnapshot
|
|
15
16
|
} from "./chunk-KXLTHWW3.js";
|
|
17
|
+
import {
|
|
18
|
+
getConnectionInfo,
|
|
19
|
+
listComputers
|
|
20
|
+
} from "./chunk-LOGK7YYJ.js";
|
|
16
21
|
|
|
17
22
|
// src/lib/mount-reconcile.ts
|
|
18
23
|
import { readdir, mkdir, rm } from "fs/promises";
|
|
19
|
-
import { join as join2 } from "path";
|
|
20
|
-
|
|
21
|
-
// src/lib/config.ts
|
|
22
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
23
|
-
import { homedir } from "os";
|
|
24
24
|
import { join } from "path";
|
|
25
|
-
var CONFIG_DIR = join(homedir(), ".computer");
|
|
26
|
-
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
27
|
-
function ensureConfigDir() {
|
|
28
|
-
if (!existsSync(CONFIG_DIR)) {
|
|
29
|
-
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function readConfig() {
|
|
33
|
-
ensureConfigDir();
|
|
34
|
-
if (!existsSync(CONFIG_FILE)) {
|
|
35
|
-
return {};
|
|
36
|
-
}
|
|
37
|
-
try {
|
|
38
|
-
return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
39
|
-
} catch {
|
|
40
|
-
return {};
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
function writeConfig(config) {
|
|
44
|
-
ensureConfigDir();
|
|
45
|
-
const tempFile = `${CONFIG_FILE}.${process.pid}.tmp`;
|
|
46
|
-
writeFileSync(tempFile, JSON.stringify(config, null, 2), { mode: 384 });
|
|
47
|
-
renameSync(tempFile, CONFIG_FILE);
|
|
48
|
-
}
|
|
49
|
-
function getAPIKey() {
|
|
50
|
-
const envValue = process.env.COMPUTER_API_KEY ?? process.env.AGENTCOMPUTER_API_KEY;
|
|
51
|
-
if (envValue) {
|
|
52
|
-
return envValue.trim();
|
|
53
|
-
}
|
|
54
|
-
return getStoredAPIKey();
|
|
55
|
-
}
|
|
56
|
-
function getStoredAPIKey() {
|
|
57
|
-
return readConfig().auth?.apiKey?.trim() || null;
|
|
58
|
-
}
|
|
59
|
-
function hasEnvAPIKey() {
|
|
60
|
-
return Boolean(process.env.COMPUTER_API_KEY ?? process.env.AGENTCOMPUTER_API_KEY);
|
|
61
|
-
}
|
|
62
|
-
function setAPIKey(apiKey) {
|
|
63
|
-
const config = readConfig();
|
|
64
|
-
config.auth = { apiKey: apiKey.trim() };
|
|
65
|
-
writeConfig(config);
|
|
66
|
-
}
|
|
67
|
-
function clearAPIKey() {
|
|
68
|
-
const config = readConfig();
|
|
69
|
-
delete config.auth;
|
|
70
|
-
writeConfig(config);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// src/lib/api.ts
|
|
74
|
-
var BASE_URL = process.env.COMPUTER_API_URL ?? process.env.AGENTCOMPUTER_API_URL ?? "https://api.computer.agentcomputer.ai";
|
|
75
|
-
var WEB_URL = process.env.COMPUTER_WEB_URL ?? process.env.AGENTCOMPUTER_WEB_URL ?? resolveDefaultWebURL(BASE_URL);
|
|
76
|
-
var ApiError = class extends Error {
|
|
77
|
-
constructor(status, message) {
|
|
78
|
-
super(message);
|
|
79
|
-
this.status = status;
|
|
80
|
-
this.name = "ApiError";
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
function getBaseURL() {
|
|
84
|
-
return BASE_URL;
|
|
85
|
-
}
|
|
86
|
-
function getWebURL() {
|
|
87
|
-
return WEB_URL;
|
|
88
|
-
}
|
|
89
|
-
async function api(path, options = {}) {
|
|
90
|
-
const apiKey = getAPIKey();
|
|
91
|
-
if (!apiKey) {
|
|
92
|
-
throw new ApiError(401, "not logged in; run 'computer login' first");
|
|
93
|
-
}
|
|
94
|
-
return requestWithKey(apiKey, path, options);
|
|
95
|
-
}
|
|
96
|
-
function resolveDefaultWebURL(apiURL) {
|
|
97
|
-
try {
|
|
98
|
-
const parsed = new URL(apiURL);
|
|
99
|
-
if (parsed.hostname === "api.computer.agentcomputer.ai") {
|
|
100
|
-
return "https://agentcomputer.ai";
|
|
101
|
-
}
|
|
102
|
-
if (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") {
|
|
103
|
-
return `${parsed.protocol}//${parsed.hostname}:3000`;
|
|
104
|
-
}
|
|
105
|
-
} catch {
|
|
106
|
-
return "https://agentcomputer.ai";
|
|
107
|
-
}
|
|
108
|
-
return "https://agentcomputer.ai";
|
|
109
|
-
}
|
|
110
|
-
async function apiWithKey(apiKey, path, options = {}) {
|
|
111
|
-
return requestWithKey(apiKey, path, options);
|
|
112
|
-
}
|
|
113
|
-
async function requestWithKey(apiKey, path, options) {
|
|
114
|
-
const headers = {
|
|
115
|
-
Accept: "application/json",
|
|
116
|
-
...options.headers ?? {}
|
|
117
|
-
};
|
|
118
|
-
if (options.body !== void 0) {
|
|
119
|
-
headers["Content-Type"] = "application/json";
|
|
120
|
-
}
|
|
121
|
-
if (apiKey) {
|
|
122
|
-
headers.Authorization = `Bearer ${apiKey}`;
|
|
123
|
-
}
|
|
124
|
-
const response = await fetch(`${BASE_URL}${path}`, {
|
|
125
|
-
...options,
|
|
126
|
-
headers
|
|
127
|
-
});
|
|
128
|
-
if (!response.ok) {
|
|
129
|
-
throw new ApiError(response.status, await readErrorMessage(response));
|
|
130
|
-
}
|
|
131
|
-
if (response.status === 204) {
|
|
132
|
-
return void 0;
|
|
133
|
-
}
|
|
134
|
-
return await response.json();
|
|
135
|
-
}
|
|
136
|
-
async function readErrorMessage(response) {
|
|
137
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
138
|
-
if (contentType.includes("application/json")) {
|
|
139
|
-
try {
|
|
140
|
-
const payload = await response.json();
|
|
141
|
-
if (payload.error) {
|
|
142
|
-
return payload.error;
|
|
143
|
-
}
|
|
144
|
-
return JSON.stringify(payload);
|
|
145
|
-
} catch {
|
|
146
|
-
return response.statusText || "request failed";
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
const body = await response.text();
|
|
150
|
-
return body || response.statusText || "request failed";
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// src/lib/computers.ts
|
|
154
|
-
async function listComputers(signal) {
|
|
155
|
-
const response = await api("/v1/computers", { signal });
|
|
156
|
-
return response.computers;
|
|
157
|
-
}
|
|
158
|
-
async function getComputerByID(id) {
|
|
159
|
-
return api(`/v1/computers/${id}`);
|
|
160
|
-
}
|
|
161
|
-
async function createComputer(input) {
|
|
162
|
-
return api("/v1/computers", {
|
|
163
|
-
method: "POST",
|
|
164
|
-
body: JSON.stringify(input)
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
async function deleteComputer(computerID) {
|
|
168
|
-
return api(`/v1/computers/${computerID}`, {
|
|
169
|
-
method: "DELETE"
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
async function getFilesystemSettings() {
|
|
173
|
-
return api("/v1/me/filesystem");
|
|
174
|
-
}
|
|
175
|
-
async function getConnectionInfo(computerID, signal) {
|
|
176
|
-
return api(`/v1/computers/${computerID}/connection`, {
|
|
177
|
-
signal
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
async function createBrowserAccess(computerID) {
|
|
181
|
-
return api(`/v1/computers/${computerID}/access/browser`, {
|
|
182
|
-
method: "POST"
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
async function listPublishedPorts(computerID) {
|
|
186
|
-
const response = await api(`/v1/computers/${computerID}/ports`);
|
|
187
|
-
return response.ports;
|
|
188
|
-
}
|
|
189
|
-
async function publishPort(computerID, input) {
|
|
190
|
-
return api(`/v1/computers/${computerID}/ports`, {
|
|
191
|
-
method: "POST",
|
|
192
|
-
body: JSON.stringify(input)
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
async function deletePublishedPort(computerID, targetPort) {
|
|
196
|
-
return api(`/v1/computers/${computerID}/ports/${targetPort}`, {
|
|
197
|
-
method: "DELETE"
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
async function resolveComputer(identifier) {
|
|
201
|
-
try {
|
|
202
|
-
return await getComputerByID(identifier);
|
|
203
|
-
} catch (error) {
|
|
204
|
-
if (!(error instanceof Error) || !("status" in error)) {
|
|
205
|
-
throw error;
|
|
206
|
-
}
|
|
207
|
-
const status = Reflect.get(error, "status");
|
|
208
|
-
if (status !== 404) {
|
|
209
|
-
throw error;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const computers = await listComputers();
|
|
213
|
-
const exact = computers.find(
|
|
214
|
-
(computer) => computer.handle === identifier || computer.id === identifier
|
|
215
|
-
);
|
|
216
|
-
if (exact) {
|
|
217
|
-
return exact;
|
|
218
|
-
}
|
|
219
|
-
throw new Error(`computer '${identifier}' not found`);
|
|
220
|
-
}
|
|
221
|
-
function webURL(computer) {
|
|
222
|
-
return `https://${computer.primary_web_host}${normalizePrimaryPath(computer.primary_path)}`;
|
|
223
|
-
}
|
|
224
|
-
function vncURL(computer) {
|
|
225
|
-
if (!computer.vnc_enabled) {
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
const domain = computer.primary_web_host.replace(/^[^.]+\./, "");
|
|
229
|
-
return `https://6080--${computer.handle}.${domain}`;
|
|
230
|
-
}
|
|
231
|
-
function normalizePrimaryPath(primaryPath) {
|
|
232
|
-
const trimmed = primaryPath?.trim();
|
|
233
|
-
if (!trimmed) {
|
|
234
|
-
return "/";
|
|
235
|
-
}
|
|
236
|
-
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
237
|
-
}
|
|
238
25
|
|
|
239
26
|
// src/lib/computer-picker.ts
|
|
240
27
|
import { select } from "@inquirer/prompts";
|
|
@@ -363,7 +150,7 @@ async function planMountReconcile(config, paths, signal) {
|
|
|
363
150
|
const desired = [];
|
|
364
151
|
const pending = [];
|
|
365
152
|
for (const computer of computers.filter(isSSHSelectable)) {
|
|
366
|
-
const mountPath =
|
|
153
|
+
const mountPath = join(paths.rootPath, computer.handle);
|
|
367
154
|
try {
|
|
368
155
|
const info = await getConnectionInfo(computer.id, signal);
|
|
369
156
|
if (!info.connection.ssh_available) {
|
|
@@ -436,7 +223,7 @@ async function reconcileMounts(config, paths, controllerPid = process.pid, signa
|
|
|
436
223
|
(candidate) => candidate.handle === entry.handle
|
|
437
224
|
)
|
|
438
225
|
);
|
|
439
|
-
const inspected = inspectSnapshotEntry(session, entry.mountPath);
|
|
226
|
+
const inspected = inspectSnapshotEntry(session, entry.mountPath, config.rootPath);
|
|
440
227
|
mounts.push(inspected.snapshot);
|
|
441
228
|
if (inspected.issue) {
|
|
442
229
|
issues.push(inspected.issue);
|
|
@@ -466,7 +253,11 @@ async function reconcileMounts(config, paths, controllerPid = process.pid, signa
|
|
|
466
253
|
(candidate) => candidate.handle === entry.handle
|
|
467
254
|
)
|
|
468
255
|
);
|
|
469
|
-
const inspected2 = inspectSnapshotEntry(
|
|
256
|
+
const inspected2 = inspectSnapshotEntry(
|
|
257
|
+
createdSession,
|
|
258
|
+
entry.mountPath,
|
|
259
|
+
config.rootPath
|
|
260
|
+
);
|
|
470
261
|
mounts.push(inspected2.snapshot);
|
|
471
262
|
if (inspected2.issue) {
|
|
472
263
|
issues.push(inspected2.issue);
|
|
@@ -474,7 +265,7 @@ async function reconcileMounts(config, paths, controllerPid = process.pid, signa
|
|
|
474
265
|
continue;
|
|
475
266
|
}
|
|
476
267
|
const session = selectPreferredSession(entry.handle, sessions);
|
|
477
|
-
const inspected = inspectSnapshotEntry(session, entry.mountPath);
|
|
268
|
+
const inspected = inspectSnapshotEntry(session, entry.mountPath, config.rootPath);
|
|
478
269
|
mounts.push(inspected.snapshot);
|
|
479
270
|
if (inspected.issue) {
|
|
480
271
|
issues.push(inspected.issue);
|
|
@@ -494,12 +285,13 @@ async function reconcileMounts(config, paths, controllerPid = process.pid, signa
|
|
|
494
285
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
495
286
|
const previousSnapshot = readMountStatusSnapshot(config.rootPath);
|
|
496
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");
|
|
497
289
|
const snapshot = {
|
|
498
290
|
updatedAt: now,
|
|
499
291
|
controllerPid,
|
|
500
292
|
running: true,
|
|
501
|
-
startupPhase: "ready",
|
|
502
|
-
startupMessage: void 0,
|
|
293
|
+
startupPhase: startupInProgress ? "syncing" : "ready",
|
|
294
|
+
startupMessage: startupInProgress ? createInitialSyncMessage(mounts) : void 0,
|
|
503
295
|
lastHealthySyncAt: healthy ? now : previousSnapshot?.lastHealthySyncAt,
|
|
504
296
|
lastSuccessfulSyncAt: healthy ? now : previousSnapshot?.lastSuccessfulSyncAt,
|
|
505
297
|
lastIssueAt: issues.length > 0 ? now : previousSnapshot?.lastIssueAt,
|
|
@@ -540,8 +332,9 @@ async function ensureMountedHandle(entry, config, paths, signal) {
|
|
|
540
332
|
config.rootPath
|
|
541
333
|
);
|
|
542
334
|
}
|
|
543
|
-
function inspectSnapshotEntry(session, mountPath) {
|
|
335
|
+
function inspectSnapshotEntry(session, mountPath, rootPath) {
|
|
544
336
|
const status = (session.status ?? "").toLowerCase();
|
|
337
|
+
const progressDetails = buildSyncProgressDetails(session, rootPath);
|
|
545
338
|
const problemParts = [];
|
|
546
339
|
if (session.conflictCount > 0) {
|
|
547
340
|
problemParts.push(
|
|
@@ -563,36 +356,137 @@ function inspectSnapshotEntry(session, mountPath) {
|
|
|
563
356
|
if (!session.alphaConnected) {
|
|
564
357
|
const message = session.lastError ?? "local sync endpoint disconnected";
|
|
565
358
|
return {
|
|
566
|
-
snapshot: snapshotEntry(session.handle, mountPath, "error", message
|
|
359
|
+
snapshot: snapshotEntry(session.handle, mountPath, "error", message, {
|
|
360
|
+
status: session.status,
|
|
361
|
+
progress: session.stagingProgress,
|
|
362
|
+
currentFile: session.currentFile
|
|
363
|
+
}),
|
|
567
364
|
issue: `${session.handle}: ${message}`
|
|
568
365
|
};
|
|
569
366
|
}
|
|
570
367
|
if (!session.betaConnected || status.includes("connecting") || status.includes("reconnect")) {
|
|
571
|
-
const message = session.lastError ?? "
|
|
368
|
+
const message = session.lastError ?? progressDetails.message ?? "connecting to remote machine";
|
|
572
369
|
return {
|
|
573
|
-
snapshot: snapshotEntry(session.handle, mountPath, "reconnecting", message
|
|
574
|
-
|
|
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
|
+
)
|
|
575
392
|
};
|
|
576
393
|
}
|
|
577
394
|
if (session.lastError && !status.includes("watching")) {
|
|
578
395
|
return {
|
|
579
|
-
snapshot: snapshotEntry(session.handle, mountPath, "degraded", session.lastError
|
|
396
|
+
snapshot: snapshotEntry(session.handle, mountPath, "degraded", session.lastError, {
|
|
397
|
+
status: session.status,
|
|
398
|
+
progress: session.stagingProgress,
|
|
399
|
+
currentFile: session.currentFile
|
|
400
|
+
}),
|
|
580
401
|
issue: `${session.handle}: ${session.lastError}`
|
|
581
402
|
};
|
|
582
403
|
}
|
|
583
404
|
return {
|
|
584
|
-
snapshot: snapshotEntry(session.handle, mountPath, "mounted"
|
|
405
|
+
snapshot: snapshotEntry(session.handle, mountPath, "mounted", void 0, {
|
|
406
|
+
status: session.status
|
|
407
|
+
})
|
|
585
408
|
};
|
|
586
409
|
}
|
|
587
|
-
function snapshotEntry(handle, mountPath, state, message) {
|
|
410
|
+
function snapshotEntry(handle, mountPath, state, message, extra = {}) {
|
|
588
411
|
return {
|
|
589
412
|
handle,
|
|
590
413
|
mountPath,
|
|
591
414
|
state,
|
|
592
415
|
message,
|
|
593
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
416
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
417
|
+
...extra
|
|
594
418
|
};
|
|
595
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
|
+
}
|
|
596
490
|
function sortSnapshots(mounts) {
|
|
597
491
|
return mounts.slice().sort((left, right) => left.handle.localeCompare(right.handle));
|
|
598
492
|
}
|
|
@@ -614,7 +508,7 @@ function terminateOwnedSessions(sessions, config, paths, signal) {
|
|
|
614
508
|
).then(() => void 0);
|
|
615
509
|
}
|
|
616
510
|
async function removeHandleFromRoot(handle, paths) {
|
|
617
|
-
await rm(
|
|
511
|
+
await rm(join(paths.rootPath, handle), { recursive: true, force: true });
|
|
618
512
|
}
|
|
619
513
|
async function listRootHandleDirectories(rootPath) {
|
|
620
514
|
try {
|
|
@@ -639,29 +533,6 @@ function throwIfAborted(signal) {
|
|
|
639
533
|
}
|
|
640
534
|
|
|
641
535
|
export {
|
|
642
|
-
getAPIKey,
|
|
643
|
-
getStoredAPIKey,
|
|
644
|
-
hasEnvAPIKey,
|
|
645
|
-
setAPIKey,
|
|
646
|
-
clearAPIKey,
|
|
647
|
-
ApiError,
|
|
648
|
-
getBaseURL,
|
|
649
|
-
getWebURL,
|
|
650
|
-
api,
|
|
651
|
-
apiWithKey,
|
|
652
|
-
listComputers,
|
|
653
|
-
getComputerByID,
|
|
654
|
-
createComputer,
|
|
655
|
-
deleteComputer,
|
|
656
|
-
getFilesystemSettings,
|
|
657
|
-
getConnectionInfo,
|
|
658
|
-
createBrowserAccess,
|
|
659
|
-
listPublishedPorts,
|
|
660
|
-
publishPort,
|
|
661
|
-
deletePublishedPort,
|
|
662
|
-
resolveComputer,
|
|
663
|
-
webURL,
|
|
664
|
-
vncURL,
|
|
665
536
|
padEnd,
|
|
666
537
|
timeAgo,
|
|
667
538
|
formatStatus,
|