obsidian-launcher 2.1.4 → 2.1.6

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.
@@ -441,6 +441,7 @@ async function extractObsidianDmg(dmg, dest) {
441
441
  } finally {
442
442
  await execFile("hdiutil", ["detach", volume]);
443
443
  }
444
+ await execFile("xattr", ["-cr", scratch]);
444
445
  return scratch;
445
446
  } else {
446
447
  await sevenZ(["x", "-o.", _path2.default.relative(scratch, dmg), "*/Obsidian.app", "Obsidian.app"], { cwd: scratch });
@@ -1600,4 +1601,4 @@ var ObsidianLauncher = class {
1600
1601
 
1601
1602
 
1602
1603
  exports.watchFiles = watchFiles; exports.ObsidianLauncher = ObsidianLauncher;
1603
- //# sourceMappingURL=chunk-YJWPOCL6.cjs.map
1604
+ //# sourceMappingURL=chunk-EPJSKILH.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/wdio-obsidian-service/wdio-obsidian-service/packages/obsidian-launcher/dist/chunk-EPJSKILH.cjs","../src/utils.ts","../src/launcher.ts","../src/types.ts","../src/apis.ts","../src/chromeLocalStorage.ts","../src/launcherUtils.ts"],"names":["path","obj","fileURLToPath","fsAsync","_"],"mappings":"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","file":"/home/runner/work/wdio-obsidian-service/wdio-obsidian-service/packages/obsidian-launcher/dist/chunk-EPJSKILH.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.stat(path);\n return true;\n } catch (e: any) {\n if (e?.code == \"ENOENT\") {\n return false\n }\n throw e\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.\n * \n * It creates a scratch dir which `func` can use as scratch space to download/create the file or folder. Then it will\n * rename the result to dest.\n * \n * Atomicity guarantees/caveats:\n * \n * This guarantees that the dest is either successfully created or not, it will never be corrupted by an error or\n * interruption. There are some caveats when dest already exists however. `fs.rename` will atomically overwrite files,\n * but throw if dest is a folder. So if dest already exists and is a directory there is a small chance of it getting\n * interrupted during the replace. You'll still never end up with a \"partial\" dest, but it could remove the original\n * dest without moving in the new one. If this happens, the original dest will be moved to `[tmpId].old` so you can\n * still recover the original manually if needed.\n * \n * If `replace` is false, this function is thread safe when dest is a folder. But if dest is a file, it is possible for\n * `fs.rename` to silently overwite dest if two processes/threads create it at almost the same time. If `replace` is\n * true, this function is thread safe when dest is a file, but not when its a folder.\n * \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\n * returns will be moved to `dest`. If no path is returned, it will move the whole tmpDir to dest.\n * @param opts.replace If true, overwrite dest if it exists. If false, skip func if dest exists. Default true.\n * @param opts.preserveTmpDir Don't delete tmpDir on failure. Default false.\n */\nexport async function atomicCreate(\n dest: string, func: (scratch: string) => Promise<string|void>,\n opts: {replace?: boolean, preserveTmpDir?: boolean} = {},\n): Promise<void> {\n const {replace = true, preserveTmpDir = false} = opts\n dest = path.resolve(dest);\n const parentDir = path.dirname(dest);\n\n if (!replace && (await fileExists(dest))) return\n\n await fsAsync.mkdir(parentDir, { recursive: true });\n const scratch = await fsAsync.mkdtemp(path.join(parentDir, `.${path.basename(dest)}.tmp.`));\n\n try {\n let result = await func(scratch) ?? scratch;\n result = path.resolve(scratch, result);\n if (!result.startsWith(scratch)) {\n throw new Error(`Returned path ${result} not under scratch`)\n }\n\n if (replace) {\n // rename will overwrite files but not directories\n if ((await fsAsync.stat(dest).catch(() => null))?.isDirectory()) {\n await fsAsync.rename(dest, `${scratch}.old`)\n }\n // Potential race condition here if a folder is immediately recreated\n await fsAsync.rename(result, dest);\n } else {\n if (!(await fileExists(dest))) {\n // Ignore error if folder already exists. However, because rename overwrites files, it\n // is theoretically possible replace dest if it's a file...\n await fsAsync.rename(result, dest)\n .catch(e => { if (e?.code != 'ENOTEMPTY') throw e });\n }\n }\n\n await fsAsync.rm(scratch, { recursive: true, force: true });\n await fsAsync.rm(`${scratch}.old`, { recursive: true, force: true });\n } catch (e: any) {\n if (!preserveTmpDir) {\n await fsAsync.rm(scratch, { recursive: true, force: true });\n }\n throw e\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 it's 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 (scratch) => {\n await fsAsync.writeFile(path.join(scratch, 'download.json'), response.result);\n return path.join(scratch, 'download.json');\n }, {replace: true});\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 if 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 installerDir = 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(installerDir, \"obsidian\");\n extractor = (installer, dest) => extractObsidianAppImage(installer, dest);\n } else if (platform == \"win32\") {\n binaryPath = path.join(installerDir, \"Obsidian.exe\")\n extractor = (installer, dest) => extractObsidianExe(installer, arch, dest);\n } else if (platform == \"darwin\") {\n binaryPath = path.join(installerDir, \"Contents/MacOS/Obsidian\");\n extractor = (installer, dest) => extractObsidianDmg(installer, dest);\n } else {\n throw Error(`Unsupported platform ${platform}`); // shouldn't happen\n }\n\n await atomicCreate(installerDir, async (scratch) => {\n console.log(`Downloading Obsidian installer v${installerVersion}...`)\n const installer = path.join(scratch, \"installer\");\n await downloadResponse(await fetch(installerInfo.url), installer);\n const extracted = path.join(scratch, \"extracted\");\n await extractor(installer, extracted);\n return extracted;\n }, {replace: false});\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 const isInsiders = new URL(appUrl).hostname.endsWith('.obsidian.md');\n if (isInsiders && !this.obsidianApiToken && !(await fileExists(appPath))) {\n // do this here to avoid readline-sync blocking in the middle of atomicCreate\n this.obsidianApiToken = await obsidianApiLogin({\n interactive: this.interactive,\n savePath: path.join(this.cacheDir, \"obsidian-credentials.env\"),\n });\n }\n\n await atomicCreate(appPath, async (scratch) => {\n console.log(`Downloading Obsidian app v${versionInfo.version} ...`)\n let response: Response;\n if (isInsiders) {\n response = await fetchObsidianApi(appUrl, {token: this.obsidianApiToken!});\n } else {\n response = await fetch(appUrl);\n }\n const archive = path.join(scratch, 'app.asar.gz');\n const asar = path.join(scratch, 'app.asar')\n await downloadResponse(response, archive);\n await extractGz(archive, asar);\n return asar;\n }, {replace: false})\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 chromedriverDir = path.join(this.cacheDir, `electron-chromedriver/${platform}-${arch}/${installerInfo.electron}`);\n let chromedriverPath: string;\n if (process.platform == \"win32\") {\n chromedriverPath = path.join(chromedriverDir, `chromedriver.exe`);\n } else {\n chromedriverPath = path.join(chromedriverDir, `chromedriver`);\n }\n\n await atomicCreate(chromedriverDir, async (scratch) => {\n console.log(`Downloading chromedriver for electron ${installerInfo.electron} ...`);\n const chromedriverZipPath = await downloadArtifact({\n version: installerInfo.electron,\n artifactName: 'chromedriver',\n cacheRoot: path.join(scratch, 'download'),\n });\n const extracted = path.join(scratch, \"extracted\");\n await extractZip(chromedriverZipPath, { dir: extracted });\n return extracted;\n }, {replace: false})\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 await atomicCreate(apkPath, async (scratch) => {\n console.log(`Downloading Obsidian apk v${versionInfo.version} ...`)\n const dest = path.join(scratch, 'obsidian.apk')\n await downloadResponse(await fetch(apkUrl), dest);\n return dest;\n }, {replace: false})\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 await atomicCreate(pluginDir, async (scratch) => {\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(scratch, file));\n } else if (required) {\n throw Error(`No ${file} found for ${repo} version ${version}`)\n }\n })\n )\n return scratch;\n }, {replace: false});\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 await atomicCreate(themeDir, async (scratch) => {\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(scratch, file));\n } else {\n throw Error(`No ${file} found for ${repo}`);\n }\n }\n ))\n }, {replace: false});\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 const cached = savePath ? dotenv.parse(await fsAsync.readFile(savePath).catch(() => '')) : {};\n let email = env.OBSIDIAN_EMAIL ?? cached.OBSIDIAN_EMAIL;\n let password = env.OBSIDIAN_PASSWORD ?? cached.OBSIDIAN_PASSWORD;\n let promptedCredentials = false;\n\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 promptedCredentials = 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 type SigninResult = {token?: string, error?: string, license?: string};\n function parseSignin(r: any): SigninResult {\n return {token: r.token ? 'token' : undefined, error: r.error?.toString(), license: r.license}\n }\n\n let needsMfa = false;\n let retries = 0;\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 * 3);\n }\n\n let mfa = '';\n if (needsMfa && interactive) {\n mfa = readlineSync.question(\"Obsidian 2FA: \");\n }\n\n const response = 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());\n signin = parseSignin(response);\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 console.warn(\"Retrying obsidian login...\")\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 (savePath && promptedCredentials) {\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 if (!opts.token) {\n throw Error(\"Obsidian credentials required to download Obsidian beta release\")\n }\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 { promisify } from \"util\";\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 { RestEndpointMethodTypes } from \"@octokit/rest\";\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\"\nconst execFile = promisify(child_process.execFile);\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 (scratch) => {\n await sevenZ([\"x\", \"-o.\", path.relative(scratch, appImage)], {cwd: scratch});\n return scratch;\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 (scratch) => {\n await extractGz(tar, path.join(scratch, \"inflated.tar\"));\n await sevenZ([\"x\", \"-o.\", \"inflated.tar\"], {cwd: scratch});\n return (await fsAsync.readdir(scratch)).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 (scratch) => {\n await sevenZ([\"x\", \"-oinstaller\", path.relative(scratch, exe), subArchive], {cwd: scratch});\n await sevenZ([\"x\", \"-oobsidian\", path.join(\"installer\", subArchive)], {cwd: scratch});\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 (scratch) => {\n if (process.platform == \"darwin\") {\n const proc = await execFile('hdiutil', ['attach', '-nobrowse', '-readonly', dmg]);\n const volume = proc.stdout.match(/\\/Volumes\\/.*$/m)![0];\n // Current mac dmg files just have `Obsidian.app`, but on older '-universal' ones it's nested another level.\n const files = await fsAsync.readdir(volume);\n let obsidianApp = files.includes(\"Obsidian.app\") ? \"Obsidian.app\" : path.join(files[0], \"Obsidian.app\");\n obsidianApp = path.join(volume, obsidianApp);\n try {\n await fsAsync.cp(obsidianApp, scratch, {recursive: true, verbatimSymlinks: true, preserveTimestamps: true});\n } finally {\n await execFile('hdiutil', ['detach', volume]);\n }\n // Clear the `com.apple.quarantine` bit to avoid MacOS bocking the downloaded Obsidian executable \"Obsidian\n // is damaged and can't be opened. This file was downloaded on an unknown date\". See issue #46 and https://ss64.com/mac/xattr.html\n await execFile('xattr', ['-cr', scratch]);\n return scratch;\n } else {\n // we'll use 7zip if you aren't on MacOS so that we can still extract the executable on other platforms\n // (needed for the update-obsidian-versions GitHub workflow)\n await sevenZ([\"x\", \"-o.\", path.relative(scratch, dmg), \"*/Obsidian.app\", \"Obsidian.app\"], {cwd: scratch});\n const files = await fsAsync.readdir(scratch);\n const obsidianApp = files.includes(\"Obsidian.app\") ? \"Obsidian.app\" : path.join(files[0], \"Obsidian.app\");\n return path.join(scratch, obsidianApp);\n }\n });\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// 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\nexport type GitHubRelease = RestEndpointMethodTypes[\"repos\"][\"listReleases\"]['response']['data'][number];\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?: GitHubRelease[],\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(newVersions[version], {\n minInstallerVersion: newVersions[version]?.minInstallerVersion ?? minInstallerVersion,\n maxInstallerVersion, // override maxInstallerVersion if it was already set\n });\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/**\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"]}
@@ -437,6 +437,7 @@ async function extractObsidianDmg(dmg, dest) {
437
437
  } finally {
438
438
  await execFile("hdiutil", ["detach", volume]);
439
439
  }
440
+ await execFile("xattr", ["-cr", scratch]);
440
441
  return scratch;
441
442
  } else {
442
443
  await sevenZ(["x", "-o.", path4.relative(scratch, dmg), "*/Obsidian.app", "Obsidian.app"], { cwd: scratch });
@@ -1596,4 +1597,4 @@ export {
1596
1597
  watchFiles,
1597
1598
  ObsidianLauncher
1598
1599
  };
1599
- //# sourceMappingURL=chunk-RFPT36TQ.js.map
1600
+ //# sourceMappingURL=chunk-O3OTELNK.js.map