From 58a8fc2ae4ecb8e48754045a195ce853bf323d25 Mon Sep 17 00:00:00 2001 From: Thaddaeus Chua Date: Mon, 6 Apr 2026 18:24:18 +0800 Subject: [PATCH 1/7] Replace error logging with silent skip when page DNE Pages that exist get indexed normally while pages that don't are skipped silently without returning an error message for missing pages. --- packages/core/src/Site/SiteGenerationManager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/Site/SiteGenerationManager.ts b/packages/core/src/Site/SiteGenerationManager.ts index a5fce2f302..a3405f039c 100644 --- a/packages/core/src/Site/SiteGenerationManager.ts +++ b/packages/core/src/Site/SiteGenerationManager.ts @@ -908,6 +908,10 @@ export class SiteGenerationManager { // Add each searchable page to the index using addHTMLFile const indexingResults = await Promise.all( searchablePages.map(async (page) => { + const fileExists = await fs.pathExists(page.pageConfig.resultPath); + if (!fileExists) { + return null; + } try { const content = await fs.readFile(page.pageConfig.resultPath, 'utf8'); const relativePath = path.relative(this.outputPath, page.pageConfig.resultPath); From c17186184afa3a9cabc66caab73eb6f7af308658 Mon Sep 17 00:00:00 2001 From: Thaddaeus Chua Date: Mon, 6 Apr 2026 18:58:14 +0800 Subject: [PATCH 2/7] Add more robust check for onepage Included a check for if serving using onepage so that the error is still raised if the file does not exist when the user serves a full build. --- packages/core/src/Site/SiteGenerationManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/Site/SiteGenerationManager.ts b/packages/core/src/Site/SiteGenerationManager.ts index a3405f039c..99ec8b9745 100644 --- a/packages/core/src/Site/SiteGenerationManager.ts +++ b/packages/core/src/Site/SiteGenerationManager.ts @@ -909,7 +909,7 @@ export class SiteGenerationManager { const indexingResults = await Promise.all( searchablePages.map(async (page) => { const fileExists = await fs.pathExists(page.pageConfig.resultPath); - if (!fileExists) { + if (!fileExists && this.onePagePath) { return null; } try { From 0417a2fa6470a19553ab1ddf753627c4b4873805 Mon Sep 17 00:00:00 2001 From: Thaddaeus Chua Date: Mon, 6 Apr 2026 19:08:19 +0800 Subject: [PATCH 3/7] Add test cases --- .../unit/Site/SiteGenerationManager.test.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/core/test/unit/Site/SiteGenerationManager.test.ts b/packages/core/test/unit/Site/SiteGenerationManager.test.ts index 02745f543b..d673529663 100644 --- a/packages/core/test/unit/Site/SiteGenerationManager.test.ts +++ b/packages/core/test/unit/Site/SiteGenerationManager.test.ts @@ -278,6 +278,53 @@ describe('SiteGenerationManager', () => { errorSpy.mockRestore(); }); + test('should skip missing pages in onePage mode without error', async () => { + const generationManagerOnePage = new SiteGenerationManager( + rootPath, + outputPath, + 'index.md', + false, + undefined, + false, + false, + () => {}, + ); + generationManagerOnePage.configure(siteAssets, sitePages); + + const json = { + ...PAGE_NJK, + 'site.json': SITE_JSON_DEFAULT, + '_site/index.html': 'Existing page', + }; + mockFs.vol.fromJSON(json, rootPath); + + const mockIndex = createMockIndex({ page_count: 1, errors: [] }, { errors: [] }); + const mockPagefindInstance = createMockPagefind(mockIndex, true); + const pagefindSpy = jest.spyOn(pagefind, 'createIndex').mockResolvedValue( + mockPagefindInstance.createIndex({}) as any, + ); + const errorSpy = jest.spyOn(logger, 'error').mockImplementation(); + + generationManagerOnePage.siteConfig = { enableSearch: true, pages: [] } as any; + generationManagerOnePage.sitePages.pages = [ + { pageConfig: { resultPath: path.join(outputPath, 'index.html'), searchable: true } }, + { pageConfig: { resultPath: path.join(outputPath, 'page2.html'), searchable: true } }, + ] as any; + + await generationManagerOnePage.indexSiteWithPagefind(); + + expect(mockIndex.addHTMLFile).toHaveBeenCalledTimes(1); + expect(mockIndex.addHTMLFile).toHaveBeenCalledWith({ + sourcePath: 'index.html', + content: 'Existing page', + }); + + expect(errorSpy).not.toHaveBeenCalled(); + + pagefindSpy.mockRestore(); + errorSpy.mockRestore(); + }); + test('should calculate searchable page count from addressablePages', async () => { const json = { ...PAGE_NJK, From 814903adfc38233ee74fa67d228183a44fdedf1f Mon Sep 17 00:00:00 2001 From: Thaddaeus Chua Date: Fri, 10 Apr 2026 02:18:26 +0800 Subject: [PATCH 4/7] Add enablePagefind option --- packages/core/src/Site/SiteConfig.ts | 1 + packages/core/src/Site/SiteGenerationManager.ts | 2 +- packages/core/src/Site/SitePagesManager.ts | 4 ++-- .../test/unit/Site/SiteGenerationManager.test.ts | 12 ++++++------ 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/core/src/Site/SiteConfig.ts b/packages/core/src/Site/SiteConfig.ts index 512660efd3..9b6536bdd7 100644 --- a/packages/core/src/Site/SiteConfig.ts +++ b/packages/core/src/Site/SiteConfig.ts @@ -66,6 +66,7 @@ export class SiteConfig { plantumlCheck: boolean; pagefind?: { + enablePagefind?: boolean; exclude_selectors?: string[]; }; diff --git a/packages/core/src/Site/SiteGenerationManager.ts b/packages/core/src/Site/SiteGenerationManager.ts index 99ec8b9745..a37164bb03 100644 --- a/packages/core/src/Site/SiteGenerationManager.ts +++ b/packages/core/src/Site/SiteGenerationManager.ts @@ -316,7 +316,7 @@ export class SiteGenerationManager { await this.siteAssets.copyOcticonsAsset(); await this.siteAssets.copyMaterialIconsAsset(); await this.writeSiteData(); - if (this.siteConfig.enableSearch) { + if (this.siteConfig.pagefind?.enablePagefind) { const indexingSucceeded = await this.indexSiteWithPagefind(); this.sitePages.pagefindIndexingSucceeded = indexingSucceeded; } diff --git a/packages/core/src/Site/SitePagesManager.ts b/packages/core/src/Site/SitePagesManager.ts index a90020a818..a75520e592 100644 --- a/packages/core/src/Site/SitePagesManager.ts +++ b/packages/core/src/Site/SitePagesManager.ts @@ -149,10 +149,10 @@ export class SitePagesManager { ? 'https://cdn.jsdelivr.net/npm/vue@3.3.11/dist/vue.global.min.js' : path.posix.join(baseAssetsPath, 'js', 'vue.global.prod.min.js'), layoutUserScriptsAndStyles: [], - pagefindJs: this.siteConfig.enableSearch && this.pagefindIndexingSucceeded + pagefindJs: this.siteConfig.pagefind?.enablePagefind && this.pagefindIndexingSucceeded ? path.posix.join(baseAssetsPath, 'pagefind', 'pagefind.js') : undefined, - pagefindLazyLoaderJs: this.siteConfig.enableSearch && this.pagefindIndexingSucceeded + pagefindLazyLoaderJs: this.siteConfig.pagefind?.enablePagefind && this.pagefindIndexingSucceeded ? path.posix.join(baseAssetsPath, 'js', 'pagefind-lazyloader.js') : undefined, }, diff --git a/packages/core/test/unit/Site/SiteGenerationManager.test.ts b/packages/core/test/unit/Site/SiteGenerationManager.test.ts index d673529663..20e8ae1ef1 100644 --- a/packages/core/test/unit/Site/SiteGenerationManager.test.ts +++ b/packages/core/test/unit/Site/SiteGenerationManager.test.ts @@ -188,7 +188,7 @@ describe('SiteGenerationManager', () => { mockPagefindInstance.createIndex({}) as any, ); - generationManager.siteConfig = { enableSearch: true, pages: [] } as any; + generationManager.siteConfig = { pagefind: { enablePagefind: true }, pages: [] } as any; const pageConfig = { resultPath: path.join(outputPath, 'index.html'), searchable: true }; generationManager.sitePages.pages = [{ pageConfig }] as any; @@ -220,7 +220,7 @@ describe('SiteGenerationManager', () => { ); const errorSpy = jest.spyOn(logger, 'error').mockImplementation(); - generationManager.siteConfig = { enableSearch: true, pages: [] } as any; + generationManager.siteConfig = { pagefind: { enablePagefind: true }, pages: [] } as any; const pageConfig2 = { resultPath: path.join(outputPath, 'index.html'), searchable: true }; generationManager.sitePages.pages = [{ pageConfig: pageConfig2 }] as any; @@ -305,7 +305,7 @@ describe('SiteGenerationManager', () => { ); const errorSpy = jest.spyOn(logger, 'error').mockImplementation(); - generationManagerOnePage.siteConfig = { enableSearch: true, pages: [] } as any; + generationManagerOnePage.siteConfig = { pagefind: { enablePagefind: true }, pages: [] } as any; generationManagerOnePage.sitePages.pages = [ { pageConfig: { resultPath: path.join(outputPath, 'index.html'), searchable: true } }, { pageConfig: { resultPath: path.join(outputPath, 'page2.html'), searchable: true } }, @@ -342,7 +342,7 @@ describe('SiteGenerationManager', () => { ); const infoSpy = jest.spyOn(logger, 'info').mockImplementation(); - generationManager.siteConfig = { enableSearch: true, pages: [] } as any; + generationManager.siteConfig = { pagefind: { enablePagefind: true }, pages: [] } as any; generationManager.sitePages.pages = [ { pageConfig: { resultPath: path.join(outputPath, 'index.html'), searchable: true } }, { pageConfig: { resultPath: path.join(outputPath, 'page1.html'), searchable: true } }, @@ -373,7 +373,7 @@ describe('SiteGenerationManager', () => { ); const infoSpy = jest.spyOn(logger, 'info').mockImplementation(); - generationManager.siteConfig = { enableSearch: true, pages: [] } as any; + generationManager.siteConfig = { pagefind: { enablePagefind: true }, pages: [] } as any; generationManager.sitePages.pages = [ { pageConfig: { resultPath: path.join(outputPath, 'index.html'), searchable: false } }, { pageConfig: { resultPath: path.join(outputPath, 'page1.html'), searchable: true } }, @@ -402,7 +402,7 @@ describe('SiteGenerationManager', () => { ); const infoSpy = jest.spyOn(logger, 'info').mockImplementation(); - generationManager.siteConfig = { enableSearch: true } as any; + generationManager.siteConfig = { pagefind: { enablePagefind: true } } as any; generationManager.sitePages.addressablePages = [{ src: 'index.md', searchable: false }]; await generationManager.indexSiteWithPagefind(); From 9ce20aefe7f0cfa1429037c482328c588638b7f0 Mon Sep 17 00:00:00 2001 From: Thaddaeus Chua Date: Sat, 11 Apr 2026 13:14:31 +0800 Subject: [PATCH 5/7] Update functional tests --- packages/cli/test/functional/test_site/expected/bugs/index.html | 1 - packages/cli/test/functional/test_site/expected/index.html | 1 - .../cli/test/functional/test_site/expected/sub_site/index.html | 1 - .../test_site/expected/sub_site/nested_sub_site/index.html | 1 - .../sub_site/nested_sub_site/testNunjucksPathResolving.html | 1 - .../test_site/expected/sub_site/testNunjucksPathResolving.html | 1 - .../test_site/expected/testAltFrontMatterInvalidKeyValue.html | 1 - .../test_site/expected/testAltFrontMatterParsing.html | 1 - .../functional/test_site/expected/testAnchorGeneration.html | 1 - .../cli/test/functional/test_site/expected/testAnnotate.html | 1 - .../test/functional/test_site/expected/testAntiFOUCStyles.html | 1 - .../functional/test_site/expected/testBootstrapIconInPage.html | 1 - .../cli/test/functional/test_site/expected/testCenterText.html | 1 - .../cli/test/functional/test_site/expected/testCodeBlocks.html | 1 - packages/cli/test/functional/test_site/expected/testDates.html | 1 - .../functional/test_site/expected/testEmptyAltFrontMatter.html | 1 - .../functional/test_site/expected/testEmptyFrontmatter.html | 1 - .../test/functional/test_site/expected/testExternalScripts.html | 1 - .../functional/test_site/expected/testFontAwesomeInPage.html | 1 - .../test/functional/test_site/expected/testGlyphiconInPage.html | 1 - packages/cli/test/functional/test_site/expected/testHr.html | 1 - .../functional/test_site/expected/testIconsInSiteLayout.html | 1 - packages/cli/test/functional/test_site/expected/testImages.html | 1 - .../functional/test_site/expected/testIncludeBoilerplate.html | 1 - .../test_site/expected/testIncludeMultipleModals.html | 1 - .../test_site/expected/testIncludePluginsRendered.html | 1 - .../cli/test/functional/test_site/expected/testLayouts.html | 1 - .../test/functional/test_site/expected/testLayoutsOverride.html | 1 - .../expected/testLayoutsOverrideWithAltFrontmatter.html | 1 - .../test_site/expected/testLayoutsWithAltFrontMatter.html | 1 - packages/cli/test/functional/test_site/expected/testLinks.html | 1 - packages/cli/test/functional/test_site/expected/testList.html | 1 - .../functional/test_site/expected/testMaterialIconsInPage.html | 1 - packages/cli/test/functional/test_site/expected/testMath.html | 1 - .../cli/test/functional/test_site/expected/testMermaid.html | 1 - packages/cli/test/functional/test_site/expected/testModals.html | 1 - .../test_site/expected/testNunjucksPathResolving.html | 1 - .../test/functional/test_site/expected/testOcticonInPage.html | 1 - .../cli/test/functional/test_site/expected/testPageNav.html | 1 - .../test/functional/test_site/expected/testPageNavPrint.html | 1 - .../test/functional/test_site/expected/testPageNavTarget.html | 1 - .../functional/test_site/expected/testPageNavWithOnlyTitle.html | 1 - .../expected/testPageNavWithoutTitleAndNavHeadings.html | 1 - .../functional/test_site/expected/testPanelMarkdownParsing.html | 1 - packages/cli/test/functional/test_site/expected/testPanels.html | 1 - .../test_site/expected/testPanelsClosingTransition.html | 1 - .../cli/test/functional/test_site/expected/testPlantUML.html | 1 - .../test/functional/test_site/expected/testPopoverTrigger.html | 1 - .../cli/test/functional/test_site/expected/testPopovers.html | 1 - .../functional/test_site/expected/testSingleAltFrontMatter.html | 1 - .../functional/test_site/expected/testSourceContainScript.html | 1 - .../cli/test/functional/test_site/expected/testThumbnails.html | 1 - .../test/functional/test_site/expected/testTooltipSpacing.html | 1 - packages/cli/test/functional/test_site/expected/testTree.html | 1 - .../test_site/expected/testVariableContainsInclude.html | 1 - .../test/functional/test_site/expected/testWeb3FormPlugin.html | 1 - .../test/functional/test_site/expected/test_md_fragment.html | 1 - .../functional/test_site_algolia_plugin/expected/index.html | 2 -- .../test_site_convert/test_basic_convert/expected/404.html | 1 - .../test_site_convert/test_basic_convert/expected/Home.html | 1 - .../test_site_convert/test_basic_convert/expected/Page-1.html | 1 - .../test_site_convert/test_basic_convert/expected/_Footer.html | 1 - .../test_site_convert/test_basic_convert/expected/_Sidebar.html | 1 - .../test_site_convert/test_basic_convert/expected/about.html | 1 - .../test_basic_convert/expected/contents/topic1.html | 1 - .../test_basic_convert/expected/contents/topic2.html | 1 - .../test_basic_convert/expected/contents/topic3a.html | 1 - .../test_basic_convert/expected/contents/topic3b.html | 1 - .../test_site_convert/test_basic_convert/expected/index.html | 1 - .../test_site_convert/test_navigation_convert/expected/404.html | 1 - .../test_navigation_convert/expected/Home.html | 1 - .../test_navigation_convert/expected/Page-1.html | 1 - .../test_navigation_convert/expected/README.html | 1 - .../test_navigation_convert/expected/about.html | 1 - .../test_navigation_convert/expected/contents/topic1.html | 1 - .../test_navigation_convert/expected/contents/topic2.html | 1 - .../test_navigation_convert/expected/contents/topic3a.html | 1 - .../test_navigation_convert/expected/contents/topic3b.html | 1 - .../test_navigation_convert/expected/index.html | 1 - .../test_navigation_convert/expected/test_folder/extra_1.html | 1 - .../test_navigation_convert/expected/test_folder/extra_2.html | 1 - .../test_navigation_convert/expected/test_folder/extra_3.html | 1 - .../functional/test_site_custom_plugins/expected/index.html | 1 - .../test/functional/test_site_special_tags/expected/index.html | 1 - .../test/functional/test_site_table_plugin/expected/index.html | 2 -- .../test_site_templates/test_default/expected/404.html | 1 - .../test_default/expected/contents/topic1.html | 1 - .../test_default/expected/contents/topic2.html | 1 - .../test_default/expected/contents/topic3a.html | 1 - .../test_default/expected/contents/topic3b.html | 1 - .../test_site_templates/test_default/expected/index.html | 1 - .../test_site_templates/test_minimal/expected/index.html | 1 - .../test_site_templates/test_portfolio/expected/index.html | 1 - .../test_project/expected/developerGuide/Configuration.html | 1 - .../test_project/expected/developerGuide/Design.html | 1 - .../test_project/expected/developerGuide/DevOps.html | 1 - .../test_project/expected/developerGuide/DeveloperGuide.html | 1 - .../test_project/expected/developerGuide/Documentation.html | 1 - .../test_project/expected/developerGuide/Implementation.html | 1 - .../test_project/expected/developerGuide/Requirements.html | 1 - .../test_project/expected/developerGuide/SettingUp.html | 1 - .../test_project/expected/developerGuide/Testing.html | 1 - .../test_project/expected/developerGuide/TracingCode.html | 1 - .../test_site_templates/test_project/expected/index.html | 1 - .../test_site_templates/test_project/expected/team/AboutUs.html | 1 - .../test_site_templates/test_project/expected/team/johndoe.html | 1 - .../test_project/expected/userGuide/FAQ.html | 1 - .../test_project/expected/userGuide/Features.html | 1 - .../test_project/expected/userGuide/QuickStart.html | 1 - .../test_project/expected/userGuide/UserGuide.html | 1 - 110 files changed, 112 deletions(-) diff --git a/packages/cli/test/functional/test_site/expected/bugs/index.html b/packages/cli/test/functional/test_site/expected/bugs/index.html index 3722de44da..d79d575b29 100644 --- a/packages/cli/test/functional/test_site/expected/bugs/index.html +++ b/packages/cli/test/functional/test_site/expected/bugs/index.html @@ -364,6 +364,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/index.html b/packages/cli/test/functional/test_site/expected/index.html index d0597ba536..d07b958c33 100644 --- a/packages/cli/test/functional/test_site/expected/index.html +++ b/packages/cli/test/functional/test_site/expected/index.html @@ -1030,6 +1030,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/sub_site/index.html b/packages/cli/test/functional/test_site/expected/sub_site/index.html index 46c2b78be8..6b02ba0da6 100644 --- a/packages/cli/test/functional/test_site/expected/sub_site/index.html +++ b/packages/cli/test/functional/test_site/expected/sub_site/index.html @@ -371,6 +371,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/sub_site/nested_sub_site/index.html b/packages/cli/test/functional/test_site/expected/sub_site/nested_sub_site/index.html index 3f72b66397..bae6a9d701 100644 --- a/packages/cli/test/functional/test_site/expected/sub_site/nested_sub_site/index.html +++ b/packages/cli/test/functional/test_site/expected/sub_site/nested_sub_site/index.html @@ -362,6 +362,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/sub_site/nested_sub_site/testNunjucksPathResolving.html b/packages/cli/test/functional/test_site/expected/sub_site/nested_sub_site/testNunjucksPathResolving.html index e05b0b2be6..773d37d052 100644 --- a/packages/cli/test/functional/test_site/expected/sub_site/nested_sub_site/testNunjucksPathResolving.html +++ b/packages/cli/test/functional/test_site/expected/sub_site/nested_sub_site/testNunjucksPathResolving.html @@ -368,6 +368,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/sub_site/testNunjucksPathResolving.html b/packages/cli/test/functional/test_site/expected/sub_site/testNunjucksPathResolving.html index e05b0b2be6..773d37d052 100644 --- a/packages/cli/test/functional/test_site/expected/sub_site/testNunjucksPathResolving.html +++ b/packages/cli/test/functional/test_site/expected/sub_site/testNunjucksPathResolving.html @@ -368,6 +368,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testAltFrontMatterInvalidKeyValue.html b/packages/cli/test/functional/test_site/expected/testAltFrontMatterInvalidKeyValue.html index 64f4844750..4a9bee3629 100644 --- a/packages/cli/test/functional/test_site/expected/testAltFrontMatterInvalidKeyValue.html +++ b/packages/cli/test/functional/test_site/expected/testAltFrontMatterInvalidKeyValue.html @@ -362,6 +362,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testAltFrontMatterParsing.html b/packages/cli/test/functional/test_site/expected/testAltFrontMatterParsing.html index 42ef7bbdc8..82b08f209e 100644 --- a/packages/cli/test/functional/test_site/expected/testAltFrontMatterParsing.html +++ b/packages/cli/test/functional/test_site/expected/testAltFrontMatterParsing.html @@ -361,6 +361,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testAnchorGeneration.html b/packages/cli/test/functional/test_site/expected/testAnchorGeneration.html index f629536282..abad51a36f 100644 --- a/packages/cli/test/functional/test_site/expected/testAnchorGeneration.html +++ b/packages/cli/test/functional/test_site/expected/testAnchorGeneration.html @@ -430,6 +430,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testAnnotate.html b/packages/cli/test/functional/test_site/expected/testAnnotate.html index 4530e97f55..ed6b100ed8 100644 --- a/packages/cli/test/functional/test_site/expected/testAnnotate.html +++ b/packages/cli/test/functional/test_site/expected/testAnnotate.html @@ -541,6 +541,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testAntiFOUCStyles.html b/packages/cli/test/functional/test_site/expected/testAntiFOUCStyles.html index 03f94d9ada..e517dad086 100644 --- a/packages/cli/test/functional/test_site/expected/testAntiFOUCStyles.html +++ b/packages/cli/test/functional/test_site/expected/testAntiFOUCStyles.html @@ -385,6 +385,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testBootstrapIconInPage.html b/packages/cli/test/functional/test_site/expected/testBootstrapIconInPage.html index 5adba1b75e..8b61659cbd 100644 --- a/packages/cli/test/functional/test_site/expected/testBootstrapIconInPage.html +++ b/packages/cli/test/functional/test_site/expected/testBootstrapIconInPage.html @@ -200,6 +200,5 @@ }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testCenterText.html b/packages/cli/test/functional/test_site/expected/testCenterText.html index 8ca6adf733..6853cb5eb1 100644 --- a/packages/cli/test/functional/test_site/expected/testCenterText.html +++ b/packages/cli/test/functional/test_site/expected/testCenterText.html @@ -361,6 +361,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testCodeBlocks.html b/packages/cli/test/functional/test_site/expected/testCodeBlocks.html index 32b3f03e12..c13340f9b9 100644 --- a/packages/cli/test/functional/test_site/expected/testCodeBlocks.html +++ b/packages/cli/test/functional/test_site/expected/testCodeBlocks.html @@ -584,6 +584,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testDates.html b/packages/cli/test/functional/test_site/expected/testDates.html index 3f8b4a0f5a..fb4e4eb8f8 100644 --- a/packages/cli/test/functional/test_site/expected/testDates.html +++ b/packages/cli/test/functional/test_site/expected/testDates.html @@ -367,6 +367,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testEmptyAltFrontMatter.html b/packages/cli/test/functional/test_site/expected/testEmptyAltFrontMatter.html index a781a27b28..dd3f07b0d4 100644 --- a/packages/cli/test/functional/test_site/expected/testEmptyAltFrontMatter.html +++ b/packages/cli/test/functional/test_site/expected/testEmptyAltFrontMatter.html @@ -368,6 +368,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testEmptyFrontmatter.html b/packages/cli/test/functional/test_site/expected/testEmptyFrontmatter.html index a2117a2f78..4af0ef2607 100644 --- a/packages/cli/test/functional/test_site/expected/testEmptyFrontmatter.html +++ b/packages/cli/test/functional/test_site/expected/testEmptyFrontmatter.html @@ -368,6 +368,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testExternalScripts.html b/packages/cli/test/functional/test_site/expected/testExternalScripts.html index 28dd62ee25..f63a6ca599 100644 --- a/packages/cli/test/functional/test_site/expected/testExternalScripts.html +++ b/packages/cli/test/functional/test_site/expected/testExternalScripts.html @@ -372,6 +372,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testFontAwesomeInPage.html b/packages/cli/test/functional/test_site/expected/testFontAwesomeInPage.html index 632d2dd031..3afded08ba 100644 --- a/packages/cli/test/functional/test_site/expected/testFontAwesomeInPage.html +++ b/packages/cli/test/functional/test_site/expected/testFontAwesomeInPage.html @@ -205,6 +205,5 @@ }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testGlyphiconInPage.html b/packages/cli/test/functional/test_site/expected/testGlyphiconInPage.html index 9594601547..329551dcc6 100644 --- a/packages/cli/test/functional/test_site/expected/testGlyphiconInPage.html +++ b/packages/cli/test/functional/test_site/expected/testGlyphiconInPage.html @@ -202,6 +202,5 @@ }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testHr.html b/packages/cli/test/functional/test_site/expected/testHr.html index f23b4b1982..cd096a38d0 100644 --- a/packages/cli/test/functional/test_site/expected/testHr.html +++ b/packages/cli/test/functional/test_site/expected/testHr.html @@ -370,6 +370,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testIconsInSiteLayout.html b/packages/cli/test/functional/test_site/expected/testIconsInSiteLayout.html index f3162f3964..93854f7518 100644 --- a/packages/cli/test/functional/test_site/expected/testIconsInSiteLayout.html +++ b/packages/cli/test/functional/test_site/expected/testIconsInSiteLayout.html @@ -204,6 +204,5 @@ }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testImages.html b/packages/cli/test/functional/test_site/expected/testImages.html index 846bc80ef3..a54b5a723d 100644 --- a/packages/cli/test/functional/test_site/expected/testImages.html +++ b/packages/cli/test/functional/test_site/expected/testImages.html @@ -368,6 +368,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testIncludeBoilerplate.html b/packages/cli/test/functional/test_site/expected/testIncludeBoilerplate.html index ceafa25353..8dac598de1 100644 --- a/packages/cli/test/functional/test_site/expected/testIncludeBoilerplate.html +++ b/packages/cli/test/functional/test_site/expected/testIncludeBoilerplate.html @@ -452,6 +452,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testIncludeMultipleModals.html b/packages/cli/test/functional/test_site/expected/testIncludeMultipleModals.html index 8f0ab9db37..828ccea3db 100644 --- a/packages/cli/test/functional/test_site/expected/testIncludeMultipleModals.html +++ b/packages/cli/test/functional/test_site/expected/testIncludeMultipleModals.html @@ -367,6 +367,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testIncludePluginsRendered.html b/packages/cli/test/functional/test_site/expected/testIncludePluginsRendered.html index e862efbcf4..c1da1d5f64 100644 --- a/packages/cli/test/functional/test_site/expected/testIncludePluginsRendered.html +++ b/packages/cli/test/functional/test_site/expected/testIncludePluginsRendered.html @@ -362,6 +362,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testLayouts.html b/packages/cli/test/functional/test_site/expected/testLayouts.html index 83768f39fe..f079e9e8fa 100644 --- a/packages/cli/test/functional/test_site/expected/testLayouts.html +++ b/packages/cli/test/functional/test_site/expected/testLayouts.html @@ -368,6 +368,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testLayoutsOverride.html b/packages/cli/test/functional/test_site/expected/testLayoutsOverride.html index 3dbe7a75fa..f2ee8c68c1 100644 --- a/packages/cli/test/functional/test_site/expected/testLayoutsOverride.html +++ b/packages/cli/test/functional/test_site/expected/testLayoutsOverride.html @@ -368,6 +368,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testLayoutsOverrideWithAltFrontmatter.html b/packages/cli/test/functional/test_site/expected/testLayoutsOverrideWithAltFrontmatter.html index c9e4e56720..9c52e2f080 100644 --- a/packages/cli/test/functional/test_site/expected/testLayoutsOverrideWithAltFrontmatter.html +++ b/packages/cli/test/functional/test_site/expected/testLayoutsOverrideWithAltFrontmatter.html @@ -368,6 +368,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testLayoutsWithAltFrontMatter.html b/packages/cli/test/functional/test_site/expected/testLayoutsWithAltFrontMatter.html index 6064badc8e..de284665ca 100644 --- a/packages/cli/test/functional/test_site/expected/testLayoutsWithAltFrontMatter.html +++ b/packages/cli/test/functional/test_site/expected/testLayoutsWithAltFrontMatter.html @@ -368,6 +368,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testLinks.html b/packages/cli/test/functional/test_site/expected/testLinks.html index b84f47747f..031ef350f2 100644 --- a/packages/cli/test/functional/test_site/expected/testLinks.html +++ b/packages/cli/test/functional/test_site/expected/testLinks.html @@ -371,6 +371,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testList.html b/packages/cli/test/functional/test_site/expected/testList.html index 25d076c0eb..229ba9deea 100644 --- a/packages/cli/test/functional/test_site/expected/testList.html +++ b/packages/cli/test/functional/test_site/expected/testList.html @@ -1299,6 +1299,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testMaterialIconsInPage.html b/packages/cli/test/functional/test_site/expected/testMaterialIconsInPage.html index 24c284cf74..0f8057658c 100644 --- a/packages/cli/test/functional/test_site/expected/testMaterialIconsInPage.html +++ b/packages/cli/test/functional/test_site/expected/testMaterialIconsInPage.html @@ -200,6 +200,5 @@ }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testMath.html b/packages/cli/test/functional/test_site/expected/testMath.html index ebe197fc79..a24db99c79 100644 --- a/packages/cli/test/functional/test_site/expected/testMath.html +++ b/packages/cli/test/functional/test_site/expected/testMath.html @@ -627,6 +627,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testMermaid.html b/packages/cli/test/functional/test_site/expected/testMermaid.html index e3f02c7c60..a214f9c8eb 100644 --- a/packages/cli/test/functional/test_site/expected/testMermaid.html +++ b/packages/cli/test/functional/test_site/expected/testMermaid.html @@ -446,6 +446,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testModals.html b/packages/cli/test/functional/test_site/expected/testModals.html index 2298934be0..65393886b5 100644 --- a/packages/cli/test/functional/test_site/expected/testModals.html +++ b/packages/cli/test/functional/test_site/expected/testModals.html @@ -503,6 +503,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testNunjucksPathResolving.html b/packages/cli/test/functional/test_site/expected/testNunjucksPathResolving.html index e05b0b2be6..773d37d052 100644 --- a/packages/cli/test/functional/test_site/expected/testNunjucksPathResolving.html +++ b/packages/cli/test/functional/test_site/expected/testNunjucksPathResolving.html @@ -368,6 +368,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testOcticonInPage.html b/packages/cli/test/functional/test_site/expected/testOcticonInPage.html index 477941731d..ae5db8fce1 100644 --- a/packages/cli/test/functional/test_site/expected/testOcticonInPage.html +++ b/packages/cli/test/functional/test_site/expected/testOcticonInPage.html @@ -213,6 +213,5 @@ }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testPageNav.html b/packages/cli/test/functional/test_site/expected/testPageNav.html index b176f983da..dc6b17d01b 100644 --- a/packages/cli/test/functional/test_site/expected/testPageNav.html +++ b/packages/cli/test/functional/test_site/expected/testPageNav.html @@ -376,6 +376,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testPageNavPrint.html b/packages/cli/test/functional/test_site/expected/testPageNavPrint.html index bfbd780c8f..e98cfa18a4 100644 --- a/packages/cli/test/functional/test_site/expected/testPageNavPrint.html +++ b/packages/cli/test/functional/test_site/expected/testPageNavPrint.html @@ -374,6 +374,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testPageNavTarget.html b/packages/cli/test/functional/test_site/expected/testPageNavTarget.html index 922c32ea0d..be69fa90b4 100644 --- a/packages/cli/test/functional/test_site/expected/testPageNavTarget.html +++ b/packages/cli/test/functional/test_site/expected/testPageNavTarget.html @@ -364,6 +364,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testPageNavWithOnlyTitle.html b/packages/cli/test/functional/test_site/expected/testPageNavWithOnlyTitle.html index e307304d80..ed90fb65b6 100644 --- a/packages/cli/test/functional/test_site/expected/testPageNavWithOnlyTitle.html +++ b/packages/cli/test/functional/test_site/expected/testPageNavWithOnlyTitle.html @@ -364,6 +364,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testPageNavWithoutTitleAndNavHeadings.html b/packages/cli/test/functional/test_site/expected/testPageNavWithoutTitleAndNavHeadings.html index fb6b889930..31c74d196a 100644 --- a/packages/cli/test/functional/test_site/expected/testPageNavWithoutTitleAndNavHeadings.html +++ b/packages/cli/test/functional/test_site/expected/testPageNavWithoutTitleAndNavHeadings.html @@ -360,6 +360,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testPanelMarkdownParsing.html b/packages/cli/test/functional/test_site/expected/testPanelMarkdownParsing.html index 93f642fe06..84127eae68 100644 --- a/packages/cli/test/functional/test_site/expected/testPanelMarkdownParsing.html +++ b/packages/cli/test/functional/test_site/expected/testPanelMarkdownParsing.html @@ -418,6 +418,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testPanels.html b/packages/cli/test/functional/test_site/expected/testPanels.html index 14c1de468c..1f14e9dc97 100644 --- a/packages/cli/test/functional/test_site/expected/testPanels.html +++ b/packages/cli/test/functional/test_site/expected/testPanels.html @@ -376,6 +376,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testPanelsClosingTransition.html b/packages/cli/test/functional/test_site/expected/testPanelsClosingTransition.html index 4318696c15..09e721db85 100644 --- a/packages/cli/test/functional/test_site/expected/testPanelsClosingTransition.html +++ b/packages/cli/test/functional/test_site/expected/testPanelsClosingTransition.html @@ -404,6 +404,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testPlantUML.html b/packages/cli/test/functional/test_site/expected/testPlantUML.html index e3b2263d28..734d954422 100644 --- a/packages/cli/test/functional/test_site/expected/testPlantUML.html +++ b/packages/cli/test/functional/test_site/expected/testPlantUML.html @@ -366,6 +366,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testPopoverTrigger.html b/packages/cli/test/functional/test_site/expected/testPopoverTrigger.html index b8064aa88e..b5df532b62 100644 --- a/packages/cli/test/functional/test_site/expected/testPopoverTrigger.html +++ b/packages/cli/test/functional/test_site/expected/testPopoverTrigger.html @@ -366,6 +366,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testPopovers.html b/packages/cli/test/functional/test_site/expected/testPopovers.html index 3ea7b40ab0..1b41cdfd0f 100644 --- a/packages/cli/test/functional/test_site/expected/testPopovers.html +++ b/packages/cli/test/functional/test_site/expected/testPopovers.html @@ -451,6 +451,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testSingleAltFrontMatter.html b/packages/cli/test/functional/test_site/expected/testSingleAltFrontMatter.html index 441c0ee2fb..0bdabe75b0 100644 --- a/packages/cli/test/functional/test_site/expected/testSingleAltFrontMatter.html +++ b/packages/cli/test/functional/test_site/expected/testSingleAltFrontMatter.html @@ -359,6 +359,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testSourceContainScript.html b/packages/cli/test/functional/test_site/expected/testSourceContainScript.html index c87ff8d8c2..d4697e888f 100644 --- a/packages/cli/test/functional/test_site/expected/testSourceContainScript.html +++ b/packages/cli/test/functional/test_site/expected/testSourceContainScript.html @@ -370,6 +370,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testThumbnails.html b/packages/cli/test/functional/test_site/expected/testThumbnails.html index 753764c642..9b7f133c09 100644 --- a/packages/cli/test/functional/test_site/expected/testThumbnails.html +++ b/packages/cli/test/functional/test_site/expected/testThumbnails.html @@ -458,6 +458,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testTooltipSpacing.html b/packages/cli/test/functional/test_site/expected/testTooltipSpacing.html index a2a111a0ea..830bf7712d 100644 --- a/packages/cli/test/functional/test_site/expected/testTooltipSpacing.html +++ b/packages/cli/test/functional/test_site/expected/testTooltipSpacing.html @@ -368,6 +368,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testTree.html b/packages/cli/test/functional/test_site/expected/testTree.html index ec615f1686..17d6c50f92 100644 --- a/packages/cli/test/functional/test_site/expected/testTree.html +++ b/packages/cli/test/functional/test_site/expected/testTree.html @@ -433,6 +433,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testVariableContainsInclude.html b/packages/cli/test/functional/test_site/expected/testVariableContainsInclude.html index 5ab8ebee62..364c04cae7 100644 --- a/packages/cli/test/functional/test_site/expected/testVariableContainsInclude.html +++ b/packages/cli/test/functional/test_site/expected/testVariableContainsInclude.html @@ -358,6 +358,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/testWeb3FormPlugin.html b/packages/cli/test/functional/test_site/expected/testWeb3FormPlugin.html index 82efd8e0cd..2af8f61a11 100644 --- a/packages/cli/test/functional/test_site/expected/testWeb3FormPlugin.html +++ b/packages/cli/test/functional/test_site/expected/testWeb3FormPlugin.html @@ -412,6 +412,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site/expected/test_md_fragment.html b/packages/cli/test/functional/test_site/expected/test_md_fragment.html index d3dd723488..412afb92b0 100644 --- a/packages/cli/test/functional/test_site/expected/test_md_fragment.html +++ b/packages/cli/test/functional/test_site/expected/test_md_fragment.html @@ -358,6 +358,5 @@

