@geml/logseq-sync 2.0.8 → 2.0.9
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/LICENSE +28 -28
- package/README.md +10 -2
- package/core/src/bridge.mjs +8 -8
- package/core/src/discovery.mjs +204 -204
- package/core/src/sync-engine.mjs +111 -11
- package/docs/how-it-works.svg +42 -42
- package/package.json +58 -54
- package/watcher/bin/create-graph.mjs +51 -51
- package/watcher/bin/create_graph_headless.cljs +22 -22
- package/watcher/bin/live-roundtrip.mjs +131 -131
- package/watcher/bin/logseq-sync.mjs +23 -1
|
@@ -1,131 +1,131 @@
|
|
|
1
|
-
// The live half of the spike: run the round trip against a REAL DB graph via
|
|
2
|
-
// the official @logseq/cli, with Logseq's own `validate` as the judge.
|
|
3
|
-
//
|
|
4
|
-
// node bin/live-roundtrip.mjs <graph-name> [--edit]
|
|
5
|
-
//
|
|
6
|
-
// Stages (each printed, each gated):
|
|
7
|
-
// 1. `logseq export-edn -g <graph>` → out/export-1.edn
|
|
8
|
-
// 2. ednToGemlFiles → out/geml/**.geml
|
|
9
|
-
// 3. `geml check --root out/geml` on every doc (zero errors required)
|
|
10
|
-
// 4. gemlFilesToEdn → out/import.edn
|
|
11
|
-
// 5. STRUCTURAL identity export-1 ⇄ import.edn (EDN semantics) — the offline
|
|
12
|
-
// criterion, now on real data
|
|
13
|
-
// 6. with --edit: `geml set` the first uuid block, rebuild import.edn, and
|
|
14
|
-
// `logseq import-edn` it back, then `logseq validate` — the semantics
|
|
15
|
-
// probe: does re-import by uuid update in place, or append?
|
|
16
|
-
//
|
|
17
|
-
// Import-back is opt-in (--edit) because import semantics on a whole-graph
|
|
18
|
-
// re-import are exactly what this stage exists to LEARN — it may merge, it may
|
|
19
|
-
// duplicate id-less blocks. The script never touches a graph unless told to.
|
|
20
|
-
import { execFileSync } from "node:child_process";
|
|
21
|
-
import { mkdirSync, writeFileSync, readFileSync, rmSync, readdirSync } from "node:fs";
|
|
22
|
-
import { dirname, join, resolve } from "node:path";
|
|
23
|
-
import { fileURLToPath } from "node:url";
|
|
24
|
-
import { parseEDNString } from "edn-data";
|
|
25
|
-
import { ednToGemlFiles, gemlFilesToEdn } from "../../core/src/mapping.mjs";
|
|
26
|
-
import { parse, addressedUnits, sliceUnit } from "../../../../geml-parser/dist/geml.js";
|
|
27
|
-
|
|
28
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
29
|
-
const out = join(here, "..", "out");
|
|
30
|
-
const GEML = resolve(here, "..", "..", "..", "geml-parser", "dist", "geml.js");
|
|
31
|
-
const lib = { parse, addressedUnits, sliceUnit };
|
|
32
|
-
|
|
33
|
-
const [graph, ...flags] = process.argv.slice(2);
|
|
34
|
-
if (!graph) { console.error("usage: node bin/live-roundtrip.mjs <graph-name> [--edit]"); process.exit(2); }
|
|
35
|
-
const doEdit = flags.includes("--edit");
|
|
36
|
-
|
|
37
|
-
// The CLI is driven through npx so nothing here depends on a global install;
|
|
38
|
-
// LOGSEQ_CLI_DIR points at a directory whose node_modules has @logseq/cli.
|
|
39
|
-
const cliCwd = process.env.LOGSEQ_CLI_DIR ?? join(here, "..");
|
|
40
|
-
const logseq = (...args) =>
|
|
41
|
-
execFileSync("npx", ["-y", "@logseq/cli", ...args], { cwd: cliCwd, encoding: "utf8", shell: true, maxBuffer: 1 << 28 });
|
|
42
|
-
const geml = (...args) =>
|
|
43
|
-
execFileSync(process.execPath, [GEML, ...args], { encoding: "utf8", maxBuffer: 1 << 28 });
|
|
44
|
-
|
|
45
|
-
// EDN-semantics canonical form (same as the test suite).
|
|
46
|
-
function canon(v) {
|
|
47
|
-
if (Array.isArray(v)) return v.map(canon);
|
|
48
|
-
if (v !== null && typeof v === "object") {
|
|
49
|
-
if (Array.isArray(v.map)) {
|
|
50
|
-
const entries = v.map.map(([k, val]) => [canon(k), canon(val)]);
|
|
51
|
-
entries.sort((a, b) => (JSON.stringify(a[0]) < JSON.stringify(b[0]) ? -1 : 1));
|
|
52
|
-
return { map: entries };
|
|
53
|
-
}
|
|
54
|
-
if (Array.isArray(v.set)) {
|
|
55
|
-
const items = v.set.map(canon);
|
|
56
|
-
items.sort((a, b) => (JSON.stringify(a) < JSON.stringify(b) ? -1 : 1));
|
|
57
|
-
return { set: items };
|
|
58
|
-
}
|
|
59
|
-
const o = {};
|
|
60
|
-
for (const k of Object.keys(v).sort()) o[k] = canon(v[k]);
|
|
61
|
-
return o;
|
|
62
|
-
}
|
|
63
|
-
return v;
|
|
64
|
-
}
|
|
65
|
-
const same = (a, b) => JSON.stringify(canon(parseEDNString(a))) === JSON.stringify(canon(parseEDNString(b)));
|
|
66
|
-
|
|
67
|
-
rmSync(out, { recursive: true, force: true });
|
|
68
|
-
mkdirSync(join(out, "geml"), { recursive: true });
|
|
69
|
-
|
|
70
|
-
console.log(`1. export-edn from graph "${graph}"`);
|
|
71
|
-
logseq("export-edn", "-g", graph, "-f", join(out, "export-1.edn"));
|
|
72
|
-
const edn1 = readFileSync(join(out, "export-1.edn"), "utf8");
|
|
73
|
-
console.log(` ${edn1.length} bytes of EDN`);
|
|
74
|
-
|
|
75
|
-
console.log("2. EDN -> GEML");
|
|
76
|
-
const files = ednToGemlFiles(edn1);
|
|
77
|
-
for (const [rel, text] of files) {
|
|
78
|
-
mkdirSync(dirname(join(out, "geml", rel)), { recursive: true });
|
|
79
|
-
writeFileSync(join(out, "geml", rel), text);
|
|
80
|
-
}
|
|
81
|
-
console.log(` ${files.size} documents`);
|
|
82
|
-
|
|
83
|
-
console.log("3. geml check on every document");
|
|
84
|
-
let dirty = 0;
|
|
85
|
-
for (const rel of files.keys()) {
|
|
86
|
-
try { geml("check", "--root", join(out, "geml"), join(out, "geml", rel)); }
|
|
87
|
-
catch (e) { dirty++; console.error(` FAIL ${rel}\n${e.stdout ?? ""}${e.stderr ?? ""}`); }
|
|
88
|
-
}
|
|
89
|
-
if (dirty) { console.error(` ${dirty} document(s) not clean — stopping`); process.exit(1); }
|
|
90
|
-
console.log(" all clean");
|
|
91
|
-
|
|
92
|
-
console.log("4. GEML -> EDN");
|
|
93
|
-
const files2 = new Map();
|
|
94
|
-
for (const rel of files.keys()) files2.set(rel, readFileSync(join(out, "geml", rel), "utf8"));
|
|
95
|
-
const edn2 = gemlFilesToEdn(files2, lib);
|
|
96
|
-
writeFileSync(join(out, "import.edn"), edn2);
|
|
97
|
-
|
|
98
|
-
console.log("5. structural identity, on the real graph");
|
|
99
|
-
if (!same(edn1, edn2)) { console.error(" NOT identical — diff out/export-1.edn against out/import.edn"); process.exit(1); }
|
|
100
|
-
console.log(" identical (EDN semantics)");
|
|
101
|
-
|
|
102
|
-
if (!doEdit) { console.log("\nround trip holds. Re-run with --edit to probe import-back semantics."); process.exit(0); }
|
|
103
|
-
|
|
104
|
-
console.log("6. edit one uuid block via `geml set`, import back, validate");
|
|
105
|
-
const withUuid = [...files.keys()].map((rel) => {
|
|
106
|
-
const text = files2.get(rel);
|
|
107
|
-
const unit = [...addressedUnits(text)].map((a) => a.unit).find((u) => u.kind === "block" && u.id && /^[0-9a-f-]{36}$/.test(u.id));
|
|
108
|
-
return unit ? { rel, unit } : null;
|
|
109
|
-
}).find(Boolean);
|
|
110
|
-
if (!withUuid) { console.log(" no uuid-bearing block in this graph (nothing referenced) — skipping the edit probe"); process.exit(0); }
|
|
111
|
-
|
|
112
|
-
const target = join(out, "geml", withUuid.rel);
|
|
113
|
-
// Keep the block's own head line (type, id, level) — the edit is to the BODY.
|
|
114
|
-
const src = files2.get(withUuid.rel);
|
|
115
|
-
const head = sliceUnit(src, withUuid.unit.span, "head").trimEnd();
|
|
116
|
-
const body = sliceUnit(src, withUuid.unit.span, "body").trimEnd();
|
|
117
|
-
const fence = head.match(/^=+/)[0];
|
|
118
|
-
writeFileSync(join(out, "edit.txt"), `${head}\n${body} — edited by geml\n${fence}\n`);
|
|
119
|
-
geml("set", target, `#${withUuid.unit.id}`, "--in", join(out, "edit.txt"), "--root", join(out, "geml"));
|
|
120
|
-
console.log(` edited #${withUuid.unit.id} in ${withUuid.rel}`);
|
|
121
|
-
|
|
122
|
-
const files3 = new Map();
|
|
123
|
-
for (const rel of files.keys()) files3.set(rel, readFileSync(join(out, "geml", rel), "utf8"));
|
|
124
|
-
writeFileSync(join(out, "import-edited.edn"), gemlFilesToEdn(files3, lib));
|
|
125
|
-
logseq("import-edn", "-g", graph, "-f", join(out, "import-edited.edn"));
|
|
126
|
-
console.log(" imported");
|
|
127
|
-
console.log(logseq("validate", "-g", graph).trim());
|
|
128
|
-
|
|
129
|
-
console.log("7. export again — inspect out/export-2.edn to judge merge semantics");
|
|
130
|
-
logseq("export-edn", "-g", graph, "-f", join(out, "export-2.edn"));
|
|
131
|
-
console.log("done — compare out/export-1.edn / out/export-2.edn");
|
|
1
|
+
// The live half of the spike: run the round trip against a REAL DB graph via
|
|
2
|
+
// the official @logseq/cli, with Logseq's own `validate` as the judge.
|
|
3
|
+
//
|
|
4
|
+
// node bin/live-roundtrip.mjs <graph-name> [--edit]
|
|
5
|
+
//
|
|
6
|
+
// Stages (each printed, each gated):
|
|
7
|
+
// 1. `logseq export-edn -g <graph>` → out/export-1.edn
|
|
8
|
+
// 2. ednToGemlFiles → out/geml/**.geml
|
|
9
|
+
// 3. `geml check --root out/geml` on every doc (zero errors required)
|
|
10
|
+
// 4. gemlFilesToEdn → out/import.edn
|
|
11
|
+
// 5. STRUCTURAL identity export-1 ⇄ import.edn (EDN semantics) — the offline
|
|
12
|
+
// criterion, now on real data
|
|
13
|
+
// 6. with --edit: `geml set` the first uuid block, rebuild import.edn, and
|
|
14
|
+
// `logseq import-edn` it back, then `logseq validate` — the semantics
|
|
15
|
+
// probe: does re-import by uuid update in place, or append?
|
|
16
|
+
//
|
|
17
|
+
// Import-back is opt-in (--edit) because import semantics on a whole-graph
|
|
18
|
+
// re-import are exactly what this stage exists to LEARN — it may merge, it may
|
|
19
|
+
// duplicate id-less blocks. The script never touches a graph unless told to.
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
21
|
+
import { mkdirSync, writeFileSync, readFileSync, rmSync, readdirSync } from "node:fs";
|
|
22
|
+
import { dirname, join, resolve } from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { parseEDNString } from "edn-data";
|
|
25
|
+
import { ednToGemlFiles, gemlFilesToEdn } from "../../core/src/mapping.mjs";
|
|
26
|
+
import { parse, addressedUnits, sliceUnit } from "../../../../geml-parser/dist/geml.js";
|
|
27
|
+
|
|
28
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const out = join(here, "..", "out");
|
|
30
|
+
const GEML = resolve(here, "..", "..", "..", "geml-parser", "dist", "geml.js");
|
|
31
|
+
const lib = { parse, addressedUnits, sliceUnit };
|
|
32
|
+
|
|
33
|
+
const [graph, ...flags] = process.argv.slice(2);
|
|
34
|
+
if (!graph) { console.error("usage: node bin/live-roundtrip.mjs <graph-name> [--edit]"); process.exit(2); }
|
|
35
|
+
const doEdit = flags.includes("--edit");
|
|
36
|
+
|
|
37
|
+
// The CLI is driven through npx so nothing here depends on a global install;
|
|
38
|
+
// LOGSEQ_CLI_DIR points at a directory whose node_modules has @logseq/cli.
|
|
39
|
+
const cliCwd = process.env.LOGSEQ_CLI_DIR ?? join(here, "..");
|
|
40
|
+
const logseq = (...args) =>
|
|
41
|
+
execFileSync("npx", ["-y", "@logseq/cli", ...args], { cwd: cliCwd, encoding: "utf8", shell: true, maxBuffer: 1 << 28 });
|
|
42
|
+
const geml = (...args) =>
|
|
43
|
+
execFileSync(process.execPath, [GEML, ...args], { encoding: "utf8", maxBuffer: 1 << 28 });
|
|
44
|
+
|
|
45
|
+
// EDN-semantics canonical form (same as the test suite).
|
|
46
|
+
function canon(v) {
|
|
47
|
+
if (Array.isArray(v)) return v.map(canon);
|
|
48
|
+
if (v !== null && typeof v === "object") {
|
|
49
|
+
if (Array.isArray(v.map)) {
|
|
50
|
+
const entries = v.map.map(([k, val]) => [canon(k), canon(val)]);
|
|
51
|
+
entries.sort((a, b) => (JSON.stringify(a[0]) < JSON.stringify(b[0]) ? -1 : 1));
|
|
52
|
+
return { map: entries };
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(v.set)) {
|
|
55
|
+
const items = v.set.map(canon);
|
|
56
|
+
items.sort((a, b) => (JSON.stringify(a) < JSON.stringify(b) ? -1 : 1));
|
|
57
|
+
return { set: items };
|
|
58
|
+
}
|
|
59
|
+
const o = {};
|
|
60
|
+
for (const k of Object.keys(v).sort()) o[k] = canon(v[k]);
|
|
61
|
+
return o;
|
|
62
|
+
}
|
|
63
|
+
return v;
|
|
64
|
+
}
|
|
65
|
+
const same = (a, b) => JSON.stringify(canon(parseEDNString(a))) === JSON.stringify(canon(parseEDNString(b)));
|
|
66
|
+
|
|
67
|
+
rmSync(out, { recursive: true, force: true });
|
|
68
|
+
mkdirSync(join(out, "geml"), { recursive: true });
|
|
69
|
+
|
|
70
|
+
console.log(`1. export-edn from graph "${graph}"`);
|
|
71
|
+
logseq("export-edn", "-g", graph, "-f", join(out, "export-1.edn"));
|
|
72
|
+
const edn1 = readFileSync(join(out, "export-1.edn"), "utf8");
|
|
73
|
+
console.log(` ${edn1.length} bytes of EDN`);
|
|
74
|
+
|
|
75
|
+
console.log("2. EDN -> GEML");
|
|
76
|
+
const files = ednToGemlFiles(edn1);
|
|
77
|
+
for (const [rel, text] of files) {
|
|
78
|
+
mkdirSync(dirname(join(out, "geml", rel)), { recursive: true });
|
|
79
|
+
writeFileSync(join(out, "geml", rel), text);
|
|
80
|
+
}
|
|
81
|
+
console.log(` ${files.size} documents`);
|
|
82
|
+
|
|
83
|
+
console.log("3. geml check on every document");
|
|
84
|
+
let dirty = 0;
|
|
85
|
+
for (const rel of files.keys()) {
|
|
86
|
+
try { geml("check", "--root", join(out, "geml"), join(out, "geml", rel)); }
|
|
87
|
+
catch (e) { dirty++; console.error(` FAIL ${rel}\n${e.stdout ?? ""}${e.stderr ?? ""}`); }
|
|
88
|
+
}
|
|
89
|
+
if (dirty) { console.error(` ${dirty} document(s) not clean — stopping`); process.exit(1); }
|
|
90
|
+
console.log(" all clean");
|
|
91
|
+
|
|
92
|
+
console.log("4. GEML -> EDN");
|
|
93
|
+
const files2 = new Map();
|
|
94
|
+
for (const rel of files.keys()) files2.set(rel, readFileSync(join(out, "geml", rel), "utf8"));
|
|
95
|
+
const edn2 = gemlFilesToEdn(files2, lib);
|
|
96
|
+
writeFileSync(join(out, "import.edn"), edn2);
|
|
97
|
+
|
|
98
|
+
console.log("5. structural identity, on the real graph");
|
|
99
|
+
if (!same(edn1, edn2)) { console.error(" NOT identical — diff out/export-1.edn against out/import.edn"); process.exit(1); }
|
|
100
|
+
console.log(" identical (EDN semantics)");
|
|
101
|
+
|
|
102
|
+
if (!doEdit) { console.log("\nround trip holds. Re-run with --edit to probe import-back semantics."); process.exit(0); }
|
|
103
|
+
|
|
104
|
+
console.log("6. edit one uuid block via `geml set`, import back, validate");
|
|
105
|
+
const withUuid = [...files.keys()].map((rel) => {
|
|
106
|
+
const text = files2.get(rel);
|
|
107
|
+
const unit = [...addressedUnits(text)].map((a) => a.unit).find((u) => u.kind === "block" && u.id && /^[0-9a-f-]{36}$/.test(u.id));
|
|
108
|
+
return unit ? { rel, unit } : null;
|
|
109
|
+
}).find(Boolean);
|
|
110
|
+
if (!withUuid) { console.log(" no uuid-bearing block in this graph (nothing referenced) — skipping the edit probe"); process.exit(0); }
|
|
111
|
+
|
|
112
|
+
const target = join(out, "geml", withUuid.rel);
|
|
113
|
+
// Keep the block's own head line (type, id, level) — the edit is to the BODY.
|
|
114
|
+
const src = files2.get(withUuid.rel);
|
|
115
|
+
const head = sliceUnit(src, withUuid.unit.span, "head").trimEnd();
|
|
116
|
+
const body = sliceUnit(src, withUuid.unit.span, "body").trimEnd();
|
|
117
|
+
const fence = head.match(/^=+/)[0];
|
|
118
|
+
writeFileSync(join(out, "edit.txt"), `${head}\n${body} — edited by geml\n${fence}\n`);
|
|
119
|
+
geml("set", target, `#${withUuid.unit.id}`, "--in", join(out, "edit.txt"), "--root", join(out, "geml"));
|
|
120
|
+
console.log(` edited #${withUuid.unit.id} in ${withUuid.rel}`);
|
|
121
|
+
|
|
122
|
+
const files3 = new Map();
|
|
123
|
+
for (const rel of files.keys()) files3.set(rel, readFileSync(join(out, "geml", rel), "utf8"));
|
|
124
|
+
writeFileSync(join(out, "import-edited.edn"), gemlFilesToEdn(files3, lib));
|
|
125
|
+
logseq("import-edn", "-g", graph, "-f", join(out, "import-edited.edn"));
|
|
126
|
+
console.log(" imported");
|
|
127
|
+
console.log(logseq("validate", "-g", graph).trim());
|
|
128
|
+
|
|
129
|
+
console.log("7. export again — inspect out/export-2.edn to judge merge semantics");
|
|
130
|
+
logseq("export-edn", "-g", graph, "-f", join(out, "export-2.edn"));
|
|
131
|
+
console.log("done — compare out/export-1.edn / out/export-2.edn");
|
|
@@ -50,6 +50,9 @@ Flags:
|
|
|
50
50
|
--no-git-commit Never touch git
|
|
51
51
|
--mirror Delete vault files for pages removed from the graph
|
|
52
52
|
(default: keep them, and report the divergence)
|
|
53
|
+
--overwrite-unmanaged Overwrite files that were already there when the sync
|
|
54
|
+
first ran (default: hold them and name them — a file
|
|
55
|
+
no manifest claims was written by someone else)
|
|
53
56
|
--markdown <dir> Also write the graph there as an OG (file-version)
|
|
54
57
|
Logseq graph: bullets, id:: lines, ((uuid)) refs — a
|
|
55
58
|
directory the old app opens. Lossy and one-way
|
|
@@ -76,6 +79,7 @@ const flags = {
|
|
|
76
79
|
twoWay: false,
|
|
77
80
|
gitCommit: "auto",
|
|
78
81
|
mirror: false,
|
|
82
|
+
overwriteUnmanaged: false,
|
|
79
83
|
markdown: null,
|
|
80
84
|
yes: false,
|
|
81
85
|
backup: true,
|
|
@@ -116,6 +120,8 @@ for (let i = 0; i < args.length; i++) {
|
|
|
116
120
|
flags.twoWay = true;
|
|
117
121
|
} else if (arg === "--mirror") {
|
|
118
122
|
flags.mirror = true;
|
|
123
|
+
} else if (arg === "--overwrite-unmanaged") {
|
|
124
|
+
flags.overwriteUnmanaged = true;
|
|
119
125
|
} else if (arg === "--markdown") {
|
|
120
126
|
needValue(i, "--markdown");
|
|
121
127
|
flags.markdown = args[++i];
|
|
@@ -736,12 +742,17 @@ async function performSync() {
|
|
|
736
742
|
const res = await syncEdnToDisk(ednText, targetDir, {
|
|
737
743
|
autoCommit: gitCommit,
|
|
738
744
|
deleteOrphans: flags.mirror,
|
|
745
|
+
overwriteUnmanaged: flags.overwriteUnmanaged,
|
|
739
746
|
preserve: twoWay?.conflicts ?? [],
|
|
740
747
|
markdownDir: flags.markdown ? resolve(expandHome(flags.markdown)) : null,
|
|
741
748
|
lib: gemlLib,
|
|
742
749
|
commitMessage: flags.message || `logseq-geml: sync graph "${graphName}" (${new Date().toISOString()})`,
|
|
743
750
|
});
|
|
744
751
|
|
|
752
|
+
// Files that were on disk before this sync ever ran. Named, never counted
|
|
753
|
+
// as written: silence here is how a person's own graph gets eaten.
|
|
754
|
+
const heldBack = [...(res.unmanaged ?? []), ...(res.markdownUnmanaged ?? [])];
|
|
755
|
+
|
|
745
756
|
lastEdnHash = currentHash;
|
|
746
757
|
writeStatus({
|
|
747
758
|
ok: true,
|
|
@@ -753,10 +764,14 @@ async function performSync() {
|
|
|
753
764
|
deleted: res.deleted.length,
|
|
754
765
|
imported: twoWay?.imported ?? 0,
|
|
755
766
|
conflicts: twoWay?.conflicts ?? [],
|
|
767
|
+
held: heldBack,
|
|
756
768
|
});
|
|
757
769
|
|
|
758
770
|
const timestamp = new Date().toLocaleTimeString();
|
|
759
771
|
const parts = [`${res.written.length} written`, `${res.unchanged.length} unchanged`];
|
|
772
|
+
if (heldBack.length > 0) {
|
|
773
|
+
parts.push(`${heldBack.length} held (not ours to overwrite)`);
|
|
774
|
+
}
|
|
760
775
|
if (twoWay && twoWay.imported > 0) {
|
|
761
776
|
parts.unshift(`${twoWay.imported} imported`);
|
|
762
777
|
}
|
|
@@ -772,8 +787,15 @@ async function performSync() {
|
|
|
772
787
|
`held as you left them, not imported, not overwritten: ${twoWay.conflicts.join(", ")}`
|
|
773
788
|
);
|
|
774
789
|
}
|
|
790
|
+
if (heldBack.length > 0) {
|
|
791
|
+
console.error(
|
|
792
|
+
` ⚠ ${heldBack.length} file(s) were already here before this sync owned them and differ from the graph — ` +
|
|
793
|
+
`left exactly as you wrote them: ${heldBack.join(", ")}. ` +
|
|
794
|
+
`Pass --overwrite-unmanaged to replace them with the graph's version.`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
775
797
|
|
|
776
|
-
if (res.written.length > 0 || res.deleted.length > 0 || twoWayActivity) {
|
|
798
|
+
if (res.written.length > 0 || res.deleted.length > 0 || heldBack.length > 0 || twoWayActivity) {
|
|
777
799
|
console.log(`[${timestamp}] Synced: ${parts.join(", ")}.`);
|
|
778
800
|
if (res.gitResult && res.gitResult.committed) {
|
|
779
801
|
console.log(` Git: ${res.gitResult.output}`);
|