aicomputer 0.1.17 → 0.1.18
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 +8 -3
- package/dist/{chunk-5IEWKH52.js → chunk-3ZUTAUUD.js} +70 -280
- package/dist/chunk-F2U4SFJ4.js +517 -0
- package/dist/{chunk-OWK5N76S.js → chunk-JMRAYXUO.js} +3 -11
- package/dist/index.js +324 -114
- package/dist/lib/mount-config.d.ts +4 -1
- package/dist/lib/mount-host.d.ts +1 -2
- package/dist/lib/mount-host.js +1 -1
- package/dist/lib/mount-mutagen.d.ts +37 -0
- package/dist/lib/mount-mutagen.js +23 -0
- package/dist/lib/mount-reconcile.d.ts +4 -18
- package/dist/lib/mount-reconcile.js +2 -1
- package/package.json +5 -3
- package/scripts/postinstall.mjs +35 -0
package/README.md
CHANGED
|
@@ -8,6 +8,9 @@ Agent Computer CLI for creating, opening, and managing computers from the termin
|
|
|
8
8
|
npm install -g aicomputer
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
On macOS and Linux, the npm install and update flow also installs the CLI-managed
|
|
12
|
+
Mutagen binary used by `computer mount`.
|
|
13
|
+
|
|
11
14
|
Upgrade the installed CLI later with:
|
|
12
15
|
|
|
13
16
|
```bash
|
|
@@ -127,9 +130,11 @@ Run `computer mount --background` to start the same controller detached from
|
|
|
127
130
|
your terminal. It prints the controller PID immediately so you can inspect it
|
|
128
131
|
later with `computer mount status`.
|
|
129
132
|
|
|
130
|
-
This uses the same SSH setup as `computer ssh --setup
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
This uses the same SSH setup as `computer ssh --setup`. For npm installs on
|
|
134
|
+
macOS and Linux, the CLI auto-installs its bundled Mutagen copy and will retry
|
|
135
|
+
that install on first mount if npm scripts were skipped. You still need the
|
|
136
|
+
OpenSSH client tools (`ssh` and `scp`) available locally. The temporary
|
|
137
|
+
`~/agentcomputer` root is removed when the command exits.
|
|
133
138
|
|
|
134
139
|
Use `computer agent` to inspect agents on one machine and manage remote sessions. Use `computer acp serve` when you want to expose one remote session through a local ACP bridge.
|
|
135
140
|
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
createHandleSession,
|
|
3
|
+
ensureMutagenSshEnvironment,
|
|
4
|
+
getHandleSessionName,
|
|
5
|
+
isAbortError,
|
|
6
|
+
listOwnedSessions,
|
|
7
|
+
selectPreferredSession,
|
|
8
|
+
terminateSession
|
|
9
|
+
} from "./chunk-F2U4SFJ4.js";
|
|
10
|
+
import {
|
|
5
11
|
readMountStatusSnapshot,
|
|
6
12
|
removeMountHandleState,
|
|
7
13
|
writeMountHandleMeta,
|
|
@@ -10,7 +16,7 @@ import {
|
|
|
10
16
|
|
|
11
17
|
// src/lib/mount-reconcile.ts
|
|
12
18
|
import { readdir, mkdir, rm } from "fs/promises";
|
|
13
|
-
import { join as
|
|
19
|
+
import { join as join2 } from "path";
|
|
14
20
|
|
|
15
21
|
// src/lib/config.ts
|
|
16
22
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
@@ -145,8 +151,8 @@ async function readErrorMessage(response) {
|
|
|
145
151
|
}
|
|
146
152
|
|
|
147
153
|
// src/lib/computers.ts
|
|
148
|
-
async function listComputers() {
|
|
149
|
-
const response = await api("/v1/computers");
|
|
154
|
+
async function listComputers(signal) {
|
|
155
|
+
const response = await api("/v1/computers", { signal });
|
|
150
156
|
return response.computers;
|
|
151
157
|
}
|
|
152
158
|
async function getComputerByID(id) {
|
|
@@ -166,8 +172,10 @@ async function deleteComputer(computerID) {
|
|
|
166
172
|
async function getFilesystemSettings() {
|
|
167
173
|
return api("/v1/me/filesystem");
|
|
168
174
|
}
|
|
169
|
-
async function getConnectionInfo(computerID) {
|
|
170
|
-
return api(`/v1/computers/${computerID}/connection
|
|
175
|
+
async function getConnectionInfo(computerID, signal) {
|
|
176
|
+
return api(`/v1/computers/${computerID}/connection`, {
|
|
177
|
+
signal
|
|
178
|
+
});
|
|
171
179
|
}
|
|
172
180
|
async function createBrowserAccess(computerID) {
|
|
173
181
|
return api(`/v1/computers/${computerID}/access/browser`, {
|
|
@@ -301,255 +309,6 @@ function describeSSHChoice(computer) {
|
|
|
301
309
|
return `${computer.runtime_family} ${timeAgo(computer.updated_at)}`;
|
|
302
310
|
}
|
|
303
311
|
|
|
304
|
-
// src/lib/mount-mutagen.ts
|
|
305
|
-
import { chmodSync, readFileSync as readFileSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
306
|
-
import { spawnSync } from "child_process";
|
|
307
|
-
import { basename, join as join2, relative, resolve } from "path";
|
|
308
|
-
var SYNC_NAME_PREFIX = "agentcomputer-mount-";
|
|
309
|
-
var DEFAULT_IGNORE_PATHS = [
|
|
310
|
-
".codex/tmp",
|
|
311
|
-
".local",
|
|
312
|
-
".ssh/sshd.log"
|
|
313
|
-
];
|
|
314
|
-
function ensureMutagenSshEnvironment(config, paths) {
|
|
315
|
-
ensureMountDirectories(paths);
|
|
316
|
-
const sshPath = resolveCommandPath("ssh");
|
|
317
|
-
const scpPath = resolveCommandPath("scp");
|
|
318
|
-
writeExecutableLink(paths.sshToolsDir, "ssh", sshPath);
|
|
319
|
-
writeExecutableLink(paths.sshToolsDir, "scp", scpPath);
|
|
320
|
-
writeExecutableLink(paths.sshToolsDir, basename(sshPath), sshPath);
|
|
321
|
-
writeExecutableLink(paths.sshToolsDir, basename(scpPath), scpPath);
|
|
322
|
-
}
|
|
323
|
-
function getHandleSessionName(handle) {
|
|
324
|
-
return `${SYNC_NAME_PREFIX}${handle}`;
|
|
325
|
-
}
|
|
326
|
-
function createHandleSession(handle, config, paths) {
|
|
327
|
-
ensureHandleDirectories(handle, config.rootPath);
|
|
328
|
-
const sessionName = getHandleSessionName(handle);
|
|
329
|
-
const args = [
|
|
330
|
-
"sync",
|
|
331
|
-
"create",
|
|
332
|
-
join2(paths.rootPath, handle),
|
|
333
|
-
`${handle}@${config.alias}:/home/node`,
|
|
334
|
-
"--name",
|
|
335
|
-
sessionName,
|
|
336
|
-
"--label",
|
|
337
|
-
`${MOUNT_SERVICE_LABEL}=true`,
|
|
338
|
-
"--label",
|
|
339
|
-
`${MOUNT_SERVICE_LABEL}.handle=${handle}`,
|
|
340
|
-
"--symlink-mode",
|
|
341
|
-
"posix-raw"
|
|
342
|
-
];
|
|
343
|
-
for (const ignorePath of DEFAULT_IGNORE_PATHS) {
|
|
344
|
-
args.push("--ignore", ignorePath);
|
|
345
|
-
}
|
|
346
|
-
runMutagen(args, config, paths, handle);
|
|
347
|
-
runMutagen(["sync", "flush", sessionName, "--skip-wait"], config, paths, handle);
|
|
348
|
-
}
|
|
349
|
-
function terminateSession(session, config, paths) {
|
|
350
|
-
runMutagen(
|
|
351
|
-
["sync", "terminate", session.identifier],
|
|
352
|
-
config,
|
|
353
|
-
paths,
|
|
354
|
-
session.handle,
|
|
355
|
-
true
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
function listOwnedSessions(config, paths) {
|
|
359
|
-
const result = runMutagen(["sync", "list", "-l"], config, paths, "mount", true);
|
|
360
|
-
const raw = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
361
|
-
if (!raw) {
|
|
362
|
-
return [];
|
|
363
|
-
}
|
|
364
|
-
if (!result.ok && raw.includes("No synchronization sessions found")) {
|
|
365
|
-
return [];
|
|
366
|
-
}
|
|
367
|
-
const rootPath = resolve(paths.rootPath);
|
|
368
|
-
const sessions = parseMutagenSyncList(raw).filter((session) => session.alphaPath).map((session) => {
|
|
369
|
-
const alphaPath = resolve(session.alphaPath);
|
|
370
|
-
const handle = basename(alphaPath);
|
|
371
|
-
const expectedName = getHandleSessionName(handle);
|
|
372
|
-
const expectedBeta = `${handle}@${config.alias}:/home/node`;
|
|
373
|
-
const owned = alphaPath === rootPath || relative(rootPath, alphaPath) === "" || !relative(rootPath, alphaPath).startsWith("..") && !relative(rootPath, alphaPath).startsWith("../");
|
|
374
|
-
if (!owned || !session.identifier) {
|
|
375
|
-
return null;
|
|
376
|
-
}
|
|
377
|
-
return {
|
|
378
|
-
identifier: session.identifier,
|
|
379
|
-
name: session.name,
|
|
380
|
-
handle,
|
|
381
|
-
alphaPath,
|
|
382
|
-
betaUrl: session.betaUrl,
|
|
383
|
-
alphaConnected: session.alphaConnected,
|
|
384
|
-
betaConnected: session.betaConnected,
|
|
385
|
-
status: session.status,
|
|
386
|
-
lastError: session.lastError,
|
|
387
|
-
scanProblemCount: session.scanProblemCount,
|
|
388
|
-
conflictCount: session.conflictCount,
|
|
389
|
-
legacy: session.name !== expectedName || session.betaUrl !== expectedBeta || alphaPath !== resolve(join2(rootPath, handle))
|
|
390
|
-
};
|
|
391
|
-
}).filter((session) => session !== null);
|
|
392
|
-
return sessions.sort((left, right) => left.handle.localeCompare(right.handle));
|
|
393
|
-
}
|
|
394
|
-
function selectPreferredSession(handle, sessions) {
|
|
395
|
-
const expectedName = getHandleSessionName(handle);
|
|
396
|
-
const exact = sessions.find((session) => session.name === expectedName);
|
|
397
|
-
return exact ?? sessions[0];
|
|
398
|
-
}
|
|
399
|
-
function parseMutagenSyncList(output) {
|
|
400
|
-
const sessions = [];
|
|
401
|
-
let current = null;
|
|
402
|
-
let section = "";
|
|
403
|
-
const finishCurrent = () => {
|
|
404
|
-
if (current?.identifier) {
|
|
405
|
-
sessions.push(current);
|
|
406
|
-
}
|
|
407
|
-
current = null;
|
|
408
|
-
section = "";
|
|
409
|
-
};
|
|
410
|
-
for (const line of output.split(/\r?\n/)) {
|
|
411
|
-
if (/^-{20,}$/.test(line.trim())) {
|
|
412
|
-
finishCurrent();
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
const trimmed = line.trim();
|
|
416
|
-
if (!trimmed) {
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
if (!current) {
|
|
420
|
-
current = {
|
|
421
|
-
alphaConnected: false,
|
|
422
|
-
betaConnected: false,
|
|
423
|
-
scanProblemCount: 0,
|
|
424
|
-
conflictCount: 0
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
if (trimmed.endsWith(":")) {
|
|
428
|
-
switch (trimmed.slice(0, -1)) {
|
|
429
|
-
case "Alpha":
|
|
430
|
-
case "Beta":
|
|
431
|
-
case "Scan problems":
|
|
432
|
-
case "Conflicts":
|
|
433
|
-
section = trimmed.slice(0, -1);
|
|
434
|
-
continue;
|
|
435
|
-
default:
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
if (trimmed.startsWith("Name: ")) {
|
|
440
|
-
current.name = trimmed.slice("Name: ".length);
|
|
441
|
-
continue;
|
|
442
|
-
}
|
|
443
|
-
if (trimmed.startsWith("Identifier: ")) {
|
|
444
|
-
current.identifier = trimmed.slice("Identifier: ".length);
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
if (trimmed.startsWith("Status: ")) {
|
|
448
|
-
current.status = trimmed.slice("Status: ".length);
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
if (trimmed.startsWith("Last error: ")) {
|
|
452
|
-
current.lastError = trimmed.slice("Last error: ".length);
|
|
453
|
-
continue;
|
|
454
|
-
}
|
|
455
|
-
if (section === "Alpha") {
|
|
456
|
-
if (trimmed.startsWith("URL: ")) {
|
|
457
|
-
current.alphaPath = trimmed.slice("URL: ".length);
|
|
458
|
-
continue;
|
|
459
|
-
}
|
|
460
|
-
if (trimmed.startsWith("Connected: ")) {
|
|
461
|
-
current.alphaConnected = parseConnected(trimmed);
|
|
462
|
-
continue;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
if (section === "Beta") {
|
|
466
|
-
if (trimmed.startsWith("URL: ")) {
|
|
467
|
-
current.betaUrl = trimmed.slice("URL: ".length);
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
if (trimmed.startsWith("Connected: ")) {
|
|
471
|
-
current.betaConnected = parseConnected(trimmed);
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
if (section === "Scan problems") {
|
|
476
|
-
current.scanProblemCount += 1;
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
if (section === "Conflicts") {
|
|
480
|
-
if (trimmed.startsWith("(alpha)")) {
|
|
481
|
-
current.conflictCount += 1;
|
|
482
|
-
}
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
finishCurrent();
|
|
487
|
-
return sessions;
|
|
488
|
-
}
|
|
489
|
-
function parseConnected(line) {
|
|
490
|
-
return line.slice("Connected: ".length).trim().toLowerCase() === "yes";
|
|
491
|
-
}
|
|
492
|
-
function runMutagen(args, config, paths, handle, ignoreFailure = false) {
|
|
493
|
-
const result = spawnSync("mutagen", args, {
|
|
494
|
-
encoding: "utf8",
|
|
495
|
-
env: {
|
|
496
|
-
...process.env,
|
|
497
|
-
MUTAGEN_SSH_PATH: paths.sshToolsDir,
|
|
498
|
-
MUTAGEN_SSH_CONNECT_TIMEOUT: String(config.connectTimeoutSeconds)
|
|
499
|
-
}
|
|
500
|
-
});
|
|
501
|
-
const stdout = result.stdout?.trim() ?? "";
|
|
502
|
-
const stderr = result.stderr?.trim() ?? "";
|
|
503
|
-
if (result.status !== 0 && !ignoreFailure) {
|
|
504
|
-
throw new Error(stderr || stdout || `mutagen failed for ${handle}`);
|
|
505
|
-
}
|
|
506
|
-
return {
|
|
507
|
-
ok: result.status === 0,
|
|
508
|
-
stdout,
|
|
509
|
-
stderr,
|
|
510
|
-
status: result.status
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
function resolveCommandPath(command) {
|
|
514
|
-
const result = spawnSync("which", [command], { encoding: "utf8" });
|
|
515
|
-
if (result.status !== 0) {
|
|
516
|
-
throw new Error(`failed to resolve ${command}`);
|
|
517
|
-
}
|
|
518
|
-
return result.stdout.trim();
|
|
519
|
-
}
|
|
520
|
-
function writeExecutableLink(directory, name, target) {
|
|
521
|
-
const linkPath = join2(directory, name);
|
|
522
|
-
try {
|
|
523
|
-
unlinkSync(linkPath);
|
|
524
|
-
} catch {
|
|
525
|
-
}
|
|
526
|
-
try {
|
|
527
|
-
symlinkSync(target, linkPath);
|
|
528
|
-
} catch {
|
|
529
|
-
const script = `#!/bin/sh
|
|
530
|
-
exec "${escapeShell(target)}" "$@"
|
|
531
|
-
`;
|
|
532
|
-
writeFileSync2(linkPath, script, { mode: 493 });
|
|
533
|
-
chmodSync(linkPath, 493);
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
try {
|
|
537
|
-
const stat = readFileSync2(linkPath);
|
|
538
|
-
if (!stat) {
|
|
539
|
-
throw new Error("empty");
|
|
540
|
-
}
|
|
541
|
-
} catch {
|
|
542
|
-
const script = `#!/bin/sh
|
|
543
|
-
exec "${escapeShell(target)}" "$@"
|
|
544
|
-
`;
|
|
545
|
-
writeFileSync2(linkPath, script, { mode: 493 });
|
|
546
|
-
chmodSync(linkPath, 493);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
function escapeShell(value) {
|
|
550
|
-
return value.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("$", "\\$").replaceAll('"', '\\"');
|
|
551
|
-
}
|
|
552
|
-
|
|
553
312
|
// src/lib/mount-reconcile.ts
|
|
554
313
|
function computeMountPlan(input) {
|
|
555
314
|
const desiredHandles = new Set(input.desired.map((entry) => entry.handle));
|
|
@@ -599,14 +358,14 @@ function computeMountPlan(input) {
|
|
|
599
358
|
pending: input.pending
|
|
600
359
|
};
|
|
601
360
|
}
|
|
602
|
-
async function planMountReconcile(config, paths) {
|
|
603
|
-
const computers = await listComputers();
|
|
361
|
+
async function planMountReconcile(config, paths, signal) {
|
|
362
|
+
const computers = await listComputers(signal);
|
|
604
363
|
const desired = [];
|
|
605
364
|
const pending = [];
|
|
606
365
|
for (const computer of computers.filter(isSSHSelectable)) {
|
|
607
|
-
const mountPath =
|
|
366
|
+
const mountPath = join2(paths.rootPath, computer.handle);
|
|
608
367
|
try {
|
|
609
|
-
const info = await getConnectionInfo(computer.id);
|
|
368
|
+
const info = await getConnectionInfo(computer.id, signal);
|
|
610
369
|
if (!info.connection.ssh_available) {
|
|
611
370
|
pending.push({
|
|
612
371
|
handle: computer.handle,
|
|
@@ -631,7 +390,8 @@ async function planMountReconcile(config, paths) {
|
|
|
631
390
|
ready: true
|
|
632
391
|
});
|
|
633
392
|
}
|
|
634
|
-
|
|
393
|
+
throwIfAborted(signal);
|
|
394
|
+
const ownedSessions = await listOwnedSessions(config, paths, signal);
|
|
635
395
|
const rootEntries = await listRootHandleDirectories(paths.rootPath);
|
|
636
396
|
return {
|
|
637
397
|
plan: computeMountPlan({
|
|
@@ -643,30 +403,38 @@ async function planMountReconcile(config, paths) {
|
|
|
643
403
|
ownedSessions
|
|
644
404
|
};
|
|
645
405
|
}
|
|
646
|
-
async function reconcileMounts(config, paths, controllerPid = process.pid) {
|
|
406
|
+
async function reconcileMounts(config, paths, controllerPid = process.pid, signal) {
|
|
647
407
|
ensureMutagenSshEnvironment(config, paths);
|
|
648
|
-
|
|
408
|
+
throwIfAborted(signal);
|
|
409
|
+
const { plan, ownedSessions } = await planMountReconcile(config, paths, signal);
|
|
649
410
|
const mounts = [];
|
|
650
411
|
const issues = [];
|
|
651
412
|
const ownedByHandle = groupSessionsByHandle(ownedSessions);
|
|
652
413
|
for (const handle of [...plan.toStop, ...plan.toReset]) {
|
|
414
|
+
throwIfAborted(signal);
|
|
653
415
|
const sessions = ownedByHandle.get(handle) ?? [];
|
|
654
416
|
try {
|
|
655
|
-
terminateOwnedSessions(sessions, config, paths);
|
|
417
|
+
await terminateOwnedSessions(sessions, config, paths, signal);
|
|
656
418
|
await removeHandleFromRoot(handle, paths);
|
|
657
419
|
removeMountHandleState(handle, config.rootPath);
|
|
658
420
|
} catch (error) {
|
|
421
|
+
if (isAbortError(error)) {
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
659
424
|
issues.push(
|
|
660
425
|
error instanceof Error ? `${handle}: ${error.message}` : `${handle}: teardown failed`
|
|
661
426
|
);
|
|
662
427
|
}
|
|
663
428
|
}
|
|
664
429
|
for (const entry of plan.toCreate) {
|
|
430
|
+
throwIfAborted(signal);
|
|
665
431
|
try {
|
|
666
|
-
await ensureMountedHandle(entry, config, paths);
|
|
432
|
+
await ensureMountedHandle(entry, config, paths, signal);
|
|
667
433
|
const session = selectPreferredSession(
|
|
668
434
|
entry.handle,
|
|
669
|
-
listOwnedSessions(config, paths).filter(
|
|
435
|
+
(await listOwnedSessions(config, paths, signal)).filter(
|
|
436
|
+
(candidate) => candidate.handle === entry.handle
|
|
437
|
+
)
|
|
670
438
|
);
|
|
671
439
|
const inspected = inspectSnapshotEntry(session, entry.mountPath);
|
|
672
440
|
mounts.push(inspected.snapshot);
|
|
@@ -674,20 +442,29 @@ async function reconcileMounts(config, paths, controllerPid = process.pid) {
|
|
|
674
442
|
issues.push(inspected.issue);
|
|
675
443
|
}
|
|
676
444
|
} catch (error) {
|
|
445
|
+
if (isAbortError(error)) {
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
677
448
|
const message = error instanceof Error ? error.message : "sync start failed";
|
|
678
449
|
mounts.push(snapshotEntry(entry.handle, entry.mountPath, "error", message));
|
|
679
450
|
issues.push(`${entry.handle}: ${message}`);
|
|
680
451
|
}
|
|
681
452
|
}
|
|
682
|
-
|
|
453
|
+
throwIfAborted(signal);
|
|
454
|
+
const postCreateSessions = groupSessionsByHandle(
|
|
455
|
+
await listOwnedSessions(config, paths, signal)
|
|
456
|
+
);
|
|
683
457
|
for (const entry of plan.toInspect) {
|
|
458
|
+
throwIfAborted(signal);
|
|
684
459
|
try {
|
|
685
460
|
const sessions = postCreateSessions.get(entry.handle) ?? [];
|
|
686
461
|
if (sessions.length === 0) {
|
|
687
|
-
await ensureMountedHandle(entry, config, paths);
|
|
462
|
+
await ensureMountedHandle(entry, config, paths, signal);
|
|
688
463
|
const createdSession = selectPreferredSession(
|
|
689
464
|
entry.handle,
|
|
690
|
-
listOwnedSessions(config, paths).filter(
|
|
465
|
+
(await listOwnedSessions(config, paths, signal)).filter(
|
|
466
|
+
(candidate) => candidate.handle === entry.handle
|
|
467
|
+
)
|
|
691
468
|
);
|
|
692
469
|
const inspected2 = inspectSnapshotEntry(createdSession, entry.mountPath);
|
|
693
470
|
mounts.push(inspected2.snapshot);
|
|
@@ -703,6 +480,9 @@ async function reconcileMounts(config, paths, controllerPid = process.pid) {
|
|
|
703
480
|
issues.push(inspected.issue);
|
|
704
481
|
}
|
|
705
482
|
} catch (error) {
|
|
483
|
+
if (isAbortError(error)) {
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
706
486
|
const message = error instanceof Error ? error.message : "sync inspection failed";
|
|
707
487
|
mounts.push(snapshotEntry(entry.handle, entry.mountPath, "error", message));
|
|
708
488
|
issues.push(`${entry.handle}: ${message}`);
|
|
@@ -718,6 +498,8 @@ async function reconcileMounts(config, paths, controllerPid = process.pid) {
|
|
|
718
498
|
updatedAt: now,
|
|
719
499
|
controllerPid,
|
|
720
500
|
running: true,
|
|
501
|
+
startupPhase: "ready",
|
|
502
|
+
startupMessage: void 0,
|
|
721
503
|
lastHealthySyncAt: healthy ? now : previousSnapshot?.lastHealthySyncAt,
|
|
722
504
|
lastSuccessfulSyncAt: healthy ? now : previousSnapshot?.lastSuccessfulSyncAt,
|
|
723
505
|
lastIssueAt: issues.length > 0 ? now : previousSnapshot?.lastIssueAt,
|
|
@@ -728,9 +510,10 @@ async function reconcileMounts(config, paths, controllerPid = process.pid) {
|
|
|
728
510
|
writeMountStatusSnapshot(snapshot, config.rootPath);
|
|
729
511
|
return snapshot;
|
|
730
512
|
}
|
|
731
|
-
async function teardownManagedSessions(config, paths) {
|
|
732
|
-
|
|
733
|
-
|
|
513
|
+
async function teardownManagedSessions(config, paths, signal) {
|
|
514
|
+
throwIfAborted(signal);
|
|
515
|
+
const ownedSessions = await listOwnedSessions(config, paths, signal);
|
|
516
|
+
await terminateOwnedSessions(ownedSessions, config, paths, signal);
|
|
734
517
|
const handles = new Set(ownedSessions.map((session) => session.handle));
|
|
735
518
|
for (const handle of await listKnownHandleStates(paths)) {
|
|
736
519
|
handles.add(handle);
|
|
@@ -743,9 +526,9 @@ async function teardownManagedSessions(config, paths) {
|
|
|
743
526
|
}
|
|
744
527
|
await rm(paths.rootPath, { recursive: true, force: true });
|
|
745
528
|
}
|
|
746
|
-
async function ensureMountedHandle(entry, config, paths) {
|
|
529
|
+
async function ensureMountedHandle(entry, config, paths, signal) {
|
|
747
530
|
await mkdir(entry.mountPath, { recursive: true });
|
|
748
|
-
createHandleSession(entry.handle, config, paths);
|
|
531
|
+
await createHandleSession(entry.handle, config, paths, signal);
|
|
749
532
|
writeMountHandleMeta(
|
|
750
533
|
entry.handle,
|
|
751
534
|
{
|
|
@@ -825,13 +608,13 @@ function groupSessionsByHandle(sessions) {
|
|
|
825
608
|
}
|
|
826
609
|
return grouped;
|
|
827
610
|
}
|
|
828
|
-
function terminateOwnedSessions(sessions, config, paths) {
|
|
829
|
-
|
|
830
|
-
terminateSession(session, config, paths)
|
|
831
|
-
|
|
611
|
+
function terminateOwnedSessions(sessions, config, paths, signal) {
|
|
612
|
+
return Promise.all(
|
|
613
|
+
sessions.map((session) => terminateSession(session, config, paths, signal))
|
|
614
|
+
).then(() => void 0);
|
|
832
615
|
}
|
|
833
616
|
async function removeHandleFromRoot(handle, paths) {
|
|
834
|
-
await rm(
|
|
617
|
+
await rm(join2(paths.rootPath, handle), { recursive: true, force: true });
|
|
835
618
|
}
|
|
836
619
|
async function listRootHandleDirectories(rootPath) {
|
|
837
620
|
try {
|
|
@@ -847,6 +630,13 @@ async function listKnownHandleStates(paths) {
|
|
|
847
630
|
return [];
|
|
848
631
|
}
|
|
849
632
|
}
|
|
633
|
+
function throwIfAborted(signal) {
|
|
634
|
+
if (signal?.aborted) {
|
|
635
|
+
const error = new Error("mount operation cancelled");
|
|
636
|
+
error.name = "AbortError";
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
850
640
|
|
|
851
641
|
export {
|
|
852
642
|
getAPIKey,
|