obsidian-e2e 0.2.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
@@ -96,10 +96,43 @@ marker into the Obsidian process. That marker is not authoritative. The
96
96
  filesystem lock is the source of truth, and the app marker is only there to
97
97
  help humans understand which run currently owns the vault.
98
98
 
99
- For lock diagnostics, `obsidian-e2e/vitest` also exports:
99
+ For manual lifecycle setups, the same lock helpers are available directly from
100
+ the main package, so `obsidian-e2e/vitest` is not required:
100
101
 
101
102
  ```ts
102
- import { inspectVaultRunLock, readVaultRunLockMarker } from "obsidian-e2e/vitest";
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";
103
136
 
104
137
  const state = await inspectVaultRunLock({
105
138
  vaultPath: "/absolute/path/to/dev-vault",
@@ -113,6 +146,39 @@ returns the current metadata, lock directory, heartbeat age, and stale status.
113
146
  `readVaultRunLockMarker()` reads the best-effort marker from the running
114
147
  Obsidian app.
115
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
+
116
182
  Within one worker/process, reacquiring the same shared-vault lock is reentrant:
117
183
  the existing lease is reused instead of contending against itself. Across
118
184
  different processes or worktrees, contention still serializes access through
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,83 +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 SharedVaultLockOptions {
14
- heartbeatMs?: number;
15
- lockRoot?: string;
16
- onBusy?: "fail" | "wait";
17
- staleMs?: number;
18
- timeoutMs?: number;
19
- }
20
- interface CreateObsidianTestOptions extends CreateObsidianClientOptions {
21
- artifactsDir?: string;
22
- captureOnFailure?: boolean | FailureArtifactOptions;
23
- sharedVaultLock?: boolean | SharedVaultLockOptions;
24
- sandboxRoot?: string;
25
- }
26
- type VaultSeedEntry = string | {
27
- json: unknown;
28
- };
29
- type VaultSeed = Record<string, VaultSeedEntry>;
30
- interface CreatePluginTestOptions extends CreateObsidianTestOptions {
31
- pluginFilter?: PluginToggleOptions["filter"];
32
- pluginId: string;
33
- seedPluginData?: unknown;
34
- seedVault?: VaultSeed;
35
- }
36
- interface ObsidianFixtures {
37
- obsidian: ObsidianClient;
38
- sandbox: SandboxApi;
39
- vault: VaultApi;
40
- }
41
- interface PluginFixtures extends ObsidianFixtures {
42
- plugin: PluginHandle;
43
- }
44
- type ObsidianTest = TestAPI<ObsidianFixtures>;
45
- type PluginTest = TestAPI<PluginFixtures>;
46
- //#endregion
47
3
  //#region src/fixtures/create-obsidian-test.d.ts
48
4
  declare function createObsidianTest(options: CreateObsidianTestOptions): ObsidianTest;
49
5
  //#endregion
50
6
  //#region src/fixtures/create-plugin-test.d.ts
51
7
  declare function createPluginTest(options: CreatePluginTestOptions): PluginTest;
52
8
  //#endregion
53
- //#region src/fixtures/vault-lock.d.ts
54
- interface VaultRunLockMetadata {
55
- acquiredAt: number;
56
- cwd: string;
57
- heartbeatAt: number;
58
- hostname: string;
59
- ownerId: string;
60
- pid: number;
61
- staleMs: number;
62
- vaultName: string;
63
- vaultPath: string;
64
- }
65
- interface VaultRunLockState {
66
- heartbeatAgeMs: number;
67
- isStale: boolean;
68
- lockDir: string;
69
- metadata: VaultRunLockMetadata;
70
- }
71
- interface AcquireVaultRunLockOptions extends SharedVaultLockOptions {
72
- vaultName: string;
73
- vaultPath: string;
74
- }
75
- declare function inspectVaultRunLock({
76
- lockRoot,
77
- staleMs,
78
- vaultPath
79
- }: Pick<AcquireVaultRunLockOptions, "lockRoot" | "staleMs" | "vaultPath">): Promise<VaultRunLockState | null>;
80
- declare function readVaultRunLockMarker(obsidian: ObsidianClient): Promise<VaultRunLockMetadata | null>;
81
- //#endregion
82
- export { type CreateObsidianTestOptions, type CreatePluginTestOptions, type ObsidianFixtures, type ObsidianTest, type PluginFixtures, type PluginTest, type SharedVaultLockOptions, type VaultRunLockMetadata, type VaultRunLockState, type VaultSeed, type VaultSeedEntry, createObsidianTest, createPluginTest, inspectVaultRunLock, readVaultRunLockMarker };
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 };
83
10
  //# sourceMappingURL=vitest.d.mts.map
package/dist/vitest.mjs CHANGED
@@ -1,9 +1,7 @@
1
- import { i as getClientInternals, n as createVaultApi, r as createObsidianClient, t as createSandboxApi } from "./sandbox-BhesE1S4.mjs";
2
- import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
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
+ import { mkdir, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { createHash, randomUUID } from "node:crypto";
5
4
  import { test } from "vite-plus/test";
6
- import os from "node:os";
7
5
  //#region src/fixtures/failure-artifacts.ts
8
6
  const DEFAULT_ARTIFACTS_DIR = ".obsidian-e2e-artifacts";
9
7
  function getFailureArtifactConfig(options) {
@@ -97,168 +95,6 @@ function formatArtifactError(error) {
97
95
  function sanitizeForPath(value) {
98
96
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "test";
99
97
  }
100
- //#endregion
101
- //#region src/fixtures/vault-lock.ts
102
- const DEFAULT_HEARTBEAT_MS = 2e3;
103
- const DEFAULT_STALE_MS = 15e3;
104
- const DEFAULT_TIMEOUT_MS = 6e4;
105
- const DEFAULT_WAIT_INTERVAL_MS = 500;
106
- const DEFAULT_LOCK_ROOT = path.join(os.tmpdir(), "obsidian-e2e-locks");
107
- const LOCK_METADATA_FILE = "lock.json";
108
- const APP_LOCK_KEY = "__obsidianE2ELock";
109
- const heldLocks = /* @__PURE__ */ new Map();
110
- async function acquireVaultRunLock({ heartbeatMs = DEFAULT_HEARTBEAT_MS, lockRoot = DEFAULT_LOCK_ROOT, onBusy = "wait", staleMs = DEFAULT_STALE_MS, timeoutMs = DEFAULT_TIMEOUT_MS, vaultName, vaultPath }) {
111
- const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));
112
- const heldLock = heldLocks.get(lockDir);
113
- if (heldLock) {
114
- heldLock.refs += 1;
115
- return createVaultRunLockHandle(heldLock);
116
- }
117
- const ownerId = randomUUID();
118
- const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);
119
- const metadata = {
120
- acquiredAt: Date.now(),
121
- cwd: process.cwd(),
122
- heartbeatAt: Date.now(),
123
- hostname: os.hostname(),
124
- ownerId,
125
- pid: process.pid,
126
- staleMs,
127
- vaultName,
128
- vaultPath
129
- };
130
- await mkdir(lockRoot, { recursive: true });
131
- const startedAt = Date.now();
132
- while (true) try {
133
- await mkdir(lockDir);
134
- await writeMetadata(metadataPath, metadata);
135
- break;
136
- } catch (error) {
137
- if (!isAlreadyExistsError(error)) throw error;
138
- const currentLock = await inspectVaultRunLock({
139
- lockRoot,
140
- staleMs,
141
- vaultPath
142
- });
143
- if (currentLock && !currentLock.isStale) {
144
- if (onBusy === "fail") throw new Error(formatBusyLockMessage(vaultPath, currentLock));
145
- } else {
146
- await rm(lockDir, {
147
- force: true,
148
- recursive: true
149
- });
150
- continue;
151
- }
152
- 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}`);
153
- await sleep(Math.min(DEFAULT_WAIT_INTERVAL_MS, heartbeatMs));
154
- }
155
- const heartbeat = setInterval(() => {
156
- metadata.heartbeatAt = Date.now();
157
- writeMetadata(metadataPath, metadata).catch(() => {});
158
- }, heartbeatMs);
159
- heartbeat.unref();
160
- const nextHeldLock = {
161
- heartbeat,
162
- lockDir,
163
- metadata,
164
- metadataPath,
165
- refs: 1
166
- };
167
- heldLocks.set(lockDir, nextHeldLock);
168
- return createVaultRunLockHandle(nextHeldLock);
169
- }
170
- async function clearVaultRunLockMarker(obsidian) {
171
- await obsidian.dev.eval(`delete window.${APP_LOCK_KEY}; delete app.${APP_LOCK_KEY}; "cleared"`, { allowNonZeroExit: true });
172
- }
173
- async function inspectVaultRunLock({ lockRoot = DEFAULT_LOCK_ROOT, staleMs = DEFAULT_STALE_MS, vaultPath }) {
174
- const lockDir = path.join(lockRoot, createVaultLockKey(vaultPath));
175
- const metadata = await readLockState(lockDir);
176
- if (!metadata) return null;
177
- return {
178
- heartbeatAgeMs: Date.now() - metadata.heartbeatAt,
179
- isStale: isLockStale(metadata, staleMs),
180
- lockDir,
181
- metadata
182
- };
183
- }
184
- async function readVaultRunLockMarker(obsidian) {
185
- return obsidian.dev.eval(`window.${APP_LOCK_KEY} ?? app.${APP_LOCK_KEY} ?? null`, { allowNonZeroExit: true });
186
- }
187
- function createVaultLockKey(vaultPath) {
188
- return createHash("sha256").update(path.resolve(vaultPath)).digest("hex");
189
- }
190
- function buildSetMarkerCode(metadata) {
191
- return `(() => {
192
- const lock = ${JSON.stringify(metadata)};
193
- window.${APP_LOCK_KEY} = lock;
194
- app.${APP_LOCK_KEY} = lock;
195
- return lock;
196
- })()`;
197
- }
198
- function createVaultRunLockHandle(heldLock) {
199
- return {
200
- get lockDir() {
201
- return heldLock.lockDir;
202
- },
203
- get metadata() {
204
- return heldLock.metadata;
205
- },
206
- async publishMarker(obsidian) {
207
- await obsidian.dev.eval(buildSetMarkerCode(heldLock.metadata));
208
- },
209
- async release() {
210
- if (heldLock.refs > 1) {
211
- heldLock.refs -= 1;
212
- return;
213
- }
214
- heldLocks.delete(heldLock.lockDir);
215
- clearInterval(heldLock.heartbeat);
216
- if ((await readLockState(heldLock.lockDir))?.ownerId !== heldLock.metadata.ownerId) return;
217
- await rm(heldLock.lockDir, {
218
- force: true,
219
- recursive: true
220
- });
221
- }
222
- };
223
- }
224
- async function readLockState(lockDir) {
225
- const metadataPath = path.join(lockDir, LOCK_METADATA_FILE);
226
- try {
227
- return JSON.parse(await readFile(metadataPath, "utf8"));
228
- } catch {
229
- try {
230
- const directoryStat = await stat(lockDir);
231
- return {
232
- acquiredAt: directoryStat.mtimeMs,
233
- cwd: "",
234
- heartbeatAt: directoryStat.mtimeMs,
235
- hostname: "",
236
- ownerId: "",
237
- pid: 0,
238
- staleMs: DEFAULT_STALE_MS,
239
- vaultName: "",
240
- vaultPath: ""
241
- };
242
- } catch {
243
- return null;
244
- }
245
- }
246
- }
247
- function formatBusyLockMessage(vaultPath, state) {
248
- 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}`}`;
249
- }
250
- function isAlreadyExistsError(error) {
251
- return error instanceof Error && "code" in error && error.code === "EEXIST";
252
- }
253
- function isLockStale(metadata, staleMs) {
254
- return Date.now() - metadata.heartbeatAt > staleMs;
255
- }
256
- async function sleep(durationMs) {
257
- await new Promise((resolve) => setTimeout(resolve, durationMs));
258
- }
259
- async function writeMetadata(metadataPath, metadata) {
260
- await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
261
- }
262
98
  function createBaseFixtures(options, fixtureOptions = {}) {
263
99
  const createVault = fixtureOptions.createVault ?? ((obsidian) => createVaultApi({ obsidian }));
264
100
  return {
@@ -365,6 +201,6 @@ async function writeSeedValue(resolvedPath, value) {
365
201
  await writeFile(resolvedPath, `${JSON.stringify(value.json, null, 2)}\n`, "utf8");
366
202
  }
367
203
  //#endregion
368
- export { createObsidianTest, createPluginTest, inspectVaultRunLock, readVaultRunLockMarker };
204
+ export { acquireVaultRunLock, clearVaultRunLockMarker, createObsidianTest, createPluginTest, inspectVaultRunLock, readVaultRunLockMarker };
369
205
 
370
206
  //# sourceMappingURL=vitest.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"vitest.mjs","names":["base","base"],"sources":["../src/fixtures/failure-artifacts.ts","../src/fixtures/vault-lock.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 { 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\ninterface 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 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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"}
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.2.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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"}