obsidian-e2e 0.1.0 → 0.3.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/README.md CHANGED
@@ -62,6 +62,142 @@ Run Obsidian-backed tests serially. A live Obsidian app and shared vault are
62
62
  not safe to hit from multiple Vitest workers at once, so `fileParallelism: false`
63
63
  and `maxWorkers: 1` should be treated as the default, not as an optimization.
64
64
 
65
+ ## Shared Vault Locking
66
+
67
+ If multiple worktrees or separate test runs all point at the same
68
+ `obsidian vault=dev` vault, you can enable `sharedVaultLock` to serialize access
69
+ across those runs:
70
+
71
+ ```ts
72
+ import { createObsidianTest, createPluginTest } from "obsidian-e2e/vitest";
73
+
74
+ export const test = createObsidianTest({
75
+ vault: "dev",
76
+ sharedVaultLock: true,
77
+ });
78
+
79
+ export const pluginTest = createPluginTest({
80
+ vault: "dev",
81
+ pluginId: "quickadd",
82
+ sharedVaultLock: {
83
+ onBusy: "wait",
84
+ timeoutMs: 60_000,
85
+ },
86
+ });
87
+ ```
88
+
89
+ `sharedVaultLock` is acquired once per worker before that worker starts using
90
+ the shared vault. The authoritative state is a host-side lock directory keyed
91
+ by the resolved vault path. That file-backed lock owns the lease, updates a
92
+ heartbeat, and allows stale-lock takeover after the configured timeout window.
93
+
94
+ For visibility inside the running app, the holder also publishes a best-effort
95
+ marker into the Obsidian process. That marker is not authoritative. The
96
+ filesystem lock is the source of truth, and the app marker is only there to
97
+ help humans understand which run currently owns the vault.
98
+
99
+ For manual lifecycle setups, the same lock helpers are available directly from
100
+ the main package, so `obsidian-e2e/vitest` is not required:
101
+
102
+ ```ts
103
+ import {
104
+ acquireVaultRunLock,
105
+ clearVaultRunLockMarker,
106
+ createObsidianClient,
107
+ type ObsidianClient,
108
+ type VaultRunLock,
109
+ } from "obsidian-e2e";
110
+
111
+ let obsidian: ObsidianClient;
112
+ let lock: VaultRunLock;
113
+
114
+ beforeAll(async () => {
115
+ obsidian = createObsidianClient({ vault: "dev" });
116
+ await obsidian.verify();
117
+
118
+ lock = await acquireVaultRunLock({
119
+ vaultName: "dev",
120
+ vaultPath: await obsidian.vaultPath(),
121
+ });
122
+
123
+ await lock.publishMarker(obsidian);
124
+ });
125
+
126
+ afterAll(async () => {
127
+ await clearVaultRunLockMarker(obsidian);
128
+ await lock.release();
129
+ });
130
+ ```
131
+
132
+ For lock diagnostics, both `obsidian-e2e` and `obsidian-e2e/vitest` export:
133
+
134
+ ```ts
135
+ import { inspectVaultRunLock, readVaultRunLockMarker } from "obsidian-e2e";
136
+
137
+ const state = await inspectVaultRunLock({
138
+ vaultPath: "/absolute/path/to/dev-vault",
139
+ });
140
+
141
+ const marker = await readVaultRunLockMarker(obsidian);
142
+ ```
143
+
144
+ `inspectVaultRunLock()` reads the authoritative host-side lock state and
145
+ returns the current metadata, lock directory, heartbeat age, and stale status.
146
+ `readVaultRunLockMarker()` reads the best-effort marker from the running
147
+ Obsidian app.
148
+
149
+ If you prefer manual `beforeAll` / `afterAll` lifecycle, you can import the
150
+ lock helpers directly from `obsidian-e2e`. You do not need
151
+ `obsidian-e2e/vitest` for that usage:
152
+
153
+ ```ts
154
+ import { afterAll, beforeAll } from "vite-plus/test";
155
+ import {
156
+ acquireVaultRunLock,
157
+ clearVaultRunLockMarker,
158
+ createObsidianClient,
159
+ type VaultRunLock,
160
+ } from "obsidian-e2e";
161
+
162
+ const obsidian = createObsidianClient({ vault: "dev" });
163
+ let vaultLock: VaultRunLock | undefined;
164
+
165
+ beforeAll(async () => {
166
+ await obsidian.verify();
167
+
168
+ vaultLock = await acquireVaultRunLock({
169
+ vaultName: obsidian.vaultName,
170
+ vaultPath: await obsidian.vaultPath(),
171
+ });
172
+
173
+ await vaultLock.publishMarker(obsidian);
174
+ });
175
+
176
+ afterAll(async () => {
177
+ await clearVaultRunLockMarker(obsidian);
178
+ await vaultLock?.release();
179
+ });
180
+ ```
181
+
182
+ Within one worker/process, reacquiring the same shared-vault lock is reentrant:
183
+ the existing lease is reused instead of contending against itself. Across
184
+ different processes or worktrees, contention still serializes access through
185
+ the host-side lock.
186
+
187
+ The lock path is covered by a real multi-process smoke test: one process can
188
+ hold the lease while another waits, and a second process can also take over
189
+ after the original holder dies and its heartbeat goes stale.
190
+
191
+ The fixture layer is also covered the same way: separate `createObsidianTest()`
192
+ runs can contend for the same `sharedVaultLock`, and the smoke path verifies
193
+ that one run waits until the other releases or goes stale. That proves lock
194
+ handoff across process boundaries, not safe parallel mutation inside one vault.
195
+
196
+ This mode prevents collisions between concurrent runs that share one live
197
+ vault, but it does not create true parallel execution inside that vault. It
198
+ serializes access. If your goal is real parallelism, use separate vaults rather
199
+ than one shared `vault: "dev"` target.
200
+
65
201
  ## Writing Tests
66
202
 
