aicomputer 0.1.15 → 0.1.17
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 +12 -3
- package/dist/{chunk-KQQUR2YX.js → chunk-5IEWKH52.js} +341 -113
- package/dist/{chunk-5JVJROSI.js → chunk-KXLTHWW3.js} +0 -2
- package/dist/index.js +161 -44
- 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
|
@@ -95,6 +95,7 @@ computer ssh
|
|
|
95
95
|
computer ssh my-box
|
|
96
96
|
computer ssh --setup
|
|
97
97
|
computer mount
|
|
98
|
+
computer mount --background
|
|
98
99
|
computer mount status
|
|
99
100
|
computer agent agents my-box
|
|
100
101
|
computer agent sessions list my-box
|
|
@@ -117,10 +118,18 @@ ssh agentcomputer.ai
|
|
|
117
118
|
ssh my-box@agentcomputer.ai
|
|
118
119
|
```
|
|
119
120
|
|
|
120
|
-
Run `computer mount` to start
|
|
121
|
-
machine homes under `~/agentcomputer/<handle
|
|
121
|
+
Run `computer mount` to start the mount controller in the foreground and mirror
|
|
122
|
+
all SSH-ready machine homes under `~/agentcomputer/<handle>`.
|
|
123
|
+
On macOS, the foreground command also opens Finder to `~/agentcomputer` after
|
|
124
|
+
the controller starts.
|
|
125
|
+
|
|
126
|
+
Run `computer mount --background` to start the same controller detached from
|
|
127
|
+
your terminal. It prints the controller PID immediately so you can inspect it
|
|
128
|
+
later with `computer mount status`.
|
|
129
|
+
|
|
122
130
|
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.
|
|
131
|
+
OpenSSH client tools (`ssh` and `scp`) to already be installed locally. The
|
|
132
|
+
temporary `~/agentcomputer` root is removed when the command exits.
|
|
124
133
|
|
|
125
134
|
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
135
|
|
|
@@ -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";
|
|
@@ -2461,7 +2460,7 @@ _computer() {
|
|
|
2461
2460
|
'open:Open in browser'
|
|
2462
2461
|
'ssh:SSH into a computer'
|
|
2463
2462
|
'ports:Manage published ports'
|
|
2464
|
-
'mount:Mirror SSH-ready machine homes into ~/agentcomputer
|
|
2463
|
+
'mount:Mirror SSH-ready machine homes into ~/agentcomputer'
|
|
2465
2464
|
'agent:Manage cloud agent sessions'
|
|
2466
2465
|
'acp:Run a local ACP bridge for remote agent sessions'
|
|
2467
2466
|
'rm:Delete a computer'
|
|
@@ -2601,6 +2600,7 @@ _computer() {
|
|
|
2601
2600
|
;;
|
|
2602
2601
|
mount)
|
|
2603
2602
|
_arguments -C \\
|
|
2603
|
+
'--background[Run the mount controller in the background and print its PID]' \\
|
|
2604
2604
|
'--alias[SSH host alias]:alias:' \\
|
|
2605
2605
|
'--host[SSH gateway host]:host:' \\
|
|
2606
2606
|
'--port[SSH gateway port]:port:' \\
|
|
@@ -2772,9 +2772,9 @@ var BASH_SCRIPT = `_computer() {
|
|
|
2772
2772
|
;;
|
|
2773
2773
|
mount)
|
|
2774
2774
|
if [[ $cword -eq 2 ]]; then
|
|
2775
|
-
COMPREPLY=($(compgen -W "$mount_commands" -- "$cur"))
|
|
2775
|
+
COMPREPLY=($(compgen -W "$mount_commands --background --alias --host --port --poll-interval --connect-timeout" -- "$cur"))
|
|
2776
2776
|
elif [[ $cword -ge 3 ]]; then
|
|
2777
|
-
COMPREPLY=($(compgen -W "--alias --host --port --poll-interval --connect-timeout" -- "$cur"))
|
|
2777
|
+
COMPREPLY=($(compgen -W "--background --alias --host --port --poll-interval --connect-timeout" -- "$cur"))
|
|
2778
2778
|
fi
|
|
2779
2779
|
;;
|
|
2780
2780
|
ports)
|
|
@@ -3988,7 +3988,8 @@ function printNextStep(machineHandle) {
|
|
|
3988
3988
|
}
|
|
3989
3989
|
|
|
3990
3990
|
// src/commands/mount.ts
|
|
3991
|
-
import {
|
|
3991
|
+
import { spawn as spawn4 } from "child_process";
|
|
3992
|
+
import { Command as Command10, Option } from "commander";
|
|
3992
3993
|
import chalk8 from "chalk";
|
|
3993
3994
|
import ora9 from "ora";
|
|
3994
3995
|
|
|
@@ -4005,7 +4006,8 @@ function getMountControllerState(rootPath = defaultMountServiceConfig().rootPath
|
|
|
4005
4006
|
}
|
|
4006
4007
|
return { running: true, pid: lock.pid };
|
|
4007
4008
|
}
|
|
4008
|
-
async function runMountDaemon(config) {
|
|
4009
|
+
async function runMountDaemon(config, options = {}) {
|
|
4010
|
+
const { onStarted } = options;
|
|
4009
4011
|
const paths = getMountPaths(config.rootPath);
|
|
4010
4012
|
ensureMountDirectories(paths);
|
|
4011
4013
|
await mkdir3(paths.rootPath, { recursive: true });
|
|
@@ -4019,6 +4021,7 @@ async function runMountDaemon(config) {
|
|
|
4019
4021
|
},
|
|
4020
4022
|
config.rootPath
|
|
4021
4023
|
);
|
|
4024
|
+
onStarted?.();
|
|
4022
4025
|
await teardownManagedSessions(config, paths);
|
|
4023
4026
|
let running = false;
|
|
4024
4027
|
let queued = false;
|
|
@@ -4046,7 +4049,10 @@ async function runMountDaemon(config) {
|
|
|
4046
4049
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4047
4050
|
controllerPid: process.pid,
|
|
4048
4051
|
running: true,
|
|
4052
|
+
lastHealthySyncAt: previous?.lastHealthySyncAt,
|
|
4049
4053
|
lastSuccessfulSyncAt: previous?.lastSuccessfulSyncAt,
|
|
4054
|
+
lastIssueAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4055
|
+
lastIssue: error instanceof Error ? error.message : "mount reconcile failed",
|
|
4050
4056
|
lastError: error instanceof Error ? error.message : "mount reconcile failed",
|
|
4051
4057
|
mounts: previous?.mounts ?? []
|
|
4052
4058
|
},
|
|
@@ -4094,7 +4100,10 @@ async function runMountDaemon(config) {
|
|
|
4094
4100
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4095
4101
|
controllerPid: previous?.controllerPid,
|
|
4096
4102
|
running: false,
|
|
4103
|
+
lastHealthySyncAt: previous?.lastHealthySyncAt,
|
|
4097
4104
|
lastSuccessfulSyncAt: previous?.lastSuccessfulSyncAt,
|
|
4105
|
+
lastIssueAt: previous?.lastIssueAt,
|
|
4106
|
+
lastIssue: void 0,
|
|
4098
4107
|
lastError: void 0,
|
|
4099
4108
|
mounts: []
|
|
4100
4109
|
},
|
|
@@ -4140,40 +4149,39 @@ function processExists(pid) {
|
|
|
4140
4149
|
}
|
|
4141
4150
|
|
|
4142
4151
|
// src/commands/mount.ts
|
|
4143
|
-
var mountCommand = new Command10("mount").description("Mirror SSH-ready machines under ~/agentcomputer
|
|
4144
|
-
|
|
4152
|
+
var mountCommand = new Command10("mount").description("Mirror SSH-ready machines under ~/agentcomputer with a local mount controller").option("--alias <alias>", "SSH host alias", "agentcomputer.ai").option("--host <host>", "SSH gateway host", "ssh.agentcomputer.ai").option("--port <port>", "SSH gateway port", "443").option("--poll-interval <ms>", "Reconcile interval in milliseconds", "5000").option("--connect-timeout <seconds>", "SSH connect timeout for Mutagen", "5").option(
|
|
4153
|
+
"--background",
|
|
4154
|
+
"Run the mount controller in the background and print its PID"
|
|
4155
|
+
).addOption(new Option("--daemonized").hideHelp()).action(async (options) => {
|
|
4156
|
+
const spinner = ora9(
|
|
4157
|
+
options.background ? "Starting machine mount controller in background..." : "Starting machine mount controller..."
|
|
4158
|
+
).start();
|
|
4145
4159
|
try {
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
);
|
|
4160
|
+
if (options.daemonized) {
|
|
4161
|
+
const config2 = readMountConfig() ?? defaultMountServiceConfig();
|
|
4162
|
+
await runMountDaemon(config2, {
|
|
4163
|
+
onStarted: () => {
|
|
4164
|
+
spinner.succeed("Machine mount controller running");
|
|
4165
|
+
printMountStartSummary(config2, process.pid, "foreground");
|
|
4166
|
+
}
|
|
4167
|
+
});
|
|
4168
|
+
return;
|
|
4154
4169
|
}
|
|
4155
|
-
const
|
|
4156
|
-
const config = {
|
|
4157
|
-
...defaultMountServiceConfig(),
|
|
4158
|
-
alias: sshSetup.alias,
|
|
4159
|
-
host: sshSetup.host,
|
|
4160
|
-
port: sshSetup.port,
|
|
4161
|
-
pollIntervalMs: parsePositiveInt(options.pollInterval, "poll interval"),
|
|
4162
|
-
connectTimeoutSeconds: parsePositiveInt(
|
|
4163
|
-
options.connectTimeout,
|
|
4164
|
-
"connect timeout"
|
|
4165
|
-
)
|
|
4166
|
-
};
|
|
4170
|
+
const config = await resolveMountServiceConfig(options);
|
|
4167
4171
|
writeMountConfig(config);
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4172
|
+
if (options.background) {
|
|
4173
|
+
const pid = await startMountControllerInBackground(config.rootPath);
|
|
4174
|
+
spinner.succeed("Machine mount controller running in background");
|
|
4175
|
+
printMountStartSummary(config, pid, "background");
|
|
4176
|
+
return;
|
|
4177
|
+
}
|
|
4178
|
+
await runMountDaemon(config, {
|
|
4179
|
+
onStarted: () => {
|
|
4180
|
+
spinner.succeed("Machine mount controller running");
|
|
4181
|
+
printMountStartSummary(config, process.pid, "foreground");
|
|
4182
|
+
revealMountRootInFinder(config.rootPath);
|
|
4183
|
+
}
|
|
4184
|
+
});
|
|
4177
4185
|
} catch (error) {
|
|
4178
4186
|
spinner.fail(
|
|
4179
4187
|
error instanceof Error ? error.message : "Failed to start machine mount controller"
|
|
@@ -4197,24 +4205,24 @@ var mountCommand = new Command10("mount").description("Mirror SSH-ready machines
|
|
|
4197
4205
|
console.log(` ${chalk8.dim("Root")} ${config.rootPath}`);
|
|
4198
4206
|
console.log(` ${chalk8.dim("Alias")} ${config.alias}`);
|
|
4199
4207
|
console.log(
|
|
4200
|
-
` ${chalk8.dim("Updated")} ${snapshot?.updatedAt ?
|
|
4208
|
+
` ${chalk8.dim("Updated")} ${snapshot?.updatedAt ? timeAgo(snapshot.updatedAt) : chalk8.dim("never")}`
|
|
4201
4209
|
);
|
|
4202
4210
|
console.log(
|
|
4203
|
-
` ${chalk8.dim("
|
|
4211
|
+
` ${chalk8.dim("Healthy")} ${snapshot?.lastHealthySyncAt ? timeAgo(snapshot.lastHealthySyncAt) : chalk8.dim("never")}`
|
|
4204
4212
|
);
|
|
4205
4213
|
if (controller.running && snapshot?.mounts.length) {
|
|
4206
4214
|
console.log();
|
|
4207
4215
|
for (const mount of snapshot.mounts) {
|
|
4208
|
-
const state =
|
|
4216
|
+
const state = formatMountState(mount.state);
|
|
4209
4217
|
console.log(` ${chalk8.white(mount.handle)} ${state} ${chalk8.dim(mount.mountPath)}`);
|
|
4210
4218
|
if (mount.message) {
|
|
4211
4219
|
console.log(` ${chalk8.dim(mount.message)}`);
|
|
4212
4220
|
}
|
|
4213
4221
|
}
|
|
4214
4222
|
}
|
|
4215
|
-
if (snapshot?.
|
|
4223
|
+
if (snapshot?.lastIssue) {
|
|
4216
4224
|
console.log();
|
|
4217
|
-
console.log(` ${chalk8.dim("Last
|
|
4225
|
+
console.log(` ${chalk8.dim("Last issue")} ${chalk8.yellow(snapshot.lastIssue)}`);
|
|
4218
4226
|
}
|
|
4219
4227
|
console.log();
|
|
4220
4228
|
})
|
|
@@ -4226,6 +4234,115 @@ function parsePositiveInt(raw, label) {
|
|
|
4226
4234
|
}
|
|
4227
4235
|
return Math.round(value);
|
|
4228
4236
|
}
|
|
4237
|
+
async function resolveMountServiceConfig(options) {
|
|
4238
|
+
const issues = getMountHostValidationIssues();
|
|
4239
|
+
if (issues.length > 0) {
|
|
4240
|
+
throw new Error(
|
|
4241
|
+
[
|
|
4242
|
+
...issues.map((issue) => issue.message),
|
|
4243
|
+
...formatMountHostInstallGuidance(issues)
|
|
4244
|
+
].join("\n")
|
|
4245
|
+
);
|
|
4246
|
+
}
|
|
4247
|
+
const sshSetup = await ensureSSHAccessConfigured(options);
|
|
4248
|
+
return {
|
|
4249
|
+
...defaultMountServiceConfig(),
|
|
4250
|
+
alias: sshSetup.alias,
|
|
4251
|
+
host: sshSetup.host,
|
|
4252
|
+
port: sshSetup.port,
|
|
4253
|
+
pollIntervalMs: parsePositiveInt(options.pollInterval, "poll interval"),
|
|
4254
|
+
connectTimeoutSeconds: parsePositiveInt(
|
|
4255
|
+
options.connectTimeout,
|
|
4256
|
+
"connect timeout"
|
|
4257
|
+
)
|
|
4258
|
+
};
|
|
4259
|
+
}
|
|
4260
|
+
async function startMountControllerInBackground(rootPath) {
|
|
4261
|
+
const controller = getMountControllerState(rootPath);
|
|
4262
|
+
if (controller.running) {
|
|
4263
|
+
throw new Error(`computer mount is already running (pid ${controller.pid})`);
|
|
4264
|
+
}
|
|
4265
|
+
const entrypoint = process.argv[1];
|
|
4266
|
+
if (!entrypoint) {
|
|
4267
|
+
throw new Error("unable to determine CLI entrypoint for background mount");
|
|
4268
|
+
}
|
|
4269
|
+
const child = spawn4(process.execPath, [entrypoint, "mount", "--daemonized"], {
|
|
4270
|
+
cwd: process.cwd(),
|
|
4271
|
+
detached: true,
|
|
4272
|
+
env: process.env,
|
|
4273
|
+
stdio: "ignore"
|
|
4274
|
+
});
|
|
4275
|
+
if (!child.pid) {
|
|
4276
|
+
throw new Error("failed to start machine mount controller in background");
|
|
4277
|
+
}
|
|
4278
|
+
child.unref();
|
|
4279
|
+
await waitForBackgroundMountController(rootPath, child.pid);
|
|
4280
|
+
return child.pid;
|
|
4281
|
+
}
|
|
4282
|
+
async function waitForBackgroundMountController(rootPath, pid) {
|
|
4283
|
+
const deadline = Date.now() + 3e3;
|
|
4284
|
+
while (Date.now() < deadline) {
|
|
4285
|
+
const controller = getMountControllerState(rootPath);
|
|
4286
|
+
if (controller.running && controller.pid === pid) {
|
|
4287
|
+
return;
|
|
4288
|
+
}
|
|
4289
|
+
if (!processExists2(pid)) {
|
|
4290
|
+
break;
|
|
4291
|
+
}
|
|
4292
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
4293
|
+
}
|
|
4294
|
+
throw new Error("failed to start machine mount controller in background");
|
|
4295
|
+
}
|
|
4296
|
+
function printMountStartSummary(config, pid, mode) {
|
|
4297
|
+
console.log();
|
|
4298
|
+
console.log(chalk8.dim(` PID: ${pid}`));
|
|
4299
|
+
console.log(chalk8.dim(` Root: ${config.rootPath}`));
|
|
4300
|
+
console.log(chalk8.dim(` SSH alias: ${config.alias}`));
|
|
4301
|
+
console.log(chalk8.dim(` Poll: ${config.pollIntervalMs}ms`));
|
|
4302
|
+
console.log();
|
|
4303
|
+
console.log(
|
|
4304
|
+
chalk8.dim(
|
|
4305
|
+
mode === "background" ? " Use `computer mount status` to inspect sync state." : " Press Ctrl-C to stop syncing."
|
|
4306
|
+
)
|
|
4307
|
+
);
|
|
4308
|
+
console.log();
|
|
4309
|
+
}
|
|
4310
|
+
function processExists2(pid) {
|
|
4311
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
4312
|
+
return false;
|
|
4313
|
+
}
|
|
4314
|
+
try {
|
|
4315
|
+
process.kill(pid, 0);
|
|
4316
|
+
return true;
|
|
4317
|
+
} catch {
|
|
4318
|
+
return false;
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
function revealMountRootInFinder(rootPath) {
|
|
4322
|
+
if (process.platform !== "darwin") {
|
|
4323
|
+
return;
|
|
4324
|
+
}
|
|
4325
|
+
const child = spawn4("open", [rootPath], {
|
|
4326
|
+
stdio: "ignore",
|
|
4327
|
+
detached: true
|
|
4328
|
+
});
|
|
4329
|
+
child.on("error", () => {
|
|
4330
|
+
});
|
|
4331
|
+
child.unref();
|
|
4332
|
+
}
|
|
4333
|
+
function formatMountState(state) {
|
|
4334
|
+
switch (state) {
|
|
4335
|
+
case "mounted":
|
|
4336
|
+
return chalk8.green(state);
|
|
4337
|
+
case "reconnecting":
|
|
4338
|
+
return chalk8.cyan(state);
|
|
4339
|
+
case "degraded":
|
|
4340
|
+
case "pending":
|
|
4341
|
+
return chalk8.yellow(state);
|
|
4342
|
+
default:
|
|
4343
|
+
return chalk8.red(state);
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4229
4346
|
|
|
4230
4347
|
// src/commands/logout.ts
|
|
4231
4348
|
import { Command as Command11 } from "commander";
|
|
@@ -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
|
|