@intentic/machine 1.331.0 → 1.332.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -4
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +1 -1
- package/dist/app.js.map +1 -1
- package/dist/autostart/autostart.js +1 -1
- package/dist/autostart/autostart.js.map +1 -1
- package/dist/cli-ui.d.ts +10 -0
- package/dist/cli-ui.d.ts.map +1 -0
- package/dist/cli-ui.js +62 -0
- package/dist/cli-ui.js.map +1 -0
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +61 -26
- package/dist/commands.js.map +1 -1
- package/dist/daemon-base.d.ts +1 -0
- package/dist/daemon-base.d.ts.map +1 -1
- package/dist/daemon-base.js +21 -18
- package/dist/daemon-base.js.map +1 -1
- package/dist/device/audit.d.ts.map +1 -1
- package/dist/device/audit.js.map +1 -1
- package/dist/device/commands.d.ts +8 -6
- package/dist/device/commands.d.ts.map +1 -1
- package/dist/device/commands.js +64 -42
- package/dist/device/commands.js.map +1 -1
- package/dist/device/connection.d.ts.map +1 -1
- package/dist/device/connection.js +2 -3
- package/dist/device/connection.js.map +1 -1
- package/dist/device/grant.d.ts +7 -0
- package/dist/device/grant.d.ts.map +1 -1
- package/dist/device/loopback-catch.d.ts.map +1 -1
- package/dist/device/loopback-catch.js +1 -1
- package/dist/device/loopback-catch.js.map +1 -1
- package/dist/device/mcp.d.ts.map +1 -1
- package/dist/device/mcp.js +19 -0
- package/dist/device/mcp.js.map +1 -1
- package/dist/device/policy.d.ts.map +1 -1
- package/dist/device/policy.js +3 -5
- package/dist/device/policy.js.map +1 -1
- package/dist/device/router.d.ts +7 -0
- package/dist/device/router.d.ts.map +1 -1
- package/dist/device/router.js +17 -5
- package/dist/device/router.js.map +1 -1
- package/dist/device/sandbox-cli.d.ts +12 -1
- package/dist/device/sandbox-cli.d.ts.map +1 -1
- package/dist/device/sandbox-cli.js +90 -18
- package/dist/device/sandbox-cli.js.map +1 -1
- package/dist/device/sandbox-rounds/keeper.d.ts +28 -1
- package/dist/device/sandbox-rounds/keeper.d.ts.map +1 -1
- package/dist/device/sandbox-rounds/keeper.js +115 -18
- package/dist/device/sandbox-rounds/keeper.js.map +1 -1
- package/dist/device/sandbox-rounds/sleep.d.ts +52 -0
- package/dist/device/sandbox-rounds/sleep.d.ts.map +1 -0
- package/dist/device/sandbox-rounds/sleep.js +115 -0
- package/dist/device/sandbox-rounds/sleep.js.map +1 -0
- package/dist/device/tools/agent.js +1 -1
- package/dist/device/tools/agent.js.map +1 -1
- package/dist/device/tools/android.d.ts.map +1 -1
- package/dist/device/tools/android.js +7 -62
- package/dist/device/tools/android.js.map +1 -1
- package/dist/device/tools/describe.d.ts +6 -0
- package/dist/device/tools/describe.d.ts.map +1 -1
- package/dist/device/tools/describe.js +30 -9
- package/dist/device/tools/describe.js.map +1 -1
- package/dist/device/tools/ic-binary.d.ts +7 -0
- package/dist/device/tools/ic-binary.d.ts.map +1 -1
- package/dist/device/tools/ic-binary.js +16 -8
- package/dist/device/tools/ic-binary.js.map +1 -1
- package/dist/device/tools/logs/logs.d.ts +30 -0
- package/dist/device/tools/logs/logs.d.ts.map +1 -0
- package/dist/device/tools/logs/logs.js +147 -0
- package/dist/device/tools/logs/logs.js.map +1 -0
- package/dist/device/tools/programs/artifacts.d.ts.map +1 -1
- package/dist/device/tools/programs/artifacts.js +5 -10
- package/dist/device/tools/programs/artifacts.js.map +1 -1
- package/dist/device/tools/programs/{tunnel.d.ts → device-tunnel.d.ts} +1 -1
- package/dist/device/tools/programs/device-tunnel.d.ts.map +1 -0
- package/dist/device/tools/programs/{tunnel.js → device-tunnel.js} +1 -1
- package/dist/device/tools/programs/device-tunnel.js.map +1 -0
- package/dist/device/tools/programs/isolated.d.ts +0 -1
- package/dist/device/tools/programs/isolated.d.ts.map +1 -1
- package/dist/device/tools/programs/isolated.js +8 -10
- package/dist/device/tools/programs/isolated.js.map +1 -1
- package/dist/device/tools/programs/programs.d.ts.map +1 -1
- package/dist/device/tools/programs/programs.js +87 -35
- package/dist/device/tools/programs/programs.js.map +1 -1
- package/dist/device/tools/sandboxes.d.ts.map +1 -1
- package/dist/device/tools/sandboxes.js +11 -10
- package/dist/device/tools/sandboxes.js.map +1 -1
- package/dist/device/tools/shell.d.ts +3 -4
- package/dist/device/tools/shell.d.ts.map +1 -1
- package/dist/device/tools/shell.js +24 -86
- package/dist/device/tools/shell.js.map +1 -1
- package/dist/engine-env.d.ts +15 -0
- package/dist/engine-env.d.ts.map +1 -0
- package/dist/engine-env.js +80 -0
- package/dist/engine-env.js.map +1 -0
- package/dist/environments/commands.d.ts +7 -1
- package/dist/environments/commands.d.ts.map +1 -1
- package/dist/environments/commands.js +37 -31
- package/dist/environments/commands.js.map +1 -1
- package/dist/environments/machine-upgrade.d.ts +8 -1
- package/dist/environments/machine-upgrade.d.ts.map +1 -1
- package/dist/environments/machine-upgrade.js +35 -26
- package/dist/environments/machine-upgrade.js.map +1 -1
- package/dist/environments/machine.d.ts +5 -4
- package/dist/environments/machine.d.ts.map +1 -1
- package/dist/environments/machine.js +13 -16
- package/dist/environments/machine.js.map +1 -1
- package/dist/environments/wsl.d.ts.map +1 -1
- package/dist/environments/wsl.js +4 -7
- package/dist/environments/wsl.js.map +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +28 -30
- package/dist/install.js.map +1 -1
- package/dist/resident/last-exit.d.ts +40 -0
- package/dist/resident/last-exit.d.ts.map +1 -0
- package/dist/resident/last-exit.js +147 -0
- package/dist/resident/last-exit.js.map +1 -0
- package/dist/resident/log-roll.d.ts +8 -0
- package/dist/resident/log-roll.d.ts.map +1 -0
- package/dist/resident/log-roll.js +30 -0
- package/dist/resident/log-roll.js.map +1 -0
- package/dist/{resident.d.ts → resident/resident.d.ts} +1 -1
- package/dist/resident/resident.d.ts.map +1 -0
- package/dist/{resident.js → resident/resident.js} +52 -27
- package/dist/resident/resident.js.map +1 -0
- package/dist/{supervision.d.ts → resident/supervision.d.ts} +7 -3
- package/dist/resident/supervision.d.ts.map +1 -0
- package/dist/{supervision.js → resident/supervision.js} +17 -14
- package/dist/resident/supervision.js.map +1 -0
- package/dist/resident/watchdog.d.ts +24 -0
- package/dist/resident/watchdog.d.ts.map +1 -0
- package/dist/resident/watchdog.js +69 -0
- package/dist/resident/watchdog.js.map +1 -0
- package/dist/status.d.ts +14 -8
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +141 -107
- package/dist/status.js.map +1 -1
- package/dist/sync/attach-commands.d.ts +1 -1
- package/dist/sync/attach-commands.d.ts.map +1 -1
- package/dist/sync/attach-commands.js +19 -12
- package/dist/sync/attach-commands.js.map +1 -1
- package/dist/sync/commands.d.ts +6 -3
- package/dist/sync/commands.d.ts.map +1 -1
- package/dist/sync/commands.js +149 -113
- package/dist/sync/commands.js.map +1 -1
- package/dist/sync/exec.d.ts +9 -0
- package/dist/sync/exec.d.ts.map +1 -1
- package/dist/sync/exec.js +21 -22
- package/dist/sync/exec.js.map +1 -1
- package/dist/sync/folders.d.ts.map +1 -1
- package/dist/sync/folders.js +3 -14
- package/dist/sync/folders.js.map +1 -1
- package/dist/sync/forget-command.d.ts +1 -1
- package/dist/sync/forget-command.d.ts.map +1 -1
- package/dist/sync/forget-command.js +10 -7
- package/dist/sync/forget-command.js.map +1 -1
- package/dist/sync/git-bridge.d.ts.map +1 -1
- package/dist/sync/git-bridge.js +4 -2
- package/dist/sync/git-bridge.js.map +1 -1
- package/dist/sync/local-sandboxes.d.ts.map +1 -1
- package/dist/sync/local-sandboxes.js +2 -4
- package/dist/sync/local-sandboxes.js.map +1 -1
- package/dist/sync/mirror.d.ts.map +1 -1
- package/dist/sync/mirror.js +7 -16
- package/dist/sync/mirror.js.map +1 -1
- package/dist/sync/mutagen.d.ts.map +1 -1
- package/dist/sync/mutagen.js +33 -37
- package/dist/sync/mutagen.js.map +1 -1
- package/dist/sync/project/project-commands.d.ts +7 -2
- package/dist/sync/project/project-commands.d.ts.map +1 -1
- package/dist/sync/project/project-commands.js +110 -46
- package/dist/sync/project/project-commands.js.map +1 -1
- package/dist/sync/project/project-files.d.ts +4 -6
- package/dist/sync/project/project-files.d.ts.map +1 -1
- package/dist/sync/project/project-files.js +4 -6
- package/dist/sync/project/project-files.js.map +1 -1
- package/dist/sync/project/project-local.d.ts.map +1 -1
- package/dist/sync/project/project-local.js +3 -19
- package/dist/sync/project/project-local.js.map +1 -1
- package/dist/sync/project/project-remote.js +4 -4
- package/dist/sync/project/project-transfer.d.ts +13 -30
- package/dist/sync/project/project-transfer.d.ts.map +1 -1
- package/dist/sync/project/project-transfer.js +1 -1
- package/dist/sync/project/project-transfer.js.map +1 -1
- package/dist/sync/retire.d.ts.map +1 -1
- package/dist/sync/retire.js +3 -3
- package/dist/sync/retire.js.map +1 -1
- package/dist/sync/siblings.d.ts.map +1 -1
- package/dist/sync/siblings.js +3 -6
- package/dist/sync/siblings.js.map +1 -1
- package/dist/sync/ssh.d.ts +1 -1
- package/dist/sync/ssh.d.ts.map +1 -1
- package/dist/sync/ssh.js +8 -7
- package/dist/sync/ssh.js.map +1 -1
- package/dist/sync/swap-pause.d.ts.map +1 -1
- package/dist/sync/swap-pause.js +2 -2
- package/dist/sync/swap-pause.js.map +1 -1
- package/dist/upgrade.d.ts +7 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +24 -13
- package/dist/upgrade.js.map +1 -1
- package/dist/upkeep/doctor.d.ts +4 -2
- package/dist/upkeep/doctor.d.ts.map +1 -1
- package/dist/upkeep/doctor.js +40 -26
- package/dist/upkeep/doctor.js.map +1 -1
- package/dist/upkeep/login.js +1 -1
- package/dist/upkeep/login.js.map +1 -1
- package/dist/upkeep/retention.d.ts +6 -0
- package/dist/upkeep/retention.d.ts.map +1 -1
- package/dist/upkeep/retention.js +55 -3
- package/dist/upkeep/retention.js.map +1 -1
- package/dist/upkeep/retired.js +1 -1
- package/dist/upkeep/retired.js.map +1 -1
- package/dist/upkeep/second-ic.d.ts.map +1 -1
- package/dist/upkeep/second-ic.js +4 -6
- package/dist/upkeep/second-ic.js.map +1 -1
- package/dist/upkeep/trash.d.ts.map +1 -1
- package/dist/upkeep/trash.js +5 -6
- package/dist/upkeep/trash.js.map +1 -1
- package/package.json +9 -9
- package/dist/device/tools/programs/tunnel.d.ts.map +0 -1
- package/dist/device/tools/programs/tunnel.js.map +0 -1
- package/dist/resident.d.ts.map +0 -1
- package/dist/resident.js.map +0 -1
- package/dist/supervision.d.ts.map +0 -1
- package/dist/supervision.js.map +0 -1
- package/dist/watchdog.d.ts +0 -13
- package/dist/watchdog.d.ts.map +0 -1
- package/dist/watchdog.js +0 -38
- package/dist/watchdog.js.map +0 -1
package/dist/sync/commands.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { mkdir, readdir } from "node:fs/promises";
|
|
3
2
|
import { join, resolve } from "node:path";
|
|
4
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
4
|
import { errorMessage, undefinedIfMissing } from "@intentic/base/errors";
|
|
6
5
|
import { plural } from "@intentic/base/format";
|
|
7
6
|
import { WORKSPACE_ROOT } from "@intentic/constants";
|
|
8
|
-
import { clearWindowsRunValue,
|
|
7
|
+
import { clearWindowsRunValue, explain, homeDir } from "@intentic/local-agent";
|
|
9
8
|
import { environmentKeyOf, projectDirNameOf, sandboxIdFromUrl, SyncEnrollmentAnswerSchema, } from "@intentic/sandbox-contract";
|
|
10
|
-
import {
|
|
9
|
+
import { buildRouteMap } from "@stricli/core";
|
|
10
|
+
import { buildCommand, machineUi, sandboxUrlOf } from "../cli-ui.js";
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { postWhileWarming } from "../daemon-base.js";
|
|
13
13
|
import { completeSetup, prepareSetup } from "../install.js";
|
|
14
14
|
import { machineId } from "../machine-id.js";
|
|
15
15
|
import { wslEnvironment } from "../environments/wsl.js";
|
|
16
|
-
import { ensureResident, readResidentBuild, readResidentPid } from "../resident.js";
|
|
16
|
+
import { ensureResident, readResidentBuild, readResidentPid } from "../resident/resident.js";
|
|
17
17
|
import { MACHINE_VERSION } from "../version.js";
|
|
18
|
-
import { machineLauncher } from "../supervision.js";
|
|
18
|
+
import { machineLauncher } from "../resident/supervision.js";
|
|
19
19
|
import { isAttachedPairing, isProjectPairing, pairingKey, pairingRemoteDir, readState, removePairing, setAutoHealOff, setMirrorOff, setPortIgnored, upsertPairing, } from "./config.js";
|
|
20
20
|
import { transportFor } from "./endpoint.js";
|
|
21
|
+
import { runProcess } from "./exec.js";
|
|
21
22
|
import { overlappingPairing } from "./folders.js";
|
|
22
23
|
import { realBridgeExec, runGitBridge } from "./git-bridge.js";
|
|
23
24
|
import { retireMirroredPort, retirePairingMirror, teardownAllForwards } from "./mirror.js";
|
|
@@ -30,22 +31,35 @@ import { refuseAcrossPc } from "./siblings.js";
|
|
|
30
31
|
import { syncSshPort, tunnelReady } from "./tunnel.js";
|
|
31
32
|
import { assertSshConfigVisible, ensureSshKey, hostKeyOf, mutagenSshPath, pairingSshConfig, probeSshTransport, removeManagedSshConfig, replaceKnownHost, sanitizeId, sshAlias, writeManagedSshConfig, } from "./ssh.js";
|
|
32
33
|
export { transportFor } from "./endpoint.js";
|
|
34
|
+
const urlHost = (value) => {
|
|
35
|
+
if (!value.includes("://")) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return new URL(value).host;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
33
45
|
export const selectPairings = (state, selector) => {
|
|
34
46
|
if (selector === undefined) {
|
|
35
47
|
return state.pairings;
|
|
36
48
|
}
|
|
37
49
|
const ids = [...new Set(state.pairings.map((pairing) => pairing.sandboxId))];
|
|
38
|
-
const
|
|
50
|
+
const wanted = sanitizeId(urlHost(selector) ?? selector);
|
|
51
|
+
const exact = ids.filter((id) => id === wanted);
|
|
39
52
|
const matched = exact.length === 1 ? exact : ids.filter((id) => id.includes(selector));
|
|
40
53
|
if (matched.length === 0) {
|
|
41
|
-
throw new Error(`
|
|
54
|
+
throw new Error(explain(`No paired sandbox matches ${selector}.`, `this device pairs ${ids.length === 0 ? "no sandbox" : ids.join(", ")}.`));
|
|
42
55
|
}
|
|
43
56
|
if (matched.length > 1) {
|
|
44
|
-
throw new Error(
|
|
57
|
+
throw new Error(explain(`${selector} matches more than one paired sandbox.`, `it matches ${matched.join(", ")}.`, "name one of them in full with --sandbox."));
|
|
45
58
|
}
|
|
46
59
|
return state.pairings.filter((pairing) => pairing.sandboxId === matched[0]);
|
|
47
60
|
};
|
|
48
|
-
|
|
61
|
+
const SYNC_NOT_SET_UP = "Desktop sync was not set up.";
|
|
62
|
+
export const enrollKey = async (sandboxUrl, pairToken, key, { attempts, delayMs, takeover = false, identity, say, } = {}) => {
|
|
49
63
|
const response = await postWhileWarming(sandboxUrl, "/system/authorized-key", {
|
|
50
64
|
headers: {
|
|
51
65
|
"content-type": "application/json",
|
|
@@ -55,25 +69,26 @@ export const enrollKey = async (sandboxUrl, pairToken, key, { attempts, delayMs,
|
|
|
55
69
|
body: JSON.stringify({ key, ...identity }),
|
|
56
70
|
}, {
|
|
57
71
|
doing: "enrolling the sync key",
|
|
58
|
-
expired: "pairing expired
|
|
72
|
+
expired: explain(SYNC_NOT_SET_UP, "the pairing has expired.", "click Enable desktop sync again in your browser for a fresh command."),
|
|
73
|
+
say,
|
|
59
74
|
attempts,
|
|
60
75
|
delayMs,
|
|
61
76
|
});
|
|
62
77
|
if (response.status === 423) {
|
|
63
78
|
const held = (await response.json().catch(() => ({})));
|
|
64
79
|
const from = held.machine !== undefined ? ` from "${held.machine}"` : "";
|
|
65
|
-
throw new Error(`
|
|
80
|
+
throw new Error(explain(`Desktop sync is already active on this sandbox${from}.`, "one device at a time holds a sandbox's file sync.", "run the same command again with --takeover to move it to this device; the other one stops syncing."));
|
|
66
81
|
}
|
|
67
82
|
if (!response.ok) {
|
|
68
|
-
throw new Error(`enrolling the sync key failed (${response.status}): ${await response.text()}`);
|
|
83
|
+
throw new Error(explain(SYNC_NOT_SET_UP, `enrolling the sync key failed (${response.status}): ${await response.text()}`));
|
|
69
84
|
}
|
|
70
85
|
const answer = SyncEnrollmentAnswerSchema.safeParse(await response.json());
|
|
71
86
|
if (!answer.success) {
|
|
72
|
-
throw new Error(`the sandbox enrolled this
|
|
87
|
+
throw new Error(explain(SYNC_NOT_SET_UP, `the sandbox enrolled this device but its answer could not be read: ${z.prettifyError(answer.error)}`));
|
|
73
88
|
}
|
|
74
89
|
const body = answer.data;
|
|
75
90
|
if (body.syncToken === undefined) {
|
|
76
|
-
throw new Error("the sandbox enrolled this
|
|
91
|
+
throw new Error(explain(SYNC_NOT_SET_UP, "the sandbox enrolled this device but returned no sync credential.", "update the sandbox, then enable sync again."));
|
|
77
92
|
}
|
|
78
93
|
return { syncToken: body.syncToken, mode: body.mode ?? "sync", hostKey: hostKeyOf(body.hostKey) };
|
|
79
94
|
};
|
|
@@ -113,11 +128,12 @@ const wantedFolder = (flags, sandboxId) => resolve(flags.dir === undefined
|
|
|
113
128
|
: flags.dir.replace(/^~(?=[\\/]|$)/, homeDir()));
|
|
114
129
|
const setup = buildCommand({
|
|
115
130
|
docs: {
|
|
116
|
-
brief: "Enroll this
|
|
131
|
+
brief: "Enroll this device with a pairing token and start a Mutagen sync of the local dir ↔ sandbox /work (or a project folder in it), over ssh or, for a project on this device's Docker engine, through Docker",
|
|
117
132
|
},
|
|
118
133
|
parameters: {
|
|
119
134
|
flags: {
|
|
120
|
-
url: { kind: "parsed", parse: String, brief: "The sandbox's public URL (e.g. https://sandbox-xxx.example.dev)" },
|
|
135
|
+
url: { kind: "parsed", parse: String, optional: true, brief: "The sandbox's public URL (e.g. https://sandbox-xxx.example.dev)" },
|
|
136
|
+
sandbox: { kind: "parsed", parse: String, optional: true, brief: "The same as --url" },
|
|
121
137
|
pair: { kind: "parsed", parse: String, brief: "The one-time pairing token from the Desktop sync card" },
|
|
122
138
|
dir: {
|
|
123
139
|
kind: "parsed",
|
|
@@ -126,7 +142,7 @@ const setup = buildCommand({
|
|
|
126
142
|
brief: "Local directory to sync (default: ~/intentic/<sandbox id>, the id in the sandbox's own URL)",
|
|
127
143
|
},
|
|
128
144
|
sandboxId: { kind: "parsed", parse: String, optional: true, brief: "Session/alias id (default: the sandbox URL host)" },
|
|
129
|
-
takeover: { kind: "boolean", brief: "Take over sync from another
|
|
145
|
+
takeover: { kind: "boolean", brief: "Take over sync from another device already enrolled on this sandbox (revokes its key)" },
|
|
130
146
|
remoteDir: {
|
|
131
147
|
kind: "parsed",
|
|
132
148
|
parse: parseRemoteDir,
|
|
@@ -141,7 +157,7 @@ const setup = buildCommand({
|
|
|
141
157
|
kind: "parsed",
|
|
142
158
|
parse: parseTransport,
|
|
143
159
|
optional: true,
|
|
144
|
-
brief: "How the sandbox is reached: auto (the default: Docker for a project whose sandbox runs on this
|
|
160
|
+
brief: "How the sandbox is reached: auto (the default: Docker for a project whose sandbox runs on this device's engine, ssh otherwise), ssh or docker",
|
|
145
161
|
},
|
|
146
162
|
projectsHost: {
|
|
147
163
|
kind: "boolean",
|
|
@@ -150,21 +166,18 @@ const setup = buildCommand({
|
|
|
150
166
|
},
|
|
151
167
|
},
|
|
152
168
|
},
|
|
153
|
-
async func(
|
|
154
|
-
|
|
155
|
-
const ui =
|
|
169
|
+
async func(given) {
|
|
170
|
+
const flags = { ...given, url: sandboxUrlOf(given.url, given.sandbox, "Desktop sync was not set up.") };
|
|
171
|
+
const ui = machineUi(this.process);
|
|
172
|
+
await prepareSetup(ui.note, process.argv.slice(2));
|
|
156
173
|
const out = ui.note;
|
|
157
174
|
ui.begin("intentic · desktop sync", SETUP_PLAN);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
finally {
|
|
162
|
-
ui.close();
|
|
163
|
-
}
|
|
175
|
+
await runSetup(ui, out, flags);
|
|
176
|
+
ui.close();
|
|
164
177
|
},
|
|
165
178
|
});
|
|
166
179
|
const SETUP_PLAN = [
|
|
167
|
-
{ phase: "sync-enrolling", label: "
|
|
180
|
+
{ phase: "sync-enrolling", label: "Connect this device", weight: 25 },
|
|
168
181
|
{ phase: "sync-linking", label: "Link the folder", weight: 5 },
|
|
169
182
|
{ phase: "sync-starting", label: "Start syncing", weight: 20 },
|
|
170
183
|
];
|
|
@@ -175,27 +188,28 @@ export const placementChange = (held, placement) => {
|
|
|
175
188
|
const same = pairingRemoteDir(held) === pairingRemoteDir(placement) && isProjectPairing(held) === isProjectPairing(placement);
|
|
176
189
|
return same
|
|
177
190
|
? undefined
|
|
178
|
-
: `${held.sandboxId} already syncs ${held.localDir} with its ${pairingRemoteDir(held)}${isProjectPairing(held) ? " as a project" : ""}; setting it up again for ${pairingRemoteDir(placement)}${isProjectPairing(placement) ? " as a project" : ""} would rewrite what that folder holds. Unpair it first
|
|
191
|
+
: explain(`${held.sandboxId} was not set up again.`, `it already syncs ${held.localDir} with its ${pairingRemoteDir(held)}${isProjectPairing(held) ? " as a project" : ""}; setting it up again for ${pairingRemoteDir(placement)}${isProjectPairing(placement) ? " as a project" : ""} would rewrite what that folder holds. Unpair it first, then set it up again.`, unpairCommand(held.sandboxId));
|
|
179
192
|
};
|
|
193
|
+
const unpairCommand = (sandboxId) => `intentic-machine sync uninstall --sandbox ${sandboxId}`;
|
|
180
194
|
export const projectAskedWithoutFlag = (flags, env = process.env) => {
|
|
181
195
|
const remote = env["SYNC_REMOTE_DIR"] ?? ``;
|
|
182
196
|
const asked = (env["SYNC_PROJECT"] ?? ``) !== `` || (remote !== `` && remote !== WORKSPACE_ROOT);
|
|
183
197
|
return asked && !flags.project
|
|
184
|
-
?
|
|
198
|
+
? explain("Nothing was enrolled.", "this setup was asked to sync a project folder (SYNC_PROJECT, SYNC_REMOTE_DIR), but the install script that ran it did not pass --project: it is older than this agent.", "run the setup again once the script is current.")
|
|
185
199
|
: undefined;
|
|
186
200
|
};
|
|
187
201
|
export const projectsHostChange = (held, flags) => {
|
|
188
202
|
if (flags.projectsHost === true && (flags.dir !== undefined || flags.remoteDir !== undefined || flags.project)) {
|
|
189
|
-
return "--projects-host enrolls this
|
|
203
|
+
return explain("Nothing was enrolled.", "--projects-host enrolls this device's sandbox with no folder of its own, so it takes no --dir, --remote-dir or --project; folders attach to it afterwards.", "run the same command again without --dir, --remote-dir and --project.");
|
|
190
204
|
}
|
|
191
205
|
if (held === undefined) {
|
|
192
206
|
return undefined;
|
|
193
207
|
}
|
|
194
208
|
if (flags.projectsHost === true && held.localDir !== undefined) {
|
|
195
|
-
return `${held.sandboxId} already syncs ${held.localDir}; setting it up as this
|
|
209
|
+
return explain(`${held.sandboxId} was not set up again.`, `it already syncs ${held.localDir}; setting it up as this device's sandbox for folders would stop that. Unpair it first, then set it up again.`, unpairCommand(held.sandboxId));
|
|
196
210
|
}
|
|
197
211
|
if (flags.projectsHost !== true && held.projectsHost === true) {
|
|
198
|
-
return `${held.sandboxId} is this
|
|
212
|
+
return explain(`${held.sandboxId} was not set up again.`, "it is this device's sandbox for folders, which attach to it; setting it up for a folder of its own would leave them without it. Set it up again with --projects-host, or unpair it first.", unpairCommand(held.sandboxId));
|
|
199
213
|
}
|
|
200
214
|
return undefined;
|
|
201
215
|
};
|
|
@@ -227,7 +241,7 @@ const planSetup = async (flags) => {
|
|
|
227
241
|
}
|
|
228
242
|
const clash = await overlappingPairing(folder, sandboxId, pairings);
|
|
229
243
|
if (clash !== undefined) {
|
|
230
|
-
throw new Error(`${folder} overlaps ${clash.localDir}, which already syncs with ${clash.sandboxId}
|
|
244
|
+
throw new Error(explain(`${folder} was not set up.`, `it overlaps ${clash.localDir}, which already syncs with ${clash.sandboxId}, and two syncs over one folder overwrite each other's files.`, "choose a folder that neither is, holds nor sits inside one this device syncs; intentic-machine status lists them."));
|
|
231
245
|
}
|
|
232
246
|
const across = await refuseAcrossPc(folder);
|
|
233
247
|
if (across.refusal !== undefined) {
|
|
@@ -237,15 +251,16 @@ const planSetup = async (flags) => {
|
|
|
237
251
|
return { sandboxId, placement, folder, reach, unread: across.unread };
|
|
238
252
|
};
|
|
239
253
|
const runSetup = async (ui, out, flags) => {
|
|
240
|
-
ui.step("sync-enrolling", "enrolling this
|
|
254
|
+
ui.step("sync-enrolling", "enrolling this device with your sandbox…");
|
|
241
255
|
const { sandboxId, placement, folder, reach, unread = [] } = await planSetup(flags);
|
|
242
256
|
if (unread.length > 0) {
|
|
243
|
-
|
|
257
|
+
ui.warn(`${unread.join(", ")} could not be asked which folders ${unread.length === 1 ? "it syncs" : "they sync"}, so ${folder ?? "this folder"} was not checked against ${unread.length === 1 ? "it" : "them"}.`);
|
|
244
258
|
}
|
|
245
259
|
const publicKey = await ensureSshKey();
|
|
246
260
|
const [{ syncToken, mode, hostKey }, mutagen] = await Promise.all([
|
|
247
261
|
enrollKey(flags.url, flags.pair, publicKey, {
|
|
248
262
|
takeover: flags.takeover,
|
|
263
|
+
say: out,
|
|
249
264
|
identity: { machineId: machineId(), environment: environmentKeyOf({ wsl: await wslEnvironment() }) },
|
|
250
265
|
}),
|
|
251
266
|
ensureMutagen(),
|
|
@@ -254,8 +269,8 @@ const runSetup = async (ui, out, flags) => {
|
|
|
254
269
|
const container = dockerContainerOf(reach, mode);
|
|
255
270
|
await pinHostKey(sandboxId, hostKey, container);
|
|
256
271
|
if ((placement.project === true || flags.projectsHost === true) && mode !== "sync") {
|
|
257
|
-
await revokeEnrollment(flags.url, syncToken).catch((error) =>
|
|
258
|
-
throw new Error(
|
|
272
|
+
await revokeEnrollment(flags.url, syncToken).catch((error) => ui.warn(`${flags.url} could not be told to forget this device (${errorMessage(error)}), so its sync key is still authorized there. Remove this device from that sandbox's Devices view.`));
|
|
273
|
+
throw new Error(explain(flags.projectsHost === true ? "No folder can attach to this sandbox here." : "This project folder cannot sync here.", "another device holds file sync for this sandbox.", "run the same command again with --takeover to move sync to this device."));
|
|
259
274
|
}
|
|
260
275
|
const localDir = mode === "sync" ? folder : undefined;
|
|
261
276
|
const adopt = localDir !== undefined && placement.project !== true && (await holdsEntries(localDir));
|
|
@@ -277,16 +292,19 @@ const runSetup = async (ui, out, flags) => {
|
|
|
277
292
|
const pairings = (await readState()).pairings;
|
|
278
293
|
await writeManagedSshConfig(pairingSshConfig(pairings));
|
|
279
294
|
ui.step("sync-starting", "starting the sync engine…");
|
|
295
|
+
const later = [];
|
|
280
296
|
await ensureResident(out);
|
|
281
297
|
await completeSetup(out);
|
|
282
|
-
await proveTransport(out, sandboxId, container);
|
|
298
|
+
await proveTransport(out, ui.warn, sandboxId, container);
|
|
283
299
|
if (mode === "sync" && localDir !== undefined && !(await syncSessionAppears(mutagen, sandboxId, SESSION_READY_MS))) {
|
|
284
|
-
|
|
300
|
+
const late = await notStartedYet(localDir);
|
|
301
|
+
ui.warn(late.said);
|
|
302
|
+
later.push(late.later);
|
|
285
303
|
}
|
|
286
304
|
retireOrphanSessions(mutagen, pairings, out);
|
|
287
305
|
runGitBridge(realBridgeExec, pairing, out, undefined);
|
|
288
306
|
registerMutagenAutostart(mutagen, machineLauncher(), out);
|
|
289
|
-
finishSetup(ui, flags.url, pairing, pairings);
|
|
307
|
+
finishSetup(ui, flags.url, pairing, pairings, later);
|
|
290
308
|
};
|
|
291
309
|
const holdsEntries = async (dir) => ((await readdir(dir).catch(undefinedIfMissing))?.length ?? 0) > 0;
|
|
292
310
|
const dockerContainerOf = (reach, mode) => mode === "sync" && reach.transport === "docker" ? reach.container : undefined;
|
|
@@ -295,18 +313,18 @@ const pinHostKey = async (sandboxId, hostKey, container) => {
|
|
|
295
313
|
await replaceKnownHost(sshAlias(sandboxId), syncSshPort(sandboxId), hostKey);
|
|
296
314
|
}
|
|
297
315
|
};
|
|
298
|
-
const proveTransport = async (out, sandboxId, container) => {
|
|
316
|
+
const proveTransport = async (out, warn, sandboxId, container) => {
|
|
299
317
|
if (container !== undefined) {
|
|
300
|
-
out(`reaching ${sandboxId} through Docker on this
|
|
318
|
+
out(`reaching ${sandboxId} through Docker on this device (${container}): no ssh key, tunnel or public address on the data path`);
|
|
301
319
|
return;
|
|
302
320
|
}
|
|
303
321
|
const alias = sshAlias(sandboxId);
|
|
304
322
|
const port = syncSshPort(sandboxId);
|
|
305
323
|
if (!(await tunnelReady(port, TUNNEL_READY_MS))) {
|
|
306
|
-
|
|
324
|
+
warn(`The sync transport for ${sandboxId} isn't listening on 127.0.0.1:${port} yet; syncing starts as soon as it is.`);
|
|
307
325
|
}
|
|
308
326
|
const ssh = mutagenSshPath(process.platform, process.env["MUTAGEN_SSH_PATH"]);
|
|
309
|
-
assertSshConfigVisible(ssh, alias, port);
|
|
327
|
+
await assertSshConfigVisible(ssh, alias, port);
|
|
310
328
|
await probeSshTransport(ssh, alias, out);
|
|
311
329
|
};
|
|
312
330
|
const syncedSentence = (pairing) => {
|
|
@@ -315,25 +333,23 @@ const syncedSentence = (pairing) => {
|
|
|
315
333
|
? `${kept}\nThe folder already held files: any that differ from the sandbox's copy are moved to ${asidePrefix(pairing.localDir)}<time> first, so the sandbox's version is the one that syncs. Nothing is deleted.`
|
|
316
334
|
: kept;
|
|
317
335
|
};
|
|
318
|
-
const finishSetup = (ui, sandboxUrl, pairing, pairings) => {
|
|
336
|
+
const finishSetup = (ui, sandboxUrl, pairing, pairings, later = []) => {
|
|
319
337
|
if (pairings.length > 1) {
|
|
320
|
-
ui.note(`This
|
|
338
|
+
ui.note(`This device now syncs ${pairings.length} pairings:`);
|
|
321
339
|
for (const held of pairings) {
|
|
322
|
-
ui.note(` ${pairingKey(held)}${held.projectsHost === true ? " (this
|
|
340
|
+
ui.note(` ${pairingKey(held)}${held.projectsHost === true ? " (this device's sandbox, for folders)" : held.localDir === undefined ? " (ports only)" : ` → ${held.localDir}`}`);
|
|
323
341
|
}
|
|
324
342
|
}
|
|
325
343
|
if (pairing.projectsHost === true) {
|
|
326
|
-
ui.finished("This
|
|
327
|
-
["attach a folder",
|
|
344
|
+
ui.finished("This device's sandbox is ready for folders.", undefined, `Ports from ${sandboxUrl} answer on this device's localhost, and folders attach to it as ${WORKSPACE_ROOT}/<name>.`, [
|
|
345
|
+
["attach a folder", `intentic-machine sync attach --sandbox-url ${sandboxUrl} --dir <folder> --name <name>`],
|
|
328
346
|
["check it", "intentic-machine status"],
|
|
347
|
+
...later,
|
|
329
348
|
]);
|
|
330
349
|
return;
|
|
331
350
|
}
|
|
332
351
|
const syncing = pairing.mode === "sync";
|
|
333
|
-
ui.finished(syncing ? "Desktop sync is running." : "Enrolled for port mirroring.", syncing ? pairing.localDir : undefined, syncing ? syncedSentence(pairing) : `Ports from ${sandboxUrl} now answer on this
|
|
334
|
-
["check it", "intentic-machine status"],
|
|
335
|
-
["remove it", "intentic-machine sync uninstall"],
|
|
336
|
-
]);
|
|
352
|
+
ui.finished(syncing ? "Desktop sync is running." : "Enrolled for port mirroring.", syncing ? pairing.localDir : undefined, syncing ? syncedSentence(pairing) : `Ports from ${sandboxUrl} now answer on this device's localhost (mirror-only, no file sync).`, [["check it", "intentic-machine status"], ["remove it", unpairCommand(pairing.sandboxId)], ...later]);
|
|
337
353
|
};
|
|
338
354
|
const TUNNEL_READY_MS = 10_000;
|
|
339
355
|
const SESSION_READY_MS = 90_000;
|
|
@@ -350,15 +366,21 @@ const syncSessionAppears = async (mutagen, key, withinMs) => {
|
|
|
350
366
|
const notStartedYet = async (folder) => {
|
|
351
367
|
const running = await readResidentBuild();
|
|
352
368
|
return running !== undefined && running !== MACHINE_VERSION
|
|
353
|
-
?
|
|
354
|
-
|
|
369
|
+
? {
|
|
370
|
+
said: `The agent running here is ${running}, which starts file sync for ${folder} only at its next start; ${MACHINE_VERSION} starts it at once.`,
|
|
371
|
+
later: ["update it now", "intentic-machine upgrade"],
|
|
372
|
+
}
|
|
373
|
+
: {
|
|
374
|
+
said: `This device's agent has not started file sync for ${folder} yet; it keeps trying.`,
|
|
375
|
+
later: ["see when it runs", "intentic-machine status"],
|
|
376
|
+
};
|
|
355
377
|
};
|
|
356
378
|
const sandboxFlag = {
|
|
357
379
|
sandbox: {
|
|
358
380
|
kind: "parsed",
|
|
359
381
|
parse: String,
|
|
360
382
|
optional: true,
|
|
361
|
-
brief: "Act on one paired sandbox (its id, or any
|
|
383
|
+
brief: "Act on one paired sandbox (its URL, its id, or any part of the id matching exactly one). Default: all of them",
|
|
362
384
|
},
|
|
363
385
|
};
|
|
364
386
|
export const syncSwitchPlan = (syncing, held) => {
|
|
@@ -372,44 +394,48 @@ const pairingCommand = (brief, flags, none, act) => buildCommand({
|
|
|
372
394
|
docs: { brief },
|
|
373
395
|
parameters: { flags },
|
|
374
396
|
async func(given) {
|
|
375
|
-
const
|
|
397
|
+
const ui = machineUi(this.process);
|
|
376
398
|
const selected = selectPairings(await readState(), given.sandbox);
|
|
377
399
|
if (selected.length === 0) {
|
|
378
|
-
|
|
400
|
+
ui.quiet(none);
|
|
379
401
|
return;
|
|
380
402
|
}
|
|
381
|
-
await act(selected, given,
|
|
403
|
+
await act(selected, given, ui);
|
|
382
404
|
},
|
|
383
405
|
});
|
|
384
|
-
const NONE_TO_MIRROR = "
|
|
385
|
-
const noteIfStopped = async (
|
|
406
|
+
const NONE_TO_MIRROR = "No sandbox is paired on this device, so there is nothing to mirror. Desktop sync starts from a sandbox's Desktop sync card.";
|
|
407
|
+
const noteIfStopped = async (ui) => {
|
|
386
408
|
if ((await readResidentPid()) === undefined) {
|
|
387
|
-
|
|
409
|
+
ui.warn("This device's agent is not running, so nothing mirrors until it starts.");
|
|
410
|
+
ui.later([["start the agent", "intentic-machine run"]]);
|
|
388
411
|
}
|
|
389
412
|
};
|
|
390
|
-
const fileSyncSwitch = (brief, verb) => pairingCommand(brief, sandboxFlag, `
|
|
413
|
+
const fileSyncSwitch = (brief, verb) => pairingCommand(brief, sandboxFlag, `No sandbox is paired on this device, so there is nothing to ${verb}. Desktop sync starts from a sandbox's Desktop sync card.`, async (selected, _flags, ui) => {
|
|
414
|
+
const done = verb === "pause" ? "paused" : "resumed";
|
|
391
415
|
const syncing = selected.filter((pairing) => pairing.mode === "sync" && pairing.projectsHost !== true);
|
|
392
416
|
if (syncing.length === 0) {
|
|
393
|
-
|
|
417
|
+
ui.quiet(`${selected.length > 1 ? "These enrollments only mirror ports" : "This enrollment only mirrors ports"}, so there is no file sync to ${verb}.`);
|
|
394
418
|
return;
|
|
395
419
|
}
|
|
420
|
+
ui.act(`${verb === "pause" ? "Pausing" : "Resuming"} file sync`);
|
|
396
421
|
const mutagen = await ensureMutagen();
|
|
397
422
|
const held = existingSyncSessions(mutagen, syncing.flatMap((pairing) => syncSessionNames(pairing)));
|
|
398
423
|
if (held === undefined) {
|
|
399
|
-
throw new Error(`Mutagen did not answer when asked which sessions it holds,
|
|
424
|
+
throw new Error(explain(`File sync was not ${done}.`, "Mutagen did not answer when asked which sessions it holds.", "run the same command again in a moment."));
|
|
400
425
|
}
|
|
401
426
|
const plan = syncSwitchPlan(syncing, held);
|
|
402
427
|
if (plan.names.length === 0) {
|
|
403
|
-
|
|
428
|
+
ui.done(`No file-sync session is running for ${named(syncing)}, so nothing was ${done}. Syncing starts when the sandbox answers again.`);
|
|
404
429
|
return;
|
|
405
430
|
}
|
|
406
431
|
runMutagen(mutagen, ["sync", verb, ...plan.names]);
|
|
407
|
-
out(`${verb === "pause" ? "Paused" : "Resumed"} file sync for: ${named(plan.acted)}`);
|
|
408
432
|
if (plan.idle.length > 0) {
|
|
409
|
-
|
|
433
|
+
ui.note(`No file-sync session to ${verb} for ${named(plan.idle)}.`);
|
|
410
434
|
}
|
|
435
|
+
ui.done(`${verb === "pause" ? "Paused" : "Resumed"} file sync for ${named(plan.acted)}.`);
|
|
411
436
|
});
|
|
412
|
-
const mirrorSwitch = (brief, off) => pairingCommand(brief, sandboxFlag, NONE_TO_MIRROR, async (selected, _flags,
|
|
437
|
+
const mirrorSwitch = (brief, off) => pairingCommand(brief, sandboxFlag, NONE_TO_MIRROR, async (selected, _flags, ui) => {
|
|
438
|
+
ui.act(off ? "Turning port mirroring off" : "Turning port mirroring on");
|
|
413
439
|
const mutagen = await ensureMutagen();
|
|
414
440
|
for (const pairing of selected) {
|
|
415
441
|
await setMirrorOff(pairingKey(pairing), off);
|
|
@@ -418,22 +444,23 @@ const mirrorSwitch = (brief, off) => pairingCommand(brief, sandboxFlag, NONE_TO_
|
|
|
418
444
|
for (const sandboxId of new Set(selected.map((pairing) => pairing.sandboxId))) {
|
|
419
445
|
await retirePairingMirror(mutagen, sandboxId);
|
|
420
446
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
out(`Port mirroring OFF for: ${named(selected)}. Those ports are off this device's localhost. File syncing is untouched.`);
|
|
447
|
+
ui.note("Those ports are off this device's localhost. File syncing is untouched.");
|
|
448
|
+
ui.done(`Turned port mirroring off for ${named(selected)}.`);
|
|
424
449
|
return;
|
|
425
450
|
}
|
|
426
|
-
|
|
427
|
-
|
|
451
|
+
ui.note("Their ports return to localhost within a few seconds.");
|
|
452
|
+
ui.done(`Turned port mirroring on for ${named(selected)}.`);
|
|
453
|
+
await noteIfStopped(ui);
|
|
428
454
|
});
|
|
429
455
|
const parsePort = (value) => {
|
|
430
456
|
const port = Number(value);
|
|
431
457
|
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
432
|
-
throw new Error(
|
|
458
|
+
throw new Error(`${value} is not a port number (1-65535).`);
|
|
433
459
|
}
|
|
434
460
|
return port;
|
|
435
461
|
};
|
|
436
|
-
const mirrorPortSwitch = (brief, ignored) => pairingCommand(brief, { ...sandboxFlag, port: { kind: "parsed", parse: parsePort, brief: "The port number, as the sandbox serves it" } }, NONE_TO_MIRROR, async (selected, flags,
|
|
462
|
+
const mirrorPortSwitch = (brief, ignored) => pairingCommand(brief, { ...sandboxFlag, port: { kind: "parsed", parse: parsePort, brief: "The port number, as the sandbox serves it" } }, NONE_TO_MIRROR, async (selected, flags, ui) => {
|
|
463
|
+
ui.act(ignored ? `Leaving port ${flags.port} off this device` : `Mirroring port ${flags.port} again`);
|
|
437
464
|
const mutagen = await ensureMutagen();
|
|
438
465
|
let taken = 0;
|
|
439
466
|
for (const pairing of selected.filter((held) => !isAttachedPairing(held))) {
|
|
@@ -443,11 +470,13 @@ const mirrorPortSwitch = (brief, ignored) => pairingCommand(brief, { ...sandboxF
|
|
|
443
470
|
}
|
|
444
471
|
}
|
|
445
472
|
if (ignored) {
|
|
446
|
-
|
|
473
|
+
ui.note(`${taken === 0 ? "" : `Took localhost:${flags.port} off this device. `}Every other port is untouched.`);
|
|
474
|
+
ui.done(`Port ${flags.port} is no longer mirrored for ${named(selected)}.`);
|
|
447
475
|
return;
|
|
448
476
|
}
|
|
449
|
-
|
|
450
|
-
|
|
477
|
+
ui.note(`It returns to localhost within a few seconds, unless something else on this device is holding it.`);
|
|
478
|
+
ui.done(`Port ${flags.port} is mirrored again for ${named(selected)}.`);
|
|
479
|
+
await noteIfStopped(ui);
|
|
451
480
|
});
|
|
452
481
|
const mirror = buildRouteMap({
|
|
453
482
|
routes: {
|
|
@@ -462,35 +491,44 @@ const clean = buildCommand({
|
|
|
462
491
|
docs: { brief: "Clear build output this device left in directories the sandbox deleted, so those deletions can land" },
|
|
463
492
|
parameters: { flags: sandboxFlag },
|
|
464
493
|
async func(flags) {
|
|
465
|
-
const
|
|
494
|
+
const ui = machineUi(this.process);
|
|
466
495
|
const syncing = selectPairings(await readState(), flags.sandbox).filter((pairing) => pairing.mode === "sync");
|
|
467
496
|
if (syncing.length === 0) {
|
|
468
|
-
|
|
497
|
+
ui.quiet("No file-syncing sandbox is paired on this device, so there is no folder here to clear anything from.");
|
|
469
498
|
return;
|
|
470
499
|
}
|
|
500
|
+
ui.act("Clearing build output the sandbox's deletions are waiting on");
|
|
471
501
|
const mutagen = await ensureMutagen();
|
|
472
502
|
let removed = 0;
|
|
473
503
|
let standing = 0;
|
|
474
504
|
for (const pairing of syncing) {
|
|
475
|
-
const outcome = await healDerivedConflicts(mutagen, pairing,
|
|
505
|
+
const outcome = await healDerivedConflicts(mutagen, pairing, ui.note, true);
|
|
476
506
|
removed += outcome.removed.length;
|
|
477
507
|
standing += outcome.standing;
|
|
478
508
|
}
|
|
479
|
-
out(removed === 0
|
|
480
|
-
? "Nothing to clear: no directory the sandbox deleted is being held open by build output on this device."
|
|
481
|
-
: `Cleared ${removed} director${removed === 1 ? "y" : "ies"}. The deletions they were holding back land within a few seconds.`);
|
|
482
509
|
if (standing > 0) {
|
|
483
|
-
|
|
510
|
+
ui.warn(`${plural(standing, "conflict")} still standing, and not this command's to settle: two copies somebody wrote is a choice only a person makes.`);
|
|
511
|
+
}
|
|
512
|
+
ui.done(removed === 0
|
|
513
|
+
? "Nothing to clear: no directory the sandbox deleted is held open by build output on this device."
|
|
514
|
+
: `Cleared ${plural(removed, "directory", "directories")}. The deletions they held back land within a few seconds.`);
|
|
515
|
+
if (standing > 0) {
|
|
516
|
+
ui.later([["see the paths", "intentic-machine status"]]);
|
|
484
517
|
}
|
|
485
518
|
},
|
|
486
519
|
});
|
|
487
|
-
const autoHealSwitch = (brief, off) => pairingCommand(brief, sandboxFlag, "
|
|
520
|
+
const autoHealSwitch = (brief, off) => pairingCommand(brief, sandboxFlag, "No sandbox is paired on this device, so there is nothing to switch.", async (selected, _flags, ui) => {
|
|
521
|
+
ui.act(off ? "Turning off clearing build output" : "Turning on clearing build output");
|
|
488
522
|
for (const pairing of selected) {
|
|
489
523
|
await setAutoHealOff(pairingKey(pairing), off);
|
|
490
524
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
525
|
+
if (!off) {
|
|
526
|
+
ui.done(`This device clears build output that holds back a deletion for ${named(selected)}.`);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
ui.note("A deletion the sandbox makes now stops syncing whenever this device has build output inside it.");
|
|
530
|
+
ui.done(`This device no longer clears build output by itself for ${named(selected)}.`);
|
|
531
|
+
ui.later([["clear it by hand", "intentic-machine sync clean"]]);
|
|
494
532
|
});
|
|
495
533
|
const autoheal = buildRouteMap({
|
|
496
534
|
routes: {
|
|
@@ -499,7 +537,7 @@ const autoheal = buildRouteMap({
|
|
|
499
537
|
},
|
|
500
538
|
docs: { brief: "Whether this device clears its own build output when it blocks a deletion, for one sandbox or all" },
|
|
501
539
|
});
|
|
502
|
-
export const syncUninstall = async (
|
|
540
|
+
export const syncUninstall = async (say, sandbox, { forGood = false } = {}) => {
|
|
503
541
|
const state = await readState();
|
|
504
542
|
const dropped = selectPairings(state, sandbox);
|
|
505
543
|
const mutagen = await ensureMutagen();
|
|
@@ -509,48 +547,46 @@ export const syncUninstall = async (out, sandbox, { forGood = false } = {}) => {
|
|
|
509
547
|
const firstOfSandbox = !revoked.has(pairing.sandboxId);
|
|
510
548
|
revoked.add(pairing.sandboxId);
|
|
511
549
|
if (pairing.syncToken !== undefined && firstOfSandbox) {
|
|
512
|
-
await revokeEnrollment(pairing.sandboxUrl, pairing.syncToken).catch((error) =>
|
|
550
|
+
await revokeEnrollment(pairing.sandboxUrl, pairing.syncToken).catch((error) => say.warn(`${pairing.sandboxUrl} could not be told to forget this device (${errorMessage(error)}), so its sync key is still authorized there. Remove this device from that sandbox's Devices view.`));
|
|
513
551
|
}
|
|
514
552
|
if (pairing.mode === "sync" && syncSessionNames(pairing).length > 0) {
|
|
515
|
-
|
|
516
|
-
stdio: "ignore",
|
|
517
|
-
windowsHide: true,
|
|
518
|
-
timeout: MUTAGEN_CALL_TIMEOUT_MS,
|
|
519
|
-
});
|
|
553
|
+
await runProcess(mutagen, ["sync", "terminate", ...syncSessionNames(pairing)], { timeoutMs: MUTAGEN_CALL_TIMEOUT_MS });
|
|
520
554
|
}
|
|
521
555
|
if (firstOfSandbox) {
|
|
522
556
|
await retirePairingMirror(mutagen, pairing.sandboxId);
|
|
523
557
|
}
|
|
524
558
|
await removePairing(pairingKey(pairing));
|
|
525
|
-
|
|
559
|
+
say.note(`Unpaired ${pairingKey(pairing)}${pairing.localDir === undefined ? "" : `; ${pairing.localDir} is no longer synced`}.`);
|
|
526
560
|
}
|
|
527
561
|
if (remaining.length > 0) {
|
|
528
562
|
await writeManagedSshConfig(pairingSshConfig(remaining));
|
|
529
|
-
await ensureResident(
|
|
530
|
-
|
|
531
|
-
return;
|
|
563
|
+
await ensureResident(say.note, { forGood });
|
|
564
|
+
return `Unpaired ${plural(dropped.length, "pairing")}. Still syncing ${plural(remaining.length, "pairing")}: ${named(remaining)}.`;
|
|
532
565
|
}
|
|
533
|
-
await ensureResident(
|
|
534
|
-
await teardownAllForwards(mutagen,
|
|
566
|
+
await ensureResident(say.note, { forGood });
|
|
567
|
+
await teardownAllForwards(mutagen, say.note);
|
|
535
568
|
await removeManagedSshConfig();
|
|
536
569
|
const ownCopy = isOwnMutagen(mutagen) && userMutagenOnPath() === undefined;
|
|
537
570
|
if (ownCopy) {
|
|
538
571
|
unregisterMutagenAutostart(mutagen);
|
|
539
|
-
|
|
572
|
+
await runProcess(mutagen, ["daemon", "stop"], { timeoutMs: MUTAGEN_CALL_TIMEOUT_MS });
|
|
573
|
+
say.note("Removed the ssh-config include, and stopped and unregistered the Mutagen daemon.");
|
|
540
574
|
}
|
|
541
|
-
else
|
|
542
|
-
|
|
575
|
+
else {
|
|
576
|
+
if (process.platform === "win32") {
|
|
577
|
+
clearWindowsRunValue(MUTAGEN_RUN_VALUE);
|
|
578
|
+
}
|
|
579
|
+
say.note("Removed the ssh-config include. Your own Mutagen install and its daemon are untouched.");
|
|
543
580
|
}
|
|
544
|
-
|
|
545
|
-
? "Sync terminated; ssh-config include removed; Mutagen daemon stopped and unregistered."
|
|
546
|
-
: "Sync terminated; ssh-config include removed. (Your own Mutagen install is untouched, `mutagen daemon unregister` if you no longer want its daemon at login.)");
|
|
581
|
+
return dropped.length === 0 ? "Nothing was syncing. Removed any leftovers." : "Stopped syncing on this device.";
|
|
547
582
|
};
|
|
548
583
|
const uninstall = buildCommand({
|
|
549
|
-
docs: { brief: "Unpair a sandbox (--sandbox), or stop syncing every sandbox on this
|
|
584
|
+
docs: { brief: "Unpair a sandbox (--sandbox), or stop syncing every sandbox on this device" },
|
|
550
585
|
parameters: { flags: sandboxFlag },
|
|
551
586
|
async func(flags) {
|
|
552
|
-
const
|
|
553
|
-
|
|
587
|
+
const ui = machineUi(this.process);
|
|
588
|
+
ui.act(flags.sandbox === undefined ? "Stopping sync on this device" : `Unpairing ${flags.sandbox}`);
|
|
589
|
+
ui.done(await syncUninstall(ui, flags.sandbox));
|
|
554
590
|
},
|
|
555
591
|
});
|
|
556
592
|
const pause = fileSyncSwitch("Pause file syncing", "pause");
|