67
203
  ```ts
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as WaitForOptions, E as WorkspaceTab, S as VaultApi, T as WorkspaceOptions, _ as PluginHandle, a as DevDomResult, b as SandboxApi, c as JsonFile, d as ObsidianArg, f as ObsidianClient, g as OpenTabOptions, h as OpenFileOptions, i as DevDomQueryOptions, l as JsonFileUpdater, m as ObsidianDevHandle, n as CommandTransport, o as ExecOptions, p as ObsidianCommandHandle, r as CreateObsidianClientOptions, s as ExecResult, t as CommandListOptions, u as ObsidianAppHandle, w as WorkspaceNode, x as TabsOptions, y as RestartAppOptions } from "./types-5UxOZM7r.mjs";
1
+ import { A as ObsidianCommandHandle, B as WorkspaceNode, C as ExecOptions, D as ObsidianAppHandle, E as JsonFileUpdater, F as RestartAppOptions, H as WorkspaceTab, I as SandboxApi, L as TabsOptions, M as OpenFileOptions, N as OpenTabOptions, O as ObsidianArg, P as PluginHandle, R as VaultApi, S as DevDomResult, T as JsonFile, V as WorkspaceOptions, a as acquireVaultRunLock, b as CreateObsidianClientOptions, c as readVaultRunLockMarker, i as VaultRunLockState, j as ObsidianDevHandle, k as ObsidianClient, n as VaultRunLock, o as clearVaultRunLockMarker, r as VaultRunLockMetadata, s as inspectVaultRunLock, t as AcquireVaultRunLockOptions, v as CommandListOptions, w as ExecResult, x as DevDomQueryOptions, y as CommandTransport, z as WaitForOptions } from "./vault-lock-DarzOEzv.mjs";
2
2
 
3
3
  //#region src/core/client.d.ts
4
4
  declare function createObsidianClient(options: CreateObsidianClientOptions): ObsidianClient;
@@ -18,5 +18,5 @@ interface CreateVaultApiOptions {
18
18
  }
19
19
  declare function createVaultApi(options: CreateVaultApiOptions): VaultApi;
20
20
  //#endregion
21
- export { type CommandListOptions, type CommandTransport, type CreateObsidianClientOptions, type DevDomQueryOptions, type DevDomResult, type ExecOptions, type ExecResult, type JsonFile, type JsonFileUpdater, type ObsidianAppHandle, type ObsidianArg, type ObsidianClient, type ObsidianCommandHandle, type ObsidianDevHandle, type OpenFileOptions, type OpenTabOptions, type PluginHandle, type RestartAppOptions, type SandboxApi, type TabsOptions, type VaultApi, type WaitForOptions, type WorkspaceNode, type WorkspaceOptions, type WorkspaceTab, createObsidianClient, createSandboxApi, createVaultApi };
21
+ export { type AcquireVaultRunLockOptions, type CommandListOptions, type CommandTransport, type CreateObsidianClientOptions, type DevDomQueryOptions, type DevDomResult, type ExecOptions, type ExecResult, type JsonFile, type JsonFileUpdater, type ObsidianAppHandle, type ObsidianArg, type ObsidianClient, type ObsidianCommandHandle, type ObsidianDevHandle, type OpenFileOptions, type OpenTabOptions, type PluginHandle, type RestartAppOptions, type SandboxApi, type TabsOptions, type VaultApi, type VaultRunLock, type VaultRunLockMetadata, type VaultRunLockState, type WaitForOptions, type WorkspaceNode, type WorkspaceOptions, type WorkspaceTab, acquireVaultRunLock, clearVaultRunLockMarker, createObsidianClient, createSandboxApi, createVaultApi, inspectVaultRunLock, readVaultRunLockMarker };
22
22
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { n as createVaultApi, r as createObsidianClient, t as createSandboxApi } from "./sandbox-BhesE1S4.mjs";
2
- export { createObsidianClient, createSandboxApi, createVaultApi };
1
+ import { a as inspectVaultRunLock, i as clearVaultRunLockMarker, n as createVaultApi, o as readVaultRunLockMarker, r as acquireVaultRunLock, s as createObsidianClient, t as createSandboxApi } from "./sandbox-Cz3rj_Rn.mjs";
2
+ export { acquireVaultRunLock, clearVaultRunLockMarker, createObsidianClient, createSandboxApi, createVaultApi, inspectVaultRunLock, readVaultRunLockMarker };
@@ -1,7 +1,8 @@
1
- import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
1
+ import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
2
2
  import path, { posix } from "node:path";
3
3
  import { spawn } from "node:child_process";
4
- import { randomUUID } from "node:crypto";
4
+ import os from "node:os";
5
+ import { createHash, randomUUID } from "node:crypto";
5
6
  //#region src/core/args.ts
6
7
  function buildCommandArgv(vaultName, command, args = {}) {
7
8
  const argv = [`vault=${vaultName}`, command];
@@ -167,8 +168,8 @@ var WaitForTimeoutError = class extends Error {
167
168
  };
168
169
  //#endregion
169
170
  //#region src/core/transport.ts
170
- const DEFAULT_TIMEOUT_MS$1 = 3e4;
171
- const executeCommand = async ({ allowNonZeroExit = false, argv, bin, cwd, env, timeoutMs = DEFAULT_TIMEOUT_MS$1 }) => {
171
+ const DEFAULT_TIMEOUT_MS$2 = 3e4;
172
+ const executeCommand = async ({ allowNonZeroExit = false, argv, bin, cwd, env, timeoutMs = DEFAULT_TIMEOUT_MS$2 }) => {
172
173
  const child = spawn(bin, argv, {
173
174
  cwd,
174
175
  env,
@@ -213,10 +214,10 @@ const executeCommand = async ({ allowNonZeroExit = false, argv, bin, cwd, env, t
213
214
  //#endregion
214
215
  //#region src/core/wait.ts
215
216
  const DEFAULT_INTERVAL_MS = 100;
216
- const DEFAULT_TIMEOUT_MS = 5e3;
217
+ const DEFAULT_TIMEOUT_MS$1 = 5e3;
217
218
  async function waitForValue(fn, options = {}) {
218
219
  const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
219
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
220
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
220
221
  const startTime = Date.now();
221
222
  let lastError;
222
223
  while (Date.now() - startTime <= timeoutMs) {
@@ -455,6 +456,168 @@ function parseWorkspaceNode(line) {
455
456
  };
456
457
  }
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
458
621
  //#region src/vault/vault.ts
459
622
  function createVaultApi(options) {
460
623
  const scopeRoot = normalizeScope(options.root);
@@ -560,6 +723,6 @@ function sanitizeSegment(value) {
560
723
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "test";
561
724
  }
562
725
  //#endregion
563
- export { getClientInternals as i, createVaultApi as n, createObsidianClient as r, createSandboxApi as t };
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 };
564
727
 
565
- //# sourceMappingURL=sandbox-BhesE1S4.mjs.map
728
+ //# sourceMappingURL=sandbox-Cz3rj_Rn.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-Cz3rj_Rn.mjs","names":["DEFAULT_TIMEOUT_MS","DEFAULT_TIMEOUT_MS","pathPosix","pathPosix"],"sources":["../src/core/args.ts","../src/core/internals.ts","../src/vault/json-file.ts","../src/plugin/plugin.ts","../src/core/errors.ts","../src/core/transport.ts","../src/core/wait.ts","../src/core/client.ts","../src/fixtures/vault-lock.ts","../src/vault/vault.ts","../src/vault/sandbox.ts"],"sourcesContent":["import type { ObsidianArg } from \"./types\";\n\nexport function buildCommandArgv(\n vaultName: string,\n command: string,\n args: Record<string, ObsidianArg> = {},\n): string[] {\n const argv = [`vault=${vaultName}`, command];\n\n for (const [key, value] of Object.entries(args)) {\n if (value === false || value === null || value === undefined) {\n continue;\n }\n\n if (value === true) {\n argv.push(key);\n continue;\n }\n\n argv.push(`${key}=${String(value)}`);\n }\n\n return argv;\n}\n","import { rm, writeFile } from \"node:fs/promises\";\n\nimport type { ObsidianClient } from \"./types\";\n\ninterface SnapshotEntry {\n exists: boolean;\n value: string;\n}\n\ninterface ClientInternals {\n restoreAll(): Promise<void>;\n restoreFile(filePath: string): Promise<void>;\n snapshotFileOnce(filePath: string): Promise<void>;\n}\n\nconst clientInternals = new WeakMap<ObsidianClient, ClientInternals>();\n\nexport function attachClientInternals(client: ObsidianClient, internals: ClientInternals): void {\n clientInternals.set(client, internals);\n}\n\nexport function getClientInternals(client: ObsidianClient): ClientInternals {\n const internals = clientInternals.get(client);\n\n if (!internals) {\n throw new Error(\"Missing obsidian client internals.\");\n }\n\n return internals;\n}\n\nexport function createRestoreManager(readFile: (filePath: string) => Promise<string>) {\n const snapshots = new Map<string, SnapshotEntry>();\n\n return {\n async restoreAll() {\n const entries = [...snapshots.entries()].reverse();\n\n for (const [filePath, snapshot] of entries) {\n await restoreSnapshot(filePath, snapshot);\n }\n\n snapshots.clear();\n },\n async restoreFile(filePath: string) {\n const snapshot = snapshots.get(filePath);\n\n if (!snapshot) {\n return;\n }\n\n await restoreSnapshot(filePath, snapshot);\n snapshots.delete(filePath);\n },\n async snapshotFileOnce(filePath: string) {\n if (snapshots.has(filePath)) {\n return;\n }\n\n try {\n snapshots.set(filePath, {\n exists: true,\n value: await readFile(filePath),\n });\n } catch (error) {\n if (isMissingFileError(error)) {\n snapshots.set(filePath, {\n exists: false,\n value: \"\",\n });\n return;\n }\n\n throw error;\n }\n },\n };\n}\n\nasync function restoreSnapshot(filePath: string, snapshot: SnapshotEntry): Promise<void> {\n if (snapshot.exists) {\n await writeFile(filePath, snapshot.value, \"utf8\");\n return;\n }\n\n await rm(filePath, { force: true, recursive: true });\n}\n\nfunction isMissingFileError(error: unknown): error is NodeJS.ErrnoException {\n return Boolean(error && typeof error === \"object\" && \"code\" in error && error.code === \"ENOENT\");\n}\n","import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { JsonFile, JsonFileUpdater } from \"../core/types\";\n\nexport function createJsonFile<T = unknown>(\n filePath: string,\n beforeMutate?: () => Promise<void>,\n): JsonFile<T> {\n return {\n async patch(updater: JsonFileUpdater<T>) {\n await beforeMutate?.();\n\n const currentValue = await this.read();\n const draft = structuredClone(currentValue);\n const result = await updater(draft);\n const nextValue = result ?? draft;\n\n await this.write(nextValue);\n\n return nextValue;\n },\n async read() {\n const value = await readFile(filePath, \"utf8\");\n return JSON.parse(value) as T;\n },\n async write(value: T) {\n await beforeMutate?.();\n await mkdir(path.dirname(filePath), { recursive: true });\n await writeFile(filePath, `${JSON.stringify(value, null, 2)}\\n`, \"utf8\");\n },\n };\n}\n","import path from \"node:path\";\n\nimport { getClientInternals } from \"../core/internals\";\nimport type { JsonFile, ObsidianClient, PluginHandle, PluginToggleOptions } from \"../core/types\";\nimport { createJsonFile } from \"../vault/json-file\";\n\nexport function createPluginHandle(client: ObsidianClient, id: string): PluginHandle {\n async function resolveDataPath() {\n const vaultPath = await client.vaultPath();\n return path.join(vaultPath, \".obsidian\", \"plugins\", id, \"data.json\");\n }\n\n return {\n data<T = unknown>(): JsonFile<T> {\n return {\n async patch(updater) {\n const dataPath = await resolveDataPath();\n return createJsonFile<T>(dataPath, () =>\n getClientInternals(client).snapshotFileOnce(dataPath),\n ).patch(updater);\n },\n async read() {\n const dataPath = await resolveDataPath();\n return createJsonFile<T>(dataPath).read();\n },\n async write(value) {\n const dataPath = await resolveDataPath();\n await createJsonFile<T>(dataPath, () =>\n getClientInternals(client).snapshotFileOnce(dataPath),\n ).write(value);\n },\n };\n },\n async dataPath() {\n return resolveDataPath();\n },\n async disable(options: PluginToggleOptions = {}) {\n await client.exec(\"plugin:disable\", {\n filter: options.filter,\n id,\n });\n },\n async enable(options: PluginToggleOptions = {}) {\n await client.exec(\"plugin:enable\", {\n filter: options.filter,\n id,\n });\n },\n id,\n async isEnabled() {\n const output = await client.execText(\"plugin\", { id }, { allowNonZeroExit: true });\n return /enabled\\s+true/i.test(output);\n },\n async reload() {\n await client.exec(\"plugin:reload\", { id });\n },\n async restoreData() {\n await getClientInternals(client).restoreFile(await resolveDataPath());\n },\n };\n}\n","import type { ExecResult } from \"./types\";\n\nexport class ObsidianCommandError extends Error {\n readonly result: ExecResult;\n\n constructor(message: string, result: ExecResult) {\n super(message);\n this.name = \"ObsidianCommandError\";\n this.result = result;\n }\n}\n\nexport class WaitForTimeoutError extends Error {\n readonly causeError?: unknown;\n\n constructor(message: string, causeError?: unknown) {\n super(message);\n this.name = \"WaitForTimeoutError\";\n this.causeError = causeError;\n }\n}\n","import { spawn } from \"node:child_process\";\n\nimport { ObsidianCommandError } from \"./errors\";\nimport type { CommandTransport, ExecuteRequest, ExecResult } from \"./types\";\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nexport const executeCommand: CommandTransport = async ({\n allowNonZeroExit = false,\n argv,\n bin,\n cwd,\n env,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n}: ExecuteRequest): Promise<ExecResult> => {\n const child = spawn(bin, argv, {\n cwd,\n env,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n\n child.stdout.on(\"data\", (chunk) => {\n stdoutChunks.push(Buffer.from(chunk));\n });\n\n child.stderr.on(\"data\", (chunk) => {\n stderrChunks.push(Buffer.from(chunk));\n });\n\n const exitCode = await new Promise<number>((resolve, reject) => {\n const timer = setTimeout(() => {\n child.kill(\"SIGTERM\");\n reject(new Error(`Command timed out after ${timeoutMs}ms: ${bin} ${argv.join(\" \")}`));\n }, timeoutMs);\n\n child.on(\"error\", (error) => {\n clearTimeout(timer);\n reject(error);\n });\n\n child.on(\"close\", (code) => {\n clearTimeout(timer);\n resolve(code ?? 0);\n });\n });\n\n const result: ExecResult = {\n argv,\n command: bin,\n exitCode,\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n };\n\n if (exitCode !== 0 && !allowNonZeroExit) {\n throw new ObsidianCommandError(\n `Obsidian command failed with exit code ${exitCode}: ${bin} ${argv.join(\" \")}`,\n result,\n );\n }\n\n return result;\n};\n","import { WaitForTimeoutError } from \"./errors\";\nimport type { WaitForOptions } from \"./types\";\n\nconst DEFAULT_INTERVAL_MS = 100;\nconst DEFAULT_TIMEOUT_MS = 5_000;\n\nexport async function waitForValue<T>(\n fn: () => Promise<T | false | null | undefined> | T | false | null | undefined,\n options: WaitForOptions = {},\n): Promise<T> {\n const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const startTime = Date.now();\n\n let lastError: unknown;\n\n while (Date.now() - startTime <= timeoutMs) {\n try {\n const result = await fn();\n if (result !== false && result !== null && result !== undefined) {\n return result;\n }\n } catch (error) {\n lastError = error;\n }\n\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n\n const label = options.message ?? \"condition\";\n throw new WaitForTimeoutError(`Timed out waiting for ${label} after ${timeoutMs}ms.`, lastError);\n}\n","import { buildCommandArgv } from \"./args\";\nimport { attachClientInternals, createRestoreManager } from \"./internals\";\nimport { createPluginHandle } from \"../plugin/plugin\";\nimport { executeCommand } from \"./transport\";\nimport type {\n CommandListOptions,\n CreateObsidianClientOptions,\n DevDomQueryOptions,\n DevDomResult,\n ExecOptions,\n ObsidianArg,\n ObsidianAppHandle,\n ObsidianCommandHandle,\n ObsidianClient,\n ObsidianDevHandle,\n OpenFileOptions,\n OpenTabOptions,\n RestartAppOptions,\n TabsOptions,\n WaitForOptions,\n WorkspaceNode,\n WorkspaceOptions,\n WorkspaceTab,\n} from \"./types\";\nimport { waitForValue } from \"./wait\";\n\nexport function createObsidianClient(options: CreateObsidianClientOptions): ObsidianClient {\n const transport = options.transport ?? executeCommand;\n const waitDefaults = {\n intervalMs: options.intervalMs,\n timeoutMs: options.timeoutMs,\n };\n\n const restoreManager = createRestoreManager(async (filePath) => {\n const { readFile } = await import(\"node:fs/promises\");\n return readFile(filePath, \"utf8\");\n });\n\n let cachedVaultPath: string | undefined;\n\n const client = {} as ObsidianClient;\n\n const app: ObsidianAppHandle = {\n async reload(execOptions: ExecOptions = {}) {\n await client.exec(\"reload\", {}, execOptions);\n },\n async restart({\n readyOptions,\n waitUntilReady = true,\n ...execOptions\n }: RestartAppOptions & ExecOptions = {}) {\n await client.exec(\"restart\", {}, execOptions);\n\n if (waitUntilReady) {\n await app.waitUntilReady(readyOptions);\n }\n },\n version(execOptions: ExecOptions = {}) {\n return client.execText(\"version\", {}, execOptions);\n },\n async waitUntilReady(waitOptions?: WaitForOptions) {\n await client.waitFor(async () => {\n try {\n await client.vaultPath();\n await client.commands();\n return true;\n } catch {\n return false;\n }\n }, waitOptions);\n },\n };\n\n const dev: ObsidianDevHandle = {\n async dom(options: DevDomQueryOptions, execOptions: ExecOptions = {}): Promise<DevDomResult> {\n const output = await client.execText(\n \"dev:dom\",\n {\n all: options.all,\n attr: options.attr,\n css: options.css,\n inner: options.inner,\n selector: options.selector,\n text: options.text,\n total: options.total,\n },\n execOptions,\n );\n\n if (options.total) {\n return Number.parseInt(output, 10);\n }\n\n if (options.all) {\n return output ? output.split(/\\r?\\n/u).filter(Boolean) : [];\n }\n\n return output;\n },\n async eval<T = unknown>(code: string, execOptions: ExecOptions = {}) {\n const output = await client.execText(\n \"eval\",\n {\n code,\n },\n execOptions,\n );\n return parseDevEvalOutput<T>(output);\n },\n async screenshot(targetPath: string, execOptions: ExecOptions = {}) {\n await client.exec(\n \"dev:screenshot\",\n {\n path: targetPath,\n },\n execOptions,\n );\n\n return targetPath;\n },\n };\n\n Object.assign(client, {\n app,\n bin: options.bin ?? \"obsidian\",\n dev,\n command(id: string): ObsidianCommandHandle {\n return {\n async exists(commandOptions: CommandListOptions = {}) {\n const commands = await client.commands({\n ...commandOptions,\n filter: commandOptions.filter ?? id,\n });\n\n return commands.includes(id);\n },\n id,\n async run(execOptions: ExecOptions = {}) {\n await client.exec(\"command\", { id }, execOptions);\n },\n };\n },\n async commands(\n commandOptions: CommandListOptions = {},\n execOptions: ExecOptions = {},\n ): Promise<string[]> {\n const output = await client.execText(\n \"commands\",\n {\n filter: commandOptions.filter,\n },\n execOptions,\n );\n return parseCommandIds(output);\n },\n exec(command: string, args: Record<string, ObsidianArg> = {}, execOptions: ExecOptions = {}) {\n return transport({\n ...execOptions,\n argv: buildCommandArgv(options.vault, command, args),\n bin: this.bin,\n });\n },\n async execJson<T = unknown>(\n command: string,\n args: Record<string, ObsidianArg> = {},\n execOptions: ExecOptions = {},\n ) {\n const output = await this.execText(command, args, execOptions);\n return JSON.parse(output) as T;\n },\n async execText(\n command: string,\n args: Record<string, ObsidianArg> = {},\n execOptions: ExecOptions = {},\n ) {\n const result = await this.exec(command, args, execOptions);\n return result.stdout.trimEnd();\n },\n async open(openOptions: OpenFileOptions, execOptions: ExecOptions = {}) {\n await client.exec(\n \"open\",\n {\n file: openOptions.file,\n newtab: openOptions.newTab,\n path: openOptions.path,\n },\n execOptions,\n );\n },\n async openTab(tabOptions: OpenTabOptions = {}, execOptions: ExecOptions = {}) {\n await client.exec(\n \"tab:open\",\n {\n file: tabOptions.file,\n group: tabOptions.group,\n view: tabOptions.view,\n },\n execOptions,\n );\n },\n plugin(id: string) {\n return createPluginHandle(this, id);\n },\n async tabs(\n tabOptions: TabsOptions = {},\n execOptions: ExecOptions = {},\n ): Promise<WorkspaceTab[]> {\n const output = await client.execText(\n \"tabs\",\n {\n ids: tabOptions.ids ?? true,\n },\n execOptions,\n );\n return parseTabs(output);\n },\n async vaultPath() {\n if (!cachedVaultPath) {\n cachedVaultPath = await this.execText(\"vault\", { info: \"path\" });\n }\n\n return cachedVaultPath;\n },\n async verify() {\n await transport({\n argv: [\"--help\"],\n bin: this.bin,\n });\n\n await this.vaultPath();\n },\n vaultName: options.vault,\n waitFor<T>(\n fn: () => Promise<T | false | null | undefined> | T | false | null | undefined,\n waitOptions?: WaitForOptions,\n ) {\n return waitForValue(fn, {\n ...waitDefaults,\n ...waitOptions,\n });\n },\n async workspace(\n workspaceOptions: WorkspaceOptions = {},\n execOptions: ExecOptions = {},\n ): Promise<WorkspaceNode[]> {\n const output = await client.execText(\n \"workspace\",\n {\n ids: workspaceOptions.ids ?? true,\n },\n execOptions,\n );\n return parseWorkspace(output);\n },\n });\n\n attachClientInternals(client, restoreManager);\n\n return client;\n}\n\nfunction parseCommandIds(output: string): string[] {\n return output\n .split(/\\r?\\n/u)\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => line.split(\"\\t\", 1)[0]?.trim() ?? \"\")\n .filter(Boolean);\n}\n\nfunction parseDevEvalOutput<T>(output: string): T {\n const normalized = output.startsWith(\"=> \") ? output.slice(3) : output;\n\n try {\n return JSON.parse(normalized) as T;\n } catch {\n return normalized as T;\n }\n}\n\nfunction parseTabs(output: string): WorkspaceTab[] {\n return output\n .split(/\\r?\\n/u)\n .map((line) => line.trim())\n .filter(Boolean)\n .map(parseTabLine);\n}\n\nfunction parseTabLine(line: string): WorkspaceTab {\n const [descriptor, id] = line.split(\"\\t\");\n const match = descriptor?.match(/^\\[(.+?)\\]\\s+(.*)$/u);\n\n if (!match) {\n return {\n id: id?.trim() || undefined,\n title: descriptor?.trim() ?? \"\",\n viewType: \"unknown\",\n };\n }\n\n return {\n id: id?.trim() || undefined,\n title: match[2]!,\n viewType: match[1]!,\n };\n}\n\nfunction parseWorkspace(output: string): WorkspaceNode[] {\n const roots: WorkspaceNode[] = [];\n const stack: Array<{ depth: number; node: WorkspaceNode }> = [];\n\n for (const rawLine of output.split(/\\r?\\n/u)) {\n if (!rawLine.trim()) {\n continue;\n }\n\n const depth = getWorkspaceDepth(rawLine);\n const node = parseWorkspaceNode(rawLine);\n\n while (stack.length > 0 && stack.at(-1)!.depth >= depth) {\n stack.pop();\n }\n\n const parent = stack.at(-1)?.node;\n\n if (parent) {\n parent.children.push(node);\n } else {\n roots.push(node);\n }\n\n stack.push({ depth, node });\n }\n\n return roots;\n}\n\nfunction getWorkspaceDepth(line: string): number {\n let depth = 0;\n let remainder = line;\n\n while (true) {\n if (\n remainder.startsWith(\"│ \") ||\n remainder.startsWith(\" \") ||\n remainder.startsWith(\"├── \") ||\n remainder.startsWith(\"└── \")\n ) {\n depth += 1;\n remainder = remainder.slice(4);\n continue;\n }\n\n return depth;\n }\n}\n\nfunction parseWorkspaceNode(line: string): WorkspaceNode {\n let withoutTree = line;\n\n while (true) {\n if (\n withoutTree.startsWith(\"│ \") ||\n withoutTree.startsWith(\" \") ||\n withoutTree.startsWith(\"├── \") ||\n withoutTree.startsWith(\"└── \")\n ) {\n withoutTree = withoutTree.slice(4);\n continue;\n }\n\n break;\n }\n\n withoutTree = withoutTree.trim();\n const idMatch = withoutTree.match(/^(.*?)(?: \\(([a-z0-9]+)\\))?$/iu);\n const content = idMatch?.[1]?.trim() ?? withoutTree;\n const id = idMatch?.[2];\n const leafMatch = content.match(/^\\[(.+?)\\]\\s+(.*)$/u);\n\n if (leafMatch) {\n return {\n children: [],\n id,\n label: leafMatch[2]!,\n title: leafMatch[2]!,\n viewType: leafMatch[1]!,\n };\n }\n\n return {\n children: [],\n id,\n label: content,\n };\n}\n","import { mkdir, readFile, rm, stat, writeFile } from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { createHash, randomUUID } from \"node:crypto\";\n\nimport type { ObsidianClient } from \"../core/types\";\nimport type { SharedVaultLockOptions } from \"./types\";\n\nconst DEFAULT_HEARTBEAT_MS = 2_000;\nconst DEFAULT_STALE_MS = 15_000;\nconst DEFAULT_TIMEOUT_MS = 60_000;\nconst DEFAULT_WAIT_INTERVAL_MS = 500;\nconst DEFAULT_LOCK_ROOT = path.join(os.tmpdir(), \"obsidian-e2e-locks\");\nconst LOCK_METADATA_FILE = \"lock.json\";\nconst APP_LOCK_KEY = \"__obsidianE2ELock\";\nconst heldLocks = new Map<string, HeldVaultRunLock>();\n\nexport interface VaultRunLockMetadata {\n acquiredAt: number;\n cwd: string;\n heartbeatAt: number;\n hostname: string;\n ownerId: string;\n pid: number;\n staleMs: number;\n vaultName: string;\n vaultPath: string;\n}\n\nexport interface VaultRunLock {\n readonly lockDir: string;\n readonly metadata: VaultRunLockMetadata;\n\n publishMarker(obsidian: ObsidianClient): Promise<void>;\n release(): Promise<void>;\n}\n\nexport interface VaultRunLockState {\n heartbeatAgeMs: number;\n isStale: boolean;\n lockDir: string;\n metadata: VaultRunLockMetadata;\n}\n\nexport interface AcquireVaultRunLockOptions extends SharedVaultLockOptions {\n vaultName: string;\n vaultPath: string;\n}\n\ninterface HeldVaultRunLock {\n heartbeat: NodeJS.Timeout;\n lockDir: string;\n metadata: VaultRunLockMetadata;\n metadataPath: string;\n refs: number;\n}\n\nexport async function acquireVaultRunLock({\n heartbeatMs = DEFAULT_HEARTBEAT_MS,\n lockRoot = DEFAULT_LOCK_ROOT,\n onBusy = \"wait\",\n staleMs = DEFAULT_STALE_MS,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n vaultName,\n vaultPath,\n}: AcquireVaultRunLockOptions): Promise<VaultRunLock> {\n const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));\n const heldLock = heldLocks.get(lockDir);\n\n if (heldLock) {\n heldLock.refs += 1;\n return createVaultRunLockHandle(heldLock);\n }\n\n const ownerId = randomUUID();\n const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);\n const metadata: VaultRunLockMetadata = {\n acquiredAt: Date.now(),\n cwd: process.cwd(),\n heartbeatAt: Date.now(),\n hostname: os.hostname(),\n ownerId,\n pid: process.pid,\n staleMs,\n vaultName,\n vaultPath,\n };\n\n await mkdir(lockRoot, { recursive: true });\n\n const startedAt = Date.now();\n\n while (true) {\n try {\n await mkdir(lockDir);\n await writeMetadata(metadataPath, metadata);\n break;\n } catch (error) {\n if (!isAlreadyExistsError(error)) {\n throw error;\n }\n\n const currentLock = await inspectVaultRunLock({\n lockRoot,\n staleMs,\n vaultPath,\n });\n\n if (currentLock && !currentLock.isStale) {\n if (onBusy === \"fail\") {\n throw new Error(formatBusyLockMessage(vaultPath, currentLock));\n }\n } else {\n await rm(lockDir, { force: true, recursive: true });\n continue;\n }\n\n if (Date.now() - startedAt >= timeoutMs) {\n throw new Error(\n currentLock\n ? `Timed out waiting for shared vault lock: ${formatBusyLockMessage(vaultPath, currentLock)}`\n : `Timed out waiting for shared vault lock on ${vaultPath}`,\n );\n }\n\n await sleep(Math.min(DEFAULT_WAIT_INTERVAL_MS, heartbeatMs));\n }\n }\n\n const heartbeat = setInterval(() => {\n metadata.heartbeatAt = Date.now();\n void writeMetadata(metadataPath, metadata).catch(() => {});\n }, heartbeatMs);\n heartbeat.unref();\n\n const nextHeldLock: HeldVaultRunLock = {\n heartbeat,\n lockDir,\n metadata,\n metadataPath,\n refs: 1,\n };\n\n heldLocks.set(lockDir, nextHeldLock);\n return createVaultRunLockHandle(nextHeldLock);\n}\n\nexport async function clearVaultRunLockMarker(obsidian: ObsidianClient): Promise<void> {\n await obsidian.dev.eval(`delete window.${APP_LOCK_KEY}; delete app.${APP_LOCK_KEY}; \"cleared\"`, {\n allowNonZeroExit: true,\n });\n}\n\nexport async function inspectVaultRunLock({\n lockRoot = DEFAULT_LOCK_ROOT,\n staleMs = DEFAULT_STALE_MS,\n vaultPath,\n}: Pick<\n AcquireVaultRunLockOptions,\n \"lockRoot\" | \"staleMs\" | \"vaultPath\"\n>): Promise<VaultRunLockState | null> {\n const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));\n const metadata = await readLockState(lockDir);\n\n if (!metadata) {\n return null;\n }\n\n return {\n heartbeatAgeMs: Date.now() - metadata.heartbeatAt,\n isStale: isLockStale(metadata, staleMs),\n lockDir,\n metadata,\n };\n}\n\nexport async function readVaultRunLockMarker(\n obsidian: ObsidianClient,\n): Promise<VaultRunLockMetadata | null> {\n return obsidian.dev.eval<VaultRunLockMetadata | null>(\n `window.${APP_LOCK_KEY} ?? app.${APP_LOCK_KEY} ?? null`,\n { allowNonZeroExit: true },\n );\n}\n\nfunction createVaultLockKey(vaultPath: string): string {\n return createHash(\"sha256\").update(path.resolve(vaultPath)).digest(\"hex\");\n}\n\nfunction buildSetMarkerCode(metadata: VaultRunLockMetadata): string {\n const encodedMetadata = JSON.stringify(metadata);\n return `(() => {\n const lock = ${encodedMetadata};\n window.${APP_LOCK_KEY} = lock;\n app.${APP_LOCK_KEY} = lock;\n return lock;\n })()`;\n}\n\nfunction createVaultRunLockHandle(heldLock: HeldVaultRunLock): VaultRunLock {\n return {\n get lockDir() {\n return heldLock.lockDir;\n },\n get metadata() {\n return heldLock.metadata;\n },\n async publishMarker(obsidian: ObsidianClient) {\n await obsidian.dev.eval(buildSetMarkerCode(heldLock.metadata));\n },\n async release() {\n if (heldLock.refs > 1) {\n heldLock.refs -= 1;\n return;\n }\n\n heldLocks.delete(heldLock.lockDir);\n clearInterval(heldLock.heartbeat);\n\n const currentLock = await readLockState(heldLock.lockDir);\n\n if (currentLock?.ownerId !== heldLock.metadata.ownerId) {\n return;\n }\n\n await rm(heldLock.lockDir, { force: true, recursive: true });\n },\n };\n}\n\nasync function readLockState(lockDir: string): Promise<VaultRunLockMetadata | null> {\n const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);\n\n try {\n return JSON.parse(await readFile(metadataPath, \"utf8\")) as VaultRunLockMetadata;\n } catch {\n try {\n const directoryStat = await stat(lockDir);\n return {\n acquiredAt: directoryStat.mtimeMs,\n cwd: \"\",\n heartbeatAt: directoryStat.mtimeMs,\n hostname: \"\",\n ownerId: \"\",\n pid: 0,\n staleMs: DEFAULT_STALE_MS,\n vaultName: \"\",\n vaultPath: \"\",\n };\n } catch {\n return null;\n }\n }\n}\n\nfunction formatBusyLockMessage(vaultPath: string, state: VaultRunLockState): string {\n const ownerDetails = state.metadata.ownerId\n ? `owner=${state.metadata.ownerId} pid=${state.metadata.pid} cwd=${state.metadata.cwd || \"<unknown>\"}`\n : \"owner=<unknown>\";\n const ageDetails = `heartbeatAgeMs=${state.heartbeatAgeMs} stale=${state.isStale}`;\n\n return `vault ${vaultPath} is locked by ${ownerDetails} ${ageDetails}`;\n}\n\nfunction isAlreadyExistsError(error: unknown): boolean {\n return error instanceof Error && \"code\" in error && error.code === \"EEXIST\";\n}\n\nfunction isLockStale(metadata: VaultRunLockMetadata, staleMs: number): boolean {\n return Date.now() - metadata.heartbeatAt > staleMs;\n}\n\nasync function sleep(durationMs: number): Promise<void> {\n await new Promise((resolve) => setTimeout(resolve, durationMs));\n}\n\nasync function writeMetadata(metadataPath: string, metadata: VaultRunLockMetadata): Promise<void> {\n await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\\n`, \"utf8\");\n}\n","import { access, mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { posix as pathPosix } from \"node:path\";\n\nimport type { DeleteOptions, JsonFile, ObsidianClient, VaultApi } from \"../core/types\";\n\ninterface CreateVaultApiOptions {\n obsidian: ObsidianClient;\n root?: string;\n}\n\nexport function createVaultApi(options: CreateVaultApiOptions): VaultApi {\n const scopeRoot = normalizeScope(options.root);\n\n return {\n async delete(targetPath, deleteOptions: DeleteOptions = {}) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await rm(resolvedPath, {\n force: true,\n recursive: true,\n });\n\n if (deleteOptions.permanent === false) {\n return;\n }\n },\n async exists(targetPath) {\n try {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await access(resolvedPath);\n return true;\n } catch {\n return false;\n }\n },\n json<T = unknown>(targetPath: string) {\n const jsonFile: JsonFile<T> = {\n async patch(updater) {\n const currentValue = await jsonFile.read();\n const draft = structuredClone(currentValue);\n const result = await updater(draft);\n const nextValue = result ?? draft;\n\n await jsonFile.write(nextValue);\n\n return nextValue;\n },\n async read() {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n const rawValue = await readFile(resolvedPath, \"utf8\");\n return JSON.parse(rawValue) as T;\n },\n async write(value) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await mkdir(path.dirname(resolvedPath), { recursive: true });\n await writeFile(resolvedPath, `${JSON.stringify(value, null, 2)}\\n`, \"utf8\");\n },\n };\n\n return jsonFile;\n },\n async mkdir(targetPath) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await mkdir(resolvedPath, { recursive: true });\n },\n async read(targetPath) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n return readFile(resolvedPath, \"utf8\");\n },\n async waitForExists(targetPath, waitOptions) {\n await options.obsidian.waitFor(async () => ((await this.exists(targetPath)) ? true : false), {\n message: `vault path \"${resolveVaultPath(scopeRoot, targetPath)}\" to exist`,\n ...waitOptions,\n });\n },\n async waitForMissing(targetPath, waitOptions) {\n await options.obsidian.waitFor(async () => ((await this.exists(targetPath)) ? false : true), {\n message: `vault path \"${resolveVaultPath(scopeRoot, targetPath)}\" to be removed`,\n ...waitOptions,\n });\n },\n async write(targetPath, content) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await mkdir(path.dirname(resolvedPath), { recursive: true });\n await writeFile(resolvedPath, content, \"utf8\");\n },\n };\n}\n\nfunction normalizeScope(scope?: string): string {\n if (!scope || scope === \".\") {\n return \"\";\n }\n\n return scope.replace(/^\\/+|\\/+$/g, \"\");\n}\n\nfunction resolveVaultPath(scopeRoot: string, targetPath: string): string {\n if (!targetPath || targetPath === \".\") {\n return scopeRoot;\n }\n\n return scopeRoot ? pathPosix.join(scopeRoot, targetPath) : pathPosix.normalize(targetPath);\n}\n\nasync function resolveFilesystemPath(\n obsidian: ObsidianClient,\n scopeRoot: string,\n targetPath: string,\n): Promise<string> {\n const vaultPath = await obsidian.vaultPath();\n const scopedPath = resolveVaultPath(scopeRoot, targetPath);\n const relativePath = scopedPath.split(\"/\").filter(Boolean);\n const resolvedPath = path.resolve(vaultPath, ...relativePath);\n const normalizedVaultPath = path.resolve(vaultPath);\n\n if (\n resolvedPath !== normalizedVaultPath &&\n !resolvedPath.startsWith(`${normalizedVaultPath}${path.sep}`)\n ) {\n throw new Error(`Resolved path escapes the vault root: ${targetPath}`);\n }\n\n return resolvedPath;\n}\n","import { posix as pathPosix } from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\nimport type { ObsidianClient, SandboxApi } from \"../core/types\";\nimport { createVaultApi } from \"./vault\";\n\ninterface CreateSandboxApiOptions {\n obsidian: ObsidianClient;\n sandboxRoot: string;\n testName: string;\n}\n\nexport async function createSandboxApi(options: CreateSandboxApiOptions): Promise<SandboxApi> {\n const root = pathPosix.join(\n options.sandboxRoot,\n `${sanitizeSegment(options.testName)}-${randomUUID().slice(0, 8)}`,\n );\n const vault = createVaultApi({\n obsidian: options.obsidian,\n root,\n });\n\n await vault.mkdir(\".\");\n\n return {\n ...vault,\n async cleanup() {\n await vault.delete(\".\", { permanent: true });\n },\n path(...segments: string[]) {\n return pathPosix.join(root, ...segments);\n },\n root,\n };\n}\n\nfunction sanitizeSegment(value: string): string {\n return (\n value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 80) || \"test\"\n );\n}\n"],"mappings":"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"}
@@ -1,3 +1,5 @@
1
+ import { TestAPI } from "vite-plus/test";
2
+
1
3
  //#region src/core/types.d.ts