Heading in footer should not be }); - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_algolia_plugin/expected/index.html b/packages/cli/test/functional/test_site_algolia_plugin/expected/index.html index ef7f1260a7..6eb4da46a4 100644 --- a/packages/cli/test/functional/test_site_algolia_plugin/expected/index.html +++ b/packages/cli/test/functional/test_site_algolia_plugin/expected/index.html @@ -143,6 +143,4 @@ document.getElementsByTagName('head')[0].appendChild(style); - - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/404.html b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/404.html index 17fc9559a9..b375f067e8 100644 --- a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/404.html +++ b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/404.html @@ -87,6 +87,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/Home.html b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/Home.html index 6d39ed5654..cf72129ebb 100644 --- a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/Home.html +++ b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/Home.html @@ -78,6 +78,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/Page-1.html b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/Page-1.html index 82eeae2cad..a709b7e093 100644 --- a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/Page-1.html +++ b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/Page-1.html @@ -78,6 +78,5 @@

Page 1 \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/_Footer.html b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/_Footer.html index e9b0cd225b..6d30656cc4 100644 --- a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/_Footer.html +++ b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/_Footer.html @@ -78,6 +78,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/_Sidebar.html b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/_Sidebar.html index c50f061a80..6f157eb297 100644 --- a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/_Sidebar.html +++ b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/_Sidebar.html @@ -81,6 +81,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/about.html b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/about.html index 50993ca4dc..2be457124d 100644 --- a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/about.html +++ b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/about.html @@ -79,6 +79,5 @@

