mdorigin 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.
Files changed (40) hide show
  1. package/README.md +97 -9
  2. package/dist/adapters/cloudflare.d.ts +7 -1
  3. package/dist/adapters/cloudflare.js +11 -1
  4. package/dist/adapters/node.d.ts +4 -0
  5. package/dist/adapters/node.js +51 -11
  6. package/dist/cli/build-cloudflare.js +11 -4
  7. package/dist/cli/build-index.js +17 -3
  8. package/dist/cli/build-search.d.ts +1 -0
  9. package/dist/cli/build-search.js +45 -0
  10. package/dist/cli/dev.js +14 -4
  11. package/dist/cli/main.js +15 -3
  12. package/dist/cli/search.d.ts +1 -0
  13. package/dist/cli/search.js +36 -0
  14. package/dist/cloudflare.d.ts +3 -0
  15. package/dist/cloudflare.js +67 -6
  16. package/dist/core/api.d.ts +13 -0
  17. package/dist/core/api.js +160 -0
  18. package/dist/core/content-store.js +5 -0
  19. package/dist/core/content-type.d.ts +1 -0
  20. package/dist/core/content-type.js +3 -0
  21. package/dist/core/directory-index.d.ts +1 -1
  22. package/dist/core/directory-index.js +5 -1
  23. package/dist/core/extensions.d.ts +66 -0
  24. package/dist/core/extensions.js +86 -0
  25. package/dist/core/markdown.d.ts +4 -0
  26. package/dist/core/markdown.js +53 -0
  27. package/dist/core/request-handler.d.ts +6 -0
  28. package/dist/core/request-handler.js +211 -68
  29. package/dist/core/site-config.d.ts +18 -0
  30. package/dist/core/site-config.js +88 -16
  31. package/dist/html/template.d.ts +8 -0
  32. package/dist/html/template.js +228 -12
  33. package/dist/html/theme.js +254 -9
  34. package/dist/index-builder.d.ts +3 -1
  35. package/dist/index-builder.js +82 -45
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.js +1 -0
  38. package/dist/search.d.ts +59 -0
  39. package/dist/search.js +370 -0
  40. package/package.json +20 -5
package/README.md CHANGED
@@ -2,34 +2,122 @@
2
2
 
3
3
  `mdorigin` is a markdown-first publishing engine.
4
4
 
