obsidian-launcher 2.1.0 → 2.1.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.
@@ -120,8 +120,29 @@ var _stream = require('stream');
120
120
  var _readlinesync = require('readline-sync'); var _readlinesync2 = _interopRequireDefault(_readlinesync);
121
121
 
122
122
 
123
- var _octokit = require('octokit');
124
123
  var _process = require('process');
124
+ function parseLinkHeader(linkHeader) {
125
+ function parseLinkData(linkData) {
126
+ return Object.fromEntries(
127
+ linkData.split(";").flatMap((x) => {
128
+ const partMatch = x.trim().match(/^([^=]+?)\s*=\s*"?([^"]+)"?$/);
129
+ return partMatch ? [[partMatch[1], partMatch[2]]] : [];
130
+ })
131
+ );
132
+ }
133
+ const linkDatas = linkHeader.split(/,\s*(?=<)/).flatMap((link) => {
134
+ const linkMatch = link.trim().match(/^<([^>]*)>(.*)$/);
135
+ if (linkMatch) {
136
+ return [{
137
+ url: linkMatch[1],
138
+ ...parseLinkData(linkMatch[2])
139
+ }];
140
+ } else {
141
+ return [];
142
+ }
143
+ }).filter((l) => l.rel);
144
+ return Object.fromEntries(linkDatas.map((l) => [l.rel, l]));
145
+ }
125
146
  function createURL(url, base, params = {}) {
126
147
  const cleanParams = _lodash2.default.call(void 0, params).pickBy((x) => x !== void 0).mapValues((v) => String(v)).value();
127
148
  const urlObj = new URL(url, base);
@@ -131,8 +152,25 @@ function createURL(url, base, params = {}) {
131
152
  }
132
153
  return urlObj.toString();
133
154
  }