About \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic1.html b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic1.html index 98c73e1abe..9c338a5e6a 100644 --- a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic1.html +++ b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic1.html @@ -83,6 +83,5 @@

Topic 1 \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic2.html b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic2.html index 5f5b04b998..10596687d1 100644 --- a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic2.html +++ b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic2.html @@ -82,6 +82,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic3a.html b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic3a.html index 0fb2854c54..6285e51dac 100644 --- a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic3a.html +++ b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic3a.html @@ -82,6 +82,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic3b.html b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic3b.html index 766a9c0033..dcf750e805 100644 --- a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic3b.html +++ b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/contents/topic3b.html @@ -82,6 +82,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/index.html b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/index.html index ac7ba4240d..1f01e0117c 100644 --- a/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/index.html +++ b/packages/cli/test/functional/test_site_convert/test_basic_convert/expected/index.html @@ -78,6 +78,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/404.html b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/404.html index 6d4b446103..f251c0529b 100644 --- a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/404.html +++ b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/404.html @@ -124,6 +124,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/Home.html b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/Home.html index 6842b05e11..65a45f4aa1 100644 --- a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/Home.html +++ b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/Home.html @@ -115,6 +115,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/Page-1.html b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/Page-1.html index 8eaa6b61b9..b606fd3e58 100644 --- a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/Page-1.html +++ b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/Page-1.html @@ -115,6 +115,5 @@

