@farming-labs/docs 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,99 @@
1
+ import { n as createAlgoliaSearchAdapter, o as createTypesenseSearchAdapter, t as buildDocsSearchDocuments } from "./search-BS6C5N1i.mjs";
2
+ import "./api-reference-DlfH-Y9c.mjs";
3
+ import { createFilesystemDocsMcpSource } from "./mcp.mjs";
4
+ import "./server.mjs";
5
+ import { a as readStringProperty, n as loadProjectEnv, o as resolveDocsConfigPath, s as resolveDocsContentDir } from "./config-CSywk3ou.mjs";
6
+ import { readFileSync } from "node:fs";
7
+ import pc from "picocolors";
8
+
9
+ //#region src/cli/search.ts
10
+ function getEnvValue(loadedEnv, key) {
11
+ return process.env[key] ?? loadedEnv[key];
12
+ }
13
+ function resolveSearchSyncProvider(options, loadedEnv) {
14
+ if (options.typesense && options.algolia) throw new Error("Use only one provider flag: --typesense or --algolia.");
15
+ if (options.typesense) return "typesense";
16
+ if (options.algolia) return "algolia";
17
+ if (options.provider === "typesense" || options.provider === "algolia") return options.provider;
18
+ if (options.provider) throw new Error(`Unsupported search provider: ${options.provider}.`);
19
+ if (options.baseUrl || getEnvValue(loadedEnv, "TYPESENSE_URL")) return "typesense";
20
+ if (options.appId || getEnvValue(loadedEnv, "ALGOLIA_APP_ID")) return "algolia";
21
+ throw new Error("Could not determine a search provider. Use --typesense, --algolia, or --provider <name>.");
22
+ }
23
+ function resolveTypesenseSyncConfig(options, loadedEnv) {
24
+ const baseUrl = options.baseUrl ?? getEnvValue(loadedEnv, "TYPESENSE_URL") ?? getEnvValue(loadedEnv, "TYPESENSE_BASE_URL");
25
+ const collection = options.collection ?? getEnvValue(loadedEnv, "TYPESENSE_COLLECTION") ?? "docs";
26
+ const apiKey = options.apiKey ?? getEnvValue(loadedEnv, "TYPESENSE_SEARCH_API_KEY") ?? getEnvValue(loadedEnv, "TYPESENSE_API_KEY");
27
+ const adminApiKey = options.adminApiKey ?? getEnvValue(loadedEnv, "TYPESENSE_ADMIN_API_KEY") ?? getEnvValue(loadedEnv, "TYPESENSE_API_KEY");
28
+ const mode = options.mode === "hybrid" ? "hybrid" : "keyword";
29
+ const ollamaModel = options.ollamaModel ?? getEnvValue(loadedEnv, "TYPESENSE_OLLAMA_MODEL");
30
+ const ollamaBaseUrl = options.ollamaBaseUrl ?? getEnvValue(loadedEnv, "TYPESENSE_OLLAMA_BASE_URL");
31
+ if (!baseUrl) throw new Error("Missing Typesense base URL. Set TYPESENSE_URL or pass --base-url.");
32
+ if (!apiKey) throw new Error("Missing Typesense API key. Set TYPESENSE_API_KEY or pass --api-key.");
33
+ if (!adminApiKey) throw new Error("Missing Typesense admin-capable key for sync. Set TYPESENSE_ADMIN_API_KEY or pass --admin-api-key.");
34
+ return {
35
+ provider: "typesense",
36
+ baseUrl,
37
+ collection,
38
+ apiKey,
39
+ adminApiKey,
40
+ mode,
41
+ ...mode === "hybrid" ? ollamaModel ? { embeddings: {
42
+ provider: "ollama",
43
+ model: ollamaModel,
44
+ baseUrl: ollamaBaseUrl
45
+ } } : (() => {
46
+ throw new Error("Typesense hybrid sync needs an embeddings model. Set TYPESENSE_OLLAMA_MODEL or pass --ollama-model.");
47
+ })() : {}
48
+ };
49
+ }
50
+ function resolveAlgoliaSyncConfig(options, loadedEnv) {
51
+ const appId = options.appId ?? getEnvValue(loadedEnv, "ALGOLIA_APP_ID");
52
+ const indexName = options.indexName ?? getEnvValue(loadedEnv, "ALGOLIA_INDEX_NAME") ?? "docs";
53
+ const adminApiKey = options.adminApiKey ?? getEnvValue(loadedEnv, "ALGOLIA_ADMIN_API_KEY");
54
+ const searchApiKey = options.searchApiKey ?? getEnvValue(loadedEnv, "ALGOLIA_SEARCH_API_KEY") ?? adminApiKey;
55
+ if (!appId) throw new Error("Missing Algolia app id. Set ALGOLIA_APP_ID or pass --app-id.");
56
+ if (!adminApiKey) throw new Error("Missing Algolia admin API key for sync. Set ALGOLIA_ADMIN_API_KEY or pass --admin-api-key.");
57
+ if (!searchApiKey) throw new Error("Missing Algolia search API key. Set ALGOLIA_SEARCH_API_KEY or pass --search-api-key.");
58
+ return {
59
+ provider: "algolia",
60
+ appId,
61
+ indexName,
62
+ searchApiKey,
63
+ adminApiKey
64
+ };
65
+ }
66
+ async function syncSearch(options = {}) {
67
+ const rootDir = process.cwd();
68
+ const configContent = readFileSync(resolveDocsConfigPath(rootDir, options.configPath), "utf-8");
69
+ const loadedEnv = loadProjectEnv(rootDir);
70
+ const provider = resolveSearchSyncProvider(options, loadedEnv);
71
+ const entry = readStringProperty(configContent, "entry") ?? "docs";
72
+ const contentDir = resolveDocsContentDir(rootDir, configContent, entry);
73
+ const source = createFilesystemDocsMcpSource({
74
+ rootDir,
75
+ entry,
76
+ contentDir,
77
+ siteTitle: "Documentation"
78
+ });
79
+ const pages = await source.getPages();
80
+ const documents = buildDocsSearchDocuments(pages);
81
+ const context = {
82
+ pages,
83
+ documents,
84
+ siteTitle: source.siteTitle
85
+ };
86
+ if (documents.length === 0) throw new Error(`No docs content was found under ${contentDir}.`);
87
+ if (provider === "typesense") {
88
+ const config = resolveTypesenseSyncConfig(options, loadedEnv);
89
+ await createTypesenseSearchAdapter(config).index?.(context);
90
+ console.log(pc.green(`Synced ${documents.length} docs search documents to Typesense collection "${config.collection}".`));
91
+ return;
92
+ }
93
+ const config = resolveAlgoliaSyncConfig(options, loadedEnv);
94
+ await createAlgoliaSearchAdapter(config).index?.(context);
95
+ console.log(pc.green(`Synced ${documents.length} docs search documents to Algolia index "${config.indexName}".`));
96
+ }
97
+
98
+ //#endregion
99
+ export { syncSearch };
@@ -0,0 +1,21 @@
1
+ import { E as DocsSearchSourcePage, N as McpDocsSearchConfig, Z as TypesenseDocsSearchConfig, b as DocsSearchConfig, g as DocsSearchAdapter, n as AlgoliaDocsSearchConfig, s as CustomDocsSearchConfig, v as DocsSearchAdapterFactory, w as DocsSearchResult, x as DocsSearchDocument, y as DocsSearchChunkingConfig } from "./types-BAulrjlV.mjs";
2
+
3
+ //#region src/search.d.ts
4
+ declare function buildDocsSearchDocuments(pages: DocsSearchSourcePage[], chunking?: DocsSearchChunkingConfig): DocsSearchDocument[];
5
+ declare function createSimpleSearchAdapter(): DocsSearchAdapter;
6
+ declare function createTypesenseSearchAdapter(config: TypesenseDocsSearchConfig): DocsSearchAdapter;
7
+ declare function resolveSearchRequestConfig(search: boolean | DocsSearchConfig | undefined, requestUrl?: string): boolean | DocsSearchConfig | undefined;
8
+ declare function createMcpSearchAdapter(config: McpDocsSearchConfig): DocsSearchAdapter;
9
+ declare function createAlgoliaSearchAdapter(config: AlgoliaDocsSearchConfig): DocsSearchAdapter;
10
+ declare function performDocsSearch(options: {
11
+ pages: DocsSearchSourcePage[];
12
+ query: string;
13
+ search?: boolean | DocsSearchConfig;
14
+ locale?: string;
15
+ pathname?: string;
16
+ siteTitle?: string;
17
+ limit?: number;
18
+ }): Promise<DocsSearchResult[]>;
19
+ declare function createCustomSearchAdapter(adapter: DocsSearchAdapter | DocsSearchAdapterFactory): CustomDocsSearchConfig;
20
+ //#endregion
21
+ export { createSimpleSearchAdapter as a, resolveSearchRequestConfig as c, createMcpSearchAdapter as i, createAlgoliaSearchAdapter as n, createTypesenseSearchAdapter as o, createCustomSearchAdapter as r, performDocsSearch as s, buildDocsSearchDocuments as t };
package/dist/server.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import { o as DocsConfig } from "./types-Bd3kyFF1.mjs";
1
+ import { C as DocsSearchQuery, E as DocsSearchSourcePage, N as McpDocsSearchConfig, _ as DocsSearchAdapterContext, b as DocsSearchConfig, c as DocsConfig, g as DocsSearchAdapter, v as DocsSearchAdapterFactory, w as DocsSearchResult, x as DocsSearchDocument } from "./types-BAulrjlV.mjs";
2
+ import { a as createSimpleSearchAdapter, c as resolveSearchRequestConfig, i as createMcpSearchAdapter, n as createAlgoliaSearchAdapter, o as createTypesenseSearchAdapter, r as createCustomSearchAdapter, s as performDocsSearch, t as buildDocsSearchDocuments } from "./search-KzREATdM.mjs";
2
3
  import { DocsMcpHttpHandlers, DocsMcpNavigationNode, DocsMcpNavigationTree, DocsMcpPage, DocsMcpResolvedConfig, DocsMcpSource, createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, normalizeDocsMcpRoute, resolveDocsMcpConfig, runDocsMcpStdio } from "./mcp.mjs";
