obsidian-launcher 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1115 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/launcher.ts
2
+ var _promises = require('fs/promises'); var _promises2 = _interopRequireDefault(_promises);
3
+ var _fs = require('fs'); var _fs2 = _interopRequireDefault(_fs);
4
+ var _zlib = require('zlib'); var _zlib2 = _interopRequireDefault(_zlib);
5
+ var _path = require('path'); var _path2 = _interopRequireDefault(_path);
6
+ var _os = require('os'); var _os2 = _interopRequireDefault(_os);
7
+ var _nodefetch = require('node-fetch'); var _nodefetch2 = _interopRequireDefault(_nodefetch);
8
+ var _extractzip = require('extract-zip'); var _extractzip2 = _interopRequireDefault(_extractzip);
9
+ var _promises3 = require('stream/promises');
10
+ var _get = require('@electron/get');
11
+ var _child_process = require('child_process'); var _child_process2 = _interopRequireDefault(_child_process);
12
+ var _semver = require('semver'); var _semver2 = _interopRequireDefault(_semver);
13
+ var _chromeremoteinterface = require('chrome-remote-interface'); var _chromeremoteinterface2 = _interopRequireDefault(_chromeremoteinterface);
14
+
15
+ // src/utils.ts
16
+
17
+
18
+ var _promisepool = require('@supercharge/promise-pool');
19
+ async function fileExists(path5) {
20
+ try {
21
+ await _promises2.default.access(path5);
22
+ return true;
23
+ } catch (e2) {
24
+ return false;
25
+ }
26
+ }
27
+ async function withTmpDir(dest, func) {
28
+ dest = _path2.default.resolve(dest);
29
+ const tmpDir = await _promises2.default.mkdtemp(_path2.default.join(_path2.default.dirname(dest), `.${_path2.default.basename(dest)}.tmp.`));
30
+ try {
31
+ let result = await _asyncNullishCoalesce(await func(tmpDir), async () => ( tmpDir));
32
+ if (!_path2.default.isAbsolute(result)) {
33
+ result = _path2.default.join(tmpDir, result);
34
+ } else if (!_path2.default.resolve(result).startsWith(tmpDir)) {
35
+ throw new Error(`Returned path ${result} not under tmpDir`);
36
+ }
37
+ if (await fileExists(dest) && (await _promises2.default.stat(dest)).isDirectory()) {
38
+ await _promises2.default.rename(dest, tmpDir + ".old");
39
+ }
40
+ await _promises2.default.rename(result, dest);
41
+ await _promises2.default.rm(tmpDir + ".old", { recursive: true, force: true });
42
+ } finally {
43
+ await _promises2.default.rm(tmpDir, { recursive: true, force: true });
44
+ }
45
+ }
46
+ async function linkOrCp(src, dest) {
47
+ try {
48
+ await _promises2.default.link(src, dest);
49
+ } catch (e3) {
50
+ await _promises2.default.copyFile(src, dest);
51
+ }
52
+ }
53
+ async function sleep(ms) {
54
+ return new Promise((resolve) => setTimeout(resolve, ms));
55
+ }
56
+ async function withTimeout(promise, timeout) {
57
+ let timer;
58
+ const result = Promise.race([
59
+ promise,
60
+ new Promise((resolve, reject) => timer = setTimeout(() => reject(Error("Promise timed out")), timeout))
61
+ ]);
62
+ return result.finally(() => clearTimeout(timer));
63
+ }
64
+ async function pool(size, items, func) {
65
+ const { results } = await _promisepool.PromisePool.for(items).withConcurrency(size).handleError(async (error) => {
66
+ throw error;
67
+ }).useCorrespondingResults().process(func);
68
+ return results;
69
+ }
70
+ async function maybe(promise) {
71
+ return promise.then((r) => ({ success: true, result: r, error: void 0 })).catch((e) => ({ success: false, result: void 0, error: e }));
72
+ }
73
+
74
+ // src/apis.ts
75
+ var _lodash = require('lodash'); var _lodash2 = _interopRequireDefault(_lodash);
76
+
77
+
78
+ var _url = require('url');
79
+ function parseLinkHeader(linkHeader) {
80
+ function parseLinkData(linkData) {
81
+ return Object.fromEntries(
82
+ linkData.split(";").flatMap((x) => {
83
+ const partMatch = x.trim().match(/^([^=]+?)\s*=\s*"?([^"]+)"?$/);
84
+ return partMatch ? [[partMatch[1], partMatch[2]]] : [];
85
+ })
86
+ );
87
+ }
88
+ const linkDatas = linkHeader.split(/,\s*(?=<)/).flatMap((link) => {
89
+ const linkMatch = link.trim().match(/^<([^>]*)>(.*)$/);
90
+ if (linkMatch) {
91
+ return [{
92
+ url: linkMatch[1],
93
+ ...parseLinkData(linkMatch[2])
94
+ }];
95
+ } else {
96
+ return [];
97
+ }
98
+ }).filter((l) => l.rel);
99
+ return Object.fromEntries(linkDatas.map((l) => [l.rel, l]));
100
+ }
101
+ function createURL(url, base, params = {}) {
102
+ params = _lodash2.default.pickBy(params, (x) => x !== void 0);
103
+ const urlObj = new URL(url, base);
104
+ const searchParams = new URLSearchParams({ ...Object.fromEntries(urlObj.searchParams), ...params });
105
+ if ([...searchParams].length > 0) {
106
+ urlObj.search = "?" + searchParams;
107
+ }
108
+ return urlObj.toString();
109
+ }
110
+ async function fetchGitHubAPI(url, params = {}) {
111
+ url = createURL(url, "https://api.github.com", params);
112
+ const token = process.env.GITHUB_TOKEN;
113
+ const headers = token ? { Authorization: "Bearer " + token } : {};
114
+ const response = await _nodefetch2.default.call(void 0, url, { headers });
115
+ if (!response.ok) {
116
+ throw new Error(`GitHub API error: ${await response.text()}`);
117
+ }
118
+ return await response;
119
+ }
120
+ async function fetchGitHubAPIPaginated(url, params = {}) {
121
+ const results = [];
122
+ let next = createURL(url, "https://api.github.com", { per_page: 100, ...params });
123
+ while (next) {
124
+ const response = await fetchGitHubAPI(next);
125
+ results.push(...await response.json());
126
+ next = _optionalChain([parseLinkHeader, 'call', _4 => _4(_nullishCoalesce(response.headers.get("link"), () => ( ""))), 'access', _5 => _5.next, 'optionalAccess', _6 => _6.url]);
127
+ }
128
+ return results;
129
+ }
130
+ async function fetchObsidianAPI(url) {
131
+ url = createURL(url, "https://releases.obsidian.md");
132
+ const username = process.env.OBSIDIAN_USERNAME;
133
+ const password = process.env.OBSIDIAN_PASSWORD;
134
+ if (!username || !password) {
135
+ throw Error("OBSIDIAN_USERNAME or OBSIDIAN_PASSWORD environment variables are required to access the Obsidian API for beta versions.");
136
+ }
137
+ const response = await _nodefetch2.default.call(void 0, url, {
138
+ headers: {
139
+ // For some reason you have to set the User-Agent or it won't let you download
140
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36",
141
+ "Origin": "app://obsidian.md",
142
+ "Authorization": "Basic " + btoa(username + ":" + password)
143
+ }
144
+ });
145
+ return response;
146
+ }
147
+ async function fetchWithFileUrl(url) {
148
+ if (url.startsWith("file:")) {
149
+ return await _promises2.default.readFile(_url.fileURLToPath.call(void 0, url), "utf-8");
150
+ } else {
151
+ const response = await _nodefetch2.default.call(void 0, url);
152
+ if (response.ok) {
153
+ return response.text();
154
+ } else {
155
+ throw Error(`Request failed with ${response.status}: ${response.text()}`);
156
+ }
157
+ }
158
+ }
159
+
160
+ // src/chromeLocalStorage.ts
161
+
162
+ var _classiclevel = require('classic-level');
163
+ var ChromeLocalStorage = class {
164
+ /** Pass the path to the user data dir for Chrome/Electron. If it doesn't exist it will be created. */
165
+ constructor(userDataDir) {
166
+ this.userDataDir = userDataDir;
167
+ this.encodeKey = (domain, key) => `_${domain}\0${key}`;
168
+ this.decodeKey = (key) => key.slice(1).split("\0");
169
+ this.encodeValue = (value) => `${value}`;
170
+ this.decodeValue = (value) => value.slice(1);
171
+ this.db = new (0, _classiclevel.ClassicLevel)(_path2.default.join(userDataDir, "Local Storage/leveldb/"));
172
+ }
173
+ /**
174
+ * Get a value from localStorage
175
+ * @param domain Domain the value is under, e.g. "https://example.com" or "app://obsidian.md"
176
+ * @param key Key to retreive
177
+ */
178
+ async getItem(domain, key) {
179
+ const value = await this.db.get(this.encodeKey(domain, key));
180
+ return value === void 0 ? null : this.decodeValue(value);
181
+ }
182
+ /**
183
+ * Set a value in localStorage
184
+ * @param domain Domain the value is under, e.g. "https://example.com" or "app://obsidian.md"
185
+ * @param key Key to set
186
+ * @param value Value to set
187
+ */
188
+ async setItem(domain, key, value) {
189
+ await this.db.put(this.encodeKey(domain, key), this.encodeValue(value));
190
+ }
191
+ /**
192
+ * Removes a key from localStorage
193
+ * @param domain Domain the values is under, e.g. "https://example.com" or "app://obsidian.md"
194
+ * @param key key to remove.
195
+ */
196
+ async removeItem(domain, key) {
197
+ await this.db.del(this.encodeKey(domain, key));
198
+ }
199
+ /** Get all items in localStorage as [domain, key, value] tuples */
200
+ async getAllItems() {
201
+ const result = [];
202
+ for await (const pair of this.db.iterator()) {
203
+ if (pair[0].startsWith("_")) {
204
+ const [domain, key] = this.decodeKey(pair[0]);
205
+ const value = this.decodeValue(pair[1]);
206
+ result.push([domain, key, value]);
207
+ }
208
+ }
209
+ return result;
210
+ }
211
+ /**
212
+ * Write multiple values to localStorage in batch
213
+ * @param domain Domain the values are under, e.g. "https://example.com" or "app://obsidian.md"
214
+ * @param data key/value mapping to write
215
+ */
216
+ async setItems(domain, data) {
217
+ await this.db.batch(
218
+ Object.entries(data).map(([key, value]) => ({
219
+ type: "put",
220
+ key: this.encodeKey(domain, key),
221
+ value: this.encodeValue(value)
222
+ }))
223
+ );
224
+ }
225
+ /**
226
+ * Close the localStorage database.
227
+ */
228
+ async close() {
229
+ await this.db.close();
230
+ }
231
+ };
232
+
233
+ // src/launcherUtils.ts
234
+
235
+
236
+ var _util = require('util');
237
+
238
+ var _which = require('which'); var _which2 = _interopRequireDefault(_which);
239
+
240
+
241
+ var execFile = _util.promisify.call(void 0, _child_process2.default.execFile);
242
+ function normalizeGitHubRepo(repo) {
243
+ return repo.replace(/^(https?:\/\/)?(github.com\/)/, "");
244
+ }
245
+ async function extractObsidianAppImage(appImage, dest) {
246
+ await withTmpDir(dest, async (tmpDir) => {
247
+ await _promises2.default.chmod(appImage, 493);
248
+ await execFile(appImage, ["--appimage-extract"], { cwd: tmpDir });
249
+ return _path2.default.join(tmpDir, "squashfs-root");
250
+ });
251
+ }
252
+ async function extractObsidianExe(exe, appArch, dest) {
253
+ const path7z = await _which2.default.call(void 0, "7z", { nothrow: true });
254
+ if (!path7z) {
255
+ throw new Error(
256
+ "Downloading Obsidian for Windows requires 7zip to be installed and available on the PATH. You install it from https://www.7-zip.org and then add the install location to the PATH."
257
+ );
258
+ }
259
+ exe = _path2.default.resolve(exe);
260
+ const subArchive = _path2.default.join("$PLUGINSDIR", appArch + ".7z");
261
+ dest = _path2.default.resolve(dest);
262
+ await withTmpDir(dest, async (tmpDir) => {
263
+ const extractedInstaller = _path2.default.join(tmpDir, "installer");
264
+ await execFile(path7z, ["x", "-o" + extractedInstaller, exe, subArchive]);
265
+ const extractedObsidian = _path2.default.join(tmpDir, "obsidian");
266
+ await execFile(path7z, ["x", "-o" + extractedObsidian, _path2.default.join(extractedInstaller, subArchive)]);
267
+ return extractedObsidian;
268
+ });
269
+ }
270
+ async function extractObsidianDmg(dmg, dest) {
271
+ dest = _path2.default.resolve(dest);
272
+ await withTmpDir(dest, async (tmpDir) => {
273
+ const proc = await execFile("hdiutil", ["attach", "-nobrowse", "-readonly", dmg]);
274
+ const volume = proc.stdout.match(/\/Volumes\/.*$/m)[0];
275
+ const obsidianApp = _path2.default.join(volume, "Obsidian.app");
276
+ try {
277
+ await _promises2.default.cp(obsidianApp, tmpDir, { recursive: true, verbatimSymlinks: true });
278
+ } finally {
279
+ await execFile("hdiutil", ["detach", volume]);
280
+ }
281
+ return tmpDir;
282
+ });
283
+ }
284
+ function parseObsidianDesktopRelease(fileRelease, isBeta) {
285
+ return {
286
+ version: fileRelease.latestVersion,
287
+ minInstallerVersion: fileRelease.minimumVersion,
288
+ maxInstallerVersion: "",
289
+ // Will be set later
290
+ isBeta,
291
+ downloads: {
292
+ asar: fileRelease.downloadUrl
293
+ }
294
+ };
295
+ }
296
+ function parseObsidianGithubRelease(gitHubRelease) {
297
+ const version = gitHubRelease.name;
298
+ const assets = gitHubRelease.assets.map((a) => a.browser_download_url);
299
+ const downloads = {
300
+ appImage: assets.find((u) => u.match(`${version}.AppImage$`)),
301
+ appImageArm: assets.find((u) => u.match(`${version}-arm64.AppImage$`)),
302
+ apk: assets.find((u) => u.match(`${version}.apk$`)),
303
+ asar: assets.find((u) => u.match(`${version}.asar.gz$`)),
304
+ dmg: assets.find((u) => u.match(`${version}(-universal)?.dmg$`)),
305
+ exe: assets.find((u) => u.match(`${version}.exe$`))
306
+ };
307
+ return {
308
+ version,
309
+ gitHubRelease: gitHubRelease.html_url,
310
+ downloads: _lodash2.default.pickBy(downloads, (x) => x !== void 0)
311
+ };
312
+ }
313
+ function correctObsidianVersionInfo(versionInfo) {
314
+ const corrections = {};
315
+ if (_semver2.default.gte(versionInfo.version, "1.5.3") && _semver2.default.lt(versionInfo.minInstallerVersion, "1.1.9")) {
316
+ corrections.minInstallerVersion = "1.1.9";
317
+ }
318
+ return corrections;
319
+ }
320
+
321
+ // src/launcher.ts
322
+
323
+ var ObsidianLauncher = class {
324
+ /**
325
+ * Construct an ObsidianLauncher.
326
+ * @param cacheDir Path to the cache directory. Defaults to "OBSIDIAN_CACHE" env var or ".obsidian-cache".
327
+ * @param versionsUrl Custom `obsidian-versions.json` url. Can be a file URL.
328
+ * @param communityPluginsUrl Custom `community-plugins.json` url. Can be a file URL.
329
+ * @param communityThemes Custom `community-css-themes.json` url. Can be a file URL.
330
+ */
331
+ constructor(options = {}) {
332
+ this.cacheDir = _path2.default.resolve(_nullishCoalesce(_nullishCoalesce(options.cacheDir, () => ( process.env.OBSIDIAN_CACHE)), () => ( "./.obsidian-cache")));
333
+ const defaultVersionsUrl = "https://raw.githubusercontent.com/jesse-r-s-hines/wdio-obsidian-service/HEAD/obsidian-versions.json";
334
+ this.versionsUrl = _nullishCoalesce(options.versionsUrl, () => ( defaultVersionsUrl));
335
+ const defaultCommunityPluginsUrl = "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/HEAD/community-plugins.json";
336
+ this.communityPluginsUrl = _nullishCoalesce(options.communityPluginsUrl, () => ( defaultCommunityPluginsUrl));
337
+ const defaultCommunityThemesUrl = "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/HEAD/community-css-themes.json";
338
+ this.communityThemesUrl = _nullishCoalesce(options.communityThemesUrl, () => ( defaultCommunityThemesUrl));
339
+ this.metadataCache = {};
340
+ }
341
+ /**
342
+ * Returns file content fetched from url as JSON. Caches content to dest and uses that cache if its more recent than
343
+ * cacheDuration ms or if there are network errors.
344
+ */
345
+ async cachedFetch(url, dest, { cacheDuration = 30 * 60 * 1e3 } = {}) {
346
+ dest = _path2.default.resolve(dest);
347
+ if (!(dest in this.metadataCache)) {
348
+ let fileContent;
349
+ const mtime = await fileExists(dest) ? (await _promises2.default.stat(dest)).mtime : void 0;
350
+ if (mtime && (/* @__PURE__ */ new Date()).getTime() - mtime.getTime() < cacheDuration) {
351
+ fileContent = await _promises2.default.readFile(dest, "utf-8");
352
+ } else {
353
+ const request = await maybe(fetchWithFileUrl(url));
354
+ if (request.success) {
355
+ await _promises2.default.mkdir(_path2.default.dirname(dest), { recursive: true });
356
+ await withTmpDir(dest, async (tmpDir) => {
357
+ await _promises2.default.writeFile(_path2.default.join(tmpDir, "download.json"), request.result);
358
+ return _path2.default.join(tmpDir, "download.json");
359
+ });
360
+ fileContent = request.result;
361
+ } else if (await fileExists(dest)) {
362
+ console.warn(request.error);
363
+ console.warn(`Unable to download ${dest}, using cached file.`);
364
+ fileContent = await _promises2.default.readFile(dest, "utf-8");
365
+ } else {
366
+ throw request.error;
367
+ }
368
+ }
369
+ this.metadataCache[dest] = JSON.parse(fileContent);
370
+ }
371
+ return this.metadataCache[dest];
372
+ }
373
+ /** Get information about all available Obsidian versions. */
374
+ async getVersions() {
375
+ const dest = _path2.default.join(this.cacheDir, "obsidian-versions.json");
376
+ return (await this.cachedFetch(this.versionsUrl, dest)).versions;
377
+ }
378
+ /** Get information about all available community plugins. */
379
+ async getCommunityPlugins() {
380
+ const dest = _path2.default.join(this.cacheDir, "obsidian-community-plugins.json");
381
+ return await this.cachedFetch(this.communityPluginsUrl, dest);
382
+ }
383
+ /** Get information about all available community themes. */
384
+ async getCommunityThemes() {
385
+ const dest = _path2.default.join(this.cacheDir, "obsidian-community-css-themes.json");
386
+ return await this.cachedFetch(this.communityThemesUrl, dest);
387
+ }
388
+ /**
389
+ * Resolves Obsidian version strings to absolute obsidian versions.
390
+ * @param appVersion Obsidian version string or "latest" or "latest-beta"
391
+ * @param installerVersion Obsidian version string or "latest" or "earliest"
392
+ * @returns [appVersion, installerVersion] with any "latest" etc. resolved to specific versions.
393
+ */
394
+ async resolveVersions(appVersion, installerVersion = "latest") {
395
+ const versions = await this.getVersions();
396
+ if (appVersion == "latest") {
397
+ appVersion = versions.filter((v) => !v.isBeta).at(-1).version;
398
+ } else if (appVersion == "latest-beta") {
399
+ appVersion = versions.at(-1).version;
400
+ } else {
401
+ appVersion = _nullishCoalesce(_semver2.default.valid(appVersion), () => ( appVersion));
402
+ }
403
+ const appVersionInfo = versions.find((v) => v.version == appVersion);
404
+ if (!appVersionInfo) {
405
+ throw Error(`No Obsidian version ${appVersion} found`);
406
+ }
407
+ if (installerVersion == "latest") {
408
+ installerVersion = appVersionInfo.maxInstallerVersion;
409
+ } else if (installerVersion == "earliest") {
410
+ installerVersion = appVersionInfo.minInstallerVersion;
411
+ } else {
412
+ installerVersion = _nullishCoalesce(_semver2.default.valid(installerVersion), () => ( installerVersion));
413
+ }
414
+ const installerVersionInfo = versions.find((v) => v.version == installerVersion);
415
+ if (!installerVersionInfo || !installerVersionInfo.chromeVersion) {
416
+ throw Error(`No Obsidian installer for version ${installerVersion} found`);
417
+ }
418
+ if (_semver2.default.lt(installerVersionInfo.version, appVersionInfo.minInstallerVersion)) {
419
+ throw Error(
420
+ `Installer and app versions incompatible: minInstallerVersion of v${appVersionInfo.version} is ${appVersionInfo.minInstallerVersion}, but v${installerVersionInfo.version} specified`
421
+ );
422
+ }
423
+ return [appVersionInfo.version, installerVersionInfo.version];
424
+ }
425
+ /** Gets details about an Obsidian version */
426
+ async getVersionInfo(version) {
427
+ version = (await this.resolveVersions(version))[0];
428
+ const result = (await this.getVersions()).find((v) => v.version == version);
429
+ if (!result) {
430
+ throw Error(`No Obsidian version ${version} found`);
431
+ }
432
+ return result;
433
+ }
434
+ /**
435
+ * Downloads the Obsidian installer for the given version and platform. Returns the file path.
436
+ * @param installerVersion Version to download.
437
+ */
438
+ async downloadInstaller(installerVersion) {
439
+ const installerVersionInfo = await this.getVersionInfo(installerVersion);
440
+ return await this.downloadInstallerFromVersionInfo(installerVersionInfo);
441
+ }
442
+ /**
443
+ * Helper for downloadInstaller that doesn't require the obsidian-versions.json file so it can be used in
444
+ * updateObsidianVersionInfos
445
+ */
446
+ async downloadInstallerFromVersionInfo(versionInfo) {
447
+ const installerVersion = versionInfo.version;
448
+ const { platform, arch } = process;
449
+ const cacheDir = _path2.default.join(this.cacheDir, `obsidian-installer/${platform}-${arch}/Obsidian-${installerVersion}`);
450
+ let installerPath;
451
+ let downloader;
452
+ if (platform == "linux") {
453
+ installerPath = _path2.default.join(cacheDir, "obsidian");
454
+ let installerUrl;
455
+ if (arch.startsWith("arm")) {
456
+ installerUrl = versionInfo.downloads.appImageArm;
457
+ } else {
458
+ installerUrl = versionInfo.downloads.appImage;
459
+ }
460
+ if (installerUrl) {
461
+ downloader = async (tmpDir) => {
462
+ const appImage = _path2.default.join(tmpDir, "Obsidian.AppImage");
463
+ await _promises2.default.writeFile(appImage, (await _nodefetch2.default.call(void 0, installerUrl)).body);
464
+ const obsidianFolder = _path2.default.join(tmpDir, "Obsidian");
465
+ await extractObsidianAppImage(appImage, obsidianFolder);
466
+ return obsidianFolder;
467
+ };
468
+ }
469
+ } else if (platform == "win32") {
470
+ installerPath = _path2.default.join(cacheDir, "Obsidian.exe");
471
+ const installerUrl = versionInfo.downloads.exe;
472
+ let appArch;
473
+ if (arch == "x64") {
474
+ appArch = "app-64";
475
+ } else if (arch == "ia32") {
476
+ appArch = "app-32";
477
+ } else if (arch.startsWith("arm")) {
478
+ appArch = "app-arm64";
479
+ }
480
+ if (installerUrl && appArch) {
481
+ downloader = async (tmpDir) => {
482
+ const installerExecutable = _path2.default.join(tmpDir, "Obsidian.exe");
483
+ await _promises2.default.writeFile(installerExecutable, (await _nodefetch2.default.call(void 0, installerUrl)).body);
484
+ const obsidianFolder = _path2.default.join(tmpDir, "Obsidian");
485
+ await extractObsidianExe(installerExecutable, appArch, obsidianFolder);
486
+ return obsidianFolder;
487
+ };
488
+ }
489
+ } else if (platform == "darwin") {
490
+ installerPath = _path2.default.join(cacheDir, "Contents/MacOS/Obsidian");
491
+ const installerUrl = versionInfo.downloads.dmg;
492
+ if (installerUrl) {
493
+ downloader = async (tmpDir) => {
494
+ const dmg = _path2.default.join(tmpDir, "Obsidian.dmg");
495
+ await _promises2.default.writeFile(dmg, (await _nodefetch2.default.call(void 0, installerUrl)).body);
496
+ const obsidianFolder = _path2.default.join(tmpDir, "Obsidian");
497
+ await extractObsidianDmg(dmg, obsidianFolder);
498
+ return obsidianFolder;
499
+ };
500
+ }
501
+ } else {
502
+ throw Error(`Unsupported platform ${platform}`);
503
+ }
504
+ if (!downloader) {
505
+ throw Error(`No Obsidian installer download available for v${installerVersion} ${platform} ${arch}`);
506
+ }
507
+ if (!await fileExists(installerPath)) {
508
+ console.log(`Downloading Obsidian installer v${installerVersion}...`);
509
+ await _promises2.default.mkdir(_path2.default.dirname(cacheDir), { recursive: true });
510
+ await withTmpDir(cacheDir, downloader);
511
+ }
512
+ return installerPath;
513
+ }
514
+ /**
515
+ * Downloads the Obsidian asar for the given version and platform. Returns the file path.
516
+ * @param appVersion Version to download.
517
+ */
518
+ async downloadApp(appVersion) {
519
+ const appVersionInfo = await this.getVersionInfo(appVersion);
520
+ const appUrl = appVersionInfo.downloads.asar;
521
+ if (!appUrl) {
522
+ throw Error(`No asar found for Obsidian version ${appVersion}`);
523
+ }
524
+ const appPath = _path2.default.join(this.cacheDir, "obsidian-app", `obsidian-${appVersionInfo.version}.asar`);
525
+ if (!await fileExists(appPath)) {
526
+ console.log(`Downloading Obsidian app v${appVersion} ...`);
527
+ await _promises2.default.mkdir(_path2.default.dirname(appPath), { recursive: true });
528
+ await withTmpDir(appPath, async (tmpDir) => {
529
+ const isInsidersBuild = new URL(appUrl).hostname.endsWith(".obsidian.md");
530
+ const response = isInsidersBuild ? await fetchObsidianAPI(appUrl) : await _nodefetch2.default.call(void 0, appUrl);
531
+ const archive = _path2.default.join(tmpDir, "app.asar.gz");
532
+ const asar = _path2.default.join(tmpDir, "app.asar");
533
+ await _promises2.default.writeFile(archive, response.body);
534
+ await _promises3.pipeline.call(void 0, _fs2.default.createReadStream(archive), _zlib2.default.createGunzip(), _fs2.default.createWriteStream(asar));
535
+ return asar;
536
+ });
537
+ }
538
+ return appPath;
539
+ }
540
+ /**
541
+ * Downloads chromedriver for the given Obsidian version.
542
+ *
543
+ * wdio will download chromedriver from the Chrome for Testing API automatically (see
544
+ * https://github.com/GoogleChromeLabs/chrome-for-testing#json-api-endpoints). However, Google has only put
545
+ * chromedriver since v115.0.5763.0 in that API, so wdio can't download older versions of chromedriver. As of
546
+ * Obsidian v1.7.7, minInstallerVersion is v0.14.5 which runs on chromium v100.0.4896.75. Here we download
547
+ * chromedriver for older versions ourselves using the @electron/get package which fetches it from
548
+ * https://github.com/electron/electron/releases.
549
+ */
550
+ async downloadChromedriver(installerVersion) {
551
+ const versionInfo = await this.getVersionInfo(installerVersion);
552
+ const electronVersion = versionInfo.electronVersion;
553
+ if (!electronVersion) {
554
+ throw Error(`${installerVersion} is not an Obsidian installer version.`);
555
+ }
556
+ const chromedriverZipPath = await _get.downloadArtifact.call(void 0, {
557
+ version: electronVersion,
558
+ artifactName: "chromedriver",
559
+ cacheRoot: _path2.default.join(this.cacheDir, "chromedriver-legacy"),
560
+ unsafelyDisableChecksums: true
561
+ // the checksums are slow and run even on cache hit.
562
+ });
563
+ let chromedriverPath;
564
+ if (process.platform == "win32") {
565
+ chromedriverPath = _path2.default.join(_path2.default.dirname(chromedriverZipPath), "chromedriver.exe");
566
+ } else {
567
+ chromedriverPath = _path2.default.join(_path2.default.dirname(chromedriverZipPath), "chromedriver");
568
+ }
569
+ if (!await fileExists(chromedriverPath)) {
570
+ console.log(`Downloading legacy chromedriver for electron ${electronVersion} ...`);
571
+ await withTmpDir(chromedriverPath, async (tmpDir) => {
572
+ await _extractzip2.default.call(void 0, chromedriverZipPath, { dir: tmpDir });
573
+ return _path2.default.join(tmpDir, _path2.default.basename(chromedriverPath));
574
+ });
575
+ }
576
+ return chromedriverPath;
577
+ }
578
+ /** Gets the latest version of a plugin. */
579
+ async getLatestPluginVersion(repo) {
580
+ repo = normalizeGitHubRepo(repo);
581
+ const manifestUrl = `https://raw.githubusercontent.com/${repo}/HEAD/manifest.json`;
582
+ const cacheDest = _path2.default.join(this.cacheDir, "obsidian-plugins", repo, "latest.json");
583
+ const manifest = await this.cachedFetch(manifestUrl, cacheDest);
584
+ return manifest.version;
585
+ }
586
+ /**
587
+ * Downloads a plugin from a GitHub repo to the cache.
588
+ * @param repo Repo
589
+ * @param version Version of the plugin to install or "latest"
590
+ * @returns path to the downloaded plugin
591
+ */
592
+ async downloadGitHubPlugin(repo, version = "latest") {
593
+ repo = normalizeGitHubRepo(repo);
594
+ if (version == "latest") {
595
+ version = await this.getLatestPluginVersion(repo);
596
+ }
597
+ if (!_semver2.default.valid(version)) {
598
+ throw Error(`Invalid version "${version}"`);
599
+ }
600
+ version = _semver2.default.valid(version);
601
+ const pluginDir = _path2.default.join(this.cacheDir, "obsidian-plugins", repo, version);
602
+ if (!await fileExists(pluginDir)) {
603
+ await _promises2.default.mkdir(_path2.default.dirname(pluginDir), { recursive: true });
604
+ await withTmpDir(pluginDir, async (tmpDir) => {
605
+ const assetsToDownload = { "manifest.json": true, "main.js": true, "styles.css": false };
606
+ await Promise.all(
607
+ Object.entries(assetsToDownload).map(async ([file, required]) => {
608
+ const url = `https://github.com/${repo}/releases/download/${version}/${file}`;
609
+ const response = await _nodefetch2.default.call(void 0, url);
610
+ if (response.ok) {
611
+ await _promises2.default.writeFile(_path2.default.join(tmpDir, file), response.body);
612
+ } else if (required) {
613
+ throw Error(`No ${file} found for ${repo} version ${version}`);
614
+ }
615
+ })
616
+ );
617
+ return tmpDir;
618
+ });
619
+ }
620
+ return pluginDir;
621
+ }
622
+ /**
623
+ * Downloads a community plugin to the cache.
624
+ * @param id Id of the plugin
625
+ * @param version Version of the plugin to install, or "latest"
626
+ * @returns path to the downloaded plugin
627
+ */
628
+ async downloadCommunityPlugin(id, version = "latest") {
629
+ const communityPlugins = await this.getCommunityPlugins();
630
+ const pluginInfo = communityPlugins.find((p) => p.id == id);
631
+ if (!pluginInfo) {
632
+ throw Error(`No plugin with id ${id} found.`);
633
+ }
634
+ return await this.downloadGitHubPlugin(pluginInfo.repo, version);
635
+ }
636
+ /**
637
+ * Downloads a list of plugins to the cache and returns a list of LocalPluginEntry with the downloaded paths.
638
+ * Also adds the `id` property to the plugins based on the manifest.
639
+ *
640
+ * You can download plugins from GitHub using `{repo: "org/repo"}` and community plugins using `{id: 'plugin-id'}`.
641
+ * Local plugins will just be passed through.
642
+ */
643
+ async downloadPlugins(plugins) {
644
+ return await Promise.all(
645
+ plugins.map(async (plugin) => {
646
+ let pluginPath;
647
+ if (typeof plugin == "string") {
648
+ pluginPath = plugin;
649
+ } else if ("path" in plugin) {
650
+ ;
651
+ pluginPath = plugin.path;
652
+ } else if ("repo" in plugin) {
653
+ pluginPath = await this.downloadGitHubPlugin(plugin.repo, plugin.version);
654
+ } else if ("id" in plugin) {
655
+ pluginPath = await this.downloadCommunityPlugin(plugin.id, plugin.version);
656
+ } else {
657
+ throw Error("You must specify one of plugin path, repo, or id");
658
+ }
659
+ let pluginId = typeof plugin == "object" && "id" in plugin ? plugin.id : void 0;
660
+ if (!pluginId) {
661
+ const manifestPath = _path2.default.join(pluginPath, "manifest.json");
662
+ pluginId = JSON.parse(await _promises2.default.readFile(manifestPath, "utf8").catch(() => "{}")).id;
663
+ if (!pluginId) {
664
+ throw Error(`${pluginPath}/manifest.json missing or malformed.`);
665
+ }
666
+ }
667
+ const enabled = typeof plugin == "string" ? true : plugin.enabled;
668
+ return { path: pluginPath, id: pluginId, enabled };
669
+ })
670
+ );
671
+ }
672
+ /** Gets the latest version of a theme. */
673
+ async getLatestThemeVersion(repo) {
674
+ repo = normalizeGitHubRepo(repo);
675
+ const manifestUrl = `https://raw.githubusercontent.com/${repo}/HEAD/manifest.json`;
676
+ const cacheDest = _path2.default.join(this.cacheDir, "obsidian-themes", repo, "latest.json");
677
+ const manifest = await this.cachedFetch(manifestUrl, cacheDest);
678
+ return manifest.version;
679
+ }
680
+ /**
681
+ * Downloads a theme from a GitHub repo to the cache.
682
+ * @param repo Repo
683
+ * @returns path to the downloaded theme
684
+ */
685
+ async downloadGitHubTheme(repo) {
686
+ repo = normalizeGitHubRepo(repo);
687
+ const version = await this.getLatestThemeVersion(repo);
688
+ const themeDir = _path2.default.join(this.cacheDir, "obsidian-themes", repo, version);
689
+ if (!await fileExists(themeDir)) {
690
+ await _promises2.default.mkdir(_path2.default.dirname(themeDir), { recursive: true });
691
+ await withTmpDir(themeDir, async (tmpDir) => {
692
+ const assetsToDownload = ["manifest.json", "theme.css"];
693
+ await Promise.all(
694
+ assetsToDownload.map(async (file) => {
695
+ const url = `https://raw.githubusercontent.com/${repo}/HEAD/${file}`;
696
+ const response = await _nodefetch2.default.call(void 0, url);
697
+ if (response.ok) {
698
+ await _promises2.default.writeFile(_path2.default.join(tmpDir, file), response.body);
699
+ } else {
700
+ throw Error(`No ${file} found for ${repo}`);
701
+ }
702
+ })
703
+ );
704
+ return tmpDir;
705
+ });
706
+ }
707
+ return themeDir;
708
+ }
709
+ /**
710
+ * Downloads a community theme to the cache.
711
+ * @param name name of the theme
712
+ * @returns path to the downloaded theme
713
+ */
714
+ async downloadCommunityTheme(name) {
715
+ const communityThemes = await this.getCommunityThemes();
716
+ const themeInfo = communityThemes.find((p) => p.name == name);
717
+ if (!themeInfo) {
718
+ throw Error(`No theme with name ${name} found.`);
719
+ }
720
+ return await this.downloadGitHubTheme(themeInfo.repo);
721
+ }
722
+ /**
723
+ * Downloads a list of themes to the cache and returns a list of LocalThemeEntry with the downloaded paths.
724
+ * Also adds the `name` property to the plugins based on the manifest.
725
+ *
726
+ * You can download themes from GitHub using `{repo: "org/repo"}` and community themes using `{name: 'theme-name'}`.
727
+ * Local themes will just be passed through.
728
+ */
729
+ async downloadThemes(themes) {
730
+ return await Promise.all(
731
+ themes.map(async (theme) => {
732
+ let themePath;
733
+ if (typeof theme == "string") {
734
+ themePath = theme;
735
+ } else if ("path" in theme) {
736
+ ;
737
+ themePath = theme.path;
738
+ } else if ("repo" in theme) {
739
+ themePath = await this.downloadGitHubTheme(theme.repo);
740
+ } else if ("name" in theme) {
741
+ themePath = await this.downloadCommunityTheme(theme.name);
742
+ } else {
743
+ throw Error("You must specify one of theme path, repo, or name");
744
+ }
745
+ let themeName = typeof theme == "object" && "name" in theme ? theme.name : void 0;
746
+ if (!themeName) {
747
+ const manifestPath = _path2.default.join(themePath, "manifest.json");
748
+ themeName = JSON.parse(await _promises2.default.readFile(manifestPath, "utf8").catch(() => "{}")).name;
749
+ if (!themeName) {
750
+ throw Error(`${themePath}/manifest.json missing or malformed.`);
751
+ }
752
+ }
753
+ const enabled = typeof theme == "string" ? true : theme.enabled;
754
+ return { path: themePath, name: themeName, enabled };
755
+ })
756
+ );
757
+ }
758
+ /**
759
+ * Installs plugins into an Obsidian vault.
760
+ * @param vault Path to the vault to install the plugin in.
761
+ * @param plugins List plugins paths to install.
762
+ */
763
+ async installPlugins(vault, plugins) {
764
+ const downloadedPlugins = await this.downloadPlugins(plugins);
765
+ const obsidianDir = _path2.default.join(vault, ".obsidian");
766
+ await _promises2.default.mkdir(obsidianDir, { recursive: true });
767
+ const enabledPluginsPath = _path2.default.join(obsidianDir, "community-plugins.json");
768
+ let originalEnabledPlugins = [];
769
+ if (await fileExists(enabledPluginsPath)) {
770
+ originalEnabledPlugins = JSON.parse(await _promises2.default.readFile(enabledPluginsPath, "utf-8"));
771
+ }
772
+ let enabledPlugins = [...originalEnabledPlugins];
773
+ for (const { path: pluginPath, enabled = true } of downloadedPlugins) {
774
+ const manifestPath = _path2.default.join(pluginPath, "manifest.json");
775
+ const pluginId = JSON.parse(await _promises2.default.readFile(manifestPath, "utf8").catch(() => "{}")).id;
776
+ if (!pluginId) {
777
+ throw Error(`${manifestPath} missing or malformed.`);
778
+ }
779
+ const pluginDest = _path2.default.join(obsidianDir, "plugins", pluginId);
780
+ await _promises2.default.rm(pluginDest, { recursive: true, force: true });
781
+ await _promises2.default.mkdir(pluginDest, { recursive: true });
782
+ const files = {
783
+ "manifest.json": true,
784
+ "main.js": true,
785
+ "styles.css": false,
786
+ "data.json": false
787
+ };
788
+ for (const [file, required] of Object.entries(files)) {
789
+ if (await fileExists(_path2.default.join(pluginPath, file))) {
790
+ await linkOrCp(_path2.default.join(pluginPath, file), _path2.default.join(pluginDest, file));
791
+ } else if (required) {
792
+ throw Error(`${pluginPath}/${file} missing.`);
793
+ }
794
+ }
795
+ const pluginAlreadyListed = enabledPlugins.includes(pluginId);
796
+ if (enabled && !pluginAlreadyListed) {
797
+ enabledPlugins.push(pluginId);
798
+ } else if (!enabled && pluginAlreadyListed) {
799
+ enabledPlugins = enabledPlugins.filter((p) => p != pluginId);
800
+ }
801
+ }
802
+ if (!_lodash2.default.isEqual(enabledPlugins, originalEnabledPlugins)) {
803
+ await _promises2.default.writeFile(enabledPluginsPath, JSON.stringify(enabledPlugins, void 0, 2));
804
+ }
805
+ }
806
+ /**
807
+ * Installs themes into an obsidian vault.
808
+ * @param vault Path to the theme to install the plugin in.
809
+ * @param themes: List of themes to install.
810
+ */
811
+ async installThemes(vault, themes) {
812
+ const downloadedThemes = await this.downloadThemes(themes);
813
+ const obsidianDir = _path2.default.join(vault, ".obsidian");
814
+ await _promises2.default.mkdir(obsidianDir, { recursive: true });
815
+ let enabledTheme = void 0;
816
+ for (const { path: themePath, enabled = true } of downloadedThemes) {
817
+ const manifestPath = _path2.default.join(themePath, "manifest.json");
818
+ const cssPath = _path2.default.join(themePath, "theme.css");
819
+ const themeName = JSON.parse(await _promises2.default.readFile(manifestPath, "utf8").catch(() => "{}")).name;
820
+ if (!themeName) {
821
+ throw Error(`${manifestPath} missing or malformed.`);
822
+ }
823
+ if (!await fileExists(cssPath)) {
824
+ throw Error(`${cssPath} missing.`);
825
+ }
826
+ const themeDest = _path2.default.join(obsidianDir, "themes", themeName);
827
+ await _promises2.default.rm(themeDest, { recursive: true, force: true });
828
+ await _promises2.default.mkdir(themeDest, { recursive: true });
829
+ await linkOrCp(manifestPath, _path2.default.join(themeDest, "manifest.json"));
830
+ await linkOrCp(cssPath, _path2.default.join(themeDest, "theme.css"));
831
+ if (enabledTheme && enabled) {
832
+ throw Error("You can only have one enabled theme.");
833
+ } else if (enabled) {
834
+ enabledTheme = themeName;
835
+ }
836
+ }
837
+ if (themes.length > 0) {
838
+ const appearancePath = _path2.default.join(obsidianDir, "appearance.json");
839
+ let appearance = {};
840
+ if (await fileExists(appearancePath)) {
841
+ appearance = JSON.parse(await _promises2.default.readFile(appearancePath, "utf-8"));
842
+ }
843
+ appearance.cssTheme = _nullishCoalesce(enabledTheme, () => ( ""));
844
+ await _promises2.default.writeFile(appearancePath, JSON.stringify(appearance, void 0, 2));
845
+ }
846
+ }
847
+ /**
848
+ * Sets up the config dir to use for the --user-data-dir in obsidian. Returns the path to the created config dir.
849
+ *
850
+ * @param appVersion Obsidian version string.
851
+ * @param installerVersion Obsidian version string.
852
+ * @param appPath Path to the asar file to install.
853
+ * @param vault Path to the vault to open in Obsidian.
854
+ * @param plugins List of plugins to install in the vault.
855
+ * @param themes List of themes to install in the vault.
856
+ * @param dest Destination path for the config dir. If omitted it will create it under `/tmp`.
857
+ */
858
+ async setupConfigDir(params) {
859
+ const [appVersion, installerVersion] = await this.resolveVersions(params.appVersion, params.installerVersion);
860
+ const configDir = await _asyncNullishCoalesce(params.dest, async () => ( await _promises2.default.mkdtemp(_path2.default.join(_os2.default.tmpdir(), "obs-launcher-config-"))));
861
+ let obsidianJson = {
862
+ updateDisabled: true
863
+ // Prevents Obsidian trying to auto-update on boot.
864
+ };
865
+ let localStorageData = {
866
+ "most-recently-installed-version": appVersion
867
+ // prevents the changelog page on boot
868
+ };
869
+ if (params.vault !== void 0) {
870
+ await this.installPlugins(params.vault, _nullishCoalesce(params.plugins, () => ( [])));
871
+ await this.installThemes(params.vault, _nullishCoalesce(params.themes, () => ( [])));
872
+ const vaultId = "1234567890abcdef";
873
+ obsidianJson = {
874
+ ...obsidianJson,
875
+ vaults: {
876
+ [vaultId]: {
877
+ path: _path2.default.resolve(params.vault),
878
+ ts: (/* @__PURE__ */ new Date()).getTime(),
879
+ open: true
880
+ }
881
+ }
882
+ };
883
+ localStorageData = {
884
+ ...localStorageData,
885
+ [`enable-plugin-${vaultId}`]: "true"
886
+ // Disable "safe mode" and enable plugins
887
+ };
888
+ }
889
+ await _promises2.default.writeFile(_path2.default.join(configDir, "obsidian.json"), JSON.stringify(obsidianJson));
890
+ if (params.appPath) {
891
+ await linkOrCp(params.appPath, _path2.default.join(configDir, _path2.default.basename(params.appPath)));
892
+ } else if (appVersion != installerVersion) {
893
+ throw Error("You must specify app path if appVersion != installerVersion");
894
+ }
895
+ const localStorage = new ChromeLocalStorage(configDir);
896
+ await localStorage.setItems("app://obsidian.md", localStorageData);
897
+ await localStorage.close();
898
+ return configDir;
899
+ }
900
+ /**
901
+ * Copies a vault to a temporary directory.
902
+ * @returns Path to the created tmpDir.
903
+ */
904
+ async copyVault(src) {
905
+ const dest = await _promises2.default.mkdtemp(_path2.default.join(_os2.default.tmpdir(), "obs-launcher-vault-"));
906
+ await _promises2.default.cp(src, dest, { recursive: true });
907
+ return dest;
908
+ }
909
+ /**
910
+ * Downloads and launches Obsidian with a sandboxed config dir. Optionally open a specific vault and install plugins
911
+ * and themes first.
912
+ *
913
+ * @param appVersion Obsidian version string.
914
+ * @param installerVersion Obsidian version string.
915
+ * @param vault Path to the vault to open in Obsidian.
916
+ * @param plugins List of plugins to install in the vault.
917
+ * @param themes List of themes to install in the vault.
918
+ * @param dest Destination path for the config dir. If omitted it will create it under `/tmp`.
919
+ * @param args CLI args to pass to Obsidian
920
+ * @param spawnOptions Options to pass to `spawn`.
921
+ * @returns The launched child process and the created config dir.
922
+ */
923
+ async launch(params) {
924
+ const [appVersion, installerVersion] = await this.resolveVersions(params.appVersion, params.installerVersion);
925
+ const appPath = await this.downloadApp(appVersion);
926
+ const installerPath = await this.downloadInstaller(installerVersion);
927
+ const configDir = await this.setupConfigDir({
928
+ appVersion,
929
+ installerVersion,
930
+ appPath,
931
+ vault: params.vault,
932
+ plugins: params.plugins,
933
+ themes: params.themes
934
+ });
935
+ const proc = _child_process2.default.spawn(installerPath, [
936
+ `--user-data-dir=${configDir}`,
937
+ ..._nullishCoalesce(params.args, () => ( []))
938
+ ], {
939
+ ...params.spawnOptions
940
+ });
941
+ return [proc, configDir];
942
+ }
943
+ /**
944
+ * Extract electron and chrome versions for an Obsidian version.
945
+ */
946
+ async getDependencyVersions(versionInfo) {
947
+ const binary = await this.downloadInstallerFromVersionInfo(versionInfo);
948
+ console.log(`${versionInfo.version}: Extracting electron & chrome versions...`);
949
+ const configDir = await _promises2.default.mkdtemp(_path2.default.join(_os2.default.tmpdir(), `fetch-obsidian-versions-`));
950
+ const proc = _child_process2.default.spawn(binary, [
951
+ `--remote-debugging-port=0`,
952
+ // 0 will make it choose a random available port
953
+ "--test-type=webdriver",
954
+ `--user-data-dir=${configDir}`,
955
+ "--no-sandbox",
956
+ // Workaround for SUID issue, see https://github.com/electron/electron/issues/42510
957
+ "--headless"
958
+ ]);
959
+ const procExit = new Promise((resolve) => proc.on("exit", (code) => resolve(_nullishCoalesce(code, () => ( -1)))));
960
+ let dependencyVersions;
961
+ try {
962
+ const portPromise = new Promise((resolve, reject) => {
963
+ procExit.then(() => reject("Processed ended without opening a port"));
964
+ proc.stderr.on("data", (data) => {
965
+ const port2 = _optionalChain([data, 'access', _7 => _7.toString, 'call', _8 => _8(), 'access', _9 => _9.match, 'call', _10 => _10(/ws:\/\/[\w.]+?:(\d+)/), 'optionalAccess', _11 => _11[1]]);
966
+ if (port2) {
967
+ resolve(Number(port2));
968
+ }
969
+ });
970
+ });
971
+ const port = await maybe(withTimeout(portPromise, 10 * 1e3));
972
+ if (!port.success) {
973
+ throw new Error("Timed out waiting for Chrome DevTools protocol port");
974
+ }
975
+ const client = await _chromeremoteinterface2.default.call(void 0, { port: port.result });
976
+ const response = await client.Runtime.evaluate({ expression: "JSON.stringify(process.versions)" });
977
+ dependencyVersions = JSON.parse(response.result.value);
978
+ await client.close();
979
+ } finally {
980
+ proc.kill("SIGTERM");
981
+ const timeout = await maybe(withTimeout(procExit, 4 * 1e3));
982
+ if (!timeout.success) {
983
+ console.log(`${versionInfo.version}: Stuck process ${proc.pid}, using SIGKILL`);
984
+ proc.kill("SIGKILL");
985
+ }
986
+ await procExit;
987
+ await sleep(1e3);
988
+ await _promises2.default.rm(configDir, { recursive: true, force: true });
989
+ }
990
+ if (!_optionalChain([dependencyVersions, 'optionalAccess', _12 => _12.electron]) || !_optionalChain([dependencyVersions, 'optionalAccess', _13 => _13.chrome])) {
991
+ throw Error(`Failed to extract electron and chrome versions for ${versionInfo.version}`);
992
+ }
993
+ return {
994
+ ...versionInfo,
995
+ electronVersion: dependencyVersions.electron,
996
+ chromeVersion: dependencyVersions.chrome,
997
+ nodeVersion: dependencyVersions.node
998
+ };
999
+ }
1000
+ /**
1001
+ * Updates the info obsidian-versions.json. The obsidian-versions.json file is used in other launcher commands
1002
+ * and in wdio-obsidian-service to get metadata about Obsidian versions in one place such as minInstallerVersion and
1003
+ * the internal electron version.
1004
+ */
1005
+ async updateObsidianVersionInfos(original, { maxInstances = 1 } = {}) {
1006
+ const repo = "obsidianmd/obsidian-releases";
1007
+ let commitHistory = await fetchGitHubAPIPaginated(`repos/${repo}/commits`, {
1008
+ path: "desktop-releases.json",
1009
+ since: _optionalChain([original, 'optionalAccess', _14 => _14.metadata, 'access', _15 => _15.commit_date])
1010
+ });
1011
+ commitHistory.reverse();
1012
+ if (original) {
1013
+ commitHistory = _lodash2.default.takeRightWhile(commitHistory, (c) => c.sha != original.metadata.commit_sha);
1014
+ }
1015
+ const fileHistory = await pool(
1016
+ 8,
1017
+ commitHistory,
1018
+ (commit) => _nodefetch2.default.call(void 0, `https://raw.githubusercontent.com/${repo}/${commit.sha}/desktop-releases.json`).then((r) => r.json())
1019
+ );
1020
+ const githubReleases = await fetchGitHubAPIPaginated(`repos/${repo}/releases`);
1021
+ const versionMap = _lodash2.default.keyBy(
1022
+ _nullishCoalesce(_optionalChain([original, 'optionalAccess', _16 => _16.versions]), () => ( [])),
1023
+ (v) => v.version
1024
+ );
1025
+ for (const { beta, ...current } of fileHistory) {
1026
+ if (beta && (!versionMap[beta.latestVersion] || versionMap[beta.latestVersion].isBeta)) {
1027
+ versionMap[beta.latestVersion] = _lodash2.default.merge(
1028
+ {},
1029
+ versionMap[beta.latestVersion],
1030
+ parseObsidianDesktopRelease(beta, true)
1031
+ );
1032
+ }
1033
+ versionMap[current.latestVersion] = _lodash2.default.merge(
1034
+ {},
1035
+ versionMap[current.latestVersion],
1036
+ parseObsidianDesktopRelease(current, false)
1037
+ );
1038
+ }
1039
+ for (const release of githubReleases) {
1040
+ if (versionMap.hasOwnProperty(release.name)) {
1041
+ versionMap[release.name] = _lodash2.default.merge({}, versionMap[release.name], parseObsidianGithubRelease(release));
1042
+ }
1043
+ }
1044
+ const dependencyVersions = await pool(
1045
+ maxInstances,
1046
+ Object.values(versionMap).filter((v) => _optionalChain([v, 'access', _17 => _17.downloads, 'optionalAccess', _18 => _18.appImage]) && !v.chromeVersion),
1047
+ (v) => this.getDependencyVersions(v)
1048
+ );
1049
+ for (const deps of dependencyVersions) {
1050
+ versionMap[deps.version] = _lodash2.default.merge({}, versionMap[deps.version], deps);
1051
+ }
1052
+ let maxInstallerVersion = "0.0.0";
1053
+ for (const version of Object.keys(versionMap).sort(_semver2.default.compare)) {
1054
+ if (versionMap[version].downloads.appImage) {
1055
+ maxInstallerVersion = version;
1056
+ }
1057
+ versionMap[version] = _lodash2.default.merge(
1058
+ {},
1059
+ versionMap[version],
1060
+ correctObsidianVersionInfo(versionMap[version]),
1061
+ { maxInstallerVersion }
1062
+ );
1063
+ }
1064
+ const versionInfos = Object.values(versionMap);
1065
+ versionInfos.sort((a, b) => _semver2.default.compare(a.version, b.version));
1066
+ const result = {
1067
+ metadata: {
1068
+ commit_date: _nullishCoalesce(_optionalChain([commitHistory, 'access', _19 => _19.at, 'call', _20 => _20(-1), 'optionalAccess', _21 => _21.commit, 'access', _22 => _22.committer, 'access', _23 => _23.date]), () => ( _optionalChain([original, 'optionalAccess', _24 => _24.metadata, 'access', _25 => _25.commit_date]))),
1069
+ commit_sha: _nullishCoalesce(_optionalChain([commitHistory, 'access', _26 => _26.at, 'call', _27 => _27(-1), 'optionalAccess', _28 => _28.sha]), () => ( _optionalChain([original, 'optionalAccess', _29 => _29.metadata, 'access', _30 => _30.commit_sha]))),
1070
+ timestamp: _nullishCoalesce(_optionalChain([original, 'optionalAccess', _31 => _31.metadata, 'access', _32 => _32.timestamp]), () => ( ""))
1071
+ // set down below
1072
+ },
1073
+ versions: versionInfos
1074
+ };
1075
+ const dayMs = 24 * 60 * 60 * 1e3;
1076
+ const timeSinceLastUpdate = (/* @__PURE__ */ new Date()).getTime() - new Date(_nullishCoalesce(_optionalChain([original, 'optionalAccess', _33 => _33.metadata, 'access', _34 => _34.timestamp]), () => ( 0))).getTime();
1077
+ if (!_lodash2.default.isEqual(original, result) || timeSinceLastUpdate > 29 * dayMs) {
1078
+ result.metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
1079
+ }
1080
+ return result;
1081
+ }
1082
+ /**
1083
+ * Returns true if the Obsidian version is already in the cache.
1084
+ */
1085
+ async isInCache(type, version) {
1086
+ version = (await this.resolveVersions(version))[0];
1087
+ let dest;
1088
+ if (type == "app") {
1089
+ dest = `obsidian-app/obsidian-${version}.asar`;
1090
+ } else {
1091
+ const { platform, arch } = process;
1092
+ dest = `obsidian-installer/${platform}-${arch}/Obsidian-${version}`;
1093
+ }
1094
+ return await fileExists(_path2.default.join(this.cacheDir, dest));
1095
+ }
1096
+ /**
1097
+ * Returns true if we either have the credentails to download the version or it's already in cache.
1098
+ * This is only relevant for Obsidian beta versions, as they require Obsidian insider credentials to download.
1099
+ */
1100
+ async isAvailable(version) {
1101
+ const versionInfo = await this.getVersionInfo(version);
1102
+ if (versionInfo.isBeta) {
1103
+ const hasCreds = !!(process.env["OBSIDIAN_USERNAME"] && process.env["OBSIDIAN_PASSWORD"]);
1104
+ const inCache = await this.isInCache("app", versionInfo.version);
1105
+ return hasCreds || inCache;
1106
+ } else {
1107
+ return !!versionInfo.downloads.asar;
1108
+ }
1109
+ }
1110
+ };
1111
+
1112
+
1113
+
1114
+ exports.ObsidianLauncher = ObsidianLauncher;
1115
+ //# sourceMappingURL=chunk-64XS3NWI.cjs.map