Page 1 \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/README.html b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/README.html index 37f25590b7..eedb604b32 100644 --- a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/README.html +++ b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/README.html @@ -116,6 +116,5 @@

Readme \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/about.html b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/about.html index 76ef777eb3..b00e41177b 100644 --- a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/about.html +++ b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/about.html @@ -116,6 +116,5 @@

About \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic1.html b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic1.html index c656c81cb7..910db630df 100644 --- a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic1.html +++ b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic1.html @@ -119,6 +119,5 @@

Topic 1 \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic2.html b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic2.html index 3f26e2deb6..2aca46f28b 100644 --- a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic2.html +++ b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic2.html @@ -118,6 +118,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic3a.html b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic3a.html index 70020406d7..ff9f48a942 100644 --- a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic3a.html +++ b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic3a.html @@ -118,6 +118,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic3b.html b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic3b.html index e9c0f8c931..ffe73bc2c3 100644 --- a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic3b.html +++ b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/contents/topic3b.html @@ -118,6 +118,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/index.html b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/index.html index 6f93851022..823b898ec2 100644 --- a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/index.html +++ b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/index.html @@ -116,6 +116,5 @@

Readme \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/test_folder/extra_1.html b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/test_folder/extra_1.html index b27ad0f8bf..dcc41673f5 100644 --- a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/test_folder/extra_1.html +++ b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/test_folder/extra_1.html @@ -115,6 +115,5 @@

Sample cont - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/test_folder/extra_2.html b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/test_folder/extra_2.html index c1f131cfdc..ce8f1cc785 100644 --- a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/test_folder/extra_2.html +++ b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/test_folder/extra_2.html @@ -115,6 +115,5 @@

Sample cont - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/test_folder/extra_3.html b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/test_folder/extra_3.html index 332ce11f6c..8bce8650bf 100644 --- a/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/test_folder/extra_3.html +++ b/packages/cli/test/functional/test_site_convert/test_navigation_convert/expected/test_folder/extra_3.html @@ -115,6 +115,5 @@

Sample cont - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_custom_plugins/expected/index.html b/packages/cli/test/functional/test_site_custom_plugins/expected/index.html index 66f1ba48c8..b07e6a3dc4 100644 --- a/packages/cli/test/functional/test_site_custom_plugins/expected/index.html +++ b/packages/cli/test/functional/test_site_custom_plugins/expected/index.html @@ -56,6 +56,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_special_tags/expected/index.html b/packages/cli/test/functional/test_site_special_tags/expected/index.html index 18fd6ed1c8..70ac200e50 100644 --- a/packages/cli/test/functional/test_site_special_tags/expected/index.html +++ b/packages/cli/test/functional/test_site_special_tags/expected/index.html @@ -109,6 +109,5 @@

So far as to comply with t - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_table_plugin/expected/index.html b/packages/cli/test/functional/test_site_table_plugin/expected/index.html index a77a9db44b..6f25bde932 100644 --- a/packages/cli/test/functional/test_site_table_plugin/expected/index.html +++ b/packages/cli/test/functional/test_site_table_plugin/expected/index.html @@ -435,6 +435,4 @@

Welcome to MarkBind - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_default/expected/404.html b/packages/cli/test/functional/test_site_templates/test_default/expected/404.html index 9c2866a466..63222ab2c7 100644 --- a/packages/cli/test/functional/test_site_templates/test_default/expected/404.html +++ b/packages/cli/test/functional/test_site_templates/test_default/expected/404.html @@ -44,6 +44,5 @@ - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic1.html b/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic1.html index 95a387b119..1653eb465d 100644 --- a/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic1.html +++ b/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic1.html @@ -114,6 +114,5 @@

Topic 1 \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic2.html b/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic2.html index ee506121a3..ba996f6ea4 100644 --- a/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic2.html +++ b/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic2.html @@ -114,6 +114,5 @@

Topic 2 \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic3a.html b/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic3a.html index c927d9c05a..13f182db95 100644 --- a/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic3a.html +++ b/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic3a.html @@ -114,6 +114,5 @@

