@geml/logseq-sync 2.0.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.
@@ -0,0 +1,324 @@
1
+ // Core Sync Engine for Logseq GEML
2
+ // Keeps a local folder of .geml files in sync with a Logseq DB graph,
3
+ // ensuring only changed files are written so Git diffs stay clean.
4
+
5
+ import {
6
+ readdirSync,
7
+ readFileSync,
8
+ writeFileSync,
9
+ unlinkSync,
10
+ mkdirSync,
11
+ rmdirSync,
12
+ renameSync,
13
+ existsSync,
14
+ } from "node:fs";
15
+ import { join, dirname, relative, resolve, sep } from "node:path";
16
+ import { execFileSync } from "node:child_process";
17
+ import { randomUUID } from "node:crypto";
18
+ import { ednToGemlFiles, gemlFilesToEdn } from "./mapping.mjs";
19
+
20
+ const MANIFEST_FILE = ".geml-manifest.json";
21
+
22
+ /**
23
+ * Normalize line endings to LF, handling CRLF (\r\n) and lone CR (\r).
24
+ */
25
+ export function normalizeEol(str) {
26
+ return typeof str === "string" ? str.replace(/\r\n?/g, "\n") : str;
27
+ }
28
+
29
+ /**
30
+ * Atomically write a file via a temporary file in the same directory.
31
+ */
32
+ export function atomicWriteFileSync(filePath, content) {
33
+ const dir = dirname(filePath);
34
+ mkdirSync(dir, { recursive: true });
35
+ const tmpPath = join(dir, `.tmp-${Date.now()}-${process.pid}-${randomUUID()}`);
36
+ writeFileSync(tmpPath, content, "utf8");
37
+ renameSync(tmpPath, filePath);
38
+ }
39
+
40
+ /**
41
+ * Remove empty parent directories recursively up to stopDir.
42
+ */
43
+ function cleanEmptyParents(dir, stopDir) {
44
+ let current = resolve(dir);
45
+ const stop = resolve(stopDir);
46
+ while (current && current !== stop && current.startsWith(stop)) {
47
+ try {
48
+ const remaining = readdirSync(current);
49
+ if (remaining.length === 0) {
50
+ rmdirSync(current);
51
+ current = dirname(current);
52
+ } else {
53
+ break;
54
+ }
55
+ } catch {
56
+ break;
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Scan a directory recursively for all .geml files.
63
+ * @param {string} dir Root directory to scan.
64
+ * @param {string} [baseDir] Base directory for computing relative paths.
65
+ * @returns {Map<string, string>} Map of relative path (POSIX style) -> file content (normalized LF).
66
+ */
67
+ export function readGemlFilesFromDisk(dir, baseDir = dir) {
68
+ const files = new Map();
69
+ if (!existsSync(dir)) return files;
70
+
71
+ const entries = readdirSync(dir, { withFileTypes: true });
72
+ for (const entry of entries) {
73
+ const fullPath = join(dir, entry.name);
74
+ if (entry.isDirectory()) {
75
+ // Ignore .git, node_modules, and hidden directories
76
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
77
+ const subFiles = readGemlFilesFromDisk(fullPath, baseDir);
78
+ for (const [rel, content] of subFiles) {
79
+ files.set(rel, content);
80
+ }
81
+ } else if (entry.isFile() && entry.name.endsWith(".geml")) {
82
+ const rel = relative(baseDir, fullPath).split(sep).join("/");
83
+ files.set(rel, normalizeEol(readFileSync(fullPath, "utf8")));
84
+ }
85
+ }
86
+ return files;
87
+ }
88
+
89
+ /**
90
+ * Incrementally sync a Map of GEML files to disk.
91
+ * Only writes files whose content has changed or do not yet exist.
92
+ * Detects files on disk that are absent from the export (e.g. deleted pages or journals),
93
+ * and reports them without destructive deletion by default.
94
+ *
95
+ * Safety note: @logseq/cli 0.4.3 does not include journals in export-edn.
96
+ * Manifest-based orphan tracking ensures user-authored files outside previous syncs are never deleted.
97
+ * Destructive deletion requires explicit `opts.deleteOrphans === true`.
98
+ *
99
+ * @param {Map<string, string>} gemlFiles Map of relative path (POSIX) -> gemlText.
100
+ * @param {string} targetDir Local destination directory.
101
+ * @param {object} [opts]
102
+ * @param {boolean} [opts.deleteOrphans=false] Whether to delete previous-sync .geml files no longer in graph.
103
+ * @returns {{ written: string[], orphaned: string[], unchanged: string[], deleted: string[] }}
104
+ */
105
+ export function writeGemlFilesToDisk(gemlFiles, targetDir, opts = {}) {
106
+ const deleteOrphans = opts.deleteOrphans ?? false;
107
+ const written = [];
108
+ const unchanged = [];
109
+ const orphaned = [];
110
+ const deleted = [];
111
+
112
+ mkdirSync(targetDir, { recursive: true });
113
+ const existingFiles = readGemlFilesFromDisk(targetDir);
114
+
115
+ // Load previous sync manifest to know which files belong to sync vs user-authored files
116
+ const manifestPath = join(targetDir, MANIFEST_FILE);
117
+ let lastManifest = new Set();
118
+ if (existsSync(manifestPath)) {
119
+ try {
120
+ const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
121
+ if (Array.isArray(parsed)) lastManifest = new Set(parsed);
122
+ } catch {}
123
+ }
124
+
125
+ // Write new or updated files atomically with CRLF normalization
126
+ for (const [rel, newContent] of gemlFiles) {
127
+ const fullPath = join(targetDir, rel);
128
+ const normNew = normalizeEol(newContent);
129
+ const existingContent = existingFiles.get(rel);
130
+
131
+ if (existingContent === undefined || existingContent !== normNew) {
132
+ atomicWriteFileSync(fullPath, normNew);
133
+ written.push(rel);
134
+ } else {
135
+ unchanged.push(rel);
136
+ }
137
+ }
138
+
139
+ // Detect files present on disk but absent from current graph export
140
+ for (const [rel] of existingFiles) {
141
+ if (!gemlFiles.has(rel)) {
142
+ orphaned.push(rel);
143
+ // Safe deletion: only delete if explicit AND file was generated by previous sync
144
+ // User-authored files in targetDir not in manifest are NEVER deleted.
145
+ if (deleteOrphans && lastManifest.has(rel)) {
146
+ const fullPath = join(targetDir, rel);
147
+ if (existsSync(fullPath)) {
148
+ unlinkSync(fullPath);
149
+ cleanEmptyParents(dirname(fullPath), targetDir);
150
+ deleted.push(rel);
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ // Save updated manifest of managed sync files:
157
+ // includes all current gemlFiles, plus any existing files on disk that were in lastManifest and not deleted.
158
+ const currentManifest = new Set(gemlFiles.keys());
159
+ for (const rel of lastManifest) {
160
+ if (existingFiles.has(rel) && !deleted.includes(rel)) {
161
+ currentManifest.add(rel);
162
+ }
163
+ }
164
+ atomicWriteFileSync(manifestPath, JSON.stringify([...currentManifest].sort(), null, 1) + "\n");
165
+
166
+ return { written, orphaned, unchanged, deleted };
167
+ }
168
+
169
+ /**
170
+ * Execute Git commands to commit changes scoped strictly to the synced files.
171
+ * Protects parent repository from having unrelated files swept into the commit.
172
+ *
173
+ * @param {string} targetDir Directory where the sync target lives.
174
+ * @param {string} commitMessage Commit message.
175
+ * @param {string[]} pathsToCommit Relative paths within targetDir that were written or deleted.
176
+ * @param {function} [gitRunner] Optional custom git runner `(args) => Promise<{ stdout, stderr, exitCode }>`.
177
+ * @returns {Promise<{ committed: boolean, changes: boolean, output: string }>}
178
+ */
179
+ export async function gitAutoCommit(targetDir, commitMessage = "logseq-geml sync", pathsToCommit = [], gitRunner = null) {
180
+ const defaultRunner = async (args) => {
181
+ try {
182
+ const stdout = execFileSync("git", args, {
183
+ cwd: targetDir,
184
+ encoding: "utf8",
185
+ stdio: ["ignore", "pipe", "pipe"],
186
+ });
187
+ return { stdout, stderr: "", exitCode: 0 };
188
+ } catch (err) {
189
+ return {
190
+ stdout: err.stdout ? String(err.stdout) : "",
191
+ stderr: err.stderr ? String(err.stderr) : err.message,
192
+ exitCode: err.status || 1,
193
+ };
194
+ }
195
+ };
196
+
197
+ const run = gitRunner || defaultRunner;
198
+
199
+ // Check if targetDir is inside a git repository
200
+ const revRes = await run(["rev-parse", "--show-toplevel"]);
201
+ if (revRes.exitCode !== 0) {
202
+ return {
203
+ committed: false,
204
+ changes: false,
205
+ output: `Not a git repository: ${revRes.stderr.trim()}`,
206
+ };
207
+ }
208
+
209
+ if (!pathsToCommit || pathsToCommit.length === 0) {
210
+ return {
211
+ committed: false,
212
+ changes: false,
213
+ output: "No synced paths to commit.",
214
+ };
215
+ }
216
+
217
+ // Always include the manifest file in staged paths
218
+ const allPaths = [...new Set([...pathsToCommit, MANIFEST_FILE])];
219
+
220
+ // Stage ONLY the specified paths (never a bare git add -A)
221
+ // Split into existing files vs deleted files
222
+ const toAdd = allPaths.filter((p) => existsSync(join(targetDir, p)));
223
+ const toRemove = allPaths.filter((p) => !existsSync(join(targetDir, p)));
224
+
225
+ if (toAdd.length > 0) {
226
+ const addRes = await run(["add", "--", ...toAdd]);
227
+ if (addRes.exitCode !== 0) {
228
+ return { committed: false, changes: true, output: `git add failed: ${addRes.stderr}` };
229
+ }
230
+ }
231
+
232
+ if (toRemove.length > 0) {
233
+ const rmRes = await run(["add", "-u", "--", ...toRemove]);
234
+ if (rmRes.exitCode !== 0) {
235
+ return { committed: false, changes: true, output: `git update index failed: ${rmRes.stderr}` };
236
+ }
237
+ }
238
+
239
+ // Verify whether our target paths have staged changes
240
+ const statusRes = await run(["status", "--porcelain", "--", ...allPaths]);
241
+ if (statusRes.exitCode !== 0 || statusRes.stdout.trim().length === 0) {
242
+ return {
243
+ committed: false,
244
+ changes: false,
245
+ output: "No changes in target paths to commit.",
246
+ };
247
+ }
248
+
249
+ // Commit with pathspec: commits ONLY changes matching our synced paths,
250
+ // leaving any other staged or unstaged changes in parent repository untouched!
251
+ const commitRes = await run(["commit", "-m", commitMessage, "--", ...allPaths]);
252
+ if (commitRes.exitCode !== 0) {
253
+ return {
254
+ committed: false,
255
+ changes: true,
256
+ output: `git commit failed: ${commitRes.stderr}`,
257
+ };
258
+ }
259
+
260
+ return {
261
+ committed: true,
262
+ changes: true,
263
+ output: commitRes.stdout.trim(),
264
+ };
265
+ }
266
+
267
+ /**
268
+ * Full Sync Pipeline from EDN string to disk.
269
+ *
270
+ * @param {string} ednText EDN string (from logseq export-edn).
271
+ * @param {string} targetDir Destination folder.
272
+ * @param {object} [opts]
273
+ * @param {boolean} [opts.autoCommit=false]
274
+ * @param {string} [opts.commitMessage]
275
+ * @param {boolean} [opts.allowEmptyGraph=false] Refuse 0-page export over non-empty targetDir unless true.
276
+ * @param {function} [opts.gitRunner]
277
+ * @returns {Promise<{ written: string[], deleted: string[], orphaned: string[], unchanged: string[], gitResult?: any }>}
278
+ */
279
+ export async function syncEdnToDisk(ednText, targetDir, opts = {}) {
280
+ // Guard 1: Refuse empty or truncated EDN input
281
+ if (!ednText || typeof ednText !== "string" || ednText.trim().length === 0) {
282
+ throw new Error("EDN input is empty or truncated; refusing to sync to prevent data loss.");
283
+ }
284
+
285
+ const gemlFiles = ednToGemlFiles(ednText);
286
+
287
+ // Guard 2: Refuse 0-page export if targetDir already has existing pages
288
+ const pageCount = [...gemlFiles.keys()].filter((k) => k.startsWith("pages/") || k.startsWith("journals/")).length;
289
+ const existingFiles = readGemlFilesFromDisk(targetDir);
290
+ const existingPageCount = [...existingFiles.keys()].filter((k) => k.startsWith("pages/") || k.startsWith("journals/")).length;
291
+
292
+ if (pageCount === 0 && existingPageCount > 0 && !opts.allowEmptyGraph) {
293
+ throw new Error(
294
+ `Refusing to sync empty graph (0 pages) over directory with existing pages (${existingPageCount} pages). Pass allowEmptyGraph: true to force.`
295
+ );
296
+ }
297
+
298
+ const diffResult = writeGemlFilesToDisk(gemlFiles, targetDir, opts);
299
+
300
+ let gitResult = null;
301
+ const pathsModified = [...diffResult.written, ...diffResult.deleted];
302
+
303
+ if (opts.autoCommit && pathsModified.length > 0) {
304
+ const msg = opts.commitMessage || `logseq-geml: synced ${diffResult.written.length} modified, ${diffResult.deleted.length} deleted`;
305
+ gitResult = await gitAutoCommit(targetDir, msg, pathsModified, opts.gitRunner);
306
+ }
307
+
308
+ return {
309
+ ...diffResult,
310
+ gitResult,
311
+ };
312
+ }
313
+
314
+ /**
315
+ * Full Sync Pipeline from disk back to EDN string.
316
+ *
317
+ * @param {string} targetDir Local folder containing .geml files.
318
+ * @param {object} lib Parser library containing { parse, addressedUnits, sliceUnit }.
319
+ * @returns {string} EDN string ready for logseq import-edn.
320
+ */
321
+ export function syncDiskToEdn(targetDir, lib) {
322
+ const files = readGemlFilesFromDisk(targetDir);
323
+ return gemlFilesToEdn(files, lib);
324
+ }
@@ -0,0 +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">geml-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 ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@geml/logseq-sync",
3
+ "version": "2.0.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
+ "geml-sync": "watcher/bin/geml-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 watcher/test/cli-sync.test.mjs && node watcher/test/signal-sync.test.mjs && node plugin/test/core.test.mjs",
47
+ "sync": "node watcher/bin/geml-sync.mjs",
48
+ "build:plugin": "node plugin/build.mjs"
49
+ },
50
+ "dependencies": {
51
+ "edn-data": "^1.2.2"
52
+ }
53
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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))