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