Topic 3a \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic3b.html b/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic3b.html index 8b07cc3b3d..a17f978a2e 100644 --- a/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic3b.html +++ b/packages/cli/test/functional/test_site_templates/test_default/expected/contents/topic3b.html @@ -114,6 +114,5 @@

Topic 3b \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_default/expected/index.html b/packages/cli/test/functional/test_site_templates/test_default/expected/index.html index d557540fb8..b5b39d195c 100644 --- a/packages/cli/test/functional/test_site_templates/test_default/expected/index.html +++ b/packages/cli/test/functional/test_site_templates/test_default/expected/index.html @@ -162,6 +162,5 @@
User Guide: Syntax Overview - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_minimal/expected/index.html b/packages/cli/test/functional/test_site_templates/test_minimal/expected/index.html index 19add8a5df..5d84215604 100644 --- a/packages/cli/test/functional/test_site_templates/test_minimal/expected/index.html +++ b/packages/cli/test/functional/test_site_templates/test_minimal/expected/index.html @@ -44,6 +44,5 @@

Welcome to MarkBind \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_portfolio/expected/index.html b/packages/cli/test/functional/test_site_templates/test_portfolio/expected/index.html index 02e65b3460..8dd9ed47eb 100644 --- a/packages/cli/test/functional/test_site_templates/test_portfolio/expected/index.html +++ b/packages/cli/test/functional/test_site_templates/test_portfolio/expected/index.html @@ -256,6 +256,5 @@

Project title \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Configuration.html b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Configuration.html index c2190d1c38..38730000f7 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Configuration.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Configuration.html @@ -183,6 +183,5 @@

Configuration guide \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Design.html b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Design.html index 87af49744a..fd27453ae8 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Design.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Design.html @@ -277,6 +277,5 @@

Component 2 MarkBind.setupWithSearch() - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/DevOps.html b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/DevOps.html index 78b5e16b1f..6ae4c61dff 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/DevOps.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/DevOps.html @@ -240,6 +240,5 @@

Making a release \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/DeveloperGuide.html b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/DeveloperGuide.html index cf3387cf7e..8acda8ce75 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/DeveloperGuide.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/DeveloperGuide.html @@ -201,6 +201,5 @@

Acknowledgements \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Documentation.html b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Documentation.html index cdac82907d..6523fa4a91 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Documentation.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Documentation.html @@ -204,6 +204,5 @@

Documentation Guide \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Implementation.html b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Implementation.html index e70c951654..d6a6952753 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Implementation.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Implementation.html @@ -226,6 +226,5 @@

[Proposed] Data archiving \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Requirements.html b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Requirements.html index 82036b852b..2b4357d267 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Requirements.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Requirements.html @@ -282,6 +282,5 @@

Non-Functional Requirements \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/SettingUp.html b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/SettingUp.html index 9704116c23..ce60040450 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/SettingUp.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/SettingUp.html @@ -240,6 +240,5 @@

Before writing code \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Testing.html b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Testing.html index 04714bb46e..31babba600 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Testing.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/Testing.html @@ -215,6 +215,5 @@

Types of tests \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/TracingCode.html b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/TracingCode.html index 17c5820b77..db7c4ef877 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/TracingCode.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/developerGuide/TracingCode.html @@ -239,6 +239,5 @@

Tracing the execution path \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/index.html b/packages/cli/test/functional/test_site_templates/test_project/expected/index.html index d33a754d8c..51dfaefaf4 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/index.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/index.html @@ -200,6 +200,5 @@

ProjectEx \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/team/AboutUs.html b/packages/cli/test/functional/test_site_templates/test_project/expected/team/AboutUs.html index 0d31afd74c..06a95be79a 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/team/AboutUs.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/team/AboutUs.html @@ -228,6 +228,5 @@

James Doe \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/team/johndoe.html b/packages/cli/test/functional/test_site_templates/test_project/expected/team/johndoe.html index ba09f41fec..ce3add93b8 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/team/johndoe.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/team/johndoe.html @@ -246,6 +246,5 @@

Project: ProjectEx \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/FAQ.html b/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/FAQ.html index da2d09ec53..2167080adf 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/FAQ.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/FAQ.html @@ -198,6 +198,5 @@

FAQ \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/Features.html b/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/Features.html index afdecc8068..d332628b38 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/Features.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/Features.html @@ -230,6 +230,5 @@

Future Feature Z \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/QuickStart.html b/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/QuickStart.html index d9ad95c22b..02d444209b 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/QuickStart.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/QuickStart.html @@ -198,6 +198,5 @@

Quick start MarkBind.setupWithSearch() - \ No newline at end of file diff --git a/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/UserGuide.html b/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/UserGuide.html index 280425c508..713dfeef45 100644 --- a/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/UserGuide.html +++ b/packages/cli/test/functional/test_site_templates/test_project/expected/userGuide/UserGuide.html @@ -199,6 +199,5 @@

