diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cbe2351d..b9b367b9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,4 +1,4 @@ -name: Deploy modules and docs to GitHub Pages +name: Deploy modules and docs on: push: @@ -10,8 +10,6 @@ on: permissions: contents: write - pages: write - id-token: write concurrency: group: "pages" @@ -21,7 +19,7 @@ jobs: deploy: runs-on: ubuntu-slim environment: - name: github-pages + name: modules url: ${{ steps.deployment.outputs.page_url }} steps: @@ -45,11 +43,7 @@ jobs: - name: Build docs run: bun build:docs - - name: Upload artifact - uses: actions/upload-pages-artifact@v5 + - name: Deploy docs + uses: JamesIves/github-pages-deploy-action@v4 with: - path: "./docs/dist" - - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v5 + folder: './docs/dist' diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index e77fb57f..940dead4 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -189,9 +189,9 @@ export default defineConfig({ logoLarge: { alt: 'Website large logo', src: '/logo.svg' }, search: { provider: 'local' }, socialLinks: [ - { icon: 'npm', link: 'https://www.npmjs.com/~hesprs' }, - { icon: 'github', link: 'https://github.com/hesprs' }, - { icon: 'x', link: 'https://x.com/Hesprs' }, + { icon: 'npm', link: 'https://www.npmjs.com/package/@hesprs/sync-engine-sdk' }, + { icon: 'github', link: 'https://github.com/hesprs/sync-engine' }, + { icon: 'obsidian', link: 'https://community.obsidian.md/plugins/sync-engine' }, ], }, vite: { diff --git a/docs/src/pages/en/deep-dive/extensibility.md b/docs/src/pages/en/deep-dive/extensibility.md index 0fe2a1cc..6d963cc7 100644 --- a/docs/src/pages/en/deep-dive/extensibility.md +++ b/docs/src/pages/en/deep-dive/extensibility.md @@ -22,6 +22,14 @@ The loader guards against: Module source URLs are stored in `settings.moduleSources`. The default source is `https://sync.consensia.cc/modules.json`, this is the official source hosted on GitHub pages, fully transparent. +Sync Engine officially hosts an alternative source `https://github.com/hesprs/sync-engine/raw/refs/heads/gh-pages/modules-alternative.json`. This source is identical to the main source except replaced all `sync.consensia.cc` to `hesprs.github.io/sync-engine`. Use this source when your firewall flags `sync.consensia.cc` as unsafe. + +::: warning + +Please avoid using two sources simultaneously, if you have decided to use `https://github.com/hesprs/sync-engine/raw/refs/heads/gh-pages/modules-alternative.json`, delete `https://sync.consensia.cc/modules.json` in the sources list. + +::: + The Module Management UI allows users to add and remove source URLs. It accepts `http:` and `https:` URLs. HTTP sources display an insecure-protocol warning but remain valid. Plugin fetches every configured source with Obsidian `requestUrl()`. A source must contain a JSON array. Each accepted entry must contain string values for: diff --git a/docs/src/pages/en/deep-dive/modules/encryption.md b/docs/src/pages/en/deep-dive/modules/encryption.md index 542a7c85..2a299e05 100644 --- a/docs/src/pages/en/deep-dive/modules/encryption.md +++ b/docs/src/pages/en/deep-dive/modules/encryption.md @@ -84,7 +84,7 @@ The function is toggled via the **Encryption** setting. The password is stored i On each sync, generate a promise to obtain _root file key_ and _name key_ that will be resolved and cached on demand: - starting check: if found the user password is an empty string or undefined, throw directly -- generate _master salt_: `SHA256` of `..` truncated to 16B. +- generate _master salt_: `SHA256` of **distinguishing data of the configured backend account that is often safe to disclose (like account name + server URL + directory prefix)** truncated to 16B. - generate _master key_ using `Argon2id` with 32MB memory, 3 iterations, and 1 thread on the _user password_ with _master salt_ - derive _root file key_ (info: `root-file-key-v1`, no salt) and _name key_ (info: `name-key-v1`, no salt) using `HKDF-SHA-256` - resolve with _root file key_ and _name key_ diff --git a/docs/src/pages/en/usage/modules.md b/docs/src/pages/en/usage/modules.md index 4f73908e..dee8f413 100644 --- a/docs/src/pages/en/usage/modules.md +++ b/docs/src/pages/en/usage/modules.md @@ -6,4 +6,6 @@ import ModuleCards from '@/components/ModuleCards.vue'; Below shows all currently available modules, the recommended way is to download them in the plugin module management UI. You can also download the JavaScript binaries here. +The official module source is `https://sync.consensia.cc/modules.json`. If your machine fails to fetch this module source (often due to aggressive firewall), you can try adding `https://github.com/hesprs/sync-engine/raw/refs/heads/gh-pages/modules-alternative.json` (and delete the original one) to your module sources. This source is the official alternative using GitHub's domain. + diff --git a/docs/src/pages/en/usage/settings.md b/docs/src/pages/en/usage/settings.md index 126ef3cd..ab169aca 100644 --- a/docs/src/pages/en/usage/settings.md +++ b/docs/src/pages/en/usage/settings.md @@ -149,6 +149,12 @@ private/other.md does not sync An included file can be inside several excluded folders. Add an inclusion rule for the file you want to keep. +::: warning + +If you sync Obsidian's plugin directory, you need to **exclude Sync Engine's modules folder in any situation (always ensure `.obsidian/plugins/sync-engine/modules` exists in your exclusion rules)**. [Due to security considerations](../deep-dive/extensibility), all untracked modifications in that folder (including sync runs) will trigger Sync Engine's security protection mechanism. And will cause the modules fail to load. + +::: + ### Writing Rules Rules are written as [`glob` expressions](). These are common examples: diff --git a/packages/i18n/src/ru/translations.ts b/packages/i18n/src/ru/translations.ts index 4d6df26c..9126906d 100644 --- a/packages/i18n/src/ru/translations.ts +++ b/packages/i18n/src/ru/translations.ts @@ -114,8 +114,20 @@ const ru: Translations = { enable: 'Включить', enableModule: 'Включить модуль', exclusionRules: 'Правила исключения', - exclusionRulesDescription: - 'Файлы и папки, соответствующие этим glob-шаблонам, не будут синхронизироваться. Не забудьте указать расширения файлов (например, .md), если хотите исключить файлы.', + exclusionRulesDescription: (frag) => { + frag.appendText( + 'Файлы и папки, соответствующие этим glob-шаблонам, не будут синхронизироваться. Не забудьте указать расширения файлов (например, ', + ); + frag.createEl('code', { text: '.md' }); + frag.appendText('), если хотите исключить файлы. См. '); + frag.createEl('a', { + attr: { + href: 'https://sync.consensia.cc/usage/settings#inclusion-and-exclusion-rules', + }, + text: 'документацию по настройкам', + }); + frag.appendText(' для руководства по настройке.'); + }, executing: 'Выполняется', export: 'Экспорт', exportLogsDescription: @@ -149,8 +161,18 @@ const ru: Translations = { iconPlaceholder: 'Введите код Lucide Icons (например, puzzle)', idle: 'В ожидании', inclusionRules: 'Правила включения', - inclusionRulesDescription: - 'Файлы и папки, подпадающие под правила исключения, но соответствующие этим glob-шаблонам, всё равно будут синхронизированы.', + inclusionRulesDescription: (frag) => { + frag.appendText( + 'Файлы и папки, подпадающие под правила исключения, но соответствующие этим glob-шаблонам, всё равно будут синхронизированы. См. ', + ); + frag.createEl('a', { + attr: { + href: 'https://sync.consensia.cc/usage/settings#inclusion-and-exclusion-rules', + }, + text: 'документацию по настройкам', + }); + frag.appendText(' для руководства по настройке.'); + }, installed: 'Установлено', integrityVerification: 'Проверка целостности', integrityVerificationDescription: (frag) => { diff --git a/packages/i18n/src/zh-TW/translations.ts b/packages/i18n/src/zh-TW/translations.ts index b79fcb08..a1b04bc8 100644 --- a/packages/i18n/src/zh-TW/translations.ts +++ b/packages/i18n/src/zh-TW/translations.ts @@ -112,8 +112,20 @@ const zhTW: Translations = { enable: '啟用', enableModule: '啟用模組', exclusionRules: '排除規則', - exclusionRulesDescription: - '符合這些 Glob 萬用字元模式的檔案或資料夾將不會進行同步。若要排除特定檔案,請記得加上副檔名(例如 .md)。', + exclusionRulesDescription: (frag) => { + frag.appendText( + '符合這些 Glob 萬用字元模式的檔案或資料夾將不會進行同步。若要排除特定檔案,請記得加上副檔名(例如 ', + ); + frag.createEl('code', { text: '.md' }); + frag.appendText(')。請參閱 '); + frag.createEl('a', { + attr: { + href: 'https://sync.consensia.cc/usage/settings#inclusion-and-exclusion-rules', + }, + text: '設定文件', + }); + frag.appendText('以瞭解設定指南。'); + }, executing: '執行中', export: '匯出', exportLogsDescription: '將外掛程式紀錄匯出至儲存庫中的檔案。請在欄位中設定紀錄匯出目錄。', @@ -144,8 +156,18 @@ const zhTW: Translations = { iconPlaceholder: '輸入 Lucide Icons 代碼(例如 puzzle)', idle: '待命', inclusionRules: '包含規則', - inclusionRulesDescription: - '即使符合排除規則,只要符合這些 Glob 萬用字元模式的檔案或資料夾仍會進行同步。', + inclusionRulesDescription: (frag) => { + frag.appendText( + '即使符合排除規則,只要符合這些 Glob 萬用字元模式的檔案或資料夾仍會進行同步。請參閱 ', + ); + frag.createEl('a', { + attr: { + href: 'https://sync.consensia.cc/usage/settings#inclusion-and-exclusion-rules', + }, + text: '設定文件', + }); + frag.appendText('以瞭解設定指南。'); + }, installed: '已安裝', integrityVerification: '完整性驗證', integrityVerificationDescription: (frag) => { diff --git a/packages/i18n/src/zh/translations.ts b/packages/i18n/src/zh/translations.ts index 358543e7..ceb53a76 100644 --- a/packages/i18n/src/zh/translations.ts +++ b/packages/i18n/src/zh/translations.ts @@ -100,8 +100,20 @@ const zh: Translations = { enable: '启用', enableModule: '启用模块', exclusionRules: '排除规则', - exclusionRulesDescription: - '匹配这些 Glob 模式的文件 / 文件夹将不会被同步。如果您想排除文件,请记得添加文件扩展名(例如 .md)。', + exclusionRulesDescription: (frag) => { + frag.appendText( + '匹配这些 Glob 模式的文件 / 文件夹将不会被同步。如果您想排除文件,请记得添加文件扩展名(例如 ', + ); + frag.createEl('code', { text: '.md' }); + frag.appendText(')。请参阅 '); + frag.createEl('a', { + attr: { + href: 'https://sync.consensia.cc/usage/settings#inclusion-and-exclusion-rules', + }, + text: '设置文档', + }); + frag.appendText('了解配置指南。'); + }, executing: '正在执行', export: '导出', exportLogsDescription: '将插件日志导出到仓库中的文件。请在输入框中设置日志导出目录。', @@ -132,7 +144,16 @@ const zh: Translations = { iconPlaceholder: '输入 Lucide 图标代码(例如 puzzle)', idle: '空闲', inclusionRules: '包含规则', - inclusionRulesDescription: '匹配排除规则但同时也匹配这些 Glob 模式的文件 / 文件夹仍会被同步。', + inclusionRulesDescription: (frag) => { + frag.appendText('匹配排除规则但同时也匹配这些 Glob 模式的文件 / 文件夹仍会被同步。请参阅 '); + frag.createEl('a', { + attr: { + href: 'https://sync.consensia.cc/usage/settings#inclusion-and-exclusion-rules', + }, + text: '设置文档', + }); + frag.appendText('了解配置指南。'); + }, installed: '已安装', integrityVerification: '完整性验证', integrityVerificationDescription: (frag) => { diff --git a/packages/plugin/dist/dev.spec.d.ts b/packages/plugin/dist/dev.spec.d.ts index cbfe4686..a0ee83c7 100644 --- a/packages/plugin/dist/dev.spec.d.ts +++ b/packages/plugin/dist/dev.spec.d.ts @@ -1,4 +1,4 @@ -import { Ct as FolderStat, Dt as RecordStatsMap, Et as RecordStat, J as TaskNames, M as Decider, Ot as Stat, St as FileStat, f as Request, kt as StatsMap, p as RequestParam, ut as Fs, vt as RootFs, xt as Binary, yt as WrappedFs } from "./index-BJDjAUwk.spec.js"; +import { Ct as FolderStat, Dt as RecordStatsMap, Et as RecordStat, J as TaskNames, M as Decider, Ot as Stat, St as FileStat, f as Request, kt as StatsMap, p as RequestParam, ut as Fs, vt as RootFs, xt as Binary, yt as WrappedFs } from "./index-yE2TnKwI.spec.js"; //#region src/sdk/debug-wrapper.d.ts declare function debugWrapper(original: Fs, log: (content: string) => void): WrappedFs; //#endregion diff --git a/packages/plugin/dist/index-BJDjAUwk.spec.d.ts b/packages/plugin/dist/index-yE2TnKwI.spec.d.ts similarity index 99% rename from packages/plugin/dist/index-BJDjAUwk.spec.d.ts rename to packages/plugin/dist/index-yE2TnKwI.spec.d.ts index d7a34f25..de70e42d 100644 --- a/packages/plugin/dist/index-BJDjAUwk.spec.d.ts +++ b/packages/plugin/dist/index-yE2TnKwI.spec.d.ts @@ -835,8 +835,8 @@ type FilterEditorTranslations = { add: string; inclusionRules: string; exclusionRules: string; - inclusionRulesDescription: string; - exclusionRulesDescription: string; + inclusionRulesDescription: Fragment; + exclusionRulesDescription: Fragment; filterPlaceholder: string; }; //#endregion diff --git a/packages/plugin/dist/index.spec.d.ts b/packages/plugin/dist/index.spec.d.ts index 0a4e9374..2123537a 100644 --- a/packages/plugin/dist/index.spec.d.ts +++ b/packages/plugin/dist/index.spec.d.ts @@ -1,2 +1,2 @@ -import { $ as TranslationResource, A as SyncTerminateReason, B as MoveRemote, C as SelectFromContext, Ct as FolderStat, D as writeWithValue, Dt as RecordStatsMap, E as readWithSize, Et as RecordStat, F as Upload, G as BaseTask, H as Download, I as ResolveConflict, J as TaskNames, K as ConflictResolver, L as RemoveRemote, M as Decider, N as DeciderInput, O as prefixWrapper, Ot as Stat, P as TaskFactory, Q as Translate, R as RemoveRecord, S as ModuleMeta, St as FileStat, T as pipe, Tt as Progress, U as CreateRemoteDir, V as MoveLocal, W as AddRecord, X as Fragment, Y as RecordStore, Z as ObsidianLanguageCode, _ as Events, _t as OutputAtom, a as FsWrapperEntry, at as StoreOperations, b as ExistingMemoryDB, bt as WriteAtom, c as RemoteFsEntry, ct as CustomAtom, d as RemoteRequestMiddlewareEntry, dt as InputAtom, et as Dispatch, f as Request, ft as ListReporter, g as Context, gt as OptimizerOutput, h as SettingEntry, ht as OptimizerInput, i as DeciderEntry, it as StoreAsync, j as CreateLocalDir, k as setNeedMigration, kt as StatsMap, l as RemoteLister, lt as DeleteAtom, m as RequestResponse, mt as MoveAtom, n as CheckConnectionResult, nt as DatabaseAsync, o as LocalRequestMiddlewareEntry, ot as StoreSync, p as RequestParam, pt as MkdirAtom, q as ConflictResolverPayload, r as ConflictResolverEntry, rt as DatabaseSync, s as OptimizerEntry, st as BatchOptimizer, t as VaultRequest, tt as On, u as RemoteListerEntry, ut as Fs, v as Settings, vt as RootFs, w as digOriginal, wt as MaybePromise, x as AugmentedModuleMeta, xt as Binary, y as Translations, yt as WrappedFs, z as RemoveLocal } from "./index-BJDjAUwk.spec.js"; +import { $ as TranslationResource, A as SyncTerminateReason, B as MoveRemote, C as SelectFromContext, Ct as FolderStat, D as writeWithValue, Dt as RecordStatsMap, E as readWithSize, Et as RecordStat, F as Upload, G as BaseTask, H as Download, I as ResolveConflict, J as TaskNames, K as ConflictResolver, L as RemoveRemote, M as Decider, N as DeciderInput, O as prefixWrapper, Ot as Stat, P as TaskFactory, Q as Translate, R as RemoveRecord, S as ModuleMeta, St as FileStat, T as pipe, Tt as Progress, U as CreateRemoteDir, V as MoveLocal, W as AddRecord, X as Fragment, Y as RecordStore, Z as ObsidianLanguageCode, _ as Events, _t as OutputAtom, a as FsWrapperEntry, at as StoreOperations, b as ExistingMemoryDB, bt as WriteAtom, c as RemoteFsEntry, ct as CustomAtom, d as RemoteRequestMiddlewareEntry, dt as InputAtom, et as Dispatch, f as Request, ft as ListReporter, g as Context, gt as OptimizerOutput, h as SettingEntry, ht as OptimizerInput, i as DeciderEntry, it as StoreAsync, j as CreateLocalDir, k as setNeedMigration, kt as StatsMap, l as RemoteLister, lt as DeleteAtom, m as RequestResponse, mt as MoveAtom, n as CheckConnectionResult, nt as DatabaseAsync, o as LocalRequestMiddlewareEntry, ot as StoreSync, p as RequestParam, pt as MkdirAtom, q as ConflictResolverPayload, r as ConflictResolverEntry, rt as DatabaseSync, s as OptimizerEntry, st as BatchOptimizer, t as VaultRequest, tt as On, u as RemoteListerEntry, ut as Fs, v as Settings, vt as RootFs, w as digOriginal, wt as MaybePromise, x as AugmentedModuleMeta, xt as Binary, y as Translations, yt as WrappedFs, z as RemoveLocal } from "./index-yE2TnKwI.spec.js"; export { type AddRecord, type AugmentedModuleMeta, type BaseTask, type BatchOptimizer, type Binary, type CheckConnectionResult, type ConflictResolver, type ConflictResolverEntry, type ConflictResolverPayload, type Context, type CreateLocalDir, type CreateRemoteDir, type CustomAtom, type DatabaseAsync, type DatabaseSync, type Decider, type DeciderEntry, type DeciderInput, type DeleteAtom, type Dispatch, type Download, type Events, type ExistingMemoryDB, type FileStat, type FolderStat, type Fragment, type Fs, type FsWrapperEntry, type InputAtom, type ListReporter, type LocalRequestMiddlewareEntry, type MaybePromise, type MkdirAtom, type ModuleMeta, type MoveAtom, type MoveLocal, type MoveRemote, type ObsidianLanguageCode, type On, type OptimizerEntry, type OptimizerInput, type OptimizerOutput, type OutputAtom, type Progress, type RecordStat, type RecordStatsMap, type RecordStore, type RemoteFsEntry, type RemoteLister, type RemoteListerEntry, type RemoteRequestMiddlewareEntry, type RemoveLocal, type RemoveRecord, type RemoveRemote, type Request, type RequestParam, type RequestResponse, type ResolveConflict, type RootFs, SelectFromContext, type SettingEntry, type Settings, type Stat, type StatsMap, type StoreAsync, type StoreOperations, type StoreSync, type SyncTerminateReason, type TaskFactory, type TaskNames, type Translate, type TranslationResource, type Translations, type Upload, type VaultRequest, type WrappedFs, type WriteAtom, digOriginal, pipe, prefixWrapper, readWithSize, setNeedMigration, writeWithValue }; \ No newline at end of file diff --git a/packages/plugin/src/components/FilterEditorModal.ts b/packages/plugin/src/components/FilterEditorModal.ts index 362d2b81..c5561fb2 100644 --- a/packages/plugin/src/components/FilterEditorModal.ts +++ b/packages/plugin/src/components/FilterEditorModal.ts @@ -1,5 +1,5 @@ import { App, Modal, setIcon, Setting, setTooltip } from 'obsidian'; -import type { Translate } from '@/modules/I18n'; +import type { Fragment, Translate } from '@/modules/I18n'; import type { GlobMatchRule } from '@/types'; type FilterType = 'include' | 'exclude'; @@ -11,8 +11,8 @@ export type FilterEditorTranslations = { add: string; inclusionRules: string; exclusionRules: string; - inclusionRulesDescription: string; - exclusionRulesDescription: string; + inclusionRulesDescription: Fragment; + exclusionRulesDescription: Fragment; filterPlaceholder: string; }; diff --git a/packages/plugin/src/en.ts b/packages/plugin/src/en.ts index f5d36f35..1146941a 100644 --- a/packages/plugin/src/en.ts +++ b/packages/plugin/src/en.ts @@ -114,8 +114,20 @@ const en: Translations = { enable: 'Enable', enableModule: 'Enable module', exclusionRules: 'Exclusion rules', - exclusionRulesDescription: - 'Files / folders matching these glob patterns will not be synced. Please remember to add file extensions (for example, .md) if you want to exclude files.', + exclusionRulesDescription: (frag) => { + frag.appendText( + 'Files / folders matching these Glob patterns will not be synced. Please remember to add file extensions (E.g. ', + ); + frag.createEl('code', { text: '.md' }); + frag.appendText(') if you want to exclude files. Refer to '); + frag.createEl('a', { + attr: { + href: 'https://sync.consensia.cc/usage/settings#inclusion-and-exclusion-rules', + }, + text: 'settings documentation', + }); + frag.appendText(' for configuration guide.'); + }, executing: 'Executing', export: 'Export', exportLogsDescription: @@ -149,8 +161,18 @@ const en: Translations = { iconPlaceholder: 'Enter Lucide Icons code (e.g. puzzle)', idle: 'Idle', inclusionRules: 'Inclusion rules', - inclusionRulesDescription: - 'Files / folders matching exclusion rules but also matching these glob patterns will still be synced.', + inclusionRulesDescription: (frag) => { + frag.appendText( + 'Files / folders matching exclusion rules but also matching these glob patterns will still be synced. Refer to ', + ); + frag.createEl('a', { + attr: { + href: 'https://sync.consensia.cc/usage/settings#inclusion-and-exclusion-rules', + }, + text: 'settings documentation', + }); + frag.appendText(' for configuration guide.'); + }, installed: 'Installed', integrityVerification: 'Integrity verification', integrityVerificationDescription: (frag) => { diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index f5dba5f4..f887d474 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -78,6 +78,7 @@ export default class SyncEngine extends Plugin { '**/~$*.pptx', '**/~$*.xls', '**/~$*.xlsx', + `${this.app.vault.configDir}/plugins/sync-engine/modules`, '.trash', this.app.vault.configDir, ].map((expr) => ({ caseSensitive: false, expr })), diff --git a/scripts/deploy-modules.ts b/scripts/deploy-modules.ts index b40f0c4e..8359e7d8 100644 --- a/scripts/deploy-modules.ts +++ b/scripts/deploy-modules.ts @@ -18,6 +18,7 @@ const ROOT = `${import.meta.dir}/..`; const PUBLIC_DIR = `${ROOT}/docs/public`; const PUBLIC_MODULES_DIR = `${PUBLIC_DIR}/modules`; const PUBLIC_MODULES_PATH = `${PUBLIC_DIR}/modules.json`; +const PUBLIC_ALTERNATIVE_MODULES_PATH = `${PUBLIC_DIR}/modules-alternative.json`; async function listMatches(pattern: string): Promise> { const matches: Array = []; @@ -53,7 +54,16 @@ async function main(): Promise { ); const result = deployed.filter((entry): entry is ModuleMeta => entry !== undefined); - await Bun.write(PUBLIC_MODULES_PATH, JSON.stringify(result)); + const source = JSON.stringify(result); + await Bun.write(PUBLIC_MODULES_PATH, source); + // https://github.com/hesprs/sync-engine/issues/229 + await Bun.write( + PUBLIC_ALTERNATIVE_MODULES_PATH, + source.replaceAll( + 'sync.consensia.cc', + 'github.com/hesprs/sync-engine/raw/refs/heads/gh-pages', + ), + ); console.log(`Wrote modules.json with ${result.length} module(s)`); }