aicomputer 0.1.15 → 0.1.16
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 +2 -1
- package/dist/{chunk-KQQUR2YX.js → chunk-5IEWKH52.js} +341 -113
- package/dist/{chunk-5JVJROSI.js → chunk-KXLTHWW3.js} +0 -2
- package/dist/index.js +13 -8
- package/dist/lib/mount-config.d.ts +5 -2
- package/dist/lib/mount-config.js +1 -1
- package/dist/lib/mount-reconcile.d.ts +24 -5
- package/dist/lib/mount-reconcile.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -120,7 +120,8 @@ ssh my-box@agentcomputer.ai
|
|
|
120
120
|
Run `computer mount` to start a foreground controller that mirrors all SSH-ready
|
|
121
121
|
machine homes under `~/agentcomputer/<handle>` while the command is running.
|
|
122
122
|
This uses the same SSH setup as `computer ssh --setup` and requires Mutagen plus
|
|
123
|
-
OpenSSH client tools (`ssh` and `scp`) to already be installed locally.
|
|
123
|
+
OpenSSH client tools (`ssh` and `scp`) to already be installed locally. The
|
|
124
|
+
temporary `~/agentcomputer` root is removed when the command exits.
|
|
124
125
|
|
|
125
126
|
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.
|
|
126
127
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
+
MOUNT_SERVICE_LABEL,
|
|
2
3
|
ensureHandleDirectories,
|
|
3
4
|
ensureMountDirectories,
|
|
4
|
-
getMountHandlePaths,
|
|
5
5
|
readMountStatusSnapshot,
|
|
6
6
|
removeMountHandleState,
|
|
7
7
|
writeMountHandleMeta,
|
|
8
8
|
writeMountStatusSnapshot
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-KXLTHWW3.js";
|
|
10
10
|
|
|
11
11
|
// src/lib/mount-reconcile.ts
|
|
12
|
-
import { readdir, mkdir,
|
|
12
|
+
import { readdir, mkdir, rm } from "fs/promises";
|
|
13
13
|
import { join as join3 } from "path";
|
|
14
14
|
|
|
15
15
|
// src/lib/config.ts
|
|
@@ -248,16 +248,6 @@ function timeAgo(dateStr) {
|
|
|
248
248
|
const days = Math.floor(hours / 24);
|
|
249
249
|
return `${days}d ago`;
|
|
250
250
|
}
|
|
251
|
-
function minuteSecondAgo(dateStr) {
|
|
252
|
-
const timestamp = new Date(dateStr).getTime();
|
|
253
|
-
if (!Number.isFinite(timestamp)) {
|
|
254
|
-
return "unknown";
|
|
255
|
-
}
|
|
256
|
-
const totalSeconds = Math.max(0, Math.floor((Date.now() - timestamp) / 1e3));
|
|
257
|
-
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
258
|
-
const seconds = totalSeconds % 60;
|
|
259
|
-
return `${totalMinutes}:${String(seconds).padStart(2, "0")} ago`;
|
|
260
|
-
}
|
|
261
251
|
function formatStatus(status) {
|
|
262
252
|
switch (status) {
|
|
263
253
|
case "running":
|
|
@@ -312,9 +302,15 @@ function describeSSHChoice(computer) {
|
|
|
312
302
|
}
|
|
313
303
|
|
|
314
304
|
// src/lib/mount-mutagen.ts
|
|
315
|
-
import { chmodSync,
|
|
305
|
+
import { chmodSync, readFileSync as readFileSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
316
306
|
import { spawnSync } from "child_process";
|
|
317
|
-
import { basename, join as join2 } from "path";
|
|
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
|
+
];
|
|
318
314
|
function ensureMutagenSshEnvironment(config, paths) {
|
|
319
315
|
ensureMountDirectories(paths);
|
|
320
316
|
const sshPath = resolveCommandPath("ssh");
|
|
@@ -324,47 +320,174 @@ function ensureMutagenSshEnvironment(config, paths) {
|
|
|
324
320
|
writeExecutableLink(paths.sshToolsDir, basename(sshPath), sshPath);
|
|
325
321
|
writeExecutableLink(paths.sshToolsDir, basename(scpPath), scpPath);
|
|
326
322
|
}
|
|
327
|
-
function
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
runMutagen(["
|
|
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
|
+
);
|
|
352
357
|
}
|
|
353
|
-
function
|
|
354
|
-
const
|
|
355
|
-
|
|
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
|
+
}
|
|
356
377
|
return {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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))
|
|
360
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
|
+
}
|
|
361
485
|
}
|
|
362
|
-
|
|
486
|
+
finishCurrent();
|
|
487
|
+
return sessions;
|
|
363
488
|
}
|
|
364
|
-
function
|
|
365
|
-
|
|
366
|
-
terminateHandleProject(handle, config, paths);
|
|
367
|
-
}
|
|
489
|
+
function parseConnected(line) {
|
|
490
|
+
return line.slice("Connected: ".length).trim().toLowerCase() === "yes";
|
|
368
491
|
}
|
|
369
492
|
function runMutagen(args, config, paths, handle, ignoreFailure = false) {
|
|
370
493
|
const result = spawnSync("mutagen", args, {
|
|
@@ -383,7 +506,8 @@ function runMutagen(args, config, paths, handle, ignoreFailure = false) {
|
|
|
383
506
|
return {
|
|
384
507
|
ok: result.status === 0,
|
|
385
508
|
stdout,
|
|
386
|
-
stderr
|
|
509
|
+
stderr,
|
|
510
|
+
status: result.status
|
|
387
511
|
};
|
|
388
512
|
}
|
|
389
513
|
function resolveCommandPath(command) {
|
|
@@ -422,35 +546,41 @@ exec "${escapeShell(target)}" "$@"
|
|
|
422
546
|
chmodSync(linkPath, 493);
|
|
423
547
|
}
|
|
424
548
|
}
|
|
425
|
-
function yamlString(value) {
|
|
426
|
-
return JSON.stringify(value);
|
|
427
|
-
}
|
|
428
549
|
function escapeShell(value) {
|
|
429
550
|
return value.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("$", "\\$").replaceAll('"', '\\"');
|
|
430
551
|
}
|
|
431
552
|
|
|
432
553
|
// src/lib/mount-reconcile.ts
|
|
433
554
|
function computeMountPlan(input) {
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
)
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const toStart = [];
|
|
441
|
-
const toCheck = [];
|
|
442
|
-
for (const entry of input.desired) {
|
|
443
|
-
const previous = previousByHandle.get(entry.handle);
|
|
444
|
-
if (previous?.state === "mounted") {
|
|
445
|
-
toCheck.push(entry);
|
|
555
|
+
const desiredHandles = new Set(input.desired.map((entry) => entry.handle));
|
|
556
|
+
const sessionGroups = /* @__PURE__ */ new Map();
|
|
557
|
+
for (const session of input.ownedSessions) {
|
|
558
|
+
const group = sessionGroups.get(session.handle);
|
|
559
|
+
if (group) {
|
|
560
|
+
group.push(session);
|
|
446
561
|
} else {
|
|
447
|
-
|
|
562
|
+
sessionGroups.set(session.handle, [session]);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const toCreate = [];
|
|
566
|
+
const toInspect = [];
|
|
567
|
+
const toReset = /* @__PURE__ */ new Set();
|
|
568
|
+
for (const entry of input.desired) {
|
|
569
|
+
const sessions = sessionGroups.get(entry.handle) ?? [];
|
|
570
|
+
const reusable = sessions.length === 1 && !sessions[0].legacy;
|
|
571
|
+
if (reusable) {
|
|
572
|
+
toInspect.push(entry);
|
|
573
|
+
continue;
|
|
448
574
|
}
|
|
575
|
+
if (sessions.length > 0) {
|
|
576
|
+
toReset.add(entry.handle);
|
|
577
|
+
}
|
|
578
|
+
toCreate.push(entry);
|
|
449
579
|
}
|
|
450
580
|
const toStop = /* @__PURE__ */ new Set();
|
|
451
|
-
for (const
|
|
452
|
-
if (!desiredHandles.has(
|
|
453
|
-
toStop.add(
|
|
581
|
+
for (const session of input.ownedSessions) {
|
|
582
|
+
if (!desiredHandles.has(session.handle)) {
|
|
583
|
+
toStop.add(session.handle);
|
|
454
584
|
}
|
|
455
585
|
}
|
|
456
586
|
for (const entry of input.rootEntries) {
|
|
@@ -458,9 +588,13 @@ function computeMountPlan(input) {
|
|
|
458
588
|
toStop.add(entry);
|
|
459
589
|
}
|
|
460
590
|
}
|
|
591
|
+
for (const handle of toReset) {
|
|
592
|
+
toStop.delete(handle);
|
|
593
|
+
}
|
|
461
594
|
return {
|
|
462
|
-
|
|
463
|
-
|
|
595
|
+
toCreate,
|
|
596
|
+
toInspect,
|
|
597
|
+
toReset: Array.from(toReset).sort(),
|
|
464
598
|
toStop: Array.from(toStop).sort(),
|
|
465
599
|
pending: input.pending
|
|
466
600
|
};
|
|
@@ -497,52 +631,81 @@ async function planMountReconcile(config, paths) {
|
|
|
497
631
|
ready: true
|
|
498
632
|
});
|
|
499
633
|
}
|
|
500
|
-
const
|
|
634
|
+
const ownedSessions = listOwnedSessions(config, paths);
|
|
501
635
|
const rootEntries = await listRootHandleDirectories(paths.rootPath);
|
|
502
|
-
return
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
636
|
+
return {
|
|
637
|
+
plan: computeMountPlan({
|
|
638
|
+
desired,
|
|
639
|
+
pending,
|
|
640
|
+
ownedSessions,
|
|
641
|
+
rootEntries
|
|
642
|
+
}),
|
|
643
|
+
ownedSessions
|
|
644
|
+
};
|
|
508
645
|
}
|
|
509
646
|
async function reconcileMounts(config, paths, controllerPid = process.pid) {
|
|
510
647
|
ensureMutagenSshEnvironment(config, paths);
|
|
511
|
-
const plan = await planMountReconcile(config, paths);
|
|
648
|
+
const { plan, ownedSessions } = await planMountReconcile(config, paths);
|
|
512
649
|
const mounts = [];
|
|
513
|
-
const
|
|
514
|
-
|
|
650
|
+
const issues = [];
|
|
651
|
+
const ownedByHandle = groupSessionsByHandle(ownedSessions);
|
|
652
|
+
for (const handle of [...plan.toStop, ...plan.toReset]) {
|
|
653
|
+
const sessions = ownedByHandle.get(handle) ?? [];
|
|
515
654
|
try {
|
|
516
|
-
|
|
517
|
-
await
|
|
655
|
+
terminateOwnedSessions(sessions, config, paths);
|
|
656
|
+
await removeHandleFromRoot(handle, paths);
|
|
518
657
|
removeMountHandleState(handle, config.rootPath);
|
|
519
658
|
} catch (error) {
|
|
520
|
-
|
|
659
|
+
issues.push(
|
|
521
660
|
error instanceof Error ? `${handle}: ${error.message}` : `${handle}: teardown failed`
|
|
522
661
|
);
|
|
523
662
|
}
|
|
524
663
|
}
|
|
525
|
-
for (const entry of plan.
|
|
664
|
+
for (const entry of plan.toCreate) {
|
|
526
665
|
try {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
666
|
+
await ensureMountedHandle(entry, config, paths);
|
|
667
|
+
const session = selectPreferredSession(
|
|
668
|
+
entry.handle,
|
|
669
|
+
listOwnedSessions(config, paths).filter((candidate) => candidate.handle === entry.handle)
|
|
670
|
+
);
|
|
671
|
+
const inspected = inspectSnapshotEntry(session, entry.mountPath);
|
|
672
|
+
mounts.push(inspected.snapshot);
|
|
673
|
+
if (inspected.issue) {
|
|
674
|
+
issues.push(inspected.issue);
|
|
530
675
|
}
|
|
531
|
-
mounts.push(snapshotEntry(entry.handle, entry.mountPath, "mounted"));
|
|
532
676
|
} catch (error) {
|
|
533
|
-
const message = error instanceof Error ? error.message : "sync
|
|
677
|
+
const message = error instanceof Error ? error.message : "sync start failed";
|
|
534
678
|
mounts.push(snapshotEntry(entry.handle, entry.mountPath, "error", message));
|
|
535
|
-
|
|
679
|
+
issues.push(`${entry.handle}: ${message}`);
|
|
536
680
|
}
|
|
537
681
|
}
|
|
538
|
-
|
|
682
|
+
const postCreateSessions = groupSessionsByHandle(listOwnedSessions(config, paths));
|
|
683
|
+
for (const entry of plan.toInspect) {
|
|
539
684
|
try {
|
|
540
|
-
|
|
541
|
-
|
|
685
|
+
const sessions = postCreateSessions.get(entry.handle) ?? [];
|
|
686
|
+
if (sessions.length === 0) {
|
|
687
|
+
await ensureMountedHandle(entry, config, paths);
|
|
688
|
+
const createdSession = selectPreferredSession(
|
|
689
|
+
entry.handle,
|
|
690
|
+
listOwnedSessions(config, paths).filter((candidate) => candidate.handle === entry.handle)
|
|
691
|
+
);
|
|
692
|
+
const inspected2 = inspectSnapshotEntry(createdSession, entry.mountPath);
|
|
693
|
+
mounts.push(inspected2.snapshot);
|
|
694
|
+
if (inspected2.issue) {
|
|
695
|
+
issues.push(inspected2.issue);
|
|
696
|
+
}
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const session = selectPreferredSession(entry.handle, sessions);
|
|
700
|
+
const inspected = inspectSnapshotEntry(session, entry.mountPath);
|
|
701
|
+
mounts.push(inspected.snapshot);
|
|
702
|
+
if (inspected.issue) {
|
|
703
|
+
issues.push(inspected.issue);
|
|
704
|
+
}
|
|
542
705
|
} catch (error) {
|
|
543
|
-
const message = error instanceof Error ? error.message : "sync
|
|
706
|
+
const message = error instanceof Error ? error.message : "sync inspection failed";
|
|
544
707
|
mounts.push(snapshotEntry(entry.handle, entry.mountPath, "error", message));
|
|
545
|
-
|
|
708
|
+
issues.push(`${entry.handle}: ${message}`);
|
|
546
709
|
}
|
|
547
710
|
}
|
|
548
711
|
for (const entry of plan.pending) {
|
|
@@ -550,47 +713,93 @@ async function reconcileMounts(config, paths, controllerPid = process.pid) {
|
|
|
550
713
|
}
|
|
551
714
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
552
715
|
const previousSnapshot = readMountStatusSnapshot(config.rootPath);
|
|
716
|
+
const healthy = mounts.every((mount) => mount.state === "mounted" || mount.state === "pending");
|
|
553
717
|
const snapshot = {
|
|
554
718
|
updatedAt: now,
|
|
555
719
|
controllerPid,
|
|
556
720
|
running: true,
|
|
557
|
-
|
|
558
|
-
|
|
721
|
+
lastHealthySyncAt: healthy ? now : previousSnapshot?.lastHealthySyncAt,
|
|
722
|
+
lastSuccessfulSyncAt: healthy ? now : previousSnapshot?.lastSuccessfulSyncAt,
|
|
723
|
+
lastIssueAt: issues.length > 0 ? now : previousSnapshot?.lastIssueAt,
|
|
724
|
+
lastIssue: issues[0],
|
|
725
|
+
lastError: mounts.some((mount) => mount.state === "error") ? issues[0] : void 0,
|
|
559
726
|
mounts: sortSnapshots(mounts)
|
|
560
727
|
};
|
|
561
728
|
writeMountStatusSnapshot(snapshot, config.rootPath);
|
|
562
729
|
return snapshot;
|
|
563
730
|
}
|
|
564
731
|
async function teardownManagedSessions(config, paths) {
|
|
565
|
-
const
|
|
566
|
-
|
|
732
|
+
const ownedSessions = listOwnedSessions(config, paths);
|
|
733
|
+
terminateOwnedSessions(ownedSessions, config, paths);
|
|
734
|
+
const handles = new Set(ownedSessions.map((session) => session.handle));
|
|
735
|
+
for (const handle of await listKnownHandleStates(paths)) {
|
|
736
|
+
handles.add(handle);
|
|
737
|
+
}
|
|
738
|
+
for (const handle of await listRootHandleDirectories(paths.rootPath)) {
|
|
739
|
+
handles.add(handle);
|
|
740
|
+
}
|
|
567
741
|
for (const handle of handles) {
|
|
568
742
|
removeMountHandleState(handle, config.rootPath);
|
|
569
743
|
}
|
|
744
|
+
await rm(paths.rootPath, { recursive: true, force: true });
|
|
570
745
|
}
|
|
571
746
|
async function ensureMountedHandle(entry, config, paths) {
|
|
572
747
|
await mkdir(entry.mountPath, { recursive: true });
|
|
573
|
-
|
|
574
|
-
writeHandleProjectFile(entry.handle, config, paths);
|
|
575
|
-
startHandleProject(entry.handle, config, paths);
|
|
748
|
+
createHandleSession(entry.handle, config, paths);
|
|
576
749
|
writeMountHandleMeta(
|
|
577
750
|
entry.handle,
|
|
578
751
|
{
|
|
579
752
|
handle: entry.handle,
|
|
753
|
+
sessionName: getHandleSessionName(entry.handle),
|
|
580
754
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
581
755
|
lastStartedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
582
756
|
},
|
|
583
757
|
config.rootPath
|
|
584
758
|
);
|
|
585
759
|
}
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
760
|
+
function inspectSnapshotEntry(session, mountPath) {
|
|
761
|
+
const status = (session.status ?? "").toLowerCase();
|
|
762
|
+
const problemParts = [];
|
|
763
|
+
if (session.conflictCount > 0) {
|
|
764
|
+
problemParts.push(
|
|
765
|
+
`${session.conflictCount} conflict${session.conflictCount === 1 ? "" : "s"}`
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
if (session.scanProblemCount > 0) {
|
|
769
|
+
problemParts.push(
|
|
770
|
+
`${session.scanProblemCount} scan problem${session.scanProblemCount === 1 ? "" : "s"}`
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
if (problemParts.length > 0) {
|
|
774
|
+
const message = problemParts.join(", ");
|
|
775
|
+
return {
|
|
776
|
+
snapshot: snapshotEntry(session.handle, mountPath, "degraded", message),
|
|
777
|
+
issue: `${session.handle}: ${message}`
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
if (!session.alphaConnected) {
|
|
781
|
+
const message = session.lastError ?? "local sync endpoint disconnected";
|
|
782
|
+
return {
|
|
783
|
+
snapshot: snapshotEntry(session.handle, mountPath, "error", message),
|
|
784
|
+
issue: `${session.handle}: ${message}`
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
if (!session.betaConnected || status.includes("connecting") || status.includes("reconnect")) {
|
|
788
|
+
const message = session.lastError ?? "reconnecting to remote machine";
|
|
789
|
+
return {
|
|
790
|
+
snapshot: snapshotEntry(session.handle, mountPath, "reconnecting", message),
|
|
791
|
+
issue: `${session.handle}: ${message}`
|
|
792
|
+
};
|
|
593
793
|
}
|
|
794
|
+
if (session.lastError && !status.includes("watching")) {
|
|
795
|
+
return {
|
|
796
|
+
snapshot: snapshotEntry(session.handle, mountPath, "degraded", session.lastError),
|
|
797
|
+
issue: `${session.handle}: ${session.lastError}`
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
snapshot: snapshotEntry(session.handle, mountPath, "mounted")
|
|
802
|
+
};
|
|
594
803
|
}
|
|
595
804
|
function snapshotEntry(handle, mountPath, state, message) {
|
|
596
805
|
return {
|
|
@@ -604,6 +813,26 @@ function snapshotEntry(handle, mountPath, state, message) {
|
|
|
604
813
|
function sortSnapshots(mounts) {
|
|
605
814
|
return mounts.slice().sort((left, right) => left.handle.localeCompare(right.handle));
|
|
606
815
|
}
|
|
816
|
+
function groupSessionsByHandle(sessions) {
|
|
817
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
818
|
+
for (const session of sessions) {
|
|
819
|
+
const group = grouped.get(session.handle);
|
|
820
|
+
if (group) {
|
|
821
|
+
group.push(session);
|
|
822
|
+
} else {
|
|
823
|
+
grouped.set(session.handle, [session]);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return grouped;
|
|
827
|
+
}
|
|
828
|
+
function terminateOwnedSessions(sessions, config, paths) {
|
|
829
|
+
for (const session of sessions) {
|
|
830
|
+
terminateSession(session, config, paths);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async function removeHandleFromRoot(handle, paths) {
|
|
834
|
+
await rm(join3(paths.rootPath, handle), { recursive: true, force: true });
|
|
835
|
+
}
|
|
607
836
|
async function listRootHandleDirectories(rootPath) {
|
|
608
837
|
try {
|
|
609
838
|
return (await readdir(rootPath, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
@@ -645,7 +874,6 @@ export {
|
|
|
645
874
|
vncURL,
|
|
646
875
|
padEnd,
|
|
647
876
|
timeAgo,
|
|
648
|
-
minuteSecondAgo,
|
|
649
877
|
formatStatus,
|
|
650
878
|
promptForSSHComputer,
|
|
651
879
|
computeMountPlan,
|
|
@@ -35,7 +35,6 @@ function getMountHandlePaths(handle, rootPath = getDefaultMountRoot()) {
|
|
|
35
35
|
const stateDir = join(paths.handlesDir, handle);
|
|
36
36
|
return {
|
|
37
37
|
stateDir,
|
|
38
|
-
projectFilePath: join(stateDir, "mutagen.yml"),
|
|
39
38
|
metaPath: join(stateDir, "meta.json"),
|
|
40
39
|
sshToolsDir: join(stateDir, "ssh-tools")
|
|
41
40
|
};
|
|
@@ -53,7 +52,6 @@ function defaultMountServiceConfig() {
|
|
|
53
52
|
function ensureMountDirectories(paths) {
|
|
54
53
|
for (const directory of [
|
|
55
54
|
paths.stateDir,
|
|
56
|
-
paths.rootPath,
|
|
57
55
|
paths.handlesDir,
|
|
58
56
|
paths.sshToolsDir,
|
|
59
57
|
paths.staleDir
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
hasEnvAPIKey,
|
|
24
24
|
listComputers,
|
|
25
25
|
listPublishedPorts,
|
|
26
|
-
minuteSecondAgo,
|
|
27
26
|
padEnd,
|
|
28
27
|
promptForSSHComputer,
|
|
29
28
|
publishPort,
|
|
@@ -34,7 +33,7 @@ import {
|
|
|
34
33
|
timeAgo,
|
|
35
34
|
vncURL,
|
|
36
35
|
webURL
|
|
37
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-5IEWKH52.js";
|
|
38
37
|
import {
|
|
39
38
|
defaultMountServiceConfig,
|
|
40
39
|
ensureMountDirectories,
|
|
@@ -46,7 +45,7 @@ import {
|
|
|
46
45
|
writeMountConfig,
|
|
47
46
|
writeMountControllerLock,
|
|
48
47
|
writeMountStatusSnapshot
|
|
49
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-KXLTHWW3.js";
|
|
50
49
|
|
|
51
50
|
// src/index.ts
|
|
52
51
|
import { Command as Command14 } from "commander";
|
|
@@ -4046,7 +4045,10 @@ async function runMountDaemon(config) {
|
|
|
4046
4045
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4047
4046
|
controllerPid: process.pid,
|
|
4048
4047
|
running: true,
|
|
4048
|
+
lastHealthySyncAt: previous?.lastHealthySyncAt,
|
|
4049
4049
|
lastSuccessfulSyncAt: previous?.lastSuccessfulSyncAt,
|
|
4050
|
+
lastIssueAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4051
|
+
lastIssue: error instanceof Error ? error.message : "mount reconcile failed",
|
|
4050
4052
|
lastError: error instanceof Error ? error.message : "mount reconcile failed",
|
|
4051
4053
|
mounts: previous?.mounts ?? []
|
|
4052
4054
|
},
|
|
@@ -4094,7 +4096,10 @@ async function runMountDaemon(config) {
|
|
|
4094
4096
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4095
4097
|
controllerPid: previous?.controllerPid,
|
|
4096
4098
|
running: false,
|
|
4099
|
+
lastHealthySyncAt: previous?.lastHealthySyncAt,
|
|
4097
4100
|
lastSuccessfulSyncAt: previous?.lastSuccessfulSyncAt,
|
|
4101
|
+
lastIssueAt: previous?.lastIssueAt,
|
|
4102
|
+
lastIssue: void 0,
|
|
4098
4103
|
lastError: void 0,
|
|
4099
4104
|
mounts: []
|
|
4100
4105
|
},
|
|
@@ -4197,24 +4202,24 @@ var mountCommand = new Command10("mount").description("Mirror SSH-ready machines
|
|
|
4197
4202
|
console.log(` ${chalk8.dim("Root")} ${config.rootPath}`);
|
|
4198
4203
|
console.log(` ${chalk8.dim("Alias")} ${config.alias}`);
|
|
4199
4204
|
console.log(
|
|
4200
|
-
` ${chalk8.dim("Updated")} ${snapshot?.updatedAt ?
|
|
4205
|
+
` ${chalk8.dim("Updated")} ${snapshot?.updatedAt ? timeAgo(snapshot.updatedAt) : chalk8.dim("never")}`
|
|
4201
4206
|
);
|
|
4202
4207
|
console.log(
|
|
4203
|
-
` ${chalk8.dim("
|
|
4208
|
+
` ${chalk8.dim("Healthy")} ${snapshot?.lastHealthySyncAt ? timeAgo(snapshot.lastHealthySyncAt) : chalk8.dim("never")}`
|
|
4204
4209
|
);
|
|
4205
4210
|
if (controller.running && snapshot?.mounts.length) {
|
|
4206
4211
|
console.log();
|
|
4207
4212
|
for (const mount of snapshot.mounts) {
|
|
4208
|
-
const state = mount.state === "mounted" ? chalk8.green(mount.state) : mount.state === "pending" ? chalk8.yellow(mount.state) : chalk8.red(mount.state);
|
|
4213
|
+
const state = mount.state === "mounted" ? chalk8.green(mount.state) : mount.state === "reconnecting" ? chalk8.cyan(mount.state) : mount.state === "degraded" ? chalk8.yellow(mount.state) : mount.state === "pending" ? chalk8.yellow(mount.state) : chalk8.red(mount.state);
|
|
4209
4214
|
console.log(` ${chalk8.white(mount.handle)} ${state} ${chalk8.dim(mount.mountPath)}`);
|
|
4210
4215
|
if (mount.message) {
|
|
4211
4216
|
console.log(` ${chalk8.dim(mount.message)}`);
|
|
4212
4217
|
}
|
|
4213
4218
|
}
|
|
4214
4219
|
}
|
|
4215
|
-
if (snapshot?.
|
|
4220
|
+
if (snapshot?.lastIssue) {
|
|
4216
4221
|
console.log();
|
|
4217
|
-
console.log(` ${chalk8.dim("Last
|
|
4222
|
+
console.log(` ${chalk8.dim("Last issue")} ${chalk8.yellow(snapshot.lastIssue)}`);
|
|
4218
4223
|
}
|
|
4219
4224
|
console.log();
|
|
4220
4225
|
})
|
|
@@ -10,7 +10,7 @@ interface MountServiceConfig {
|
|
|
10
10
|
interface ManagedMountSnapshot {
|
|
11
11
|
handle: string;
|
|
12
12
|
mountPath: string;
|
|
13
|
-
state: "mounted" | "pending" | "error";
|
|
13
|
+
state: "mounted" | "reconnecting" | "degraded" | "pending" | "error";
|
|
14
14
|
message?: string;
|
|
15
15
|
updatedAt: string;
|
|
16
16
|
}
|
|
@@ -18,7 +18,10 @@ interface MountStatusSnapshot {
|
|
|
18
18
|
updatedAt: string;
|
|
19
19
|
controllerPid?: number;
|
|
20
20
|
running: boolean;
|
|
21
|
+
lastHealthySyncAt?: string;
|
|
21
22
|
lastSuccessfulSyncAt?: string;
|
|
23
|
+
lastIssueAt?: string;
|
|
24
|
+
lastIssue?: string;
|
|
22
25
|
lastError?: string;
|
|
23
26
|
mounts: ManagedMountSnapshot[];
|
|
24
27
|
}
|
|
@@ -35,12 +38,12 @@ interface MountPaths {
|
|
|
35
38
|
}
|
|
36
39
|
interface MountHandlePaths {
|
|
37
40
|
stateDir: string;
|
|
38
|
-
projectFilePath: string;
|
|
39
41
|
metaPath: string;
|
|
40
42
|
sshToolsDir: string;
|
|
41
43
|
}
|
|
42
44
|
interface MountHandleMeta {
|
|
43
45
|
handle: string;
|
|
46
|
+
sessionName?: string;
|
|
44
47
|
startedAt: string;
|
|
45
48
|
lastStartedAt?: string;
|
|
46
49
|
}
|
package/dist/lib/mount-config.js
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MountServiceConfig, MountPaths, MountStatusSnapshot } from './mount-config.js';
|
|
2
|
+
|
|
3
|
+
interface OwnedMountSession {
|
|
4
|
+
identifier: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
handle: string;
|
|
7
|
+
alphaPath: string;
|
|
8
|
+
betaUrl?: string;
|
|
9
|
+
alphaConnected: boolean;
|
|
10
|
+
betaConnected: boolean;
|
|
11
|
+
status?: string;
|
|
12
|
+
lastError?: string;
|
|
13
|
+
scanProblemCount: number;
|
|
14
|
+
conflictCount: number;
|
|
15
|
+
legacy: boolean;
|
|
16
|
+
}
|
|
2
17
|
|
|
3
18
|
interface DesiredMount {
|
|
4
19
|
handle: string;
|
|
@@ -7,18 +22,22 @@ interface DesiredMount {
|
|
|
7
22
|
message?: string;
|
|
8
23
|
}
|
|
9
24
|
interface MountPlan {
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
toCreate: DesiredMount[];
|
|
26
|
+
toInspect: DesiredMount[];
|
|
27
|
+
toReset: string[];
|
|
12
28
|
toStop: string[];
|
|
13
29
|
pending: DesiredMount[];
|
|
14
30
|
}
|
|
15
31
|
declare function computeMountPlan(input: {
|
|
16
32
|
desired: DesiredMount[];
|
|
17
33
|
pending: DesiredMount[];
|
|
18
|
-
|
|
34
|
+
ownedSessions: Pick<OwnedMountSession, "handle" | "legacy">[];
|
|
19
35
|
rootEntries: string[];
|
|
20
36
|
}): MountPlan;
|
|
21
|
-
declare function planMountReconcile(config: MountServiceConfig, paths: MountPaths): Promise<
|
|
37
|
+
declare function planMountReconcile(config: MountServiceConfig, paths: MountPaths): Promise<{
|
|
38
|
+
plan: MountPlan;
|
|
39
|
+
ownedSessions: OwnedMountSession[];
|
|
40
|
+
}>;
|
|
22
41
|
declare function reconcileMounts(config: MountServiceConfig, paths: MountPaths, controllerPid?: number): Promise<MountStatusSnapshot>;
|
|
23
42
|
declare function teardownManagedSessions(config: MountServiceConfig, paths: MountPaths): Promise<void>;
|
|
24
43
|
|