Purpose of this Guide MarkBind.setupWithSearch() - \ No newline at end of file From 229d7debc6b97b9c6c9c4ec7523e7f2c6ad576b8 Mon Sep 17 00:00:00 2001 From: Thaddaeus Chua Date: Mon, 13 Apr 2026 16:37:08 +0800 Subject: [PATCH 6/7] Add docs for enablePagefind --- docs/userGuide/makingTheSiteSearchable.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/userGuide/makingTheSiteSearchable.md b/docs/userGuide/makingTheSiteSearchable.md index a1e92e96f4..22ce04db87 100644 --- a/docs/userGuide/makingTheSiteSearchable.md +++ b/docs/userGuide/makingTheSiteSearchable.md @@ -47,7 +47,19 @@ You can add a search bar component to your website to allow users to search the MarkBind now supports [Pagefind](https://pagefind.app/), a static low-bandwidth search library, as a built-in feature. This provides full-text search capabilities without external services. -This is a beta feature and will be refined in future updates. To use it, you must have enableSearch: true in your site.json (this is the default). +This is a beta feature and will be refined in future updates. To use it, you explicitly enable it in your site.json. Add the pagefind configuration with enablePagefind: true. + + + +```json +{ + "pagefind": { + "enablePagefind": true + } +} +``` + + @@ -94,6 +106,7 @@ In your `site.json`: ```json { "pagefind": { + "enablePagefind": true, "exclude_selectors": [".algolia-no-index", "[class*='algolia-no-index']"] } } From e7077610fd0222840f1b2b78a918293adf96e7f0 Mon Sep 17 00:00:00 2001 From: Hon Yi Hao <165232024+yihao03@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:41:56 +0800 Subject: [PATCH 7/7] Add Command to pull user-facing skills (#2891) * Add skills commands * Add symlink logic for each agent * Add tests * Improve error handling * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix bugs * Lint copilot suggestion * Fix windows bug * Update docs * Add markbind repo check * Update tests --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/userGuide/cliCommands.md | 45 ++ docs/userGuide/gettingStarted.md | 6 + package-lock.json | 405 +++++++++++++++++- packages/cli/index.ts | 92 ++++- packages/cli/package.json | 2 + packages/cli/src/cmd/skills.ts | 218 ++++++++++ packages/cli/test/unit/skills.test.ts | 570 ++++++++++++++++++++++++++ 7 files changed, 1330 insertions(+), 8 deletions(-) create mode 100644 packages/cli/src/cmd/skills.ts create mode 100644 packages/cli/test/unit/skills.test.ts diff --git a/docs/userGuide/cliCommands.md b/docs/userGuide/cliCommands.md index fee910eb03..0757892264 100644 --- a/docs/userGuide/cliCommands.md +++ b/docs/userGuide/cliCommands.md @@ -28,6 +28,7 @@ Options: Setup Commands init|i [options] [root] init a markbind site + skills Manage AI coding skills for this project Site Commands serve|s [options] [root] Build then serve a website from a directory @@ -73,6 +74,50 @@ Commands:
+
+ +### `skills` Command +
+ +**Format:** `markbind skills [command] [options]` + +**Description:** Manages AI coding skills for the current project. + +Use `markbind skills --help` to view all available subcommands. + + + +**Subcommands** :fas-cogs: + +* `install`
+ Downloads skills from the MarkBind skills repository and installs them into `.agents/skills`. + During installation, MarkBind prompts you to choose additional agent directories for optional symlinks. + + * **Format:** `markbind skills install [options]` + * **Options:** + * `--ref `: Uses a specific git tag or branch instead of the MarkBind-version-matched ref. + * `--force`: Overwrites existing installed skills. + * **{{ icon_examples }}** + * `markbind skills install` : Installs skills using the default ref for your MarkBind version. + * `markbind skills install --ref v7.0.0` : Installs skills from the `v7.0.0` ref. + * `markbind skills install --force` : Reinstalls skills and overwrites existing installed skills. + +* `update`
+ Re-downloads skills for the current MarkBind version and overwrites the existing installation. + + * **Format:** `markbind skills update [options]` + * **Options:** + * `--ref `: Uses a specific git tag or branch instead of the MarkBind-version-matched ref. + * **{{ icon_examples }}** + * `markbind skills update` : Updates installed skills using the default ref for your MarkBind version. + * `markbind skills update --ref v7.0.0` : Updates installed skills from the `v7.0.0` ref. + +
+ +
+ +
+ ### `serve` Command
diff --git a/docs/userGuide/gettingStarted.md b/docs/userGuide/gettingStarted.md index a5f317ae64..c76f90f48b 100644 --- a/docs/userGuide/gettingStarted.md +++ b/docs/userGuide/gettingStarted.md @@ -98,6 +98,12 @@ You can add the `--help` flag to any command to show the help screen.
The `init` command populates the project with the [default project template](https://markbind-init-typical.netlify.app/). Refer to [templates](templates.html) section to learn how to use a different template. + + + + +If you use AI coding assistants, you can install project-level skills using `markbind skills install`. See [CLI Commands: `skills`](cliCommands.html#markbind-skills). + diff --git a/package-lock.json b/package-lock.json index d07e27aac9..33a2e8ab88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2556,6 +2556,195 @@ "node": ">=6.9.0" } }, + "node_modules/@inquirer/ansi": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz", + "integrity": "sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.1.3.tgz", + "integrity": "sha512-+G7I8CT+EHv/hasNfUl3P37DVoMoZfpA+2FXmM54dA8MxYle1YqucxbacxHalw1iAFSdKNEDTGNV7F+j1Ldqcg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.8", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.0.11.tgz", + "integrity": "sha512-pTpHjg0iEIRMYV/7oCZUMf27/383E6Wyhfc/MY+AVQGEoUobffIYWOK9YLP2XFRGz/9i6WlTQh1CkFVIo2Y7XA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.1.8.tgz", + "integrity": "sha512-/u+yJk2pOKNDOh1ZgdUH2RQaRx6OOH4I0uwL95qPvTFTIL38YBsuSC4r1yXBB3Q6JvNqFFc202gk0Ew79rrcjA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@inquirer/core/node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/editor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.1.0.tgz", + "integrity": "sha512-6wlkYl65Qfayy48gPCfU4D7li6KCAGN79mLXa/tYHZH99OfZ820yY+HA+DgE88r8YwwgeuY6PQgNqMeK6LuMmw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.8", + "@inquirer/external-editor": "^3.0.0", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor/node_modules/@inquirer/external-editor": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.0.tgz", + "integrity": "sha512-lDSwMgg+M5rq6JKBYaJwSX6T9e/HK2qqZ1oxmOwn4AQoJE5D+7TumsxLGC02PWS//rkIVqbZv3XA3ejsc9FYvg==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@inquirer/expand": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.0.12.tgz", + "integrity": "sha512-vOfrB33b7YIZfDauXS8vNNz2Z86FozTZLIt7e+7/dCaPJ1RXZsHCuI9TlcERzEUq57vkM+UdnBgxP0rFd23JYQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@inquirer/external-editor": { "version": "1.0.3", "dev": true, @@ -2591,6 +2780,191 @@ "url": "https://opencollective.com/express" } }, + "node_modules/@inquirer/figures": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.5.tgz", + "integrity": "sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.0.11.tgz", + "integrity": "sha512-twUWidn4ocPO8qi6fRM7tNWt7W1FOnOZqQ+/+PsfLUacMR5rFLDPK9ql0nBPwxi0oELbo8T5NhRs8B2+qQEqFQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.0.11.tgz", + "integrity": "sha512-Vscmim9TCksQsfjPtka/JwPUcbLhqWYrgfPf1cHrCm24X/F2joFwnageD50yMKsaX14oNGOyKf/RNXAFkNjWpA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.0.11.tgz", + "integrity": "sha512-9KZFeRaNHIcejtPb0wN4ddFc7EvobVoAFa049eS3LrDZFxI8O7xUXiITEOinBzkZFAIwY5V4yzQae/QfO9cbbg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.4.1.tgz", + "integrity": "sha512-AH5xPQ997K7e0F0vulPlteIHke2awMkFi8F0dBemrDfmvtPmHJo82mdHbONC4F/t8d1NHwrbI5cGVI+RbLWdoQ==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.1.3", + "@inquirer/confirm": "^6.0.11", + "@inquirer/editor": "^5.1.0", + "@inquirer/expand": "^5.0.12", + "@inquirer/input": "^5.0.11", + "@inquirer/number": "^4.0.11", + "@inquirer/password": "^5.0.11", + "@inquirer/rawlist": "^5.2.7", + "@inquirer/search": "^4.1.7", + "@inquirer/select": "^5.1.3" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.2.7.tgz", + "integrity": "sha512-AqRMiD9+uE1lskDPrdqHwrV/EUmxKEBLX44SR7uxK3vD2413AmVfE5EQaPeNzYf5Pq5SitHJDYUFVF0poIr09w==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.8", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.1.7.tgz", + "integrity": "sha512-1y7+0N65AWk5RdlXH/Kn13txf3IjIQ7OEfhCEkDTU+h5wKMLq8DUF3P6z+/kLSxDGDtQT1dRBWEUC3o/VvImsQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.1.8", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.1.3.tgz", + "integrity": "sha512-zYyqWgGQi3NhBcNq4Isc5rB3oEdQEh1Q/EcAnOW0FK4MpnXWkvSBYgA4cYrTM4A9UB573omouZbnL9JJ74Mq3A==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.5", + "@inquirer/core": "^11.1.8", + "@inquirer/figures": "^2.0.5", + "@inquirer/type": "^4.0.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.5.tgz", + "integrity": "sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "dev": true, @@ -5306,7 +5680,7 @@ }, "node_modules/@types/node": { "version": "22.19.11", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -7529,7 +7903,6 @@ }, "node_modules/chardet": { "version": "2.1.1", - "dev": true, "license": "MIT" }, "node_modules/cheerio": { @@ -10330,6 +10703,21 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, "node_modules/fast-uri": { "version": "3.0.6", "dev": true, @@ -10345,6 +10733,15 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.0.tgz", + "integrity": "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "dev": true, @@ -18599,7 +18996,6 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "dev": true, "license": "MIT" }, "node_modules/schema-utils": { @@ -20898,7 +21294,7 @@ }, "node_modules/undici-types": { "version": "6.21.0", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -22036,6 +22432,7 @@ "version": "7.0.1", "license": "MIT", "dependencies": { + "@inquirer/prompts": "^8.4.1", "@markbind/core": "7.0.1", "@markbind/core-web": "7.0.1", "chalk": "^3.0.0", diff --git a/packages/cli/index.ts b/packages/cli/index.ts index 9e9b7f70ee..94b2f34848 100755 --- a/packages/cli/index.ts +++ b/packages/cli/index.ts @@ -3,11 +3,13 @@ // Entry file for MarkBind project import { program, Option } from 'commander'; import chalk from 'chalk'; +import { checkbox } from '@inquirer/prompts'; import * as logger from './src/util/logger.js'; import { build } from './src/cmd/build.js'; import { deploy } from './src/cmd/deploy.js'; import { init } from './src/cmd/init.js'; import { serve } from './src/cmd/serve.js'; +import { install as installSkills } from './src/cmd/skills.js'; import { preFlightChecks } from './src/util/preFlightChecks.js'; import packageJson from './package.json' with { type: 'json' }; @@ -72,14 +74,14 @@ program .addOption( program.createOption('-o, --one-page [file]', 'build and serve only a single page in the site initially, ' - + 'building more pages when they are navigated to. Also lazily rebuilds only ' - + 'the page being viewed when there are changes to the source files (if needed), ' - + 'building others when navigated to')) + + 'building more pages when they are navigated to. Also lazily rebuilds only ' + + 'the page being viewed when there are changes to the source files (if needed), ' + + 'building others when navigated to')) .addOption( program.createOption('-b, --background-build', 'when --one-page is specified, enhances one-page serve by building ' - + 'remaining pages in the background')) + + 'remaining pages in the background')) .optionsGroup('Server Options') .addOption( @@ -126,4 +128,86 @@ program deploy(userSpecifiedRoot, options); }); +const skillsCmd = program + .commandsGroup('Setup Commands') + .command('skills') + .summary('Manage AI coding skills for this project') + .description('Download and manage AI coding skills from the MarkBind skills repository'); + +const agentChoices + = [ + { name: 'Augment', value: '.augment' }, + { name: 'IBM Bob', value: '.bob' }, + { name: 'Claude Code', value: '.claude' }, + { name: 'OpenClaw', value: '.openclaw' }, + { name: 'CodeBuddy', value: '.codebuddy' }, + { name: 'Command Code', value: '.commandcode' }, + { name: 'Continue', value: '.continue' }, + { name: 'Cortex Code', value: '.cortex' }, + { name: 'Crush', value: '.crush' }, + { name: 'Droid', value: '.factory' }, + { name: 'Goose', value: '.goose' }, + { name: 'Junie', value: '.junie' }, + { name: 'iFlow CLI', value: '.iflow' }, + { name: 'Kilo Code', value: '.kilocode' }, + { name: 'Kiro CLI', value: '.kiro' }, + { name: 'Kode', value: '.kode' }, + { name: 'MCPJam', value: '.mcpjam' }, + { name: 'Mistral Vibe', value: '.vibe' }, + { name: 'Mux', value: '.mux' }, + { name: 'OpenHands', value: '.openhands' }, + { name: 'Pi', value: '.pi' }, + { name: 'Qoder', value: '.qoder' }, + { name: 'Qwen Code', value: '.qwen' }, + { name: 'Roo Code', value: '.roo' }, + { name: 'Trae', value: '.trae' }, + { name: 'Trae CN', value: '.trae' }, + { name: 'Windsurf', value: '.windsurf' }, + { name: 'Zencoder', value: '.zencoder' }, + { name: 'Neovate', value: '.neovate' }, + { name: 'Pochi', value: '.pochi' }, + { name: 'AdaL', value: '.adal' }, + ]; + +skillsCmd + .command('install') + .option('--ref ', 'specify a git ref (tag or branch) instead of auto-resolving from MarkBind version') + .option('--force', 'overwrite existing skills') + .summary('Install AI coding skills into .agents/skills with optional agent symlinks') + .description('Download skills from https://github.com/MarkBind/skills.git,' + + ' install them into .agents/skills, and optionally create symlinks for selected additional agents') + .action((options) => { + checkbox({ + message: ` +── Universal (.agents/skills) ── always included ──────────── + • Amp + • Antigravity + • Cline + • Codex + • Cursor + • Deep Agents + • Firebender + • Gemini CLI + • GitHub Copilot + • Kimi Code CLI + • OpenCode + • Warp + +── Additional agents ─────────────────────────────`, + choices: agentChoices, + }).then(agent => + installSkills({ ...options, agents: agent }), + ); + }); + +skillsCmd + .command('update') + .option('--ref ', 'specify a git ref (tag or branch) instead of auto-resolving from MarkBind version') + .summary('Update installed skills to match current MarkBind version') + .description('Re-download skills matching the current MarkBind CLI version,' + + 'overwriting any existing installation') + .action((options) => { + installSkills({ ...options, force: true }); + }); + program.parse(process.argv); diff --git a/packages/cli/package.json b/packages/cli/package.json index d02288b5f9..1389482175 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -3,6 +3,7 @@ "version": "7.0.1", "type": "module", "description": "Command line interface for MarkBind", + "aiSkillsVersion": "0.1.0", "keywords": [ "mark", "markdown", @@ -33,6 +34,7 @@ "dev": "tsc --watch" }, "dependencies": { + "@inquirer/prompts": "^8.4.1", "@markbind/core": "7.0.1", "@markbind/core-web": "7.0.1", "chalk": "^3.0.0", diff --git a/packages/cli/src/cmd/skills.ts b/packages/cli/src/cmd/skills.ts new file mode 100644 index 0000000000..cdeb8833f6 --- /dev/null +++ b/packages/cli/src/cmd/skills.ts @@ -0,0 +1,218 @@ +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import os from 'os'; +import path from 'path'; +import fs from 'fs-extra'; +import _ from 'lodash'; + +import * as logger from '../util/logger.js'; +import packageJson from '../../package.json' with { type: 'json' }; +import { findRootFolder } from '../util/cliUtil.js'; + +const execFileAsync = promisify(execFile); + +const METADATA_FILE = '.markbind-skills.json'; +const SKILLS_REPO = 'https://github.com/MarkBind/skills.git'; +const SKILLS_TARGET = path.join('.agents', 'skills'); +const SKILL_MARKER = 'SKILL.md'; +const CLONE_TIMEOUT_MS = 30000; + +interface SkillsInstallOptions { + ref?: string; + force?: boolean; + agents?: string[]; +} + +interface SkillsMetadata { + ref: string; + skills: string[]; + installedAt: string; +} + +async function findSkillDirs(baseDir: string): Promise { + const entries = await fs.readdir(baseDir, { withFileTypes: true }); + const results = await Promise.all( + entries.map(async (entry) => { + if (!entry.isDirectory()) return null; + const skillMdPath = path.join(baseDir, entry.name, SKILL_MARKER); + if (await fs.pathExists(skillMdPath)) return entry.name; + return null; + }), + ); + return results + .filter((name): name is string => name !== null) + .sort((a, b) => a.localeCompare(b)); +} + +async function writeMetadata(targetDir: string, skillsRef: string, skillNames: string[]) { + const metadata: SkillsMetadata = { + ref: skillsRef, + skills: skillNames, + installedAt: new Date().toISOString(), + }; + const metadataPath = path.join(targetDir, METADATA_FILE); + await fs.writeJson(metadataPath, metadata, { spaces: 2 }); +} + +async function readMetadata(targetDir: string): Promise { + const metadataPath = path.join(targetDir, METADATA_FILE); + if (await fs.pathExists(metadataPath)) { + try { + return await fs.readJson(metadataPath); + } catch (e) { + throw new Error(`Failed to read metadata file: ${(e as Error).message}`); + } + } else { + throw new Error('Metadata file not found'); + } +} + +function isSemverTag(ref: string): boolean { + return /^v\d+\.\d+\.\d+$/.test(ref); +} + +// Expects versions in format vX.Y.Z or X.Y.Z, compares them numerically +function compareSemver(v1: string, v2: string): number { + const parse = (v: string) => v.replace('v', '') + .split('.') + .map(num => parseInt(num, 10)); + const v1Parsed = parse(v1); + const v2Parsed = parse(v2); + for (let i = 0; i < Math.max(v1Parsed.length, v2Parsed.length); i += 1) { + const num1 = v1Parsed[i] || 0; + const num2 = v2Parsed[i] || 0; + if (num1 > num2) return 1; + if (num1 < num2) return -1; + } + return 0; +} + +async function install(options: SkillsInstallOptions) { + let rootFolder; + try { + rootFolder = findRootFolder(''); + } catch (error) { + if (_.isError(error)) { + logger.error(error.message); + logger.error('This directory does not appear to contain a valid MarkBind site. ' + + 'Check that you are running the command in the correct directory!\n' + + '\n' + + 'To create a new MarkBind site, run:\n' + + ' markbind init'); + } else { + logger.error(`Unknown error occurred: ${error}`); + } + process.exitCode = 1; + process.exit(); + } + const ref = options.ref || `v${packageJson.aiSkillsVersion}`; + const targetDir = path.resolve(rootFolder, SKILLS_TARGET); + + // Check git is available + try { + await execFileAsync('git', ['--version']); + } catch { + logger.error('Git is required but was not found on your PATH. Please install git and try again.'); + process.exitCode = 1; + return; + } + + // Check if already installed + if (await fs.pathExists(targetDir) && !options.force) { + const metadata = await readMetadata(targetDir).catch( + (e) => { + logger.warn(`Failed to read existing skills metadata: ${(e as Error).message}`); + return null; + }, + ); + if (!metadata) { + logger.info('Skills already installed. Use --force to reinstall.'); + return; + } + + // If the existing ref is not a semver tag (e.g. master) we require force + // flag to update, otherwise we allow updating if the new ref is a semver + // tag that is newer than the existing one + if (!isSemverTag(metadata.ref) || (isSemverTag(ref) && compareSemver(metadata.ref, ref) >= 0)) { + logger.info(`Skills already installed (ref ${metadata.ref}). Use --force to reinstall.`); + return; + } + logger.info(`Upgrading skills from version ${metadata.ref} to ${ref}`); + } + + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'markbind-skills-')); + + try { + logger.info(`Downloading skills (${ref})...`); + + await execFileAsync( + 'git', + ['clone', '--depth', '1', '--branch', ref, SKILLS_REPO, tempDir], + { timeout: CLONE_TIMEOUT_MS }, + ); + + // Skills may be at repo root or in a skills/ subdirectory + const skillsSubdir = path.join(tempDir, 'skills'); + const searchDir = await fs.pathExists(skillsSubdir) ? skillsSubdir : tempDir; + + const skillNames = await findSkillDirs(searchDir); + + if (skillNames.length === 0) { + logger.error('No skills found in the downloaded repository.'); + process.exitCode = 1; + return; + } + + // Clear existing skills + await fs.rm(targetDir, { recursive: true, force: true }); + await fs.ensureDir(targetDir); + + const copyPromises = skillNames.map((name) => { + logger.info(`Installing skill: ${name}...`); + return fs.copy(path.join(searchDir, name), path.join(targetDir, name)); + }); + + await Promise.all(copyPromises); + + await writeMetadata(targetDir, ref, skillNames); + + logger.info(`Installed ${skillNames.length} skill(s) to ${SKILLS_TARGET}/`); + + if (options.agents) { + await Promise.all(options.agents.map(async (agent) => { + const agentSkillsDir = path.join(rootFolder, agent, 'skills'); + if (await fs.pathExists(agentSkillsDir)) { + logger.warn('Agent skills directory already exist. Skipping symlink creation.'); + logger.warn(`Please manually symlink ${targetDir} to ${agentSkillsDir}` + + ` if you want to use the skills with ${options.agents}.`); + } else { + await fs.ensureSymlink(targetDir, agentSkillsDir, 'dir'); + logger.info(`Symlinked skills to ${agent}/skills/`); + } + })); + } + } catch (error) { + if (_.isError(error)) { + const msg = error.message; + if ((msg.includes('Remote branch') && msg.includes('not found')) + || msg.includes('not found in upstream') + || msg.includes('does not exist')) { + logger.error(`Skills ref '${ref}' was not found in the repository.`); + logger.error('Use --ref to specify a branch or tag (e.g., --ref main).'); + } else if (msg.includes('timed out') || msg.includes('block timeout')) { + logger.error('Download timed out. Check your network connection and try again.'); + } else { + logger.error(`Failed to install skills: ${msg}`); + } + } else { + logger.error(`Failed to install skills: ${error}`); + } + process.exitCode = 1; + } finally { + await fs.remove(tempDir).catch(() => { }); + } +} + +export { + install, findSkillDirs, writeMetadata, readMetadata, isSemverTag, compareSemver, +}; diff --git a/packages/cli/test/unit/skills.test.ts b/packages/cli/test/unit/skills.test.ts new file mode 100644 index 0000000000..d8827c3776 --- /dev/null +++ b/packages/cli/test/unit/skills.test.ts @@ -0,0 +1,570 @@ +import path from 'path'; +import os from 'os'; +import { vol, fs as memfs } from 'memfs'; + +import { execFile } from 'child_process'; +import _ from 'lodash'; +import * as logger from '../../src/util/logger.js'; +import * as cliUtil from '../../src/util/cliUtil.js'; +import { + install, + findSkillDirs, + writeMetadata, + readMetadata, + isSemverTag, + compareSemver, +} from '../../src/cmd/skills.js'; + +jest.mock('fs-extra', () => { + const pathModule = jest.requireActual('path'); + const { fs } = jest.requireActual('memfs'); + + const copyRecursive = async (src: string, dest: string) => { + const stats = await fs.promises.lstat(src); + if (stats.isDirectory()) { + await fs.promises.mkdir(dest, { recursive: true }); + const entries = await fs.promises.readdir(src); + await Promise.all(entries.map((entry: string) => copyRecursive( + pathModule.join(src, entry), + pathModule.join(dest, entry), + ))); + return; + } + + await fs.promises.mkdir(pathModule.dirname(dest), { recursive: true }); + await fs.promises.copyFile(src, dest); + }; + + return { + __esModule: true, + default: { + readdir: (dir: string, options?: unknown) => fs.promises.readdir(dir, options as never), + pathExists: async (filePath: string) => fs.existsSync(filePath), + writeJson: async (filePath: string, data: unknown, options?: { spaces?: number }) => { + await fs.promises.mkdir(pathModule.dirname(filePath), { recursive: true }); + await fs.promises.writeFile(filePath, JSON.stringify(data, null, options?.spaces ?? 0)); + }, + readJson: async (filePath: string) => JSON.parse(await fs.promises.readFile(filePath, 'utf8')), + mkdtemp: (prefix: string) => fs.promises.mkdtemp(prefix), + rm: (filePath: string, options?: unknown) => fs.promises.rm(filePath, options as never), + ensureDir: (dir: string) => fs.promises.mkdir(dir, { recursive: true }), + copy: copyRecursive, + remove: (filePath: string) => fs.promises.rm(filePath, { recursive: true, force: true }), + ensureSymlink: async (target: string, filePath: string, type: unknown) => { + await fs.promises.mkdir(pathModule.dirname(filePath), { recursive: true }); + await fs.promises.symlink(target, filePath, type as never); + }, + }, + }; +}); + +jest.mock('child_process', () => ({ + execFile: jest.fn(), +})); + +jest.mock('../../src/util/logger.js', () => ({ + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), +})); + +jest.mock('../../src/util/cliUtil.js', () => ({ + findRootFolder: jest.fn(), +})); + +type ExecCallback = (error: Error | null, stdout?: string, stderr?: string) => void; + +const mockExecFile = execFile as unknown as jest.Mock; + +const mockedLogger = logger as jest.Mocked; +const mockedCliUtil = cliUtil as jest.Mocked; + +const WORKDIR = '/workspace/project'; +const TMPDIR = '/tmp'; + +const flushPromises = () => new Promise(process.nextTick); + +function setExecFileMock( + impl: (file: string, args: string[], cb: ExecCallback, options?: { timeout?: number }) => void, +) { + mockExecFile.mockImplementation( + (file: string, args: string[], optionsOrCb: unknown, cbMaybe?: ExecCallback) => { + const cb = _.isFunction(optionsOrCb) + ? optionsOrCb as ExecCallback + : cbMaybe as ExecCallback; + const options = _.isFunction(optionsOrCb) ? undefined : optionsOrCb as { timeout?: number }; + impl(file, args, cb, options); + }); +} + +function seedClonedSkills(tempDir: string, skillNames: string[], underSkillsSubdir = false) { + const root = underSkillsSubdir ? path.join(tempDir, 'skills') : tempDir; + const files = Object.fromEntries(skillNames.map(name => [path.join(root, name, 'SKILL.md'), `# ${name}`])); + vol.fromJSON(files, '/'); +} + +function listTmpSkillCloneDirs(): string[] { + if (!memfs.existsSync(TMPDIR)) { + return []; + } + const tmpEntries = memfs.readdirSync(TMPDIR, 'utf8') as string[]; + return tmpEntries.filter(name => name.startsWith('markbind-skills-')); +} + +beforeEach(() => { + vol.reset(); + vol.fromJSON({ + [path.join(TMPDIR, '.keep')]: '', + [path.join(WORKDIR, '.keep')]: '', + }, '/'); + jest.resetAllMocks(); + jest.spyOn(os, 'tmpdir').mockReturnValue(TMPDIR); + jest.spyOn(process, 'cwd').mockReturnValue(WORKDIR); + mockedCliUtil.findRootFolder.mockReturnValue(WORKDIR); + process.exitCode = undefined; +}); + +afterEach(() => { + vol.reset(); + jest.restoreAllMocks(); + process.exitCode = undefined; +}); + +describe('isSemverTag', () => { + test.each([ + ['v1.0.0', true], + ['v12.34.56', true], + ['1.0.0', false], + ['v1.0', false], + ['main', false], + ['v1.0.0-beta', false], + ['', false], + ])('returns %p for %p', (tag, expected) => { + expect(isSemverTag(tag)).toBe(expected); + }); +}); + +describe('compareSemver', () => { + test('returns 0 for equal versions', () => { + expect(compareSemver('v1.2.3', 'v1.2.3')).toBe(0); + }); + + test('compares major versions correctly', () => { + expect(compareSemver('v2.0.0', 'v1.9.9')).toBe(1); + expect(compareSemver('v1.0.0', 'v2.0.0')).toBe(-1); + }); + + test('compares minor versions correctly', () => { + expect(compareSemver('v1.4.0', 'v1.3.9')).toBe(1); + expect(compareSemver('v1.2.0', 'v1.3.0')).toBe(-1); + }); + + test('compares patch versions correctly', () => { + expect(compareSemver('v1.2.4', 'v1.2.3')).toBe(1); + expect(compareSemver('v1.2.3', 'v1.2.4')).toBe(-1); + }); + + test('handles missing and mixed v prefixes', () => { + expect(compareSemver('1.2.3', '1.2.3')).toBe(0); + expect(compareSemver('v1.2.3', '1.2.3')).toBe(0); + }); + + test('compares multi-digit parts numerically', () => { + expect(compareSemver('v1.10.0', 'v1.9.0')).toBe(1); + }); +}); + +describe('findSkillDirs', () => { + test('finds only directories containing SKILL.md', async () => { + vol.fromJSON({ + '/skills/alpha/SKILL.md': '# alpha', + '/skills/beta/README.md': '# beta', + '/skills/gamma/SKILL.md': '# gamma', + '/skills/not-a-dir.md': 'file', + }, '/'); + + await expect(findSkillDirs('/skills')).resolves.toEqual(['alpha', 'gamma']); + }); + + test('returns empty array when directory has no skill folders', async () => { + vol.fromJSON({ + '/skills/README.md': 'root file', + }, '/'); + + await expect(findSkillDirs('/skills')).resolves.toEqual([]); + }); +}); + +describe('writeMetadata and readMetadata', () => { + test('round-trips metadata', async () => { + const target = '/skills-target'; + + await writeMetadata(target, 'v1.2.3', ['one', 'two']); + const metadata = await readMetadata(target); + + expect(metadata).toEqual({ + ref: 'v1.2.3', + skills: ['one', 'two'], + installedAt: expect.any(String), + }); + expect(new Date(metadata!.installedAt).toISOString()).toBe(metadata!.installedAt); + }); + + test('throws when metadata file is missing', async () => { + await expect(readMetadata('/missing')).rejects.toThrow('Metadata file not found'); + }); + + test('throws when metadata file is corrupted', async () => { + vol.fromJSON({ + '/skills/.markbind-skills.json': '{not-json', + }, '/'); + + await expect(readMetadata('/skills')).rejects.toThrow('Failed to read metadata file:'); + }); +}); + +describe('install', () => { + test('fails when current directory is not inside a MarkBind site', async () => { + mockedCliUtil.findRootFolder.mockImplementation(() => { + throw new Error(`No config file found in parent directories of ${WORKDIR}`); + }); + + const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => { + throw new Error('process.exit called'); + }) as never); + + await expect(install({})).rejects.toThrow('process.exit called'); + + expect(mockedLogger.error).toHaveBeenCalledWith( + `No config file found in parent directories of ${WORKDIR}`, + ); + expect(mockedLogger.error).toHaveBeenCalledWith( + 'This directory does not appear to contain a valid MarkBind site. ' + + 'Check that you are running the command in the correct directory!\n' + + '\n' + + 'To create a new MarkBind site, run:\n' + + ' markbind init', + ); + expect(process.exitCode).toBe(1); + expect(exitSpy).toHaveBeenCalled(); + expect(mockExecFile).not.toHaveBeenCalled(); + }); + + test('installs with default ref from packageJson aiSkillsVersion', async () => { + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, 'git version 2.43.0', ''); + return; + } + seedClonedSkills(args[args.length - 1], ['skill-a', 'skill-b']); + cb(null, '', ''); + }); + + await install({}); + + const cloneCall = mockExecFile.mock.calls.find(([, args]) => args[0] === 'clone'); + expect(cloneCall).toBeDefined(); + expect(cloneCall![1]).toEqual(expect.arrayContaining(['--branch', 'v0.1.0'])); + expect(memfs.existsSync(path.join(WORKDIR, '.agents/skills/skill-a/SKILL.md'))).toBe(true); + expect(memfs.existsSync(path.join(WORKDIR, '.agents/skills/skill-b/SKILL.md'))).toBe(true); + expect(memfs.existsSync(path.join(WORKDIR, '.agents/skills/.markbind-skills.json'))).toBe(true); + expect(process.exitCode).toBeUndefined(); + }); + + test('installs into detected root folder when cwd is a nested directory', async () => { + const rootFolder = '/workspace/site-root'; + const nestedCwd = path.join(rootFolder, 'docs', 'chapter-1'); + mockedCliUtil.findRootFolder.mockReturnValue(rootFolder); + jest.spyOn(process, 'cwd').mockReturnValue(nestedCwd); + + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, 'git version 2.43.0', ''); + return; + } + seedClonedSkills(args[args.length - 1], ['nested-root-skill']); + cb(null, '', ''); + }); + + await install({}); + + expect(memfs.existsSync(path.join(rootFolder, '.agents/skills/nested-root-skill/SKILL.md'))).toBe(true); + expect(memfs.existsSync(path.join(nestedCwd, '.agents/skills/nested-root-skill/SKILL.md'))).toBe(false); + }); + + test('installs with custom ref', async () => { + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, '', ''); + return; + } + seedClonedSkills(args[args.length - 1], ['skill-a']); + cb(null, '', ''); + }); + + await install({ ref: 'main' }); + + const cloneCall = mockExecFile.mock.calls.find(([, args]) => args[0] === 'clone'); + expect(cloneCall![1]).toEqual(expect.arrayContaining(['--branch', 'main'])); + }); + + test('sets exitCode when git is not found', async () => { + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(new Error('not found')); + } + }); + + await install({}); + + expect(mockedLogger.error).toHaveBeenCalledWith( + 'Git is required but was not found on your PATH. Please install git and try again.', + ); + expect(process.exitCode).toBe(1); + }); + + test('skips install when same version is already installed', async () => { + vol.fromJSON({ + [path.join(WORKDIR, '.agents/skills/.markbind-skills.json')]: JSON.stringify({ + ref: 'v0.1.0', + skills: ['existing'], + installedAt: new Date().toISOString(), + }), + }, '/'); + + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, '', ''); + } + }); + + await install({}); + + expect(mockedLogger.info).toHaveBeenCalledWith( + 'Skills already installed (ref v0.1.0). Use --force to reinstall.', + ); + expect(mockExecFile.mock.calls.some(([, args]) => args[0] === 'clone')).toBe(false); + }); + + test('reinstalls with --force', async () => { + vol.fromJSON({ + [path.join(WORKDIR, '.agents/skills/.markbind-skills.json')]: JSON.stringify({ + ref: 'v0.1.0', + skills: ['old-skill'], + installedAt: new Date().toISOString(), + }), + [path.join(WORKDIR, '.agents/skills/old-skill/SKILL.md')]: '# old', + }, '/'); + + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, '', ''); + return; + } + seedClonedSkills(args[args.length - 1], ['new-skill']); + cb(null, '', ''); + }); + + await install({ force: true }); + + expect(memfs.existsSync(path.join(WORKDIR, '.agents/skills/new-skill/SKILL.md'))).toBe(true); + expect(memfs.existsSync(path.join(WORKDIR, '.agents/skills/old-skill/SKILL.md'))).toBe(false); + }); + + test('upgrades when new semver ref is newer', async () => { + vol.fromJSON({ + [path.join(WORKDIR, '.agents/skills/.markbind-skills.json')]: JSON.stringify({ + ref: 'v0.0.9', + skills: ['existing'], + installedAt: new Date().toISOString(), + }), + }, '/'); + + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, '', ''); + return; + } + seedClonedSkills(args[args.length - 1], ['upgraded']); + cb(null, '', ''); + }); + + await install({}); + + expect(mockedLogger.info).toHaveBeenCalledWith('Upgrading skills from version v0.0.9 to v0.1.0'); + expect(memfs.existsSync(path.join(WORKDIR, '.agents/skills/upgraded/SKILL.md'))).toBe(true); + }); + + test('skips when existing ref is non-semver without force', async () => { + vol.fromJSON({ + [path.join(WORKDIR, '.agents/skills/.markbind-skills.json')]: JSON.stringify({ + ref: 'main', + skills: ['existing'], + installedAt: new Date().toISOString(), + }), + }, '/'); + + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, '', ''); + } + }); + + await install({}); + + expect(mockedLogger.info).toHaveBeenCalledWith( + 'Skills already installed (ref main). Use --force to reinstall.', + ); + expect(mockExecFile.mock.calls.some(([, args]) => args[0] === 'clone')).toBe(false); + }); + + test('sets exitCode when no skills are found', async () => { + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, '', ''); + return; + } + vol.fromJSON({ [path.join(args[args.length - 1], 'README.md')]: '# repo' }, '/'); + cb(null, '', ''); + }); + + await install({}); + + expect(mockedLogger.error).toHaveBeenCalledWith('No skills found in the downloaded repository.'); + expect(process.exitCode).toBe(1); + }); + + test('finds skills in skills/ subdirectory', async () => { + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, '', ''); + return; + } + seedClonedSkills(args[args.length - 1], ['nested-skill'], true); + cb(null, '', ''); + }); + + await install({}); + + expect(memfs.existsSync(path.join(WORKDIR, '.agents/skills/nested-skill/SKILL.md'))).toBe(true); + }); + + test('handles ref-not-found errors', async () => { + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, '', ''); + return; + } + cb(new Error('Remote branch no-such-branch not found in upstream origin')); + }); + + await install({ ref: 'no-such-branch' }); + + expect(mockedLogger.error).toHaveBeenCalledWith( + "Skills ref 'no-such-branch' was not found in the repository.", + ); + expect(mockedLogger.error).toHaveBeenCalledWith( + 'Use --ref to specify a branch or tag (e.g., --ref main).', + ); + expect(process.exitCode).toBe(1); + }); + + test('handles timeout errors', async () => { + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, '', ''); + return; + } + cb(new Error('command timed out after 30000 milliseconds')); + }); + + await install({}); + + expect(mockedLogger.error).toHaveBeenCalledWith( + 'Download timed out. Check your network connection and try again.', + ); + expect(process.exitCode).toBe(1); + }); + + test('handles generic errors', async () => { + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, '', ''); + return; + } + cb(new Error('boom')); + }); + + await install({}); + + expect(mockedLogger.error).toHaveBeenCalledWith('Failed to install skills: boom'); + expect(process.exitCode).toBe(1); + }); + + test('cleans up temporary clone directory on success and failure', async () => { + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, '', ''); + return; + } + seedClonedSkills(args[args.length - 1], ['cleanup']); + cb(null, '', ''); + }); + + await install({}); + expect(listTmpSkillCloneDirs()).toEqual([]); + + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, '', ''); + return; + } + cb(new Error('boom')); + }); + + await install({ force: true }); + expect(listTmpSkillCloneDirs()).toEqual([]); + }); + + test('creates symlinks for specified agents', async () => { + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, '', ''); + return; + } + seedClonedSkills(args[args.length - 1], ['skill-a']); + cb(null, '', ''); + }); + + await install({ agents: ['.claude', '.cursor'] }); + await flushPromises(); + + const claudeLink = path.join(WORKDIR, '.claude/skills'); + const cursorLink = path.join(WORKDIR, '.cursor/skills'); + expect(memfs.lstatSync(claudeLink).isSymbolicLink()).toBe(true); + expect(memfs.lstatSync(cursorLink).isSymbolicLink()).toBe(true); + expect(memfs.readlinkSync(claudeLink).toString()).toBe(path.resolve(WORKDIR, '.agents/skills')); + }); + + test('warns when agent skills directory already exists', async () => { + vol.fromJSON({ + [path.join(WORKDIR, '.claude/skills/existing.txt')]: 'x', + }, '/'); + + setExecFileMock((file, args, cb) => { + if (args[0] === '--version') { + cb(null, '', ''); + return; + } + seedClonedSkills(args[args.length - 1], ['skill-a']); + cb(null, '', ''); + }); + + await install({ agents: ['.claude'] }); + await flushPromises(); + + expect(mockedLogger.warn).toHaveBeenCalledWith( + 'Agent skills directory already exist. Skipping symlink creation.', + ); + }); +});