obsidian-e2e 0.3.1 → 0.5.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.
@@ -1,728 +0,0 @@
1
- import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
2
- import path, { posix } from "node:path";
3
- import { spawn } from "node:child_process";
4
- import os from "node:os";
5
- import { createHash, randomUUID } from "node:crypto";
6
- //#region src/core/args.ts
7
- function buildCommandArgv(vaultName, command, args = {}) {
8
- const argv = [`vault=${vaultName}`, command];
9
- for (const [key, value] of Object.entries(args)) {
10
- if (value === false || value === null || value === void 0) continue;
11
- if (value === true) {
12
- argv.push(key);
13
- continue;
14
- }
15
- argv.push(`${key}=${String(value)}`);
16
- }
17
- return argv;
18
- }
19
- //#endregion
20
- //#region src/core/internals.ts
21
- const clientInternals = /* @__PURE__ */ new WeakMap();
22
- function attachClientInternals(client, internals) {
23
- clientInternals.set(client, internals);
24
- }
25
- function getClientInternals(client) {
26
- const internals = clientInternals.get(client);
27
- if (!internals) throw new Error("Missing obsidian client internals.");
28
- return internals;
29
- }
30
- function createRestoreManager(readFile) {
31
- const snapshots = /* @__PURE__ */ new Map();
32
- return {
33
- async restoreAll() {
34
- const entries = [...snapshots.entries()].reverse();
35
- for (const [filePath, snapshot] of entries) await restoreSnapshot(filePath, snapshot);
36
- snapshots.clear();
37
- },
38
- async restoreFile(filePath) {
39
- const snapshot = snapshots.get(filePath);
40
- if (!snapshot) return;
41
- await restoreSnapshot(filePath, snapshot);
42
- snapshots.delete(filePath);
43
- },
44
- async snapshotFileOnce(filePath) {
45
- if (snapshots.has(filePath)) return;
46
- try {
47
- snapshots.set(filePath, {
48
- exists: true,
49
- value: await readFile(filePath)
50
- });
51
- } catch (error) {
52
- if (isMissingFileError(error)) {
53
- snapshots.set(filePath, {
54
- exists: false,
55
- value: ""
56
- });
57
- return;
58
- }
59
- throw error;
60
- }
61
- }
62
- };
63
- }
64
- async function restoreSnapshot(filePath, snapshot) {
65
- if (snapshot.exists) {
66
- await writeFile(filePath, snapshot.value, "utf8");
67
- return;
68
- }
69
- await rm(filePath, {
70
- force: true,
71
- recursive: true
72
- });
73
- }
74
- function isMissingFileError(error) {
75
- return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
76
- }
77
- //#endregion
78
- //#region src/vault/json-file.ts
79
- function createJsonFile(filePath, beforeMutate) {
80
- return {
81
- async patch(updater) {
82
- await beforeMutate?.();
83
- const currentValue = await this.read();
84
- const draft = structuredClone(currentValue);
85
- const nextValue = await updater(draft) ?? draft;
86
- await this.write(nextValue);
87
- return nextValue;
88
- },
89
- async read() {
90
- const value = await readFile(filePath, "utf8");
91
- return JSON.parse(value);
92
- },
93
- async write(value) {
94
- await beforeMutate?.();
95
- await mkdir(path.dirname(filePath), { recursive: true });
96
- await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
97
- }
98
- };
99
- }
100
- //#endregion
101
- //#region src/plugin/plugin.ts
102
- function createPluginHandle(client, id) {
103
- async function resolveDataPath() {
104
- const vaultPath = await client.vaultPath();
105
- return path.join(vaultPath, ".obsidian", "plugins", id, "data.json");
106
- }
107
- return {
108
- data() {
109
- return {
110
- async patch(updater) {
111
- const dataPath = await resolveDataPath();
112
- return createJsonFile(dataPath, () => getClientInternals(client).snapshotFileOnce(dataPath)).patch(updater);
113
- },
114
- async read() {
115
- return createJsonFile(await resolveDataPath()).read();
116
- },
117
- async write(value) {
118
- const dataPath = await resolveDataPath();
119
- await createJsonFile(dataPath, () => getClientInternals(client).snapshotFileOnce(dataPath)).write(value);
120
- }
121
- };
122
- },
123
- async dataPath() {
124
- return resolveDataPath();
125
- },
126
- async disable(options = {}) {
127
- await client.exec("plugin:disable", {
128
- filter: options.filter,
129
- id
130
- });
131
- },
132
- async enable(options = {}) {
133
- await client.exec("plugin:enable", {
134
- filter: options.filter,
135
- id
136
- });
137
- },
138
- id,
139
- async isEnabled() {
140
- const output = await client.execText("plugin", { id }, { allowNonZeroExit: true });
141
- return /enabled\s+true/i.test(output);
142
- },
143
- async reload() {
144
- await client.exec("plugin:reload", { id });
145
- },
146
- async restoreData() {
147
- await getClientInternals(client).restoreFile(await resolveDataPath());
148
- }
149
- };
150
- }
151
- //#endregion
152
- //#region src/core/errors.ts
153
- var ObsidianCommandError = class extends Error {
154
- result;
155
- constructor(message, result) {
156
- super(message);
157
- this.name = "ObsidianCommandError";
158
- this.result = result;
159
- }
160
- };
161
- var WaitForTimeoutError = class extends Error {
162
- causeError;
163
- constructor(message, causeError) {
164
- super(message);
165
- this.name = "WaitForTimeoutError";
166
- this.causeError = causeError;
167
- }
168
- };
169
- //#endregion
170
- //#region src/core/transport.ts
171
- const DEFAULT_TIMEOUT_MS$2 = 3e4;
172
- const executeCommand = async ({ allowNonZeroExit = false, argv, bin, cwd, env, timeoutMs = DEFAULT_TIMEOUT_MS$2 }) => {
173
- const child = spawn(bin, argv, {
174
- cwd,
175
- env,
176
- stdio: [
177
- "ignore",
178
- "pipe",
179
- "pipe"
180
- ]
181
- });
182
- const stdoutChunks = [];
183
- const stderrChunks = [];
184
- child.stdout.on("data", (chunk) => {
185
- stdoutChunks.push(Buffer.from(chunk));
186
- });
187
- child.stderr.on("data", (chunk) => {
188
- stderrChunks.push(Buffer.from(chunk));
189
- });
190
- const exitCode = await new Promise((resolve, reject) => {
191
- const timer = setTimeout(() => {
192
- child.kill("SIGTERM");
193
- reject(/* @__PURE__ */ new Error(`Command timed out after ${timeoutMs}ms: ${bin} ${argv.join(" ")}`));
194
- }, timeoutMs);
195
- child.on("error", (error) => {
196
- clearTimeout(timer);
197
- reject(error);
198
- });
199
- child.on("close", (code) => {
200
- clearTimeout(timer);
201
- resolve(code ?? 0);
202
- });
203
- });
204
- const result = {
205
- argv,
206
- command: bin,
207
- exitCode,
208
- stderr: Buffer.concat(stderrChunks).toString("utf8"),
209
- stdout: Buffer.concat(stdoutChunks).toString("utf8")
210
- };
211
- if (exitCode !== 0 && !allowNonZeroExit) throw new ObsidianCommandError(`Obsidian command failed with exit code ${exitCode}: ${bin} ${argv.join(" ")}`, result);
212
- return result;
213
- };
214
- //#endregion
215
- //#region src/core/wait.ts
216
- const DEFAULT_INTERVAL_MS = 100;
217
- const DEFAULT_TIMEOUT_MS$1 = 5e3;
218
- async function waitForValue(fn, options = {}) {
219
- const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
220
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
221
- const startTime = Date.now();
222
- let lastError;
223
- while (Date.now() - startTime <= timeoutMs) {
224
- try {
225
- const result = await fn();
226
- if (result !== false && result !== null && result !== void 0) return result;
227
- } catch (error) {
228
- lastError = error;
229
- }
230
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
231
- }
232
- throw new WaitForTimeoutError(`Timed out waiting for ${options.message ?? "condition"} after ${timeoutMs}ms.`, lastError);
233
- }
234
- //#endregion
235
- //#region src/core/client.ts
236
- function createObsidianClient(options) {
237
- const transport = options.transport ?? executeCommand;
238
- const waitDefaults = {
239
- intervalMs: options.intervalMs,
240
- timeoutMs: options.timeoutMs
241
- };
242
- const restoreManager = createRestoreManager(async (filePath) => {
243
- const { readFile } = await import("node:fs/promises");
244
- return readFile(filePath, "utf8");
245
- });
246
- let cachedVaultPath;
247
- const client = {};
248
- const app = {
249
- async reload(execOptions = {}) {
250
- await client.exec("reload", {}, execOptions);
251
- },
252
- async restart({ readyOptions, waitUntilReady = true, ...execOptions } = {}) {
253
- await client.exec("restart", {}, execOptions);
254
- if (waitUntilReady) await app.waitUntilReady(readyOptions);
255
- },
256
- version(execOptions = {}) {
257
- return client.execText("version", {}, execOptions);
258
- },
259
- async waitUntilReady(waitOptions) {
260
- await client.waitFor(async () => {
261
- try {
262
- await client.vaultPath();
263
- await client.commands();
264
- return true;
265
- } catch {
266
- return false;
267
- }
268
- }, waitOptions);
269
- }
270
- };
271
- Object.assign(client, {
272
- app,
273
- bin: options.bin ?? "obsidian",
274
- dev: {
275
- async dom(options, execOptions = {}) {
276
- const output = await client.execText("dev:dom", {
277
- all: options.all,
278
- attr: options.attr,
279
- css: options.css,
280
- inner: options.inner,
281
- selector: options.selector,
282
- text: options.text,
283
- total: options.total
284
- }, execOptions);
285
- if (options.total) return Number.parseInt(output, 10);
286
- if (options.all) return output ? output.split(/\r?\n/u).filter(Boolean) : [];
287
- return output;
288
- },
289
- async eval(code, execOptions = {}) {
290
- return parseDevEvalOutput(await client.execText("eval", { code }, execOptions));
291
- },
292
- async screenshot(targetPath, execOptions = {}) {
293
- await client.exec("dev:screenshot", { path: targetPath }, execOptions);
294
- return targetPath;
295
- }
296
- },
297
- command(id) {
298
- return {
299
- async exists(commandOptions = {}) {
300
- return (await client.commands({
301
- ...commandOptions,
302
- filter: commandOptions.filter ?? id
303
- })).includes(id);
304
- },
305
- id,
306
- async run(execOptions = {}) {
307
- await client.exec("command", { id }, execOptions);
308
- }
309
- };
310
- },
311
- async commands(commandOptions = {}, execOptions = {}) {
312
- return parseCommandIds(await client.execText("commands", { filter: commandOptions.filter }, execOptions));
313
- },
314
- exec(command, args = {}, execOptions = {}) {
315
- return transport({
316
- ...execOptions,
317
- argv: buildCommandArgv(options.vault, command, args),
318
- bin: this.bin
319
- });
320
- },
321
- async execJson(command, args = {}, execOptions = {}) {
322
- const output = await this.execText(command, args, execOptions);
323
- return JSON.parse(output);
324
- },
325
- async execText(command, args = {}, execOptions = {}) {
326
- return (await this.exec(command, args, execOptions)).stdout.trimEnd();
327
- },
328
- async open(openOptions, execOptions = {}) {
329
- await client.exec("open", {
330
- file: openOptions.file,
331
- newtab: openOptions.newTab,
332
- path: openOptions.path
333
- }, execOptions);
334
- },
335
- async openTab(tabOptions = {}, execOptions = {}) {
336
- await client.exec("tab:open", {
337
- file: tabOptions.file,
338
- group: tabOptions.group,
339
- view: tabOptions.view
340
- }, execOptions);
341
- },
342
- plugin(id) {
343
- return createPluginHandle(this, id);
344
- },
345
- async tabs(tabOptions = {}, execOptions = {}) {
346
- return parseTabs(await client.execText("tabs", { ids: tabOptions.ids ?? true }, execOptions));
347
- },
348
- async vaultPath() {
349
- if (!cachedVaultPath) cachedVaultPath = await this.execText("vault", { info: "path" });
350
- return cachedVaultPath;
351
- },
352
- async verify() {
353
- await transport({
354
- argv: ["--help"],
355
- bin: this.bin
356
- });
357
- await this.vaultPath();
358
- },
359
- vaultName: options.vault,
360
- waitFor(fn, waitOptions) {
361
- return waitForValue(fn, {
362
- ...waitDefaults,
363
- ...waitOptions
364
- });
365
- },
366
- async workspace(workspaceOptions = {}, execOptions = {}) {
367
- return parseWorkspace(await client.execText("workspace", { ids: workspaceOptions.ids ?? true }, execOptions));
368
- }
369
- });
370
- attachClientInternals(client, restoreManager);
371
- return client;
372
- }
373
- function parseCommandIds(output) {
374
- return output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map((line) => line.split(" ", 1)[0]?.trim() ?? "").filter(Boolean);
375
- }
376
- function parseDevEvalOutput(output) {
377
- const normalized = output.startsWith("=> ") ? output.slice(3) : output;
378
- try {
379
- return JSON.parse(normalized);
380
- } catch {
381
- return normalized;
382
- }
383
- }
384
- function parseTabs(output) {
385
- return output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map(parseTabLine);
386
- }
387
- function parseTabLine(line) {
388
- const [descriptor, id] = line.split(" ");
389
- const match = descriptor?.match(/^\[(.+?)\]\s+(.*)$/u);
390
- if (!match) return {
391
- id: id?.trim() || void 0,
392
- title: descriptor?.trim() ?? "",
393
- viewType: "unknown"
394
- };
395
- return {
396
- id: id?.trim() || void 0,
397
- title: match[2],
398
- viewType: match[1]
399
- };
400
- }
401
- function parseWorkspace(output) {
402
- const roots = [];
403
- const stack = [];
404
- for (const rawLine of output.split(/\r?\n/u)) {
405
- if (!rawLine.trim()) continue;
406
- const depth = getWorkspaceDepth(rawLine);
407
- const node = parseWorkspaceNode(rawLine);
408
- while (stack.length > 0 && stack.at(-1).depth >= depth) stack.pop();
409
- const parent = stack.at(-1)?.node;
410
- if (parent) parent.children.push(node);
411
- else roots.push(node);
412
- stack.push({
413
- depth,
414
- node
415
- });
416
- }
417
- return roots;
418
- }
419
- function getWorkspaceDepth(line) {
420
- let depth = 0;
421
- let remainder = line;
422
- while (true) {
423
- if (remainder.startsWith("│ ") || remainder.startsWith(" ") || remainder.startsWith("├── ") || remainder.startsWith("└── ")) {
424
- depth += 1;
425
- remainder = remainder.slice(4);
426
- continue;
427
- }
428
- return depth;
429
- }
430
- }
431
- function parseWorkspaceNode(line) {
432
- let withoutTree = line;
433
- while (true) {
434
- if (withoutTree.startsWith("│ ") || withoutTree.startsWith(" ") || withoutTree.startsWith("├── ") || withoutTree.startsWith("└── ")) {
435
- withoutTree = withoutTree.slice(4);
436
- continue;
437
- }
438
- break;
439
- }
440
- withoutTree = withoutTree.trim();
441
- const idMatch = withoutTree.match(/^(.*?)(?: \(([a-z0-9]+)\))?$/iu);
442
- const content = idMatch?.[1]?.trim() ?? withoutTree;
443
- const id = idMatch?.[2];
444
- const leafMatch = content.match(/^\[(.+?)\]\s+(.*)$/u);
445
- if (leafMatch) return {
446
- children: [],
447
- id,
448
- label: leafMatch[2],
449
- title: leafMatch[2],
450
- viewType: leafMatch[1]
451
- };
452
- return {
453
- children: [],
454
- id,
455
- label: content
456
- };
457
- }
458
- //#endregion
459
- //#region src/fixtures/vault-lock.ts
460
- const DEFAULT_HEARTBEAT_MS = 2e3;
461
- const DEFAULT_STALE_MS = 15e3;
462
- const DEFAULT_TIMEOUT_MS = 6e4;
463
- const DEFAULT_WAIT_INTERVAL_MS = 500;
464
- const DEFAULT_LOCK_ROOT = path.join(os.tmpdir(), "obsidian-e2e-locks");
465
- const LOCK_METADATA_FILE = "lock.json";
466
- const APP_LOCK_KEY = "__obsidianE2ELock";
467
- const heldLocks = /* @__PURE__ */ new Map();
468
- async function acquireVaultRunLock({ heartbeatMs = DEFAULT_HEARTBEAT_MS, lockRoot = DEFAULT_LOCK_ROOT, onBusy = "wait", staleMs = DEFAULT_STALE_MS, timeoutMs = DEFAULT_TIMEOUT_MS, vaultName, vaultPath }) {
469
- const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));
470
- const heldLock = heldLocks.get(lockDir);
471
- if (heldLock) {
472
- heldLock.refs += 1;
473
- return createVaultRunLockHandle(heldLock);
474
- }
475
- const ownerId = randomUUID();
476
- const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);
477
- const metadata = {
478
- acquiredAt: Date.now(),
479
- cwd: process.cwd(),
480
- heartbeatAt: Date.now(),
481
- hostname: os.hostname(),
482
- ownerId,
483
- pid: process.pid,
484
- staleMs,
485
- vaultName,
486
- vaultPath
487
- };
488
- await mkdir(lockRoot, { recursive: true });
489
- const startedAt = Date.now();
490
- while (true) try {
491
- await mkdir(lockDir);
492
- await writeMetadata(metadataPath, metadata);
493
- break;
494
- } catch (error) {
495
- if (!isAlreadyExistsError(error)) throw error;
496
- const currentLock = await inspectVaultRunLock({
497
- lockRoot,
498
- staleMs,
499
- vaultPath
500
- });
501
- if (currentLock && !currentLock.isStale) {
502
- if (onBusy === "fail") throw new Error(formatBusyLockMessage(vaultPath, currentLock));
503
- } else {
504
- await rm(lockDir, {
505
- force: true,
506
- recursive: true
507
- });
508
- continue;
509
- }
510
- if (Date.now() - startedAt >= timeoutMs) throw new Error(currentLock ? `Timed out waiting for shared vault lock: ${formatBusyLockMessage(vaultPath, currentLock)}` : `Timed out waiting for shared vault lock on ${vaultPath}`);
511
- await sleep(Math.min(DEFAULT_WAIT_INTERVAL_MS, heartbeatMs));
512
- }
513
- const heartbeat = setInterval(() => {
514
- metadata.heartbeatAt = Date.now();
515
- writeMetadata(metadataPath, metadata).catch(() => {});
516
- }, heartbeatMs);
517
- heartbeat.unref();
518
- const nextHeldLock = {
519
- heartbeat,
520
- lockDir,
521
- metadata,
522
- metadataPath,
523
- refs: 1
524
- };
525
- heldLocks.set(lockDir, nextHeldLock);
526
- return createVaultRunLockHandle(nextHeldLock);
527
- }
528
- async function clearVaultRunLockMarker(obsidian) {
529
- await obsidian.dev.eval(`delete window.${APP_LOCK_KEY}; delete app.${APP_LOCK_KEY}; "cleared"`, { allowNonZeroExit: true });
530
- }
531
- async function inspectVaultRunLock({ lockRoot = DEFAULT_LOCK_ROOT, staleMs = DEFAULT_STALE_MS, vaultPath }) {
532
- const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));
533
- const metadata = await readLockState(lockDir);
534
- if (!metadata) return null;
535
- return {
536
- heartbeatAgeMs: Date.now() - metadata.heartbeatAt,
537
- isStale: isLockStale(metadata, staleMs),
538
- lockDir,
539
- metadata
540
- };
541
- }
542
- async function readVaultRunLockMarker(obsidian) {
543
- return obsidian.dev.eval(`window.${APP_LOCK_KEY} ?? app.${APP_LOCK_KEY} ?? null`, { allowNonZeroExit: true });
544
- }
545
- function createVaultLockKey(vaultPath) {
546
- return createHash("sha256").update(path.resolve(vaultPath)).digest("hex");
547
- }
548
- function buildSetMarkerCode(metadata) {
549
- return `(() => {
550
- const lock = ${JSON.stringify(metadata)};
551
- window.${APP_LOCK_KEY} = lock;
552
- app.${APP_LOCK_KEY} = lock;
553
- return lock;
554
- })()`;
555
- }
556
- function createVaultRunLockHandle(heldLock) {
557
- return {
558
- get lockDir() {
559
- return heldLock.lockDir;
560
- },
561
- get metadata() {
562
- return heldLock.metadata;
563
- },
564
- async publishMarker(obsidian) {
565
- await obsidian.dev.eval(buildSetMarkerCode(heldLock.metadata));
566
- },
567
- async release() {
568
- if (heldLock.refs > 1) {
569
- heldLock.refs -= 1;
570
- return;
571
- }
572
- heldLocks.delete(heldLock.lockDir);
573
- clearInterval(heldLock.heartbeat);
574
- if ((await readLockState(heldLock.lockDir))?.ownerId !== heldLock.metadata.ownerId) return;
575
- await rm(heldLock.lockDir, {
576
- force: true,
577
- recursive: true
578
- });
579
- }
580
- };
581
- }
582
- async function readLockState(lockDir) {
583
- const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);
584
- try {
585
- return JSON.parse(await readFile(metadataPath, "utf8"));
586
- } catch {
587
- try {
588
- const directoryStat = await stat(lockDir);
589
- return {
590
- acquiredAt: directoryStat.mtimeMs,
591
- cwd: "",
592
- heartbeatAt: directoryStat.mtimeMs,
593
- hostname: "",
594
- ownerId: "",
595
- pid: 0,
596
- staleMs: DEFAULT_STALE_MS,
597
- vaultName: "",
598
- vaultPath: ""
599
- };
600
- } catch {
601
- return null;
602
- }
603
- }
604
- }
605
- function formatBusyLockMessage(vaultPath, state) {
606
- return `vault ${vaultPath} is locked by ${state.metadata.ownerId ? `owner=${state.metadata.ownerId} pid=${state.metadata.pid} cwd=${state.metadata.cwd || "<unknown>"}` : "owner=<unknown>"} ${`heartbeatAgeMs=${state.heartbeatAgeMs} stale=${state.isStale}`}`;
607
- }
608
- function isAlreadyExistsError(error) {
609
- return error instanceof Error && "code" in error && error.code === "EEXIST";
610
- }
611
- function isLockStale(metadata, staleMs) {
612
- return Date.now() - metadata.heartbeatAt > staleMs;
613
- }
614
- async function sleep(durationMs) {
615
- await new Promise((resolve) => setTimeout(resolve, durationMs));
616
- }
617
- async function writeMetadata(metadataPath, metadata) {
618
- await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
619
- }
620
- //#endregion
621
- //#region src/vault/vault.ts
622
- function createVaultApi(options) {
623
- const scopeRoot = normalizeScope(options.root);
624
- return {
625
- async delete(targetPath, deleteOptions = {}) {
626
- await rm(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), {
627
- force: true,
628
- recursive: true
629
- });
630
- if (deleteOptions.permanent === false) return;
631
- },
632
- async exists(targetPath) {
633
- try {
634
- await access(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath));
635
- return true;
636
- } catch {
637
- return false;
638
- }
639
- },
640
- json(targetPath) {
641
- const jsonFile = {
642
- async patch(updater) {
643
- const currentValue = await jsonFile.read();
644
- const draft = structuredClone(currentValue);
645
- const nextValue = await updater(draft) ?? draft;
646
- await jsonFile.write(nextValue);
647
- return nextValue;
648
- },
649
- async read() {
650
- const rawValue = await readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
651
- return JSON.parse(rawValue);
652
- },
653
- async write(value) {
654
- const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
655
- await mkdir(path.dirname(resolvedPath), { recursive: true });
656
- await writeFile(resolvedPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
657
- }
658
- };
659
- return jsonFile;
660
- },
661
- async mkdir(targetPath) {
662
- await mkdir(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), { recursive: true });
663
- },
664
- async read(targetPath) {
665
- return readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
666
- },
667
- async waitForExists(targetPath, waitOptions) {
668
- await options.obsidian.waitFor(async () => await this.exists(targetPath) ? true : false, {
669
- message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to exist`,
670
- ...waitOptions
671
- });
672
- },
673
- async waitForMissing(targetPath, waitOptions) {
674
- await options.obsidian.waitFor(async () => await this.exists(targetPath) ? false : true, {
675
- message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to be removed`,
676
- ...waitOptions
677
- });
678
- },
679
- async write(targetPath, content) {
680
- const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
681
- await mkdir(path.dirname(resolvedPath), { recursive: true });
682
- await writeFile(resolvedPath, content, "utf8");
683
- }
684
- };
685
- }
686
- function normalizeScope(scope) {
687
- if (!scope || scope === ".") return "";
688
- return scope.replace(/^\/+|\/+$/g, "");
689
- }
690
- function resolveVaultPath(scopeRoot, targetPath) {
691
- if (!targetPath || targetPath === ".") return scopeRoot;
692
- return scopeRoot ? posix.join(scopeRoot, targetPath) : posix.normalize(targetPath);
693
- }
694
- async function resolveFilesystemPath(obsidian, scopeRoot, targetPath) {
695
- const vaultPath = await obsidian.vaultPath();
696
- const relativePath = resolveVaultPath(scopeRoot, targetPath).split("/").filter(Boolean);
697
- const resolvedPath = path.resolve(vaultPath, ...relativePath);
698
- const normalizedVaultPath = path.resolve(vaultPath);
699
- if (resolvedPath !== normalizedVaultPath && !resolvedPath.startsWith(`${normalizedVaultPath}${path.sep}`)) throw new Error(`Resolved path escapes the vault root: ${targetPath}`);
700
- return resolvedPath;
701
- }
702
- //#endregion
703
- //#region src/vault/sandbox.ts
704
- async function createSandboxApi(options) {
705
- const root = posix.join(options.sandboxRoot, `${sanitizeSegment(options.testName)}-${randomUUID().slice(0, 8)}`);
706
- const vault = createVaultApi({
707
- obsidian: options.obsidian,
708
- root
709
- });
710
- await vault.mkdir(".");
711
- return {
712
- ...vault,
713
- async cleanup() {
714
- await vault.delete(".", { permanent: true });
715
- },
716
- path(...segments) {
717
- return posix.join(root, ...segments);
718
- },
719
- root
720
- };
721
- }
722
- function sanitizeSegment(value) {
723
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "test";
724
- }
725
- //#endregion
726
- export { inspectVaultRunLock as a, getClientInternals as c, clearVaultRunLockMarker as i, createVaultApi as n, readVaultRunLockMarker as o, acquireVaultRunLock as r, createObsidianClient as s, createSandboxApi as t };
727
-
728
- //# sourceMappingURL=sandbox-Cz3rj_Rn.mjs.map