2
4
  type ObsidianArg = boolean | number | string | null | undefined;
3
5
  interface ExecOptions {
@@ -148,5 +150,93 @@ interface SandboxApi extends VaultApi {
148
150
  path(...segments: string[]): string;
149
151
  }
150
152
  //#endregion
151
- export { WaitForOptions as C, WorkspaceTab as E, VaultApi as S, WorkspaceOptions as T, PluginHandle as _, DevDomResult as a, SandboxApi as b, JsonFile as c, ObsidianArg as d, ObsidianClient as f, OpenTabOptions as g, OpenFileOptions as h, DevDomQueryOptions as i, JsonFileUpdater as l, ObsidianDevHandle as m, CommandTransport as n, ExecOptions as o, ObsidianCommandHandle as p, CreateObsidianClientOptions as r, ExecResult as s, CommandListOptions as t, ObsidianAppHandle as u, PluginToggleOptions as v, WorkspaceNode as w, TabsOptions as x, RestartAppOptions as y };
152
- //# sourceMappingURL=types-5UxOZM7r.d.mts.map
153
+ //#region src/fixtures/types.d.ts
154
+ interface FailureArtifactOptions {
155
+ activeFile?: boolean;
156
+ dom?: boolean;
157
+ editorText?: boolean;
158
+ screenshot?: boolean;
159
+ tabs?: boolean;
160
+ workspace?: boolean;
161
+ }
162
+ interface SharedVaultLockOptions {
163
+ heartbeatMs?: number;
164
+ lockRoot?: string;
165
+ onBusy?: "fail" | "wait";
166
+ staleMs?: number;
167
+ timeoutMs?: number;
168
+ }
169
+ interface CreateObsidianTestOptions extends CreateObsidianClientOptions {
170
+ artifactsDir?: string;
171
+ captureOnFailure?: boolean | FailureArtifactOptions;
172
+ sharedVaultLock?: boolean | SharedVaultLockOptions;
173
+ sandboxRoot?: string;
174
+ }
175
+ type VaultSeedEntry = string | {
176
+ json: unknown;
177
+ };
178
+ type VaultSeed = Record<string, VaultSeedEntry>;
179
+ interface CreatePluginTestOptions extends CreateObsidianTestOptions {
180
+ pluginFilter?: PluginToggleOptions["filter"];
181
+ pluginId: string;
182
+ seedPluginData?: unknown;
183
+ seedVault?: VaultSeed;
184
+ }
185
+ interface ObsidianFixtures {
186
+ obsidian: ObsidianClient;
187
+ sandbox: SandboxApi;
188
+ vault: VaultApi;
189
+ }
190
+ interface PluginFixtures extends ObsidianFixtures {
191
+ plugin: PluginHandle;
192
+ }
193
+ type ObsidianTest = TestAPI<ObsidianFixtures>;
194
+ type PluginTest = TestAPI<PluginFixtures>;
195
+ //#endregion
196
+ //#region src/fixtures/vault-lock.d.ts
197
+ interface VaultRunLockMetadata {
198
+ acquiredAt: number;
199
+ cwd: string;
200
+ heartbeatAt: number;
201
+ hostname: string;
202
+ ownerId: string;
203
+ pid: number;
204
+ staleMs: number;
205
+ vaultName: string;
206
+ vaultPath: string;
207
+ }
208
+ interface VaultRunLock {
209
+ readonly lockDir: string;
210
+ readonly metadata: VaultRunLockMetadata;
211
+ publishMarker(obsidian: ObsidianClient): Promise<void>;
212
+ release(): Promise<void>;
213
+ }
214
+ interface VaultRunLockState {
215
+ heartbeatAgeMs: number;
216
+ isStale: boolean;
217
+ lockDir: string;
218
+ metadata: VaultRunLockMetadata;
219
+ }
220
+ interface AcquireVaultRunLockOptions extends SharedVaultLockOptions {
221
+ vaultName: string;
222
+ vaultPath: string;
223
+ }
224
+ declare function acquireVaultRunLock({
225
+ heartbeatMs,
226
+ lockRoot,
227
+ onBusy,
228
+ staleMs,
229
+ timeoutMs,
230
+ vaultName,
231
+ vaultPath
232
+ }: AcquireVaultRunLockOptions): Promise<VaultRunLock>;
233
+ declare function clearVaultRunLockMarker(obsidian: ObsidianClient): Promise<void>;
234
+ declare function inspectVaultRunLock({
235
+ lockRoot,
236
+ staleMs,
237
+ vaultPath
238
+ }: Pick<AcquireVaultRunLockOptions, "lockRoot" | "staleMs" | "vaultPath">): Promise<VaultRunLockState | null>;
239
+ declare function readVaultRunLockMarker(obsidian: ObsidianClient): Promise<VaultRunLockMetadata | null>;
240
+ //#endregion
241
+ export { ObsidianCommandHandle as A, WorkspaceNode as B, ExecOptions as C, ObsidianAppHandle as D, JsonFileUpdater as E, RestartAppOptions as F, WorkspaceTab as H, SandboxApi as I, TabsOptions as L, OpenFileOptions as M, OpenTabOptions as N, ObsidianArg as O, PluginHandle as P, VaultApi as R, DevDomResult as S, JsonFile as T, WorkspaceOptions as V, VaultSeedEntry as _, acquireVaultRunLock as a, CreateObsidianClientOptions as b, readVaultRunLockMarker as c, ObsidianFixtures as d, ObsidianTest as f, VaultSeed as g, SharedVaultLockOptions as h, VaultRunLockState as i, ObsidianDevHandle as j, ObsidianClient as k, CreateObsidianTestOptions as l, PluginTest as m, VaultRunLock as n, clearVaultRunLockMarker as o, PluginFixtures as p, VaultRunLockMetadata as r, inspectVaultRunLock as s, AcquireVaultRunLockOptions as t, CreatePluginTestOptions as u, CommandListOptions as v, ExecResult as w, DevDomQueryOptions as x, CommandTransport as y, WaitForOptions as z };
242
+ //# sourceMappingURL=vault-lock-DarzOEzv.d.mts.map
package/dist/vitest.d.mts CHANGED
@@ -1,46 +1,10 @@
1
- import { S as VaultApi, _ as PluginHandle, b as SandboxApi, f as ObsidianClient, r as CreateObsidianClientOptions, v as PluginToggleOptions } from "./types-5UxOZM7r.mjs";
2
- import { TestAPI } from "vite-plus/test";
1
+ import { _ as VaultSeedEntry, a as acquireVaultRunLock, c as readVaultRunLockMarker, d as ObsidianFixtures, f as ObsidianTest, g as VaultSeed, h as SharedVaultLockOptions, i as VaultRunLockState, l as CreateObsidianTestOptions, m as PluginTest, n as VaultRunLock, o as clearVaultRunLockMarker, p as PluginFixtures, r as VaultRunLockMetadata, s as inspectVaultRunLock, t as AcquireVaultRunLockOptions, u as CreatePluginTestOptions } from "./vault-lock-DarzOEzv.mjs";
3
2
 
4
- //#region src/fixtures/types.d.ts
5
- interface FailureArtifactOptions {
6
- activeFile?: boolean;
7
- dom?: boolean;
8
- editorText?: boolean;
9
- screenshot?: boolean;
10
- tabs?: boolean;
11
- workspace?: boolean;
12
- }
13
- interface CreateObsidianTestOptions extends CreateObsidianClientOptions {
14
- artifactsDir?: string;
15
- captureOnFailure?: boolean | FailureArtifactOptions;
16
- sandboxRoot?: string;
17
- }
18
- type VaultSeedEntry = string | {
19
- json: unknown;
20
- };
21
- type VaultSeed = Record<string, VaultSeedEntry>;
22
- interface CreatePluginTestOptions extends CreateObsidianTestOptions {
23
- pluginFilter?: PluginToggleOptions["filter"];
24
- pluginId: string;
25
- seedPluginData?: unknown;
26
- seedVault?: VaultSeed;
27
- }
28
- interface ObsidianFixtures {
29
- obsidian: ObsidianClient;
30
- sandbox: SandboxApi;
31
- vault: VaultApi;
32
- }
33
- interface PluginFixtures extends ObsidianFixtures {
34
- plugin: PluginHandle;
35
- }
36
- type ObsidianTest = TestAPI<ObsidianFixtures>;
37
- type PluginTest = TestAPI<PluginFixtures>;
38
- //#endregion
39
3
  //#region src/fixtures/create-obsidian-test.d.ts
40
4
  declare function createObsidianTest(options: CreateObsidianTestOptions): ObsidianTest;
41
5
  //#endregion
42
6
  //#region src/fixtures/create-plugin-test.d.ts
43
7
  declare function createPluginTest(options: CreatePluginTestOptions): PluginTest;
44
8
  //#endregion
45
- export { type CreateObsidianTestOptions, type CreatePluginTestOptions, type ObsidianFixtures, type ObsidianTest, type PluginFixtures, type PluginTest, type VaultSeed, type VaultSeedEntry, createObsidianTest, createPluginTest };
9
+ export { type AcquireVaultRunLockOptions, type CreateObsidianTestOptions, type CreatePluginTestOptions, type ObsidianFixtures, type ObsidianTest, type PluginFixtures, type PluginTest, type SharedVaultLockOptions, type VaultRunLock, type VaultRunLockMetadata, type VaultRunLockState, type VaultSeed, type VaultSeedEntry, acquireVaultRunLock, clearVaultRunLockMarker, createObsidianTest, createPluginTest, inspectVaultRunLock, readVaultRunLockMarker };
46
10
  //# sourceMappingURL=vitest.d.mts.map
package/dist/vitest.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as getClientInternals, n as createVaultApi, r as createObsidianClient, t as createSandboxApi } from "./sandbox-BhesE1S4.mjs";
1
+ import { a as inspectVaultRunLock, c as getClientInternals, i as clearVaultRunLockMarker, n as createVaultApi, o as readVaultRunLockMarker, r as acquireVaultRunLock, s as createObsidianClient, t as createSandboxApi } from "./sandbox-Cz3rj_Rn.mjs";
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { test } from "vite-plus/test";
@@ -98,9 +98,32 @@ function sanitizeForPath(value) {
98
98
  function createBaseFixtures(options, fixtureOptions = {}) {
99
99
  const createVault = fixtureOptions.createVault ?? ((obsidian) => createVaultApi({ obsidian }));
100
100
  return {
101
- obsidian: async ({ onTestFailed, task }, use) => {
101
+ _vaultLock: [async ({}, use) => {
102
+ if (!options.sharedVaultLock) {
103
+ await use(null);
104
+ return;
105
+ }
106
+ const lockClient = createObsidianClient(options);
107
+ await lockClient.verify();
108
+ const vaultLock = await acquireVaultRunLock({
109
+ ...options.sharedVaultLock === true ? {} : options.sharedVaultLock,
110
+ vaultName: options.vault,
111
+ vaultPath: await lockClient.vaultPath()
112
+ });
113
+ await vaultLock.publishMarker(lockClient);
114
+ try {
115
+ await use(vaultLock);
116
+ } finally {
117
+ try {
118
+ await clearVaultRunLockMarker(lockClient);
119
+ } catch {}
120
+ await vaultLock.release();
121
+ }
122
+ }, { scope: "worker" }],
123
+ obsidian: async ({ _vaultLock, onTestFailed, task }, use) => {
102
124
  const obsidian = createObsidianClient(options);
103
125
  await obsidian.verify();
126
+ if (_vaultLock) await _vaultLock.publishMarker(obsidian);
104
127
  registerFailureArtifacts({
105
128
  onTestFailed,
106
129
  task
@@ -136,7 +159,7 @@ function createObsidianTest(options) {
136
159
  //#endregion
137
160
  //#region src/fixtures/create-plugin-test.ts
138
161
  function createPluginTest(options) {
139
- return test.extend({
162
+ const fixtures = {
140
163
  ...createBaseFixtures(options, { async createVault(obsidian) {
141
164
  if (options.seedVault) await applyVaultSeed(obsidian, options.seedVault);
142
165
  return createVaultApi({ obsidian });
@@ -156,7 +179,8 @@ function createPluginTest(options) {
156
179
  if (!wasEnabled) await plugin.disable({ filter: options.pluginFilter });
157
180
  }
158
181
  }
159
- });
182
+ };
183
+ return test.extend(fixtures);
160
184
  }
161
185
  async function applyVaultSeed(obsidian, seedVault) {
162
186
  const vaultRoot = await obsidian.vaultPath();
@@ -177,6 +201,6 @@ async function writeSeedValue(resolvedPath, value) {
177
201
  await writeFile(resolvedPath, `${JSON.stringify(value.json, null, 2)}\n`, "utf8");
178
202
  }
179
203
  //#endregion
180
- export { createObsidianTest, createPluginTest };
204
+ export { acquireVaultRunLock, clearVaultRunLockMarker, createObsidianTest, createPluginTest, inspectVaultRunLock, readVaultRunLockMarker };
181
205
 
182
206
  //# sourceMappingURL=vitest.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"vitest.mjs","names":["base","base"],"sources":["../src/fixtures/failure-artifacts.ts","../src/fixtures/base-fixtures.ts","../src/fixtures/create-obsidian-test.ts","../src/fixtures/create-plugin-test.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { TestContext } from \"vite-plus/test\";\n\nimport type { ObsidianClient, PluginHandle } from \"../core/types\";\nimport type { CreateObsidianTestOptions, FailureArtifactOptions } from \"./types\";\n\nconst DEFAULT_ARTIFACTS_DIR = \".obsidian-e2e-artifacts\";\n\ninterface FailureArtifactConfig {\n artifactsDir: string;\n capture: Required<FailureArtifactOptions>;\n enabled: boolean;\n}\n\nexport function getFailureArtifactConfig(\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): FailureArtifactConfig {\n if (!options.captureOnFailure) {\n return {\n artifactsDir: path.resolve(options.artifactsDir ?? DEFAULT_ARTIFACTS_DIR),\n capture: {\n activeFile: true,\n dom: true,\n editorText: true,\n screenshot: true,\n tabs: true,\n workspace: true,\n },\n enabled: false,\n };\n }\n\n const overrides = options.captureOnFailure === true ? {} : options.captureOnFailure;\n\n return {\n artifactsDir: path.resolve(options.artifactsDir ?? DEFAULT_ARTIFACTS_DIR),\n capture: {\n activeFile: overrides.activeFile ?? true,\n dom: overrides.dom ?? true,\n editorText: overrides.editorText ?? true,\n screenshot: overrides.screenshot ?? true,\n tabs: overrides.tabs ?? true,\n workspace: overrides.workspace ?? true,\n },\n enabled: true,\n };\n}\n\nexport function getFailureArtifactDirectory(\n artifactsDir: string,\n task: Pick<TestContext[\"task\"], \"id\" | \"name\">,\n): string {\n const suffix = task.id.split(\"_\").at(-1) ?? \"test\";\n return path.join(artifactsDir, `${sanitizeForPath(task.name)}-${suffix}`);\n}\n\nexport function registerFailureArtifacts(\n context: Pick<TestContext, \"onTestFailed\" | \"task\">,\n obsidian: ObsidianClient,\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): void {\n const config = getFailureArtifactConfig(options);\n\n if (!config.enabled) {\n return;\n }\n\n context.onTestFailed(async () => {\n const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, context.task);\n await mkdir(artifactDirectory, { recursive: true });\n\n await Promise.all([\n captureJsonArtifact(\n artifactDirectory,\n \"active-file.json\",\n config.capture.activeFile,\n async () => ({\n activeFile: await obsidian.dev.eval<string | null>(\n \"app.workspace.getActiveFile()?.path ?? null\",\n ),\n }),\n ),\n captureTextArtifact(artifactDirectory, \"dom.txt\", config.capture.dom, async () =>\n String(\n await obsidian.dev.dom({\n inner: true,\n selector: \".workspace\",\n }),\n ),\n ),\n captureJsonArtifact(\n artifactDirectory,\n \"editor.json\",\n config.capture.editorText,\n async () => ({\n text: await obsidian.dev.eval<string | null>(\n \"app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null\",\n ),\n }),\n ),\n captureScreenshotArtifact(artifactDirectory, config.capture.screenshot, obsidian),\n captureJsonArtifact(artifactDirectory, \"tabs.json\", config.capture.tabs, () =>\n obsidian.tabs(),\n ),\n captureJsonArtifact(artifactDirectory, \"workspace.json\", config.capture.workspace, () =>\n obsidian.workspace(),\n ),\n ]);\n });\n}\n\nexport function registerPluginFailureArtifacts(\n context: Pick<TestContext, \"onTestFailed\" | \"task\">,\n plugin: PluginHandle,\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): void {\n const config = getFailureArtifactConfig(options);\n\n if (!config.enabled) {\n return;\n }\n\n context.onTestFailed(async () => {\n const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, context.task);\n await mkdir(artifactDirectory, { recursive: true });\n await captureJsonArtifact(artifactDirectory, `${plugin.id}-data.json`, true, () =>\n plugin.data().read(),\n );\n });\n}\n\nasync function captureJsonArtifact(\n artifactDirectory: string,\n filename: string,\n enabled: boolean,\n readValue: () => Promise<unknown>,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n try {\n const value = await readValue();\n await writeFile(\n path.join(artifactDirectory, filename),\n `${JSON.stringify(value, null, 2)}\\n`,\n \"utf8\",\n );\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, `${filename}.error.txt`),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nasync function captureScreenshotArtifact(\n artifactDirectory: string,\n enabled: boolean,\n obsidian: ObsidianClient,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n const screenshotPath = path.join(artifactDirectory, \"screenshot.png\");\n\n try {\n await obsidian.dev.screenshot(screenshotPath);\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, \"screenshot.error.txt\"),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nasync function captureTextArtifact(\n artifactDirectory: string,\n filename: string,\n enabled: boolean,\n readValue: () => Promise<string>,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n try {\n await writeFile(path.join(artifactDirectory, filename), await readValue(), \"utf8\");\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, `${filename}.error.txt`),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nfunction formatArtifactError(error: unknown): string {\n return error instanceof Error ? `${error.name}: ${error.message}\\n` : `${String(error)}\\n`;\n}\n\nfunction sanitizeForPath(value: string): string {\n return (\n value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60) || \"test\"\n );\n}\n","import type { TestContext } from \"vite-plus/test\";\n\nimport { createObsidianClient } from \"../core/client\";\nimport { getClientInternals } from \"../core/internals\";\nimport type { ObsidianClient, VaultApi } from \"../core/types\";\nimport { createSandboxApi } from \"../vault/sandbox\";\nimport { createVaultApi } from \"../vault/vault\";\nimport { registerFailureArtifacts } from \"./failure-artifacts\";\nimport type { CreateObsidianTestOptions } from \"./types\";\n\nexport const DEFAULT_SANDBOX_ROOT = \"__obsidian_e2e__\";\n\ninterface BaseFixtureOptions {\n createVault?: (obsidian: ObsidianClient) => Promise<VaultApi> | VaultApi;\n}\n\nexport function createBaseFixtures(\n options: CreateObsidianTestOptions,\n fixtureOptions: BaseFixtureOptions = {},\n) {\n const createVault =\n fixtureOptions.createVault ?? ((obsidian: ObsidianClient) => createVaultApi({ obsidian }));\n\n return {\n // oxlint-disable-next-line no-empty-pattern\n obsidian: async (\n { onTestFailed, task }: Pick<TestContext, \"onTestFailed\" | \"task\">,\n use: (obsidian: ObsidianClient) => Promise<void>,\n ) => {\n const obsidian = createObsidianClient(options);\n\n await obsidian.verify();\n registerFailureArtifacts({ onTestFailed, task }, obsidian, options);\n\n try {\n await use(obsidian);\n } finally {\n await getClientInternals(obsidian).restoreAll();\n }\n },\n sandbox: async (\n { obsidian }: { obsidian: ObsidianClient },\n use: (sandbox: Awaited<ReturnType<typeof createSandboxApi>>) => Promise<void>,\n ) => {\n const sandbox = await createSandboxApi({\n obsidian,\n sandboxRoot: options.sandboxRoot ?? DEFAULT_SANDBOX_ROOT,\n testName: \"test\",\n });\n\n try {\n await use(sandbox);\n } finally {\n await sandbox.cleanup();\n }\n },\n vault: async (\n { obsidian }: { obsidian: ObsidianClient },\n use: (vault: VaultApi) => Promise<void>,\n ) => {\n await use(await createVault(obsidian));\n },\n };\n}\n","import { test as base } from \"vite-plus/test\";\n\nimport { createBaseFixtures } from \"./base-fixtures\";\nimport type { CreateObsidianTestOptions, ObsidianFixtures, ObsidianTest } from \"./types\";\n\nexport function createObsidianTest(options: CreateObsidianTestOptions): ObsidianTest {\n return base.extend<ObsidianFixtures>(createBaseFixtures(options));\n}\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { test as base } from \"vite-plus/test\";\nimport type { TestContext } from \"vite-plus/test\";\n\nimport { getClientInternals } from \"../core/internals\";\nimport type { ObsidianClient } from \"../core/types\";\nimport { createVaultApi } from \"../vault/vault\";\nimport { createBaseFixtures } from \"./base-fixtures\";\nimport { registerPluginFailureArtifacts } from \"./failure-artifacts\";\nimport type {\n CreatePluginTestOptions,\n PluginFixtures,\n PluginTest,\n VaultSeed,\n VaultSeedEntry,\n} from \"./types\";\n\nexport function createPluginTest(options: CreatePluginTestOptions): PluginTest {\n return base.extend<PluginFixtures>({\n ...createBaseFixtures(options, {\n async createVault(obsidian) {\n if (options.seedVault) {\n await applyVaultSeed(obsidian, options.seedVault);\n }\n\n return createVaultApi({ obsidian });\n },\n }),\n plugin: async (\n {\n obsidian,\n onTestFailed,\n task,\n }: Pick<PluginFixtures & TestContext, \"obsidian\" | \"onTestFailed\" | \"task\">,\n use,\n ) => {\n const plugin = obsidian.plugin(options.pluginId);\n const wasEnabled = await plugin.isEnabled();\n\n if (!wasEnabled) {\n await plugin.enable({ filter: options.pluginFilter });\n }\n\n if (options.seedPluginData !== undefined) {\n await plugin.data().write(options.seedPluginData);\n }\n\n registerPluginFailureArtifacts({ onTestFailed, task }, plugin, options);\n\n try {\n await use(plugin);\n } finally {\n if (!wasEnabled) {\n await plugin.disable({ filter: options.pluginFilter });\n }\n }\n },\n });\n}\n\nasync function applyVaultSeed(obsidian: ObsidianClient, seedVault: VaultSeed): Promise<void> {\n const vaultRoot = await obsidian.vaultPath();\n\n for (const [targetPath, value] of Object.entries(seedVault)) {\n const resolvedPath = path.resolve(vaultRoot, ...targetPath.split(\"/\").filter(Boolean));\n const normalizedVaultRoot = path.resolve(vaultRoot);\n\n if (\n resolvedPath !== normalizedVaultRoot &&\n !resolvedPath.startsWith(`${normalizedVaultRoot}${path.sep}`)\n ) {\n throw new Error(`Seed path escapes the vault root: ${targetPath}`);\n }\n\n await getClientInternals(obsidian).snapshotFileOnce(resolvedPath);\n await mkdir(path.dirname(resolvedPath), { recursive: true });\n await writeSeedValue(resolvedPath, value);\n }\n}\n\nasync function writeSeedValue(resolvedPath: string, value: VaultSeedEntry): Promise<void> {\n if (typeof value === \"string\") {\n await writeFile(resolvedPath, value, \"utf8\");\n return;\n }\n\n await writeFile(resolvedPath, `${JSON.stringify(value.json, null, 2)}\\n`, \"utf8\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"vitest.mjs","names":["base","base"],"sources":["../src/fixtures/failure-artifacts.ts","../src/fixtures/base-fixtures.ts","../src/fixtures/create-obsidian-test.ts","../src/fixtures/create-plugin-test.ts"],"sourcesContent":["import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { TestContext } from \"vite-plus/test\";\n\nimport type { ObsidianClient, PluginHandle } from \"../core/types\";\nimport type { CreateObsidianTestOptions, FailureArtifactOptions } from \"./types\";\n\nconst DEFAULT_ARTIFACTS_DIR = \".obsidian-e2e-artifacts\";\n\ninterface FailureArtifactConfig {\n artifactsDir: string;\n capture: Required<FailureArtifactOptions>;\n enabled: boolean;\n}\n\nexport function getFailureArtifactConfig(\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): FailureArtifactConfig {\n if (!options.captureOnFailure) {\n return {\n artifactsDir: path.resolve(options.artifactsDir ?? DEFAULT_ARTIFACTS_DIR),\n capture: {\n activeFile: true,\n dom: true,\n editorText: true,\n screenshot: true,\n tabs: true,\n workspace: true,\n },\n enabled: false,\n };\n }\n\n const overrides = options.captureOnFailure === true ? {} : options.captureOnFailure;\n\n return {\n artifactsDir: path.resolve(options.artifactsDir ?? DEFAULT_ARTIFACTS_DIR),\n capture: {\n activeFile: overrides.activeFile ?? true,\n dom: overrides.dom ?? true,\n editorText: overrides.editorText ?? true,\n screenshot: overrides.screenshot ?? true,\n tabs: overrides.tabs ?? true,\n workspace: overrides.workspace ?? true,\n },\n enabled: true,\n };\n}\n\nexport function getFailureArtifactDirectory(\n artifactsDir: string,\n task: Pick<TestContext[\"task\"], \"id\" | \"name\">,\n): string {\n const suffix = task.id.split(\"_\").at(-1) ?? \"test\";\n return path.join(artifactsDir, `${sanitizeForPath(task.name)}-${suffix}`);\n}\n\nexport function registerFailureArtifacts(\n context: Pick<TestContext, \"onTestFailed\" | \"task\">,\n obsidian: ObsidianClient,\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): void {\n const config = getFailureArtifactConfig(options);\n\n if (!config.enabled) {\n return;\n }\n\n context.onTestFailed(async () => {\n const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, context.task);\n await mkdir(artifactDirectory, { recursive: true });\n\n await Promise.all([\n captureJsonArtifact(\n artifactDirectory,\n \"active-file.json\",\n config.capture.activeFile,\n async () => ({\n activeFile: await obsidian.dev.eval<string | null>(\n \"app.workspace.getActiveFile()?.path ?? null\",\n ),\n }),\n ),\n captureTextArtifact(artifactDirectory, \"dom.txt\", config.capture.dom, async () =>\n String(\n await obsidian.dev.dom({\n inner: true,\n selector: \".workspace\",\n }),\n ),\n ),\n captureJsonArtifact(\n artifactDirectory,\n \"editor.json\",\n config.capture.editorText,\n async () => ({\n text: await obsidian.dev.eval<string | null>(\n \"app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null\",\n ),\n }),\n ),\n captureScreenshotArtifact(artifactDirectory, config.capture.screenshot, obsidian),\n captureJsonArtifact(artifactDirectory, \"tabs.json\", config.capture.tabs, () =>\n obsidian.tabs(),\n ),\n captureJsonArtifact(artifactDirectory, \"workspace.json\", config.capture.workspace, () =>\n obsidian.workspace(),\n ),\n ]);\n });\n}\n\nexport function registerPluginFailureArtifacts(\n context: Pick<TestContext, \"onTestFailed\" | \"task\">,\n plugin: PluginHandle,\n options: Pick<CreateObsidianTestOptions, \"artifactsDir\" | \"captureOnFailure\">,\n): void {\n const config = getFailureArtifactConfig(options);\n\n if (!config.enabled) {\n return;\n }\n\n context.onTestFailed(async () => {\n const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, context.task);\n await mkdir(artifactDirectory, { recursive: true });\n await captureJsonArtifact(artifactDirectory, `${plugin.id}-data.json`, true, () =>\n plugin.data().read(),\n );\n });\n}\n\nasync function captureJsonArtifact(\n artifactDirectory: string,\n filename: string,\n enabled: boolean,\n readValue: () => Promise<unknown>,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n try {\n const value = await readValue();\n await writeFile(\n path.join(artifactDirectory, filename),\n `${JSON.stringify(value, null, 2)}\\n`,\n \"utf8\",\n );\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, `${filename}.error.txt`),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nasync function captureScreenshotArtifact(\n artifactDirectory: string,\n enabled: boolean,\n obsidian: ObsidianClient,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n const screenshotPath = path.join(artifactDirectory, \"screenshot.png\");\n\n try {\n await obsidian.dev.screenshot(screenshotPath);\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, \"screenshot.error.txt\"),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nasync function captureTextArtifact(\n artifactDirectory: string,\n filename: string,\n enabled: boolean,\n readValue: () => Promise<string>,\n): Promise<void> {\n if (!enabled) {\n return;\n }\n\n try {\n await writeFile(path.join(artifactDirectory, filename), await readValue(), \"utf8\");\n } catch (error) {\n await writeFile(\n path.join(artifactDirectory, `${filename}.error.txt`),\n formatArtifactError(error),\n \"utf8\",\n );\n }\n}\n\nfunction formatArtifactError(error: unknown): string {\n return error instanceof Error ? `${error.name}: ${error.message}\\n` : `${String(error)}\\n`;\n}\n\nfunction sanitizeForPath(value: string): string {\n return (\n value\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 60) || \"test\"\n );\n}\n","import type { TestContext } from \"vite-plus/test\";\n\nimport { createObsidianClient } from \"../core/client\";\nimport { getClientInternals } from \"../core/internals\";\nimport type { ObsidianClient, VaultApi } from \"../core/types\";\nimport { createSandboxApi } from \"../vault/sandbox\";\nimport { createVaultApi } from \"../vault/vault\";\nimport { registerFailureArtifacts } from \"./failure-artifacts\";\nimport type { CreateObsidianTestOptions } from \"./types\";\nimport { acquireVaultRunLock, clearVaultRunLockMarker, type VaultRunLock } from \"./vault-lock\";\n\nexport const DEFAULT_SANDBOX_ROOT = \"__obsidian_e2e__\";\n\nexport interface BaseFixtureState {\n _vaultLock: VaultRunLock | null;\n}\n\ninterface BaseFixtureOptions {\n createVault?: (obsidian: ObsidianClient) => Promise<VaultApi> | VaultApi;\n}\n\nexport function createBaseFixtures(\n options: CreateObsidianTestOptions,\n fixtureOptions: BaseFixtureOptions = {},\n) {\n const createVault =\n fixtureOptions.createVault ?? ((obsidian: ObsidianClient) => createVaultApi({ obsidian }));\n\n return {\n _vaultLock: [\n // eslint-disable-next-line no-empty-pattern\n async ({}, use: (vaultLock: VaultRunLock | null) => Promise<void>) => {\n if (!options.sharedVaultLock) {\n await use(null);\n return;\n }\n\n const lockClient = createObsidianClient(options);\n await lockClient.verify();\n\n const lockOptions = options.sharedVaultLock === true ? {} : options.sharedVaultLock;\n const vaultLock = await acquireVaultRunLock({\n ...lockOptions,\n vaultName: options.vault,\n vaultPath: await lockClient.vaultPath(),\n });\n\n await vaultLock.publishMarker(lockClient);\n try {\n await use(vaultLock);\n } finally {\n try {\n await clearVaultRunLockMarker(lockClient);\n } catch {}\n\n await vaultLock.release();\n }\n },\n { scope: \"worker\" },\n ],\n // oxlint-disable-next-line no-empty-pattern\n obsidian: async (\n {\n _vaultLock,\n onTestFailed,\n task,\n }: Pick<BaseFixtureState & TestContext, \"_vaultLock\" | \"onTestFailed\" | \"task\">,\n use: (obsidian: ObsidianClient) => Promise<void>,\n ) => {\n const obsidian = createObsidianClient(options);\n\n await obsidian.verify();\n if (_vaultLock) {\n await _vaultLock.publishMarker(obsidian);\n }\n registerFailureArtifacts({ onTestFailed, task }, obsidian, options);\n\n try {\n await use(obsidian);\n } finally {\n await getClientInternals(obsidian).restoreAll();\n }\n },\n sandbox: async (\n { obsidian }: { obsidian: ObsidianClient },\n use: (sandbox: Awaited<ReturnType<typeof createSandboxApi>>) => Promise<void>,\n ) => {\n const sandbox = await createSandboxApi({\n obsidian,\n sandboxRoot: options.sandboxRoot ?? DEFAULT_SANDBOX_ROOT,\n testName: \"test\",\n });\n\n try {\n await use(sandbox);\n } finally {\n await sandbox.cleanup();\n }\n },\n vault: async (\n { obsidian }: { obsidian: ObsidianClient },\n use: (vault: VaultApi) => Promise<void>,\n ) => {\n await use(await createVault(obsidian));\n },\n };\n}\n","import { test as base } from \"vite-plus/test\";\n\nimport { createBaseFixtures, type BaseFixtureState } from \"./base-fixtures\";\nimport type { CreateObsidianTestOptions, ObsidianFixtures, ObsidianTest } from \"./types\";\n\nexport function createObsidianTest(options: CreateObsidianTestOptions): ObsidianTest {\n return base.extend<ObsidianFixtures & BaseFixtureState>(\n createBaseFixtures(options) as never,\n ) as ObsidianTest;\n}\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport { test as base } from \"vite-plus/test\";\nimport type { TestContext } from \"vite-plus/test\";\n\nimport { getClientInternals } from \"../core/internals\";\nimport type { ObsidianClient } from \"../core/types\";\nimport { createVaultApi } from \"../vault/vault\";\nimport { createBaseFixtures, type BaseFixtureState } from \"./base-fixtures\";\nimport { registerPluginFailureArtifacts } from \"./failure-artifacts\";\nimport type {\n CreatePluginTestOptions,\n PluginFixtures,\n PluginTest,\n VaultSeed,\n VaultSeedEntry,\n} from \"./types\";\n\nexport function createPluginTest(options: CreatePluginTestOptions): PluginTest {\n const fixtures = {\n ...createBaseFixtures(options, {\n async createVault(obsidian) {\n if (options.seedVault) {\n await applyVaultSeed(obsidian, options.seedVault);\n }\n\n return createVaultApi({ obsidian });\n },\n }),\n plugin: async (\n {\n obsidian,\n onTestFailed,\n task,\n }: Pick<PluginFixtures & TestContext, \"obsidian\" | \"onTestFailed\" | \"task\">,\n use: (plugin: PluginFixtures[\"plugin\"]) => Promise<void>,\n ) => {\n const plugin = obsidian.plugin(options.pluginId);\n const wasEnabled = await plugin.isEnabled();\n\n if (!wasEnabled) {\n await plugin.enable({ filter: options.pluginFilter });\n }\n\n if (options.seedPluginData !== undefined) {\n await plugin.data().write(options.seedPluginData);\n }\n\n registerPluginFailureArtifacts({ onTestFailed, task }, plugin, options);\n\n try {\n await use(plugin);\n } finally {\n if (!wasEnabled) {\n await plugin.disable({ filter: options.pluginFilter });\n }\n }\n },\n };\n\n return base.extend<PluginFixtures & BaseFixtureState>(fixtures as never) as PluginTest;\n}\n\nasync function applyVaultSeed(obsidian: ObsidianClient, seedVault: VaultSeed): Promise<void> {\n const vaultRoot = await obsidian.vaultPath();\n\n for (const [targetPath, value] of Object.entries(seedVault)) {\n const resolvedPath = path.resolve(vaultRoot, ...targetPath.split(\"/\").filter(Boolean));\n const normalizedVaultRoot = path.resolve(vaultRoot);\n\n if (\n resolvedPath !== normalizedVaultRoot &&\n !resolvedPath.startsWith(`${normalizedVaultRoot}${path.sep}`)\n ) {\n throw new Error(`Seed path escapes the vault root: ${targetPath}`);\n }\n\n await getClientInternals(obsidian).snapshotFileOnce(resolvedPath);\n await mkdir(path.dirname(resolvedPath), { recursive: true });\n await writeSeedValue(resolvedPath, value);\n }\n}\n\nasync function writeSeedValue(resolvedPath: string, value: VaultSeedEntry): Promise<void> {\n if (typeof value === \"string\") {\n await writeFile(resolvedPath, value, \"utf8\");\n return;\n }\n\n await writeFile(resolvedPath, `${JSON.stringify(value.json, null, 2)}\\n`, \"utf8\");\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obsidian-e2e",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Vitest-first end-to-end test utilities for Obsidian plugins.",
5
5
  "homepage": "https://github.com/chhoumann/obsidian-e2e#readme",
6
6
  "bugs": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"sandbox-BhesE1S4.mjs","names":["DEFAULT_TIMEOUT_MS","pathPosix","pathPosix"],"sources":["../src/core/args.ts","../src/core/internals.ts","../src/vault/json-file.ts","../src/plugin/plugin.ts","../src/core/errors.ts","../src/core/transport.ts","../src/core/wait.ts","../src/core/client.ts","../src/vault/vault.ts","../src/vault/sandbox.ts"],"sourcesContent":["import type { ObsidianArg } from \"./types\";\n\nexport function buildCommandArgv(\n vaultName: string,\n command: string,\n args: Record<string, ObsidianArg> = {},\n): string[] {\n const argv = [`vault=${vaultName}`, command];\n\n for (const [key, value] of Object.entries(args)) {\n if (value === false || value === null || value === undefined) {\n continue;\n }\n\n if (value === true) {\n argv.push(key);\n continue;\n }\n\n argv.push(`${key}=${String(value)}`);\n }\n\n return argv;\n}\n","import { rm, writeFile } from \"node:fs/promises\";\n\nimport type { ObsidianClient } from \"./types\";\n\ninterface SnapshotEntry {\n exists: boolean;\n value: string;\n}\n\ninterface ClientInternals {\n restoreAll(): Promise<void>;\n restoreFile(filePath: string): Promise<void>;\n snapshotFileOnce(filePath: string): Promise<void>;\n}\n\nconst clientInternals = new WeakMap<ObsidianClient, ClientInternals>();\n\nexport function attachClientInternals(client: ObsidianClient, internals: ClientInternals): void {\n clientInternals.set(client, internals);\n}\n\nexport function getClientInternals(client: ObsidianClient): ClientInternals {\n const internals = clientInternals.get(client);\n\n if (!internals) {\n throw new Error(\"Missing obsidian client internals.\");\n }\n\n return internals;\n}\n\nexport function createRestoreManager(readFile: (filePath: string) => Promise<string>) {\n const snapshots = new Map<string, SnapshotEntry>();\n\n return {\n async restoreAll() {\n const entries = [...snapshots.entries()].reverse();\n\n for (const [filePath, snapshot] of entries) {\n await restoreSnapshot(filePath, snapshot);\n }\n\n snapshots.clear();\n },\n async restoreFile(filePath: string) {\n const snapshot = snapshots.get(filePath);\n\n if (!snapshot) {\n return;\n }\n\n await restoreSnapshot(filePath, snapshot);\n snapshots.delete(filePath);\n },\n async snapshotFileOnce(filePath: string) {\n if (snapshots.has(filePath)) {\n return;\n }\n\n try {\n snapshots.set(filePath, {\n exists: true,\n value: await readFile(filePath),\n });\n } catch (error) {\n if (isMissingFileError(error)) {\n snapshots.set(filePath, {\n exists: false,\n value: \"\",\n });\n return;\n }\n\n throw error;\n }\n },\n };\n}\n\nasync function restoreSnapshot(filePath: string, snapshot: SnapshotEntry): Promise<void> {\n if (snapshot.exists) {\n await writeFile(filePath, snapshot.value, \"utf8\");\n return;\n }\n\n await rm(filePath, { force: true, recursive: true });\n}\n\nfunction isMissingFileError(error: unknown): error is NodeJS.ErrnoException {\n return Boolean(error && typeof error === \"object\" && \"code\" in error && error.code === \"ENOENT\");\n}\n","import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { JsonFile, JsonFileUpdater } from \"../core/types\";\n\nexport function createJsonFile<T = unknown>(\n filePath: string,\n beforeMutate?: () => Promise<void>,\n): JsonFile<T> {\n return {\n async patch(updater: JsonFileUpdater<T>) {\n await beforeMutate?.();\n\n const currentValue = await this.read();\n const draft = structuredClone(currentValue);\n const result = await updater(draft);\n const nextValue = result ?? draft;\n\n await this.write(nextValue);\n\n return nextValue;\n },\n async read() {\n const value = await readFile(filePath, \"utf8\");\n return JSON.parse(value) as T;\n },\n async write(value: T) {\n await beforeMutate?.();\n await mkdir(path.dirname(filePath), { recursive: true });\n await writeFile(filePath, `${JSON.stringify(value, null, 2)}\\n`, \"utf8\");\n },\n };\n}\n","import path from \"node:path\";\n\nimport { getClientInternals } from \"../core/internals\";\nimport type { JsonFile, ObsidianClient, PluginHandle, PluginToggleOptions } from \"../core/types\";\nimport { createJsonFile } from \"../vault/json-file\";\n\nexport function createPluginHandle(client: ObsidianClient, id: string): PluginHandle {\n async function resolveDataPath() {\n const vaultPath = await client.vaultPath();\n return path.join(vaultPath, \".obsidian\", \"plugins\", id, \"data.json\");\n }\n\n return {\n data<T = unknown>(): JsonFile<T> {\n return {\n async patch(updater) {\n const dataPath = await resolveDataPath();\n return createJsonFile<T>(dataPath, () =>\n getClientInternals(client).snapshotFileOnce(dataPath),\n ).patch(updater);\n },\n async read() {\n const dataPath = await resolveDataPath();\n return createJsonFile<T>(dataPath).read();\n },\n async write(value) {\n const dataPath = await resolveDataPath();\n await createJsonFile<T>(dataPath, () =>\n getClientInternals(client).snapshotFileOnce(dataPath),\n ).write(value);\n },\n };\n },\n async dataPath() {\n return resolveDataPath();\n },\n async disable(options: PluginToggleOptions = {}) {\n await client.exec(\"plugin:disable\", {\n filter: options.filter,\n id,\n });\n },\n async enable(options: PluginToggleOptions = {}) {\n await client.exec(\"plugin:enable\", {\n filter: options.filter,\n id,\n });\n },\n id,\n async isEnabled() {\n const output = await client.execText(\"plugin\", { id }, { allowNonZeroExit: true });\n return /enabled\\s+true/i.test(output);\n },\n async reload() {\n await client.exec(\"plugin:reload\", { id });\n },\n async restoreData() {\n await getClientInternals(client).restoreFile(await resolveDataPath());\n },\n };\n}\n","import type { ExecResult } from \"./types\";\n\nexport class ObsidianCommandError extends Error {\n readonly result: ExecResult;\n\n constructor(message: string, result: ExecResult) {\n super(message);\n this.name = \"ObsidianCommandError\";\n this.result = result;\n }\n}\n\nexport class WaitForTimeoutError extends Error {\n readonly causeError?: unknown;\n\n constructor(message: string, causeError?: unknown) {\n super(message);\n this.name = \"WaitForTimeoutError\";\n this.causeError = causeError;\n }\n}\n","import { spawn } from \"node:child_process\";\n\nimport { ObsidianCommandError } from \"./errors\";\nimport type { CommandTransport, ExecuteRequest, ExecResult } from \"./types\";\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\nexport const executeCommand: CommandTransport = async ({\n allowNonZeroExit = false,\n argv,\n bin,\n cwd,\n env,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n}: ExecuteRequest): Promise<ExecResult> => {\n const child = spawn(bin, argv, {\n cwd,\n env,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n\n child.stdout.on(\"data\", (chunk) => {\n stdoutChunks.push(Buffer.from(chunk));\n });\n\n child.stderr.on(\"data\", (chunk) => {\n stderrChunks.push(Buffer.from(chunk));\n });\n\n const exitCode = await new Promise<number>((resolve, reject) => {\n const timer = setTimeout(() => {\n child.kill(\"SIGTERM\");\n reject(new Error(`Command timed out after ${timeoutMs}ms: ${bin} ${argv.join(\" \")}`));\n }, timeoutMs);\n\n child.on(\"error\", (error) => {\n clearTimeout(timer);\n reject(error);\n });\n\n child.on(\"close\", (code) => {\n clearTimeout(timer);\n resolve(code ?? 0);\n });\n });\n\n const result: ExecResult = {\n argv,\n command: bin,\n exitCode,\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n };\n\n if (exitCode !== 0 && !allowNonZeroExit) {\n throw new ObsidianCommandError(\n `Obsidian command failed with exit code ${exitCode}: ${bin} ${argv.join(\" \")}`,\n result,\n );\n }\n\n return result;\n};\n","import { WaitForTimeoutError } from \"./errors\";\nimport type { WaitForOptions } from \"./types\";\n\nconst DEFAULT_INTERVAL_MS = 100;\nconst DEFAULT_TIMEOUT_MS = 5_000;\n\nexport async function waitForValue<T>(\n fn: () => Promise<T | false | null | undefined> | T | false | null | undefined,\n options: WaitForOptions = {},\n): Promise<T> {\n const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const startTime = Date.now();\n\n let lastError: unknown;\n\n while (Date.now() - startTime <= timeoutMs) {\n try {\n const result = await fn();\n if (result !== false && result !== null && result !== undefined) {\n return result;\n }\n } catch (error) {\n lastError = error;\n }\n\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n\n const label = options.message ?? \"condition\";\n throw new WaitForTimeoutError(`Timed out waiting for ${label} after ${timeoutMs}ms.`, lastError);\n}\n","import { buildCommandArgv } from \"./args\";\nimport { attachClientInternals, createRestoreManager } from \"./internals\";\nimport { createPluginHandle } from \"../plugin/plugin\";\nimport { executeCommand } from \"./transport\";\nimport type {\n CommandListOptions,\n CreateObsidianClientOptions,\n DevDomQueryOptions,\n DevDomResult,\n ExecOptions,\n ObsidianArg,\n ObsidianAppHandle,\n ObsidianCommandHandle,\n ObsidianClient,\n ObsidianDevHandle,\n OpenFileOptions,\n OpenTabOptions,\n RestartAppOptions,\n TabsOptions,\n WaitForOptions,\n WorkspaceNode,\n WorkspaceOptions,\n WorkspaceTab,\n} from \"./types\";\nimport { waitForValue } from \"./wait\";\n\nexport function createObsidianClient(options: CreateObsidianClientOptions): ObsidianClient {\n const transport = options.transport ?? executeCommand;\n const waitDefaults = {\n intervalMs: options.intervalMs,\n timeoutMs: options.timeoutMs,\n };\n\n const restoreManager = createRestoreManager(async (filePath) => {\n const { readFile } = await import(\"node:fs/promises\");\n return readFile(filePath, \"utf8\");\n });\n\n let cachedVaultPath: string | undefined;\n\n const client = {} as ObsidianClient;\n\n const app: ObsidianAppHandle = {\n async reload(execOptions: ExecOptions = {}) {\n await client.exec(\"reload\", {}, execOptions);\n },\n async restart({\n readyOptions,\n waitUntilReady = true,\n ...execOptions\n }: RestartAppOptions & ExecOptions = {}) {\n await client.exec(\"restart\", {}, execOptions);\n\n if (waitUntilReady) {\n await app.waitUntilReady(readyOptions);\n }\n },\n version(execOptions: ExecOptions = {}) {\n return client.execText(\"version\", {}, execOptions);\n },\n async waitUntilReady(waitOptions?: WaitForOptions) {\n await client.waitFor(async () => {\n try {\n await client.vaultPath();\n await client.commands();\n return true;\n } catch {\n return false;\n }\n }, waitOptions);\n },\n };\n\n const dev: ObsidianDevHandle = {\n async dom(options: DevDomQueryOptions, execOptions: ExecOptions = {}): Promise<DevDomResult> {\n const output = await client.execText(\n \"dev:dom\",\n {\n all: options.all,\n attr: options.attr,\n css: options.css,\n inner: options.inner,\n selector: options.selector,\n text: options.text,\n total: options.total,\n },\n execOptions,\n );\n\n if (options.total) {\n return Number.parseInt(output, 10);\n }\n\n if (options.all) {\n return output ? output.split(/\\r?\\n/u).filter(Boolean) : [];\n }\n\n return output;\n },\n async eval<T = unknown>(code: string, execOptions: ExecOptions = {}) {\n const output = await client.execText(\n \"eval\",\n {\n code,\n },\n execOptions,\n );\n return parseDevEvalOutput<T>(output);\n },\n async screenshot(targetPath: string, execOptions: ExecOptions = {}) {\n await client.exec(\n \"dev:screenshot\",\n {\n path: targetPath,\n },\n execOptions,\n );\n\n return targetPath;\n },\n };\n\n Object.assign(client, {\n app,\n bin: options.bin ?? \"obsidian\",\n dev,\n command(id: string): ObsidianCommandHandle {\n return {\n async exists(commandOptions: CommandListOptions = {}) {\n const commands = await client.commands({\n ...commandOptions,\n filter: commandOptions.filter ?? id,\n });\n\n return commands.includes(id);\n },\n id,\n async run(execOptions: ExecOptions = {}) {\n await client.exec(\"command\", { id }, execOptions);\n },\n };\n },\n async commands(\n commandOptions: CommandListOptions = {},\n execOptions: ExecOptions = {},\n ): Promise<string[]> {\n const output = await client.execText(\n \"commands\",\n {\n filter: commandOptions.filter,\n },\n execOptions,\n );\n return parseCommandIds(output);\n },\n exec(command: string, args: Record<string, ObsidianArg> = {}, execOptions: ExecOptions = {}) {\n return transport({\n ...execOptions,\n argv: buildCommandArgv(options.vault, command, args),\n bin: this.bin,\n });\n },\n async execJson<T = unknown>(\n command: string,\n args: Record<string, ObsidianArg> = {},\n execOptions: ExecOptions = {},\n ) {\n const output = await this.execText(command, args, execOptions);\n return JSON.parse(output) as T;\n },\n async execText(\n command: string,\n args: Record<string, ObsidianArg> = {},\n execOptions: ExecOptions = {},\n ) {\n const result = await this.exec(command, args, execOptions);\n return result.stdout.trimEnd();\n },\n async open(openOptions: OpenFileOptions, execOptions: ExecOptions = {}) {\n await client.exec(\n \"open\",\n {\n file: openOptions.file,\n newtab: openOptions.newTab,\n path: openOptions.path,\n },\n execOptions,\n );\n },\n async openTab(tabOptions: OpenTabOptions = {}, execOptions: ExecOptions = {}) {\n await client.exec(\n \"tab:open\",\n {\n file: tabOptions.file,\n group: tabOptions.group,\n view: tabOptions.view,\n },\n execOptions,\n );\n },\n plugin(id: string) {\n return createPluginHandle(this, id);\n },\n async tabs(\n tabOptions: TabsOptions = {},\n execOptions: ExecOptions = {},\n ): Promise<WorkspaceTab[]> {\n const output = await client.execText(\n \"tabs\",\n {\n ids: tabOptions.ids ?? true,\n },\n execOptions,\n );\n return parseTabs(output);\n },\n async vaultPath() {\n if (!cachedVaultPath) {\n cachedVaultPath = await this.execText(\"vault\", { info: \"path\" });\n }\n\n return cachedVaultPath;\n },\n async verify() {\n await transport({\n argv: [\"--help\"],\n bin: this.bin,\n });\n\n await this.vaultPath();\n },\n vaultName: options.vault,\n waitFor<T>(\n fn: () => Promise<T | false | null | undefined> | T | false | null | undefined,\n waitOptions?: WaitForOptions,\n ) {\n return waitForValue(fn, {\n ...waitDefaults,\n ...waitOptions,\n });\n },\n async workspace(\n workspaceOptions: WorkspaceOptions = {},\n execOptions: ExecOptions = {},\n ): Promise<WorkspaceNode[]> {\n const output = await client.execText(\n \"workspace\",\n {\n ids: workspaceOptions.ids ?? true,\n },\n execOptions,\n );\n return parseWorkspace(output);\n },\n });\n\n attachClientInternals(client, restoreManager);\n\n return client;\n}\n\nfunction parseCommandIds(output: string): string[] {\n return output\n .split(/\\r?\\n/u)\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => line.split(\"\\t\", 1)[0]?.trim() ?? \"\")\n .filter(Boolean);\n}\n\nfunction parseDevEvalOutput<T>(output: string): T {\n const normalized = output.startsWith(\"=> \") ? output.slice(3) : output;\n\n try {\n return JSON.parse(normalized) as T;\n } catch {\n return normalized as T;\n }\n}\n\nfunction parseTabs(output: string): WorkspaceTab[] {\n return output\n .split(/\\r?\\n/u)\n .map((line) => line.trim())\n .filter(Boolean)\n .map(parseTabLine);\n}\n\nfunction parseTabLine(line: string): WorkspaceTab {\n const [descriptor, id] = line.split(\"\\t\");\n const match = descriptor?.match(/^\\[(.+?)\\]\\s+(.*)$/u);\n\n if (!match) {\n return {\n id: id?.trim() || undefined,\n title: descriptor?.trim() ?? \"\",\n viewType: \"unknown\",\n };\n }\n\n return {\n id: id?.trim() || undefined,\n title: match[2]!,\n viewType: match[1]!,\n };\n}\n\nfunction parseWorkspace(output: string): WorkspaceNode[] {\n const roots: WorkspaceNode[] = [];\n const stack: Array<{ depth: number; node: WorkspaceNode }> = [];\n\n for (const rawLine of output.split(/\\r?\\n/u)) {\n if (!rawLine.trim()) {\n continue;\n }\n\n const depth = getWorkspaceDepth(rawLine);\n const node = parseWorkspaceNode(rawLine);\n\n while (stack.length > 0 && stack.at(-1)!.depth >= depth) {\n stack.pop();\n }\n\n const parent = stack.at(-1)?.node;\n\n if (parent) {\n parent.children.push(node);\n } else {\n roots.push(node);\n }\n\n stack.push({ depth, node });\n }\n\n return roots;\n}\n\nfunction getWorkspaceDepth(line: string): number {\n let depth = 0;\n let remainder = line;\n\n while (true) {\n if (\n remainder.startsWith(\"│ \") ||\n remainder.startsWith(\" \") ||\n remainder.startsWith(\"├── \") ||\n remainder.startsWith(\"└── \")\n ) {\n depth += 1;\n remainder = remainder.slice(4);\n continue;\n }\n\n return depth;\n }\n}\n\nfunction parseWorkspaceNode(line: string): WorkspaceNode {\n let withoutTree = line;\n\n while (true) {\n if (\n withoutTree.startsWith(\"│ \") ||\n withoutTree.startsWith(\" \") ||\n withoutTree.startsWith(\"├── \") ||\n withoutTree.startsWith(\"└── \")\n ) {\n withoutTree = withoutTree.slice(4);\n continue;\n }\n\n break;\n }\n\n withoutTree = withoutTree.trim();\n const idMatch = withoutTree.match(/^(.*?)(?: \\(([a-z0-9]+)\\))?$/iu);\n const content = idMatch?.[1]?.trim() ?? withoutTree;\n const id = idMatch?.[2];\n const leafMatch = content.match(/^\\[(.+?)\\]\\s+(.*)$/u);\n\n if (leafMatch) {\n return {\n children: [],\n id,\n label: leafMatch[2]!,\n title: leafMatch[2]!,\n viewType: leafMatch[1]!,\n };\n }\n\n return {\n children: [],\n id,\n label: content,\n };\n}\n","import { access, mkdir, readFile, rm, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { posix as pathPosix } from \"node:path\";\n\nimport type { DeleteOptions, JsonFile, ObsidianClient, VaultApi } from \"../core/types\";\n\ninterface CreateVaultApiOptions {\n obsidian: ObsidianClient;\n root?: string;\n}\n\nexport function createVaultApi(options: CreateVaultApiOptions): VaultApi {\n const scopeRoot = normalizeScope(options.root);\n\n return {\n async delete(targetPath, deleteOptions: DeleteOptions = {}) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await rm(resolvedPath, {\n force: true,\n recursive: true,\n });\n\n if (deleteOptions.permanent === false) {\n return;\n }\n },\n async exists(targetPath) {\n try {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await access(resolvedPath);\n return true;\n } catch {\n return false;\n }\n },\n json<T = unknown>(targetPath: string) {\n const jsonFile: JsonFile<T> = {\n async patch(updater) {\n const currentValue = await jsonFile.read();\n const draft = structuredClone(currentValue);\n const result = await updater(draft);\n const nextValue = result ?? draft;\n\n await jsonFile.write(nextValue);\n\n return nextValue;\n },\n async read() {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n const rawValue = await readFile(resolvedPath, \"utf8\");\n return JSON.parse(rawValue) as T;\n },\n async write(value) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await mkdir(path.dirname(resolvedPath), { recursive: true });\n await writeFile(resolvedPath, `${JSON.stringify(value, null, 2)}\\n`, \"utf8\");\n },\n };\n\n return jsonFile;\n },\n async mkdir(targetPath) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await mkdir(resolvedPath, { recursive: true });\n },\n async read(targetPath) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n return readFile(resolvedPath, \"utf8\");\n },\n async waitForExists(targetPath, waitOptions) {\n await options.obsidian.waitFor(async () => ((await this.exists(targetPath)) ? true : false), {\n message: `vault path \"${resolveVaultPath(scopeRoot, targetPath)}\" to exist`,\n ...waitOptions,\n });\n },\n async waitForMissing(targetPath, waitOptions) {\n await options.obsidian.waitFor(async () => ((await this.exists(targetPath)) ? false : true), {\n message: `vault path \"${resolveVaultPath(scopeRoot, targetPath)}\" to be removed`,\n ...waitOptions,\n });\n },\n async write(targetPath, content) {\n const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);\n await mkdir(path.dirname(resolvedPath), { recursive: true });\n await writeFile(resolvedPath, content, \"utf8\");\n },\n };\n}\n\nfunction normalizeScope(scope?: string): string {\n if (!scope || scope === \".\") {\n return \"\";\n }\n\n return scope.replace(/^\\/+|\\/+$/g, \"\");\n}\n\nfunction resolveVaultPath(scopeRoot: string, targetPath: string): string {\n if (!targetPath || targetPath === \".\") {\n return scopeRoot;\n }\n\n return scopeRoot ? pathPosix.join(scopeRoot, targetPath) : pathPosix.normalize(targetPath);\n}\n\nasync function resolveFilesystemPath(\n obsidian: ObsidianClient,\n scopeRoot: string,\n targetPath: string,\n): Promise<string> {\n const vaultPath = await obsidian.vaultPath();\n const scopedPath = resolveVaultPath(scopeRoot, targetPath);\n const relativePath = scopedPath.split(\"/\").filter(Boolean);\n const resolvedPath = path.resolve(vaultPath, ...relativePath);\n const normalizedVaultPath = path.resolve(vaultPath);\n\n if (\n resolvedPath !== normalizedVaultPath &&\n !resolvedPath.startsWith(`${normalizedVaultPath}${path.sep}`)\n ) {\n throw new Error(`Resolved path escapes the vault root: ${targetPath}`);\n }\n\n return resolvedPath;\n}\n","import { posix as pathPosix } from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\n\nimport type { ObsidianClient, SandboxApi } from \"../core/types\";\nimport { createVaultApi } from \"./vault\";\n\ninterface CreateSandboxApiOptions {\n obsidian: ObsidianClient;\n sandboxRoot: string;\n testName: string;\n}\n\nexport async function createSandboxApi(options: CreateSandboxApiOptions): Promise<SandboxApi> {\n const root = pathPosix.join(\n options.sandboxRoot,\n `${sanitizeSegment(options.testName)}-${randomUUID().slice(0, 8)}`,\n );\n const vault = createVaultApi({\n obsidian: options.obsidian,\n root,\n });\n\n await vault.mkdir(\".\");\n\n return {\n ...vault,\n async cleanup() {\n await vault.delete(\".\", { permanent: true });\n },\n path(...segments: string[]) {\n return pathPosix.join(root, ...segments);\n },\n root,\n };\n}\n\nfunction sanitizeSegment(value: string): string {\n return (\n value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 80) || \"test\"\n );\n}\n"],"mappings":"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"}