5
- It treats markdown as the only source of truth, serves raw `.md` directly for agents, renders `.html` views for humans from the same directory tree, can return markdown from extensionless routes when clients send `Accept: text/markdown`, and supports frontmatter aliases for old URL redirects.
5
+ It treats markdown as the only source of truth, serves raw `.md` directly for agents, renders HTML for humans from the same directory tree, and can expose the same content to both browsers and tools through stable routes.
6
+
7
+ ## Core Principle
8
+
9
+ `mdorigin` is not meant to become a template system.
10
+
11
+ Its core is:
12
+
13
+ - routing rules
14
+ - markdown tree normalization
15
+ - document, index, asset, search, and site semantics
16
+ - stable page models derived from the same content tree
17
+
18
+ That means `mdorigin` should own content semantics, while page rendering remains extensible. In practice, this is the direction for advanced customization: users should be able to replace page-level rendering with code, without replacing the routing and content kernel itself.
19
+
20
+ ## Why mdorigin
21
+
22
+ - markdown stays directly accessible at `.md` routes
23
+ - extensionless routes render human-friendly HTML from the same files
24
+ - `README.md`, `index.md`, and `SKILL.md` all fit into one routing model
25
+ - the same core works in local preview and Cloudflare Workers
26
+ - optional search is powered by [`indexbind`](https://github.com/jolestar/indexbind)
6
27
 
7
28
  ## Install
8
29
 
9
30
  ```bash
10
- npm install --save-dev mdorigin
31
+ npm install -g mdorigin
11
32
  ```
12
33
 
13
- Then run it with `npx`:
34
+ Then run it directly:
14
35
 
15
36
  ```bash
16
- npx mdorigin dev --root docs/site
37
+ mdorigin dev --root docs/site
17
38
  ```
18
39
 
19
- ## Repo Development
40
+ If you prefer a project-local install instead, use `npm install --save-dev mdorigin` and run it with `npx --no-install mdorigin ...`.
41
+
42
+ ## Quick Start
20
43
 
21
44
  ```bash
22
- npm install
23
- npm run check
24
- npm run dev -- --root docs/site
25
- npm run build:index -- --root docs/site
45
+ mdorigin dev --root docs/site
46
+ mdorigin build index --root docs/site
47
+ mdorigin build cloudflare --root docs/site
26
48
  ```
27
49
 
50
+ That is enough to preview a site locally, keep directory indexes up to date, and generate a Cloudflare Worker bundle.
51
+
52
+ ## Code-Based Extensions
53
+
54
+ `mdorigin` now supports a code config alongside `mdorigin.config.json`.
55
+
56
+ Use `mdorigin.config.ts` when you want to customize rendering or indexing with code instead of asking for a template system:
57
+
58
+ ```ts
59
+ export default {
60
+ siteTitle: "My Site",
61
+ plugins: [
62
+ {
63
+ name: "custom-footer",
64
+ renderFooter() {
65
+ return '<footer class="custom-footer">Built with mdorigin</footer>';
66
+ },
67
+ },
68
+ ],
69
+ };
70
+ ```
71
+
72
+ Current stable hooks are:
73
+
74
+ - `transformIndex(entries, context)`
75
+ - `renderHeader(context)`
76
+ - `renderFooter(context)`
77
+ - `renderPage(page, context, next)`
78
+ - `transformHtml(html, context)`
79
+
80
+ The intended boundary is:
81
+
82
+ - `mdorigin` owns routing and normalized content semantics
83
+ - plugins may replace page rendering
84
+ - plugins do not replace the request kernel itself
85
+
86
+ ## Optional Search
87
+
88
+ `mdorigin` can build a local retrieval bundle through the optional [`indexbind`](https://github.com/jolestar/indexbind) package. For the retrieval engine itself, see the `indexbind` docs: <https://indexbind.jolestar.workers.dev>.
89
+
90
+ ```bash
91
+ npm install indexbind
92
+ mdorigin build search --root docs/site
93
+ mdorigin search --index dist/search "cloudflare deploy"
94
+ ```
95
+
96
+ `build search` now defaults to the higher-quality `model2vec` backend. If you need a smaller or faster fallback, you can opt back into hashing:
97
+
98
+ ```bash
99
+ mdorigin build search --root docs/site --embedding-backend hashing
100
+ ```
101
+
102
+ To expose the same search bundle from the site runtime:
103
+
104
+ ```bash
105
+ mdorigin dev --root docs/site --search dist/search
106
+ mdorigin build cloudflare --root docs/site --search dist/search
107
+ ```
108
+
109
+ Runtime endpoints:
110
+
111
+ - `/api/search?q=cloudflare+deploy`
112
+ - `/api/openapi.json`
113
+
28
114
  ## Docs
29
115
 
116
+ - Docs site: <https://mdorigin.jolestar.workers.dev>
30
117
  - Getting started: [`docs/site/guides/getting-started.md`](docs/site/guides/getting-started.md)
31
118
  - Routing model: [`docs/site/concepts/routing.md`](docs/site/concepts/routing.md)
32
119
  - Directory indexes: [`docs/site/concepts/directory-indexes.md`](docs/site/concepts/directory-indexes.md)
33
120
  - Configuration: [`docs/site/reference/configuration.md`](docs/site/reference/configuration.md)
34
121
  - CLI: [`docs/site/reference/cli.md`](docs/site/reference/cli.md)
122
+ - Search setup: [`docs/site/guides/getting-started.md`](docs/site/guides/getting-started.md#quick-start)
35
123
  - Cloudflare deployment: [`docs/site/guides/cloudflare.md`](docs/site/guides/cloudflare.md)
@@ -1,5 +1,7 @@
1
1
  import { type ContentEntryKind } from '../core/content-store.js';
2
+ import type { MdoPlugin } from '../core/extensions.js';
2
3
  import type { ResolvedSiteConfig } from '../core/site-config.js';
4
+ import { type SearchBundleEntry } from '../search.js';
3
5
  export interface CloudflareManifestEntry {
4
6
  path: string;
5
7
  kind: ContentEntryKind;
@@ -10,8 +12,12 @@ export interface CloudflareManifestEntry {
10
12
  export interface CloudflareManifest {
11
13
  entries: CloudflareManifestEntry[];
12
14
  siteConfig?: ResolvedSiteConfig;
15
+ searchEntries?: SearchBundleEntry[];
13
16
  }
14
17
  export interface ExportedHandlerLike {
15
18
  fetch(request: Request): Promise<Response>;
16
19
  }
17
- export declare function createCloudflareWorker(manifest: CloudflareManifest): ExportedHandlerLike;
20
+ export interface CreateCloudflareWorkerOptions {
21
+ plugins?: MdoPlugin[];
22
+ }
23
+ export declare function createCloudflareWorker(manifest: CloudflareManifest, options?: CreateCloudflareWorkerOptions): ExportedHandlerLike;
@@ -1,6 +1,7 @@
1
1
  import { MemoryContentStore, } from '../core/content-store.js';
2
2
  import { handleSiteRequest } from '../core/request-handler.js';
3
- export function createCloudflareWorker(manifest) {
3
+ import { createSearchApiFromBundle } from '../search.js';
4
+ export function createCloudflareWorker(manifest, options = {}) {
4
5
  const store = new MemoryContentStore(manifest.entries.map((entry) => {
5
6
  if (entry.kind === 'text') {
6
7
  return {
@@ -17,6 +18,9 @@ export function createCloudflareWorker(manifest) {
17
18
  bytes: decodeBase64(entry.base64 ?? ''),
18
19
  };
19
20
  }));
21
+ const searchApi = manifest.searchEntries && manifest.searchEntries.length > 0
22
+ ? createSearchApiFromBundle(manifest.searchEntries)
23
+ : undefined;
20
24
  return {
21
25
  async fetch(request) {
22
26
  const url = new URL(request.url);
@@ -37,10 +41,16 @@ export function createCloudflareWorker(manifest) {
37
41
  socialLinks: [],
38
42
  editLink: undefined,
39
43
  showHomeIndex: true,
44
+ catalogInitialPostCount: 10,
45
+ catalogLoadMoreStep: 10,
40
46
  siteTitleConfigured: false,
41
47
  siteDescriptionConfigured: false,
42
48
  },
43
49
  acceptHeader: request.headers.get('accept') ?? undefined,
50
+ searchParams: url.searchParams,
51
+ requestUrl: request.url,
52
+ searchApi,
53
+ plugins: options.plugins,
44
54
  });
45
55
  const headers = new Headers(siteResponse.headers);
46
56
  const body = siteResponse.body instanceof Uint8Array
@@ -1,10 +1,14 @@
1
1
  import { type IncomingMessage, type ServerResponse } from 'node:http';
2
2
  import type { ContentStore } from '../core/content-store.js';
3
+ import type { MdoPlugin } from '../core/extensions.js';
3
4
  import type { ResolvedSiteConfig } from '../core/site-config.js';
5
+ import type { SearchApi } from '../search.js';
4
6
  export interface NodeAdapterOptions {
5
7
  rootDir: string;
6
8
  draftMode: 'include' | 'exclude';
7
9
  siteConfig: ResolvedSiteConfig;
10
+ searchApi?: SearchApi;
11
+ plugins?: MdoPlugin[];
8
12
  }
9
13
  export declare function createFileSystemContentStore(rootDir: string): ContentStore;
10
14
  export declare function createNodeRequestListener(options: NodeAdapterOptions): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { createServer } from 'node:http';
2
- import { readFile, readdir } from 'node:fs/promises';
2
+ import { readFile, readdir, stat } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { getMediaTypeForPath, isLikelyTextPath, normalizeContentPath, normalizeDirectoryPath, } from '../core/content-store.js';
5
5
  import { handleSiteRequest } from '../core/request-handler.js';
@@ -12,10 +12,14 @@ export function createFileSystemContentStore(rootDir) {
12
12
  return null;
13
13
  }
14
14
  const filePath = path.resolve(resolvedRootDir, normalizedPath);
15
- if (!filePath.startsWith(`${resolvedRootDir}${path.sep}`) && filePath !== resolvedRootDir) {
15
+ if (!isVisiblePathWithinRoot(resolvedRootDir, filePath)) {
16
16
  return null;
17
17
  }
18
18
  try {
19
+ const fileStats = await stat(filePath);
20
+ if (!fileStats.isFile()) {
21
+ return null;
22
+ }
19
23
  const mediaType = getMediaTypeForPath(normalizedPath);
20
24
  if (isLikelyTextPath(normalizedPath)) {
21
25
  const text = await readFile(filePath, 'utf8');
@@ -47,21 +51,49 @@ export function createFileSystemContentStore(rootDir) {
47
51
  return null;
48
52
  }
49
53
  const directoryPath = path.resolve(resolvedRootDir, normalizedPath);
50
- if (!directoryPath.startsWith(`${resolvedRootDir}${path.sep}`) &&
51
- directoryPath !== resolvedRootDir) {
54
+ if (!isVisiblePathWithinRoot(resolvedRootDir, directoryPath)) {
52
55
  return null;
53
56
  }
54
57
  try {
58
+ const directoryStats = await stat(directoryPath);
59
+ if (!directoryStats.isDirectory()) {
60
+ return null;
61
+ }
55
62
  const entries = await readdir(directoryPath, { withFileTypes: true });
56
- return entries
63
+ const resolvedEntries = await Promise.all(entries
57
64
  .filter((entry) => !entry.name.startsWith('.'))
58
- .map((entry) => ({
59
- name: entry.name,
60
- path: normalizedPath === ''
65
+ .map(async (entry) => {
66
+ const childVisiblePath = normalizedPath === ''
61
67
  ? entry.name
62
- : `${normalizedPath}/${entry.name}`,
63
- kind: entry.isDirectory() ? 'directory' : 'file',
64
- }))
68
+ : `${normalizedPath}/${entry.name}`;
69
+ const childFilePath = path.resolve(resolvedRootDir, childVisiblePath);
70
+ try {
71
+ const childStats = await stat(childFilePath);
72
+ if (childStats.isDirectory()) {
73
+ return {
74
+ name: entry.name,
75
+ path: childVisiblePath,
76
+ kind: 'directory',
77
+ };
78
+ }
79
+ if (childStats.isFile()) {
80
+ return {
81
+ name: entry.name,
82
+ path: childVisiblePath,
83
+ kind: 'file',
84
+ };
85
+ }
86
+ }
87
+ catch (error) {
88
+ if (isNodeNotFound(error)) {
89
+ return null;
90
+ }
91
+ throw error;
92
+ }
93
+ return null;
94
+ }));
95
+ return resolvedEntries
96
+ .filter((entry) => entry !== null)
65
97
  .sort((left, right) => {
66
98
  if (left.kind !== right.kind) {
67
99
  return left.kind === 'directory' ? -1 : 1;
@@ -78,6 +110,10 @@ export function createFileSystemContentStore(rootDir) {
78
110
  },
79
111
  };
80
112
  }
113
+ function isVisiblePathWithinRoot(rootDir, candidatePath) {
114
+ return (candidatePath === rootDir ||
115
+ candidatePath.startsWith(`${rootDir}${path.sep}`));
116
+ }
81
117
  export function createNodeRequestListener(options) {
82
118
  const store = createFileSystemContentStore(options.rootDir);
83
119
  return async function onRequest(request, response) {
@@ -87,6 +123,10 @@ export function createNodeRequestListener(options) {
87
123
  draftMode: options.draftMode,
88
124
  siteConfig: options.siteConfig,
89
125
  acceptHeader: request.headers.accept,
126
+ searchParams: url.searchParams,
127
+ requestUrl: url.toString(),
128
+ searchApi: options.searchApi,
129
+ plugins: options.plugins,
90
130
  });
91
131
  response.statusCode = siteResponse.status;
92
132
  for (const [headerName, headerValue] of Object.entries(siteResponse.headers)) {
@@ -1,26 +1,28 @@
1
1
  import path from 'node:path';
2
2
  import { createFileSystemContentStore } from '../adapters/node.js';
3
3
  import { writeCloudflareBundle } from '../cloudflare.js';
4
- import { applySiteConfigFrontmatterDefaults, loadSiteConfig, } from '../core/site-config.js';
4
+ import { applySiteConfigFrontmatterDefaults, loadUserSiteConfig, } from '../core/site-config.js';
5
5
  export async function runBuildCloudflareCommand(argv) {
6
6
  const args = parseArgs(argv);
7
7
  if (!args.root) {
8
- console.error('Usage: mdorigin build cloudflare --root <content-dir> [--out ./dist/cloudflare] [--config mdorigin.config.json]');
8
+ console.error('Usage: mdorigin build cloudflare --root <content-dir> [--out ./dist/cloudflare] [--config <config-file>] [--search ./dist/search]');
9
9
  process.exitCode = 1;
10
10
  return;
11
11
  }
12
12
  const rootDir = path.resolve(args.root);
13
- const loadedSiteConfig = await loadSiteConfig({
13
+ const loadedConfig = await loadUserSiteConfig({
14
14
  cwd: process.cwd(),
15
15
  rootDir,
16
16
  configPath: args.config,
17
17
  });
18
18
  const store = createFileSystemContentStore(rootDir);
19
- const siteConfig = await applySiteConfigFrontmatterDefaults(store, loadedSiteConfig);
19
+ const siteConfig = await applySiteConfigFrontmatterDefaults(store, loadedConfig.siteConfig);
20
20
  const result = await writeCloudflareBundle({
21
21
  rootDir,
22
22
  outDir: path.resolve(args.out ?? 'dist/cloudflare'),
23
23
  siteConfig,
24
+ searchDir: args.search ? path.resolve(args.search) : undefined,
25
+ configModulePath: loadedConfig.configModulePath,
24
26
  });
25
27
  console.log(`cloudflare worker written to ${result.workerFile}`);
26
28
  }
@@ -42,6 +44,11 @@ function parseArgs(argv) {
42
44
  if (argument === '--config' && nextValue) {
43
45
  result.config = nextValue;
44
46
  index += 1;
47
+ continue;
48
+ }
49
+ if (argument === '--search' && nextValue) {
50
+ result.search = nextValue;
51
+ index += 1;
45
52
  }
46
53
  }
47
54
  return result;
@@ -1,15 +1,24 @@
1
1
  import path from 'node:path';
2
2
  import { buildDirectoryIndexes } from '../index-builder.js';
3
+ import { loadUserSiteConfig } from '../core/site-config.js';
3
4
  export async function runBuildIndexCommand(argv) {
4
5
  const args = parseArgs(argv);
5
6
  if (!args.root && !args.dir) {
6
- console.error('Usage: mdorigin build index (--root <content-dir> | --dir <content-dir>)');
7
+ console.error('Usage: mdorigin build index (--root <content-dir> | --dir <content-dir>) [--config <config-file>]');
7
8
  process.exitCode = 1;
8
9
  return;
9
10
  }
11
+ const rootDir = args.root ? path.resolve(args.root) : undefined;
12
+ const dir = args.dir ? path.resolve(args.dir) : undefined;
13
+ const loadedConfig = await loadUserSiteConfig({
14
+ cwd: process.cwd(),
15
+ rootDir: rootDir ?? dir,
16
+ configPath: args.config,
17
+ });
10
18
  const result = await buildDirectoryIndexes({
11
- rootDir: args.root ? path.resolve(args.root) : undefined,
12
- dir: args.dir ? path.resolve(args.dir) : undefined,
19
+ rootDir,
20
+ dir,
21
+ plugins: loadedConfig.plugins,
13
22
  });
14
23
  console.log(`updated ${result.updatedFiles.length} index file(s)`);
15
24
  if (result.skippedDirectories.length > 0) {
@@ -29,6 +38,11 @@ function parseArgs(argv) {
29
38
  if (argument === '--dir' && nextValue) {
30
39
  result.dir = nextValue;
31
40
  index += 1;
41
+ continue;
42
+ }
43
+ if (argument === '--config' && nextValue) {
44
+ result.config = nextValue;
45
+ index += 1;
32
46
  }
33
47
  }
34
48
  return result;
@@ -0,0 +1 @@
1
+ export declare function runBuildSearchCommand(rawArgs: string[]): Promise<void>;
@@ -0,0 +1,45 @@
1
+ import path from 'node:path';
2
+ import { buildSearchBundle } from '../search.js';
3
+ import { applySiteConfigFrontmatterDefaults, loadUserSiteConfig, } from '../core/site-config.js';
4
+ import { createFileSystemContentStore } from '../adapters/node.js';
5
+ export async function runBuildSearchCommand(rawArgs) {
6
+ const args = parseArgs(rawArgs);
7
+ if (!args.root) {
8
+ throw new Error('Usage: mdorigin build search --root <content-dir> [--out ./dist/search] [--embedding-backend model2vec|hashing] [--model sentence-transformers/all-MiniLM-L6-v2] [--config <config-file>]');
9
+ }
10
+ const rootDir = path.resolve(args.root);
11
+ const store = createFileSystemContentStore(rootDir);
12
+ const loadedConfig = await loadUserSiteConfig({
13
+ rootDir,
14
+ configPath: args.config,
15
+ });
16
+ const siteConfig = await applySiteConfigFrontmatterDefaults(store, loadedConfig.siteConfig);
17
+ const result = await buildSearchBundle({
18
+ rootDir,
19
+ outDir: path.resolve(args.out ?? 'dist/search'),
20
+ siteConfig,
21
+ embeddingBackend: args.embeddingBackend ?? 'model2vec',
22
+ model: args.model,
23
+ });
24
+ console.log(`search bundle written to ${result.outputDir} (${result.documentCount} documents, ${result.chunkCount} chunks)`);
25
+ }
26
+ function parseArgs(rawArgs) {
27
+ const parsed = {};
28
+ for (let index = 0; index < rawArgs.length; index += 1) {
29
+ const arg = rawArgs[index];
30
+ if (arg.startsWith('--')) {
31
+ const value = rawArgs[index + 1];
32
+ if (value && !value.startsWith('--')) {
33
+ parsed[arg.slice(2)] = value;
34
+ index += 1;
35
+ }
36
+ }
37
+ }
38
+ return {
39
+ root: parsed.root,
40
+ out: parsed.out,
41
+ embeddingBackend: parsed['embedding-backend'],
42
+ model: parsed.model,
43
+ config: parsed.config,
44
+ };
45
+ }
package/dist/cli/dev.js CHANGED
@@ -1,26 +1,31 @@
1
1
  import path from 'node:path';
2
2
  import { createFileSystemContentStore, createNodeServer } from '../adapters/node.js';
3
- import { applySiteConfigFrontmatterDefaults, loadSiteConfig, } from '../core/site-config.js';
3
+ import { applySiteConfigFrontmatterDefaults, loadUserSiteConfig, } from '../core/site-config.js';
4
+ import { createSearchApiFromDirectory } from '../search.js';
4
5
  export async function runDevCommand(argv) {
5
6
  const args = parseArgs(argv);
6
7
  if (!args.root) {
7
- console.error('Usage: mdorigin dev --root <content-dir> [--port 3000] [--config mdorigin.config.json]');
8
+ console.error('Usage: mdorigin dev --root <content-dir> [--port 3000] [--config <config-file>] [--search ./dist/search]');
8
9
  process.exitCode = 1;
9
10
  return;
10
11
  }
11
12
  const rootDir = path.resolve(args.root);
12
13
  const port = args.port ?? 3000;
13
- const loadedSiteConfig = await loadSiteConfig({
14
+ const loadedConfig = await loadUserSiteConfig({
14
15
  cwd: process.cwd(),
15
16
  rootDir,
16
17
  configPath: args.config,
17
18
  });
18
19
  const store = createFileSystemContentStore(rootDir);
19
- const siteConfig = await applySiteConfigFrontmatterDefaults(store, loadedSiteConfig);
20
+ const siteConfig = await applySiteConfigFrontmatterDefaults(store, loadedConfig.siteConfig);
20
21
  const server = createNodeServer({
21
22
  rootDir,
22
23
  draftMode: 'include',
23
24
  siteConfig,
25
+ searchApi: args.search
26
+ ? await createSearchApiFromDirectory(path.resolve(args.search))
27
+ : undefined,
28
+ plugins: loadedConfig.plugins,
24
29
  });
25
30
  await new Promise((resolve, reject) => {
26
31
  server.once('error', reject);
@@ -47,6 +52,11 @@ function parseArgs(argv) {
47
52
  if (argument === '--config' && nextValue) {
48
53
  result.config = nextValue;
49
54
  index += 1;
55
+ continue;
56
+ }
57
+ if (argument === '--search' && nextValue) {
58
+ result.search = nextValue;
59
+ index += 1;
50
60
  }
51
61
  }
52
62
  return result;
package/dist/cli/main.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { runBuildIndexCommand } from './build-index.js';
3
3
  import { runBuildCloudflareCommand } from './build-cloudflare.js';
4
+ import { runBuildSearchCommand } from './build-search.js';
4
5
  import { runDevCommand } from './dev.js';
5
6
  import { runInitCloudflareCommand } from './init-cloudflare.js';
7
+ import { runSearchCommand } from './search.js';
6
8
  async function main() {
7
9
  const [command, subcommand, ...rest] = process.argv.slice(2);
8
10
  if (command === 'dev') {
@@ -17,16 +19,26 @@ async function main() {
17
19
  await runBuildIndexCommand(rest);
18
20
  return;
19
21
  }
22
+ if (command === 'build' && subcommand === 'search') {
23
+ await runBuildSearchCommand(rest);
24
+ return;
25
+ }
20
26
  if (command === 'init' && subcommand === 'cloudflare') {
21
27
  await runInitCloudflareCommand(rest);
22
28
  return;
23
29
  }
30
+ if (command === 'search') {
31
+ await runSearchCommand([subcommand, ...rest].filter(isDefined));
32
+ return;
33
+ }
24
34
  console.error([
25
35
  'Usage:',
26
- ' mdorigin dev --root <content-dir> [--port 3000] [--config mdorigin.config.json]',
27
- ' mdorigin build index (--root <content-dir> | --dir <content-dir>)',
28
- ' mdorigin build cloudflare --root <content-dir> [--out ./dist/cloudflare] [--config mdorigin.config.json]',
36
+ ' mdorigin dev --root <content-dir> [--port 3000] [--config <config-file>] [--search ./dist/search]',
37
+ ' mdorigin build index (--root <content-dir> | --dir <content-dir>) [--config <config-file>]',
38
+ ' mdorigin build search --root <content-dir> [--out ./dist/search] [--embedding-backend model2vec|hashing] [--model sentence-transformers/all-MiniLM-L6-v2] [--config <config-file>]',
39
+ ' mdorigin build cloudflare --root <content-dir> [--out ./dist/cloudflare] [--config <config-file>] [--search ./dist/search]',
29
40
  ' mdorigin init cloudflare [--dir .] [--entry ./dist/cloudflare/worker.mjs] [--name <worker-name>] [--compatibility-date 2026-03-20] [--force]',
41
+ ' mdorigin search --index <search-dir> [--top-k 10] <query>',
30
42
  ].join('\n'));
31
43
  process.exitCode = 1;
32
44
  }
@@ -0,0 +1 @@
1
+ export declare function runSearchCommand(rawArgs: string[]): Promise<void>;
@@ -0,0 +1,36 @@
1
+ import path from 'node:path';
2
+ import { searchBundle } from '../search.js';
3
+ export async function runSearchCommand(rawArgs) {
4
+ const args = parseArgs(rawArgs);
5
+ if (!args.indexDir || !args.query) {
6
+ throw new Error('Usage: mdorigin search --index <search-dir> [--top-k 10] <query>');
7
+ }
8
+ const hits = await searchBundle({
9
+ indexDir: path.resolve(args.indexDir),
10
+ query: args.query,
11
+ topK: args.topK,
12
+ });
13
+ console.log(JSON.stringify(hits, null, 2));
14
+ }
15
+ function parseArgs(rawArgs) {
16
+ const flags = {};
17
+ const positionals = [];
18
+ for (let index = 0; index < rawArgs.length; index += 1) {
19
+ const arg = rawArgs[index];
20
+ if (arg.startsWith('--')) {
21
+ const value = rawArgs[index + 1];
22
+ if (value && !value.startsWith('--')) {
23
+ flags[arg.slice(2)] = value;
24
+ index += 1;
25
+ }
26
+ continue;
27
+ }
28
+ positionals.push(arg);
29
+ }
30
+ const topK = flags['top-k'] ? Number.parseInt(flags['top-k'], 10) : undefined;
31
+ return {
32
+ indexDir: flags.index,
33
+ topK: Number.isInteger(topK) && topK > 0 ? topK : undefined,
34
+ query: positionals.join(' ').trim(),
35
+ };
36
+ }
@@ -6,12 +6,15 @@ export type { CloudflareManifest, CloudflareManifestEntry };
6
6
  export interface BuildCloudflareManifestOptions {
7
7
  rootDir: string;
8
8
  siteConfig: ResolvedSiteConfig;
9
+ searchDir?: string;
9
10
  }
10
11
  export interface WriteCloudflareBundleOptions {
11
12
  rootDir: string;
12
13
  outDir: string;
13
14
  siteConfig: ResolvedSiteConfig;
14
15
  packageImport?: string;
16
+ searchDir?: string;
17
+ configModulePath?: string;
15
18
  }
16
19
  export interface InitCloudflareProjectOptions {
17
20
  projectDir: string;