3
4
 
4
5
  //#region src/api-reference.d.ts
@@ -36,4 +37,4 @@ declare function buildApiReferenceOpenApiDocumentAsync(config: DocsConfig, optio
36
37
  declare function buildApiReferenceHtmlDocument(config: DocsConfig, options: BuildApiReferenceHtmlOptions): string;
37
38
  declare function buildApiReferenceHtmlDocumentAsync(config: DocsConfig, options: BuildApiReferenceHtmlOptions): Promise<string>;
38
39
  //#endregion
39
- export { type ApiReferenceFramework, type ApiReferenceRoute, type DocsMcpHttpHandlers, type DocsMcpNavigationNode, type DocsMcpNavigationTree, type DocsMcpPage, type DocsMcpResolvedConfig, type DocsMcpSource, type ResolvedApiReferenceConfig, buildApiReferenceHtmlDocument, buildApiReferenceHtmlDocumentAsync, buildApiReferenceOpenApiDocument, buildApiReferenceOpenApiDocumentAsync, buildApiReferencePageTitle, buildApiReferenceScalarCss, createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, normalizeDocsMcpRoute, resolveApiReferenceConfig, resolveDocsMcpConfig, runDocsMcpStdio };
40
+ export { type ApiReferenceFramework, type ApiReferenceRoute, type DocsMcpHttpHandlers, type DocsMcpNavigationNode, type DocsMcpNavigationTree, type DocsMcpPage, type DocsMcpResolvedConfig, type DocsMcpSource, type DocsSearchAdapter, type DocsSearchAdapterContext, type DocsSearchAdapterFactory, type DocsSearchConfig, type DocsSearchDocument, type DocsSearchQuery, type DocsSearchResult, type DocsSearchSourcePage, type McpDocsSearchConfig, type ResolvedApiReferenceConfig, buildApiReferenceHtmlDocument, buildApiReferenceHtmlDocumentAsync, buildApiReferenceOpenApiDocument, buildApiReferenceOpenApiDocumentAsync, buildApiReferencePageTitle, buildApiReferenceScalarCss, buildDocsSearchDocuments, createAlgoliaSearchAdapter, createCustomSearchAdapter, createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, createMcpSearchAdapter, createSimpleSearchAdapter, createTypesenseSearchAdapter, normalizeDocsMcpRoute, performDocsSearch, resolveApiReferenceConfig, resolveDocsMcpConfig, resolveSearchRequestConfig, runDocsMcpStdio };
package/dist/server.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { a as buildApiReferencePageTitle, i as buildApiReferenceOpenApiDocumentAsync, n as buildApiReferenceHtmlDocumentAsync, o as buildApiReferenceScalarCss, r as buildApiReferenceOpenApiDocument, s as resolveApiReferenceConfig, t as buildApiReferenceHtmlDocument } from "./api-reference-wh4_pwG8.mjs";
1
+ import { a as createSimpleSearchAdapter, c as resolveSearchRequestConfig, i as createMcpSearchAdapter, n as createAlgoliaSearchAdapter, o as createTypesenseSearchAdapter, r as createCustomSearchAdapter, s as performDocsSearch, t as buildDocsSearchDocuments } from "./search-BS6C5N1i.mjs";
2
+ import { a as buildApiReferencePageTitle, i as buildApiReferenceOpenApiDocumentAsync, n as buildApiReferenceHtmlDocumentAsync, o as buildApiReferenceScalarCss, r as buildApiReferenceOpenApiDocument, s as resolveApiReferenceConfig, t as buildApiReferenceHtmlDocument } from "./api-reference-DlfH-Y9c.mjs";
2
3
  import { createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, normalizeDocsMcpRoute, resolveDocsMcpConfig, runDocsMcpStdio } from "./mcp.mjs";
3
4
 
4
- export { buildApiReferenceHtmlDocument, buildApiReferenceHtmlDocumentAsync, buildApiReferenceOpenApiDocument, buildApiReferenceOpenApiDocumentAsync, buildApiReferencePageTitle, buildApiReferenceScalarCss, createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, normalizeDocsMcpRoute, resolveApiReferenceConfig, resolveDocsMcpConfig, runDocsMcpStdio };
5
+ export { buildApiReferenceHtmlDocument, buildApiReferenceHtmlDocumentAsync, buildApiReferenceOpenApiDocument, buildApiReferenceOpenApiDocumentAsync, buildApiReferencePageTitle, buildApiReferenceScalarCss, buildDocsSearchDocuments, createAlgoliaSearchAdapter, createCustomSearchAdapter, createDocsMcpHttpHandler, createDocsMcpServer, createFilesystemDocsMcpSource, createMcpSearchAdapter, createSimpleSearchAdapter, createTypesenseSearchAdapter, normalizeDocsMcpRoute, performDocsSearch, resolveApiReferenceConfig, resolveDocsMcpConfig, resolveSearchRequestConfig, runDocsMcpStdio };
@@ -614,6 +614,144 @@ interface DocsMcpConfig {
614
614
  /** Fine-grained tool toggles. Omitted tools stay enabled. */
615
615
  tools?: DocsMcpToolsConfig;
616
616
  }
617
+ type DocsSearchResultType = "page" | "heading" | "text";
618
+ interface DocsSearchSourcePage {
619
+ title: string;
620
+ url: string;
621
+ content: string;
622
+ description?: string;
623
+ rawContent?: string;
624
+ type?: "page" | "api" | "code" | "changelog";
625
+ locale?: string;
626
+ framework?: string;
627
+ version?: string;
628
+ tags?: string[];
629
+ }
630
+ interface DocsSearchDocument {
631
+ id: string;
632
+ url: string;
633
+ title: string;
634
+ content: string;
635
+ description?: string;
636
+ section?: string;
637
+ type: DocsSearchResultType;
638
+ locale?: string;
639
+ framework?: string;
640
+ version?: string;
641
+ tags?: string[];
642
+ }
643
+ interface DocsSearchResult {
644
+ id: string;
645
+ url: string;
646
+ content: string;
647
+ description?: string;
648
+ type: DocsSearchResultType;
649
+ score?: number;
650
+ section?: string;
651
+ }
652
+ interface DocsSearchQuery {
653
+ query: string;
654
+ limit?: number;
655
+ locale?: string;
656
+ pathname?: string;
657
+ }
658
+ interface DocsSearchAdapterContext {
659
+ pages: DocsSearchSourcePage[];
660
+ documents: DocsSearchDocument[];
661
+ locale?: string;
662
+ pathname?: string;
663
+ siteTitle?: string;
664
+ }
665
+ interface DocsSearchAdapter {
666
+ name: string;
667
+ index?(context: DocsSearchAdapterContext): Promise<void>;
668
+ search(query: DocsSearchQuery, context: DocsSearchAdapterContext): Promise<DocsSearchResult[]>;
669
+ }
670
+ type DocsSearchAdapterFactory = (context: DocsSearchAdapterContext) => DocsSearchAdapter | Promise<DocsSearchAdapter>;
671
+ interface DocsSearchChunkingConfig {
672
+ /**
673
+ * How docs content should be chunked before searching.
674
+ *
675
+ * - `"page"` keeps one search document per page
676
+ * - `"section"` splits pages by headings and improves precision
677
+ *
678
+ * @default "section"
679
+ */
680
+ strategy?: "page" | "section";
681
+ }
682
+ interface DocsSearchEmbeddingsConfig {
683
+ /**
684
+ * Embeddings provider used for hybrid / semantic search.
685
+ * The initial built-in provider is Ollama for local or self-hosted setups.
686
+ */
687
+ provider: "ollama";
688
+ /** Embedding model id, e.g. `embeddinggemma`. */
689
+ model: string;
690
+ /** Base URL of the embedding API. @default "http://127.0.0.1:11434" */
691
+ baseUrl?: string;
692
+ }
693
+ interface SimpleDocsSearchConfig {
694
+ provider?: "simple";
695
+ enabled?: boolean;
696
+ maxResults?: number;
697
+ chunking?: DocsSearchChunkingConfig;
698
+ }
699
+ interface AlgoliaDocsSearchConfig {
700
+ provider: "algolia";
701
+ enabled?: boolean;
702
+ appId: string;
703
+ indexName: string;
704
+ searchApiKey: string;
705
+ adminApiKey?: string;
706
+ maxResults?: number;
707
+ syncOnSearch?: boolean;
708
+ chunking?: DocsSearchChunkingConfig;
709
+ }
710
+ interface TypesenseDocsSearchConfig {
711
+ provider: "typesense";
712
+ enabled?: boolean;
713
+ baseUrl: string;
714
+ collection: string;
715
+ apiKey: string;
716
+ adminApiKey?: string;
717
+ maxResults?: number;
718
+ syncOnSearch?: boolean;
719
+ queryBy?: string[];
720
+ mode?: "keyword" | "hybrid";
721
+ embeddings?: DocsSearchEmbeddingsConfig;
722
+ chunking?: DocsSearchChunkingConfig;
723
+ }
724
+ interface McpDocsSearchConfig {
725
+ provider: "mcp";
726
+ enabled?: boolean;
727
+ /**
728
+ * Streamable HTTP MCP endpoint. Relative paths like `/api/docs/mcp` are resolved
729
+ * against the current docs API request URL.
730
+ */
731
+ endpoint: string;
732
+ /**
733
+ * Optional extra headers passed to the MCP endpoint on initialize/tool calls.
734
+ */
735
+ headers?: Record<string, string>;
736
+ /**
737
+ * MCP tool name used for search. Defaults to `search_docs`.
738
+ */
739
+ toolName?: string;
740
+ /**
741
+ * Override the MCP protocol version header when needed.
742
+ */
743
+ protocolVersion?: string;
744
+ maxResults?: number;
745
+ chunking?: DocsSearchChunkingConfig;
746
+ }
747
+ interface CustomDocsSearchConfig {
748
+ provider: "custom";
749
+ enabled?: boolean;
750
+ adapter: DocsSearchAdapter | DocsSearchAdapterFactory;
751
+ maxResults?: number;
752
+ chunking?: DocsSearchChunkingConfig;
753
+ }
754
+ type DocsSearchConfig = SimpleDocsSearchConfig | AlgoliaDocsSearchConfig | McpDocsSearchConfig | TypesenseDocsSearchConfig | CustomDocsSearchConfig;
617
755
  interface LastUpdatedConfig {
618
756
  /**
619
757
  * Whether to show the "Last updated" date.
@@ -1353,6 +1491,16 @@ interface DocsConfig {
1353
1491
  * ```
1354
1492
  */
1355
1493
  pageActions?: PageActionsConfig;
1494
+ /**
1495
+ * Built-in docs search configuration.
1496
+ *
1497
+ * - `true` or omitted → use the built-in simple search
1498
+ * - `{ provider: "typesense", ... }` → use Typesense
1499
+ * - `{ provider: "algolia", ... }` → use Algolia
1500
+ * - `{ provider: "custom", adapter }` → use a custom adapter
1501
+ * - `false` → disable docs search
1502
+ */
1503
+ search?: boolean | DocsSearchConfig;
1356
1504
  /**
1357
1505
  * Configuration for the "Last updated" date display.
1358
1506
  *
@@ -1481,4 +1629,4 @@ interface DocsConfig {
1481
1629
  og?: OGConfig;
1482
1630
  }
1483
1631
  //#endregion
1484
- export { SidebarConfig as A, OpenGraphImage as C, PageOpenGraph as D, PageFrontmatter as E, ThemeToggleConfig as F, TypographyConfig as I, UIConfig as L, SidebarNode as M, SidebarPageNode as N, PageTwitter as O, SidebarTree as P, OpenDocsProvider as S, PageActionsConfig as T, GithubConfig as _, CopyMarkdownConfig as a, OGConfig as b, DocsFeedbackValue as c, DocsMcpToolsConfig as d, DocsMetadata as f, FontStyle as g, FeedbackConfig as h, CodeBlockCopyData as i, SidebarFolderNode as j, SidebarComponentProps as k, DocsI18nConfig as l, DocsTheme as m, ApiReferenceConfig as n, DocsConfig as o, DocsNav as p, BreadcrumbConfig as r, DocsFeedbackData as s, AIConfig as t, DocsMcpConfig as u, LastUpdatedConfig as v, OrderingItem as w, OpenDocsConfig as x, LlmsTxtConfig as y };
1632
+ export { UIConfig as $, GithubConfig as A, PageFrontmatter as B, DocsSearchQuery as C, DocsTheme as D, DocsSearchSourcePage as E, OpenDocsConfig as F, SidebarFolderNode as G, PageTwitter as H, OpenDocsProvider as I, SidebarTree as J, SidebarNode as K, OpenGraphImage as L, LlmsTxtConfig as M, McpDocsSearchConfig as N, FeedbackConfig as O, OGConfig as P, TypographyConfig as Q, OrderingItem as R, DocsSearchEmbeddingsConfig as S, DocsSearchResultType as T, SidebarComponentProps as U, PageOpenGraph as V, SidebarConfig as W, ThemeToggleConfig as X, SimpleDocsSearchConfig as Y, TypesenseDocsSearchConfig as Z, DocsSearchAdapterContext as _, CodeBlockCopyData as a, DocsSearchConfig as b, DocsConfig as c, DocsI18nConfig as d, DocsMcpConfig as f, DocsSearchAdapter as g, DocsNav as h, BreadcrumbConfig as i, LastUpdatedConfig as j, FontStyle as k, DocsFeedbackData as l, DocsMetadata as m, AlgoliaDocsSearchConfig as n, CopyMarkdownConfig as o, DocsMcpToolsConfig as p, SidebarPageNode as q, ApiReferenceConfig as r, CustomDocsSearchConfig as s, AIConfig as t, DocsFeedbackValue as u, DocsSearchAdapterFactory as v, DocsSearchResult as w, DocsSearchDocument as x, DocsSearchChunkingConfig as y, PageActionsConfig as z };
@@ -0,0 +1,138 @@
1
+ import { c as installCommand, i as detectPackageManagerFromLockfile, o as exec, s as fileExists, t as detectFramework } from "./utils-CRhME2g-.mjs";
2
+ import path from "node:path";
3
+ import pc from "picocolors";
4
+ import * as p from "@clack/prompts";
5
+
6
+ //#region src/cli/upgrade.ts
7
+ /**
8
+ * Upgrade @farming-labs/* packages to latest.
9
+ * Detects framework from package.json by default, or use --framework (next, tanstack-start, nuxt, sveltekit, astro).
10
+ */
11
+ const PRESETS = [
12
+ "next",
13
+ "tanstack-start",
14
+ "nuxt",
15
+ "sveltekit",
16
+ "astro"
17
+ ];
18
+ const PACKAGES_BY_FRAMEWORK = {
19
+ nextjs: [
20
+ "@farming-labs/docs",
21
+ "@farming-labs/theme",
22
+ "@farming-labs/next"
23
+ ],
24
+ "tanstack-start": [
25
+ "@farming-labs/docs",
26
+ "@farming-labs/theme",
27
+ "@farming-labs/tanstack-start"
28
+ ],
29
+ nuxt: [
30
+ "@farming-labs/docs",
31
+ "@farming-labs/nuxt",
32
+ "@farming-labs/nuxt-theme"
33
+ ],
34
+ sveltekit: [
35
+ "@farming-labs/docs",
36
+ "@farming-labs/svelte",
37
+ "@farming-labs/svelte-theme"
38
+ ],
39
+ astro: [
40
+ "@farming-labs/docs",
41
+ "@farming-labs/astro",
42
+ "@farming-labs/astro-theme"
43
+ ]
44
+ };
45
+ function presetFromFramework(fw) {
46
+ return fw === "nextjs" ? "next" : fw;
47
+ }
48
+ function frameworkFromPreset(preset) {
49
+ return preset === "next" ? "nextjs" : preset;
50
+ }
51
+ /** Return package list for a framework (for testing and CLI). */
52
+ function getPackagesForFramework(framework) {
53
+ return PACKAGES_BY_FRAMEWORK[framework];
54
+ }
55
+ /** Build the install command for upgrade (for testing). */
56
+ function buildUpgradeCommand(framework, tag, pm) {
57
+ const packagesWithTag = PACKAGES_BY_FRAMEWORK[framework].map((name) => `${name}@${tag}`);
58
+ return `${installCommand(pm)} ${packagesWithTag.join(" ")}`;
59
+ }
60
+ async function upgrade(options = {}) {
61
+ const cwd = process.cwd();
62
+ const tag = options.tag ?? "latest";
63
+ p.intro(pc.bgCyan(pc.black(" @farming-labs/docs upgrade ")));
64
+ if (!fileExists(path.join(cwd, "package.json"))) {
65
+ p.log.error("No package.json found in the current directory. Run this from your project root.");
66
+ process.exit(1);
67
+ }
68
+ let framework = null;
69
+ let preset;
70
+ if (options.framework) {
71
+ const raw = options.framework.toLowerCase().trim();
72
+ const normalized = raw === "nextjs" ? "next" : raw;
73
+ if (!PRESETS.includes(normalized)) {
74
+ p.log.error(`Invalid framework ${pc.cyan(options.framework)}. Use one of: ${PRESETS.map((t) => pc.cyan(t)).join(", ")}`);
75
+ process.exit(1);
76
+ }
77
+ preset = normalized;
78
+ framework = frameworkFromPreset(preset);
79
+ } else {
80
+ const detected = detectFramework(cwd);
81
+ if (!detected) {
82
+ p.log.error("Could not detect a supported framework (Next.js, TanStack Start, Nuxt, SvelteKit, Astro). Use " + pc.cyan("--framework <next|tanstack-start|nuxt|sveltekit|astro>") + " to specify.");
83
+ process.exit(1);
84
+ }
85
+ framework = detected;
86
+ preset = presetFromFramework(framework);
87
+ }
88
+ let pm = detectPackageManagerFromLockfile(cwd);
89
+ if (pm) p.log.info(`Detected ${pc.cyan(pm)} from lockfile`);
90
+ else {
91
+ const pmAnswer = await p.select({
92
+ message: "Which package manager do you want to use for this upgrade?",
93
+ options: [
94
+ {
95
+ value: "pnpm",
96
+ label: "pnpm",
97
+ hint: "Use pnpm add"
98
+ },
99
+ {
100
+ value: "npm",
101
+ label: "npm",
102
+ hint: "Use npm add"
103
+ },
104
+ {
105
+ value: "yarn",
106
+ label: "yarn",
107
+ hint: "Use yarn add"
108
+ },
109
+ {
110
+ value: "bun",
111
+ label: "bun",
112
+ hint: "Use bun add"
113
+ }
114
+ ]
115
+ });
116
+ if (p.isCancel(pmAnswer)) {
117
+ p.outro(pc.red("Upgrade cancelled."));
118
+ process.exit(0);
119
+ }
120
+ pm = pmAnswer;
121
+ p.log.info(`Using ${pc.cyan(pm)} as package manager`);
122
+ }
123
+ const cmd = buildUpgradeCommand(framework, tag, pm);
124
+ const packages = getPackagesForFramework(framework);
125
+ p.log.step(`Upgrading ${preset} docs packages to ${tag}...`);
126
+ p.log.message(pc.dim(packages.join(", ")));
127
+ try {
128
+ exec(cmd, cwd);
129
+ p.log.success(`Packages upgraded to ${tag}.`);
130
+ p.outro(pc.green("Done. Run your dev server to confirm everything works."));
131
+ } catch {
132
+ p.log.error("Upgrade failed. Try running manually:\n " + pc.cyan(cmd));
133
+ process.exit(1);
134
+ }
135
+ }
136
+
137
+ //#endregion
138
+ export { upgrade };
@@ -0,0 +1,145 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execSync, spawn } from "node:child_process";
4
+
5
+ //#region src/cli/utils.ts
6
+ function detectFramework(cwd) {
7
+ const pkgPath = path.join(cwd, "package.json");
8
+ if (!fs.existsSync(pkgPath)) return null;
9
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
10
+ const allDeps = {
11
+ ...pkg.dependencies,
12
+ ...pkg.devDependencies
13
+ };
14
+ if (allDeps["next"]) return "nextjs";
15
+ if (allDeps["@tanstack/react-start"]) return "tanstack-start";
16
+ if (allDeps["@sveltejs/kit"]) return "sveltekit";
17
+ if (allDeps["astro"]) return "astro";
18
+ if (allDeps["nuxt"]) return "nuxt";
19
+ return null;
20
+ }
21
+ function detectPackageManagerFromLockfile(cwd) {
22
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
23
+ if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) return "bun";
24
+ if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
25
+ if (fs.existsSync(path.join(cwd, "package-lock.json"))) return "npm";
26
+ return null;
27
+ }
28
+ function installCommand(pm) {
29
+ return pm === "yarn" ? "yarn add" : `${pm} add`;
30
+ }
31
+ function devInstallCommand(pm) {
32
+ if (pm === "yarn") return "yarn add -D";
33
+ if (pm === "npm") return "npm install -D";
34
+ return `${pm} add -D`;
35
+ }
36
+ /**
37
+ * Write a file, creating parent directories as needed.
38
+ * Returns true if the file was written, false if it already existed and was skipped.
39
+ */
40
+ function writeFileSafe(filePath, content, overwrite = false) {
41
+ if (fs.existsSync(filePath) && !overwrite) return false;
42
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
43
+ fs.writeFileSync(filePath, content, "utf-8");
44
+ return true;
45
+ }
46
+ /**
47
+ * Check if a file exists.
48
+ */
49
+ function fileExists(filePath) {
50
+ return fs.existsSync(filePath);
51
+ }
52
+ /**
53
+ * Read a file, returning null if it does not exist.
54
+ */
55
+ function readFileSafe(filePath) {
56
+ if (!fs.existsSync(filePath)) return null;
57
+ return fs.readFileSync(filePath, "utf-8");
58
+ }
59
+ /** Common locations where global CSS files live in Next.js / SvelteKit projects. */
60
+ const GLOBAL_CSS_CANDIDATES = [
61
+ "app/globals.css",
62
+ "app/global.css",
63
+ "src/app/globals.css",
64
+ "src/app/global.css",
65
+ "src/app.css",
66
+ "src/styles/app.css",
67
+ "styles/globals.css",
68
+ "styles/global.css",
69
+ "src/styles/globals.css",
70
+ "src/styles/global.css",
71
+ "assets/css/main.css",
72
+ "assets/main.css"
73
+ ];
74
+ /**
75
+ * Find existing global CSS files in the project.
76
+ * Returns relative paths that exist.
77
+ */
78
+ function detectGlobalCssFiles(cwd) {
79
+ return GLOBAL_CSS_CANDIDATES.filter((rel) => fs.existsSync(path.join(cwd, rel)));
80
+ }
81
+ /**
82
+ * Detect whether the Next.js project uses `app` or `src/app` for the App Router.
83
+ * Returns the directory that exists; if both exist, prefers src/app; if neither, returns null.
84
+ */
85
+ function detectNextAppDir(cwd) {
86
+ const hasSrcApp = fs.existsSync(path.join(cwd, "src", "app"));
87
+ const hasApp = fs.existsSync(path.join(cwd, "app"));
88
+ if (hasSrcApp) return "src/app";
89
+ if (hasApp) return "app";
90
+ return null;
91
+ }
92
+ /**
93
+ * Run a shell command synchronously, inheriting stdio.
94
+ */
95
+ function exec(command, cwd) {
96
+ execSync(command, {
97
+ cwd,
98
+ stdio: "inherit"
99
+ });
100
+ }
101
+ /**
102
+ * Spawn a process and wait for a specific string in stdout,
103
+ * then resolve with the child process (still running).
104
+ */
105
+ function spawnAndWaitFor(command, args, cwd, waitFor, timeoutMs = 6e4) {
106
+ return new Promise((resolve, reject) => {
107
+ const child = spawn(command, args, {
108
+ cwd,
109
+ stdio: [
110
+ "ignore",
111
+ "pipe",
112
+ "pipe"
113
+ ],
114
+ shell: true
115
+ });
116
+ let output = "";
117
+ const timer = setTimeout(() => {
118
+ child.kill();
119
+ reject(/* @__PURE__ */ new Error(`Timed out waiting for "${waitFor}" after ${timeoutMs}ms`));
120
+ }, timeoutMs);
121
+ child.stdout?.on("data", (data) => {
122
+ const text = data.toString();
123
+ output += text;
124
+ process.stdout.write(text);
125
+ if (output.includes(waitFor)) {
126
+ clearTimeout(timer);
127
+ resolve(child);
128
+ }
129
+ });
130
+ child.stderr?.on("data", (data) => {
131
+ process.stderr.write(data.toString());
132
+ });
133
+ child.on("error", (err) => {
134
+ clearTimeout(timer);
135
+ reject(err);
136
+ });
137
+ child.on("close", (code) => {
138
+ clearTimeout(timer);
139
+ if (!output.includes(waitFor)) reject(/* @__PURE__ */ new Error(`Process exited with code ${code} before "${waitFor}" appeared`));
140
+ });
141
+ });
142
+ }
143
+
144
+ //#endregion
145
+ export { devInstallCommand as a, installCommand as c, writeFileSafe as d, detectPackageManagerFromLockfile as i, readFileSafe as l, detectGlobalCssFiles as n, exec as o, detectNextAppDir as r, fileExists as s, detectFramework as t, spawnAndWaitFor as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/docs",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Modern, flexible MDX-based docs framework — core types, config, and CLI",
5
5
  "keywords": [
6
6
  "docs",