gnosys 5.11.4 → 5.12.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/dist/cli.js +324 -5150
- package/dist/index.js +364 -235
- package/dist/lib/addCommand.d.ts +9 -0
- package/dist/lib/addCommand.js +103 -0
- package/dist/lib/addStructuredCommand.d.ts +16 -0
- package/dist/lib/addStructuredCommand.js +103 -0
- package/dist/lib/ambiguityCommand.d.ts +4 -0
- package/dist/lib/ambiguityCommand.js +36 -0
- package/dist/lib/apiKeyVault.d.ts +78 -0
- package/dist/lib/apiKeyVault.js +447 -0
- package/dist/lib/askCommand.d.ts +13 -0
- package/dist/lib/askCommand.js +145 -0
- package/dist/lib/audioExtract.js +4 -1
- package/dist/lib/auditCommand.d.ts +7 -0
- package/dist/lib/auditCommand.js +27 -0
- package/dist/lib/backupCommand.d.ts +6 -0
- package/dist/lib/backupCommand.js +54 -0
- package/dist/lib/bootstrapCommand.d.ts +15 -0
- package/dist/lib/bootstrapCommand.js +51 -0
- package/dist/lib/briefingCommand.d.ts +7 -0
- package/dist/lib/briefingCommand.js +92 -0
- package/dist/lib/centralizeCommand.d.ts +5 -0
- package/dist/lib/centralizeCommand.js +16 -0
- package/dist/lib/chatCommand.d.ts +12 -0
- package/dist/lib/chatCommand.js +46 -0
- package/dist/lib/checkCommand.d.ts +4 -0
- package/dist/lib/checkCommand.js +133 -0
- package/dist/lib/clientReadOverlay.d.ts +27 -0
- package/dist/lib/clientReadOverlay.js +73 -0
- package/dist/lib/clientReadResolve.d.ts +32 -0
- package/dist/lib/clientReadResolve.js +84 -0
- package/dist/lib/commitContextCommand.d.ts +9 -0
- package/dist/lib/commitContextCommand.js +142 -0
- package/dist/lib/config.d.ts +43 -3
- package/dist/lib/config.js +58 -57
- package/dist/lib/configCommand.d.ts +10 -0
- package/dist/lib/configCommand.js +321 -0
- package/dist/lib/connectCommand.d.ts +8 -0
- package/dist/lib/connectCommand.js +19 -0
- package/dist/lib/db.d.ts +52 -0
- package/dist/lib/db.js +169 -1
- package/dist/lib/dearchiveCommand.d.ts +7 -0
- package/dist/lib/dearchiveCommand.js +41 -0
- package/dist/lib/discoverCommand.d.ts +9 -0
- package/dist/lib/discoverCommand.js +87 -0
- package/dist/lib/doctorCommand.d.ts +6 -0
- package/dist/lib/doctorCommand.js +256 -0
- package/dist/lib/dream.d.ts +42 -2
- package/dist/lib/dream.js +290 -30
- package/dist/lib/dreamCommand.d.ts +10 -0
- package/dist/lib/dreamCommand.js +195 -0
- package/dist/lib/dreamLaunchd.d.ts +2 -0
- package/dist/lib/dreamLaunchd.js +72 -0
- package/dist/lib/dreamLogCommand.d.ts +10 -0
- package/dist/lib/dreamLogCommand.js +58 -0
- package/dist/lib/dreamReport.d.ts +7 -0
- package/dist/lib/dreamReport.js +114 -0
- package/dist/lib/dreamRunLog.d.ts +121 -0
- package/dist/lib/dreamRunLog.js +212 -0
- package/dist/lib/embeddings.js +3 -0
- package/dist/lib/exportCommand.d.ts +18 -0
- package/dist/lib/exportCommand.js +101 -0
- package/dist/lib/fsearchCommand.d.ts +8 -0
- package/dist/lib/fsearchCommand.js +44 -0
- package/dist/lib/graphCommand.d.ts +4 -0
- package/dist/lib/graphCommand.js +68 -0
- package/dist/lib/helperGenerateCommand.d.ts +5 -0
- package/dist/lib/helperGenerateCommand.js +27 -0
- package/dist/lib/historyCommand.d.ts +5 -0
- package/dist/lib/historyCommand.js +51 -0
- package/dist/lib/hybridSearchCommand.d.ts +12 -0
- package/dist/lib/hybridSearchCommand.js +95 -0
- package/dist/lib/importCommand.d.ts +16 -0
- package/dist/lib/importCommand.js +89 -0
- package/dist/lib/importProjectCommand.d.ts +6 -0
- package/dist/lib/importProjectCommand.js +43 -0
- package/dist/lib/ingestCommand.d.ts +13 -0
- package/dist/lib/ingestCommand.js +95 -0
- package/dist/lib/installOutput.d.ts +36 -0
- package/dist/lib/installOutput.js +55 -0
- package/dist/lib/lensCommand.d.ts +20 -0
- package/dist/lib/lensCommand.js +61 -0
- package/dist/lib/lensing.d.ts +1 -0
- package/dist/lib/lensing.js +50 -9
- package/dist/lib/linksCommand.d.ts +7 -0
- package/dist/lib/linksCommand.js +48 -0
- package/dist/lib/listCommand.d.ts +8 -0
- package/dist/lib/listCommand.js +74 -0
- package/dist/lib/llm.d.ts +1 -1
- package/dist/lib/llm.js +26 -8
- package/dist/lib/localDiskCheck.d.ts +17 -0
- package/dist/lib/localDiskCheck.js +54 -0
- package/dist/lib/machineConfig.d.ts +11 -1
- package/dist/lib/machineConfig.js +16 -0
- package/dist/lib/machineRegistry.d.ts +61 -0
- package/dist/lib/machineRegistry.js +80 -0
- package/dist/lib/maintainCommand.d.ts +8 -0
- package/dist/lib/maintainCommand.js +34 -0
- package/dist/lib/masterLease.d.ts +20 -0
- package/dist/lib/masterLease.js +68 -0
- package/dist/lib/migrateCommand.d.ts +7 -0
- package/dist/lib/migrateCommand.js +158 -0
- package/dist/lib/migrateDbCommand.d.ts +9 -0
- package/dist/lib/migrateDbCommand.js +94 -0
- package/dist/lib/modelValidation.d.ts +5 -0
- package/dist/lib/modelValidation.js +27 -0
- package/dist/lib/openrouterTiers.d.ts +29 -0
- package/dist/lib/openrouterTiers.js +113 -0
- package/dist/lib/prefCommand.d.ts +10 -0
- package/dist/lib/prefCommand.js +118 -0
- package/dist/lib/projectsCommand.d.ts +8 -0
- package/dist/lib/projectsCommand.js +131 -0
- package/dist/lib/readCommand.d.ts +7 -0
- package/dist/lib/readCommand.js +62 -0
- package/dist/lib/recall.d.ts +3 -0
- package/dist/lib/recall.js +19 -4
- package/dist/lib/recallCommand.d.ts +11 -0
- package/dist/lib/recallCommand.js +112 -0
- package/dist/lib/reflectCommand.d.ts +8 -0
- package/dist/lib/reflectCommand.js +61 -0
- package/dist/lib/reindexCommand.d.ts +4 -0
- package/dist/lib/reindexCommand.js +34 -0
- package/dist/lib/reindexGraphCommand.d.ts +4 -0
- package/dist/lib/reindexGraphCommand.js +12 -0
- package/dist/lib/reinforceCommand.d.ts +8 -0
- package/dist/lib/reinforceCommand.js +40 -0
- package/dist/lib/remote.d.ts +5 -1
- package/dist/lib/remote.js +5 -1
- package/dist/lib/remoteWizard.d.ts +24 -5
- package/dist/lib/remoteWizard.js +308 -319
- package/dist/lib/restoreCommand.d.ts +5 -0
- package/dist/lib/restoreCommand.js +35 -0
- package/dist/lib/sandboxStartCommand.d.ts +6 -0
- package/dist/lib/sandboxStartCommand.js +25 -0
- package/dist/lib/sandboxStatusCommand.d.ts +4 -0
- package/dist/lib/sandboxStatusCommand.js +24 -0
- package/dist/lib/sandboxStopCommand.d.ts +4 -0
- package/dist/lib/sandboxStopCommand.js +21 -0
- package/dist/lib/searchCommand.d.ts +9 -0
- package/dist/lib/searchCommand.js +90 -0
- package/dist/lib/semanticSearchCommand.d.ts +8 -0
- package/dist/lib/semanticSearchCommand.js +52 -0
- package/dist/lib/setup/configSetRender.js +2 -0
- package/dist/lib/setup/providerGlyphs.d.ts +19 -0
- package/dist/lib/setup/providerGlyphs.js +42 -0
- package/dist/lib/setup/remoteRender.d.ts +31 -1
- package/dist/lib/setup/remoteRender.js +95 -4
- package/dist/lib/setup/sections/providers.d.ts +17 -0
- package/dist/lib/setup/sections/providers.js +255 -0
- package/dist/lib/setup/sections/routing.d.ts +2 -6
- package/dist/lib/setup/sections/routing.js +33 -85
- package/dist/lib/setup/sections/taskRoutingEditor.d.ts +17 -0
- package/dist/lib/setup/sections/taskRoutingEditor.js +149 -0
- package/dist/lib/setup/summary.d.ts +9 -0
- package/dist/lib/setup/summary.js +51 -37
- package/dist/lib/setup/ui/status.d.ts +1 -0
- package/dist/lib/setup/ui/status.js +2 -0
- package/dist/lib/setup.d.ts +108 -3
- package/dist/lib/setup.js +762 -157
- package/dist/lib/setupKeys.d.ts +42 -0
- package/dist/lib/setupKeys.js +564 -0
- package/dist/lib/setupRemoteCommand.d.ts +4 -0
- package/dist/lib/setupRemoteCommand.js +28 -0
- package/dist/lib/setupRemotePullCommand.d.ts +5 -0
- package/dist/lib/setupRemotePullCommand.js +52 -0
- package/dist/lib/setupRemotePushCommand.d.ts +5 -0
- package/dist/lib/setupRemotePushCommand.js +57 -0
- package/dist/lib/setupRemoteResolveCommand.d.ts +4 -0
- package/dist/lib/setupRemoteResolveCommand.js +48 -0
- package/dist/lib/setupRemoteStatusCommand.d.ts +4 -0
- package/dist/lib/setupRemoteStatusCommand.js +73 -0
- package/dist/lib/setupRemoteSyncCommand.d.ts +6 -0
- package/dist/lib/setupRemoteSyncCommand.js +65 -0
- package/dist/lib/setupSyncProjectsCommand.d.ts +4 -0
- package/dist/lib/setupSyncProjectsCommand.js +292 -0
- package/dist/lib/staleCommand.d.ts +8 -0
- package/dist/lib/staleCommand.js +34 -0
- package/dist/lib/statsCommand.d.ts +6 -0
- package/dist/lib/statsCommand.js +142 -0
- package/dist/lib/statusCommand.d.ts +18 -0
- package/dist/lib/statusCommand.js +250 -0
- package/dist/lib/storesCommand.d.ts +2 -0
- package/dist/lib/storesCommand.js +4 -0
- package/dist/lib/syncClient.d.ts +47 -0
- package/dist/lib/syncClient.js +212 -0
- package/dist/lib/syncCommand.d.ts +6 -0
- package/dist/lib/syncCommand.js +57 -0
- package/dist/lib/syncDoctorCommand.d.ts +5 -0
- package/dist/lib/syncDoctorCommand.js +100 -0
- package/dist/lib/syncIngest.d.ts +19 -0
- package/dist/lib/syncIngest.js +152 -0
- package/dist/lib/syncIngestLaunchd.d.ts +8 -0
- package/dist/lib/syncIngestLaunchd.js +93 -0
- package/dist/lib/syncIngestStartup.d.ts +5 -0
- package/dist/lib/syncIngestStartup.js +29 -0
- package/dist/lib/syncIngestSystemd.d.ts +10 -0
- package/dist/lib/syncIngestSystemd.js +97 -0
- package/dist/lib/syncIngestTimer.d.ts +8 -0
- package/dist/lib/syncIngestTimer.js +27 -0
- package/dist/lib/syncIngestTimerCommand.d.ts +7 -0
- package/dist/lib/syncIngestTimerCommand.js +83 -0
- package/dist/lib/syncLock.d.ts +6 -0
- package/dist/lib/syncLock.js +74 -0
- package/dist/lib/syncSnapshot.d.ts +30 -0
- package/dist/lib/syncSnapshot.js +184 -0
- package/dist/lib/syncStaging.d.ts +81 -0
- package/dist/lib/syncStaging.js +239 -0
- package/dist/lib/tagsAddCommand.d.ts +8 -0
- package/dist/lib/tagsAddCommand.js +18 -0
- package/dist/lib/tagsCommand.d.ts +4 -0
- package/dist/lib/tagsCommand.js +16 -0
- package/dist/lib/timelineCommand.d.ts +7 -0
- package/dist/lib/timelineCommand.js +49 -0
- package/dist/lib/traceCommand.d.ts +6 -0
- package/dist/lib/traceCommand.js +39 -0
- package/dist/lib/traverseCommand.d.ts +6 -0
- package/dist/lib/traverseCommand.js +58 -0
- package/dist/lib/updateCommand.d.ts +13 -0
- package/dist/lib/updateCommand.js +67 -0
- package/dist/lib/updateStatusCommand.d.ts +5 -0
- package/dist/lib/updateStatusCommand.js +38 -0
- package/dist/lib/webAddCommand.d.ts +8 -0
- package/dist/lib/webAddCommand.js +55 -0
- package/dist/lib/webBuildCommand.d.ts +10 -0
- package/dist/lib/webBuildCommand.js +65 -0
- package/dist/lib/webBuildIndexCommand.d.ts +8 -0
- package/dist/lib/webBuildIndexCommand.js +37 -0
- package/dist/lib/webIngestCommand.d.ts +11 -0
- package/dist/lib/webIngestCommand.js +51 -0
- package/dist/lib/webInitCommand.d.ts +9 -0
- package/dist/lib/webInitCommand.js +167 -0
- package/dist/lib/webRemoveCommand.d.ts +5 -0
- package/dist/lib/webRemoveCommand.js +41 -0
- package/dist/lib/webStatusCommand.d.ts +5 -0
- package/dist/lib/webStatusCommand.js +94 -0
- package/dist/lib/webUpdateCommand.d.ts +7 -0
- package/dist/lib/webUpdateCommand.js +72 -0
- package/dist/lib/workingSetCommand.d.ts +6 -0
- package/dist/lib/workingSetCommand.js +37 -0
- package/package.json +2 -1
package/dist/lib/remoteWizard.js
CHANGED
|
@@ -1,53 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Interactive wizard for
|
|
2
|
+
* Interactive wizard for multi-machine sync (v13 design).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 3. Join existing — second machine joining a remote that already has data
|
|
4
|
+
* Flows:
|
|
5
|
+
* - Fresh setup: explanation → master vs client → role-specific prompts
|
|
6
|
+
* - Reconfigure: change path, re-validate, or disconnect (when already configured)
|
|
8
7
|
*/
|
|
9
|
-
import {
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
import { existsSync, mkdirSync, renameSync } from "fs";
|
|
10
10
|
import * as path from "path";
|
|
11
11
|
import { createInterface } from "readline/promises";
|
|
12
12
|
import { GnosysDB } from "./db.js";
|
|
13
13
|
import { RemoteSync, validateLocation, getConfiguredRemotePath, clearRemoteSyncConfig, } from "./remote.js";
|
|
14
|
+
import { ensureMachineConfig, writeMachineConfig, readMachineConfig, } from "./machineConfig.js";
|
|
15
|
+
import { getGnosysHome } from "./paths.js";
|
|
16
|
+
import { atomicWriteFileSync } from "./atomicWrite.js";
|
|
17
|
+
import { readMasterMarker, writeMasterMarker } from "./masterLease.js";
|
|
18
|
+
import { checkMasterPathLocalDisk, LOCAL_DISK_ACK_PHRASE, } from "./localDiskCheck.js";
|
|
19
|
+
import { stagingDirForMachine, clientPresencePath, machineStagingDir } from "./syncStaging.js";
|
|
14
20
|
import { safeQuestion } from "./setup/ui/safePrompt.js";
|
|
15
21
|
import { Spinner } from "./setup/ui/spinner.js";
|
|
16
22
|
import { printStatus } from "./setup/ui/status.js";
|
|
17
23
|
import { Footer } from "./setup/ui/footer.js";
|
|
18
|
-
import { renderRemoteIntro, renderValidationSummary, renderRemoteDiff,
|
|
24
|
+
import { renderRemoteIntro, renderValidationSummary, renderRemoteDiff, renderV13ExplanationScreen, renderMasterBackupWarning, renderBackupDeclineAckPrompt, BACKUP_RISK_PHRASE, TAILSCALE_GUIDE_URL, } from "./setup/remoteRender.js";
|
|
19
25
|
const REMOTE_PATH_KEY = "remote_path";
|
|
20
26
|
const REMOTE_MODE_KEY = "remote_mode";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
return statSync(p).isDirectory();
|
|
27
|
+
const PRE_MASTER_BACKUP_NAME = ".pre-master-backup";
|
|
28
|
+
export { BACKUP_RISK_PHRASE, TAILSCALE_GUIDE_URL };
|
|
29
|
+
const TAILSCALE_INLINE_FALLBACK = `Tailscale lets other machines reach this one as if it were on the same LAN.
|
|
30
|
+
Install Tailscale on each machine, sign in with the same account, then use the master folder path
|
|
31
|
+
that Tailscale exposes (often under /Volumes/ or a synced folder path).`;
|
|
32
|
+
export async function showTailscaleClientGuide(rl) {
|
|
33
|
+
console.log("");
|
|
34
|
+
console.log("You will need a way for this machine to reach the master folder (usually Tailscale).");
|
|
35
|
+
console.log("A basic explanation is shown below. For a full walkthrough, visit:");
|
|
36
|
+
console.log(` ${TAILSCALE_GUIDE_URL}`);
|
|
37
|
+
console.log("(If that page is unavailable, this inline text is the primary source.)");
|
|
38
|
+
console.log("");
|
|
39
|
+
const open = await askConfirm(rl, "Open the guide in your browser now?", false);
|
|
40
|
+
if (open) {
|
|
41
|
+
try {
|
|
42
|
+
const { execSync } = await import("child_process");
|
|
43
|
+
if (process.platform === "darwin") {
|
|
44
|
+
execSync(`open "${TAILSCALE_GUIDE_URL}"`, { stdio: "ignore" });
|
|
41
45
|
}
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
else if (process.platform === "win32") {
|
|
47
|
+
execSync(`start "" "${TAILSCALE_GUIDE_URL}"`, { stdio: "ignore", shell: "cmd.exe" });
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
execSync(`xdg-open "${TAILSCALE_GUIDE_URL}"`, { stdio: "ignore" });
|
|
44
51
|
}
|
|
45
|
-
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
console.log(TAILSCALE_INLINE_FALLBACK);
|
|
55
|
+
}
|
|
46
56
|
}
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
else {
|
|
58
|
+
console.log(TAILSCALE_INLINE_FALLBACK);
|
|
49
59
|
}
|
|
60
|
+
console.log("");
|
|
61
|
+
}
|
|
62
|
+
/** Returns true when the user typed the exact expected phrase (trimmed). */
|
|
63
|
+
export function matchesTypedPhrase(input, expected) {
|
|
64
|
+
return input.trim() === expected;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Cloned-install detection: a presence file already exists for this machineId
|
|
68
|
+
* (typical after VM clone / backup restore).
|
|
69
|
+
*/
|
|
70
|
+
export function detectClonedStagingPresence(masterPath, machineId) {
|
|
71
|
+
return existsSync(clientPresencePath(masterPath, machineId));
|
|
50
72
|
}
|
|
73
|
+
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
51
74
|
async function ask(rl, prompt) {
|
|
52
75
|
return (await safeQuestion(rl, prompt)).trim();
|
|
53
76
|
}
|
|
@@ -76,9 +99,11 @@ async function askConfirm(rl, prompt, defaultYes = true) {
|
|
|
76
99
|
return defaultYes;
|
|
77
100
|
return answer === "y" || answer === "yes";
|
|
78
101
|
}
|
|
102
|
+
async function askTypedPhrase(rl, prompt, expected) {
|
|
103
|
+
const typed = await ask(rl, prompt);
|
|
104
|
+
return matchesTypedPhrase(typed, expected);
|
|
105
|
+
}
|
|
79
106
|
function showValidationSummary(validation) {
|
|
80
|
-
// v5.9.3 Screen 6 — route through the renderer so each check renders as
|
|
81
|
-
// a `✓` / `✗` status line. Identical content, atom-styled output.
|
|
82
107
|
console.log(renderValidationSummary({
|
|
83
108
|
pathExists: validation.checks.pathExists,
|
|
84
109
|
writable: validation.checks.writable,
|
|
@@ -93,63 +118,52 @@ function showValidationSummary(validation) {
|
|
|
93
118
|
errors: validation.errors,
|
|
94
119
|
}));
|
|
95
120
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
console.log("");
|
|
110
|
-
console.log(Footer("1–2 · pick enter · use recommended"));
|
|
111
|
-
const answer = (await safeQuestion(rl, " > ")).trim();
|
|
112
|
-
if (!answer || answer === "1")
|
|
113
|
-
return "read-write";
|
|
114
|
-
if (answer !== "2") {
|
|
115
|
-
printStatus("warn", "invalid choice — using `read & write`");
|
|
116
|
-
return "read-write";
|
|
117
|
-
}
|
|
118
|
-
// Nested submenu — all three modes + back.
|
|
119
|
-
console.log("");
|
|
120
|
-
console.log(` 1 read & write ${SYNC_MODE_LABELS["read-write"]} ◂ recommended`);
|
|
121
|
-
console.log(` 2 pull-only ${SYNC_MODE_LABELS["pull-only"]}`);
|
|
122
|
-
console.log(` 3 push-only ${SYNC_MODE_LABELS["push-only"]}`);
|
|
123
|
-
console.log(` 4 back`);
|
|
124
|
-
console.log("");
|
|
125
|
-
console.log(Footer("1–4 · pick"));
|
|
126
|
-
const sub = (await safeQuestion(rl, " > ")).trim();
|
|
127
|
-
switch (sub) {
|
|
128
|
-
case "1": return "read-write";
|
|
129
|
-
case "2": return "pull-only";
|
|
130
|
-
case "3": return "push-only";
|
|
131
|
-
case "4": return null;
|
|
132
|
-
default:
|
|
133
|
-
printStatus("warn", "invalid choice — using `read & write`");
|
|
134
|
-
return "read-write";
|
|
121
|
+
function persistMultiMachineConfig(localDb, masterPath, role) {
|
|
122
|
+
localDb.setMeta(REMOTE_PATH_KEY, masterPath);
|
|
123
|
+
localDb.setMeta(REMOTE_MODE_KEY, role);
|
|
124
|
+
const mc = ensureMachineConfig().config;
|
|
125
|
+
mc.remote = { enabled: true, path: masterPath, role };
|
|
126
|
+
writeMachineConfig(mc);
|
|
127
|
+
}
|
|
128
|
+
function archiveLocalDbBeforeMaster() {
|
|
129
|
+
const home = getGnosysHome();
|
|
130
|
+
const dbPath = path.join(home, "gnosys.db");
|
|
131
|
+
const backupPath = path.join(home, PRE_MASTER_BACKUP_NAME);
|
|
132
|
+
if (existsSync(dbPath) && !existsSync(backupPath)) {
|
|
133
|
+
renameSync(dbPath, backupPath);
|
|
135
134
|
}
|
|
136
135
|
}
|
|
136
|
+
function writeClientPresenceFile(masterPath, machineId) {
|
|
137
|
+
const dir = machineStagingDir(masterPath, machineId);
|
|
138
|
+
mkdirSync(dir, { recursive: true });
|
|
139
|
+
const presencePath = clientPresencePath(masterPath, machineId);
|
|
140
|
+
if (!existsSync(presencePath)) {
|
|
141
|
+
atomicWriteFileSync(presencePath, JSON.stringify({ machineId, firstSeenAt: new Date().toISOString() }, null, 2) + "\n");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function pickFolderPath(rl, prompt, defaultPath) {
|
|
145
|
+
const hint = defaultPath ? ` [${defaultPath}] ` : " ";
|
|
146
|
+
const raw = await ask(rl, prompt + hint);
|
|
147
|
+
const chosen = raw || defaultPath || "";
|
|
148
|
+
if (!chosen)
|
|
149
|
+
return null;
|
|
150
|
+
return path.resolve(chosen);
|
|
151
|
+
}
|
|
137
152
|
// ─── Main wizard ────────────────────────────────────────────────────────
|
|
138
153
|
export async function runConfigureWizard(centralDb, externalRl) {
|
|
139
154
|
const ownsRl = !externalRl;
|
|
140
155
|
const rl = externalRl ?? createInterface({ input: process.stdin, output: process.stdout });
|
|
141
156
|
try {
|
|
142
157
|
const localCount = centralDb.getMemoryCount();
|
|
143
|
-
const currentRemote = centralDb
|
|
144
|
-
console.log("");
|
|
145
|
-
console.log(renderRemoteIntro(localCount.active, localCount.archived, currentRemote || null));
|
|
146
|
-
console.log("");
|
|
158
|
+
const currentRemote = getConfiguredRemotePath(centralDb);
|
|
147
159
|
if (currentRemote) {
|
|
148
|
-
|
|
160
|
+
console.log("");
|
|
161
|
+
console.log(renderRemoteIntro(localCount.active, localCount.archived, currentRemote));
|
|
162
|
+
console.log("");
|
|
149
163
|
const choice = await askChoice(rl, "What would you like to do?", [
|
|
150
|
-
{ key: "1", label: "Change
|
|
151
|
-
{ key: "2", label: "Re-validate current
|
|
152
|
-
{ key: "3", label: "Disconnect
|
|
164
|
+
{ key: "1", label: "Change master folder path" },
|
|
165
|
+
{ key: "2", label: "Re-validate current master folder" },
|
|
166
|
+
{ key: "3", label: "Disconnect multi-machine sync (single-machine only)" },
|
|
153
167
|
{ key: "4", label: "Cancel" },
|
|
154
168
|
], "4");
|
|
155
169
|
if (choice === "4")
|
|
@@ -158,151 +172,118 @@ export async function runConfigureWizard(centralDb, externalRl) {
|
|
|
158
172
|
return await disconnectRemote(rl, centralDb);
|
|
159
173
|
if (choice === "2")
|
|
160
174
|
return await revalidateRemote(rl, centralDb, currentRemote);
|
|
161
|
-
// choice
|
|
175
|
+
// choice 1: fall through to fresh v13 flow with explanation skipped? Use change-path only
|
|
176
|
+
return await changeMasterPathFlow(rl, centralDb, currentRemote);
|
|
162
177
|
}
|
|
163
|
-
|
|
164
|
-
return await setupRemoteFlow(rl, centralDb, localCount.active);
|
|
178
|
+
return await runFreshV13SetupFlow(rl, centralDb, localCount.active);
|
|
165
179
|
}
|
|
166
180
|
finally {
|
|
167
|
-
if (ownsRl)
|
|
181
|
+
if (ownsRl)
|
|
168
182
|
rl.close();
|
|
169
|
-
}
|
|
170
183
|
}
|
|
171
184
|
}
|
|
172
|
-
|
|
173
|
-
|
|
185
|
+
async function runFreshV13SetupFlow(rl, centralDb, localActiveCount) {
|
|
186
|
+
console.log(renderV13ExplanationScreen());
|
|
174
187
|
console.log("");
|
|
175
|
-
|
|
188
|
+
const proceed = await askConfirm(rl, "Would you like to set this up?", true);
|
|
189
|
+
if (!proceed) {
|
|
190
|
+
printStatus("progress", "staying single-machine", "multi-machine sync not configured");
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
176
193
|
console.log("");
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
194
|
+
const roleChoice = await askChoice(rl, "Is this machine going to be the master, or a client that joins an existing master?", [
|
|
195
|
+
{ key: "1", label: "This machine is the master (it will hold the main folder)" },
|
|
196
|
+
{ key: "2", label: "This machine is a client (it will connect to a master on another machine)" },
|
|
197
|
+
{ key: "3", label: "Cancel" },
|
|
198
|
+
], "3");
|
|
199
|
+
if (roleChoice === "3")
|
|
200
|
+
return false;
|
|
201
|
+
if (roleChoice === "1") {
|
|
202
|
+
return await runMasterSetupFlow(rl, centralDb, localActiveCount);
|
|
203
|
+
}
|
|
204
|
+
return await runClientSetupFlow(rl, centralDb, localActiveCount);
|
|
205
|
+
}
|
|
206
|
+
async function runMasterSetupFlow(rl, centralDb, localActiveCount) {
|
|
207
|
+
console.log(renderMasterBackupWarning());
|
|
208
|
+
const keepBackups = await askConfirm(rl, "Do you want to keep automatic backups enabled?", true);
|
|
209
|
+
if (!keepBackups) {
|
|
210
|
+
console.log(renderBackupDeclineAckPrompt());
|
|
211
|
+
if (!(await askTypedPhrase(rl, "Phrase: ", BACKUP_RISK_PHRASE))) {
|
|
212
|
+
printStatus("warn", "backup acknowledgement required", "setup cancelled");
|
|
188
213
|
return false;
|
|
189
|
-
if (choice === String(candidates.length + 1)) {
|
|
190
|
-
remotePath = await ask(rl, "Custom path (e.g. /Volumes/nas/gnosys): ");
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
const idx = parseInt(choice, 10) - 1;
|
|
194
|
-
const volume = candidates[idx];
|
|
195
|
-
// Suggest a gnosys subdirectory inside the volume
|
|
196
|
-
const suggested = path.join(volume, "gnosys");
|
|
197
|
-
const useSubdir = await askConfirm(rl, `Use ${suggested} (recommended subdirectory)?`);
|
|
198
|
-
remotePath = useSubdir ? suggested : volume;
|
|
199
214
|
}
|
|
200
215
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
console.log("
|
|
204
|
-
|
|
216
|
+
let moveExisting = false;
|
|
217
|
+
if (localActiveCount > 0) {
|
|
218
|
+
console.log("");
|
|
219
|
+
console.log(`This machine already has a local brain with ${localActiveCount} memories.`);
|
|
220
|
+
console.log("");
|
|
221
|
+
const brainChoice = await askChoice(rl, "Do you want to:", [
|
|
222
|
+
{ key: "1", label: "Move the existing brain into the master folder (recommended)" },
|
|
223
|
+
{ key: "2", label: "Start a fresh master folder (existing local memories will be ignored)" },
|
|
224
|
+
], "1");
|
|
225
|
+
moveExisting = brainChoice === "1";
|
|
205
226
|
}
|
|
206
|
-
|
|
207
|
-
|
|
227
|
+
console.log("");
|
|
228
|
+
console.log("The master database must live on this machine's local disk (not NAS, iCloud, Dropbox, or a network mount).");
|
|
229
|
+
console.log("");
|
|
230
|
+
const defaultMaster = path.join(getGnosysHome(), "master-brain");
|
|
231
|
+
const masterPath = await pickFolderPath(rl, "Master folder path on this machine's local disk:", defaultMaster);
|
|
232
|
+
if (!masterPath) {
|
|
233
|
+
printStatus("warn", "no path provided", "setup cancelled");
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
const diskCheck = checkMasterPathLocalDisk(masterPath);
|
|
237
|
+
console.log(`\n${diskCheck.message}`);
|
|
238
|
+
if (diskCheck.verdict === "network") {
|
|
239
|
+
printStatus("fail", "master folder must be on local disk, not a network mount");
|
|
208
240
|
return false;
|
|
209
241
|
}
|
|
210
|
-
|
|
211
|
-
|
|
242
|
+
if (diskCheck.verdict === "unknown") {
|
|
243
|
+
console.log(`\nType this phrase exactly to continue:\n ${LOCAL_DISK_ACK_PHRASE}\n`);
|
|
244
|
+
if (!(await askTypedPhrase(rl, "Phrase: ", LOCAL_DISK_ACK_PHRASE))) {
|
|
245
|
+
printStatus("warn", "local disk acknowledgement required", "setup cancelled");
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
212
249
|
console.log("");
|
|
213
|
-
const validateSpinner = Spinner(`checking ${
|
|
214
|
-
const validation = await validateLocation(
|
|
250
|
+
const validateSpinner = Spinner(`checking ${masterPath}…`);
|
|
251
|
+
const validation = await validateLocation(masterPath);
|
|
215
252
|
if (validation.ok) {
|
|
216
253
|
const latency = validation.checks.latencyMs;
|
|
217
|
-
validateSpinner.ok("
|
|
254
|
+
validateSpinner.ok("folder ready", latency !== null ? `${latency} ms` : undefined);
|
|
218
255
|
}
|
|
219
256
|
else {
|
|
220
257
|
validateSpinner.fail("validation failed");
|
|
221
258
|
}
|
|
222
259
|
showValidationSummary(validation);
|
|
223
260
|
if (!validation.ok) {
|
|
224
|
-
printStatus("fail", "
|
|
261
|
+
printStatus("fail", "master folder not configured");
|
|
225
262
|
return false;
|
|
226
263
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
264
|
+
const { config: mc } = ensureMachineConfig();
|
|
265
|
+
const existingMarker = readMasterMarker(masterPath);
|
|
266
|
+
if (existingMarker?.holderMachineId && existingMarker.holderMachineId !== mc.machineId) {
|
|
267
|
+
printStatus("fail", "another machine already owns this master folder", `holder: ${existingMarker.holderMachineId}`);
|
|
268
|
+
const takeover = await askConfirm(rl, "Attempt stale-takeover (advanced — only if the previous master is gone)?", false);
|
|
269
|
+
if (!takeover)
|
|
230
270
|
return false;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
let strategy = "configure-only";
|
|
245
|
-
if (!remoteHasData && !localHasData) {
|
|
246
|
-
// Both empty — just point at remote
|
|
247
|
-
console.log(" Both local and remote are empty. Configuring remote without data transfer.");
|
|
248
|
-
strategy = "configure-only";
|
|
249
|
-
}
|
|
250
|
-
else if (!remoteHasData && localHasData) {
|
|
251
|
-
// Local has data, remote is empty — initial migration
|
|
252
|
-
console.log(` Your local DB has ${localActiveCount} memories.`);
|
|
253
|
-
console.log(" The remote is empty.");
|
|
254
|
-
const migrate = await askConfirm(rl, "Copy your local memories to the remote now?", true);
|
|
255
|
-
strategy = migrate ? "migrate" : "configure-only";
|
|
256
|
-
}
|
|
257
|
-
else if (remoteHasData && !localHasData) {
|
|
258
|
-
// Remote has data, local empty — pull from remote (this is the "second machine" scenario)
|
|
259
|
-
console.log(` The remote has ${validation.checks.existingDb.memoryCount} memories.`);
|
|
260
|
-
console.log(" Your local DB is empty.");
|
|
261
|
-
const pull = await askConfirm(rl, "Pull all memories from remote to local now?", true);
|
|
262
|
-
strategy = pull ? "pull" : "configure-only";
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
// BOTH have data — the tricky case
|
|
266
|
-
// Reword to match deci-037: remote is the canonical source of truth,
|
|
267
|
-
// local is an offline-resilience cache. The two counts shown here are
|
|
268
|
-
// pre-merge snapshots, not "two co-equal copies".
|
|
269
|
-
console.log(` Remote DB (source of truth): ${validation.checks.existingDb.memoryCount} memories`);
|
|
270
|
-
console.log(` Local cache (offline backup): ${localActiveCount} memories`);
|
|
271
|
-
console.log("");
|
|
272
|
-
const choice = await askChoice(rl, "How do you want to combine them?", [
|
|
273
|
-
{ key: "1", label: "Merge — push local-only memories up, pull remote-only down, flag conflicts (recommended)" },
|
|
274
|
-
{ key: "2", label: "Replace remote with local (overwrites remote — destructive)" },
|
|
275
|
-
{ key: "3", label: "Replace local with remote (overwrites local cache)" },
|
|
276
|
-
{ key: "4", label: "Skip — configure remote without touching either DB" },
|
|
277
|
-
], "1");
|
|
278
|
-
if (choice === "1")
|
|
279
|
-
strategy = "merge";
|
|
280
|
-
else if (choice === "2")
|
|
281
|
-
strategy = "migrate"; // overwrites
|
|
282
|
-
else if (choice === "3")
|
|
283
|
-
strategy = "pull";
|
|
284
|
-
else
|
|
285
|
-
strategy = "configure-only"; // "Skip" is configure-only, not cancel
|
|
286
|
-
if (strategy === "migrate" || strategy === "pull") {
|
|
287
|
-
const confirm = await askConfirm(rl, `\nThis will overwrite the ${strategy === "migrate" ? "remote" : "local"} DB. Are you sure?`, false);
|
|
288
|
-
if (!confirm) {
|
|
289
|
-
console.log("Cancelled.");
|
|
290
|
-
return false;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
// Step 4: Save config and execute strategy. v5.9.3 Screen 6 wraps the
|
|
295
|
-
// long-running sync calls in Spinners and prints a final Diff() block.
|
|
296
|
-
const previousRemote = centralDb.getMeta(REMOTE_PATH_KEY) || null;
|
|
297
|
-
centralDb.setMeta(REMOTE_PATH_KEY, remotePath);
|
|
298
|
-
centralDb.setMeta(REMOTE_MODE_KEY, syncMode);
|
|
299
|
-
const sync = new RemoteSync(centralDb, remotePath);
|
|
300
|
-
try {
|
|
301
|
-
if (strategy === "migrate") {
|
|
302
|
-
const spin = Spinner(`doing first sync to ${remotePath}…`);
|
|
271
|
+
writeMasterMarker(masterPath, mc.machineId, { previousEpoch: existingMarker.epoch });
|
|
272
|
+
}
|
|
273
|
+
const previousRemote = getConfiguredRemotePath(centralDb);
|
|
274
|
+
persistMultiMachineConfig(centralDb, masterPath, "master");
|
|
275
|
+
if (!existingMarker?.holderMachineId || existingMarker.holderMachineId === mc.machineId) {
|
|
276
|
+
writeMasterMarker(masterPath, mc.machineId);
|
|
277
|
+
}
|
|
278
|
+
mkdirSync(path.join(masterPath, "backups"), { recursive: true });
|
|
279
|
+
mkdirSync(path.join(masterPath, ".gnosys-staging"), { recursive: true });
|
|
280
|
+
if (moveExisting) {
|
|
281
|
+
const spin = Spinner(`moving local brain into ${masterPath}…`);
|
|
282
|
+
const sync = new RemoteSync(centralDb, masterPath);
|
|
283
|
+
try {
|
|
303
284
|
const result = await sync.migrate();
|
|
304
285
|
if (result.ok) {
|
|
305
|
-
spin.ok("
|
|
286
|
+
spin.ok("brain moved", `${result.copied} memories`);
|
|
306
287
|
}
|
|
307
288
|
else {
|
|
308
289
|
spin.fail("migration had errors");
|
|
@@ -311,42 +292,116 @@ async function setupRemoteFlow(rl, centralDb, localActiveCount) {
|
|
|
311
292
|
return false;
|
|
312
293
|
}
|
|
313
294
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const result = await sync.pull({ strategy: "newer-wins" });
|
|
317
|
-
spin.ok("first sync complete", `${result.pulled} memories pulled`);
|
|
318
|
-
for (const e of result.errors)
|
|
319
|
-
printStatus("fail", e);
|
|
295
|
+
finally {
|
|
296
|
+
sync.closeRemote();
|
|
320
297
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
298
|
+
}
|
|
299
|
+
archiveLocalDbBeforeMaster();
|
|
300
|
+
console.log("");
|
|
301
|
+
console.log(renderRemoteDiff({ previousRemote, newRemote: masterPath, roleOrMode: "master" }));
|
|
302
|
+
printStatus("ok", "saved", "machine.json + remote_path");
|
|
303
|
+
console.log(Footer("run `gnosys remote status` to check sync state"));
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
async function runClientSetupFlow(rl, centralDb, localActiveCount) {
|
|
307
|
+
await showTailscaleClientGuide(rl);
|
|
308
|
+
const masterPath = await pickFolderPath(rl, "Enter the master folder path as it appears on this machine:");
|
|
309
|
+
if (!masterPath) {
|
|
310
|
+
printStatus("warn", "no path provided", "setup cancelled");
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
console.log("");
|
|
314
|
+
const validateSpinner = Spinner(`checking ${masterPath}…`);
|
|
315
|
+
const validation = await validateLocation(masterPath);
|
|
316
|
+
if (validation.ok) {
|
|
317
|
+
validateSpinner.ok("master folder reachable");
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
validateSpinner.fail("validation failed");
|
|
321
|
+
}
|
|
322
|
+
showValidationSummary(validation);
|
|
323
|
+
if (!validation.ok) {
|
|
324
|
+
printStatus("fail", "master folder not configured");
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
const marker = readMasterMarker(masterPath);
|
|
328
|
+
if (!marker?.holderMachineId) {
|
|
329
|
+
printStatus("warn", "no master.json found", "the folder may not be set up as a master yet — continue only if you trust this path");
|
|
330
|
+
const trust = await askConfirm(rl, "Continue anyway?", false);
|
|
331
|
+
if (!trust)
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
let { config: mc } = ensureMachineConfig();
|
|
335
|
+
if (detectClonedStagingPresence(masterPath, mc.machineId)) {
|
|
336
|
+
console.log("");
|
|
337
|
+
printStatus("warn", "presence file already exists for this machineId on the master", "common after VM clone or backup restore");
|
|
338
|
+
const remint = await askConfirm(rl, "Re-mint this machine's ID (recommended)?", true);
|
|
339
|
+
if (remint) {
|
|
340
|
+
mc = { ...mc, machineId: randomUUID() };
|
|
341
|
+
writeMachineConfig(mc);
|
|
342
|
+
printStatus("ok", "new machineId", mc.machineId);
|
|
333
343
|
}
|
|
334
344
|
}
|
|
335
|
-
|
|
336
|
-
|
|
345
|
+
if (localActiveCount > 0) {
|
|
346
|
+
console.log("");
|
|
347
|
+
console.log(`This machine already has ${localActiveCount} memories locally.`);
|
|
348
|
+
console.log("");
|
|
349
|
+
console.log("If you choose to keep them, they will be treated as NEW memories and sent to the master.");
|
|
350
|
+
console.log("This may create duplicates if the same memories already exist on the master.");
|
|
351
|
+
console.log("");
|
|
352
|
+
const dupChoice = await askChoice(rl, "Do you want to:", [
|
|
353
|
+
{
|
|
354
|
+
key: "1",
|
|
355
|
+
label: "Keep the local memories and push them later (acknowledged risk of duplicates)",
|
|
356
|
+
},
|
|
357
|
+
{ key: "2", label: "Start fresh (local memories will be ignored)" },
|
|
358
|
+
], "2");
|
|
359
|
+
if (dupChoice === "1") {
|
|
360
|
+
const ack = await askConfirm(rl, "Acknowledge duplicate risk and continue?", false);
|
|
361
|
+
if (!ack)
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
337
364
|
}
|
|
338
|
-
|
|
365
|
+
const previousRemote = getConfiguredRemotePath(centralDb);
|
|
366
|
+
persistMultiMachineConfig(centralDb, masterPath, "client");
|
|
367
|
+
writeClientPresenceFile(masterPath, mc.machineId);
|
|
339
368
|
console.log("");
|
|
340
|
-
console.log(renderRemoteDiff({ previousRemote, newRemote:
|
|
341
|
-
printStatus("ok", "saved", "
|
|
342
|
-
console.log(Footer("
|
|
369
|
+
console.log(renderRemoteDiff({ previousRemote, newRemote: masterPath, roleOrMode: "client" }));
|
|
370
|
+
printStatus("ok", "saved", "machine.json + remote_path");
|
|
371
|
+
console.log(Footer("client machines stage new memories; ingest runs on the master"));
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
async function changeMasterPathFlow(rl, centralDb, currentRemote) {
|
|
375
|
+
const role = readMachineConfig()?.remote.role ??
|
|
376
|
+
centralDb.getMeta(REMOTE_MODE_KEY) ??
|
|
377
|
+
"client";
|
|
378
|
+
const newPath = await pickFolderPath(rl, `New master folder path (current: ${currentRemote}):`);
|
|
379
|
+
if (!newPath || newPath === currentRemote) {
|
|
380
|
+
printStatus("warn", "no change", "path unchanged");
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
const validation = await validateLocation(newPath);
|
|
384
|
+
showValidationSummary(validation);
|
|
385
|
+
if (!validation.ok)
|
|
386
|
+
return false;
|
|
387
|
+
persistMultiMachineConfig(centralDb, newPath, role);
|
|
388
|
+
if (role === "master") {
|
|
389
|
+
const { config: mc } = ensureMachineConfig();
|
|
390
|
+
const existing = readMasterMarker(newPath);
|
|
391
|
+
if (existing?.holderMachineId && existing.holderMachineId !== mc.machineId) {
|
|
392
|
+
printStatus("fail", "another machine owns the target master folder");
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
writeMasterMarker(newPath, mc.machineId, { previousEpoch: existing?.epoch });
|
|
396
|
+
}
|
|
397
|
+
printStatus("ok", "master folder updated", newPath);
|
|
343
398
|
return true;
|
|
344
399
|
}
|
|
345
400
|
// ─── Reconfigure helpers ────────────────────────────────────────────────
|
|
346
401
|
async function disconnectRemote(rl, localDb) {
|
|
347
402
|
const remotePath = getConfiguredRemotePath(localDb);
|
|
348
403
|
if (!remotePath) {
|
|
349
|
-
printStatus("warn", "
|
|
404
|
+
printStatus("warn", "multi-machine sync is not configured");
|
|
350
405
|
return false;
|
|
351
406
|
}
|
|
352
407
|
const localCounts = localDb.getMemoryCount();
|
|
@@ -371,103 +426,24 @@ async function disconnectRemote(rl, localDb) {
|
|
|
371
426
|
const remoteReachable = Boolean(syncStatus?.reachable && validation.ok);
|
|
372
427
|
const remoteActive = remoteCounts?.active ?? validation.checks.existingDb.memoryCount ?? null;
|
|
373
428
|
console.log("");
|
|
374
|
-
printStatus("warn", "disconnecting
|
|
429
|
+
printStatus("warn", "disconnecting returns this machine to single-machine mode");
|
|
375
430
|
console.log(` local ~/.gnosys/gnosys.db — ${localCounts.active} active memories`);
|
|
376
431
|
if (remoteReachable && remoteActive !== null) {
|
|
377
|
-
console.log(`
|
|
432
|
+
console.log(` master ${remotePath} — ${remoteActive} active memories`);
|
|
378
433
|
}
|
|
379
434
|
else {
|
|
380
|
-
printStatus("warn", "
|
|
435
|
+
printStatus("warn", "master folder is not reachable", remotePath);
|
|
381
436
|
}
|
|
382
437
|
console.log("");
|
|
383
|
-
console.log(" The
|
|
384
|
-
console.log(" After disconnect, this
|
|
385
|
-
|
|
386
|
-
const pendingPull = syncStatus?.pendingPull ?? 0;
|
|
387
|
-
const pendingPush = syncStatus?.pendingPush ?? 0;
|
|
388
|
-
const conflicts = syncStatus?.conflicts.length ?? 0;
|
|
389
|
-
const countGap = remoteActive !== null && remoteActive > localCounts.active
|
|
390
|
-
? remoteActive - localCounts.active
|
|
391
|
-
: 0;
|
|
392
|
-
const shouldRecommendSync = remoteReachable &&
|
|
393
|
-
(pendingPull > 0 || pendingPush > 0 || conflicts > 0 || countGap > 0);
|
|
394
|
-
if (shouldRecommendSync) {
|
|
395
|
-
console.log("");
|
|
396
|
-
printStatus("warn", "your local cache may be behind the shared remote brain");
|
|
397
|
-
if (countGap > 0) {
|
|
398
|
-
console.log(` Remote has about ${countGap} more active memor${countGap === 1 ? "y" : "ies"} than local.`);
|
|
399
|
-
}
|
|
400
|
-
if (pendingPull > 0 || pendingPush > 0) {
|
|
401
|
-
const parts = [];
|
|
402
|
-
if (pendingPull > 0)
|
|
403
|
-
parts.push(`${pendingPull} to pull into local`);
|
|
404
|
-
if (pendingPush > 0)
|
|
405
|
-
parts.push(`${pendingPush} to push to remote`);
|
|
406
|
-
console.log(` Pending sync: ${parts.join(", ")}.`);
|
|
407
|
-
}
|
|
408
|
-
if (conflicts > 0) {
|
|
409
|
-
console.log(` ${conflicts} unresolved conflict${conflicts === 1 ? "" : "s"} — resolve before or during sync.`);
|
|
410
|
-
}
|
|
411
|
-
printStatus("progress", "recommended", "gnosys setup remote sync");
|
|
412
|
-
}
|
|
413
|
-
const choice = await askChoice(rl, shouldRecommendSync
|
|
414
|
-
? "Sync first so you do not lose access to remote-only memories on this Mac:"
|
|
415
|
-
: "How do you want to proceed?", remoteReachable
|
|
416
|
-
? [
|
|
417
|
-
{ key: "1", label: "Run gnosys setup remote sync, then disconnect (recommended)" },
|
|
418
|
-
{ key: "2", label: "Disconnect now without syncing (keep current local DB only)" },
|
|
419
|
-
{ key: "3", label: "Cancel" },
|
|
420
|
-
]
|
|
421
|
-
: [
|
|
422
|
-
{ key: "2", label: "Disconnect anyway (local DB only; remote not reachable to sync)" },
|
|
423
|
-
{ key: "3", label: "Cancel" },
|
|
424
|
-
], "3");
|
|
425
|
-
if (choice === "3") {
|
|
426
|
-
console.log("Cancelled.");
|
|
427
|
-
return false;
|
|
428
|
-
}
|
|
429
|
-
if (choice === "1" && remoteReachable) {
|
|
430
|
-
const spin = Spinner("syncing local and remote…");
|
|
431
|
-
const syncRun = new RemoteSync(localDb, remotePath);
|
|
432
|
-
try {
|
|
433
|
-
const result = await syncRun.sync();
|
|
434
|
-
if (result.errors.length > 0) {
|
|
435
|
-
spin.fail("sync had errors");
|
|
436
|
-
for (const e of result.errors)
|
|
437
|
-
printStatus("fail", e);
|
|
438
|
-
const proceed = await askConfirm(rl, "Disconnect anyway without a clean sync?", false);
|
|
439
|
-
if (!proceed)
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
|
-
else {
|
|
443
|
-
spin.ok("sync complete", `pushed ${result.pushed} · pulled ${result.pulled} · conflicts ${result.conflicts.length}`);
|
|
444
|
-
if (result.conflicts.length > 0) {
|
|
445
|
-
printStatus("warn", "conflicts still open", "gnosys setup remote resolve <id> --keep <local|remote>");
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
finally {
|
|
450
|
-
syncRun.closeRemote();
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
else if (choice === "1" && !remoteReachable) {
|
|
454
|
-
printStatus("fail", "cannot sync — mount the remote or cancel");
|
|
455
|
-
return false;
|
|
456
|
-
}
|
|
457
|
-
if (choice === "2" && shouldRecommendSync) {
|
|
458
|
-
const risky = await askConfirm(rl, "Disconnect without syncing? You may only see local memories on this Mac until you reconnect.", false);
|
|
459
|
-
if (!risky) {
|
|
460
|
-
console.log("Cancelled.");
|
|
461
|
-
return false;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
const confirm = await askConfirm(rl, "Disconnect now? Remote files stay on disk; this machine uses ~/.gnosys/gnosys.db only.", false);
|
|
438
|
+
console.log(" The master folder on disk is not deleted.");
|
|
439
|
+
console.log(" After disconnect, this machine stops using multi-machine sync.");
|
|
440
|
+
const confirm = await askConfirm(rl, "Disconnect now? (You can re-run setup later.)", false);
|
|
465
441
|
if (!confirm) {
|
|
466
442
|
console.log("Cancelled.");
|
|
467
443
|
return false;
|
|
468
444
|
}
|
|
469
445
|
clearRemoteSyncConfig(localDb);
|
|
470
|
-
|
|
446
|
+
printStatus("ok", "disconnected", "multi-machine sync is off on this machine");
|
|
471
447
|
return true;
|
|
472
448
|
}
|
|
473
449
|
async function revalidateRemote(_rl, _centralDb, currentRemote) {
|
|
@@ -475,10 +451,10 @@ async function revalidateRemote(_rl, _centralDb, currentRemote) {
|
|
|
475
451
|
const spin = Spinner(`checking ${currentRemote}…`);
|
|
476
452
|
const validation = await validateLocation(currentRemote);
|
|
477
453
|
if (validation.ok) {
|
|
478
|
-
spin.ok("
|
|
454
|
+
spin.ok("master folder is reachable");
|
|
479
455
|
}
|
|
480
456
|
else {
|
|
481
|
-
spin.fail("validation failed", "the
|
|
457
|
+
spin.fail("validation failed", "the master may be unreachable or the path is wrong");
|
|
482
458
|
}
|
|
483
459
|
showValidationSummary(validation);
|
|
484
460
|
return validation.ok;
|
|
@@ -489,21 +465,27 @@ export async function configureFromPath(centralDb, remotePath, opts = {}) {
|
|
|
489
465
|
const validation = await validateLocation(remotePath);
|
|
490
466
|
showValidationSummary(validation);
|
|
491
467
|
if (!validation.ok) {
|
|
492
|
-
console.log("\nValidation failed.
|
|
468
|
+
console.log("\nValidation failed. Master folder not configured.");
|
|
493
469
|
return false;
|
|
494
470
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
471
|
+
const role = opts.role ?? "client";
|
|
472
|
+
persistMultiMachineConfig(centralDb, remotePath, role);
|
|
473
|
+
console.log(`\n✓ Multi-machine sync configured: ${remotePath} (${role})`);
|
|
474
|
+
if (role === "master") {
|
|
475
|
+
const { config: mc } = ensureMachineConfig();
|
|
476
|
+
writeMasterMarker(remotePath, mc.machineId);
|
|
477
|
+
}
|
|
478
|
+
if (opts.migrate && role === "master" && !validation.checks.existingDb.found) {
|
|
479
|
+
console.log("\nMigrating local DB to master folder...");
|
|
499
480
|
const sync = new RemoteSync(centralDb, remotePath);
|
|
500
481
|
try {
|
|
501
482
|
const result = await sync.migrate();
|
|
502
483
|
if (result.ok) {
|
|
503
|
-
console.log(` ✓ Copied ${result.copied} memories to
|
|
484
|
+
console.log(` ✓ Copied ${result.copied} memories to master folder.`);
|
|
485
|
+
archiveLocalDbBeforeMaster();
|
|
504
486
|
}
|
|
505
487
|
else {
|
|
506
|
-
console.log(
|
|
488
|
+
console.log(" ✗ Migration had errors:");
|
|
507
489
|
for (const e of result.errors)
|
|
508
490
|
console.log(` ${e}`);
|
|
509
491
|
return false;
|
|
@@ -514,7 +496,14 @@ export async function configureFromPath(centralDb, remotePath, opts = {}) {
|
|
|
514
496
|
}
|
|
515
497
|
}
|
|
516
498
|
else if (validation.checks.existingDb.found) {
|
|
517
|
-
console.log("\nExisting DB found at
|
|
499
|
+
console.log("\nExisting DB found at master folder.");
|
|
518
500
|
}
|
|
519
501
|
return true;
|
|
520
502
|
}
|
|
503
|
+
export { stagingDirForMachine, clientPresencePath } from "./syncStaging.js";
|
|
504
|
+
export const __test = {
|
|
505
|
+
matchesTypedPhrase,
|
|
506
|
+
detectClonedStagingPresence,
|
|
507
|
+
stagingDirForMachine,
|
|
508
|
+
clientPresencePath,
|
|
509
|
+
};
|