@geml/logseq-sync 2.0.9 → 2.2.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/LICENSE +27 -28
- package/README.md +396 -377
- package/core/src/bridge.mjs +8 -8
- package/core/src/discovery.mjs +204 -204
- package/core/src/sync-engine.mjs +623 -560
- package/docs/how-it-works.svg +42 -42
- package/package.json +58 -58
- 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 +994 -940
package/docs/how-it-works.svg
CHANGED
|
@@ -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,58 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@geml/logseq-sync",
|
|
3
|
-
"version": "2.0
|
|
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
|
+
{
|
|
2
|
+
"name": "@geml/logseq-sync",
|
|
3
|
+
"version": "2.2.0",
|
|
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))
|
|
@@ -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");
|