134
- function getGithubClient() {
135
- return new (0, _octokit.Octokit)({ auth: _process.env.GITHUB_TOKEN });
155
+ async function fetchGitHubAPI(url, params = {}) {
156
+ url = createURL(url, "https://api.github.com", params);
157
+ const token = _process.env.GITHUB_TOKEN;
158
+ const headers = token ? { Authorization: "Bearer " + token } : {};
159
+ const response = await fetch(url, { headers });
160
+ if (!response.ok) {
161
+ throw new Error(`GitHub API error: ${await response.text()}`);
162
+ }
163
+ return response;
164
+ }
165
+ async function fetchGitHubAPIPaginated(url, params = {}) {
166
+ const results = [];
167
+ let next = createURL(url, "https://api.github.com", { per_page: 100, ...params });
168
+ while (next) {
169
+ const response = await fetchGitHubAPI(next);
170
+ results.push(...await response.json());
171
+ next = _optionalChain([parseLinkHeader, 'call', _5 => _5(_nullishCoalesce(response.headers.get("link"), () => ( ""))), 'access', _6 => _6.next, 'optionalAccess', _7 => _7.url]);
172
+ }
173
+ return results;
136
174
  }
137
175
  async function obsidianApiLogin(opts) {
138
176
  const { interactive = false, savePath } = opts;
@@ -153,7 +191,7 @@ async function obsidianApiLogin(opts) {
153
191
  let needsMfa = false;
154
192
  let retries = 0;
155
193
  let signin = void 0;
156
- while (!_optionalChain([signin, 'optionalAccess', _5 => _5.token]) && retries < 3) {
194
+ while (!_optionalChain([signin, 'optionalAccess', _8 => _8.token]) && retries < 3) {
157
195
  if (retries > 0 || _process.env.CI) {
158
196
  await sleep(2 * Math.random() + retries * retries * 2);
159
197
  }
@@ -170,24 +208,24 @@ async function obsidianApiLogin(opts) {
170
208
  },
171
209
  body: JSON.stringify({ email, password, mfa })
172
210
  }).then((r) => r.json());
173
- const error = _optionalChain([signin, 'access', _6 => _6.error, 'optionalAccess', _7 => _7.toLowerCase, 'call', _8 => _8()]);
174
- if (_optionalChain([error, 'optionalAccess', _9 => _9.includes, 'call', _10 => _10("2fa")]) && !needsMfa) {
211
+ const error = _optionalChain([signin, 'access', _9 => _9.error, 'optionalAccess', _10 => _10.toLowerCase, 'call', _11 => _11()]);
212
+ if (_optionalChain([error, 'optionalAccess', _12 => _12.includes, 'call', _13 => _13("2fa")]) && !needsMfa) {
175
213
  needsMfa = true;
176
214
  if (!interactive) {
177
215
  throw Error(
178
216
  "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 app -v <version>`"
179
217
  );
180
218
  }
181
- } else if (["please wait", "try again"].some((m) => _optionalChain([error, 'optionalAccess', _11 => _11.includes, 'call', _12 => _12(m)]))) {
219
+ } else if (["please wait", "try again"].some((m) => _optionalChain([error, 'optionalAccess', _14 => _14.includes, 'call', _15 => _15(m)]))) {
182
220
  console.warn(`Obsidian login failed: ${signin.error}`);
183
221
  retries++;
184
222
  } else if (!signin.token) {
185
223
  throw Error(`Obsidian login failed: ${_nullishCoalesce(signin.error, () => ( "unknown error"))}`);
186
224
  }
187
225
  }
188
- if (!_optionalChain([signin, 'optionalAccess', _13 => _13.token])) {
189
- throw Error(`Obsidian login failed: ${_nullishCoalesce(_optionalChain([signin, 'optionalAccess', _14 => _14.error]), () => ( "unknown error"))}`);
190
- } else if (!_optionalChain([signin, 'optionalAccess', _15 => _15.license])) {
226
+ if (!_optionalChain([signin, 'optionalAccess', _16 => _16.token])) {
227
+ throw Error(`Obsidian login failed: ${_nullishCoalesce(_optionalChain([signin, 'optionalAccess', _17 => _17.error]), () => ( "unknown error"))}`);
228
+ } else if (!_optionalChain([signin, 'optionalAccess', _18 => _18.license])) {
191
229
  throw Error("Obsidian Insiders account is required to download Obsidian beta versions");
192
230
  }
193
231
  if (interactive && savePath && (!_process.env.OBSIDIAN_EMAIL || !_process.env.OBSIDIAN_PASSWORD)) {
@@ -311,7 +349,7 @@ var ChromeLocalStorage = class {
311
349
  var _zlib = require('zlib'); var _zlib2 = _interopRequireDefault(_zlib);
312
350
 
313
351
  function normalizeGitHubRepo(repo) {
314
- return _nullishCoalesce(_optionalChain([repo, 'access', _16 => _16.match, 'call', _17 => _17(/^(https?:\/\/)?(github.com\/)?(.*?)\/?$/), 'optionalAccess', _18 => _18[3]]), () => ( repo));
352
+ return _nullishCoalesce(_optionalChain([repo, 'access', _19 => _19.match, 'call', _20 => _20(/^(https?:\/\/)?(github.com\/)?(.*?)\/?$/), 'optionalAccess', _21 => _21[3]]), () => ( repo));
315
353
  }
316
354
  async function extractGz(archive, dest) {
317
355
  await _promises3.pipeline.call(void 0, _fs2.default.createReadStream(archive), _zlib2.default.createGunzip(), _fs2.default.createWriteStream(dest));
@@ -379,13 +417,9 @@ async function extractObsidianDmg(dmg, dest) {
379
417
  }
380
418
  async function fetchObsidianDesktopReleases(sinceDate, sinceSha) {
381
419
  const repo = "obsidianmd/obsidian-releases";
382
- const github = getGithubClient();
383
- let commitHistory = await github.paginate(github.rest.repos.listCommits, {
384
- owner: "obsidianmd",
385
- repo: "obsidian-releases",
420
+ let commitHistory = await fetchGitHubAPIPaginated(`repos/${repo}/commits`, {
386
421
  path: "desktop-releases.json",
387
- since: sinceDate,
388
- per_page: 100
422
+ since: sinceDate
389
423
  });
390
424
  commitHistory.reverse();
391
425
  if (sinceSha) {
@@ -396,19 +430,13 @@ async function fetchObsidianDesktopReleases(sinceDate, sinceSha) {
396
430
  commitHistory,
397
431
  (commit) => fetch(`https://raw.githubusercontent.com/${repo}/${commit.sha}/desktop-releases.json`).then((r) => r.json())
398
432
  );
399
- const commitDate = _nullishCoalesce(_optionalChain([commitHistory, 'access', _19 => _19.at, 'call', _20 => _20(-1), 'optionalAccess', _21 => _21.commit, 'access', _22 => _22.committer, 'optionalAccess', _23 => _23.date]), () => ( sinceDate));
400
- const commitSha = _nullishCoalesce(_optionalChain([commitHistory, 'access', _24 => _24.at, 'call', _25 => _25(-1), 'optionalAccess', _26 => _26.sha]), () => ( sinceSha));
433
+ const commitDate = _nullishCoalesce(_optionalChain([commitHistory, 'access', _22 => _22.at, 'call', _23 => _23(-1), 'optionalAccess', _24 => _24.commit, 'access', _25 => _25.committer, 'access', _26 => _26.date]), () => ( sinceDate));
434
+ const commitSha = _nullishCoalesce(_optionalChain([commitHistory, 'access', _27 => _27.at, 'call', _28 => _28(-1), 'optionalAccess', _29 => _29.sha]), () => ( sinceSha));
401
435
  return [fileHistory, { commitDate, commitSha }];
402
436
  }
403
437
  async function fetchObsidianGitHubReleases() {
404
- const github = getGithubClient();
405
- let gitHubReleases = await github.paginate(github.rest.repos.listReleases, {
406
- owner: "obsidianmd",
407
- repo: "obsidian-releases",
408
- per_page: 100
409
- });
410
- gitHubReleases = gitHubReleases.reverse();
411
- return gitHubReleases;
438
+ const gitHubReleases = await fetchGitHubAPIPaginated(`repos/obsidianmd/obsidian-releases/releases`);
439
+ return gitHubReleases.reverse();
412
440
  }
413
441
  var BROKEN_ASSETS = [
414
442
  "https://releases.obsidian.md/release/obsidian-0.12.16.asar.gz",
@@ -461,14 +489,14 @@ function parseObsidianGithubRelease(gitHubRelease) {
461
489
  version,
462
490
  gitHubRelease: gitHubRelease.html_url,
463
491
  downloads: {
464
- asar: _optionalChain([asar, 'optionalAccess', _27 => _27.url]),
465
- appImage: _optionalChain([appImage, 'optionalAccess', _28 => _28.url]),
466
- appImageArm: _optionalChain([appImageArm, 'optionalAccess', _29 => _29.url]),
467
- tar: _optionalChain([tar, 'optionalAccess', _30 => _30.url]),
468
- tarArm: _optionalChain([tarArm, 'optionalAccess', _31 => _31.url]),
469
- dmg: _optionalChain([dmg, 'optionalAccess', _32 => _32.url]),
470
- exe: _optionalChain([exe, 'optionalAccess', _33 => _33.url]),
471
- apk: _optionalChain([apk, 'optionalAccess', _34 => _34.url])
492
+ asar: _optionalChain([asar, 'optionalAccess', _30 => _30.url]),
493
+ appImage: _optionalChain([appImage, 'optionalAccess', _31 => _31.url]),
494
+ appImageArm: _optionalChain([appImageArm, 'optionalAccess', _32 => _32.url]),
495
+ tar: _optionalChain([tar, 'optionalAccess', _33 => _33.url]),
496
+ tarArm: _optionalChain([tarArm, 'optionalAccess', _34 => _34.url]),
497
+ dmg: _optionalChain([dmg, 'optionalAccess', _35 => _35.url]),
498
+ exe: _optionalChain([exe, 'optionalAccess', _36 => _36.url]),
499
+ apk: _optionalChain([apk, 'optionalAccess', _37 => _37.url])
472
500
  },
473
501
  installers: {
474
502
  appImage: appImage ? { digest: appImage.digest } : void 0,
@@ -504,8 +532,8 @@ function updateObsidianVersionList(args) {
504
532
  const parsed = parseObsidianGithubRelease(githubRelease);
505
533
  const newVersion = _lodash2.default.merge(_nullishCoalesce(newVersions[parsed.version], () => ( {})), parsed);
506
534
  for (const installerKey of INSTALLER_KEYS) {
507
- const oldDigest = _optionalChain([oldVersions, 'access', _35 => _35[parsed.version], 'optionalAccess', _36 => _36.installers, 'access', _37 => _37[installerKey], 'optionalAccess', _38 => _38.digest]);
508
- const newDigest = _optionalChain([newVersion, 'access', _39 => _39.installers, 'optionalAccess', _40 => _40[installerKey], 'optionalAccess', _41 => _41.digest]);
535
+ const oldDigest = _optionalChain([oldVersions, 'access', _38 => _38[parsed.version], 'optionalAccess', _39 => _39.installers, 'access', _40 => _40[installerKey], 'optionalAccess', _41 => _41.digest]);
536
+ const newDigest = _optionalChain([newVersion, 'access', _42 => _42.installers, 'optionalAccess', _43 => _43[installerKey], 'optionalAccess', _44 => _44.digest]);
509
537
  if (oldDigest && oldDigest != newDigest) {
510
538
  newVersion.installers[installerKey] = { digest: newDigest };
511
539
  }
@@ -590,8 +618,8 @@ async function extractInstallerInfo(installerKey, url) {
590
618
  }
591
619
  function normalizeObsidianVersionInfo(versionInfo) {
592
620
  versionInfo = _lodash2.default.cloneDeep(versionInfo);
593
- versionInfo.electronVersion = _optionalChain([versionInfo, 'access', _42 => _42.installers, 'optionalAccess', _43 => _43.appImage, 'optionalAccess', _44 => _44.electron]);
594
- versionInfo.chromeVersion = _optionalChain([versionInfo, 'access', _45 => _45.installers, 'optionalAccess', _46 => _46.appImage, 'optionalAccess', _47 => _47.chrome]);
621
+ versionInfo.electronVersion = _optionalChain([versionInfo, 'access', _45 => _45.installers, 'optionalAccess', _46 => _46.appImage, 'optionalAccess', _47 => _47.electron]);
622
+ versionInfo.chromeVersion = _optionalChain([versionInfo, 'access', _48 => _48.installers, 'optionalAccess', _49 => _49.appImage, 'optionalAccess', _50 => _50.chrome]);
595
623
  versionInfo.downloads = _nullishCoalesce(versionInfo.downloads, () => ( {}));
596
624
  versionInfo.installers = _nullishCoalesce(versionInfo.installers, () => ( {}));
597
625
  const canonicalForm = {
@@ -663,7 +691,7 @@ var ObsidianLauncher = class {
663
691
  if (!(dest in this.metadataCache)) {
664
692
  let data;
665
693
  let error;
666
- const cacheMtime = await _asyncOptionalChain([(await _promises2.default.stat(dest).catch(() => void 0)), 'optionalAccess', async _48 => _48.mtime]);
694
+ const cacheMtime = await _asyncOptionalChain([(await _promises2.default.stat(dest).catch(() => void 0)), 'optionalAccess', async _51 => _51.mtime]);
667
695
  if (url.startsWith("file:")) {
668
696
  data = JSON.parse(await _promises2.default.readFile(_url.fileURLToPath.call(void 0, url), "utf-8"));
669
697
  }
@@ -809,7 +837,7 @@ var ObsidianLauncher = class {
809
837
  appVersion = versions.filter((v) => !v.isBeta).at(-1).version;
810
838
  } else if (appVersion == "earliest") {
811
839
  const manifest = await this.getRootManifest();
812
- if (!_optionalChain([manifest, 'optionalAccess', _49 => _49.minAppVersion])) {
840
+ if (!_optionalChain([manifest, 'optionalAccess', _52 => _52.minAppVersion])) {
813
841
  throw Error('Unable to resolve Obsidian appVersion "earliest", no manifest.json or minAppVersion found.');
814
842
  }
815
843
  appVersion = manifest.minAppVersion;
@@ -1473,16 +1501,16 @@ var ObsidianLauncher = class {
1473
1501
  async updateVersionList(original, opts = {}) {
1474
1502
  const { maxInstances = 1 } = opts;
1475
1503
  const [destkopReleases, commitInfo] = await fetchObsidianDesktopReleases(
1476
- _optionalChain([original, 'optionalAccess', _50 => _50.metadata, 'access', _51 => _51.commitDate]),
1477
- _optionalChain([original, 'optionalAccess', _52 => _52.metadata, 'access', _53 => _53.commitSha])
1504
+ _optionalChain([original, 'optionalAccess', _53 => _53.metadata, 'access', _54 => _54.commitDate]),
1505
+ _optionalChain([original, 'optionalAccess', _55 => _55.metadata, 'access', _56 => _56.commitSha])
1478
1506
  );
1479
1507
  const gitHubReleases = await fetchObsidianGitHubReleases();
1480
1508
  let newVersions = updateObsidianVersionList({
1481
- original: _optionalChain([original, 'optionalAccess', _54 => _54.versions]),
1509
+ original: _optionalChain([original, 'optionalAccess', _57 => _57.versions]),
1482
1510
  destkopReleases,
1483
1511
  gitHubReleases
1484
1512
  });
1485
- const newInstallers = newVersions.flatMap((v) => INSTALLER_KEYS.map((k) => [v, k])).filter(([v, key]) => _optionalChain([v, 'access', _55 => _55.downloads, 'optionalAccess', _56 => _56[key]]) && !_optionalChain([v, 'access', _57 => _57.installers, 'optionalAccess', _58 => _58[key], 'optionalAccess', _59 => _59.chrome]));
1513
+ const newInstallers = newVersions.flatMap((v) => INSTALLER_KEYS.map((k) => [v, k])).filter(([v, key]) => _optionalChain([v, 'access', _58 => _58.downloads, 'optionalAccess', _59 => _59[key]]) && !_optionalChain([v, 'access', _60 => _60.installers, 'optionalAccess', _61 => _61[key], 'optionalAccess', _62 => _62.chrome]));
1486
1514
  const installerInfos = await pool(maxInstances, newInstallers, async ([v, key]) => {
1487
1515
  const installerInfo = await extractInstallerInfo(key, v.downloads[key]);
1488
1516
  return { version: v.version, key, installerInfo };
@@ -1493,13 +1521,13 @@ var ObsidianLauncher = class {
1493
1521
  schemaVersion: obsidianVersionsSchemaVersion,
1494
1522
  commitDate: commitInfo.commitDate,
1495
1523
  commitSha: commitInfo.commitSha,
1496
- timestamp: _nullishCoalesce(_optionalChain([original, 'optionalAccess', _60 => _60.metadata, 'access', _61 => _61.timestamp]), () => ( ""))
1524
+ timestamp: _nullishCoalesce(_optionalChain([original, 'optionalAccess', _63 => _63.metadata, 'access', _64 => _64.timestamp]), () => ( ""))
1497
1525
  // set down below
1498
1526
  },
1499
1527
  versions: newVersions
1500
1528
  };
1501
1529
  const dayMs = 24 * 60 * 60 * 1e3;
1502
- const timeSinceLastUpdate = (/* @__PURE__ */ new Date()).getTime() - new Date(_nullishCoalesce(_optionalChain([original, 'optionalAccess', _62 => _62.metadata, 'access', _63 => _63.timestamp]), () => ( 0))).getTime();
1530
+ const timeSinceLastUpdate = (/* @__PURE__ */ new Date()).getTime() - new Date(_nullishCoalesce(_optionalChain([original, 'optionalAccess', _65 => _65.metadata, 'access', _66 => _66.timestamp]), () => ( 0))).getTime();
1503
1531
  if (!_lodash2.default.isEqual(original, result) || timeSinceLastUpdate > 29 * dayMs) {
1504
1532
  result.metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
1505
1533
  }
@@ -1545,4 +1573,4 @@ var ObsidianLauncher = class {
1545
1573
 
1546
1574
 
1547
1575
  exports.watchFiles = watchFiles; exports.ObsidianLauncher = ObsidianLauncher;
1548
- //# sourceMappingURL=chunk-SG2CJVPK.cjs.map
1576
+ //# sourceMappingURL=chunk-BLHP5CDO.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/wdio-obsidian-service/wdio-obsidian-service/packages/obsidian-launcher/dist/chunk-BLHP5CDO.cjs","../src/utils.ts","../src/launcher.ts","../src/types.ts","../src/apis.ts","../src/chromeLocalStorage.ts","../src/launcherUtils.ts"],"names":["path","canonical","obj"],"mappings":"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","file":"/home/runner/work/wdio-obsidian-service/wdio-obsidian-service/packages/obsidian-launcher/dist/chunk-BLHP5CDO.cjs","sourcesContent":[null,"import fsAsync from \"fs/promises\"\nimport fs from \"fs\";\nimport path from \"path\"\nimport os from \"os\"\nimport { PromisePool } from '@supercharge/promise-pool'\nimport _ from \"lodash\"\n\n/// Files ///\n\nexport async function fileExists(path: string) {\n try {\n await fsAsync.access(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Create tmpdir under the system temporary directory.\n * @param prefix \n * @returns \n */\nexport async function makeTmpDir(prefix?: string) {\n return fsAsync.mkdtemp(path.join(os.tmpdir(), prefix ?? 'tmp-'));\n}\n\n/**\n * Handles creating a file or folder \"atomically\" by creating a tmpDir, then downloading or otherwise creating the file\n * under it, then renaming it to the final location when done.\n * @param dest Path the file or folder should end up at.\n * @param func Function takes path to a temporary directory it can use as scratch space. The path it returns will be\n * moved to `dest`. If no path is returned, it will move the whole tmpDir to dest.\n * @param opts.preserveTmpDir Don't delete tmpDir on failure. Default false.\n */\nexport async function atomicCreate(\n dest: string, func: (tmpDir: string) => Promise<string|void>,\n opts: {preserveTmpDir?: boolean} = {},\n): Promise<void> {\n dest = path.resolve(dest);\n // mkdir returns first parent created, or undefined if none were created\n const createdParentDir = await fsAsync.mkdir(path.dirname(dest), { recursive: true });\n const tmpDir = await fsAsync.mkdtemp(path.join(path.dirname(dest), `.${path.basename(dest)}.tmp.`));\n let success = false;\n try {\n let result = await func(tmpDir) ?? tmpDir;\n result = path.resolve(tmpDir, result);\n if (!result.startsWith(tmpDir)) {\n throw new Error(`Returned path ${result} not under tmpDir`)\n }\n // rename will overwrite files but not directories\n if (await fileExists(dest) && (await fsAsync.stat(dest)).isDirectory()) {\n await fsAsync.rename(dest, tmpDir + \".old\")\n }\n await fsAsync.rename(result, dest);\n success = true;\n } finally {\n if (success) {\n await fsAsync.rm(tmpDir + \".old\", { recursive: true, force: true });\n await fsAsync.rm(tmpDir, { recursive: true, force: true });\n } else if (!opts.preserveTmpDir) {\n await fsAsync.rm(createdParentDir ?? tmpDir, { recursive: true, force: true });\n }\n }\n}\n\n/**\n * Tries to hardlink a file, falls back to copy if it fails\n */\nexport async function linkOrCp(src: string, dest: string) {\n await fsAsync.rm(dest, {recursive: true, force: true});\n try {\n await fsAsync.link(src, dest);\n } catch {\n await fsAsync.copyFile(src, dest);\n }\n}\n\n\n/// Promises ///\n\nexport async function sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/**\n * Await a promise or reject if it takes longer than timeout.\n */\nexport async function withTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {\n let timer: NodeJS.Timeout;\n const result = Promise.race([\n promise,\n new Promise<T>((resolve, reject) => timer = setTimeout(() => reject(Error(\"Promise timed out\")), timeout))\n ])\n return result.finally(() => clearTimeout(timer));\n}\n\n/**\n * Wrapper around PromisePool that throws on any error.\n */\nexport async function pool<T, U>(size: number, items: T[], func: (item: T) => Promise<U>): Promise<U[]> {\n const { results } = await PromisePool\n .for(items)\n .withConcurrency(size)\n .handleError(async (error) => { throw error; })\n .useCorrespondingResults()\n .process(func);\n return results as U[];\n}\n\nexport type SuccessResult<T> = {success: true, result: T, error: undefined};\nexport type ErrorResult = {success: false, result: undefined, error: any};\nexport type Maybe<T> = SuccessResult<T>|ErrorResult;\n\n/**\n * Helper for handling asynchronous errors with less hassle.\n */\nexport async function maybe<T>(promise: Promise<T>): Promise<Maybe<T>> {\n return promise\n .then(r => ({success: true, result: r, error: undefined} as const))\n .catch(e => ({success: false, result: undefined, error: e} as const));\n}\n\n\n/**\n * Watch a list of files and call func whenever they change.\n */\nexport function watchFiles(\n files: string[],\n func: (curr: fs.Stats, prev: fs.Stats) => void,\n options: { interval: number, persistent: boolean, debounce: number },\n) {\n const debouncedFunc = _.debounce((curr: fs.Stats, prev: fs.Stats) => {\n if (curr.mtimeMs > prev.mtimeMs || (curr.mtimeMs == 0 && prev.mtimeMs != 0)) {\n func(curr, prev)\n }\n }, options.debounce);\n for (const file of files) {\n fs.watchFile(file, {interval: options.interval, persistent: options.persistent}, debouncedFunc);\n }\n}\n\n\nexport type CanonicalForm = {\n [key: string]: CanonicalForm|null,\n};\n\n/**\n * Normalize object key order and remove any undefined values.\n * CanonicalForm is an object with keys in the order you want.\n * - If a value is \"null\" the value under that key won't be changed\n * - if its an object, the value will also be normalized to match that object's key order\n */\nexport function normalizeObject<T>(canonical: CanonicalForm, obj: T): T {\n // might be better to just use zod or something for this\n const rootCanonical = canonical, rootObj = obj;\n function helper(canonical: any, obj: any) {\n if (_.isPlainObject(canonical)) {\n if (_.isPlainObject(obj)) {\n obj = _.pick(obj, Object.keys(canonical))\n obj = _.mapValues(obj, (v, k) => helper(canonical[k], v));\n obj = _.omitBy(obj, v => v === undefined);\n return obj;\n } else {\n return obj;\n }\n } else if (canonical === null) {\n return obj;\n } else {\n throw Error(`Invalid canonical form ${JSON.stringify(rootCanonical)}`);\n }\n }\n return helper(rootCanonical, rootObj);\n}\n","import fsAsync from \"fs/promises\"\nimport path from \"path\"\nimport crypto from \"crypto\";\nimport extractZip from \"extract-zip\"\nimport { downloadArtifact } from '@electron/get';\nimport child_process from \"child_process\"\nimport semver from \"semver\"\nimport { fileURLToPath } from \"url\";\nimport _ from \"lodash\"\nimport dotenv from \"dotenv\";\nimport { fileExists, makeTmpDir, atomicCreate, linkOrCp, maybe, pool } from \"./utils.js\";\nimport {\n ObsidianVersionInfo, ObsidianVersionList, ObsidianInstallerInfo, PluginEntry, DownloadedPluginEntry, ThemeEntry,\n DownloadedThemeEntry, obsidianVersionsSchemaVersion,\n} from \"./types.js\";\nimport { ObsidianAppearanceConfig, ObsidianCommunityPlugin, ObsidianCommunityTheme, PluginManifest } from \"./obsidianTypes.js\";\nimport { obsidianApiLogin, fetchObsidianApi, downloadResponse } from \"./apis.js\";\nimport ChromeLocalStorage from \"./chromeLocalStorage.js\";\nimport {\n normalizeGitHubRepo, extractGz, extractObsidianAppImage, extractObsidianExe, extractObsidianDmg,\n extractInstallerInfo, fetchObsidianDesktopReleases, fetchObsidianGitHubReleases, updateObsidianVersionList,\n INSTALLER_KEYS,\n} from \"./launcherUtils.js\";\n\nconst currentPlatform = {\n platform: process.platform,\n arch: process.arch,\n}\n\ndotenv.config({path: [\".env\"], quiet: true});\n\n/**\n * The `ObsidianLauncher` class.\n * \n * Helper class that handles downloading and installing Obsidian versions, plugins, and themes and launching Obsidian\n * with sandboxed configuration directories.\n */\nexport class ObsidianLauncher {\n readonly cacheDir: string\n\n readonly versionsUrl: string\n readonly communityPluginsUrl: string\n readonly communityThemesUrl: string\n readonly cacheDuration: number\n\n /** Cached metadata files and requests */\n private metadataCache: Record<string, any>\n\n readonly interactive: boolean = false;\n private obsidianApiToken: string|undefined;\n\n /**\n * Construct an ObsidianLauncher.\n * @param opts.cacheDir Path to the cache directory. Defaults to \"OBSIDIAN_CACHE\" env var or \".obsidian-cache\".\n * @param opts.versionsUrl Custom `obsidian-versions.json` url. Can be a file URL.\n * @param opts.communityPluginsUrl Custom `community-plugins.json` url. Can be a file URL.\n * @param opts.communityThemesUrl Custom `community-css-themes.json` url. Can be a file URL.\n * @param opts.cacheDuration If the cached version list is older than this (in ms), refetch it. Defaults to 30 minutes.\n * @param opts.interactive If it can prompt the user for input (e.g. for Obsidian credentials). Default false.\n */\n constructor(opts: {\n cacheDir?: string,\n versionsUrl?: string,\n communityPluginsUrl?: string,\n communityThemesUrl?: string,\n cacheDuration?: number,\n interactive?: boolean,\n } = {}) {\n this.cacheDir = path.resolve(opts.cacheDir ?? process.env.OBSIDIAN_CACHE ?? \"./.obsidian-cache\");\n \n const defaultVersionsUrl = 'https://raw.githubusercontent.com/jesse-r-s-hines/wdio-obsidian-service/HEAD/obsidian-versions.json'\n this.versionsUrl = opts.versionsUrl ?? defaultVersionsUrl;\n \n const defaultCommunityPluginsUrl = \"https://raw.githubusercontent.com/obsidianmd/obsidian-releases/HEAD/community-plugins.json\";\n this.communityPluginsUrl = opts.communityPluginsUrl ?? defaultCommunityPluginsUrl;\n\n const defaultCommunityThemesUrl = \"https://raw.githubusercontent.com/obsidianmd/obsidian-releases/HEAD/community-css-themes.json\";\n this.communityThemesUrl = opts.communityThemesUrl ?? defaultCommunityThemesUrl;\n\n this.cacheDuration = opts.cacheDuration ?? (30 * 60 * 1000);\n this.interactive = opts.interactive ?? false;\n\n this.metadataCache = {};\n }\n\n /**\n * Returns file content fetched from url as JSON. Caches content to dest and uses that cache if its more recent than\n * cacheDuration ms or if there are network errors.\n */\n private async cachedFetch(url: string, dest: string, cacheValid?: (data: any) => boolean): Promise<any> {\n cacheValid = cacheValid ?? (() => true);\n dest = path.join(this.cacheDir, dest);\n\n if (!(dest in this.metadataCache)) {\n let data: any;\n let error: any;\n const cacheMtime = (await fsAsync.stat(dest).catch(() => undefined))?.mtime;\n\n // read file urls directly\n if (url.startsWith(\"file:\")) {\n data = JSON.parse(await fsAsync.readFile(fileURLToPath(url), 'utf-8'));\n }\n // read from cache if its recent and valid\n if (!data && cacheMtime && new Date().getTime() - cacheMtime.getTime() < this.cacheDuration) {\n const parsed = JSON.parse(await fsAsync.readFile(dest, 'utf-8'));\n if (cacheValid(parsed)) {\n data = parsed;\n }\n }\n // otherwise try to fetch the url\n if (!data) {\n const response = await maybe(fetch(url).then(async (r) => {\n if (!r.ok) throw Error(`Fetch ${url} failed with status ${r.status}`);\n const d = await r.text();\n // throw if invalid JSON, but keep original formatting\n if (_.isError(_.attempt(JSON.parse, d))) throw Error(`Failed to parse response from ${url}`);\n return d;\n }));\n if (response.success) {\n await atomicCreate(dest, async (tmpDir) => {\n await fsAsync.writeFile(path.join(tmpDir, 'download.json'), response.result);\n return path.join(tmpDir, 'download.json');\n });\n data = JSON.parse(response.result);\n } else {\n error = response.error;\n }\n }\n // use cache on network error, even if old\n if (!data && (await fileExists(dest))) {\n const parsed = JSON.parse(await fsAsync.readFile(dest, 'utf-8'));\n if (cacheValid(parsed)) {\n console.warn(error)\n console.warn(`Unable to download ${url}, using cached file.`);\n data = parsed;\n }\n }\n if (!data) {\n throw error;\n }\n\n this.metadataCache[dest] = data;\n }\n return this.metadataCache[dest];\n }\n\n /**\n * Get parsed content of the current project's manifest.json\n */\n private async getRootManifest(): Promise<PluginManifest|null> {\n if (!('manifest.json' in this.metadataCache)) {\n const root = path.parse(process.cwd()).root;\n let dir = process.cwd();\n while (dir != root && !(await fileExists(path.join(dir, 'manifest.json')))) {\n dir = path.dirname(dir);\n }\n const manifestPath = path.join(dir, 'manifest.json');\n if (await fileExists(manifestPath)) {\n this.metadataCache['manifest.json'] = JSON.parse(await fsAsync.readFile(manifestPath, 'utf-8'));\n } else {\n this.metadataCache['manifest.json'] = null;\n }\n }\n return this.metadataCache['manifest.json'];\n }\n\n /**\n * Get information about all available Obsidian versions.\n */\n async getVersions(): Promise<ObsidianVersionInfo[]> {\n const isValid = (d: ObsidianVersionList) =>\n semver.satisfies(d.metadata.schemaVersion ?? '1.0.0', `^${obsidianVersionsSchemaVersion}`);\n const versions = await this.cachedFetch(this.versionsUrl, \"obsidian-versions.json\", isValid);\n if (!isValid(versions)) {\n throw new Error(`${this.versionsUrl} format has changed, please update obsidian-launcher and wdio-obsidian-service`)\n }\n return versions.versions;\n }\n\n /**\n * Get information about all available community plugins.\n */\n async getCommunityPlugins(): Promise<ObsidianCommunityPlugin[]> {\n return await this.cachedFetch(this.communityPluginsUrl, \"obsidian-community-plugins.json\");\n }\n\n /**\n * Get information about all available community themes.\n */\n async getCommunityThemes(): Promise<ObsidianCommunityTheme[]> {\n return await this.cachedFetch(this.communityThemesUrl, \"obsidian-community-css-themes.json\");\n }\n\n /**\n * Resolves Obsidian app and installer version strings to absolute versions.\n * @param appVersion specific version or one of\n * - \"latest\": Get the current latest non-beta Obsidian version\n * - \"latest-beta\": Get the current latest beta Obsidian version (or latest is there is no current beta)\n * - \"earliest\": Get the `minAppVersion` set in your `manifest.json`\n * @param installerVersion specific version or one of\n * - \"latest\": Get the latest Obsidian installer compatible with `appVersion`\n * - \"earliest\": Get the oldest Obsidian installer compatible with `appVersion`\n * \n * See also: [Obsidian App vs Installer Versions](../README.md#obsidian-app-vs-installer-versions)\n *\n * @returns [appVersion, installerVersion] with any \"latest\" etc. resolved to specific versions.\n */\n async resolveVersion(appVersion: string, installerVersion = \"latest\"): Promise<[string, string]> {\n const versions = await this.getVersions();\n const appVersionInfo = await this.getVersionInfo(appVersion);\n appVersion = appVersionInfo.version;\n let installerVersionInfo: ObsidianVersionInfo|undefined;\n const { platform, arch } = process;\n\n if (!appVersionInfo.minInstallerVersion || !appVersionInfo.maxInstallerVersion) {\n throw Error(`No installers available for Obsidian ${appVersion}`);\n }\n if (installerVersion == \"latest\") {\n installerVersionInfo = _.findLast(versions, v =>\n semver.lte(v.version, appVersionInfo.version) && !!this.getInstallerKey(v, {platform, arch})\n );\n } else if (installerVersion == \"earliest\") {\n installerVersionInfo = versions.find(v =>\n semver.gte(v.version, appVersionInfo.minInstallerVersion!) && !!this.getInstallerKey(v, {platform, arch})\n );\n } else {\n installerVersion = semver.valid(installerVersion) ?? installerVersion; // normalize\n installerVersionInfo = versions.find(v => v.version == installerVersion);\n }\n if (!installerVersionInfo) {\n if ([\"earliest\", \"latest\"].includes(installerVersion)) {\n throw Error(`No compatible installers available for Obsidian ${appVersion}`);\n } else {\n throw Error(`No Obsidian installer ${installerVersion} found`);\n }\n }\n\n if (\n semver.lt(installerVersionInfo.version, appVersionInfo.minInstallerVersion) ||\n semver.gt(installerVersionInfo.version, appVersionInfo.maxInstallerVersion)\n ) {\n throw Error(\n `App and installer versions incompatible: app ${appVersionInfo.version} is compatible with installer ` +\n `>=${appVersionInfo.minInstallerVersion} <=${appVersionInfo.maxInstallerVersion} but ` +\n `${installerVersionInfo.version} specified`\n )\n }\n\n return [appVersionInfo.version, installerVersionInfo.version];\n }\n\n /**\n * Gets details about an Obsidian version.\n * @param appVersion Obsidian app version\n */\n async getVersionInfo(appVersion: string): Promise<ObsidianVersionInfo> {\n const versions = await this.getVersions();\n if (appVersion == \"latest-beta\") {\n appVersion = versions.at(-1)!.version;\n } else if (appVersion == \"latest\") {\n appVersion = versions.filter(v => !v.isBeta).at(-1)!.version;\n } else if (appVersion == \"earliest\") {\n const manifest = await this.getRootManifest();\n if (!manifest?.minAppVersion) {\n throw Error('Unable to resolve Obsidian appVersion \"earliest\", no manifest.json or minAppVersion found.')\n }\n appVersion = manifest.minAppVersion;\n } else {\n // if invalid match won't be found and we'll throw error below\n appVersion = semver.valid(appVersion) ?? appVersion;\n }\n const versionInfo = versions.find(v => v.version == appVersion);\n if (!versionInfo) {\n throw Error(`No Obsidian app version \"${appVersion}\" found`);\n }\n\n return versionInfo;\n }\n\n /**\n * Parses a string of Obsidian versions into [appVersion, installerVersion] tuples.\n * \n * `versions` should be a space separated list of Obsidian app versions. You can optionally specify the installer\n * version by using \"appVersion/installerVersion\" e.g. `\"1.7.7/1.8.10\"`.\n * \n * Example: \n * ```js\n * launcher.parseVersions(\"1.8.10/1.7.7 latest latest-beta/earliest\")\n * ```\n * \n * See also: [Obsidian App vs Installer Versions](../README.md#obsidian-app-vs-installer-versions)\n * \n * @param versions string to parse\n * @returns [appVersion, installerVersion][] resolved to specific versions.\n */\n async parseVersions(versions: string): Promise<[string, string][]> {\n const parsedVersions = versions.split(/[ ,]/).filter(v => v).map((v) => {\n const [appVersion, installerVersion = 'earliest'] = v.split(\"/\");\n return [appVersion, installerVersion] as [string, string];\n });\n const resolvedVersions: [string, string][] = [];\n for (const [appVersion, installerVersion] of parsedVersions) {\n resolvedVersions.push(await this.resolveVersion(appVersion, installerVersion));\n }\n return _.uniqBy(resolvedVersions, v => v.join('/'));\n }\n\n private getInstallerKey(\n installerVersionInfo: ObsidianVersionInfo,\n opts: {platform?: NodeJS.Platform, arch?: NodeJS.Architecture} = {},\n ): keyof ObsidianVersionInfo['installers']|undefined {\n const {platform, arch} = _.defaults({}, opts, currentPlatform);\n const platformName = `${platform}-${arch}`;\n const key = _.findKey(installerVersionInfo.installers, v => v && v.platforms.includes(platformName));\n return key as keyof ObsidianVersionInfo['installers']|undefined;\n }\n\n /**\n * Gets details about the Obsidian installer for the given platform.\n * @param installerVersion Obsidian installer version\n * @param opts.platform Platform/os (defaults to host platform)\n * @param opts.arch Architecture (defaults to host architecture)\n */\n async getInstallerInfo(\n installerVersion: string,\n opts: {platform?: NodeJS.Platform, arch?: NodeJS.Architecture} = {},\n ): Promise<ObsidianInstallerInfo & {url: string}> {\n const {platform, arch} = _.defaults({}, opts, currentPlatform);\n const versionInfo = await this.getVersionInfo(installerVersion);\n const key = this.getInstallerKey(versionInfo, {platform, arch});\n if (key) {\n return {...versionInfo.installers[key]!, url: versionInfo.downloads[key]!};\n } else {\n throw Error(\n `No Obsidian installer for ${installerVersion} ${platform}-${arch}` +\n (versionInfo.isBeta ? ` (${installerVersion} is a beta version)` : '')\n );\n }\n }\n\n /**\n * Downloads the Obsidian installer for the given version and platform/arch (defaults to host platform/arch).\n * Returns the file path.\n * @param installerVersion Obsidian installer version to download\n * @param opts.platform Platform/os of the installer to download (defaults to host platform)\n * @param opts.arch Architecture of the installer to download (defaults to host architecture)\n */\n async downloadInstaller(\n installerVersion: string,\n opts: {platform?: NodeJS.Platform, arch?: NodeJS.Architecture} = {},\n ): Promise<string> {\n const {platform, arch} = _.defaults({}, opts, currentPlatform);\n const versionInfo = await this.getVersionInfo(installerVersion);\n installerVersion = versionInfo.version;\n const installerInfo = await this.getInstallerInfo(installerVersion, {platform, arch});\n const cacheDir = path.join(this.cacheDir, `obsidian-installer/${platform}-${arch}/Obsidian-${installerVersion}`);\n\n let binaryPath: string;\n let extractor: (installer: string, dest: string) => Promise<void>;\n\n if (platform == \"linux\") {\n binaryPath = path.join(cacheDir, \"obsidian\");\n extractor = (installer, dest) => extractObsidianAppImage(installer, dest);\n } else if (platform == \"win32\") {\n binaryPath = path.join(cacheDir, \"Obsidian.exe\")\n extractor = (installer, dest) => extractObsidianExe(installer, arch, dest);\n } else if (platform == \"darwin\") {\n binaryPath = path.join(cacheDir, \"Contents/MacOS/Obsidian\");\n extractor = (installer, dest) => extractObsidianDmg(installer, dest);\n } else {\n throw Error(`Unsupported platform ${platform}`); // shouldn't happen\n }\n\n if (!(await fileExists(binaryPath))) {\n console.log(`Downloading Obsidian installer v${installerVersion}...`)\n await atomicCreate(cacheDir, async (tmpDir) => {\n const installer = path.join(tmpDir, \"installer\");\n await downloadResponse(await fetch(installerInfo.url), installer);\n const extracted = path.join(tmpDir, \"extracted\");\n await extractor(installer, extracted);\n return extracted;\n });\n }\n\n return binaryPath;\n }\n\n /**\n * Downloads the Obsidian asar for the given version. Returns the file path.\n * \n * To download Obsidian beta versions you'll need to have an Obsidian Insiders account and either set the \n * `OBSIDIAN_EMAIL` and `OBSIDIAN_PASSWORD` env vars (`.env` file is supported) or pre-download the Obsidian beta\n * with `npx obsidian-launcher download app -v latest-beta`\n * \n * @param appVersion Obsidian version to download\n */\n async downloadApp(appVersion: string): Promise<string> {\n const versionInfo = await this.getVersionInfo(appVersion);\n const appUrl = versionInfo.downloads.asar;\n if (!appUrl) {\n throw Error(`No asar found for Obsidian version ${appVersion}`);\n }\n const appPath = path.join(this.cacheDir, 'obsidian-app', `obsidian-${versionInfo.version}.asar`);\n\n if (!(await fileExists(appPath))) {\n console.log(`Downloading Obsidian app v${versionInfo.version} ...`)\n await atomicCreate(appPath, async (tmpDir) => {\n const isInsiders = new URL(appUrl).hostname.endsWith('.obsidian.md');\n let response: Response;\n if (isInsiders) {\n if (!this.obsidianApiToken) {\n this.obsidianApiToken = await obsidianApiLogin({\n interactive: this.interactive,\n savePath: path.join(this.cacheDir, \"obsidian-credentials.env\"),\n });\n }\n response = await fetchObsidianApi(appUrl, {token: this.obsidianApiToken});\n } else {\n response = await fetch(appUrl);\n }\n const archive = path.join(tmpDir, 'app.asar.gz');\n const asar = path.join(tmpDir, 'app.asar')\n await downloadResponse(response, archive);\n await extractGz(archive, asar);\n return asar;\n })\n }\n\n return appPath;\n }\n\n /**\n * Downloads chromedriver for the given Obsidian version.\n * \n * wdio will download chromedriver from the Chrome for Testing API automatically (see\n * https://github.com/GoogleChromeLabs/chrome-for-testing#json-api-endpoints). However, Google has only put\n * chromedriver since v115.0.5763.0 in that API, so wdio can't automatically download older versions of chromedriver\n * for old Electron versions. Here we download chromedriver for older versions ourselves using the @electron/get\n * package which fetches chromedriver from https://github.com/electron/electron/releases.\n * \n * @param installerVersion Obsidian installer version\n */\n async downloadChromedriver(\n installerVersion: string,\n opts: {platform?: NodeJS.Platform, arch?: NodeJS.Architecture} = {},\n ): Promise<string> {\n const {platform, arch} = _.defaults({}, opts, currentPlatform);\n const installerInfo = await this.getInstallerInfo(installerVersion, {platform, arch});\n const cacheDir = path.join(this.cacheDir, `electron-chromedriver/${platform}-${arch}/${installerInfo.electron}`);\n let chromedriverPath: string;\n if (process.platform == \"win32\") {\n chromedriverPath = path.join(cacheDir, `chromedriver.exe`);\n } else {\n chromedriverPath = path.join(cacheDir, `chromedriver`);\n }\n\n if (!(await fileExists(chromedriverPath))) {\n console.log(`Downloading chromedriver for electron ${installerInfo.electron} ...`);\n await atomicCreate(cacheDir, async (tmpDir) => {\n const chromedriverZipPath = await downloadArtifact({\n version: installerInfo.electron,\n artifactName: 'chromedriver',\n cacheRoot: path.join(tmpDir, 'download'),\n });\n const extracted = path.join(tmpDir, \"extracted\");\n await extractZip(chromedriverZipPath, { dir: extracted });\n return extracted;\n })\n }\n return chromedriverPath;\n }\n\n /**\n * Downloads the Obsidian apk.\n */\n async downloadAndroid(version: string): Promise<string> {\n const versionInfo = await this.getVersionInfo(version);\n const apkUrl = versionInfo.downloads.apk;\n if (!apkUrl) {\n throw Error(\n `No apk found for Obsidian version ${version}` +\n (versionInfo.isBeta ? ` (${version} is a beta version)` : '')\n );\n }\n const apkPath = path.join(this.cacheDir, 'obsidian-apk', `obsidian-${versionInfo.version}.apk`);\n\n if (!(await fileExists(apkPath))) {\n console.log(`Downloading Obsidian apk v${versionInfo.version} ...`)\n await atomicCreate(apkPath, async (tmpDir) => {\n const dest = path.join(tmpDir, 'obsidian.apk')\n await downloadResponse(await fetch(apkUrl), dest);\n return dest;\n })\n }\n\n return apkPath;\n }\n\n /** Gets the latest version of a plugin. */\n private async getLatestPluginVersion(repo: string) {\n repo = normalizeGitHubRepo(repo)\n const manifestUrl = `https://raw.githubusercontent.com/${repo}/HEAD/manifest.json`;\n const cacheDest = path.join(\"obsidian-plugins\", repo, \"latest.json\");\n const manifest = await this.cachedFetch(manifestUrl, cacheDest);\n return manifest.version;\n }\n\n /**\n * Downloads a plugin from a GitHub repo to the cache.\n * @param repo Repo\n * @param version Version of the plugin to install or \"latest\"\n * @returns path to the downloaded plugin\n */\n private async downloadGitHubPlugin(repo: string, version = \"latest\"): Promise<string> {\n repo = normalizeGitHubRepo(repo)\n if (version == \"latest\") {\n version = await this.getLatestPluginVersion(repo);\n }\n if (!semver.valid(version)) {\n throw Error(`Invalid version \"${version}\"`);\n }\n version = semver.valid(version)!;\n\n const pluginDir = path.join(this.cacheDir, \"obsidian-plugins\", repo, version);\n if (!(await fileExists(pluginDir))) {\n await atomicCreate(pluginDir, async (tmpDir) => {\n const assetsToDownload = {'manifest.json': true, 'main.js': true, 'styles.css': false};\n await Promise.all(\n Object.entries(assetsToDownload).map(async ([file, required]) => {\n const url = `https://github.com/${repo}/releases/download/${version}/${file}`;\n const response = await fetch(url);\n if (response.ok) {\n await downloadResponse(response, path.join(tmpDir, file));\n } else if (required) {\n throw Error(`No ${file} found for ${repo} version ${version}`)\n }\n })\n )\n return tmpDir;\n });\n }\n\n return pluginDir;\n }\n\n /**\n * Downloads a community plugin to the cache.\n * @param id Id of the plugin\n * @param version Version of the plugin to install, or \"latest\"\n * @returns path to the downloaded plugin\n */\n private async downloadCommunityPlugin(id: string, version = \"latest\"): Promise<string> {\n const communityPlugins = await this.getCommunityPlugins();\n const pluginInfo = communityPlugins.find(p => p.id == id);\n if (!pluginInfo) {\n throw Error(`No plugin with id ${id} found.`);\n }\n return await this.downloadGitHubPlugin(pluginInfo.repo, version);\n }\n\n /**\n * Downloads a list of plugins to the cache and returns a list of {@link DownloadedPluginEntry} with the downloaded\n * paths. Also adds the `id` property to the plugins based on the manifest.\n * \n * You can download plugins from GitHub using `{repo: \"org/repo\"}` and community plugins using `{id: 'plugin-id'}`.\n * Local plugins will just be passed through.\n * \n * @param plugins List of plugins to download.\n */\n async downloadPlugins(plugins: PluginEntry[]): Promise<DownloadedPluginEntry[]> {\n return await Promise.all(\n plugins.map(async (plugin) => {\n if (typeof plugin == \"object\" && \"originalType\" in plugin) {\n return {...plugin as DownloadedPluginEntry}\n }\n let pluginPath: string\n let originalType: \"local\"|\"github\"|\"community\"\n if (typeof plugin == \"string\") {\n pluginPath = path.resolve(plugin);\n originalType = \"local\";\n } else if (\"path\" in plugin) {;\n pluginPath = path.resolve(plugin.path);\n originalType = \"local\";\n } else if (\"repo\" in plugin) {\n pluginPath = await this.downloadGitHubPlugin(plugin.repo, plugin.version);\n originalType = \"github\";\n } else if (\"id\" in plugin) {\n pluginPath = await this.downloadCommunityPlugin(plugin.id, plugin.version);\n originalType = \"community\";\n } else {\n throw Error(\"You must specify one of plugin path, repo, or id\")\n }\n\n const manifestPath = path.join(pluginPath, \"manifest.json\");\n if (!(await fileExists(manifestPath))) {\n throw Error(`No plugin found at ${pluginPath}`)\n }\n let pluginId = (typeof plugin == \"object\" && (\"id\" in plugin)) ? plugin.id : undefined;\n if (!pluginId) {\n pluginId = JSON.parse(await fsAsync.readFile(manifestPath, 'utf8').catch(() => \"{}\")).id;\n if (!pluginId) {\n throw Error(`${manifestPath} malformed.`);\n }\n }\n if (!(await fileExists(path.join(pluginPath, \"main.js\")))) {\n throw Error(`No main.js found under ${pluginPath}`)\n }\n\n let enabled: boolean\n if (typeof plugin == \"string\") {\n enabled = true\n } else {\n enabled = plugin.enabled ?? true;\n }\n return {path: pluginPath, id: pluginId, enabled, originalType}\n })\n );\n }\n\n /**\n * Installs plugins into an Obsidian vault\n * @param vault Path to the vault to install the plugins in\n * @param plugins List plugins to install\n */\n async installPlugins(vault: string, plugins: PluginEntry[]) {\n const downloadedPlugins = await this.downloadPlugins(plugins);\n\n const obsidianDir = path.join(vault, '.obsidian');\n await fsAsync.mkdir(obsidianDir, { recursive: true });\n\n const enabledPluginsPath = path.join(obsidianDir, 'community-plugins.json');\n let originalEnabledPlugins: string[] = [];\n if (await fileExists(enabledPluginsPath)) {\n originalEnabledPlugins = JSON.parse(await fsAsync.readFile(enabledPluginsPath, 'utf-8'));\n }\n let enabledPlugins = [...originalEnabledPlugins];\n\n for (const {path: pluginPath, enabled = true, originalType} of downloadedPlugins) {\n const manifestPath = path.join(pluginPath, 'manifest.json');\n const pluginId = JSON.parse(await fsAsync.readFile(manifestPath, 'utf8').catch(() => \"{}\")).id;\n if (!pluginId) {\n throw Error(`${manifestPath} missing or malformed.`);\n }\n\n const pluginDest = path.join(obsidianDir, 'plugins', pluginId);\n await fsAsync.mkdir(pluginDest, { recursive: true });\n\n const files = {\n \"manifest.json\": true,\n \"main.js\": true,\n \"styles.css\": false,\n }\n for (const [file, required] of Object.entries(files)) {\n if (await fileExists(path.join(pluginPath, file))) {\n await linkOrCp(path.join(pluginPath, file), path.join(pluginDest, file));\n } else if (required) {\n throw Error(`${pluginPath}/${file} missing.`);\n } else {\n await fsAsync.rm(path.join(pluginDest, file), {force: true});\n }\n }\n if (await fileExists(path.join(pluginPath, \"data.json\"))) {\n // don't link data.json since it can be modified. Don't delete it if it already exists.\n await fsAsync.cp(path.join(pluginPath, \"data.json\"), path.join(pluginDest, \"data.json\"));\n }\n\n const pluginAlreadyListed = enabledPlugins.includes(pluginId);\n if (enabled && !pluginAlreadyListed) {\n enabledPlugins.push(pluginId)\n } else if (!enabled && pluginAlreadyListed) {\n enabledPlugins = enabledPlugins.filter(p => p != pluginId);\n }\n\n if (originalType == \"local\") {\n // Add a .hotreload file for the https://github.com/pjeby/hot-reload plugin\n await fsAsync.writeFile(path.join(pluginDest, '.hotreload'), '');\n }\n }\n\n if (!_.isEqual(enabledPlugins, originalEnabledPlugins)) {\n await fsAsync.writeFile(enabledPluginsPath, JSON.stringify(enabledPlugins, undefined, 2));\n }\n }\n\n /** Gets the latest version of a theme. */\n private async getLatestThemeVersion(repo: string) {\n repo = normalizeGitHubRepo(repo)\n const manifestUrl = `https://raw.githubusercontent.com/${repo}/HEAD/manifest.json`;\n const cacheDest = path.join(\"obsidian-themes\", repo, \"latest.json\");\n const manifest = await this.cachedFetch(manifestUrl, cacheDest);\n return manifest.version;\n }\n\n /**\n * Downloads a theme from a GitHub repo to the cache.\n * @param repo Repo\n * @returns path to the downloaded theme\n */\n private async downloadGitHubTheme(repo: string, version = \"latest\"): Promise<string> {\n repo = normalizeGitHubRepo(repo)\n const latest = await this.getLatestThemeVersion(repo);\n if (version == \"latest\") {\n version = latest;\n }\n if (!semver.valid(version)) {\n throw Error(`Invalid version \"${version}\"`);\n }\n version = semver.valid(version)!;\n \n const themeDir = path.join(this.cacheDir, \"obsidian-themes\", repo, version);\n\n if (!(await fileExists(themeDir))) {\n await atomicCreate(themeDir, async (tmpDir) => {\n // Obsidian themes can be downloaded from releases like plugins, but have fallback \"legacy\" behavior\n // that just downloads from repo HEAD directly.\n const assetsToDownload = ['manifest.json', 'theme.css'];\n let baseUrl = `https://github.com/${repo}/releases/download/${version}`;\n if (!(await fetch(`${baseUrl}/manifest.json`)).ok) {\n if (version != latest) {\n throw Error(`No theme version \"${version}\" found`);\n }\n baseUrl = `https://raw.githubusercontent.com/${repo}/HEAD`;\n }\n await Promise.all(\n assetsToDownload.map(async (file) => {\n const url = `${baseUrl}/${file}`;\n const response = await fetch(url);\n if (response.ok) {\n await downloadResponse(response, path.join(tmpDir, file));\n } else {\n throw Error(`No ${file} found for ${repo}`);\n }\n }\n ))\n });\n }\n\n return themeDir;\n }\n\n /**\n * Downloads a community theme to the cache.\n * @param name name of the theme\n * @returns path to the downloaded theme\n */\n private async downloadCommunityTheme(name: string, version = \"latest\"): Promise<string> {\n const communityThemes = await this.getCommunityThemes();\n const themeInfo = communityThemes.find(p => p.name == name);\n if (!themeInfo) {\n throw Error(`No theme with name ${name} found.`);\n }\n return await this.downloadGitHubTheme(themeInfo.repo, version);\n }\n\n /**\n * Downloads a list of themes to the cache and returns a list of {@link DownloadedThemeEntry} with the downloaded\n * paths. Also adds the `name` property to the plugins based on the manifest.\n * \n * You can download themes from GitHub using `{repo: \"org/repo\"}` and community themes using `{name: 'theme-name'}`.\n * Local themes will just be passed through.\n * \n * @param themes List of themes to download\n */\n async downloadThemes(themes: ThemeEntry[]): Promise<DownloadedThemeEntry[]> {\n return await Promise.all(\n themes.map(async (theme) => {\n if (typeof theme == \"object\" && \"originalType\" in theme) {\n return {...theme as DownloadedThemeEntry}\n }\n let themePath: string\n let originalType: \"local\"|\"github\"|\"community\"\n if (typeof theme == \"string\") {\n themePath = path.resolve(theme);\n originalType = \"local\";\n } else if (\"path\" in theme) {;\n themePath = path.resolve(theme.path);\n originalType = \"local\";\n } else if (\"repo\" in theme) {\n themePath = await this.downloadGitHubTheme(theme.repo, theme.version);\n originalType = \"github\";\n } else if (\"name\" in theme) {\n themePath = await this.downloadCommunityTheme(theme.name, theme.version);\n originalType = \"community\";\n } else {\n throw Error(\"You must specify one of theme path, repo, or name\")\n }\n\n const manifestPath = path.join(themePath, \"manifest.json\");\n if (!(await fileExists(manifestPath))) {\n throw Error(`No theme found at ${themePath}`)\n }\n let themeName = (typeof theme == \"object\" && (\"name\" in theme)) ? theme.name : undefined;\n if (!themeName) {\n const manifestPath = path.join(themePath, \"manifest.json\");\n themeName = JSON.parse(await fsAsync.readFile(manifestPath, 'utf8').catch(() => \"{}\")).name;\n if (!themeName) {\n throw Error(`${themePath}/manifest.json malformed.`);\n }\n }\n if (!(await fileExists(path.join(themePath, \"theme.css\")))) {\n throw Error(`No theme.css found under ${themePath}`)\n }\n\n let enabled: boolean\n if (typeof theme == \"string\") {\n enabled = true\n } else {\n enabled = theme.enabled ?? true;\n }\n return {path: themePath, name: themeName, enabled: enabled, originalType};\n })\n );\n }\n\n /**\n * Installs themes into an Obsidian vault\n * @param vault Path to the theme to install the themes in\n * @param themes List of themes to install\n */\n async installThemes(vault: string, themes: ThemeEntry[]) {\n const downloadedThemes = await this.downloadThemes(themes);\n\n const obsidianDir = path.join(vault, '.obsidian');\n await fsAsync.mkdir(obsidianDir, { recursive: true });\n\n let enabledTheme: string|undefined = undefined;\n\n for (const {path: themePath, enabled = true} of downloadedThemes) {\n const manifestPath = path.join(themePath, 'manifest.json');\n const cssPath = path.join(themePath, 'theme.css');\n\n const themeName = JSON.parse(await fsAsync.readFile(manifestPath, 'utf8').catch(() => \"{}\")).name;\n if (!themeName) {\n throw Error(`${manifestPath} missing or malformed.`);\n }\n if (!(await fileExists(cssPath))) {\n throw Error(`${cssPath} missing.`);\n }\n\n const themeDest = path.join(obsidianDir, 'themes', themeName);\n await fsAsync.mkdir(themeDest, { recursive: true });\n\n await linkOrCp(manifestPath, path.join(themeDest, \"manifest.json\"));\n await linkOrCp(cssPath, path.join(themeDest, \"theme.css\"));\n\n if (enabledTheme && enabled) {\n throw Error(\"You can only have one enabled theme.\")\n } else if (enabled) {\n enabledTheme = themeName;\n }\n }\n\n if (themes.length > 0) { // Only update appearance.json if we set the themes\n const appearancePath = path.join(obsidianDir, 'appearance.json');\n let appearance: ObsidianAppearanceConfig = {}\n if (await fileExists(appearancePath)) {\n appearance = JSON.parse(await fsAsync.readFile(appearancePath, 'utf-8'));\n }\n appearance.cssTheme = enabledTheme ?? \"\";\n await fsAsync.writeFile(appearancePath, JSON.stringify(appearance, undefined, 2));\n }\n }\n\n /**\n * Sets up the config dir to use for the `--user-data-dir` in obsidian. Returns the path to the created config dir.\n *\n * @param params.appVersion Obsidian app version\n * @param params.installerVersion Obsidian version string.\n * @param params.appPath Path to the asar file to install. Will download if omitted.\n * @param params.vault Path to the vault to open in Obsidian\n * @param params.localStorage items to add to localStorage. `$vaultId` in the keys will be replaced with the vaultId\n * @param params.chromePreferences Chrome preferences to add to the Preferences file\n */\n async setupConfigDir(params: {\n appVersion: string, installerVersion: string,\n appPath?: string,\n vault?: string,\n localStorage?: Record<string, string>,\n chromePreferences?: Record<string, any>,\n }): Promise<string> {\n const [appVersion, installerVersion] = await this.resolveVersion(params.appVersion, params.installerVersion);\n const configDir = await makeTmpDir('obsidian-launcher-config-');\n const vaultId = crypto.randomBytes(8).toString(\"hex\");\n \n const localStorageData: Record<string, string> = {\n \"most-recently-installed-version\": appVersion, // prevents the changelog page on boot\n [`enable-plugin-${vaultId}`]: \"true\", // Disable \"safe mode\" and enable plugins\n ..._.mapKeys(params.localStorage ?? {}, (v, k) => k.replace('$vaultId', vaultId ?? '')),\n }\n const chromePreferences = _.merge(\n // disables the \"allow pasting\" bit in the dev tools console\n {\"electron\": {\"devtools\": {\"preferences\": {\"disable-self-xss-warning\": \"true\"}}}},\n params.chromePreferences ?? {},\n )\n const obsidianJson: any = {\n updateDisabled: true, // prevents Obsidian trying to auto-update on boot.\n }\n\n if (params.vault !== undefined) {\n if (!await fileExists(params.vault)) {\n throw Error(`Vault path ${params.vault} doesn't exist.`)\n }\n Object.assign(obsidianJson, {\n vaults: {\n [vaultId]: {\n path: path.resolve(params.vault),\n ts: new Date().getTime(),\n open: true,\n },\n },\n });\n }\n\n await fsAsync.writeFile(path.join(configDir, 'obsidian.json'), JSON.stringify(obsidianJson));\n await fsAsync.writeFile(path.join(configDir, 'Preferences'), JSON.stringify(chromePreferences));\n\n let appPath = params.appPath;\n if (!appPath) {\n appPath = await this.downloadApp(appVersion);\n }\n await linkOrCp(appPath, path.join(configDir, path.basename(appPath)));\n\n const localStorage = new ChromeLocalStorage(configDir);\n await localStorage.setItems(\"app://obsidian.md\", localStorageData)\n await localStorage.close();\n\n return configDir;\n }\n\n /**\n * Sets up a vault for Obsidian, installing plugins and themes and optionally copying the vault to a temporary\n * directory first.\n * @param params.vault Path to the vault to open in Obsidian\n * @param params.copy Whether to copy the vault to a tmpdir first. Default false\n * @param params.plugins List of plugins to install in the vault\n * @param params.themes List of themes to install in the vault\n * @returns Path to the copied vault (or just the path to the vault if copy is false)\n */\n async setupVault(params: {\n vault: string,\n copy?: boolean,\n plugins?: PluginEntry[], themes?: ThemeEntry[],\n }): Promise<string> {\n let vault = params.vault;\n if (params.copy) {\n const dest = await makeTmpDir(`${path.basename(vault)}-`);\n await fsAsync.cp(vault, dest, { recursive: true, preserveTimestamps: true });\n vault = dest;\n }\n await this.installPlugins(vault, params.plugins ?? []);\n await this.installThemes(vault, params.themes ?? []);\n\n return vault;\n }\n\n /**\n * Downloads and launches Obsidian with a sandboxed config dir and a specifc vault open. Optionally install plugins\n * and themes first.\n * \n * @param params.appVersion Obsidian app version. Default \"latest\"\n * @param params.installerVersion Obsidian installer version. Default \"latest\"\n * @param params.vault Path to the vault to open in Obsidian\n * @param params.copy Whether to copy the vault to a tmpdir first. Default false\n * @param params.plugins List of plugins to install in the vault\n * @param params.themes List of themes to install in the vault\n * @param params.args CLI args to pass to Obsidian\n * @param params.localStorage items to add to localStorage. `$vaultId` in the keys will be replaced with the vaultId\n * @param params.spawnOptions Options to pass to `spawn`\n * @returns The launched child process and the created tmpdirs\n */\n async launch(params: {\n appVersion?: string, installerVersion?: string,\n copy?: boolean,\n vault?: string,\n plugins?: PluginEntry[], themes?: ThemeEntry[],\n args?: string[],\n localStorage?: Record<string, string>,\n spawnOptions?: child_process.SpawnOptions,\n }): Promise<{proc: child_process.ChildProcess, configDir: string, vault?: string}> {\n const [appVersion, installerVersion] = await this.resolveVersion(\n params.appVersion ?? \"latest\",\n params.installerVersion ?? \"latest\",\n );\n const appPath = await this.downloadApp(appVersion);\n const installerPath = await this.downloadInstaller(installerVersion);\n\n let vault = params.vault;\n if (vault) {\n vault = await this.setupVault({\n vault,\n copy: params.copy ?? false,\n plugins: params.plugins, themes: params.themes,\n })\n }\n\n const configDir = await this.setupConfigDir({\n appVersion, installerVersion, appPath, vault,\n localStorage: params.localStorage,\n });\n\n // Spawn child.\n const proc = child_process.spawn(installerPath, [\n `--user-data-dir=${configDir}`,\n // Workaround for SUID issue on linux. See https://github.com/electron/electron/issues/42510\n ...(process.platform == 'linux' ? [\"--no-sandbox\"] : []),\n ...(params.args ?? []),\n ], {\n ...params.spawnOptions,\n });\n\n return {proc, configDir, vault};\n }\n\n /** \n * Updates the info in obsidian-versions.json. The obsidian-versions.json file is used in other launcher commands\n * and in wdio-obsidian-service to get metadata about Obsidian versions in one place such as minInstallerVersion and\n * the internal Electron version.\n */\n async updateVersionList(\n original?: ObsidianVersionList, opts: { maxInstances?: number } = {},\n ): Promise<ObsidianVersionList> {\n const { maxInstances = 1 } = opts;\n\n const [destkopReleases, commitInfo] = await fetchObsidianDesktopReleases(\n original?.metadata.commitDate, original?.metadata.commitSha,\n );\n const gitHubReleases = await fetchObsidianGitHubReleases();\n let newVersions = updateObsidianVersionList({\n original: original?.versions,\n destkopReleases, gitHubReleases,\n });\n\n // extract installer info\n const newInstallers = newVersions\n .flatMap(v => INSTALLER_KEYS.map(k => [v, k] as const))\n .filter(([v, key]) => v.downloads?.[key] && !v.installers?.[key]?.chrome);\n const installerInfos = await pool(maxInstances, newInstallers, async ([v, key]) => {\n const installerInfo = await extractInstallerInfo(key, v.downloads[key]!);\n return {version: v.version, key, installerInfo};\n });\n\n // update again with the installerInfo\n newVersions = updateObsidianVersionList({original: newVersions, installerInfos});\n\n const result: ObsidianVersionList = {\n metadata: {\n schemaVersion: obsidianVersionsSchemaVersion,\n commitDate: commitInfo.commitDate,\n commitSha: commitInfo.commitSha,\n timestamp: original?.metadata.timestamp ?? \"\", // set down below\n },\n versions: newVersions,\n }\n\n // Update timestamp if anything has changed. Also, GitHub will cancel scheduled workflows if the repository is\n // \"inactive\" for 60 days. So we'll update the timestamp every once in a while even if there are no Obsidian\n // updates to make sure there's commit activity in the repo.\n const dayMs = 24 * 60 * 60 * 1000;\n const timeSinceLastUpdate = new Date().getTime() - new Date(original?.metadata.timestamp ?? 0).getTime();\n if (!_.isEqual(original, result) || timeSinceLastUpdate > 29 * dayMs) {\n result.metadata.timestamp = new Date().toISOString();\n }\n\n return result;\n }\n\n /**\n * Returns true if the Obsidian version is already in the cache.\n * @param type on of \"app\" or \"installer\"\n * @param version Obsidian app/installer version\n */\n async isInCache(type: \"app\"|\"installer\", version: string) {\n version = (await this.getVersionInfo(version)).version;\n\n let dest: string\n if (type == \"app\") {\n dest = `obsidian-app/obsidian-${version}.asar`;\n } else { // type == \"installer\"\n const {platform, arch} = process;\n dest =`obsidian-installer/${platform}-${arch}/Obsidian-${version}`;\n }\n\n return (await fileExists(path.join(this.cacheDir, dest)));\n }\n\n /**\n * Returns true if we either have the credentials to download the version or it's already in cache.\n * This is only relevant for Obsidian beta versions, as they require Obsidian insider credentials to download.\n * @param appVersion Obsidian app version\n */\n async isAvailable(appVersion: string): Promise<boolean> {\n const versionInfo = await this.getVersionInfo(appVersion);\n if (!versionInfo.downloads.asar || !versionInfo.minInstallerVersion) { // check if version has a download\n return false;\n }\n\n if (new URL(versionInfo.downloads.asar).hostname.endsWith('.obsidian.md')) {\n const hasCreds = !!(process.env['OBSIDIAN_EMAIL'] && process.env['OBSIDIAN_PASSWORD']);\n const inCache = await this.isInCache('app', versionInfo.version);\n return (hasCreds || inCache);\n } else {\n return true;\n }\n }\n}\n","export const obsidianVersionsSchemaVersion = '2.0.0';\n\n/**\n * Type of the obsidian-versions.json file.\n * @category Types\n */\nexport type ObsidianVersionList = {\n metadata: {\n schemaVersion: string,\n commitDate: string,\n commitSha: string,\n timestamp: string,\n },\n versions: ObsidianVersionInfo[],\n}\n\n/**\n * Metadata about a specific Obsidian desktop installer.\n * @category Types\n */\nexport type ObsidianInstallerInfo = {\n /**\n * Hash of the file content.\n * For releases from before GitHub started storing digests, this will be set to file \"id\" which also changes if the\n * file is updated.\n */\n digest: string,\n /** Electron version */\n electron: string,\n /** Chrome version */\n chrome: string,\n /** platform-arch combinations supported by the installer */\n platforms: string[],\n}\n\n/**\n * Metadata about a specific Obsidian version, including the min/max compatible installer versions, download urls, and\n * the internal Electron version.\n * @category Types\n */\nexport type ObsidianVersionInfo = {\n version: string,\n minInstallerVersion?: string,\n maxInstallerVersion?: string,\n isBeta: boolean,\n gitHubRelease?: string,\n downloads: {\n asar?: string,\n appImage?: string,\n appImageArm?: string,\n tar?: string,\n tarArm?: string,\n dmg?: string,\n exe?: string,\n apk?: string,\n },\n installers: {\n appImage?: ObsidianInstallerInfo,\n appImageArm?: ObsidianInstallerInfo,\n tar?: ObsidianInstallerInfo,\n tarArm?: ObsidianInstallerInfo,\n dmg?: ObsidianInstallerInfo,\n exe?: ObsidianInstallerInfo,\n },\n /** @deprecated Use installers instead */\n electronVersion?: string,\n /** @deprecated Use installers instead */\n chromeVersion?: string,\n}\n\n\n/** @inline */\ninterface BasePluginEntry {\n /** Set false to install the plugin but start it disabled. Default true. */\n enabled?: boolean,\n}\n/** @inline */\ninterface LocalPluginEntry extends BasePluginEntry {\n /** Path on disk to the plugin to install. */\n path: string,\n}\n/** @inline */\ninterface GitHubPluginEntry extends BasePluginEntry {\n /** Github repo of the plugin to install, e.g. \"some-user/some-plugin\". */\n repo: string,\n /** Version of the plugin to install. Defaults to latest. */\n version?: string,\n}\n/** @inline */\ninterface CommunityPluginEntry extends BasePluginEntry {\n /** Plugin ID to install from Obsidian community plugins. */\n id: string,\n /** Version of the plugin to install. Defaults to latest. */\n version?: string,\n}\n/**\n * A plugin to install. Can be a simple string path to the local plugin to install, or an object.\n * If an object set one of:\n * - `path` to install a local plugin\n * - `repo` to install a plugin from github\n * - `id` to install a community plugin\n * \n * You can also pass `enabled: false` to install the plugin, but start it disabled by default.\n * \n * @category Types\n */\nexport type PluginEntry = string|LocalPluginEntry|GitHubPluginEntry|CommunityPluginEntry\n\n/**\n * PluginEntry plus downloaded path.\n * @category Types\n */\nexport type DownloadedPluginEntry = {\n /** If the plugin is enabled */\n enabled: boolean,\n /** Path on disk to the downloaded plugin. */\n path: string,\n /** Id of the plugin */\n id: string,\n /** Type of the plugin entry before downloading */\n originalType: \"local\"|\"github\"|\"community\",\n}\n\n/** @inline */\ninterface BaseThemeEntry {\n /**\n * Set false to install the theme but not enable it. Defaults to true.\n * Only one theme can be enabled.\n */\n enabled?: boolean,\n}\n/** @inline */\ninterface LocalThemeEntry extends BaseThemeEntry {\n /** Path on disk to the theme to install. */\n path: string,\n}\n/** @inline */\ninterface GitHubThemeEntry extends BaseThemeEntry {\n /** Github repo of the theme to install, e.g. \"some-user/some-theme\". */\n repo: string,\n /** Version of the theme to install. Defaults to latest. */\n version?: string,\n}\n/** @inline */\ninterface CommunityThemeEntry extends BaseThemeEntry {\n /** Theme name to install from Obsidian community themes. */\n name: string,\n /** Version of the theme to install. Defaults to latest. */\n version?: string,\n}\n\n/**\n * A theme to install. Can be a simple string path to the local theme to install, or an object.\n * If an object, set one of:\n * - `path` to install a local theme\n * - `repo` to install a theme from github\n * - `name` to install a community theme\n * \n * You can also pass `enabled: false` to install the theme, but start it disabled by default. You can only have one\n * enabled theme, so if you pass multiple you need to disable all but one.\n * \n * @category Types\n */\nexport type ThemeEntry = string|LocalThemeEntry|GitHubThemeEntry|CommunityThemeEntry\n\n/**\n * ThemeEntry plus downloaded path.\n * @category Types\n */\nexport type DownloadedThemeEntry = {\n /** If the theme is enabled */\n enabled: boolean,\n /** Path on disk to the downloaded theme. */\n path: string,\n /** Name of the theme */\n name: string,\n /** Type of the theme entry before downloading */\n originalType: \"local\"|\"github\"|\"community\",\n}\n","import _ from \"lodash\"\nimport fs from \"fs\";\nimport { finished } from 'stream/promises';\nimport { Readable } from 'stream';\nimport { ReadableStream } from \"stream/web\"\nimport fsAsync from \"fs/promises\";\nimport readlineSync from \"readline-sync\";\nimport dotenv from \"dotenv\";\nimport path from \"path\";\nimport { env } from \"process\";\nimport { sleep } from \"./utils.js\";\n\n/**\n * GitHub API stores pagination information in the \"Link\" header. The header looks like this:\n * ```\n * <https://api.github.com/repositories/1300192/issues?page=2>; rel=\"prev\", <https://api.github.com/repositories/1300192/issues?page=4>; rel=\"next\"\n * ```\n */\nexport function parseLinkHeader(linkHeader: string): Record<string, Record<string, string>> {\n function parseLinkData(linkData: string) {\n return Object.fromEntries(\n linkData.split(\";\").flatMap(x => {\n const partMatch = x.trim().match(/^([^=]+?)\\s*=\\s*\"?([^\"]+)\"?$/);\n return partMatch ? [[partMatch[1], partMatch[2]]] : [];\n })\n )\n }\n\n const linkDatas = linkHeader\n .split(/,\\s*(?=<)/)\n .flatMap(link => {\n const linkMatch = link.trim().match(/^<([^>]*)>(.*)$/);\n if (linkMatch) {\n return [{\n url: linkMatch[1],\n ...parseLinkData(linkMatch[2]),\n } as Record<string, string>];\n } else {\n return [];\n }\n })\n .filter(l => l.rel)\n return Object.fromEntries(linkDatas.map(l => [l.rel, l]));\n};\n\ntype SearchParamsDict = Record<string, string|number|undefined>;\nfunction createURL(url: string, base: string, params: SearchParamsDict = {}) {\n const cleanParams = _(params).pickBy(x => x !== undefined).mapValues(v => String(v)).value();\n const urlObj = new URL(url, base);\n const searchParams = new URLSearchParams({...Object.fromEntries(urlObj.searchParams), ...cleanParams});\n if ([...searchParams].length > 0) {\n urlObj.search = '?' + searchParams;\n }\n return urlObj.toString();\n}\n\n\n/**\n * Fetch from the GitHub API. Uses GITHUB_TOKEN if available. You can access the API without a token, but will hit\n * the usage caps very quickly.\n * \n * Note that I'm not using the Octokit client, as it required `moduleResolution: node16` or higher, and I want to keep\n * support for old plugins and build setups. So I'm only using the Octokit package for types.\n */\nexport async function fetchGitHubAPI(url: string, params: SearchParamsDict = {}) {\n url = createURL(url, \"https://api.github.com\", params)\n const token = env.GITHUB_TOKEN;\n const headers: Record<string, string> = token ? {Authorization: \"Bearer \" + token} : {};\n const response = await fetch(url, { headers });\n if (!response.ok) {\n throw new Error(`GitHub API error: ${await response.text()}`);\n }\n return response;\n}\n\n\n/**\n * Fetch all data from a paginated GitHub API request.\n */\nexport async function fetchGitHubAPIPaginated(url: string, params: SearchParamsDict = {}) {\n const results: any[] = [];\n let next: string|undefined = createURL(url, \"https://api.github.com\", { per_page: 100, ...params });\n while (next) {\n const response = await fetchGitHubAPI(next);\n results.push(...await response.json());\n next = parseLinkHeader(response.headers.get('link') ?? '').next?.url;\n }\n return results;\n}\n\n\n/**\n * Login and returns the token from the Obsidian API.\n * @param opts.interactive if true, we can prompt the user for credentials\n * @param opts.savePath Save/cache Obsidian credentials to this path\n */\nexport async function obsidianApiLogin(opts: {\n interactive?: boolean,\n savePath?: string,\n}): Promise<string> {\n const {interactive = false, savePath} = opts;\n // you can also just use a regular .env file, but we'll prompt to cache credentials for convenience\n // The root .env is loaded elsewhere\n if (savePath) dotenv.config({path: [savePath], quiet: true});\n\n let email = env.OBSIDIAN_EMAIL;\n let password = env.OBSIDIAN_PASSWORD;\n if (!email || !password) {\n if (interactive) {\n console.log(\"Obsidian Insiders account is required to download Obsidian beta versions.\")\n email = email || readlineSync.question(\"Obsidian email: \");\n password = password || readlineSync.question(\"Obsidian password: \", {hideEchoBack: true});\n } else {\n throw Error(\n \"Obsidian Insiders account is required to download Obsidian beta versions. Either set the \" +\n \"OBSIDIAN_EMAIL and OBSIDIAN_PASSWORD env vars (.env file is supported) or pre-download the \" +\n \"Obsidian beta with `npx obsidian-launcher download app -v <version>`\"\n )\n }\n }\n\n let needsMfa = false;\n let retries = 0;\n type SigninResult = {token?: string, error?: string, license?: string};\n let signin: SigninResult|undefined = undefined;\n while (!signin?.token && retries < 3) {\n // exponential backoff with random offset. Always trigger in CI to avoid multiple jobs hitting the API at once\n if (retries > 0 || env.CI) {\n await sleep(2*Math.random() + retries*retries * 2);\n }\n\n let mfa = '';\n if (needsMfa && interactive) {\n mfa = readlineSync.question(\"Obsidian 2FA: \");\n }\n\n signin = await fetch(\"https://api.obsidian.md/user/signin\", {\n method: \"post\",\n headers: {\n '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',\n \"Origin\": \"app://obsidian.md\",\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({email, password, mfa})\n }).then(r => r.json()) as SigninResult;\n\n const error = signin.error?.toLowerCase();\n if (error?.includes(\"2fa\") && !needsMfa) {\n needsMfa = true; // when interactive, continue to next loop\n if (!interactive) {\n throw Error(\n \"Can't login with 2FA in a non-interactive session. To download Obsidian beta versions, either \" +\n \"disable 2FA on your account or pre-download the Obsidian beta with \" +\n \"`npx obsidian-launcher download app -v <version>`\"\n );\n }\n } else if ([\"please wait\", \"try again\"].some(m => error?.includes(m))) {\n console.warn(`Obsidian login failed: ${signin.error}`);\n retries++; // continue to next loop\n } else if (!signin.token) { // fatal error\n throw Error(`Obsidian login failed: ${signin.error ?? 'unknown error'}`);\n }\n }\n\n if (!signin?.token) {\n throw Error(`Obsidian login failed: ${signin?.error ?? 'unknown error'}`);\n } else if (!signin?.license) {\n throw Error(\"Obsidian Insiders account is required to download Obsidian beta versions\");\n }\n\n if (interactive && savePath && (!env.OBSIDIAN_EMAIL || !env.OBSIDIAN_PASSWORD)) {\n const save = readlineSync.question(\"Cache credentails to disk? [y/n]: \");\n if (['y', 'yes'].includes(save.toLowerCase())) {\n // you don't need to escape ' in dotenv, it still reads to the last quote (weird...)\n await fsAsync.writeFile(savePath,\n `OBSIDIAN_EMAIL='${email}'\\n` +\n `OBSIDIAN_PASSWORD='${password}'\\n`\n );\n console.log(`Saved Obsidian credentials to ${path.relative(process.cwd(), savePath)}`);\n }\n }\n\n return signin.token;\n}\n\n/**\n * Fetch from the Obsidian API to download insider versions.\n */\nexport async function fetchObsidianApi(url: string, opts: {token: string}) {\n url = createURL(url, \"https://releases.obsidian.md\");\n const response = await fetch(url, {\n headers: {\n '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',\n \"Origin\": \"app://obsidian.md\",\n 'Authorization': 'Bearer ' + opts.token,\n },\n })\n return response;\n}\n\n/**\n * Downloads a url to disk.\n */\nexport async function downloadResponse(response: Response, dest: string) {\n if (!response.ok) {\n throw Error(`${response.url} failed with ${response.status}`);\n }\n const fileStream = fs.createWriteStream(dest, { flags: 'w' });\n // not sure why I have to cast this\n const fetchStream = Readable.fromWeb(response.body as ReadableStream);\n await finished(fetchStream.pipe(fileStream));\n}\n","import path from \"path\"\nimport { ClassicLevel } from \"classic-level\"\n\n\n/**\n * Class to directly manipulate chrome/electron local storage.\n *\n * Normally you'd just manipulate `localStorage` directly during the webdriver tests. However, there's not a built in\n * way to set up localStorage values *before* the app boots. We need to set `enable-plugins` and some other keys for\n * Obsidian to read during the boot process. This class lets us setup the local storage before launching Obsidian.\n */\nexport default class ChromeLocalStorage {\n private db: ClassicLevel;\n\n /** Pass the path to the user data dir for Chrome/Electron. If it doesn't exist it will be created. */\n constructor(public readonly userDataDir: string) {\n this.db = new ClassicLevel(path.join(userDataDir, 'Local Storage/leveldb/'));\n }\n\n private encodeKey = (domain: string, key: string) => `_${domain}\\u0000\\u0001${key}`;\n private decodeKey = (key: string) => {\n const parts = key.slice(1).split(\"\\u0000\\u0001\");\n return [parts[0], parts.slice(1).join(\"\\u0000\\u0001\")] as [string, string]\n };\n private encodeValue = (value: string) => `\\u0001${value}`;\n private decodeValue = (value: string) => value.slice(1);\n\n /**\n * Get a value from localStorage\n * @param domain Domain the value is under, e.g. \"https://example.com\" or \"app://obsidian.md\"\n * @param key Key to retreive\n */\n async getItem(domain: string, key: string): Promise<string|null> {\n const value = await this.db.get(this.encodeKey(domain, key));\n return (value === undefined) ? null : this.decodeValue(value);\n }\n\n /**\n * Set a value in localStorage\n * @param domain Domain the value is under, e.g. \"https://example.com\" or \"app://obsidian.md\"\n * @param key Key to set\n * @param value Value to set\n */\n async setItem(domain: string, key: string, value: string) {\n await this.db.put(this.encodeKey(domain, key), this.encodeValue(value))\n }\n\n /**\n * Removes a key from localStorage\n * @param domain Domain the values is under, e.g. \"https://example.com\" or \"app://obsidian.md\"\n * @param key key to remove.\n */\n async removeItem(domain: string, key: string) {\n await this.db.del(this.encodeKey(domain, key))\n }\n\n /** Get all items in localStorage as [domain, key, value] tuples */\n async getAllItems(): Promise<[string, string, string][]> {\n const result: [string, string, string][] = []\n for await (const pair of this.db.iterator()) {\n if (pair[0].startsWith(\"_\")) { // ignore the META values\n const [domain, key] = this.decodeKey(pair[0]);\n const value = this.decodeValue(pair[1]);\n result.push([domain, key, value]);\n }\n }\n return result;\n }\n\n /**\n * Write multiple values to localStorage in batch\n * @param domain Domain the values are under, e.g. \"https://example.com\" or \"app://obsidian.md\"\n * @param data key/value mapping to write\n */\n async setItems(domain: string, data: Record<string, string>) {\n await this.db.batch(\n Object.entries(data).map(([key, value]) => ({\n type: \"put\",\n key: this.encodeKey(domain, key),\n value: this.encodeValue(value),\n }))\n )\n }\n\n /**\n * Close the localStorage database.\n */\n async close() {\n await this.db.close();\n }\n}\n","import fsAsync from \"fs/promises\"\nimport fs from \"fs\"\nimport path from \"path\"\nimport child_process from \"child_process\"\nimport semver from \"semver\"\nimport _ from \"lodash\"\nimport { pipeline } from \"stream/promises\";\nimport zlib from \"zlib\"\nimport { fileURLToPath } from \"url\"\nimport { DeepPartial } from \"ts-essentials\";\nimport type { Octokit } from \"octokit\";\nimport { atomicCreate, makeTmpDir, normalizeObject, pool } from \"./utils.js\";\nimport { downloadResponse, fetchGitHubAPIPaginated } from \"./apis.js\"\nimport { ObsidianInstallerInfo, ObsidianVersionInfo } from \"./types.js\";\nimport { ObsidianDesktopRelease } from \"./obsidianTypes.js\"\n\n\nexport function normalizeGitHubRepo(repo: string) {\n return repo.match(/^(https?:\\/\\/)?(github.com\\/)?(.*?)\\/?$/)?.[3] ?? repo;\n}\n\nexport async function extractGz(archive: string, dest: string) {\n await pipeline(fs.createReadStream(archive), zlib.createGunzip(), fs.createWriteStream(dest));\n}\n\n/**\n * Run 7zip.\n * Note there's some weirdness around absolute paths because of the way wasm's filesystem works. The root is mounted\n * under /nodefs, so either use relative paths or prefix paths with /nodefs.\n */\nexport async function sevenZ(args: string[], options?: child_process.SpawnOptions) {\n // run 7z.js script as sub_process (so it doesn't block the main thread)\n const sevenZipScript = path.resolve(fileURLToPath(import.meta.url), '../7z.js');\n const proc = child_process.spawn(process.execPath, [sevenZipScript, ...args], {\n stdio: \"pipe\",\n ...options,\n });\n\n let stdout = \"\", stderr = \"\";\n proc.stdout!.on('data', data => stdout += data);\n proc.stderr!.on('data', data => stderr += data);\n const procExit = new Promise<number>((resolve) => proc.on('close', (code) => resolve(code ?? -1)));\n const exitCode = await procExit;\n\n const result = {stdout, stderr}\n if (exitCode != 0) {\n throw Error(`\"7z ${args.join(' ')}\" failed with ${exitCode}:\\n${stdout}\\n${stderr}`)\n }\n return result;\n}\n\n/**\n * Running AppImage requires libfuse2, extracting the AppImage first avoids that.\n */\nexport async function extractObsidianAppImage(appImage: string, dest: string) {\n // Could also use `--appimage-extract` instead.\n await atomicCreate(dest, async (tmpDir) => {\n await sevenZ([\"x\", \"-o.\", path.relative(tmpDir, appImage)], {cwd: tmpDir});\n return tmpDir;\n })\n}\n\n\n/**\n * Extract the obsidian.tar.gz\n */\nexport async function extractObsidianTar(tar: string, dest: string) {\n await atomicCreate(dest, async (tmpDir) => {\n await extractGz(tar, path.join(tmpDir, \"inflated.tar\"));\n await sevenZ([\"x\", \"-o.\", \"inflated.tar\"], {cwd: tmpDir});\n return (await fsAsync.readdir(tmpDir)).find(p => p.match(\"obsidian-\"))!;\n })\n}\n\n\n/**\n * Obsidian appears to use NSIS to bundle their Window's installers. We want to extract the executable\n * files directly without running the installer. 7zip can extract the raw files from the exe.\n */\nexport async function extractObsidianExe(exe: string, arch: NodeJS.Architecture, dest: string) {\n // The installer contains several `.7z` files with files for different architectures\n let subArchive: string\n if (arch == \"x64\") {\n subArchive = `$PLUGINSDIR/app-64.7z`;\n } else if (arch == \"ia32\") {\n subArchive = `$PLUGINSDIR/app-32.7z`;\n } else if (arch == \"arm64\") {\n subArchive = `$PLUGINSDIR/app-arm64.7z`;\n } else {\n throw Error(`No Obsidian installer found for ${process.platform} ${process.arch}`);\n }\n await atomicCreate(dest, async (tmpDir) => {\n await sevenZ([\"x\", \"-oinstaller\", path.relative(tmpDir, exe), subArchive], {cwd: tmpDir});\n await sevenZ([\"x\", \"-oobsidian\", path.join(\"installer\", subArchive)], {cwd: tmpDir});\n return \"obsidian\";\n })\n}\n\n/**\n * Extract the executable from the Obsidian dmg installer.\n */\nexport async function extractObsidianDmg(dmg: string, dest: string) {\n dest = path.resolve(dest);\n\n await atomicCreate(dest, async (tmpDir) => {\n // Current mac dmg files just have `Obsidian.app`, but on older '-universal' ones it's nested another level.\n await sevenZ([\"x\", \"-o.\", path.relative(tmpDir, dmg), \"*/Obsidian.app\", \"Obsidian.app\"], {cwd: tmpDir});\n const files = await fsAsync.readdir(tmpDir);\n if (files.includes(\"Obsidian.app\")) {\n return \"Obsidian.app\"\n } else {\n return path.join(files[0], \"Obsidian.app\")\n }\n })\n}\n\n// Helpers for use in updateVersionList\n\ntype CommitInfo = {commitDate: string, commitSha: string}\n/**\n * Fetch all versions of obsidianmd/obsidian-releases desktop-releases.json since sinceDate and sinceSha\n */\nexport async function fetchObsidianDesktopReleases(\n sinceDate?: string, sinceSha?: string,\n): Promise<[ObsidianDesktopRelease[], CommitInfo]> {\n // Extract info from desktop-releases.json\n const repo = \"obsidianmd/obsidian-releases\";\n let commitHistory = await fetchGitHubAPIPaginated(`repos/${repo}/commits`, {\n path: \"desktop-releases.json\",\n since: sinceDate,\n });\n commitHistory.reverse(); // sort oldest first\n if (sinceSha) {\n commitHistory = _.takeRightWhile(commitHistory, c => c.sha != sinceSha);\n }\n const fileHistory = await pool(4, commitHistory, commit =>\n fetch(`https://raw.githubusercontent.com/${repo}/${commit.sha}/desktop-releases.json`).then(r => r.json())\n );\n \n const commitDate = commitHistory.at(-1)?.commit.committer.date ?? sinceDate;\n const commitSha = commitHistory.at(-1)?.sha ?? sinceSha;\n\n return [fileHistory, {commitDate, commitSha}]\n}\n\ntype GitHubRelease = ReturnType<Octokit['rest'][\"repos\"][\"listReleases\"]>\n/** Fetches all GitHub release information from obsidianmd/obsidian-releases */\nexport async function fetchObsidianGitHubReleases(): Promise<GitHubRelease[]> {\n const gitHubReleases = await fetchGitHubAPIPaginated(`repos/obsidianmd/obsidian-releases/releases`);\n return gitHubReleases.reverse(); // sort oldest first\n}\n\n/** Obsidian assets that have broken download links */\nconst BROKEN_ASSETS = [\n \"https://releases.obsidian.md/release/obsidian-0.12.16.asar.gz\",\n \"https://github.com/obsidianmd/obsidian-releases/releases/download/v0.12.16/obsidian-0.12.16.asar.gz\",\n \"https://releases.obsidian.md/release/obsidian-1.4.7.asar.gz\",\n \"https://releases.obsidian.md/release/obsidian-1.4.8.asar.gz\",\n];\n\nexport type ParsedDesktopRelease = {current: DeepPartial<ObsidianVersionInfo>, beta?: DeepPartial<ObsidianVersionInfo>}\nexport function parseObsidianDesktopRelease(fileRelease: ObsidianDesktopRelease): ParsedDesktopRelease {\n const parse = (r: ObsidianDesktopRelease, isBeta: boolean): DeepPartial<ObsidianVersionInfo> => {\n const version = r.latestVersion;\n let minInstallerVersion: string|undefined = r.minimumVersion;\n if (minInstallerVersion == \"0.0.0\") {\n minInstallerVersion = undefined;\n // there's some errors in the minInstaller versions listed that we need to correct manually\n } else if (semver.satisfies(version, '>=1.3.0 <=1.3.4')) {\n minInstallerVersion = \"0.14.5\"\n // running Obsidian with installer older than 1.1.9 won't boot with errors about \"ERR_BLOCKED_BY_CLIENT\"\n } else if (semver.gte(version, \"1.5.3\") && semver.lt(minInstallerVersion, \"1.1.9\")) {\n minInstallerVersion = \"1.1.9\"\n }\n\n return {\n version: r.latestVersion,\n minInstallerVersion: minInstallerVersion,\n isBeta: isBeta,\n downloads: {\n asar: BROKEN_ASSETS.includes(r.downloadUrl) ? undefined : r.downloadUrl,\n },\n };\n };\n\n const result: ParsedDesktopRelease = { current: parse(fileRelease, false) };\n if (fileRelease.beta && fileRelease.beta.latestVersion !== fileRelease.latestVersion) {\n result.beta = parse(fileRelease.beta, true);\n }\n return result;\n}\n\nexport function parseObsidianGithubRelease(gitHubRelease: any): DeepPartial<ObsidianVersionInfo> {\n const version = gitHubRelease.name;\n let assets: {url: string, digest: string}[] = gitHubRelease.assets.map((a: any) => ({\n url: a.browser_download_url,\n digest: a.digest ?? `id:${a.id}`,\n }));\n assets = assets.filter(a => !BROKEN_ASSETS.includes(a.url));\n\n const asar = assets.find(a => a.url.match(`${version}.asar.gz$`));\n const appImage = assets.find(a => a.url.match(`${version}.AppImage$`));\n const appImageArm = assets.find(a => a.url.match(`${version}-arm64.AppImage$`));\n const tar = assets.find(a => a.url.match(`${version}.tar.gz$`));\n const tarArm = assets.find(a => a.url.match(`${version}-arm64.tar.gz$`));\n const dmg = assets.find(a => a.url.match(`${version}(-universal)?.dmg$`));\n const exe = assets.find(a => a.url.match(`${version}.exe$`));\n const apk = assets.find(a => a.url.match(`${version}.apk$`));\n\n return {\n version: version,\n gitHubRelease: gitHubRelease.html_url,\n downloads: {\n asar: asar?.url,\n appImage: appImage?.url,\n appImageArm: appImageArm?.url,\n tar: tar?.url,\n tarArm: tarArm?.url,\n dmg: dmg?.url,\n exe: exe?.url,\n apk: apk?.url,\n },\n installers: {\n appImage: appImage ? {digest: appImage.digest} : undefined,\n appImageArm: appImageArm ? {digest: appImageArm.digest} : undefined,\n tar: tar ? {digest: tar.digest} : undefined,\n tarArm: tarArm ? {digest: tarArm.digest} : undefined,\n dmg: dmg ? {digest: dmg.digest} : undefined,\n exe: exe ? {digest: exe.digest} : undefined,\n },\n }\n}\n\ntype InstallerKey = keyof ObsidianVersionInfo['installers'];\nexport const INSTALLER_KEYS: InstallerKey[] = [\n \"appImage\", \"appImageArm\", \"tar\", \"tarArm\", \"dmg\", \"exe\",\n];\n\n/**\n * Updates obsidian version information.\n * Does NOT call add installer electron/chromium information, but does remove any out-of-date installer info.\n */\nexport function updateObsidianVersionList(args: {\n original?: ObsidianVersionInfo[],\n destkopReleases?: ObsidianDesktopRelease[],\n gitHubReleases?: any[],\n installerInfos?: {version: string, key: InstallerKey, installerInfo: Omit<ObsidianInstallerInfo, \"digest\">}[],\n}): ObsidianVersionInfo[] {\n const {original = [], destkopReleases = [], gitHubReleases = [], installerInfos = []} = args;\n const oldVersions = _.keyBy(original, v => v.version);\n const newVersions: _.Dictionary<DeepPartial<ObsidianVersionInfo>> = _.cloneDeep(oldVersions);\n\n for (const destkopRelease of destkopReleases) {\n const {current, beta} = parseObsidianDesktopRelease(destkopRelease);\n if (beta) {\n newVersions[beta.version!] = _.merge(newVersions[beta.version!] ?? {}, beta);\n }\n newVersions[current.version!] = _.merge(newVersions[current.version!] ?? {}, current);\n }\n\n for (const githubRelease of gitHubReleases) {\n // Skip some special \"preleases\"\n if (semver.valid(githubRelease.name) && !semver.prerelease(githubRelease.name)) {\n const parsed = parseObsidianGithubRelease(githubRelease);\n const newVersion = _.merge(newVersions[parsed.version!] ?? {}, parsed);\n // remove out of date installerInfo (the installers can change for a version as happened with 1.8.10)\n for (const installerKey of INSTALLER_KEYS) {\n const oldDigest = oldVersions[parsed.version!]?.installers[installerKey]?.digest;\n const newDigest = newVersion.installers?.[installerKey]?.digest;\n if (oldDigest && oldDigest != newDigest) {\n newVersion.installers![installerKey] = {digest: newDigest}; // wipe electron/chrome versions\n }\n }\n newVersions[parsed.version!] = newVersion;\n }\n }\n\n // populate minInstallerVersion and maxInstallerVersion\n let minInstallerVersion: string|undefined = undefined;\n let maxInstallerVersion: string|undefined = undefined;\n for (const version of Object.keys(newVersions).sort(semver.compare)) {\n if (newVersions[version].downloads!.appImage) {\n maxInstallerVersion = version;\n if (!minInstallerVersion) {\n minInstallerVersion = version;\n }\n }\n newVersions[version] = _.merge({ minInstallerVersion, maxInstallerVersion }, newVersions[version]);\n }\n\n // merge in installerInfos\n for (const installerInfo of installerInfos) {\n newVersions[installerInfo.version] = _.merge(newVersions[installerInfo.version] ?? {}, {\n version: installerInfo.version,\n installers: {[installerInfo.key]: installerInfo.installerInfo},\n });\n }\n\n return Object.values(newVersions)\n .map(normalizeObsidianVersionInfo)\n .sort((a, b) => semver.compare(a.version, b.version));\n}\n\n/**\n * Extract Electron and Chrome versions for an Obsidian version.\n * Takes path to the installer (the whole folder, not just the entrypoint executable).\n */\nexport async function extractInstallerInfo(\n installerKey: keyof ObsidianVersionInfo['installers'], url: string,\n): Promise<Omit<ObsidianInstallerInfo, \"digest\">> {\n const installerName = url.split(\"/\").at(-1)!;\n console.log(`Extrating installer info for ${installerName}...`)\n const tmpDir = await makeTmpDir(\"obsidian-launcher-\");\n try {\n const installerPath = path.join(tmpDir, url.split(\"/\").at(-1)!)\n await downloadResponse(await fetch(url), installerPath);\n const exractedPath = path.join(tmpDir, \"Obsidian\");\n let platforms: string[] = [];\n\n if (installerKey == \"appImage\" || installerKey == \"appImageArm\") {\n await extractObsidianAppImage(installerPath, exractedPath);\n platforms = ['linux-' + (installerKey == \"appImage\" ? 'x64' : 'arm64')];\n } else if (installerKey == \"tar\" || installerKey == \"tarArm\") {\n await extractObsidianTar(installerPath, exractedPath);\n platforms = ['linux-' + (installerKey == \"tar\" ? 'x64' : 'arm64')];\n } else if (installerKey == \"exe\") {\n await extractObsidianExe(installerPath, \"x64\", exractedPath);\n const {stdout} = await sevenZ([\"l\", '-ba', path.relative(tmpDir, installerPath)], {cwd: tmpDir});\n const lines = stdout.trim().split(\"\\n\").map(l => l.trim());\n const files = lines.map(l => l.split(/\\s+/).at(-1)!.replace(/\\\\/g, \"/\"));\n\n if (files.includes('$PLUGINSDIR/app-arm64.7z')) platforms.push(\"win32-arm64\");\n if (files.includes('$PLUGINSDIR/app-32.7z')) platforms.push(\"win32-ia32\");\n if (files.includes('$PLUGINSDIR/app-64.7z')) platforms.push(\"win32-x64\");\n } else if (installerKey == \"dmg\") {\n await extractObsidianDmg(installerPath, exractedPath);\n platforms = ['darwin-arm64', 'darwin-x64'];\n } else {\n throw new Error(`Unknown installer key ${installerKey}`)\n }\n\n // This is horrific but works...\n // We grep the binary for the electron and chrome version strings. The proper way to do this would be to spin up\n // Obsidian and use CDP protocol to extract `process.versions`. However, that requires running Obsidian, and we\n // want to get the versions for all platforms and architectures. So we'd either have to set up some kind of\n // GitHub job matrix to run this on all platform/arch combinations or we can just grep the binary.\n\n const matches: string[] = [];\n const installerFiles = await fsAsync.readdir(exractedPath, {recursive: true, withFileTypes: true});\n for (const file of installerFiles) {\n if (file.isFile() && !file.name.endsWith(\".asar\")) {\n const stream = fs.createReadStream(path.join(file.parentPath, file.name), {encoding: \"utf-8\"});\n let prev = \"\";\n for await (let chunk of stream) {\n const regex = /Chrome\\/\\d+\\.\\d+\\.\\d+\\.\\d+|Electron\\/\\d+\\.\\d+\\.\\d+/g;\n chunk = prev + chunk; // include part of prev in case string gets split across chunks\n matches.push(...[...(prev + chunk).matchAll(regex)].map(m => m[0]))\n prev = chunk.slice(-64);\n }\n }\n }\n\n // get most recent versions\n const versionSortKey = (v: string) => v.split(\".\").map(s => s.padStart(9, '0')).join(\".\");\n const versions = _(matches)\n .map(m => m.split(\"/\"))\n .groupBy(0)\n .mapValues(ms => ms.map(m => m[1]))\n .mapValues(ms => _.sortBy(ms, versionSortKey).at(-1)!)\n .value();\n \n const electron = versions['Electron'];\n const chrome = versions['Chrome'];\n\n if (!electron || !chrome) {\n throw new Error(`Failed to extract Electron and Chrome versions from binary ${installerPath}`);\n }\n\n console.log(`Extracted installer info for ${installerName}`)\n return { electron, chrome, platforms };\n } finally {\n await fsAsync.rm(tmpDir, { recursive: true, force: true });\n }\n}\n\n\n/**\n * Normalize order and remove undefined values.\n */\nexport function normalizeObsidianVersionInfo(versionInfo: DeepPartial<ObsidianVersionInfo>): ObsidianVersionInfo {\n versionInfo = _.cloneDeep(versionInfo);\n // kept for backwards compatibility\n versionInfo.electronVersion = versionInfo.installers?.appImage?.electron;\n versionInfo.chromeVersion = versionInfo.installers?.appImage?.chrome;\n // make sure downloads and installers exist even if empty\n versionInfo.downloads = versionInfo.downloads ?? {};\n versionInfo.installers = versionInfo.installers ?? {};\n\n // normalize order and removed undefined\n const canonicalForm = {\n version: null,\n minInstallerVersion: null,\n maxInstallerVersion: null,\n isBeta: null,\n gitHubRelease: null,\n downloads: {\n asar: null,\n appImage: null,\n appImageArm: null,\n tar: null,\n tarArm: null,\n dmg: null,\n exe: null,\n apk: null,\n },\n installers: {\n appImage: {digest: null, electron: null, chrome: null, platforms: null},\n appImageArm: {digest: null, electron: null, chrome: null, platforms: null},\n tar: {digest: null, electron: null, chrome: null, platforms: null},\n tarArm: {digest: null, electron: null, chrome: null, platforms: null},\n dmg: {digest: null, electron: null, chrome: null, platforms: null},\n exe: {digest: null, electron: null, chrome: null, platforms: null},\n },\n electronVersion: null,\n chromeVersion: null,\n };\n return normalizeObject(canonicalForm, versionInfo) as ObsidianVersionInfo;\n}\n\n"]}