obsidian-launcher 1.3.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,11 @@
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; } async function _asyncOptionalChain(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 = await fn(value); } else if (op === 'call' || op === 'optionalCall') { value = await fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/launcher.ts
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; } async function _asyncOptionalChain(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 = await fn(value); } else if (op === 'call' || op === 'optionalCall') { value = await fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+ var _chunkQQJCBP4Ncjs = require('./chunk-QQJCBP4N.cjs');
4
+
5
+ // src/utils.ts
2
6
  var _promises = require('fs/promises'); var _promises2 = _interopRequireDefault(_promises);
3
7
  var _fs = require('fs'); var _fs2 = _interopRequireDefault(_fs);
4
- var _zlib = require('zlib'); var _zlib2 = _interopRequireDefault(_zlib);
5
8
  var _path = require('path'); var _path2 = _interopRequireDefault(_path);
6
- var _crypto = require('crypto'); var _crypto2 = _interopRequireDefault(_crypto);
7
- var _extractzip = require('extract-zip'); var _extractzip2 = _interopRequireDefault(_extractzip);
8
- var _promises3 = require('stream/promises');
9
- var _get = require('@electron/get');
10
- var _child_process = require('child_process'); var _child_process2 = _interopRequireDefault(_child_process);
11
- var _semver = require('semver'); var _semver2 = _interopRequireDefault(_semver);
12
- var _url = require('url');
13
-
14
- // src/utils.ts
15
-
16
-
17
9
  var _os = require('os'); var _os2 = _interopRequireDefault(_os);
18
10
  var _promisepool = require('@supercharge/promise-pool');
19
11
  var _lodash = require('lodash'); var _lodash2 = _interopRequireDefault(_lodash);
@@ -28,14 +20,14 @@ async function fileExists(path5) {
28
20
  async function makeTmpDir(prefix) {
29
21
  return _promises2.default.mkdtemp(_path2.default.join(_os2.default.tmpdir(), _nullishCoalesce(prefix, () => ( "tmp-"))));
30
22
  }
31
- async function withTmpDir(dest, func) {
23
+ async function atomicCreate(dest, func, options = {}) {
32
24
  dest = _path2.default.resolve(dest);
25
+ const createdParentDir = await _promises2.default.mkdir(_path2.default.dirname(dest), { recursive: true });
33
26
  const tmpDir = await _promises2.default.mkdtemp(_path2.default.join(_path2.default.dirname(dest), `.${_path2.default.basename(dest)}.tmp.`));
34
27
  try {
35
28
  let result = await _asyncNullishCoalesce(await func(tmpDir), async () => ( tmpDir));
36
- if (!_path2.default.isAbsolute(result)) {
37
- result = _path2.default.join(tmpDir, result);
38
- } else if (!_path2.default.resolve(result).startsWith(tmpDir)) {
29
+ result = _path2.default.resolve(tmpDir, result);
30
+ if (!result.startsWith(tmpDir)) {
39
31
  throw new Error(`Returned path ${result} not under tmpDir`);
40
32
  }
41
33
  if (await fileExists(dest) && (await _promises2.default.stat(dest)).isDirectory()) {
@@ -43,28 +35,22 @@ async function withTmpDir(dest, func) {
43
35
  }
44
36
  await _promises2.default.rename(result, dest);
45
37
  await _promises2.default.rm(tmpDir + ".old", { recursive: true, force: true });
46
- } finally {
47
38
  await _promises2.default.rm(tmpDir, { recursive: true, force: true });
39
+ } catch (e) {
40
+ if (!options.preserveTmpDir) {
41
+ await _promises2.default.rm(_nullishCoalesce(createdParentDir, () => ( tmpDir)), { recursive: true, force: true });
42
+ }
43
+ throw e;
48
44
  }
49
45
  }
50
46
  async function linkOrCp(src, dest) {
47
+ await _promises2.default.rm(dest, { recursive: true, force: true });
51
48
  try {
52
49
  await _promises2.default.link(src, dest);
53
50
  } catch (e3) {
54
51
  await _promises2.default.copyFile(src, dest);
55
52
  }
56
53
  }
57
- async function sleep(ms) {
58
- return new Promise((resolve) => setTimeout(resolve, ms));
59
- }
60
- async function withTimeout(promise, timeout) {
61
- let timer;
62
- const result = Promise.race([
63
- promise,
64
- new Promise((resolve, reject) => timer = setTimeout(() => reject(Error("Promise timed out")), timeout))
65
- ]);
66
- return result.finally(() => clearTimeout(timer));
67
- }
68
54
  async function pool(size, items, func) {
69
55
  const { results } = await _promisepool.PromisePool.for(items).withConcurrency(size).handleError(async (error) => {
70
56
  throw error;
@@ -74,23 +60,58 @@ async function pool(size, items, func) {
74
60
  async function maybe(promise) {
75
61
  return promise.then((r) => ({ success: true, result: r, error: void 0 })).catch((e) => ({ success: false, result: void 0, error: e }));
76
62
  }
77
- function mergeKeepUndefined(object, ...sources) {
78
- return _lodash2.default.mergeWith(
79
- object,
80
- ...sources,
81
- (objValue, srcValue, key, obj) => {
82
- if (_lodash2.default.isPlainObject(obj) && objValue !== srcValue && srcValue === void 0) {
83
- obj[key] = srcValue;
63
+ function watchFiles(files, func, options) {
64
+ const debouncedFunc = _lodash2.default.debounce((curr, prev) => {
65
+ if (curr.mtimeMs > prev.mtimeMs || curr.mtimeMs == 0 && prev.mtimeMs != 0) {
66
+ func(curr, prev);
67
+ }
68
+ }, options.debounce);
69
+ for (const file of files) {
70
+ _fs2.default.watchFile(file, { interval: options.interval, persistent: options.persistent }, debouncedFunc);
71
+ }
72
+ }
73
+ function normalizeObject(canonical, obj) {
74
+ const rootCanonical = canonical, rootObj = obj;
75
+ function helper(canonical2, obj2) {
76
+ if (_lodash2.default.isPlainObject(canonical2)) {
77
+ if (_lodash2.default.isPlainObject(obj2)) {
78
+ obj2 = _lodash2.default.pick(obj2, Object.keys(canonical2));
79
+ obj2 = _lodash2.default.mapValues(obj2, (v, k) => helper(canonical2[k], v));
80
+ obj2 = _lodash2.default.omitBy(obj2, (v) => v === void 0);
81
+ return obj2;
82
+ } else {
83
+ return obj2;
84
84
  }
85
+ } else if (canonical2 === null) {
86
+ return obj2;
87
+ } else {
88
+ throw Error(`Invalid canonical form ${JSON.stringify(rootCanonical)}`);
85
89
  }
86
- );
90
+ }
91
+ return helper(rootCanonical, rootObj);
87
92
  }
88
93
 
89
- // src/apis.ts
94
+ // src/launcher.ts
95
+
96
+
97
+ var _crypto = require('crypto'); var _crypto2 = _interopRequireDefault(_crypto);
98
+ var _extractzip = require('extract-zip'); var _extractzip2 = _interopRequireDefault(_extractzip);
99
+ var _get = require('@electron/get');
100
+ var _child_process = require('child_process'); var _child_process2 = _interopRequireDefault(_child_process);
101
+ var _semver = require('semver'); var _semver2 = _interopRequireDefault(_semver);
102
+ var _url = require('url');
103
+ var _dotenv = require('dotenv'); var _dotenv2 = _interopRequireDefault(_dotenv);
104
+
105
+ // src/types.ts
106
+ var obsidianVersionsSchemaVersion = "2.0.0";
90
107
 
108
+ // src/apis.ts
91
109
 
92
110
 
111
+ var _promises3 = require('stream/promises');
93
112
  var _stream = require('stream');
113
+
114
+ var _readlinesync = require('readline-sync'); var _readlinesync2 = _interopRequireDefault(_readlinesync);
94
115
  function parseLinkHeader(linkHeader) {
95
116
  function parseLinkData(linkData) {
96
117
  return Object.fromEntries(
@@ -130,7 +151,7 @@ async function fetchGitHubAPI(url, params = {}) {
130
151
  if (!response.ok) {
131
152
  throw new Error(`GitHub API error: ${await response.text()}`);
132
153
  }
133
- return await response;
154
+ return response;
134
155
  }
135
156
  async function fetchGitHubAPIPaginated(url, params = {}) {
136
157
  const results = [];
@@ -142,24 +163,87 @@ async function fetchGitHubAPIPaginated(url, params = {}) {
142
163
  }
143
164
  return results;
144
165
  }
145
- async function fetchObsidianAPI(url) {
146
- url = createURL(url, "https://releases.obsidian.md");
147
- const username = process.env.OBSIDIAN_USERNAME;
148
- const password = process.env.OBSIDIAN_PASSWORD;
149
- if (!username || !password) {
150
- throw Error("OBSIDIAN_USERNAME or OBSIDIAN_PASSWORD environment variables are required to access the Obsidian API for beta versions.");
166
+ async function obsidianApiLogin(opts) {
167
+ const { interactive = false, savePath } = opts;
168
+ let email = process.env.OBSIDIAN_EMAIL;
169
+ let password = process.env.OBSIDIAN_PASSWORD;
170
+ let mfa;
171
+ if (!email || !password) {
172
+ if (interactive) {
173
+ console.log("Obsidian Insiders account is required to download Obsidian beta versions.");
174
+ email = email || _readlinesync2.default.question("Obsidian email: ");
175
+ password = password || _readlinesync2.default.question("Obsidian password: ", { hideEchoBack: true });
176
+ } else {
177
+ throw Error(
178
+ "Obsidian Insiders account is required to download Obsidian beta versions. Either set the OBSIDIAN_EMAIL and OBSIDIAN_PASSWORD env vars (.env is supported) or pre-download the Obsidian beta with `npx obsidian-launcher download -v <version>`"
179
+ );
180
+ }
151
181
  }
182
+ const headers = {
183
+ "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",
184
+ "Origin": "app://obsidian.md",
185
+ "Content-Type": "application/json"
186
+ };
187
+ let signin = await fetch("https://api.obsidian.md/user/signin", {
188
+ method: "post",
189
+ headers,
190
+ body: JSON.stringify({ email, password, mfa: "" })
191
+ }).then((r) => r.json());
192
+ if (signin.error && signin.error.includes("2FA")) {
193
+ if (interactive) {
194
+ mfa = await _readlinesync2.default.question("Obsidian 2FA: ");
195
+ signin = await fetch("https://api.obsidian.md/user/signin", {
196
+ method: "post",
197
+ headers,
198
+ body: JSON.stringify({ email, password, mfa })
199
+ }).then((r) => r.json());
200
+ } else {
201
+ throw Error(
202
+ "Can't login with 2FA in a non-interactive session. To download Obsidian beta versions, either disable 2FA on your account or pre-download the Obsidian beta with `npx obsidian-launcher download -v <version>`"
203
+ );
204
+ }
205
+ }
206
+ let error = void 0;
207
+ if (!signin.token) {
208
+ error = Error(`Obsidian login failed: ${signin.error}`);
209
+ }
210
+ if (!error && !signin.license) {
211
+ error = Error("Obsidian Insiders account is required to download Obsidian beta versions");
212
+ }
213
+ if (error) {
214
+ if (savePath) {
215
+ await _promises2.default.rm(savePath, { force: true });
216
+ }
217
+ throw error;
218
+ }
219
+ if (interactive && savePath && (!process.env.OBSIDIAN_EMAIL || !process.env.OBSIDIAN_PASSWORD)) {
220
+ const save = _readlinesync2.default.question("Save credentails to disk? [y/n]: ");
221
+ if (["y", "yes"].includes(save.toLocaleLowerCase())) {
222
+ await _promises2.default.writeFile(
223
+ savePath,
224
+ `OBSIDIAN_EMAIL='${email}'
225
+ OBSIDIAN_PASSWORD='${password}'
226
+ `
227
+ );
228
+ }
229
+ }
230
+ return signin.token;
231
+ }
232
+ async function fetchObsidianApi(url, opts) {
233
+ url = createURL(url, "https://releases.obsidian.md");
152
234
  const response = await fetch(url, {
153
235
  headers: {
154
- // For some reason you have to set the User-Agent or it won't let you download
155
236
  "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",
156
237
  "Origin": "app://obsidian.md",
157
- "Authorization": "Basic " + btoa(username + ":" + password)
238
+ "Authorization": "Bearer " + opts.token
158
239
  }
159
240
  });
160
241
  return response;
161
242
  }
162
243
  async function downloadResponse(response, dest) {
244
+ if (!response.ok) {
245
+ throw Error(`${response.url} failed with ${response.status}`);
246
+ }
163
247
  const fileStream = _fs2.default.createWriteStream(dest, { flags: "w" });
164
248
  const fetchStream = _stream.Readable.fromWeb(response.body);
165
249
  await _promises3.finished.call(void 0, fetchStream.pipe(fileStream));
@@ -173,7 +257,10 @@ var ChromeLocalStorage = class {
173
257
  constructor(userDataDir) {
174
258
  this.userDataDir = userDataDir;
175
259
  this.encodeKey = (domain, key) => `_${domain}\0${key}`;
176
- this.decodeKey = (key) => key.slice(1).split("\0");
260
+ this.decodeKey = (key) => {
261
+ const parts = key.slice(1).split("\0");
262
+ return [parts[0], parts.slice(1).join("\0")];
263
+ };
177
264
  this.encodeValue = (value) => `${value}`;
178
265
  this.decodeValue = (value) => value.slice(1);
179
266
  this.db = new (0, _classiclevel.ClassicLevel)(_path2.default.join(userDataDir, "Local Storage/leveldb/"));
@@ -241,231 +328,325 @@ var ChromeLocalStorage = class {
241
328
  // src/launcherUtils.ts
242
329
 
243
330
 
244
- var _util = require('util');
245
331
 
246
- var _which = require('which'); var _which2 = _interopRequireDefault(_which);
247
332
 
248
333
 
249
- var _chromeremoteinterface = require('chrome-remote-interface'); var _chromeremoteinterface2 = _interopRequireDefault(_chromeremoteinterface);
250
- var execFile = _util.promisify.call(void 0, _child_process2.default.execFile);
334
+
335
+
336
+ var _zlib = require('zlib'); var _zlib2 = _interopRequireDefault(_zlib);
337
+
251
338
  function normalizeGitHubRepo(repo) {
252
- return repo.replace(/^(https?:\/\/)?(github.com\/)/, "");
339
+ return _nullishCoalesce(_optionalChain([repo, 'access', _8 => _8.match, 'call', _9 => _9(/^(https?:\/\/)?(github.com\/)?(.*?)\/?$/), 'optionalAccess', _10 => _10[3]]), () => ( repo));
340
+ }
341
+ async function extractGz(archive, dest) {
342
+ await _promises3.pipeline.call(void 0, _fs2.default.createReadStream(archive), _zlib2.default.createGunzip(), _fs2.default.createWriteStream(dest));
343
+ }
344
+ async function sevenZ(args, options) {
345
+ const sevenZipScript = _path2.default.resolve(_url.fileURLToPath.call(void 0, _chunkQQJCBP4Ncjs.importMetaUrl), "../7z.js");
346
+ const proc = _child_process2.default.spawn(process.execPath, [sevenZipScript, ...args], {
347
+ stdio: "pipe",
348
+ ...options
349
+ });
350
+ let stdout = "", stderr = "";
351
+ proc.stdout.on("data", (data) => stdout += data);
352
+ proc.stderr.on("data", (data) => stderr += data);
353
+ const procExit = new Promise((resolve) => proc.on("close", (code) => resolve(_nullishCoalesce(code, () => ( -1)))));
354
+ const exitCode = await procExit;
355
+ const result = { stdout, stderr };
356
+ if (exitCode != 0) {
357
+ throw Error(`"7z ${args.join(" ")}" failed with ${exitCode}:
358
+ ${stdout}
359
+ ${stderr}`);
360
+ }
361
+ return result;
253
362
  }
254
363
  async function extractObsidianAppImage(appImage, dest) {
255
- await withTmpDir(dest, async (tmpDir) => {
256
- await _promises2.default.chmod(appImage, 493);
257
- await execFile(appImage, ["--appimage-extract"], { cwd: tmpDir });
258
- return _path2.default.join(tmpDir, "squashfs-root");
364
+ await atomicCreate(dest, async (tmpDir) => {
365
+ await sevenZ(["x", "-o.", _path2.default.relative(tmpDir, appImage)], { cwd: tmpDir });
366
+ return tmpDir;
259
367
  });
260
368
  }
261
- async function extractObsidianExe(exe, appArch, dest) {
262
- const path7z = await _which2.default.call(void 0, "7z", { nothrow: true });
263
- if (!path7z) {
264
- throw new Error(
265
- "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."
266
- );
369
+ async function extractObsidianTar(tar, dest) {
370
+ await atomicCreate(dest, async (tmpDir) => {
371
+ await extractGz(tar, _path2.default.join(tmpDir, "inflated.tar"));
372
+ await sevenZ(["x", "-o.", "inflated.tar"], { cwd: tmpDir });
373
+ return (await _promises2.default.readdir(tmpDir)).find((p) => p.match("obsidian-"));
374
+ });
375
+ }
376
+ async function extractObsidianExe(exe, arch, dest) {
377
+ let subArchive;
378
+ if (arch == "x64") {
379
+ subArchive = `$PLUGINSDIR/app-64.7z`;
380
+ } else if (arch == "ia32") {
381
+ subArchive = `$PLUGINSDIR/app-32.7z`;
382
+ } else if (arch == "arm64") {
383
+ subArchive = `$PLUGINSDIR/app-arm64.7z`;
384
+ } else {
385
+ throw Error(`No Obsidian installer found for ${process.platform} ${process.arch}`);
267
386
  }
268
- exe = _path2.default.resolve(exe);
269
- const subArchive = _path2.default.join("$PLUGINSDIR", appArch + ".7z");
270
- dest = _path2.default.resolve(dest);
271
- await withTmpDir(dest, async (tmpDir) => {
272
- const extractedInstaller = _path2.default.join(tmpDir, "installer");
273
- await execFile(path7z, ["x", "-o" + extractedInstaller, exe, subArchive]);
274
- const extractedObsidian = _path2.default.join(tmpDir, "obsidian");
275
- await execFile(path7z, ["x", "-o" + extractedObsidian, _path2.default.join(extractedInstaller, subArchive)]);
276
- return extractedObsidian;
387
+ await atomicCreate(dest, async (tmpDir) => {
388
+ await sevenZ(["x", "-oinstaller", _path2.default.relative(tmpDir, exe), subArchive], { cwd: tmpDir });
389
+ await sevenZ(["x", "-oobsidian", _path2.default.join("installer", subArchive)], { cwd: tmpDir });
390
+ return "obsidian";
277
391
  });
278
392
  }
279
393
  async function extractObsidianDmg(dmg, dest) {
280
394
  dest = _path2.default.resolve(dest);
281
- await withTmpDir(dest, async (tmpDir) => {
282
- const proc = await execFile("hdiutil", ["attach", "-nobrowse", "-readonly", dmg]);
283
- const volume = proc.stdout.match(/\/Volumes\/.*$/m)[0];
284
- const obsidianApp = _path2.default.join(volume, "Obsidian.app");
285
- try {
286
- await _promises2.default.cp(obsidianApp, tmpDir, { recursive: true, verbatimSymlinks: true });
287
- } finally {
288
- await execFile("hdiutil", ["detach", volume]);
395
+ await atomicCreate(dest, async (tmpDir) => {
396
+ await sevenZ(["x", "-o.", _path2.default.relative(tmpDir, dmg), "*/Obsidian.app", "Obsidian.app"], { cwd: tmpDir });
397
+ const files = await _promises2.default.readdir(tmpDir);
398
+ if (files.includes("Obsidian.app")) {
399
+ return "Obsidian.app";
400
+ } else {
401
+ return _path2.default.join(files[0], "Obsidian.app");
289
402
  }
290
- return tmpDir;
291
403
  });
292
404
  }
293
- function parseObsidianDesktopRelease(fileRelease, isBeta) {
294
- return {
295
- version: fileRelease.latestVersion,
296
- minInstallerVersion: fileRelease.minimumVersion != "0.0.0" ? fileRelease.minimumVersion : void 0,
297
- isBeta,
298
- downloads: {
299
- asar: fileRelease.downloadUrl
405
+ var BROKEN_ASSETS = [
406
+ "https://releases.obsidian.md/release/obsidian-0.12.16.asar.gz",
407
+ "https://github.com/obsidianmd/obsidian-releases/releases/download/v0.12.16/obsidian-0.12.16.asar.gz",
408
+ "https://releases.obsidian.md/release/obsidian-1.4.7.asar.gz",
409
+ "https://releases.obsidian.md/release/obsidian-1.4.8.asar.gz"
410
+ ];
411
+ function parseObsidianDesktopRelease(fileRelease) {
412
+ const parse = (r, isBeta) => {
413
+ const version = r.latestVersion;
414
+ let minInstallerVersion = r.minimumVersion;
415
+ if (minInstallerVersion == "0.0.0") {
416
+ minInstallerVersion = void 0;
417
+ } else if (_semver2.default.satisfies(version, ">=1.3.0 <=1.3.4")) {
418
+ minInstallerVersion = "0.14.5";
419
+ } else if (_semver2.default.gte(version, "1.5.3") && _semver2.default.lt(minInstallerVersion, "1.1.9")) {
420
+ minInstallerVersion = "1.1.9";
300
421
  }
422
+ return {
423
+ version: r.latestVersion,
424
+ minInstallerVersion,
425
+ isBeta,
426
+ downloads: {
427
+ asar: BROKEN_ASSETS.includes(r.downloadUrl) ? void 0 : r.downloadUrl
428
+ }
429
+ };
301
430
  };
431
+ const result = { current: parse(fileRelease, false) };
432
+ if (fileRelease.beta && fileRelease.beta.latestVersion !== fileRelease.latestVersion) {
433
+ result.beta = parse(fileRelease.beta, true);
434
+ }
435
+ return result;
302
436
  }
303
437
  function parseObsidianGithubRelease(gitHubRelease) {
304
438
  const version = gitHubRelease.name;
305
- const assets = gitHubRelease.assets.map((a) => a.browser_download_url);
306
- const downloads = {
307
- appImage: assets.find((u) => u.match(`${version}.AppImage$`)),
308
- appImageArm: assets.find((u) => u.match(`${version}-arm64.AppImage$`)),
309
- apk: assets.find((u) => u.match(`${version}.apk$`)),
310
- asar: assets.find((u) => u.match(`${version}.asar.gz$`)),
311
- dmg: assets.find((u) => u.match(`${version}(-universal)?.dmg$`)),
312
- exe: assets.find((u) => u.match(`${version}.exe$`))
313
- };
439
+ let assets = gitHubRelease.assets.map((a) => ({
440
+ url: a.browser_download_url,
441
+ digest: _nullishCoalesce(a.digest, () => ( `id:${a.id}`))
442
+ }));
443
+ assets = assets.filter((a) => !BROKEN_ASSETS.includes(a.url));
444
+ const asar = assets.find((a) => a.url.match(`${version}.asar.gz$`));
445
+ const appImage = assets.find((a) => a.url.match(`${version}.AppImage$`));
446
+ const appImageArm = assets.find((a) => a.url.match(`${version}-arm64.AppImage$`));
447
+ const tar = assets.find((a) => a.url.match(`${version}.tar.gz$`));
448
+ const tarArm = assets.find((a) => a.url.match(`${version}-arm64.tar.gz$`));
449
+ const dmg = assets.find((a) => a.url.match(`${version}(-universal)?.dmg$`));
450
+ const exe = assets.find((a) => a.url.match(`${version}.exe$`));
451
+ const apk = assets.find((a) => a.url.match(`${version}.apk$`));
314
452
  return {
315
453
  version,
316
454
  gitHubRelease: gitHubRelease.html_url,
317
- downloads
455
+ downloads: {
456
+ asar: _optionalChain([asar, 'optionalAccess', _11 => _11.url]),
457
+ appImage: _optionalChain([appImage, 'optionalAccess', _12 => _12.url]),
458
+ appImageArm: _optionalChain([appImageArm, 'optionalAccess', _13 => _13.url]),
459
+ tar: _optionalChain([tar, 'optionalAccess', _14 => _14.url]),
460
+ tarArm: _optionalChain([tarArm, 'optionalAccess', _15 => _15.url]),
461
+ dmg: _optionalChain([dmg, 'optionalAccess', _16 => _16.url]),
462
+ exe: _optionalChain([exe, 'optionalAccess', _17 => _17.url]),
463
+ apk: _optionalChain([apk, 'optionalAccess', _18 => _18.url])
464
+ },
465
+ installers: {
466
+ appImage: appImage ? { digest: appImage.digest } : void 0,
467
+ appImageArm: appImageArm ? { digest: appImageArm.digest } : void 0,
468
+ tar: tar ? { digest: tar.digest } : void 0,
469
+ tarArm: tarArm ? { digest: tarArm.digest } : void 0,
470
+ dmg: dmg ? { digest: dmg.digest } : void 0,
471
+ exe: exe ? { digest: exe.digest } : void 0
472
+ }
318
473
  };
319
474
  }
320
- async function getElectronVersionInfo(version, binaryPath) {
321
- console.log(`${version}: Retrieving electron & chrome versions...`);
322
- const configDir = await makeTmpDir("fetch-obsidian-versions-");
323
- const proc = _child_process2.default.spawn(binaryPath, [
324
- `--remote-debugging-port=0`,
325
- // 0 will make it choose a random available port
326
- "--test-type=webdriver",
327
- `--user-data-dir=${configDir}`,
328
- "--no-sandbox"
329
- // Workaround for SUID issue, see https://github.com/electron/electron/issues/42510
330
- ]);
331
- const procExit = new Promise((resolve) => proc.on("exit", (code) => resolve(_nullishCoalesce(code, () => ( -1)))));
332
- let dependencyVersions;
475
+ async function getInstallerInfo(installerKey, url) {
476
+ const installerName = url.split("/").at(-1);
477
+ console.log(`Extrating installer info for ${installerName}...`);
478
+ const tmpDir = await makeTmpDir("obsidian-launcher-");
333
479
  try {
334
- const portPromise = new Promise((resolve, reject) => {
335
- procExit.then(() => reject("Processed ended without opening a port"));
336
- proc.stderr.on("data", (data) => {
337
- const port2 = _optionalChain([data, 'access', _8 => _8.toString, 'call', _9 => _9(), 'access', _10 => _10.match, 'call', _11 => _11(/ws:\/\/[\w.]+?:(\d+)/), 'optionalAccess', _12 => _12[1]]);
338
- if (port2) {
339
- resolve(Number(port2));
480
+ const installerPath = _path2.default.join(tmpDir, url.split("/").at(-1));
481
+ await downloadResponse(await fetch(url), installerPath);
482
+ const exractedPath = _path2.default.join(tmpDir, "Obsidian");
483
+ let platforms = [];
484
+ if (installerKey == "appImage" || installerKey == "appImageArm") {
485
+ await extractObsidianAppImage(installerPath, exractedPath);
486
+ platforms = ["linux-" + (installerKey == "appImage" ? "x64" : "arm64")];
487
+ } else if (installerKey == "tar" || installerKey == "tarArm") {
488
+ await extractObsidianTar(installerPath, exractedPath);
489
+ platforms = ["linux-" + (installerKey == "tar" ? "x64" : "arm64")];
490
+ } else if (installerKey == "exe") {
491
+ await extractObsidianExe(installerPath, "x64", exractedPath);
492
+ const { stdout } = await sevenZ(["l", "-ba", _path2.default.relative(tmpDir, installerPath)], { cwd: tmpDir });
493
+ const lines = stdout.trim().split("\n").map((l) => l.trim());
494
+ const files = lines.map((l) => l.split(/\s+/).at(-1).replace(/\\/g, "/"));
495
+ if (files.includes("$PLUGINSDIR/app-arm64.7z")) platforms.push("win32-arm64");
496
+ if (files.includes("$PLUGINSDIR/app-32.7z")) platforms.push("win32-ia32");
497
+ if (files.includes("$PLUGINSDIR/app-64.7z")) platforms.push("win32-x64");
498
+ } else if (installerKey == "dmg") {
499
+ await extractObsidianDmg(installerPath, exractedPath);
500
+ platforms = ["darwin-arm64", "darwin-x64"];
501
+ } else {
502
+ throw new Error(`Unknown installer key ${installerKey}`);
503
+ }
504
+ const matches = [];
505
+ const installerFiles = await _promises2.default.readdir(exractedPath, { recursive: true, withFileTypes: true });
506
+ for (const file of installerFiles) {
507
+ if (file.isFile() && !file.name.endsWith(".asar")) {
508
+ const stream = _fs2.default.createReadStream(_path2.default.join(file.parentPath, file.name), { encoding: "utf-8" });
509
+ let prev = "";
510
+ for await (let chunk of stream) {
511
+ const regex = /Chrome\/\d+\.\d+\.\d+\.\d+|Electron\/\d+\.\d+\.\d+/g;
512
+ chunk = prev + chunk;
513
+ matches.push(...[...(prev + chunk).matchAll(regex)].map((m) => m[0]));
514
+ prev = chunk.slice(-64);
340
515
  }
341
- });
342
- });
343
- const port = await maybe(withTimeout(portPromise, 10 * 1e3));
344
- if (!port.success) {
345
- throw new Error("Timed out waiting for Chrome DevTools protocol port");
516
+ }
346
517
  }
347
- const client = await _chromeremoteinterface2.default.call(void 0, { port: port.result });
348
- const response = await client.Runtime.evaluate({ expression: "JSON.stringify(process.versions)" });
349
- dependencyVersions = JSON.parse(response.result.value);
350
- await client.close();
351
- } finally {
352
- proc.kill("SIGTERM");
353
- const timeout = await maybe(withTimeout(procExit, 4 * 1e3));
354
- if (!timeout.success) {
355
- console.log(`${version}: Stuck process ${proc.pid}, using SIGKILL`);
356
- proc.kill("SIGKILL");
518
+ const versionSortKey = (v) => v.split(".").map((s) => s.padStart(9, "0")).join(".");
519
+ const versions = _lodash2.default.call(void 0, matches).map((m) => m.split("/")).groupBy(0).mapValues((ms) => ms.map((m) => m[1])).mapValues((ms) => _lodash2.default.sortBy(ms, versionSortKey).at(-1)).value();
520
+ const electron = versions["Electron"];
521
+ const chrome = versions["Chrome"];
522
+ if (!electron || !chrome) {
523
+ throw new Error(`Failed to extract Electron and Chrome versions from binary ${installerPath}`);
357
524
  }
358
- await procExit;
359
- await sleep(1e3);
360
- await _promises2.default.rm(configDir, { recursive: true, force: true });
361
- }
362
- if (!_optionalChain([dependencyVersions, 'optionalAccess', _13 => _13.electron]) || !_optionalChain([dependencyVersions, 'optionalAccess', _14 => _14.chrome])) {
363
- throw Error(`Failed to extract electron and chrome versions for ${version}`);
364
- }
365
- return {
366
- electronVersion: dependencyVersions.electron,
367
- chromeVersion: dependencyVersions.chrome,
368
- nodeVersion: dependencyVersions.node
369
- };
370
- }
371
- function correctObsidianVersionInfo(versionInfo) {
372
- const corrections = [
373
- // These version's downloads are missing or broken.
374
- { version: "0.12.16", downloads: { asar: void 0 } },
375
- { version: "1.4.7", downloads: { asar: void 0 } },
376
- { version: "1.4.8", downloads: { asar: void 0 } },
377
- // The minInstallerVersion here is incorrect
378
- { version: "1.3.4", minInstallerVersion: "0.14.5" },
379
- { version: "1.3.3", minInstallerVersion: "0.14.5" },
380
- { version: "1.3.2", minInstallerVersion: "0.14.5" },
381
- { version: "1.3.1", minInstallerVersion: "0.14.5" },
382
- { version: "1.3.0", minInstallerVersion: "0.14.5" }
383
- ];
384
- const result = _nullishCoalesce(corrections.find((v) => v.version == versionInfo.version), () => ( {}));
385
- if (_semver2.default.gte(versionInfo.version, "1.5.3") && _semver2.default.lt(versionInfo.minInstallerVersion, "1.1.9")) {
386
- result.minInstallerVersion = "1.1.9";
525
+ console.log(`Extracted installer info for ${installerName}`);
526
+ return { electron, chrome, platforms };
527
+ } finally {
528
+ await _promises2.default.rm(tmpDir, { recursive: true, force: true });
387
529
  }
388
- return result;
389
530
  }
390
531
  function normalizeObsidianVersionInfo(versionInfo) {
391
532
  versionInfo = {
392
- version: versionInfo.version,
393
- minInstallerVersion: versionInfo.minInstallerVersion,
394
- maxInstallerVersion: versionInfo.maxInstallerVersion,
395
- isBeta: versionInfo.isBeta,
396
- gitHubRelease: versionInfo.gitHubRelease,
533
+ ...versionInfo,
534
+ // kept for backwards compatibility
535
+ electronVersion: _optionalChain([versionInfo, 'access', _19 => _19.installers, 'optionalAccess', _20 => _20.appImage, 'optionalAccess', _21 => _21.electron]),
536
+ chromeVersion: _optionalChain([versionInfo, 'access', _22 => _22.installers, 'optionalAccess', _23 => _23.appImage, 'optionalAccess', _24 => _24.chrome])
537
+ };
538
+ const canonicalForm = {
539
+ version: null,
540
+ minInstallerVersion: null,
541
+ maxInstallerVersion: null,
542
+ isBeta: null,
543
+ gitHubRelease: null,
397
544
  downloads: {
398
- asar: _optionalChain([versionInfo, 'access', _15 => _15.downloads, 'optionalAccess', _16 => _16.asar]),
399
- appImage: _optionalChain([versionInfo, 'access', _17 => _17.downloads, 'optionalAccess', _18 => _18.appImage]),
400
- appImageArm: _optionalChain([versionInfo, 'access', _19 => _19.downloads, 'optionalAccess', _20 => _20.appImageArm]),
401
- apk: _optionalChain([versionInfo, 'access', _21 => _21.downloads, 'optionalAccess', _22 => _22.apk]),
402
- dmg: _optionalChain([versionInfo, 'access', _23 => _23.downloads, 'optionalAccess', _24 => _24.dmg]),
403
- exe: _optionalChain([versionInfo, 'access', _25 => _25.downloads, 'optionalAccess', _26 => _26.exe])
545
+ asar: null,
546
+ appImage: null,
547
+ appImageArm: null,
548
+ tar: null,
549
+ tarArm: null,
550
+ dmg: null,
551
+ exe: null,
552
+ apk: null
553
+ },
554
+ installers: {
555
+ appImage: { digest: null, electron: null, chrome: null, platforms: null },
556
+ appImageArm: { digest: null, electron: null, chrome: null, platforms: null },
557
+ tar: { digest: null, electron: null, chrome: null, platforms: null },
558
+ tarArm: { digest: null, electron: null, chrome: null, platforms: null },
559
+ dmg: { digest: null, electron: null, chrome: null, platforms: null },
560
+ exe: { digest: null, electron: null, chrome: null, platforms: null }
404
561
  },
405
- electronVersion: versionInfo.electronVersion,
406
- chromeVersion: versionInfo.chromeVersion,
407
- nodeVersion: versionInfo.nodeVersion
562
+ electronVersion: null,
563
+ chromeVersion: null
408
564
  };
409
- versionInfo.downloads = _lodash2.default.omitBy(versionInfo.downloads, _lodash2.default.isUndefined);
410
- versionInfo = _lodash2.default.omitBy(versionInfo, _lodash2.default.isUndefined);
411
- return versionInfo;
565
+ return normalizeObject(canonicalForm, versionInfo);
412
566
  }
413
567
 
414
568
  // src/launcher.ts
415
569
 
570
+ var currentPlatform = {
571
+ platform: process.platform,
572
+ arch: process.arch
573
+ };
416
574
  var ObsidianLauncher = class {
417
575
  /**
418
576
  * Construct an ObsidianLauncher.
419
- * @param options.cacheDir Path to the cache directory. Defaults to "OBSIDIAN_CACHE" env var or ".obsidian-cache".
420
- * @param options.versionsUrl Custom `obsidian-versions.json` url. Can be a file URL.
421
- * @param options.communityPluginsUrl Custom `community-plugins.json` url. Can be a file URL.
422
- * @param options.communityThemesUrl Custom `community-css-themes.json` url. Can be a file URL.
423
- * @param options.cacheDuration If the cached version list is older than this (in ms), refetch it. Defaults to 30 minutes.
577
+ * @param opts.cacheDir Path to the cache directory. Defaults to "OBSIDIAN_CACHE" env var or ".obsidian-cache".
578
+ * @param opts.versionsUrl Custom `obsidian-versions.json` url. Can be a file URL.
579
+ * @param opts.communityPluginsUrl Custom `community-plugins.json` url. Can be a file URL.
580
+ * @param opts.communityThemesUrl Custom `community-css-themes.json` url. Can be a file URL.
581
+ * @param opts.cacheDuration If the cached version list is older than this (in ms), refetch it. Defaults to 30 minutes.
582
+ * @param opts.interactive If it can prompt the user for input (e.g. for Obsidian credentials). Default false.
424
583
  */
425
- constructor(options = {}) {
426
- this.cacheDir = _path2.default.resolve(_nullishCoalesce(_nullishCoalesce(options.cacheDir, () => ( process.env.OBSIDIAN_CACHE)), () => ( "./.obsidian-cache")));
584
+ constructor(opts = {}) {
585
+ this.interactive = false;
586
+ this.cacheDir = _path2.default.resolve(_nullishCoalesce(_nullishCoalesce(opts.cacheDir, () => ( process.env.OBSIDIAN_CACHE)), () => ( "./.obsidian-cache")));
427
587
  const defaultVersionsUrl = "https://raw.githubusercontent.com/jesse-r-s-hines/wdio-obsidian-service/HEAD/obsidian-versions.json";
428
- this.versionsUrl = _nullishCoalesce(options.versionsUrl, () => ( defaultVersionsUrl));
588
+ this.versionsUrl = _nullishCoalesce(opts.versionsUrl, () => ( defaultVersionsUrl));
429
589
  const defaultCommunityPluginsUrl = "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/HEAD/community-plugins.json";
430
- this.communityPluginsUrl = _nullishCoalesce(options.communityPluginsUrl, () => ( defaultCommunityPluginsUrl));
590
+ this.communityPluginsUrl = _nullishCoalesce(opts.communityPluginsUrl, () => ( defaultCommunityPluginsUrl));
431
591
  const defaultCommunityThemesUrl = "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/HEAD/community-css-themes.json";
432
- this.communityThemesUrl = _nullishCoalesce(options.communityThemesUrl, () => ( defaultCommunityThemesUrl));
433
- this.cacheDuration = _nullishCoalesce(options.cacheDuration, () => ( 30 * 60 * 1e3));
592
+ this.communityThemesUrl = _nullishCoalesce(opts.communityThemesUrl, () => ( defaultCommunityThemesUrl));
593
+ this.cacheDuration = _nullishCoalesce(opts.cacheDuration, () => ( 30 * 60 * 1e3));
594
+ this.interactive = _nullishCoalesce(opts.interactive, () => ( false));
434
595
  this.metadataCache = {};
596
+ _dotenv2.default.config({
597
+ path: [".env", _path2.default.join(this.cacheDir, "obsidian-credentials.env")],
598
+ quiet: true
599
+ });
435
600
  }
436
601
  /**
437
602
  * Returns file content fetched from url as JSON. Caches content to dest and uses that cache if its more recent than
438
603
  * cacheDuration ms or if there are network errors.
439
604
  */
440
- async cachedFetch(url, dest) {
605
+ async cachedFetch(url, dest, cacheValid) {
606
+ cacheValid = _nullishCoalesce(cacheValid, () => ( (() => true)));
441
607
  dest = _path2.default.join(this.cacheDir, dest);
442
608
  if (!(dest in this.metadataCache)) {
443
- let fileContent;
609
+ let data;
610
+ let error;
611
+ const cacheMtime = await _asyncOptionalChain([(await _promises2.default.stat(dest).catch(() => void 0)), 'optionalAccess', async _25 => _25.mtime]);
444
612
  if (url.startsWith("file:")) {
445
- fileContent = await _promises2.default.readFile(_url.fileURLToPath.call(void 0, url), "utf-8");
446
- } else {
447
- const mtime = await fileExists(dest) ? (await _promises2.default.stat(dest)).mtime : void 0;
448
- if (mtime && (/* @__PURE__ */ new Date()).getTime() - mtime.getTime() < this.cacheDuration) {
449
- fileContent = await _promises2.default.readFile(dest, "utf-8");
613
+ data = JSON.parse(await _promises2.default.readFile(_url.fileURLToPath.call(void 0, url), "utf-8"));
614
+ }
615
+ if (!data && cacheMtime && (/* @__PURE__ */ new Date()).getTime() - cacheMtime.getTime() < this.cacheDuration) {
616
+ const parsed = JSON.parse(await _promises2.default.readFile(dest, "utf-8"));
617
+ if (cacheValid(parsed)) {
618
+ data = parsed;
619
+ }
620
+ }
621
+ if (!data) {
622
+ const response = await maybe(fetch(url).then(async (r) => {
623
+ if (!r.ok) throw Error(`Fetch ${url} failed with status ${r.status}`);
624
+ const d = await r.text();
625
+ if (_lodash2.default.isError(_lodash2.default.attempt(JSON.parse, d))) throw Error(`Failed to parse response from ${url}`);
626
+ return d;
627
+ }));
628
+ if (response.success) {
629
+ await atomicCreate(dest, async (tmpDir) => {
630
+ await _promises2.default.writeFile(_path2.default.join(tmpDir, "download.json"), response.result);
631
+ return _path2.default.join(tmpDir, "download.json");
632
+ });
633
+ data = JSON.parse(response.result);
450
634
  } else {
451
- const request = await maybe(fetch(url).then((r) => r.text()));
452
- if (request.success) {
453
- await _promises2.default.mkdir(_path2.default.dirname(dest), { recursive: true });
454
- await withTmpDir(dest, async (tmpDir) => {
455
- await _promises2.default.writeFile(_path2.default.join(tmpDir, "download.json"), request.result);
456
- return _path2.default.join(tmpDir, "download.json");
457
- });
458
- fileContent = request.result;
459
- } else if (await fileExists(dest)) {
460
- console.warn(request.error);
461
- console.warn(`Unable to download ${dest}, using cached file.`);
462
- fileContent = await _promises2.default.readFile(dest, "utf-8");
463
- } else {
464
- throw request.error;
465
- }
635
+ error = response.error;
636
+ }
637
+ }
638
+ if (!data && await fileExists(dest)) {
639
+ const parsed = JSON.parse(await _promises2.default.readFile(dest, "utf-8"));
640
+ if (cacheValid(parsed)) {
641
+ console.warn(error);
642
+ console.warn(`Unable to download ${url}, using cached file.`);
643
+ data = parsed;
466
644
  }
467
645
  }
468
- this.metadataCache[dest] = JSON.parse(fileContent);
646
+ if (!data) {
647
+ throw error;
648
+ }
649
+ this.metadataCache[dest] = data;
469
650
  }
470
651
  return this.metadataCache[dest];
471
652
  }
@@ -492,7 +673,12 @@ var ObsidianLauncher = class {
492
673
  * Get information about all available Obsidian versions.
493
674
  */
494
675
  async getVersions() {
495
- return (await this.cachedFetch(this.versionsUrl, "obsidian-versions.json")).versions;
676
+ const isValid = (d) => _semver2.default.satisfies(_nullishCoalesce(d.metadata.schemaVersion, () => ( "1.0.0")), `^${obsidianVersionsSchemaVersion}`);
677
+ const versions = await this.cachedFetch(this.versionsUrl, "obsidian-versions.json", isValid);
678
+ if (!isValid(versions)) {
679
+ throw new Error(`${this.versionsUrl} format has changed, please update obsidian-launcher and wdio-obsidian-service`);
680
+ }
681
+ return versions.versions;
496
682
  }
497
683
  /**
498
684
  * Get information about all available community plugins.
@@ -508,11 +694,11 @@ var ObsidianLauncher = class {
508
694
  }
509
695
  /**
510
696
  * Resolves Obsidian app and installer version strings to absolute versions.
511
- * @param appVersion Obsidian version string or one of
697
+ * @param appVersion specific version or one of
512
698
  * - "latest": Get the current latest non-beta Obsidian version
513
699
  * - "latest-beta": Get the current latest beta Obsidian version (or latest is there is no current beta)
514
700
  * - "earliest": Get the `minAppVersion` set in your `manifest.json`
515
- * @param installerVersion Obsidian version string or one of
701
+ * @param installerVersion specific version or one of
516
702
  * - "latest": Get the latest Obsidian installer compatible with `appVersion`
517
703
  * - "earliest": Get the oldest Obsidian installer compatible with `appVersion`
518
704
  *
@@ -520,22 +706,34 @@ var ObsidianLauncher = class {
520
706
  *
521
707
  * @returns [appVersion, installerVersion] with any "latest" etc. resolved to specific versions.
522
708
  */
523
- async resolveVersions(appVersion, installerVersion = "latest") {
709
+ async resolveVersion(appVersion, installerVersion = "latest") {
524
710
  const versions = await this.getVersions();
525
711
  const appVersionInfo = await this.getVersionInfo(appVersion);
712
+ appVersion = appVersionInfo.version;
713
+ let installerVersionInfo;
714
+ const { platform, arch } = process;
526
715
  if (!appVersionInfo.minInstallerVersion || !appVersionInfo.maxInstallerVersion) {
527
- throw Error(`No compatible installers available for app version ${appVersion}`);
716
+ throw Error(`No installers available for Obsidian ${appVersion}`);
528
717
  }
529
718
  if (installerVersion == "latest") {
530
- installerVersion = appVersionInfo.maxInstallerVersion;
719
+ installerVersionInfo = _lodash2.default.findLast(
720
+ versions,
721
+ (v) => _semver2.default.lte(v.version, appVersionInfo.version) && !!this.getInstallerKey(v, { platform, arch })
722
+ );
531
723
  } else if (installerVersion == "earliest") {
532
- installerVersion = appVersionInfo.minInstallerVersion;
724
+ installerVersionInfo = versions.find(
725
+ (v) => _semver2.default.gte(v.version, appVersionInfo.minInstallerVersion) && !!this.getInstallerKey(v, { platform, arch })
726
+ );
533
727
  } else {
534
728
  installerVersion = _nullishCoalesce(_semver2.default.valid(installerVersion), () => ( installerVersion));
729
+ installerVersionInfo = versions.find((v) => v.version == installerVersion);
535
730
  }
536
- const installerVersionInfo = versions.find((v) => v.version == installerVersion);
537
- if (!installerVersionInfo || !installerVersionInfo.chromeVersion) {
538
- throw Error(`No Obsidian installer for version ${installerVersion} found`);
731
+ if (!installerVersionInfo) {
732
+ if (["earliest", "latest"].includes(installerVersion)) {
733
+ throw Error(`No compatible installers available for Obsidian ${appVersion}`);
734
+ } else {
735
+ throw Error(`No Obsidian installer ${installerVersion} found`);
736
+ }
539
737
  }
540
738
  if (_semver2.default.lt(installerVersionInfo.version, appVersionInfo.minInstallerVersion) || _semver2.default.gt(installerVersionInfo.version, appVersionInfo.maxInstallerVersion)) {
541
739
  throw Error(
@@ -555,124 +753,145 @@ var ObsidianLauncher = class {
555
753
  } else if (appVersion == "latest") {
556
754
  appVersion = versions.filter((v) => !v.isBeta).at(-1).version;
557
755
  } else if (appVersion == "earliest") {
558
- appVersion = await _asyncOptionalChain([(await this.getRootManifest()), 'optionalAccess', async _27 => _27.minAppVersion]);
756
+ appVersion = await _asyncOptionalChain([(await this.getRootManifest()), 'optionalAccess', async _26 => _26.minAppVersion]);
559
757
  if (!appVersion) {
560
- throw Error('Unable to resolve Obsidian app appVersion "earliest", no manifest.json or minAppVersion found.');
758
+ throw Error('Unable to resolve Obsidian appVersion "earliest", no manifest.json or minAppVersion found.');
561
759
  }
562
760
  } else {
563
761
  appVersion = _nullishCoalesce(_semver2.default.valid(appVersion), () => ( appVersion));
564
762
  }
565
763
  const versionInfo = versions.find((v) => v.version == appVersion);
566
764
  if (!versionInfo) {
567
- throw Error(`No Obsidian app version ${appVersion} found`);
765
+ throw Error(`No Obsidian app version "${appVersion}" found`);
568
766
  }
569
767
  return versionInfo;
570
768
  }
571
769
  /**
572
- * Downloads the Obsidian installer for the given version and current platform. Returns the file path.
573
- * @param installerVersion Obsidian installer version to download
770
+ * Parses a string of Obsidian versions into [appVersion, installerVersion] tuples.
771
+ *
772
+ * `versions` should be a space separated list of Obsidian app versions. You can optionally specify the installer
773
+ * version by using "appVersion/installerVersion" e.g. `"1.7.7/1.8.10"`.
774
+ *
775
+ * Example:
776
+ * ```js
777
+ * launcher.parseVersions("1.8.10/1.7.7 latest latest-beta/earliest")
778
+ * ```
779
+ *
780
+ * See also: [Obsidian App vs Installer Versions](../README.md#obsidian-app-vs-installer-versions)
781
+ *
782
+ * @param versions string to parse
783
+ * @returns [appVersion, installerVersion][] resolved to specific versions.
784
+ */
785
+ async parseVersions(versions) {
786
+ let parsedVersions = versions.split(/[ ,]/).filter((v) => v).map((v) => {
787
+ const [appVersion, installerVersion = "earliest"] = v.split("/");
788
+ return [appVersion, installerVersion];
789
+ });
790
+ let resolvedVersions = [];
791
+ for (let [appVersion, installerVersion] of parsedVersions) {
792
+ resolvedVersions.push(await this.resolveVersion(appVersion, installerVersion));
793
+ }
794
+ return _lodash2.default.uniqBy(resolvedVersions, (v) => v.join("/"));
795
+ }
796
+ getInstallerKey(installerVersionInfo, opts = {}) {
797
+ const { platform, arch } = _lodash2.default.merge({}, opts, currentPlatform);
798
+ const platformName = `${platform}-${arch}`;
799
+ const key = _lodash2.default.findKey(installerVersionInfo.installers, (v) => v && v.platforms.includes(platformName));
800
+ return key;
801
+ }
802
+ /**
803
+ * Gets details about the Obsidian installer for the given platform.
804
+ * @param installerVersion Obsidian installer version
805
+ * @param opts.platform Platform/os (defaults to host platform)
806
+ * @param opts.arch Architecture (defaults to host architecture)
574
807
  */
575
- async downloadInstaller(installerVersion) {
576
- const installerVersionInfo = await this.getVersionInfo(installerVersion);
577
- return await this.downloadInstallerFromVersionInfo(installerVersionInfo);
808
+ async getInstallerInfo(installerVersion, opts = {}) {
809
+ const { platform, arch } = _lodash2.default.merge({}, opts, currentPlatform);
810
+ const versionInfo = await this.getVersionInfo(installerVersion);
811
+ const key = this.getInstallerKey(versionInfo, { platform, arch });
812
+ if (key) {
813
+ return { ...versionInfo.installers[key], url: versionInfo.downloads[key] };
814
+ } else {
815
+ throw Error(
816
+ `No Obsidian installer for ${installerVersion} ${platform}-${arch}` + (versionInfo.isBeta ? ` (${installerVersion} is a beta version)` : "")
817
+ );
818
+ }
578
819
  }
579
820
  /**
580
- * Helper for downloadInstaller that doesn't require the obsidian-versions.json file so it can be used in
581
- * updateObsidianVersionInfos
821
+ * Downloads the Obsidian installer for the given version and platform/arch (defaults to host platform/arch).
822
+ * Returns the file path.
823
+ * @param installerVersion Obsidian installer version to download
824
+ * @param opts.platform Platform/os of the installer to download (defaults to host platform)
825
+ * @param opts.arch Architecture of the installer to download (defaults to host architecture)
582
826
  */
583
- async downloadInstallerFromVersionInfo(versionInfo) {
584
- const installerVersion = versionInfo.version;
585
- const { platform, arch } = process;
827
+ async downloadInstaller(installerVersion, opts = {}) {
828
+ const { platform, arch } = _lodash2.default.merge({}, opts, currentPlatform);
829
+ const versionInfo = await this.getVersionInfo(installerVersion);
830
+ installerVersion = versionInfo.version;
831
+ const installerInfo = await this.getInstallerInfo(installerVersion, { platform, arch });
586
832
  const cacheDir = _path2.default.join(this.cacheDir, `obsidian-installer/${platform}-${arch}/Obsidian-${installerVersion}`);
587
- let installerPath;
588
- let downloader;
833
+ let binaryPath;
834
+ let extractor;
589
835
  if (platform == "linux") {
590
- installerPath = _path2.default.join(cacheDir, "obsidian");
591
- let installerUrl;
592
- if (arch.startsWith("arm")) {
593
- installerUrl = versionInfo.downloads.appImageArm;
594
- } else {
595
- installerUrl = versionInfo.downloads.appImage;
596
- }
597
- if (installerUrl) {
598
- downloader = async (tmpDir) => {
599
- const appImage = _path2.default.join(tmpDir, "Obsidian.AppImage");
600
- await downloadResponse(await fetch(installerUrl), appImage);
601
- const obsidianFolder = _path2.default.join(tmpDir, "Obsidian");
602
- await extractObsidianAppImage(appImage, obsidianFolder);
603
- return obsidianFolder;
604
- };
605
- }
836
+ binaryPath = _path2.default.join(cacheDir, "obsidian");
837
+ extractor = (installer, dest) => extractObsidianAppImage(installer, dest);
606
838
  } else if (platform == "win32") {
607
- installerPath = _path2.default.join(cacheDir, "Obsidian.exe");
608
- const installerUrl = versionInfo.downloads.exe;
609
- let appArch;
610
- if (arch == "x64") {
611
- appArch = "app-64";
612
- } else if (arch == "ia32") {
613
- appArch = "app-32";
614
- } else if (arch.startsWith("arm")) {
615
- appArch = "app-arm64";
616
- }
617
- if (installerUrl && appArch) {
618
- downloader = async (tmpDir) => {
619
- const installerExecutable = _path2.default.join(tmpDir, "Obsidian.exe");
620
- await downloadResponse(await fetch(installerUrl), installerExecutable);
621
- const obsidianFolder = _path2.default.join(tmpDir, "Obsidian");
622
- await extractObsidianExe(installerExecutable, appArch, obsidianFolder);
623
- return obsidianFolder;
624
- };
625
- }
839
+ binaryPath = _path2.default.join(cacheDir, "Obsidian.exe");
840
+ extractor = (installer, dest) => extractObsidianExe(installer, arch, dest);
626
841
  } else if (platform == "darwin") {
627
- installerPath = _path2.default.join(cacheDir, "Contents/MacOS/Obsidian");
628
- const installerUrl = versionInfo.downloads.dmg;
629
- if (installerUrl) {
630
- downloader = async (tmpDir) => {
631
- const dmg = _path2.default.join(tmpDir, "Obsidian.dmg");
632
- await downloadResponse(await fetch(installerUrl), dmg);
633
- const obsidianFolder = _path2.default.join(tmpDir, "Obsidian");
634
- await extractObsidianDmg(dmg, obsidianFolder);
635
- return obsidianFolder;
636
- };
637
- }
842
+ binaryPath = _path2.default.join(cacheDir, "Contents/MacOS/Obsidian");
843
+ extractor = (installer, dest) => extractObsidianDmg(installer, dest);
638
844
  } else {
639
845
  throw Error(`Unsupported platform ${platform}`);
640
846
  }
641
- if (!downloader) {
642
- throw Error(`No Obsidian installer download available for v${installerVersion} ${platform} ${arch}`);
643
- }
644
- if (!await fileExists(installerPath)) {
847
+ if (!await fileExists(binaryPath)) {
645
848
  console.log(`Downloading Obsidian installer v${installerVersion}...`);
646
- await _promises2.default.mkdir(_path2.default.dirname(cacheDir), { recursive: true });
647
- await withTmpDir(cacheDir, downloader);
849
+ await atomicCreate(cacheDir, async (tmpDir) => {
850
+ const installer = _path2.default.join(tmpDir, "installer");
851
+ await downloadResponse(await fetch(installerInfo.url), installer);
852
+ const extracted = _path2.default.join(tmpDir, "extracted");
853
+ await extractor(installer, extracted);
854
+ return extracted;
855
+ });
648
856
  }
649
- return installerPath;
857
+ return binaryPath;
650
858
  }
651
859
  /**
652
860
  * Downloads the Obsidian asar for the given version. Returns the file path.
653
861
  *
654
- * To download beta versions you'll need to have an Obsidian account with Catalyst and set the `OBSIDIAN_USERNAME`
655
- * and `OBSIDIAN_PASSWORD` environment variables. 2FA needs to be disabled.
862
+ * To download Obsidian beta versions you'll need have an Obsidian Insiders account and either set the
863
+ * `OBSIDIAN_EMAIL` and `OBSIDIAN_PASSWORD` env vars (`.env` is supported) or pre-download the Obsidian beta with
864
+ * `npx obsidian-launcher download -v latest-beta`
656
865
  *
657
866
  * @param appVersion Obsidian version to download
658
867
  */
659
868
  async downloadApp(appVersion) {
660
- const appVersionInfo = await this.getVersionInfo(appVersion);
661
- const appUrl = appVersionInfo.downloads.asar;
869
+ const versionInfo = await this.getVersionInfo(appVersion);
870
+ const appUrl = versionInfo.downloads.asar;
662
871
  if (!appUrl) {
663
872
  throw Error(`No asar found for Obsidian version ${appVersion}`);
664
873
  }
665
- const appPath = _path2.default.join(this.cacheDir, "obsidian-app", `obsidian-${appVersionInfo.version}.asar`);
874
+ const appPath = _path2.default.join(this.cacheDir, "obsidian-app", `obsidian-${versionInfo.version}.asar`);
666
875
  if (!await fileExists(appPath)) {
667
- console.log(`Downloading Obsidian app v${appVersion} ...`);
668
- await _promises2.default.mkdir(_path2.default.dirname(appPath), { recursive: true });
669
- await withTmpDir(appPath, async (tmpDir) => {
670
- const isInsidersBuild = new URL(appUrl).hostname.endsWith(".obsidian.md");
671
- const response = isInsidersBuild ? await fetchObsidianAPI(appUrl) : await fetch(appUrl);
876
+ console.log(`Downloading Obsidian app v${versionInfo.version} ...`);
877
+ await atomicCreate(appPath, async (tmpDir) => {
878
+ const isInsiders = new URL(appUrl).hostname.endsWith(".obsidian.md");
879
+ let response;
880
+ if (isInsiders) {
881
+ if (!this.obsidianApiToken) {
882
+ this.obsidianApiToken = await obsidianApiLogin({
883
+ interactive: this.interactive,
884
+ savePath: _path2.default.join(this.cacheDir, "obsidian-credentials.env")
885
+ });
886
+ }
887
+ response = await fetchObsidianApi(appUrl, { token: this.obsidianApiToken });
888
+ } else {
889
+ response = await fetch(appUrl);
890
+ }
672
891
  const archive = _path2.default.join(tmpDir, "app.asar.gz");
673
892
  const asar = _path2.default.join(tmpDir, "app.asar");
674
893
  await downloadResponse(response, archive);
675
- await _promises3.pipeline.call(void 0, _fs2.default.createReadStream(archive), _zlib2.default.createGunzip(), _fs2.default.createWriteStream(asar));
894
+ await extractGz(archive, asar);
676
895
  return asar;
677
896
  });
678
897
  }
@@ -689,34 +908,53 @@ var ObsidianLauncher = class {
689
908
  *
690
909
  * @param installerVersion Obsidian installer version
691
910
  */
692
- async downloadChromedriver(installerVersion) {
693
- const versionInfo = await this.getVersionInfo(installerVersion);
694
- const electronVersion = versionInfo.electronVersion;
695
- if (!electronVersion) {
696
- throw Error(`${installerVersion} is not an Obsidian installer version.`);
697
- }
698
- const chromedriverZipPath = await _get.downloadArtifact.call(void 0, {
699
- version: electronVersion,
700
- artifactName: "chromedriver",
701
- cacheRoot: _path2.default.join(this.cacheDir, "chromedriver-legacy"),
702
- unsafelyDisableChecksums: true
703
- // the checksums are slow and run even on cache hit.
704
- });
911
+ async downloadChromedriver(installerVersion, opts = {}) {
912
+ const { platform, arch } = _lodash2.default.merge({}, opts, currentPlatform);
913
+ const installerInfo = await this.getInstallerInfo(installerVersion, { platform, arch });
914
+ const cacheDir = _path2.default.join(this.cacheDir, `electron-chromedriver/${platform}-${arch}/${installerInfo.electron}`);
705
915
  let chromedriverPath;
706
916
  if (process.platform == "win32") {
707
- chromedriverPath = _path2.default.join(_path2.default.dirname(chromedriverZipPath), "chromedriver.exe");
917
+ chromedriverPath = _path2.default.join(cacheDir, `chromedriver.exe`);
708
918
  } else {
709
- chromedriverPath = _path2.default.join(_path2.default.dirname(chromedriverZipPath), "chromedriver");
919
+ chromedriverPath = _path2.default.join(cacheDir, `chromedriver`);
710
920
  }
711
921
  if (!await fileExists(chromedriverPath)) {
712
- console.log(`Downloading legacy chromedriver for electron ${electronVersion} ...`);
713
- await withTmpDir(chromedriverPath, async (tmpDir) => {
714
- await _extractzip2.default.call(void 0, chromedriverZipPath, { dir: tmpDir });
715
- return _path2.default.join(tmpDir, _path2.default.basename(chromedriverPath));
922
+ console.log(`Downloading chromedriver for electron ${installerInfo.electron} ...`);
923
+ await atomicCreate(cacheDir, async (tmpDir) => {
924
+ const chromedriverZipPath = await _get.downloadArtifact.call(void 0, {
925
+ version: installerInfo.electron,
926
+ artifactName: "chromedriver",
927
+ cacheRoot: _path2.default.join(tmpDir, "download")
928
+ });
929
+ const extracted = _path2.default.join(tmpDir, "extracted");
930
+ await _extractzip2.default.call(void 0, chromedriverZipPath, { dir: extracted });
931
+ return extracted;
716
932
  });
717
933
  }
718
934
  return chromedriverPath;
719
935
  }
936
+ /**
937
+ * Downloads the Obsidian apk.
938
+ */
939
+ async downloadAndroid(version) {
940
+ const versionInfo = await this.getVersionInfo(version);
941
+ const apkUrl = versionInfo.downloads.apk;
942
+ if (!apkUrl) {
943
+ throw Error(
944
+ `No apk found for Obsidian version ${version}` + (versionInfo.isBeta ? ` (${version} is a beta version)` : "")
945
+ );
946
+ }
947
+ const apkPath = _path2.default.join(this.cacheDir, "obsidian-apk", `obsidian-${versionInfo.version}.apk`);
948
+ if (!await fileExists(apkPath)) {
949
+ console.log(`Downloading Obsidian apk v${versionInfo.version} ...`);
950
+ await atomicCreate(apkPath, async (tmpDir) => {
951
+ const dest = _path2.default.join(tmpDir, "obsidian.apk");
952
+ await downloadResponse(await fetch(apkUrl), dest);
953
+ return dest;
954
+ });
955
+ }
956
+ return apkPath;
957
+ }
720
958
  /** Gets the latest version of a plugin. */
721
959
  async getLatestPluginVersion(repo) {
722
960
  repo = normalizeGitHubRepo(repo);
@@ -742,8 +980,7 @@ var ObsidianLauncher = class {
742
980
  version = _semver2.default.valid(version);
743
981
  const pluginDir = _path2.default.join(this.cacheDir, "obsidian-plugins", repo, version);
744
982
  if (!await fileExists(pluginDir)) {
745
- await _promises2.default.mkdir(_path2.default.dirname(pluginDir), { recursive: true });
746
- await withTmpDir(pluginDir, async (tmpDir) => {
983
+ await atomicCreate(pluginDir, async (tmpDir) => {
747
984
  const assetsToDownload = { "manifest.json": true, "main.js": true, "styles.css": false };
748
985
  await Promise.all(
749
986
  Object.entries(assetsToDownload).map(async ([file, required]) => {
@@ -776,8 +1013,8 @@ var ObsidianLauncher = class {
776
1013
  return await this.downloadGitHubPlugin(pluginInfo.repo, version);
777
1014
  }
778
1015
  /**
779
- * Downloads a list of plugins to the cache and returns a list of `DownloadedPluginEntry` with the downloaded paths.
780
- * Also adds the `id` property to the plugins based on the manifest.
1016
+ * Downloads a list of plugins to the cache and returns a list of {@link DownloadedPluginEntry} with the downloaded
1017
+ * paths. Also adds the `id` property to the plugins based on the manifest.
781
1018
  *
782
1019
  * You can download plugins from GitHub using `{repo: "org/repo"}` and community plugins using `{id: 'plugin-id'}`.
783
1020
  * Local plugins will just be passed through.
@@ -850,8 +1087,7 @@ var ObsidianLauncher = class {
850
1087
  const version = await this.getLatestThemeVersion(repo);
851
1088
  const themeDir = _path2.default.join(this.cacheDir, "obsidian-themes", repo, version);
852
1089
  if (!await fileExists(themeDir)) {
853
- await _promises2.default.mkdir(_path2.default.dirname(themeDir), { recursive: true });
854
- await withTmpDir(themeDir, async (tmpDir) => {
1090
+ await atomicCreate(themeDir, async (tmpDir) => {
855
1091
  const assetsToDownload = ["manifest.json", "theme.css"];
856
1092
  await Promise.all(
857
1093
  assetsToDownload.map(async (file) => {
@@ -883,8 +1119,8 @@ var ObsidianLauncher = class {
883
1119
  return await this.downloadGitHubTheme(themeInfo.repo);
884
1120
  }
885
1121
  /**
886
- * Downloads a list of themes to the cache and returns a list of `DownloadedThemeEntry` with the downloaded paths.
887
- * Also adds the `name` property to the plugins based on the manifest.
1122
+ * Downloads a list of themes to the cache and returns a list of {@link DownloadedThemeEntry} with the downloaded
1123
+ * paths. Also adds the `name` property to the plugins based on the manifest.
888
1124
  *
889
1125
  * You can download themes from GitHub using `{repo: "org/repo"}` and community themes using `{name: 'theme-name'}`.
890
1126
  * Local themes will just be passed through.
@@ -1038,27 +1274,35 @@ var ObsidianLauncher = class {
1038
1274
  * @param params.appVersion Obsidian app version
1039
1275
  * @param params.installerVersion Obsidian version string.
1040
1276
  * @param params.appPath Path to the asar file to install. Will download if omitted.
1041
- * @param params.vault Path to the vault to open in Obsidian.
1042
- * @param params.dest Destination path for the config dir. If omitted it will create a temporary directory.
1277
+ * @param params.vault Path to the vault to open in Obsidian
1278
+ * @param params.localStorage items to add to localStorage. `$vaultId` in the keys will be replaced with the vaultId
1279
+ * @param params.chromePreferences Chrome preferences to add to the Preferences file
1043
1280
  */
1044
1281
  async setupConfigDir(params) {
1045
- const [appVersion, installerVersion] = await this.resolveVersions(params.appVersion, params.installerVersion);
1046
- const configDir = await _asyncNullishCoalesce(params.dest, async () => ( await makeTmpDir("obs-launcher-config-")));
1047
- let obsidianJson = {
1048
- updateDisabled: true
1049
- // Prevents Obsidian trying to auto-update on boot.
1050
- };
1051
- let localStorageData = {
1052
- "most-recently-installed-version": appVersion
1282
+ const [appVersion, installerVersion] = await this.resolveVersion(params.appVersion, params.installerVersion);
1283
+ const configDir = await makeTmpDir("obsidian-launcher-config-");
1284
+ const vaultId = _crypto2.default.randomBytes(8).toString("hex");
1285
+ const localStorageData = {
1286
+ "most-recently-installed-version": appVersion,
1053
1287
  // prevents the changelog page on boot
1288
+ [`enable-plugin-${vaultId}`]: "true",
1289
+ // Disable "safe mode" and enable plugins
1290
+ ..._lodash2.default.mapKeys(_nullishCoalesce(params.localStorage, () => ( {})), (v, k) => k.replace("$vaultId", _nullishCoalesce(vaultId, () => ( ""))))
1291
+ };
1292
+ const chromePreferences = _lodash2.default.merge(
1293
+ // disables the "allow pasting" bit in the dev tools console
1294
+ { "electron": { "devtools": { "preferences": { "disable-self-xss-warning": "true" } } } },
1295
+ _nullishCoalesce(params.chromePreferences, () => ( {}))
1296
+ );
1297
+ const obsidianJson = {
1298
+ updateDisabled: true
1299
+ // prevents Obsidian trying to auto-update on boot.
1054
1300
  };
1055
1301
  if (params.vault !== void 0) {
1056
1302
  if (!await fileExists(params.vault)) {
1057
1303
  throw Error(`Vault path ${params.vault} doesn't exist.`);
1058
1304
  }
1059
- const vaultId = _crypto2.default.randomBytes(8).toString("hex");
1060
- obsidianJson = {
1061
- ...obsidianJson,
1305
+ Object.assign(obsidianJson, {
1062
1306
  vaults: {
1063
1307
  [vaultId]: {
1064
1308
  path: _path2.default.resolve(params.vault),
@@ -1066,14 +1310,10 @@ var ObsidianLauncher = class {
1066
1310
  open: true
1067
1311
  }
1068
1312
  }
1069
- };
1070
- localStorageData = {
1071
- ...localStorageData,
1072
- [`enable-plugin-${vaultId}`]: "true"
1073
- // Disable "safe mode" and enable plugins
1074
- };
1313
+ });
1075
1314
  }
1076
1315
  await _promises2.default.writeFile(_path2.default.join(configDir, "obsidian.json"), JSON.stringify(obsidianJson));
1316
+ await _promises2.default.writeFile(_path2.default.join(configDir, "Preferences"), JSON.stringify(chromePreferences));
1077
1317
  let appPath = params.appPath;
1078
1318
  if (!appPath) {
1079
1319
  appPath = await this.downloadApp(appVersion);
@@ -1108,8 +1348,6 @@ var ObsidianLauncher = class {
1108
1348
  * Downloads and launches Obsidian with a sandboxed config dir and a specifc vault open. Optionally install plugins
1109
1349
  * and themes first.
1110
1350
  *
1111
- * This is just a shortcut for calling `downloadApp`, `downloadInstaller`, `setupVault` and `setupConfDir`.
1112
- *
1113
1351
  * @param params.appVersion Obsidian app version. Default "latest"
1114
1352
  * @param params.installerVersion Obsidian installer version. Default "latest"
1115
1353
  * @param params.vault Path to the vault to open in Obsidian
@@ -1117,11 +1355,12 @@ var ObsidianLauncher = class {
1117
1355
  * @param params.plugins List of plugins to install in the vault
1118
1356
  * @param params.themes List of themes to install in the vault
1119
1357
  * @param params.args CLI args to pass to Obsidian
1358
+ * @param params.localStorage items to add to localStorage. `$vaultId` in the keys will be replaced with the vaultId
1120
1359
  * @param params.spawnOptions Options to pass to `spawn`
1121
1360
  * @returns The launched child process and the created tmpdirs
1122
1361
  */
1123
1362
  async launch(params) {
1124
- const [appVersion, installerVersion] = await this.resolveVersions(
1363
+ const [appVersion, installerVersion] = await this.resolveVersion(
1125
1364
  _nullishCoalesce(params.appVersion, () => ( "latest")),
1126
1365
  _nullishCoalesce(params.installerVersion, () => ( "latest"))
1127
1366
  );
@@ -1136,10 +1375,16 @@ var ObsidianLauncher = class {
1136
1375
  themes: params.themes
1137
1376
  });
1138
1377
  }
1139
- const configDir = await this.setupConfigDir({ appVersion, installerVersion, appPath, vault });
1378
+ const configDir = await this.setupConfigDir({
1379
+ appVersion,
1380
+ installerVersion,
1381
+ appPath,
1382
+ vault,
1383
+ localStorage: params.localStorage
1384
+ });
1140
1385
  const proc = _child_process2.default.spawn(installerPath, [
1141
1386
  `--user-data-dir=${configDir}`,
1142
- // Workaround for SUID issue on AppImages. See https://github.com/electron/electron/issues/42510
1387
+ // Workaround for SUID issue on linux. See https://github.com/electron/electron/issues/42510
1143
1388
  ...process.platform == "linux" ? ["--no-sandbox"] : [],
1144
1389
  ..._nullishCoalesce(params.args, () => ( []))
1145
1390
  ], {
@@ -1148,91 +1393,80 @@ var ObsidianLauncher = class {
1148
1393
  return { proc, configDir, vault };
1149
1394
  }
1150
1395
  /**
1151
- * Updates the info obsidian-versions.json. The obsidian-versions.json file is used in other launcher commands
1396
+ * Updates the info in obsidian-versions.json. The obsidian-versions.json file is used in other launcher commands
1152
1397
  * and in wdio-obsidian-service to get metadata about Obsidian versions in one place such as minInstallerVersion and
1153
- * the internal electron version.
1398
+ * the internal Electron version.
1154
1399
  */
1155
- async updateObsidianVersionInfos(original, { maxInstances = 1 } = {}) {
1400
+ async updateVersionList(original, opts = {}) {
1401
+ const { maxInstances = 1 } = opts;
1156
1402
  const repo = "obsidianmd/obsidian-releases";
1403
+ const originalVersions = _lodash2.default.keyBy(_nullishCoalesce(_optionalChain([original, 'optionalAccess', _27 => _27.versions]), () => ( [])), (v) => v.version);
1404
+ const versions = _lodash2.default.cloneDeep(originalVersions);
1157
1405
  let commitHistory = await fetchGitHubAPIPaginated(`repos/${repo}/commits`, {
1158
1406
  path: "desktop-releases.json",
1159
- since: _optionalChain([original, 'optionalAccess', _28 => _28.metadata, 'access', _29 => _29.commitDate])
1407
+ since: _optionalChain([original, 'optionalAccess', _28 => _28.metadata, 'optionalAccess', _29 => _29.commitDate])
1160
1408
  });
1161
1409
  commitHistory.reverse();
1162
- if (original) {
1410
+ if (_optionalChain([original, 'optionalAccess', _30 => _30.metadata, 'optionalAccess', _31 => _31.commitSha])) {
1163
1411
  commitHistory = _lodash2.default.takeRightWhile(commitHistory, (c) => c.sha != original.metadata.commitSha);
1164
1412
  }
1165
1413
  const fileHistory = await pool(
1166
- 8,
1414
+ maxInstances,
1167
1415
  commitHistory,
1168
1416
  (commit) => fetch(`https://raw.githubusercontent.com/${repo}/${commit.sha}/desktop-releases.json`).then((r) => r.json())
1169
1417
  );
1170
- const githubReleases = await fetchGitHubAPIPaginated(`repos/${repo}/releases`);
1171
- const versionMap = _lodash2.default.keyBy(
1172
- _nullishCoalesce(_optionalChain([original, 'optionalAccess', _30 => _30.versions]), () => ( [])),
1173
- (v) => v.version
1174
- );
1175
- for (const { beta, ...current } of fileHistory) {
1176
- if (beta && (!versionMap[beta.latestVersion] || versionMap[beta.latestVersion].isBeta)) {
1177
- versionMap[beta.latestVersion] = _lodash2.default.merge(
1178
- {},
1179
- versionMap[beta.latestVersion],
1180
- parseObsidianDesktopRelease(beta, true)
1181
- );
1418
+ for (const fileVersion of fileHistory) {
1419
+ const { current, beta } = parseObsidianDesktopRelease(fileVersion);
1420
+ if (beta) {
1421
+ versions[beta.version] = _lodash2.default.merge(_nullishCoalesce(versions[beta.version], () => ( {})), beta);
1182
1422
  }
1183
- versionMap[current.latestVersion] = _lodash2.default.merge(
1184
- {},
1185
- versionMap[current.latestVersion],
1186
- parseObsidianDesktopRelease(current, false)
1187
- );
1423
+ versions[current.version] = _lodash2.default.merge(_nullishCoalesce(versions[current.version], () => ( {})), current);
1188
1424
  }
1425
+ const githubReleases = (await fetchGitHubAPIPaginated(`repos/${repo}/releases`)).reverse();
1189
1426
  for (const release of githubReleases) {
1190
- if (versionMap.hasOwnProperty(release.name)) {
1191
- versionMap[release.name] = _lodash2.default.merge({}, versionMap[release.name], parseObsidianGithubRelease(release));
1427
+ if (_semver2.default.valid(release.name)) {
1428
+ const parsed = parseObsidianGithubRelease(release);
1429
+ versions[parsed.version] = _lodash2.default.merge(_nullishCoalesce(versions[parsed.version], () => ( {})), parsed);
1192
1430
  }
1193
1431
  }
1194
- const dependencyVersions = await pool(
1195
- maxInstances,
1196
- Object.values(versionMap).filter((v) => _optionalChain([v, 'access', _31 => _31.downloads, 'optionalAccess', _32 => _32.appImage]) && !v.chromeVersion),
1197
- async (v) => {
1198
- const binaryPath = await this.downloadInstallerFromVersionInfo(v);
1199
- const electronVersionInfo = await getElectronVersionInfo(v.version, binaryPath);
1200
- return { ...v, ...electronVersionInfo };
1432
+ const installerKeys = ["appImage", "appImageArm", "tar", "tarArm", "dmg", "exe"];
1433
+ const newInstallers = Object.values(versions).flatMap((v) => installerKeys.map((k) => [v, k]));
1434
+ const installerInfos = await pool(maxInstances, newInstallers, async ([v, key]) => {
1435
+ let installerInfo;
1436
+ const hasAsset = !!_optionalChain([v, 'access', _32 => _32.downloads, 'optionalAccess', _33 => _33[key]]);
1437
+ const alreadyExtracted = !!_optionalChain([v, 'access', _34 => _34.installers, 'optionalAccess', _35 => _35[key], 'optionalAccess', _36 => _36.chrome]);
1438
+ const changed = _optionalChain([v, 'access', _37 => _37.installers, 'optionalAccess', _38 => _38[key], 'optionalAccess', _39 => _39.digest]) != _optionalChain([originalVersions, 'access', _40 => _40[v.version], 'optionalAccess', _41 => _41.installers, 'access', _42 => _42[key], 'optionalAccess', _43 => _43.digest]);
1439
+ if (hasAsset && (!alreadyExtracted || changed)) {
1440
+ installerInfo = await getInstallerInfo(key, v.downloads[key]);
1201
1441
  }
1202
- );
1203
- for (const deps of dependencyVersions) {
1204
- versionMap[deps.version] = _lodash2.default.merge({}, versionMap[deps.version], deps);
1442
+ return { version: v.version, installerInfo: { [key]: installerInfo } };
1443
+ });
1444
+ for (const installerInfo of installerInfos) {
1445
+ versions[installerInfo.version] = _lodash2.default.merge(_nullishCoalesce(versions[installerInfo.version], () => ( {})), installerInfo);
1205
1446
  }
1206
1447
  let minInstallerVersion = void 0;
1207
1448
  let maxInstallerVersion = void 0;
1208
- for (const version of Object.keys(versionMap).sort(_semver2.default.compare)) {
1209
- if (!minInstallerVersion && versionMap[version].chromeVersion) {
1449
+ for (const version of Object.keys(versions).sort(_semver2.default.compare)) {
1450
+ if (!minInstallerVersion && versions[version].downloads.appImage) {
1210
1451
  minInstallerVersion = version;
1211
1452
  }
1212
- if (versionMap[version].downloads.appImage) {
1453
+ if (versions[version].downloads.appImage) {
1213
1454
  maxInstallerVersion = version;
1214
1455
  }
1215
- versionMap[version] = mergeKeepUndefined(
1216
- {},
1217
- versionMap[version],
1218
- {
1219
- minInstallerVersion: _nullishCoalesce(versionMap[version].minInstallerVersion, () => ( minInstallerVersion)),
1220
- maxInstallerVersion
1221
- },
1222
- correctObsidianVersionInfo(versionMap[version])
1223
- );
1456
+ versions[version] = _lodash2.default.merge({ minInstallerVersion, maxInstallerVersion }, versions[version]);
1224
1457
  }
1225
1458
  const result = {
1226
1459
  metadata: {
1227
- commitDate: _nullishCoalesce(_optionalChain([commitHistory, 'access', _33 => _33.at, 'call', _34 => _34(-1), 'optionalAccess', _35 => _35.commit, 'access', _36 => _36.committer, 'access', _37 => _37.date]), () => ( _optionalChain([original, 'optionalAccess', _38 => _38.metadata, 'access', _39 => _39.commitDate]))),
1228
- commitSha: _nullishCoalesce(_optionalChain([commitHistory, 'access', _40 => _40.at, 'call', _41 => _41(-1), 'optionalAccess', _42 => _42.sha]), () => ( _optionalChain([original, 'optionalAccess', _43 => _43.metadata, 'access', _44 => _44.commitSha]))),
1229
- timestamp: _nullishCoalesce(_optionalChain([original, 'optionalAccess', _45 => _45.metadata, 'access', _46 => _46.timestamp]), () => ( ""))
1460
+ schemaVersion: obsidianVersionsSchemaVersion,
1461
+ commitDate: _nullishCoalesce(_optionalChain([commitHistory, 'access', _44 => _44.at, 'call', _45 => _45(-1), 'optionalAccess', _46 => _46.commit, 'access', _47 => _47.committer, 'access', _48 => _48.date]), () => ( _optionalChain([original, 'optionalAccess', _49 => _49.metadata, 'access', _50 => _50.commitDate]))),
1462
+ commitSha: _nullishCoalesce(_optionalChain([commitHistory, 'access', _51 => _51.at, 'call', _52 => _52(-1), 'optionalAccess', _53 => _53.sha]), () => ( _optionalChain([original, 'optionalAccess', _54 => _54.metadata, 'access', _55 => _55.commitSha]))),
1463
+ timestamp: _nullishCoalesce(_optionalChain([original, 'optionalAccess', _56 => _56.metadata, 'access', _57 => _57.timestamp]), () => ( ""))
1230
1464
  // set down below
1231
1465
  },
1232
- versions: Object.values(versionMap).map(normalizeObsidianVersionInfo).sort((a, b) => _semver2.default.compare(a.version, b.version))
1466
+ versions: Object.values(versions).map(normalizeObsidianVersionInfo).sort((a, b) => _semver2.default.compare(a.version, b.version))
1233
1467
  };
1234
1468
  const dayMs = 24 * 60 * 60 * 1e3;
1235
- const timeSinceLastUpdate = (/* @__PURE__ */ new Date()).getTime() - new Date(_nullishCoalesce(_optionalChain([original, 'optionalAccess', _47 => _47.metadata, 'access', _48 => _48.timestamp]), () => ( 0))).getTime();
1469
+ const timeSinceLastUpdate = (/* @__PURE__ */ new Date()).getTime() - new Date(_nullishCoalesce(_optionalChain([original, 'optionalAccess', _58 => _58.metadata, 'access', _59 => _59.timestamp]), () => ( 0))).getTime();
1236
1470
  if (!_lodash2.default.isEqual(original, result) || timeSinceLastUpdate > 29 * dayMs) {
1237
1471
  result.metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
1238
1472
  }
@@ -1261,18 +1495,21 @@ var ObsidianLauncher = class {
1261
1495
  */
1262
1496
  async isAvailable(appVersion) {
1263
1497
  const versionInfo = await this.getVersionInfo(appVersion);
1264
- const versionExists = !!(versionInfo.downloads.asar && versionInfo.minInstallerVersion);
1265
- if (versionInfo.isBeta) {
1266
- const hasCreds = !!(process.env["OBSIDIAN_USERNAME"] && process.env["OBSIDIAN_PASSWORD"]);
1498
+ if (!versionInfo.downloads.asar || !versionInfo.minInstallerVersion) {
1499
+ return false;
1500
+ }
1501
+ if (new URL(versionInfo.downloads.asar).hostname.endsWith(".obsidian.md")) {
1502
+ const hasCreds = !!(process.env["OBSIDIAN_EMAIL"] && process.env["OBSIDIAN_PASSWORD"]);
1267
1503
  const inCache = await this.isInCache("app", versionInfo.version);
1268
- return versionExists && (hasCreds || inCache);
1504
+ return hasCreds || inCache;
1269
1505
  } else {
1270
- return versionExists;
1506
+ return true;
1271
1507
  }
1272
1508
  }
1273
1509
  };
1274
1510
 
1275
1511
 
1276
1512
 
1277
- exports.ObsidianLauncher = ObsidianLauncher;
1278
- //# sourceMappingURL=chunk-CUFVRSMT.cjs.map
1513
+
1514
+ exports.watchFiles = watchFiles; exports.ObsidianLauncher = ObsidianLauncher;
1515
+ //# sourceMappingURL=chunk-KQZZMJOZ.cjs.map