@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.
@@ -19,6 +19,11 @@ import { ednToGemlFiles, gemlFilesToEdn } from "./mapping.mjs";
19
19
  import { gemlToOgMarkdown } from "./og-markdown.mjs";
20
20
 
21
21
  const MANIFEST_FILE = ".geml-manifest.json";
22
+ // The Markdown tree needs a ledger of its own: `--markdown` takes ANY
23
+ // directory, so it is the tree most likely to be pointed at a graph someone
24
+ // already has, and it must know which .md files are its own writes before it
25
+ // overwrites one. A separate file because markdownDir may BE targetDir.
26
+ const MD_MANIFEST_FILE = ".geml-md-manifest.json";
22
27
 
23
28
  const sha256 = (s) => createHash("sha256").update(s).digest("hex");
24
29
 
@@ -31,8 +36,8 @@ const sha256 = (s) => createHash("sha256").update(s).digest("hex");
31
36
  * watcher's own last write, not something a person or agent changed.
32
37
  * @returns {{ known: boolean, hashed: boolean, files: Map<string, string|null> }}
33
38
  */
34
- function readManifest(targetDir) {
35
- const manifestPath = join(targetDir, MANIFEST_FILE);
39
+ function readManifest(targetDir, manifestFile = MANIFEST_FILE) {
40
+ const manifestPath = join(targetDir, manifestFile);
36
41
  if (!existsSync(manifestPath)) return { known: false, hashed: false, files: new Map() };
37
42
  try {
38
43
  const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
@@ -159,6 +164,38 @@ export function readGemlFilesFromDisk(dir, baseDir = dir) {
159
164
  return files;
160
165
  }
161
166
 
167
+ /**
168
+ * Count the Markdown pages of an OG graph laid out under dir.
169
+ * readGemlFilesFromDisk answers "what of ours is here"; this answers "is this
170
+ * already somebody's graph", which is a different question and the one the
171
+ * empty-export guard actually asks.
172
+ */
173
+ function countMarkdownPages(dir) {
174
+ let count = 0;
175
+ for (const sub of ["pages", "journals"]) {
176
+ const root = join(dir, sub);
177
+ if (!existsSync(root)) continue;
178
+ const stack = [root];
179
+ while (stack.length > 0) {
180
+ const current = stack.pop();
181
+ let entries;
182
+ try {
183
+ entries = readdirSync(current, { withFileTypes: true });
184
+ } catch {
185
+ continue;
186
+ }
187
+ for (const entry of entries) {
188
+ if (entry.isDirectory()) {
189
+ if (!entry.name.startsWith(".") && entry.name !== "node_modules") stack.push(join(current, entry.name));
190
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
191
+ count += 1;
192
+ }
193
+ }
194
+ }
195
+ }
196
+ return count;
197
+ }
198
+
162
199
  /**
163
200
  * Incrementally sync a Map of GEML files to disk.
164
201
  * Only writes files whose content has changed or do not yet exist.
@@ -176,16 +213,22 @@ export function readGemlFilesFromDisk(dir, baseDir = dir) {
176
213
  * @param {string[]} [opts.preserve] Files NOT to overwrite even when the graph
177
214
  * differs — the conflicted files of a two-way cycle. Their manifest entry
178
215
  * keeps its previous hash, so they stay flagged until a person resolves them.
179
- * @returns {{ written: string[], orphaned: string[], unchanged: string[], deleted: string[], preserved: string[] }}
216
+ * @param {boolean} [opts.overwriteUnmanaged=false] Overwrite files that exist
217
+ * on disk but that no manifest ever claimed. Off by default: a vault IS a
218
+ * graph, so people point this at one they already have, and a file we never
219
+ * wrote is theirs, not our own echo.
220
+ * @returns {{ written: string[], orphaned: string[], unchanged: string[], deleted: string[], preserved: string[], unmanaged: string[] }}
180
221
  */
181
222
  export function writeGemlFilesToDisk(gemlFiles, targetDir, opts = {}) {
182
223
  const deleteOrphans = opts.deleteOrphans ?? false;
224
+ const overwriteUnmanaged = opts.overwriteUnmanaged ?? false;
183
225
  const preserve = new Set(opts.preserve ?? []);
184
226
  const written = [];
185
227
  const unchanged = [];
186
228
  const orphaned = [];
187
229
  const deleted = [];
188
230
  const preserved = [];
231
+ const unmanaged = [];
189
232
 
190
233
  mkdirSync(targetDir, { recursive: true });
191
234
  const existingFiles = readGemlFilesFromDisk(targetDir);
@@ -204,6 +247,17 @@ export function writeGemlFilesToDisk(gemlFiles, targetDir, opts = {}) {
204
247
  if (preserve.has(rel)) {
205
248
  if (existingContent !== normNew) preserved.push(rel);
206
249
  else unchanged.push(rel);
250
+ } else if (
251
+ existingContent !== undefined &&
252
+ existingContent !== normNew &&
253
+ !lastManifest.has(rel) &&
254
+ !overwriteUnmanaged
255
+ ) {
256
+ // On disk, different from the graph, and no manifest ever claimed it:
257
+ // someone else's file, not our own echo. A file that already MATCHES
258
+ // what we would write falls through and is adopted — identical bytes
259
+ // mean there is nothing of theirs to lose.
260
+ unmanaged.push(rel);
207
261
  } else if (existingContent === undefined || existingContent !== normNew) {
208
262
  atomicWriteFileSync(fullPath, normNew);
209
263
  written.push(rel);
@@ -235,6 +289,9 @@ export function writeGemlFilesToDisk(gemlFiles, targetDir, opts = {}) {
235
289
  // detectExternalEdits() compares against, so the watcher's own writes never
236
290
  // read as someone else's edits.
237
291
  const currentManifest = new Set(gemlFiles.keys());
292
+ // A held file stays unowned: recording a hash for it would make the next run
293
+ // read the person's content as the sync's own last write and clobber it.
294
+ for (const rel of unmanaged) currentManifest.delete(rel);
238
295
  for (const rel of lastManifest) {
239
296
  if (existingFiles.has(rel) && !deleted.includes(rel)) {
240
297
  currentManifest.add(rel);
@@ -254,7 +311,7 @@ export function writeGemlFilesToDisk(gemlFiles, targetDir, opts = {}) {
254
311
  }
255
312
  atomicWriteFileSync(manifestPath, JSON.stringify({ version: 2, files: manifestFiles }, null, 1) + "\n");
256
313
 
257
- return { written, orphaned, unchanged, deleted, preserved };
314
+ return { written, orphaned, unchanged, deleted, preserved, unmanaged };
258
315
  }
259
316
 
260
317
  /**
@@ -364,10 +421,14 @@ export async function gitAutoCommit(targetDir, commitMessage = "logseq-geml sync
364
421
  * @param {boolean} [opts.autoCommit=false]
365
422
  * @param {string} [opts.commitMessage]
366
423
  * @param {boolean} [opts.allowEmptyGraph=false] Refuse 0-page export over non-empty targetDir unless true.
424
+ * @param {boolean} [opts.overwriteUnmanaged=false] Overwrite files no manifest
425
+ * ever claimed, in BOTH trees. Off by default — see writeGemlFilesToDisk.
367
426
  * @param {function} [opts.gitRunner]
368
- * @returns {Promise<{ written: string[], deleted: string[], orphaned: string[], unchanged: string[], gitResult?: any }>}
427
+ * @returns {Promise<{ written: string[], deleted: string[], orphaned: string[], unchanged: string[], unmanaged: string[], markdownWritten: string[], markdownUnmanaged: string[], gitResult?: any }>}
369
428
  */
370
429
  export async function syncEdnToDisk(ednText, targetDir, opts = {}) {
430
+ const overwriteUnmanaged = opts.overwriteUnmanaged ?? false;
431
+
371
432
  // Guard 1: Refuse empty or truncated EDN input
372
433
  if (!ednText || typeof ednText !== "string" || ednText.trim().length === 0) {
373
434
  throw new Error("EDN input is empty or truncated; refusing to sync to prevent data loss.");
@@ -378,7 +439,14 @@ export async function syncEdnToDisk(ednText, targetDir, opts = {}) {
378
439
  // Guard 2: Refuse 0-page export if targetDir already has existing pages
379
440
  const pageCount = [...gemlFiles.keys()].filter((k) => k.startsWith("pages/") || k.startsWith("journals/")).length;
380
441
  const existingFiles = readGemlFilesFromDisk(targetDir);
381
- const existingPageCount = [...existingFiles.keys()].filter((k) => k.startsWith("pages/") || k.startsWith("journals/")).length;
442
+ // Counting only .geml left this guard blind to exactly what it guards
443
+ // against: a directory that is ALREADY an OG graph holds its pages as
444
+ // Markdown, and readGemlFilesFromDisk does not see one of them.
445
+ const markdownDir = opts.markdownDir ?? null;
446
+ const existingPageCount =
447
+ [...existingFiles.keys()].filter((k) => k.startsWith("pages/") || k.startsWith("journals/")).length +
448
+ countMarkdownPages(targetDir) +
449
+ (markdownDir && resolve(markdownDir) !== resolve(targetDir) ? countMarkdownPages(markdownDir) : 0);
382
450
 
383
451
  if (pageCount === 0 && existingPageCount > 0 && !opts.allowEmptyGraph) {
384
452
  throw new Error(
@@ -399,13 +467,16 @@ export async function syncEdnToDisk(ednText, targetDir, opts = {}) {
399
467
  // round-trips, and `restore` never reads this. The parser library is
400
468
  // injected, so core keeps its single dependency.
401
469
  const markdownWritten = [];
402
- if (opts.markdownDir && opts.lib) {
470
+ const markdownUnmanaged = [];
471
+ if (markdownDir && opts.lib) {
472
+ const mdPrevious = readManifest(markdownDir, MD_MANIFEST_FILE);
473
+ const mdManifest = {};
403
474
  for (const [rel, content] of gemlFiles) {
404
475
  // Only pages and journals are a graph; the index and the ontology carry
405
476
  // machine bookkeeping OG has no page for.
406
477
  if (!rel.startsWith("pages/") && !rel.startsWith("journals/")) continue;
407
478
  const mdRel = rel.replace(/\.geml$/, ".md");
408
- const full = join(opts.markdownDir, mdRel);
479
+ const full = join(markdownDir, mdRel);
409
480
  let md;
410
481
  try {
411
482
  md = normalizeEol(gemlToOgMarkdown(content, opts.lib));
@@ -413,18 +484,46 @@ export async function syncEdnToDisk(ednText, targetDir, opts = {}) {
413
484
  continue; // one unconvertible document must not fail the sync
414
485
  }
415
486
  if (md === "") continue; // nothing OG can hold — write no file
416
- mkdirSync(dirname(full), { recursive: true });
417
- if (!existsSync(full) || readFileSync(full, "utf8") !== md) {
487
+ const onDisk = existsSync(full) ? normalizeEol(readFileSync(full, "utf8")) : null;
488
+ if (onDisk !== null && onDisk !== md && !mdPrevious.files.has(mdRel) && !overwriteUnmanaged) {
489
+ // The GEML tree's rule, and this tree needs it more: `--markdown` takes
490
+ // any directory, so somebody's own pages/*.md is precisely what it
491
+ // lands on. Held, named, and left exactly as they wrote it.
492
+ markdownUnmanaged.push(mdRel);
493
+ continue;
494
+ }
495
+ if (onDisk !== md) {
496
+ mkdirSync(dirname(full), { recursive: true });
418
497
  atomicWriteFileSync(full, md);
419
498
  markdownWritten.push(mdRel);
420
499
  }
500
+ mdManifest[mdRel] = sha256(md);
501
+ }
502
+ // A page the graph stopped exporting keeps its entry while the file is
503
+ // still there: dropping it would make our own past write read as a
504
+ // stranger's on the next run, and the sync would refuse to touch it.
505
+ for (const [mdRel, hash] of mdPrevious.files) {
506
+ if (!(mdRel in mdManifest) && existsSync(join(markdownDir, mdRel))) mdManifest[mdRel] = hash;
507
+ }
508
+ const mdManifestPath = join(markdownDir, MD_MANIFEST_FILE);
509
+ const mdManifestFiles = {};
510
+ for (const mdRel of Object.keys(mdManifest).sort()) mdManifestFiles[mdRel] = mdManifest[mdRel];
511
+ const mdManifestText = JSON.stringify({ version: 2, files: mdManifestFiles }, null, 1) + "\n";
512
+ const mdManifestExists = existsSync(mdManifestPath);
513
+ // Only when it actually changed: this file lives inside someone's OG graph,
514
+ // and rewriting it every poll would have Logseq re-reading it forever.
515
+ if (
516
+ (Object.keys(mdManifestFiles).length > 0 || mdManifestExists) &&
517
+ (!mdManifestExists || readFileSync(mdManifestPath, "utf8") !== mdManifestText)
518
+ ) {
519
+ atomicWriteFileSync(mdManifestPath, mdManifestText);
421
520
  }
422
521
  }
423
522
 
424
523
  let gitResult = null;
425
524
  const pathsModified = [...diffResult.written, ...diffResult.deleted];
426
525
  for (const rel of markdownWritten) {
427
- const abs = join(opts.markdownDir, rel);
526
+ const abs = join(markdownDir, rel);
428
527
  const insideVault = relative(targetDir, abs);
429
528
  if (insideVault && !insideVault.startsWith("..") && !isAbsolute(insideVault)) {
430
529
  pathsModified.push(insideVault);
@@ -439,6 +538,7 @@ export async function syncEdnToDisk(ednText, targetDir, opts = {}) {
439
538
  return {
440
539
  ...diffResult,
441
540
  markdownWritten,
541
+ markdownUnmanaged,
442
542
  gitResult,
443
543
  };
444
544
  }
@@ -1,42 +1,42 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 250" font-family="ui-sans-serif,system-ui,Segoe UI,Helvetica,Arial,sans-serif">
2
- <defs>
3
- <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
4
- <path d="M0,0 L10,5 L0,10 z" fill="#8b949e"/>
5
- </marker>
6
- </defs>
7
- <rect x="0" y="0" width="760" height="250" rx="12" fill="#f6f8fa" stroke="#d0d7de"/>
8
-
9
- <!-- Logseq app box -->
10
- <rect x="24" y="36" width="220" height="130" rx="10" fill="#ffffff" stroke="#a475f9" stroke-width="1.5"/>
11
- <text x="134" y="62" text-anchor="middle" font-size="15" font-weight="700" fill="#57606a">Logseq 2.0 (DB graph)</text>
12
- <rect x="44" y="80" width="180" height="66" rx="8" fill="#f3ecff" stroke="#a475f9"/>
13
- <text x="134" y="104" text-anchor="middle" font-size="13" font-weight="600" fill="#3b2a63">Sync Vault with GEML plugin</text>
14
- <text x="134" y="124" text-anchor="middle" font-size="11.5" fill="#57606a">DB.onChanged → debounce</text>
15
- <text x="134" y="139" text-anchor="middle" font-size="11.5" fill="#57606a">status in toolbar ⇄</text>
16
-
17
- <!-- signal file -->
18
- <rect x="286" y="70" width="160" height="42" rx="8" fill="#fff8c5" stroke="#d4a72c"/>
19
- <text x="366" y="88" text-anchor="middle" font-size="12" font-weight="600" fill="#57606a">dirty-marker file</text>
20
- <text x="366" y="103" text-anchor="middle" font-size="10.5" fill="#7d8590">…/storages/logseq-plugin-sync-vault-with-geml/</text>
21
-
22
- <!-- status file -->
23
- <rect x="286" y="128" width="160" height="34" rx="8" fill="#ddf4ff" stroke="#54aeff"/>
24
- <text x="366" y="149" text-anchor="middle" font-size="12" font-weight="600" fill="#57606a">geml-sync-status.json</text>
25
-
26
- <!-- watcher box -->
27
- <rect x="488" y="36" width="248" height="130" rx="10" fill="#ffffff" stroke="#2da44e" stroke-width="1.5"/>
28
- <text x="612" y="62" text-anchor="middle" font-size="15" font-weight="700" fill="#57606a">logseq-sync watcher (CLI)</text>
29
- <text x="612" y="86" text-anchor="middle" font-size="11.5" fill="#57606a">export via official @logseq/cli</text>
30
- <text x="612" y="103" text-anchor="middle" font-size="11.5" fill="#57606a">writes only files that changed</text>
31
- <text x="612" y="120" text-anchor="middle" font-size="11.5" fill="#57606a">git commit scoped to the vault</text>
32
- <text x="612" y="145" text-anchor="middle" font-size="11.5" font-weight="600" fill="#2da44e">your-vault/pages/*.geml · git</text>
33
-
34
- <!-- arrows -->
35
- <line x1="244" y1="91" x2="284" y2="91" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
36
- <line x1="446" y1="91" x2="486" y2="91" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
37
- <line x1="486" y1="145" x2="446" y2="145" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
38
- <line x1="286" y1="145" x2="246" y2="145" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
39
-
40
- <text x="380" y="196" text-anchor="middle" font-size="12.5" fill="#57606a">The plugin is a doorbell, not the mover: the sandbox has no filesystem and no git,</text>
41
- <text x="380" y="214" text-anchor="middle" font-size="12.5" fill="#57606a">so everything with side effects lives in the watcher — auditable, scoped, outside the app.</text>
42
- </svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 250" font-family="ui-sans-serif,system-ui,Segoe UI,Helvetica,Arial,sans-serif">
2
+ <defs>
3
+ <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
4
+ <path d="M0,0 L10,5 L0,10 z" fill="#8b949e"/>
5
+ </marker>
6
+ </defs>
7
+ <rect x="0" y="0" width="760" height="250" rx="12" fill="#f6f8fa" stroke="#d0d7de"/>
8
+
9
+ <!-- Logseq app box -->
10
+ <rect x="24" y="36" width="220" height="130" rx="10" fill="#ffffff" stroke="#a475f9" stroke-width="1.5"/>
11
+ <text x="134" y="62" text-anchor="middle" font-size="15" font-weight="700" fill="#57606a">Logseq 2.0 (DB graph)</text>
12
+ <rect x="44" y="80" width="180" height="66" rx="8" fill="#f3ecff" stroke="#a475f9"/>
13
+ <text x="134" y="104" text-anchor="middle" font-size="13" font-weight="600" fill="#3b2a63">Sync Vault with GEML plugin</text>
14
+ <text x="134" y="124" text-anchor="middle" font-size="11.5" fill="#57606a">DB.onChanged → debounce</text>
15
+ <text x="134" y="139" text-anchor="middle" font-size="11.5" fill="#57606a">status in toolbar ⇄</text>
16
+
17
+ <!-- signal file -->
18
+ <rect x="286" y="70" width="160" height="42" rx="8" fill="#fff8c5" stroke="#d4a72c"/>
19
+ <text x="366" y="88" text-anchor="middle" font-size="12" font-weight="600" fill="#57606a">dirty-marker file</text>
20
+ <text x="366" y="103" text-anchor="middle" font-size="10.5" fill="#7d8590">…/storages/logseq-plugin-sync-vault-with-geml/</text>
21
+
22
+ <!-- status file -->
23
+ <rect x="286" y="128" width="160" height="34" rx="8" fill="#ddf4ff" stroke="#54aeff"/>
24
+ <text x="366" y="149" text-anchor="middle" font-size="12" font-weight="600" fill="#57606a">geml-sync-status.json</text>
25
+
26
+ <!-- watcher box -->
27
+ <rect x="488" y="36" width="248" height="130" rx="10" fill="#ffffff" stroke="#2da44e" stroke-width="1.5"/>
28
+ <text x="612" y="62" text-anchor="middle" font-size="15" font-weight="700" fill="#57606a">logseq-sync watcher (CLI)</text>
29
+ <text x="612" y="86" text-anchor="middle" font-size="11.5" fill="#57606a">export via official @logseq/cli</text>
30
+ <text x="612" y="103" text-anchor="middle" font-size="11.5" fill="#57606a">writes only files that changed</text>
31
+ <text x="612" y="120" text-anchor="middle" font-size="11.5" fill="#57606a">git commit scoped to the vault</text>
32
+ <text x="612" y="145" text-anchor="middle" font-size="11.5" font-weight="600" fill="#2da44e">your-vault/pages/*.geml · git</text>
33
+
34
+ <!-- arrows -->
35
+ <line x1="244" y1="91" x2="284" y2="91" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
36
+ <line x1="446" y1="91" x2="486" y2="91" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
37
+ <line x1="486" y1="145" x2="446" y2="145" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
38
+ <line x1="286" y1="145" x2="246" y2="145" stroke="#8b949e" stroke-width="1.6" marker-end="url(#arr)"/>
39
+
40
+ <text x="380" y="196" text-anchor="middle" font-size="12.5" fill="#57606a">The plugin is a doorbell, not the mover: the sandbox has no filesystem and no git,</text>
41
+ <text x="380" y="214" text-anchor="middle" font-size="12.5" fill="#57606a">so everything with side effects lives in the watcher — auditable, scoped, outside the app.</text>
42
+ </svg>
package/package.json CHANGED
@@ -1,54 +1,58 @@
1
- {
2
- "name": "@geml/logseq-sync",
3
- "version": "2.0.8",
4
- "publishConfig": {
5
- "access": "public"
6
- },
7
- "description": "Continuously sync a Logseq DB graph to a Git-friendly folder of readable plain-text GEML files — the watcher half of the Sync Vault with GEML plugin. Built on the official @logseq/cli export; writes only files that changed, commits scoped strictly to the vault.",
8
- "type": "module",
9
- "bin": {
10
- "logseq-sync": "watcher/bin/logseq-sync.mjs"
11
- },
12
- "files": [
13
- "core/src",
14
- "watcher/bin",
15
- "docs",
16
- "README.md",
17
- "LICENSE"
18
- ],
19
- "engines": {
20
- "node": ">=22"
21
- },
22
- "keywords": [
23
- "logseq",
24
- "logseq-plugin",
25
- "sync",
26
- "git",
27
- "plain-text",
28
- "geml",
29
- "vault",
30
- "backup"
31
- ],
32
- "repository": {
33
- "type": "git",
34
- "url": "git+https://github.com/geml-spec/geml.git",
35
- "directory": "integrations/logseq"
36
- },
37
- "homepage": "https://github.com/geml-spec/logseq-plugin-sync-vault-with-geml#readme",
38
- "bugs": {
39
- "url": "https://github.com/geml-spec/logseq-plugin-sync-vault-with-geml/issues"
40
- },
41
- "license": "MIT",
42
- "workspaces": [
43
- "plugin"
44
- ],
45
- "scripts": {
46
- "test": "node core/test/roundtrip.test.mjs && node core/test/sync.test.mjs && node core/test/discovery.test.mjs && node watcher/test/cli-sync.test.mjs && node watcher/test/signal-sync.test.mjs && node watcher/test/zero-config.test.mjs && node plugin/test/core.test.mjs",
47
- "sync": "node watcher/bin/logseq-sync.mjs",
48
- "build:plugin": "node plugin/build.mjs"
49
- },
50
- "dependencies": {
51
- "@geml/geml": "^1.8.8",
52
- "edn-data": "^1.2.2"
53
- }
54
- }
1
+ {
2
+ "name": "@geml/logseq-sync",
3
+ "version": "2.0.9",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "Continuously sync a Logseq DB graph to a Git-friendly folder of readable plain-text GEML files — the watcher half of the Sync Vault with GEML plugin. Built on the official @logseq/cli export; writes only files that changed, commits scoped strictly to the vault.",
8
+ "type": "module",
9
+ "bin": {
10
+ "logseq-sync": "watcher/bin/logseq-sync.mjs"
11
+ },
12
+ "files": [
13
+ "core/src",
14
+ "watcher/bin",
15
+ "docs",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "engines": {
20
+ "node": ">=22"
21
+ },
22
+ "keywords": [
23
+ "logseq",
24
+ "logseq-plugin",
25
+ "sync",
26
+ "git",
27
+ "plain-text",
28
+ "geml",
29
+ "vault",
30
+ "backup"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/geml-spec/geml.git",
35
+ "directory": "integrations/logseq"
36
+ },
37
+ "homepage": "https://github.com/geml-spec/logseq-plugin-sync-vault-with-geml#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/geml-spec/logseq-plugin-sync-vault-with-geml/issues"
40
+ },
41
+ "license": "MIT",
42
+ "workspaces": [
43
+ "plugin"
44
+ ],
45
+ "scripts": {
46
+ "test": "node core/test/roundtrip.test.mjs && node core/test/sync.test.mjs && node core/test/discovery.test.mjs && node watcher/test/cli-sync.test.mjs && node watcher/test/signal-sync.test.mjs && node watcher/test/zero-config.test.mjs && node plugin/test/core.test.mjs",
47
+ "sync": "node watcher/bin/logseq-sync.mjs",
48
+ "build:plugin": "node plugin/build.mjs"
49
+ },
50
+ "dependencies": {
51
+ "@geml/geml": "^1.8.8",
52
+ "edn-data": "^1.2.2"
53
+ },
54
+ "overrides": {
55
+ "dompurify": "^3.4.14",
56
+ "lodash-es": "^4.18.1"
57
+ }
58
+ }
@@ -1,51 +1,51 @@
1
- // Create an empty Logseq DB graph WITHOUT the desktop app.
2
- //
3
- // LOGSEQ_CLI_DIR=<dir whose node_modules holds @logseq/cli> \
4
- // node bin/create-graph.mjs <graph-name> (name travels via GEML_GRAPH_NAME —
5
- // nbb loadFile does not surface *command-line-args*)
6
- //
7
- // Why this exists: `@logseq/cli` (0.4.3) can export, import and validate a DB
8
- // graph, but cannot create one — creation lives in the desktop app, and on a
9
- // machine where the app cannot run (permissions, CI) that is a dead end. The
10
- // CLI package VENDORS the whole logseq.db stack though, and its `open-db!`
11
- // creates the sqlite tables on open; the only other thing the app does at
12
- // create time is transact `build-db-initial-data`. So this does exactly those
13
- // two steps, through the CLI's own vendored code — the resulting graph is one
14
- // `logseq list/show/validate` accepts as its own (verified: schema 65.22,
15
- // "Valid!").
16
- import { fileURLToPath, pathToFileURL } from "url";
17
- import { dirname, resolve } from "path";
18
- import { existsSync, readFileSync, copyFileSync, rmSync } from "fs";
19
-
20
- const here = fileURLToPath(dirname(import.meta.url));
21
- const cliDir = process.env.LOGSEQ_CLI_DIR ?? resolve(here, "..");
22
- const CLI = resolve(cliDir, "node_modules", "@logseq", "cli");
23
- if (!existsSync(CLI)) {
24
- console.error(`@logseq/cli not found under ${cliDir}/node_modules — set LOGSEQ_CLI_DIR to a directory where it is installed.`);
25
- console.error("Note: on Node 24 its better-sqlite3 needs an override to >=12.11.1 for a prebuilt binding.");
26
- process.exit(2);
27
- }
28
-
29
- // nbb-logseq is resolved from the CLI's install, not from this package: ESM
30
- // import specifiers resolve relative to THIS file, which would demand a local
31
- // install of a runtime the CLI already carries.
32
- const nbbDir = resolve(cliDir, "node_modules", "@logseq", "nbb-logseq");
33
- const nbbPkg = JSON.parse(readFileSync(resolve(nbbDir, "package.json"), "utf8"));
34
- const entry = typeof nbbPkg.exports === "object"
35
- ? (nbbPkg.exports["."]?.import ?? nbbPkg.exports["."]) : (nbbPkg.main ?? "index.mjs");
36
- const { loadFile, addClassPath } = await import(pathToFileURL(resolve(nbbDir, entry)).href);
37
-
38
- global.__dirname = here;
39
- addClassPath(resolve(CLI, "src"));
40
- addClassPath(resolve(CLI, "vendor/src"));
41
- // nbb resolves the stack's node `require`s (better-sqlite3) relative to the
42
- // LOADED FILE's directory, so the .cljs must sit beside a node_modules that
43
- // has them — copy it into the CLI dir for the duration of the run.
44
- process.env.GEML_GRAPH_NAME = process.argv[2] ?? "geml-spike";
45
- const staged = resolve(cliDir, ".geml-create-graph.cljs");
46
- copyFileSync(resolve(here, "create_graph_headless.cljs"), staged);
47
- try {
48
- await loadFile(staged);
49
- } finally {
50
- rmSync(staged, { force: true });
51
- }
1
+ // Create an empty Logseq DB graph WITHOUT the desktop app.
2
+ //
3
+ // LOGSEQ_CLI_DIR=<dir whose node_modules holds @logseq/cli> \
4
+ // node bin/create-graph.mjs <graph-name> (name travels via GEML_GRAPH_NAME —
5
+ // nbb loadFile does not surface *command-line-args*)
6
+ //
7
+ // Why this exists: `@logseq/cli` (0.4.3) can export, import and validate a DB
8
+ // graph, but cannot create one — creation lives in the desktop app, and on a
9
+ // machine where the app cannot run (permissions, CI) that is a dead end. The
10
+ // CLI package VENDORS the whole logseq.db stack though, and its `open-db!`
11
+ // creates the sqlite tables on open; the only other thing the app does at
12
+ // create time is transact `build-db-initial-data`. So this does exactly those
13
+ // two steps, through the CLI's own vendored code — the resulting graph is one
14
+ // `logseq list/show/validate` accepts as its own (verified: schema 65.22,
15
+ // "Valid!").
16
+ import { fileURLToPath, pathToFileURL } from "url";
17
+ import { dirname, resolve } from "path";
18
+ import { existsSync, readFileSync, copyFileSync, rmSync } from "fs";
19
+
20
+ const here = fileURLToPath(dirname(import.meta.url));
21
+ const cliDir = process.env.LOGSEQ_CLI_DIR ?? resolve(here, "..");
22
+ const CLI = resolve(cliDir, "node_modules", "@logseq", "cli");
23
+ if (!existsSync(CLI)) {
24
+ console.error(`@logseq/cli not found under ${cliDir}/node_modules — set LOGSEQ_CLI_DIR to a directory where it is installed.`);
25
+ console.error("Note: on Node 24 its better-sqlite3 needs an override to >=12.11.1 for a prebuilt binding.");
26
+ process.exit(2);
27
+ }
28
+
29
+ // nbb-logseq is resolved from the CLI's install, not from this package: ESM
30
+ // import specifiers resolve relative to THIS file, which would demand a local
31
+ // install of a runtime the CLI already carries.
32
+ const nbbDir = resolve(cliDir, "node_modules", "@logseq", "nbb-logseq");
33
+ const nbbPkg = JSON.parse(readFileSync(resolve(nbbDir, "package.json"), "utf8"));
34
+ const entry = typeof nbbPkg.exports === "object"
35
+ ? (nbbPkg.exports["."]?.import ?? nbbPkg.exports["."]) : (nbbPkg.main ?? "index.mjs");
36
+ const { loadFile, addClassPath } = await import(pathToFileURL(resolve(nbbDir, entry)).href);
37
+
38
+ global.__dirname = here;
39
+ addClassPath(resolve(CLI, "src"));
40
+ addClassPath(resolve(CLI, "vendor/src"));
41
+ // nbb resolves the stack's node `require`s (better-sqlite3) relative to the
42
+ // LOADED FILE's directory, so the .cljs must sit beside a node_modules that
43
+ // has them — copy it into the CLI dir for the duration of the run.
44
+ process.env.GEML_GRAPH_NAME = process.argv[2] ?? "geml-spike";
45
+ const staged = resolve(cliDir, ".geml-create-graph.cljs");
46
+ copyFileSync(resolve(here, "create_graph_headless.cljs"), staged);
47
+ try {
48
+ await loadFile(staged);
49
+ } finally {
50
+ rmSync(staged, { force: true });
51
+ }
@@ -1,22 +1,22 @@
1
- (ns create-graph-headless
2
- "Create an empty Logseq DB graph without the desktop app: the CLI package
3
- vendors the whole logseq.db stack, and open-db! creates tables on open.
4
- This does exactly what the app's create-graph does at the db level:
5
- mkdir + open + transact build-db-initial-data.
6
-
7
- open-db! creates the DATABASE but refuses a missing DIRECTORY (its error
8
- says only 'Cannot open database because the directory does not exist'),
9
- so the mkdir here is load-bearing."
10
- (:require [logseq.db.common.sqlite-cli :as sqlite-cli]
11
- [logseq.db.sqlite.create-graph :as sqlite-create-graph]
12
- [datascript.core :as d]
13
- ["fs" :as fs]
14
- ["os" :as os]
15
- ["path" :as node-path]))
16
-
17
- (def graph-name (or (aget (.-env js/process) "GEML_GRAPH_NAME") "geml-spike"))
18
- (def graphs-dir (node-path/join (os/homedir) "logseq" "graphs"))
19
- (fs/mkdirSync (node-path/join graphs-dir graph-name) #js {:recursive true})
20
- (def conn (sqlite-cli/open-db! graphs-dir graph-name))
21
- (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
22
- (println "created" (node-path/join graphs-dir graph-name))
1
+ (ns create-graph-headless
2
+ "Create an empty Logseq DB graph without the desktop app: the CLI package
3
+ vendors the whole logseq.db stack, and open-db! creates tables on open.
4
+ This does exactly what the app's create-graph does at the db level:
5
+ mkdir + open + transact build-db-initial-data.
6
+
7
+ open-db! creates the DATABASE but refuses a missing DIRECTORY (its error
8
+ says only 'Cannot open database because the directory does not exist'),
9
+ so the mkdir here is load-bearing."
10
+ (:require [logseq.db.common.sqlite-cli :as sqlite-cli]
11
+ [logseq.db.sqlite.create-graph :as sqlite-create-graph]
12
+ [datascript.core :as d]
13
+ ["fs" :as fs]
14
+ ["os" :as os]
15
+ ["path" :as node-path]))
16
+
17
+ (def graph-name (or (aget (.-env js/process) "GEML_GRAPH_NAME") "geml-spike"))
18
+ (def graphs-dir (node-path/join (os/homedir) "logseq" "graphs"))
19
+ (fs/mkdirSync (node-path/join graphs-dir graph-name) #js {:recursive true})
20
+ (def conn (sqlite-cli/open-db! graphs-dir graph-name))
21
+ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
22
+ (println "created" (node-path/join graphs-dir graph-name))