botdocs 0.1.0 → 0.2.0

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.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Botdocs
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/botdocs.svg)](https://www.npmjs.com/package/botdocs)
4
+
3
5
  Convert markdown documentation into beautiful static sites with AI-powered semantic search — no backend required.
4
6
 
5
7
  ## Features
@@ -33,8 +35,35 @@ botdocs ./docs -o ./public
33
35
 
34
36
  # Verbose logging
35
37
  botdocs ./docs -v
38
+
39
+ # Use a specific theme
40
+ botdocs ./docs -t material
41
+
42
+ # Custom config file
43
+ botdocs ./docs -c ./my-config.json
44
+
45
+ # Combine multiple options
46
+ botdocs ./docs -o ./public -t slate -v
36
47
  ```
37
48
 
49
+ ### CLI Options
50
+
51
+ | Option | Alias | Description | Default |
52
+ |--------|-------|-------------|---------|
53
+ | `--output <dir>` | `-o` | Output directory for generated site | `output` |
54
+ | `--no-chat` | | Disable AI chatbot functionality | `false` |
55
+ | `--config <file>` | `-c` | Path to config file | `botdocs.config.json` |
56
+ | `--theme <theme>` | `-t` | Theme to use | `classic` |
57
+ | `--verbose` | `-v` | Enable verbose logging | `false` |
58
+
59
+ ### Available Themes
60
+
61
+ - **classic** - Clean, professional theme (default)
62
+ - **material** - Material Design theme
63
+ - **minimal** - Clean, minimalist theme
64
+ - **slate** - Dark slate theme
65
+ - **modern** - Modern documentation theme
66
+
38
67
  ## Configuration
39
68
 
40
69
  Create `botdocs.config.json` in your docs directory:
@@ -43,15 +72,31 @@ Create `botdocs.config.json` in your docs directory:
43
72
  {
44
73
  "title": "My Documentation",
45
74
  "description": "Project docs",
75
+ "theme": "classic",
76
+ "attribution": true,
46
77
  "chat": { "enabled": true },
47
78
  "build": {
48
79
  "chunkSize": 500,
49
80
  "chunkOverlap": 50,
50
- "topK": 5
81
+ "topK": 3
51
82
  }
52
83
  }
53
84
  ```
54
85
 
86
+ ### Configuration Options
87
+
88
+ | Option | Type | Default | Description |
89
+ |--------|------|---------|-------------|
90
+ | `title` | string | `"Documentation"` | Site title |
91
+ | `description` | string | `"Project documentation"` | Site description |
92
+ | `theme` | string | `"classic"` | Theme to use (classic, material, minimal, slate, modern) |
93
+ | `attribution` | boolean | `true` | Show "Built with Botdocs" footer link |
94
+ | `chat.enabled` | boolean | `true` | Enable AI chatbot |
95
+ | `chat.welcomeMessage` | string | `"Ask me anything about the docs!"` | Chatbot welcome message |
96
+ | `build.chunkSize` | number | `500` | Text chunk size for embeddings |
97
+ | `build.chunkOverlap` | number | `50` | Overlap between chunks |
98
+ | `build.topK` | number | `3` | Number of results to return |
99
+
55
100
  ## Front Matter
56
101
 
57
102
  ```markdown
@@ -1,8 +1,7 @@
1
1
  import { defaultConfig } from '../types/config.js';
2
2
  import { SiteGenerator } from './site-generator.js';
3
3
  import { VectorDBBuilder } from './vector-db-builder.js';
4
- import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'fs';
5
- import fs from 'fs-extra';
4
+ import { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync, cpSync } from 'fs';
6
5
  import { join, dirname, resolve } from 'path';
7
6
  import { fileURLToPath } from 'url';
8
7
  import { exec } from 'child_process';
@@ -14,7 +13,7 @@ const __dirname = dirname(__filename);
14
13
  * Main build orchestrator
15
14
  */
16
15
  export async function build(options) {
17
- const { inputDir, outputDir, chatEnabled, configPath, verbose } = options;
16
+ const { inputDir, outputDir, chatEnabled, configPath, verbose, theme } = options;
18
17
  // Validate input directory
19
18
  if (!existsSync(inputDir)) {
20
19
  throw new Error(`Input directory does not exist: ${inputDir}`);
@@ -26,11 +25,15 @@ export async function build(options) {
26
25
  config.chat = config.chat || {};
27
26
  config.chat.enabled = chatEnabled;
28
27
  }
28
+ // Override theme if specified in CLI
29
+ if (theme) {
30
+ config.theme = theme;
31
+ }
29
32
  if (verbose) {
30
33
  console.log('Configuration:', JSON.stringify(config, null, 2));
31
34
  }
32
35
  // Prepare output directory
33
- fs.ensureDirSync(outputDir);
36
+ mkdirSync(outputDir, { recursive: true });
34
37
  if (verbose) {
35
38
  console.log(`Cleaning output directory: ${outputDir}`);
36
39
  }
@@ -40,8 +43,8 @@ export async function build(options) {
40
43
  const documents = await generator.generate(inputDir, outputDir, config);
41
44
  // Create assets directories
42
45
  const assetsDir = join(outputDir, 'assets');
43
- fs.ensureDirSync(join(assetsDir, 'css'));
44
- fs.ensureDirSync(join(assetsDir, 'js'));
46
+ mkdirSync(join(assetsDir, 'css'), { recursive: true });
47
+ mkdirSync(join(assetsDir, 'js'), { recursive: true });
45
48
  if (verbose) {
46
49
  console.log(`Processed ${documents.length} documents`);
47
50
  }
@@ -85,7 +88,7 @@ export async function build(options) {
85
88
  const distAssetsDir = join(distClientDir, 'assets');
86
89
  if (existsSync(distAssetsDir)) {
87
90
  const outputAssetsDir = join(jsOutputDir, 'assets');
88
- fs.copySync(distAssetsDir, outputAssetsDir);
91
+ cpSync(distAssetsDir, outputAssetsDir, { recursive: true });
89
92
  if (verbose) {
90
93
  console.log('Copied code-split chunks');
91
94
  }
@@ -108,9 +111,12 @@ export async function build(options) {
108
111
  console.log('Copying styles...');
109
112
  // From dist/src/builder, go to project root, then to src/styles
110
113
  const stylesDir = resolve(__dirname, '../../../src/styles');
114
+ const themesDir = join(stylesDir, 'themes');
111
115
  const outputCssDir = join(assetsDir, 'css');
112
- // Combine all CSS files into one bundle
113
- const cssFiles = ['main.css', 'themes.css', 'chat.css'];
116
+ // Determine which theme to use
117
+ const selectedTheme = config.theme || defaultConfig.theme || 'classic';
118
+ // Combine CSS files: base styles + selected theme + chat
119
+ const cssFiles = ['themes.css', 'chat.css'];
114
120
  let bundledCss = '';
115
121
  for (const cssFile of cssFiles) {
116
122
  const cssPath = join(stylesDir, cssFile);
@@ -118,6 +124,21 @@ export async function build(options) {
118
124
  bundledCss += readFileSync(cssPath, 'utf-8') + '\n\n';
119
125
  }
120
126
  }
127
+ // Add selected theme CSS
128
+ const themePath = join(themesDir, `${selectedTheme}.css`);
129
+ if (existsSync(themePath)) {
130
+ bundledCss += readFileSync(themePath, 'utf-8') + '\n\n';
131
+ if (verbose) {
132
+ console.log(`Using theme: ${selectedTheme}`);
133
+ }
134
+ }
135
+ else {
136
+ console.warn(`Theme '${selectedTheme}' not found, falling back to classic`);
137
+ const fallbackPath = join(themesDir, 'classic.css');
138
+ if (existsSync(fallbackPath)) {
139
+ bundledCss += readFileSync(fallbackPath, 'utf-8') + '\n\n';
140
+ }
141
+ }
121
142
  writeFileSync(join(outputCssDir, 'bundle.css'), bundledCss, 'utf-8');
122
143
  if (verbose) {
123
144
  console.log('Styles copied');
@@ -1,6 +1,4 @@
1
- import { glob } from 'glob';
2
- import { readFileSync, writeFileSync } from 'fs';
3
- import fs from 'fs-extra';
1
+ import { readFileSync, writeFileSync, readdirSync, mkdirSync, cpSync } from 'fs';
4
2
  import { join, dirname, resolve } from 'path';
5
3
  import { MarkdownProcessor } from './markdown-processor.js';
6
4
  import { TemplateEngine } from './template-engine.js';
@@ -20,12 +18,16 @@ export class SiteGenerator {
20
18
  */
21
19
  async generate(inputDir, outputDir, config) {
22
20
  console.log('Processing markdown files...');
23
- // Find all markdown files
24
- const markdownFiles = await glob('**/*.md', {
25
- cwd: inputDir,
26
- absolute: true,
27
- ignore: ['node_modules/**', '**/node_modules/**'],
21
+ // Find all markdown files using native Node.js readdir
22
+ const allFiles = readdirSync(inputDir, {
23
+ recursive: true,
24
+ withFileTypes: true
28
25
  });
26
+ const markdownFiles = allFiles
27
+ .filter(dirent => dirent.isFile() &&
28
+ dirent.name.endsWith('.md') &&
29
+ !dirent.parentPath.includes('node_modules'))
30
+ .map(dirent => join(dirent.parentPath || dirent.path, dirent.name));
29
31
  if (markdownFiles.length === 0) {
30
32
  throw new Error(`No markdown files found in ${inputDir}`);
31
33
  }
@@ -90,10 +92,11 @@ export class SiteGenerator {
90
92
  content,
91
93
  navigation: this.renderNavigation(navigation, doc.url),
92
94
  chatEnabled: config.chat?.enabled,
95
+ attribution: config.attribution !== false, // defaults to true
93
96
  });
94
97
  // Write HTML file
95
98
  const outputPath = join(outputDir, doc.relativePath.replace(/\.md$/, '.html'));
96
- fs.ensureDirSync(dirname(outputPath));
99
+ mkdirSync(dirname(outputPath), { recursive: true });
97
100
  writeFileSync(outputPath, html, 'utf-8');
98
101
  }
99
102
  // Copy index.html if README.md exists
@@ -102,7 +105,7 @@ export class SiteGenerator {
102
105
  const readmePath = join(outputDir, readmeDoc.relativePath.replace(/\.md$/, '.html'));
103
106
  const indexPath = join(outputDir, 'index.html');
104
107
  if (readmePath !== indexPath) {
105
- fs.copySync(readmePath, indexPath);
108
+ cpSync(readmePath, indexPath);
106
109
  }
107
110
  }
108
111
  console.log(`Generated ${this.documents.length} HTML pages`);
@@ -18,6 +18,7 @@ program
18
18
  .option('-o, --output <dir>', 'Output directory for generated site', defaultOptions.output)
19
19
  .option('--no-chat', 'Disable AI chatbot functionality')
20
20
  .option('-c, --config <file>', 'Path to config file (botdocs.config.json)')
21
+ .option('-t, --theme <theme>', 'Theme to use (classic, material, minimal, slate, modern)', defaultOptions.theme)
21
22
  .option('-v, --verbose', 'Enable verbose logging')
22
23
  .action(async (input, options) => {
23
24
  try {
@@ -28,6 +29,7 @@ program
28
29
  console.log(`Input: ${inputDir}`);
29
30
  console.log(`Output: ${outputDir}`);
30
31
  console.log(`Chat enabled: ${!options.noChat}`);
32
+ console.log(`Theme: ${options.theme || defaultOptions.theme}`);
31
33
  }
32
34
  await build({
33
35
  inputDir,
@@ -35,6 +37,7 @@ program
35
37
  chatEnabled: !options.noChat,
36
38
  configPath: options.config,
37
39
  verbose: options.verbose || false,
40
+ theme: options.theme,
38
41
  });
39
42
  console.log('Build complete!');
40
43
  console.log(`Site generated at: ${outputDir}`);
@@ -1,7 +1,9 @@
1
+ export type Theme = 'classic' | 'material' | 'minimal' | 'slate' | 'modern';
1
2
  export interface CliOptions {
2
3
  output?: string;
3
4
  noChat?: boolean;
4
5
  config?: string;
5
6
  verbose?: boolean;
7
+ theme?: Theme;
6
8
  }
7
9
  export declare const defaultOptions: Partial<CliOptions>;
@@ -2,4 +2,5 @@ export const defaultOptions = {
2
2
  output: 'output',
3
3
  noChat: false,
4
4
  verbose: false,
5
+ theme: 'classic',
5
6
  };
@@ -1,9 +1,9 @@
1
+ export type Theme = 'classic' | 'material' | 'minimal' | 'slate' | 'modern';
1
2
  export interface BotdocsConfig {
2
3
  title?: string;
3
4
  description?: string;
4
- theme?: {
5
- primaryColor?: string;
6
- };
5
+ theme?: Theme;
6
+ attribution?: boolean;
7
7
  chat?: {
8
8
  enabled?: boolean;
9
9
  welcomeMessage?: string;
@@ -20,5 +20,6 @@ export interface BuildOptions {
20
20
  chatEnabled: boolean;
21
21
  configPath?: string;
22
22
  verbose: boolean;
23
+ theme?: Theme;
23
24
  }
24
25
  export declare const defaultConfig: BotdocsConfig;
@@ -1,9 +1,8 @@
1
1
  export const defaultConfig = {
2
2
  title: 'Documentation',
3
3
  description: 'Project documentation',
4
- theme: {
5
- primaryColor: '#3b82f6',
6
- },
4
+ theme: 'classic',
5
+ attribution: true,
7
6
  chat: {
8
7
  enabled: true,
9
8
  welcomeMessage: 'Ask me anything about the docs!',
@@ -11,6 +10,6 @@ export const defaultConfig = {
11
10
  build: {
12
11
  chunkSize: 500,
13
12
  chunkOverlap: 50,
14
- topK: 5,
13
+ topK: 3,
15
14
  },
16
15
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botdocs",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "An npm CLI tool that converts markdown documentation into a static website with an optional client-side AI chatbot",
5
5
  "author": "usr-wwelsh <https://wwel.sh>",
6
6
  "license": "MIT",
@@ -38,8 +38,6 @@
38
38
  "@shikijs/markdown-it": "^1.0.0",
39
39
  "@xenova/transformers": "^2.17.0",
40
40
  "commander": "^12.0.0",
41
- "fs-extra": "^11.2.0",
42
- "glob": "^11.0.0",
43
41
  "gray-matter": "^4.0.3",
44
42
  "markdown-it": "^14.0.0",
45
43
  "markdown-it-anchor": "^9.0.0",
@@ -53,8 +51,7 @@
53
51
  "shiki": "^1.0.0"
54
52
  },
55
53
  "devDependencies": {
56
- "@types/fs-extra": "^11.0.4",
57
- "@types/markdown-it": "^13.0.0",
54
+ "@types/markdown-it": "^14.0.0",
58
55
  "@types/node": "^20.11.0",
59
56
  "terser": "^5.36.0",
60
57
  "typescript": "^5.3.0",
@@ -503,6 +503,29 @@ img {
503
503
  font-size: 0.875rem;
504
504
  }
505
505
 
506
+ /* Footer Attribution */
507
+ .botdocs-attribution {
508
+ margin-top: 4rem;
509
+ padding-top: 2rem;
510
+ border-top: 1px solid var(--border-color);
511
+ text-align: center;
512
+ font-size: 0.875rem;
513
+ color: var(--text-secondary);
514
+ opacity: 0.8;
515
+ }
516
+
517
+ .botdocs-attribution a {
518
+ color: var(--text-secondary);
519
+ text-decoration: none;
520
+ font-weight: 500;
521
+ transition: color 0.2s;
522
+ }
523
+
524
+ .botdocs-attribution a:hover {
525
+ color: var(--link-color);
526
+ text-decoration: none;
527
+ }
528
+
506
529
  /* Responsive */
507
530
  @media (max-width: 768px) {
508
531
  .sidebar {