this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

style: 🎨 format code with applicable `biome` settings

+194 -456
+13 -13
manifest.json
··· 1 1 { 2 - "id": "obsidian42-brat", 3 - "name": "BRAT", 4 - "version": "1.0.6", 5 - "minAppVersion": "1.7.2", 6 - "description": "Easily install a beta version of a plugin for testing.", 7 - "author": "TfTHacker", 8 - "authorUrl": "https://github.com/TfTHacker/obsidian42-brat", 9 - "helpUrl": "https://tfthacker.com/BRAT", 10 - "isDesktopOnly": false, 11 - "fundingUrl": { 12 - "Visit my site": "https://tfthacker.com" 13 - } 14 - } 2 + "id": "obsidian42-brat", 3 + "name": "BRAT", 4 + "version": "1.0.6", 5 + "minAppVersion": "1.7.2", 6 + "description": "Easily install a beta version of a plugin for testing.", 7 + "author": "TfTHacker", 8 + "authorUrl": "https://github.com/TfTHacker/obsidian42-brat", 9 + "helpUrl": "https://tfthacker.com/BRAT", 10 + "isDesktopOnly": false, 11 + "fundingUrl": { 12 + "Visit my site": "https://tfthacker.com" 13 + } 14 + }
+72 -119
src/features/githubUtils.ts
··· 1 + import { compareVersions } from "compare-versions"; 1 2 import type { PluginManifest } from "obsidian"; 2 3 import { request } from "obsidian"; 3 - import { compareVersions } from 'compare-versions'; 4 4 5 5 export interface ReleaseVersion { 6 - version: string; // The tag name of the release 7 - prerelease: boolean; // Indicates if the release is a pre-release 6 + version: string; // The tag name of the release 7 + prerelease: boolean; // Indicates if the release is a pre-release 8 8 } 9 9 10 10 const GITHUB_RAW_USERCONTENT_PATH = "https://raw.githubusercontent.com/"; 11 11 12 - export const isPrivateRepo = async ( 13 - repository: string, 14 - debugLogging = true, 15 - personalAccessToken = "", 16 - ): Promise<boolean> => { 12 + export const isPrivateRepo = async (repository: string, debugLogging = true, personalAccessToken = ""): Promise<boolean> => { 17 13 const URL = `https://api.github.com/repos/${repository}`; 18 14 try { 19 15 const response = await request({ ··· 34 30 35 31 /** 36 32 * Fetches available release versions from a GitHub repository 37 - * 33 + * 38 34 * @param repository - path to GitHub repository in format USERNAME/repository 39 35 * @returns array of version strings, or null if error 40 36 */ ··· 52 48 }, 53 49 }); 54 50 const data = await JSON.parse(response); 55 - return data.map((release: { tag_name: string, prerelease: boolean }) => ({ 51 + return data.map((release: { tag_name: string; prerelease: boolean }) => ({ 56 52 version: release.tag_name, 57 - prerelease: release.prerelease 53 + prerelease: release.prerelease, 58 54 })); 59 55 } catch (e) { 60 56 if (debugLogging) console.log("error in fetchReleaseVersions", URL, e); ··· 62 58 } 63 59 }; 64 60 65 - 66 61 /** 67 62 * pulls from github a release file by its version number 68 63 * ··· 82 77 try { 83 78 // get the asset based on the asset url in the release 84 79 // We can use this both for private and public repos 85 - const asset = release.assets.find( 86 - (asset: { name: string }) => asset.name === fileName, 87 - ); 80 + const asset = release.assets.find((asset: { name: string }) => asset.name === fileName); 88 81 if (!asset) { 89 82 return null; 90 83 } 91 - 84 + 92 85 const headers: Record<string, string> = { 93 - Accept: "application/octet-stream" 86 + Accept: "application/octet-stream", 94 87 }; 95 88 96 89 // Authenticated requests get a higher rate limit 97 - if (isPrivate && personalAccessToken || personalAccessToken) { 90 + if ((isPrivate && personalAccessToken) || personalAccessToken) { 98 91 headers.Authorization = `Token ${personalAccessToken}`; 99 92 } 100 93 101 94 const download = await request({ 102 95 url: asset.url, 103 - headers 96 + headers, 104 97 }); 105 - return download === "Not Found" || download === `{"error":"Not Found"}` 106 - ? null 107 - : download; 108 - 98 + return download === "Not Found" || download === `{"error":"Not Found"}` ? null : download; 109 99 } catch (error) { 110 - if (debugLogging) 111 - console.log("error in grabReleaseFileFromRepository", URL, error); 100 + if (debugLogging) console.log("error in grabReleaseFileFromRepository", URL, error); 112 101 return null; 113 102 } 114 103 }; 115 - 116 104 117 105 export interface CommunityPlugin { 118 106 id: string; ··· 122 110 repo: string; 123 111 } 124 112 125 - export const grabCommmunityPluginList = async ( 126 - debugLogging = true, 127 - ): Promise<CommunityPlugin[] | null> => { 128 - const pluginListUrl = 129 - "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/HEAD/community-plugins.json"; 113 + export const grabCommmunityPluginList = async (debugLogging = true): Promise<CommunityPlugin[] | null> => { 114 + const pluginListUrl = "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/HEAD/community-plugins.json"; 130 115 try { 131 116 const response = await request({ url: pluginListUrl }); 132 - return response === "404: Not Found" 133 - ? null 134 - : ((await JSON.parse(response)) as CommunityPlugin[]); 117 + return response === "404: Not Found" ? null : ((await JSON.parse(response)) as CommunityPlugin[]); 135 118 } catch (error) { 136 119 if (debugLogging) console.log("error in grabCommmunityPluginList", error); 137 120 return null; ··· 144 127 repo: string; 145 128 } 146 129 147 - export const grabCommmunityThemesList = async ( 148 - debugLogging = true, 149 - ): Promise<CommunityTheme[] | null> => { 150 - const themesUrl = 151 - "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/HEAD/community-css-themes.json"; 130 + export const grabCommmunityThemesList = async (debugLogging = true): Promise<CommunityTheme[] | null> => { 131 + const themesUrl = "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/HEAD/community-css-themes.json"; 152 132 try { 153 133 const response = await request({ url: themesUrl }); 154 - return response === "404: Not Found" 155 - ? null 156 - : ((await JSON.parse(response)) as CommunityTheme[]); 134 + return response === "404: Not Found" ? null : ((await JSON.parse(response)) as CommunityTheme[]); 157 135 } catch (error) { 158 136 if (debugLogging) console.log("error in grabCommmunityThemesList", error); 159 137 return null; ··· 165 143 betaVersion = false, 166 144 debugLogging = false, 167 145 ): Promise<string | null> => { 168 - const themesUrl = `https://raw.githubusercontent.com/${repositoryPath}/HEAD/theme${ 169 - betaVersion ? "-beta" : "" 170 - }.css`; 146 + const themesUrl = `https://raw.githubusercontent.com/${repositoryPath}/HEAD/theme${betaVersion ? "-beta" : ""}.css`; 171 147 try { 172 148 const response = await request({ url: themesUrl }); 173 149 return response === "404: Not Found" ? null : response; ··· 177 153 } 178 154 }; 179 155 180 - export const grabCommmunityThemeManifestFile = async ( 181 - repositoryPath: string, 182 - debugLogging = true, 183 - ): Promise<string | null> => { 156 + export const grabCommmunityThemeManifestFile = async (repositoryPath: string, debugLogging = true): Promise<string | null> => { 184 157 const themesUrl = `https://raw.githubusercontent.com/${repositoryPath}/HEAD/manifest.json`; 185 158 try { 186 159 const response = await request({ url: themesUrl }); 187 160 return response === "404: Not Found" ? null : response; 188 161 } catch (error) { 189 - if (debugLogging) 190 - console.log("error in grabCommmunityThemeManifestFile", error); 162 + if (debugLogging) console.log("error in grabCommmunityThemeManifestFile", error); 191 163 return null; 192 164 } 193 165 }; ··· 204 176 return checksum(str).toString(); 205 177 }; 206 178 207 - export const grabChecksumOfThemeCssFile = async ( 208 - repositoryPath: string, 209 - betaVersion: boolean, 210 - debugLogging: boolean, 211 - ): Promise<string> => { 212 - const themeCss = await grabCommmunityThemeCssFile( 213 - repositoryPath, 214 - betaVersion, 215 - debugLogging, 216 - ); 179 + export const grabChecksumOfThemeCssFile = async (repositoryPath: string, betaVersion: boolean, debugLogging: boolean): Promise<string> => { 180 + const themeCss = await grabCommmunityThemeCssFile(repositoryPath, betaVersion, debugLogging); 217 181 return themeCss ? checksumForString(themeCss) : "0"; 218 182 }; 219 183 ··· 233 197 const url = `https://api.github.com/repos/${repositoryPath}/commits?path=${path}&page=1&per_page=1`; 234 198 try { 235 199 const response = await request({ url: url }); 236 - return response === "404: Not Found" 237 - ? null 238 - : (JSON.parse(response) as CommitInfo[]); 200 + return response === "404: Not Found" ? null : (JSON.parse(response) as CommitInfo[]); 239 201 } catch (error) { 240 202 if (debugLogging) console.log("error in grabLastCommitInfoForAFile", error); 241 203 return null; 242 204 } 243 205 }; 244 206 245 - export const grabLastCommitDateForFile = async ( 246 - repositoryPath: string, 247 - path: string, 248 - ): Promise<string> => { 249 - const test: CommitInfo[] | null = await grabLastCommitInfoForFile( 250 - repositoryPath, 251 - path, 252 - ); 207 + export const grabLastCommitDateForFile = async (repositoryPath: string, path: string): Promise<string> => { 208 + const test: CommitInfo[] | null = await grabLastCommitInfoForFile(repositoryPath, path); 253 209 if (test && test.length > 0 && test[0].commit.committer?.date) { 254 210 return test[0].commit.committer.date; 255 211 } ··· 257 213 }; 258 214 259 215 export type Release = { 260 - url: string; 261 - tag_name: string; 262 - prerelease: boolean; 263 - assets: { 264 - name: string; 265 - url: string; 266 - browser_download_url: string; 267 - }[]; 216 + url: string; 217 + tag_name: string; 218 + prerelease: boolean; 219 + assets: { 220 + name: string; 221 + url: string; 222 + browser_download_url: string; 223 + }[]; 268 224 }; 269 225 270 226 /** 271 227 * Gets either a specific release or the latest release from a GitHub repository 272 - * 228 + * 273 229 * @param repositoryPath - Repository path in format username/repository 274 230 * @param version - Optional version/tag to fetch. If not provided, fetches latest release 275 231 * @param includePrereleases - Whether to include pre-releases in the results (default: false) ··· 277 233 * @param isPrivate - Whether the repository is private (default: false) 278 234 * @param personalAccessToken - GitHub personal access token for private repos 279 235 * @returns Promise<Release | null> Release information or null if not found/error 280 - * 236 + * 281 237 * @example 282 238 * // Get latest release 283 239 * const release = await grabReleaseFromRepository('username/repo'); 284 - * 240 + * 285 241 * // Get specific version 286 242 * const release = await grabReleaseFromRepository('username/repo', '1.0.0'); 287 - * 243 + * 288 244 * // Include pre-releases 289 245 * const beta = await grabReleaseFromRepository('username/repo', undefined, true); 290 - * 246 + * 291 247 * // Access private repository 292 248 * const private = await grabReleaseFromRepository('username/repo', undefined, false, false, true, 'token'); 293 249 */ 294 250 export const grabReleaseFromRepository = async ( 295 - repositoryPath: string, 296 - version?: string, 297 - includePrereleases = false, 298 - debugLogging = false, 251 + repositoryPath: string, 252 + version?: string, 253 + includePrereleases = false, 254 + debugLogging = false, 299 255 isPrivate = false, 300 - personalAccessToken?: string 256 + personalAccessToken?: string, 301 257 ): Promise<Release | null> => { 302 - try { 303 - const apiUrl = version 304 - ? `https://api.github.com/repos/${repositoryPath}/releases/tags/${version}` 305 - : `https://api.github.com/repos/${repositoryPath}/releases`; 258 + try { 259 + const apiUrl = version 260 + ? `https://api.github.com/repos/${repositoryPath}/releases/tags/${version}` 261 + : `https://api.github.com/repos/${repositoryPath}/releases`; 306 262 307 - const headers: Record<string, string> = { 308 - 'Accept': 'application/vnd.github.v3+json' 309 - }; 263 + const headers: Record<string, string> = { 264 + Accept: "application/vnd.github.v3+json", 265 + }; 310 266 311 267 // Authenticated requests get a higher rate limit 312 - if (isPrivate && personalAccessToken || personalAccessToken) { 313 - headers.Authorization = `Token ${personalAccessToken}`; 314 - } 268 + if ((isPrivate && personalAccessToken) || personalAccessToken) { 269 + headers.Authorization = `Token ${personalAccessToken}`; 270 + } 315 271 316 - const response = await request({ url: apiUrl, headers }); 272 + const response = await request({ url: apiUrl, headers }); 317 273 318 - if (response === "404: Not Found") return null; 274 + if (response === "404: Not Found") return null; 319 275 320 - const releases: Release[] = version 321 - ? [JSON.parse(response)] 322 - : JSON.parse(response); 276 + const releases: Release[] = version ? [JSON.parse(response)] : JSON.parse(response); 323 277 324 - if (debugLogging) { 325 - console.log(`grabReleaseFromRepository for ${repositoryPath}:`, releases); 326 - } 278 + if (debugLogging) { 279 + console.log(`grabReleaseFromRepository for ${repositoryPath}:`, releases); 280 + } 327 281 328 - 329 - return releases 330 - .sort((a, b) => compareVersions(b.tag_name, a.tag_name)) 331 - .filter(release => includePrereleases || !release.prerelease)[0] ?? null; 332 - 333 - } catch (error) { 334 - if (debugLogging) { 335 - console.log(`Error in grabReleaseFromRepository for ${repositoryPath}:`, error); 336 - } 337 - return null; 338 - } 282 + return ( 283 + releases.sort((a, b) => compareVersions(b.tag_name, a.tag_name)).filter((release) => includePrereleases || !release.prerelease)[0] ?? 284 + null 285 + ); 286 + } catch (error) { 287 + if (debugLogging) { 288 + console.log(`Error in grabReleaseFromRepository for ${repositoryPath}:`, error); 289 + } 290 + return null; 291 + } 339 292 };
+20 -82
src/features/themes.ts
··· 1 1 import { Notice, normalizePath } from "obsidian"; 2 2 import type { ThemeManifest } from "obsidian-typings"; 3 3 import type BratPlugin from "../main"; 4 - import { 5 - addBetaThemeToList, 6 - updateBetaThemeLastUpdateChecksum, 7 - } from "../settings"; 4 + import { addBetaThemeToList, updateBetaThemeLastUpdateChecksum } from "../settings"; 8 5 import { isConnectedToInternet } from "../utils/internetconnection"; 9 6 import { toastMessage } from "../utils/notifications"; 10 - import { 11 - checksumForString, 12 - grabChecksumOfThemeCssFile, 13 - grabCommmunityThemeCssFile, 14 - grabCommmunityThemeManifestFile, 15 - } from "./githubUtils"; 7 + import { checksumForString, grabChecksumOfThemeCssFile, grabCommmunityThemeCssFile, grabCommmunityThemeManifestFile } from "./githubUtils"; 16 8 17 9 /** 18 10 * Installs or updates a theme ··· 23 15 * 24 16 * @returns true for succcess 25 17 */ 26 - export const themeSave = async ( 27 - plugin: BratPlugin, 28 - cssGithubRepository: string, 29 - newInstall: boolean, 30 - ): Promise<boolean> => { 18 + export const themeSave = async (plugin: BratPlugin, cssGithubRepository: string, newInstall: boolean): Promise<boolean> => { 31 19 // test for themes-beta.css 32 - let themeCss = await grabCommmunityThemeCssFile( 33 - cssGithubRepository, 34 - true, 35 - plugin.settings.debuggingMode, 36 - ); 20 + let themeCss = await grabCommmunityThemeCssFile(cssGithubRepository, true, plugin.settings.debuggingMode); 37 21 // grabe themes.css if no beta 38 - if (!themeCss) 39 - themeCss = await grabCommmunityThemeCssFile( 40 - cssGithubRepository, 41 - false, 42 - plugin.settings.debuggingMode, 43 - ); 22 + if (!themeCss) themeCss = await grabCommmunityThemeCssFile(cssGithubRepository, false, plugin.settings.debuggingMode); 44 23 45 24 if (!themeCss) { 46 25 toastMessage( ··· 50 29 return false; 51 30 } 52 31 53 - const themeManifest = await grabCommmunityThemeManifestFile( 54 - cssGithubRepository, 55 - plugin.settings.debuggingMode, 56 - ); 32 + const themeManifest = await grabCommmunityThemeManifestFile(cssGithubRepository, plugin.settings.debuggingMode); 57 33 if (!themeManifest) { 58 - toastMessage( 59 - plugin, 60 - "There is no manifest.json file in the root path of this repository, so theme cannot be installed.", 61 - ); 34 + toastMessage(plugin, "There is no manifest.json file in the root path of this repository, so theme cannot be installed."); 62 35 return false; 63 36 } 64 37 65 38 const manifestInfo = (await JSON.parse(themeManifest)) as ThemeManifest; 66 39 67 - const themeTargetFolderPath = normalizePath( 68 - themesRootPath(plugin) + manifestInfo.name, 69 - ); 40 + const themeTargetFolderPath = normalizePath(themesRootPath(plugin) + manifestInfo.name); 70 41 71 42 const { adapter } = plugin.app.vault; 72 - if (!(await adapter.exists(themeTargetFolderPath))) 73 - await adapter.mkdir(themeTargetFolderPath); 43 + if (!(await adapter.exists(themeTargetFolderPath))) await adapter.mkdir(themeTargetFolderPath); 74 44 75 - await adapter.write( 76 - normalizePath(`${themeTargetFolderPath}/theme.css`), 77 - themeCss, 78 - ); 79 - await adapter.write( 80 - normalizePath(`${themeTargetFolderPath}/manifest.json`), 81 - themeManifest, 82 - ); 45 + await adapter.write(normalizePath(`${themeTargetFolderPath}/theme.css`), themeCss); 46 + await adapter.write(normalizePath(`${themeTargetFolderPath}/manifest.json`), themeManifest); 83 47 84 - updateBetaThemeLastUpdateChecksum( 85 - plugin, 86 - cssGithubRepository, 87 - checksumForString(themeCss), 88 - ); 48 + updateBetaThemeLastUpdateChecksum(plugin, cssGithubRepository, checksumForString(themeCss)); 89 49 90 50 let msg = ""; 91 51 ··· 99 59 msg = `${manifestInfo.name} theme updated from ${cssGithubRepository}.`; 100 60 } 101 61 102 - void plugin.log( 103 - `${msg}[Theme Info](https://github.com/${cssGithubRepository})`, 104 - false, 105 - ); 62 + void plugin.log(`${msg}[Theme Info](https://github.com/${cssGithubRepository})`, false); 106 63 toastMessage(plugin, msg, 20, (): void => { 107 64 window.open(`https://github.com/${cssGithubRepository}`); 108 65 }); ··· 116 73 * @param showInfo - provide notices during the update proces 117 74 * 118 75 */ 119 - export const themesCheckAndUpdates = async ( 120 - plugin: BratPlugin, 121 - showInfo: boolean, 122 - ): Promise<void> => { 76 + export const themesCheckAndUpdates = async (plugin: BratPlugin, showInfo: boolean): Promise<void> => { 123 77 if (!(await isConnectedToInternet())) { 124 78 console.log("BRAT: No internet detected."); 125 79 return; ··· 127 81 let newNotice: Notice | undefined; 128 82 const msg1 = "Checking for beta theme updates STARTED"; 129 83 await plugin.log(msg1, true); 130 - if (showInfo && plugin.settings.notificationsEnabled) 131 - newNotice = new Notice(`BRAT\n${msg1}`, 30000); 84 + if (showInfo && plugin.settings.notificationsEnabled) newNotice = new Notice(`BRAT\n${msg1}`, 30000); 132 85 for (const t of plugin.settings.themesList) { 133 86 // first test to see if theme-beta.css exists 134 - let lastUpdateOnline = await grabChecksumOfThemeCssFile( 135 - t.repo, 136 - true, 137 - plugin.settings.debuggingMode, 138 - ); 87 + let lastUpdateOnline = await grabChecksumOfThemeCssFile(t.repo, true, plugin.settings.debuggingMode); 139 88 // if theme-beta.css does NOT exist, try to get theme.css 140 - if (lastUpdateOnline === "0") 141 - lastUpdateOnline = await grabChecksumOfThemeCssFile( 142 - t.repo, 143 - false, 144 - plugin.settings.debuggingMode, 145 - ); 89 + if (lastUpdateOnline === "0") lastUpdateOnline = await grabChecksumOfThemeCssFile(t.repo, false, plugin.settings.debuggingMode); 146 90 console.log("BRAT: lastUpdateOnline", lastUpdateOnline); 147 - if (lastUpdateOnline !== t.lastUpdate) 148 - await themeSave(plugin, t.repo, false); 91 + if (lastUpdateOnline !== t.lastUpdate) await themeSave(plugin, t.repo, false); 149 92 } 150 93 const msg2 = "Checking for beta theme updates COMPLETED"; 151 94 (async (): Promise<void> => { ··· 164 107 * @param cssGithubRepository - Repository path 165 108 * 166 109 */ 167 - export const themeDelete = ( 168 - plugin: BratPlugin, 169 - cssGithubRepository: string, 170 - ): void => { 171 - plugin.settings.themesList = plugin.settings.themesList.filter( 172 - (t) => t.repo !== cssGithubRepository, 173 - ); 110 + export const themeDelete = (plugin: BratPlugin, cssGithubRepository: string): void => { 111 + plugin.settings.themesList = plugin.settings.themesList.filter((t) => t.repo !== cssGithubRepository); 174 112 void plugin.saveSettings(); 175 113 const msg = `Removed ${cssGithubRepository} from BRAT themes list and will no longer be updated. However, the theme files still exist in the vault. To remove them, go into Settings > Appearance and remove the theme.`; 176 114 void plugin.log(msg, true);
+7 -32
src/settings.ts
··· 49 49 * @param repositoryPath - path to the GitHub repository 50 50 * @param specifyVersion - if the plugin needs to stay at the frozen version, we need to also record the version 51 51 */ 52 - export function addBetaPluginToList( 53 - plugin: BratPlugin, 54 - repositoryPath: string, 55 - specifyVersion = "", 56 - ): void { 52 + export function addBetaPluginToList(plugin: BratPlugin, repositoryPath: string, specifyVersion = ""): void { 57 53 let save = false; 58 54 if (!plugin.settings.pluginList.contains(repositoryPath)) { 59 55 plugin.settings.pluginList.unshift(repositoryPath); 60 56 save = true; 61 57 } 62 - if ( 63 - specifyVersion !== "" && 64 - plugin.settings.pluginSubListFrozenVersion.filter( 65 - (x) => x.repo === repositoryPath, 66 - ).length === 0 67 - ) { 58 + if (specifyVersion !== "" && plugin.settings.pluginSubListFrozenVersion.filter((x) => x.repo === repositoryPath).length === 0) { 68 59 plugin.settings.pluginSubListFrozenVersion.unshift({ 69 60 repo: repositoryPath, 70 61 version: specifyVersion, ··· 83 74 * @param repositoryPath - path to the GitHub repository 84 75 * 85 76 */ 86 - export function existBetaPluginInList( 87 - plugin: BratPlugin, 88 - repositoryPath: string, 89 - ): boolean { 77 + export function existBetaPluginInList(plugin: BratPlugin, repositoryPath: string): boolean { 90 78 return plugin.settings.pluginList.contains(repositoryPath); 91 79 } 92 80 ··· 98 86 * @param themeCss - raw text of the theme. It is checksummed and this is used for tracking if changes occurred to the theme 99 87 * 100 88 */ 101 - export function addBetaThemeToList( 102 - plugin: BratPlugin, 103 - repositoryPath: string, 104 - themeCss: string, 105 - ): void { 89 + export function addBetaThemeToList(plugin: BratPlugin, repositoryPath: string, themeCss: string): void { 106 90 const newTheme: ThemeInforamtion = { 107 91 repo: repositoryPath, 108 92 lastUpdate: checksumForString(themeCss), ··· 118 102 * @param repositoryPath - path to the GitHub repository 119 103 * 120 104 */ 121 - export function existBetaThemeinInList( 122 - plugin: BratPlugin, 123 - repositoryPath: string, 124 - ): boolean { 125 - const testIfThemExists = plugin.settings.themesList.find( 126 - (t) => t.repo === repositoryPath, 127 - ); 105 + export function existBetaThemeinInList(plugin: BratPlugin, repositoryPath: string): boolean { 106 + const testIfThemExists = plugin.settings.themesList.find((t) => t.repo === repositoryPath); 128 107 return !!testIfThemExists; 129 108 } 130 109 ··· 136 115 * @param checksum - checksum of file. In past we used the date of file update, but this proved to not be consisent with the GitHub cache. 137 116 * 138 117 */ 139 - export function updateBetaThemeLastUpdateChecksum( 140 - plugin: BratPlugin, 141 - repositoryPath: string, 142 - checksum: string, 143 - ): void { 118 + export function updateBetaThemeLastUpdateChecksum(plugin: BratPlugin, repositoryPath: string, checksum: string): void { 144 119 for (const t of plugin.settings.themesList) { 145 120 if (t.repo === repositoryPath) { 146 121 t.lastUpdate = checksum;
+6 -15
src/ui/AddNewTheme.ts
··· 24 24 if (this.address === "") return; 25 25 const scrubbedAddress = this.address.replace("https://github.com/", ""); 26 26 if (existBetaThemeinInList(this.plugin, scrubbedAddress)) { 27 - toastMessage( 28 - this.plugin, 29 - "This theme is already in the list for beta testing", 30 - 10, 31 - ); 27 + toastMessage(this.plugin, "This theme is already in the list for beta testing", 10); 32 28 return; 33 29 } 34 30 ··· 44 40 this.contentEl.createEl("form", {}, (formEl) => { 45 41 formEl.addClass("brat-modal"); 46 42 new Setting(formEl).addText((textEl) => { 47 - textEl.setPlaceholder( 48 - "Repository (example: https://github.com/GitubUserName/repository-name", 49 - ); 43 + textEl.setPlaceholder("Repository (example: https://github.com/GitubUserName/repository-name"); 50 44 textEl.setValue(this.address); 51 45 textEl.onChange((value) => { 52 46 this.address = value.trim(); ··· 66 60 }); 67 61 68 62 formEl.createDiv("modal-button-container", (buttonContainerEl) => { 69 - buttonContainerEl 70 - .createEl("button", { attr: { type: "button" }, text: "Never mind" }) 71 - .addEventListener("click", () => { 72 - this.close(); 73 - }); 63 + buttonContainerEl.createEl("button", { attr: { type: "button" }, text: "Never mind" }).addEventListener("click", () => { 64 + this.close(); 65 + }); 74 66 buttonContainerEl.createEl("button", { 75 67 attr: { type: "submit" }, 76 68 cls: "mod-cta", ··· 82 74 newDiv.style.borderTop = "1px solid #ccc"; 83 75 newDiv.style.marginTop = "30px"; 84 76 const byTfThacker = newDiv.createSpan(); 85 - byTfThacker.innerHTML = 86 - "BRAT by <a href='https://bit.ly/o42-twitter'>TFTHacker</a>"; 77 + byTfThacker.innerHTML = "BRAT by <a href='https://bit.ly/o42-twitter'>TFTHacker</a>"; 87 78 byTfThacker.style.fontStyle = "italic"; 88 79 newDiv.appendChild(byTfThacker); 89 80 promotionalLinks(newDiv, false);
+5 -20
src/ui/GenericFuzzySuggester.ts
··· 17 17 */ 18 18 export class GenericFuzzySuggester extends FuzzySuggestModal<SuggesterItem> { 19 19 data: SuggesterItem[] = []; 20 - callbackFunction!: ( 21 - item: SuggesterItem, 22 - evt: MouseEvent | KeyboardEvent, 23 - ) => void; 20 + callbackFunction!: (item: SuggesterItem, evt: MouseEvent | KeyboardEvent) => void; 24 21 25 22 constructor(plugin: BratPlugin) { 26 23 super(plugin.app); ··· 36 33 this.data = suggesterData; 37 34 } 38 35 39 - display( 40 - callBack: (item: SuggesterItem, evt: MouseEvent | KeyboardEvent) => void, 41 - ) { 36 + display(callBack: (item: SuggesterItem, evt: MouseEvent | KeyboardEvent) => void) { 42 37 this.callbackFunction = callBack; 43 38 this.open(); 44 39 } ··· 60 55 } 61 56 62 57 enterTrigger(evt: KeyboardEvent): void { 63 - const selectedText = document.querySelector( 64 - ".suggestion-item.is-selected div", 65 - )?.textContent; 58 + const selectedText = document.querySelector(".suggestion-item.is-selected div")?.textContent; 66 59 const item = this.data.find((i) => i.display === selectedText); 67 60 if (item) { 68 61 this.invokeCallback(item, evt); ··· 70 63 } 71 64 } 72 65 73 - onChooseSuggestion( 74 - item: FuzzyMatch<SuggesterItem>, 75 - evt: MouseEvent | KeyboardEvent, 76 - ): void { 66 + onChooseSuggestion(item: FuzzyMatch<SuggesterItem>, evt: MouseEvent | KeyboardEvent): void { 77 67 this.invokeCallback(item.item, evt); 78 68 } 79 69 80 70 invokeCallback(item: SuggesterItem, evt: MouseEvent | KeyboardEvent): void { 81 71 if (typeof this.callbackFunction === "function") { 82 - ( 83 - this.callbackFunction as ( 84 - item: SuggesterItem, 85 - evt: MouseEvent | KeyboardEvent, 86 - ) => void 87 - )(item, evt); 72 + (this.callbackFunction as (item: SuggesterItem, evt: MouseEvent | KeyboardEvent) => void)(item, evt); 88 73 } 89 74 } 90 75 }
+35 -92
src/ui/PluginCommands.ts
··· 1 1 import type { SettingTab } from "obsidian"; 2 2 import type { CommunityPlugin, CommunityTheme } from "../features/githubUtils"; 3 - import { 4 - grabCommmunityPluginList, 5 - grabCommmunityThemesList, 6 - } from "../features/githubUtils"; 3 + import { grabCommmunityPluginList, grabCommmunityThemesList } from "../features/githubUtils"; 7 4 import { themesCheckAndUpdates } from "../features/themes"; 8 5 import type BratPlugin from "../main"; 9 6 import { toastMessage } from "../utils/notifications"; ··· 38 35 name: "Plugins: Check for updates to all beta plugins and UPDATE", 39 36 showInRibbon: true, 40 37 callback: async () => { 41 - await this.plugin.betaPlugins.checkForPluginUpdatesAndInstallUpdates( 42 - true, 43 - false, 44 - ); 38 + await this.plugin.betaPlugins.checkForPluginUpdatesAndInstallUpdates(true, false); 45 39 }, 46 40 }, 47 41 { ··· 50 44 name: "Plugins: Only check for updates to beta plugins, but don't Update", 51 45 showInRibbon: true, 52 46 callback: async () => { 53 - await this.plugin.betaPlugins.checkForPluginUpdatesAndInstallUpdates( 54 - true, 55 - true, 56 - ); 47 + await this.plugin.betaPlugins.checkForPluginUpdatesAndInstallUpdates(true, true); 57 48 }, 58 49 }, 59 50 { ··· 62 53 name: "Plugins: Choose a single plugin version to update", 63 54 showInRibbon: true, 64 55 callback: () => { 65 - const pluginSubListFrozenVersionNames = new Set( 66 - this.plugin.settings.pluginSubListFrozenVersion.map((f) => f.repo), 67 - ); 68 - const pluginList: SuggesterItem[] = Object.values( 69 - this.plugin.settings.pluginList, 70 - ) 56 + const pluginSubListFrozenVersionNames = new Set(this.plugin.settings.pluginSubListFrozenVersion.map((f) => f.repo)); 57 + const pluginList: SuggesterItem[] = Object.values(this.plugin.settings.pluginList) 71 58 .filter((f) => !pluginSubListFrozenVersionNames.has(f)) 72 59 .map((m) => { 73 60 return { display: m, info: m }; ··· 78 65 const msg = `Checking for updates for ${results.info as string}`; 79 66 void this.plugin.log(msg, true); 80 67 toastMessage(this.plugin, `\n${msg}`, 3); 81 - void this.plugin.betaPlugins.updatePlugin( 82 - results.info as string, 83 - false, 84 - true, 85 - ); 68 + void this.plugin.betaPlugins.updatePlugin(results.info as string, false, true); 86 69 }); 87 70 }, 88 71 }, ··· 92 75 name: "Plugins: Choose a single plugin to reinstall", 93 76 showInRibbon: true, 94 77 callback: () => { 95 - const pluginSubListFrozenVersionNames = new Set( 96 - this.plugin.settings.pluginSubListFrozenVersion.map((f) => f.repo), 97 - ); 98 - const pluginList: SuggesterItem[] = Object.values( 99 - this.plugin.settings.pluginList, 100 - ) 78 + const pluginSubListFrozenVersionNames = new Set(this.plugin.settings.pluginSubListFrozenVersion.map((f) => f.repo)); 79 + const pluginList: SuggesterItem[] = Object.values(this.plugin.settings.pluginList) 101 80 .filter((f) => !pluginSubListFrozenVersionNames.has(f)) 102 81 .map((m) => { 103 82 return { display: m, info: m }; ··· 108 87 const msg = `Reinstalling ${results.info as string}`; 109 88 toastMessage(this.plugin, `\n${msg}`, 3); 110 89 void this.plugin.log(msg, true); 111 - void this.plugin.betaPlugins.updatePlugin( 112 - results.info as string, 113 - false, 114 - false, 115 - true, 116 - ); 90 + void this.plugin.betaPlugins.updatePlugin(results.info as string, false, false, true); 117 91 }); 118 92 }, 119 93 }, ··· 123 97 name: "Plugins: Restart a plugin that is already installed", 124 98 showInRibbon: true, 125 99 callback: () => { 126 - const pluginList: SuggesterItem[] = Object.values( 127 - this.plugin.app.plugins.manifests, 128 - ).map((m) => { 100 + const pluginList: SuggesterItem[] = Object.values(this.plugin.app.plugins.manifests).map((m) => { 129 101 return { display: m.id, info: m.id }; 130 102 }); 131 103 const gfs = new GenericFuzzySuggester(this.plugin); 132 104 gfs.setSuggesterData(pluginList); 133 105 gfs.display((results) => { 134 - toastMessage( 135 - this.plugin, 136 - `${results.info as string}\nPlugin reloading .....`, 137 - 5, 138 - ); 106 + toastMessage(this.plugin, `${results.info as string}\nPlugin reloading .....`, 5); 139 107 void this.plugin.betaPlugins.reloadPlugin(results.info as string); 140 108 }); 141 109 }, ··· 146 114 name: "Plugins: Disable a plugin - toggle it off", 147 115 showInRibbon: true, 148 116 callback: () => { 149 - const pluginList = this.plugin.betaPlugins 150 - .getEnabledDisabledPlugins(true) 151 - .map((manifest) => { 152 - return { 153 - display: `${manifest.name} (${manifest.id})`, 154 - info: manifest.id, 155 - }; 156 - }); 117 + const pluginList = this.plugin.betaPlugins.getEnabledDisabledPlugins(true).map((manifest) => { 118 + return { 119 + display: `${manifest.name} (${manifest.id})`, 120 + info: manifest.id, 121 + }; 122 + }); 157 123 const gfs = new GenericFuzzySuggester(this.plugin); 158 124 gfs.setSuggesterData(pluginList); 159 125 gfs.display((results) => { 160 126 void this.plugin.log(`${results.display} plugin disabled`, false); 161 127 if (this.plugin.settings.debuggingMode) console.log(results.info); 162 - void this.plugin.app.plugins.disablePluginAndSave( 163 - results.info as string, 164 - ); 128 + void this.plugin.app.plugins.disablePluginAndSave(results.info as string); 165 129 }); 166 130 }, 167 131 }, ··· 171 135 name: "Plugins: Enable a plugin - toggle it on", 172 136 showInRibbon: true, 173 137 callback: () => { 174 - const pluginList = this.plugin.betaPlugins 175 - .getEnabledDisabledPlugins(false) 176 - .map((manifest) => { 177 - return { 178 - display: `${manifest.name} (${manifest.id})`, 179 - info: manifest.id, 180 - }; 181 - }); 138 + const pluginList = this.plugin.betaPlugins.getEnabledDisabledPlugins(false).map((manifest) => { 139 + return { 140 + display: `${manifest.name} (${manifest.id})`, 141 + info: manifest.id, 142 + }; 143 + }); 182 144 const gfs = new GenericFuzzySuggester(this.plugin); 183 145 gfs.setSuggesterData(pluginList); 184 146 gfs.display((results) => { 185 147 void this.plugin.log(`${results.display} plugin enabled`, false); 186 - void this.plugin.app.plugins.enablePluginAndSave( 187 - results.info as string, 188 - ); 148 + void this.plugin.app.plugins.enablePluginAndSave(results.info as string); 189 149 }); 190 150 }, 191 151 }, ··· 195 155 name: "Plugins: Open the GitHub repository for a plugin", 196 156 showInRibbon: true, 197 157 callback: async () => { 198 - const communityPlugins = await grabCommmunityPluginList( 199 - this.plugin.settings.debuggingMode, 200 - ); 158 + const communityPlugins = await grabCommmunityPluginList(this.plugin.settings.debuggingMode); 201 159 if (communityPlugins) { 202 - const communityPluginList: SuggesterItem[] = Object.values( 203 - communityPlugins, 204 - ).map((p: CommunityPlugin) => { 160 + const communityPluginList: SuggesterItem[] = Object.values(communityPlugins).map((p: CommunityPlugin) => { 205 161 return { display: `Plugin: ${p.name} (${p.repo})`, info: p.repo }; 206 162 }); 207 - const bratList: SuggesterItem[] = Object.values( 208 - this.plugin.settings.pluginList, 209 - ).map((p) => { 163 + const bratList: SuggesterItem[] = Object.values(this.plugin.settings.pluginList).map((p) => { 210 164 return { display: `BRAT: ${p}`, info: p }; 211 165 }); 212 166 for (const si of communityPluginList) { ··· 215 169 const gfs = new GenericFuzzySuggester(this.plugin); 216 170 gfs.setSuggesterData(bratList); 217 171 gfs.display((results) => { 218 - if (results.info) 219 - window.open(`https://github.com/${results.info as string}`); 172 + if (results.info) window.open(`https://github.com/${results.info as string}`); 220 173 }); 221 174 } 222 175 }, ··· 227 180 name: "Themes: Open the GitHub repository for a theme (appearance)", 228 181 showInRibbon: true, 229 182 callback: async () => { 230 - const communityTheme = await grabCommmunityThemesList( 231 - this.plugin.settings.debuggingMode, 232 - ); 183 + const communityTheme = await grabCommmunityThemesList(this.plugin.settings.debuggingMode); 233 184 if (communityTheme) { 234 - const communityThemeList: SuggesterItem[] = Object.values( 235 - communityTheme, 236 - ).map((p: CommunityTheme) => { 185 + const communityThemeList: SuggesterItem[] = Object.values(communityTheme).map((p: CommunityTheme) => { 237 186 return { display: `Theme: ${p.name} (${p.repo})`, info: p.repo }; 238 187 }); 239 188 const gfs = new GenericFuzzySuggester(this.plugin); 240 189 gfs.setSuggesterData(communityThemeList); 241 190 gfs.display((results) => { 242 - if (results.info) 243 - window.open(`https://github.com/${results.info as string}`); 191 + if (results.info) window.open(`https://github.com/${results.info as string}`); 244 192 }); 245 193 } 246 194 }, ··· 252 200 showInRibbon: true, 253 201 callback: () => { 254 202 const settings = this.plugin.app.setting; 255 - const listOfPluginSettingsTabs: SuggesterItem[] = Object.values( 256 - settings.pluginTabs, 257 - ).map((t) => { 203 + const listOfPluginSettingsTabs: SuggesterItem[] = Object.values(settings.pluginTabs).map((t) => { 258 204 return { display: `Plugin: ${t.name}`, info: t.id }; 259 205 }); 260 206 const gfs = new GenericFuzzySuggester(this.plugin); 261 - const listOfCoreSettingsTabs: SuggesterItem[] = Object.values( 262 - settings.settingTabs, 263 - ).map((t) => { 207 + const listOfCoreSettingsTabs: SuggesterItem[] = Object.values(settings.settingTabs).map((t) => { 264 208 return { display: `Core: ${t.name}`, info: t.id }; 265 209 }); 266 210 for (const si of listOfPluginSettingsTabs) { ··· 305 249 ribbonDisplayCommands(): void { 306 250 const bratCommandList: SuggesterItem[] = []; 307 251 for (const cmd of this.bratCommands) { 308 - if (cmd.showInRibbon) 309 - bratCommandList.push({ display: cmd.name, info: cmd.callback }); 252 + if (cmd.showInRibbon) bratCommandList.push({ display: cmd.name, info: cmd.callback }); 310 253 } 311 254 const gfs = new GenericFuzzySuggester(this.plugin); 312 255 // @ts-ignore
+1 -4
src/ui/Promotional.ts
··· 1 - export const promotionalLinks = ( 2 - containerEl: HTMLElement, 3 - settingsTab = true, 4 - ): HTMLElement => { 1 + export const promotionalLinks = (containerEl: HTMLElement, settingsTab = true): HTMLElement => { 5 2 const linksDiv = containerEl.createEl("div"); 6 3 linksDiv.style.float = "right"; 7 4
+10 -44
src/utils/BratAPI.ts
··· 1 - import { 2 - grabChecksumOfThemeCssFile, 3 - grabCommmunityThemeCssFile, 4 - grabLastCommitDateForFile, 5 - } from "../features/githubUtils"; 6 - import { 7 - themeDelete, 8 - themeSave, 9 - themesCheckAndUpdates, 10 - } from "../features/themes"; 1 + import { grabChecksumOfThemeCssFile, grabCommmunityThemeCssFile, grabLastCommitDateForFile } from "../features/githubUtils"; 2 + import { themeDelete, themeSave, themesCheckAndUpdates } from "../features/themes"; 11 3 import type BratPlugin from "../main"; 12 4 13 5 // This module is for API access for use in debuging console ··· 19 11 this.plugin = plugin; 20 12 } 21 13 22 - console = ( 23 - logDescription: string, 24 - ...outputs: (string | number | boolean)[] 25 - ): void => { 14 + console = (logDescription: string, ...outputs: (string | number | boolean)[]): void => { 26 15 console.log(`BRAT: ${logDescription}`, ...outputs); 27 16 }; 28 17 ··· 32 21 }, 33 22 34 23 themeInstallTheme: async (cssGithubRepository: string): Promise<void> => { 35 - const scrubbedAddress = cssGithubRepository.replace( 36 - "https://github.com/", 37 - "", 38 - ); 24 + const scrubbedAddress = cssGithubRepository.replace("https://github.com/", ""); 39 25 await themeSave(this.plugin, scrubbedAddress, true); 40 26 }, 41 27 42 28 themesDelete: (cssGithubRepository: string): void => { 43 - const scrubbedAddress = cssGithubRepository.replace( 44 - "https://github.com/", 45 - "", 46 - ); 29 + const scrubbedAddress = cssGithubRepository.replace("https://github.com/", ""); 47 30 themeDelete(this.plugin, scrubbedAddress); 48 31 }, 49 32 50 - grabCommmunityThemeCssFile: async ( 51 - repositoryPath: string, 52 - betaVersion = false, 53 - ): Promise<string | null> => { 54 - return await grabCommmunityThemeCssFile( 55 - repositoryPath, 56 - betaVersion, 57 - this.plugin.settings.debuggingMode, 58 - ); 33 + grabCommmunityThemeCssFile: async (repositoryPath: string, betaVersion = false): Promise<string | null> => { 34 + return await grabCommmunityThemeCssFile(repositoryPath, betaVersion, this.plugin.settings.debuggingMode); 59 35 }, 60 36 61 - grabChecksumOfThemeCssFile: async ( 62 - repositoryPath: string, 63 - betaVersion = false, 64 - ): Promise<string> => { 65 - return await grabChecksumOfThemeCssFile( 66 - repositoryPath, 67 - betaVersion, 68 - this.plugin.settings.debuggingMode, 69 - ); 37 + grabChecksumOfThemeCssFile: async (repositoryPath: string, betaVersion = false): Promise<string> => { 38 + return await grabChecksumOfThemeCssFile(repositoryPath, betaVersion, this.plugin.settings.debuggingMode); 70 39 }, 71 40 72 - grabLastCommitDateForFile: async ( 73 - repositoryPath: string, 74 - path: string, 75 - ): Promise<string> => { 41 + grabLastCommitDateForFile: async (repositoryPath: string, path: string): Promise<string> => { 76 42 // example await grabLastCommitDateForAFile(t.repo, "theme-beta.css"); 77 43 return await grabLastCommitDateForFile(repositoryPath, path); 78 44 },
+1 -5
src/utils/logging.ts
··· 11 11 * @param verboseLoggingOn - True if should only be logged if verbose logging is enabled 12 12 * 13 13 */ 14 - export async function logger( 15 - plugin: BratPlugin, 16 - textToLog: string, 17 - verboseLoggingOn = false, 18 - ): Promise<void> { 14 + export async function logger(plugin: BratPlugin, textToLog: string, verboseLoggingOn = false): Promise<void> { 19 15 if (plugin.settings.debuggingMode) console.log(`BRAT: ${textToLog}`); 20 16 if (plugin.settings.loggingEnabled) { 21 17 if (!plugin.settings.loggingVerboseEnabled && verboseLoggingOn) return;
+3 -15
src/utils/notifications.ts
··· 9 9 * @param timeoutInSeconds - Number of seconds to show the Toast message 10 10 * @param contextMenuCallback - function to call if right mouse clicked 11 11 */ 12 - export function toastMessage( 13 - plugin: BratPlugin, 14 - msg: string, 15 - timeoutInSeconds = 10, 16 - contextMenuCallback?: () => void, 17 - ): void { 12 + export function toastMessage(plugin: BratPlugin, msg: string, timeoutInSeconds = 10, contextMenuCallback?: () => void): void { 18 13 if (!plugin.settings.notificationsEnabled) return; 19 - const additionalInfo = contextMenuCallback 20 - ? Platform.isDesktop 21 - ? "(click=dismiss, right-click=Info)" 22 - : "(click=dismiss)" 23 - : ""; 24 - const newNotice: Notice = new Notice( 25 - `BRAT\n${msg}\n${additionalInfo}`, 26 - timeoutInSeconds * 1000, 27 - ); 14 + const additionalInfo = contextMenuCallback ? (Platform.isDesktop ? "(click=dismiss, right-click=Info)" : "(click=dismiss)") : ""; 15 + const newNotice: Notice = new Notice(`BRAT\n${msg}\n${additionalInfo}`, timeoutInSeconds * 1000); 28 16 if (contextMenuCallback) 29 17 newNotice.noticeEl.oncontextmenu = () => { 30 18 contextMenuCallback();
+8 -5
styles.css
··· 1 1 .brat-modal .modal-button-container { 2 - margin-top: 5px !important; 2 + margin-top: 5px !important; 3 3 } 4 4 5 5 .brat-modal .valid-repository { 6 - border-color: var(--color-green) !important; 6 + border-color: var(--color-green) !important; 7 7 } 8 8 .brat-modal .invalid-repository { 9 - border-color: var(--color-red) !important; 9 + border-color: var(--color-red) !important; 10 10 } 11 11 .brat-modal .disabled-setting { 12 - opacity: 0.5; 13 - } 12 + opacity: 0.5; 13 + } 14 + .brat-modal .disabled-setting:hover { 15 + cursor: not-allowed; 16 + }
+13 -10
version-github-action.mjs
··· 1 - import fs from 'fs'; 2 - import { exec } from 'child_process'; 1 + import { exec } from "node:child_process"; 2 + import fs from "node:fs"; 3 3 4 4 // Read the manifest.json file 5 - fs.readFile('manifest.json', 'utf8', (err, data) => { 5 + fs.readFile("manifest.json", "utf8", (err, data) => { 6 6 if (err) { 7 7 console.error(`Error reading file from disk: ${err}`); 8 8 } else { ··· 13 13 const version = manifest.version; 14 14 15 15 // Execute the git commands 16 - exec(`git tag -a ${version} -m "${version}" && git push origin ${version}`, (error, stdout, stderr) => { 17 - if (error) { 18 - console.error(`exec error: ${error}`); 19 - return; 16 + exec( 17 + `git tag -a ${version} -m "${version}" && git push origin ${version}`, 18 + (error, stdout, stderr) => { 19 + if (error) { 20 + console.error(`exec error: ${error}`); 21 + return; 22 + } 23 + console.log(`stdout: ${stdout}`); 24 + console.error(`stderr: ${stderr}`); 20 25 } 21 - console.log(`stdout: ${stdout}`); 22 - console.error(`stderr: ${stderr}`); 23 - }); 26 + ); 24 27 } 25 28 });