living-documentation 1.0.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 ADDED
@@ -0,0 +1,178 @@
1
+ # Living Documentation
2
+
3
+ A CLI tool that serves a local Markdown documentation viewer in your browser.
4
+
5
+ No cloud, no database, no build step — just point it at a folder of `.md` files.
6
+
7
+ ![Node.js](https://img.shields.io/badge/Node.js-18%2B-green)
8
+ ![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue)
9
+ ![License](https://img.shields.io/badge/License-MIT-lightgrey)
10
+
11
+ ---
12
+
13
+ ## Features
14
+
15
+ - **Sidebar** grouped by category, sorted by date (newest first)
16
+ - **Full-text search** — instant filter + server-side content search
17
+ - **Dark mode** — follows system preference, manually toggleable
18
+ - **Export to PDF** — print-friendly layout via `window.print()`
19
+ - **Deep links** — share a direct URL to any document (`?doc=…`)
20
+ - **Admin panel** — configure title, theme, filename pattern in the browser
21
+ - **Zero frontend build** — Tailwind and highlight.js loaded from CDN
22
+
23
+ ---
24
+
25
+ ## Quick start
26
+
27
+ ```bash
28
+ npx living-documentation ./path/to/docs
29
+ ```
30
+
31
+ Then open [http://localhost:4321](http://localhost:4321).
32
+
33
+ ---
34
+
35
+ ## Installation
36
+
37
+ ### npx (no install)
38
+
39
+ ```bash
40
+ npx living-documentation ./docs
41
+ ```
42
+
43
+ ### Global install
44
+
45
+ ```bash
46
+ npm install -g living-documentation
47
+ living-documentation ./docs
48
+ ```
49
+
50
+ ### Local development
51
+
52
+ ```bash
53
+ git clone <repo>
54
+ cd living-documentation
55
+ npm install
56
+ npm run dev -- ./docs # runs via ts-node, no build needed
57
+ ```
58
+
59
+ ---
60
+
61
+ ## Usage
62
+
63
+ ```
64
+ living-documentation [folder] [options]
65
+
66
+ Arguments:
67
+ folder Path to the documentation folder (default: ".")
68
+
69
+ Options:
70
+ -p, --port <number> Port to listen on (default: 4321)
71
+ -o, --open Open browser automatically
72
+ -V, --version Print version
73
+ -h, --help Show help
74
+ ```
75
+
76
+ **Examples:**
77
+
78
+ ```bash
79
+ living-documentation ./docs
80
+ living-documentation ./docs --port 4000 --open # override port
81
+ living-documentation . # current folder
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Filename convention
87
+
88
+ Documents are parsed using this default pattern:
89
+
90
+ ```
91
+ YYYY_MM_DD_[Category]_title_words.md
92
+ ```
93
+
94
+ | Part | Example | Parsed as |
95
+ |------|---------|-----------|
96
+ | `2024_01_15` | `2024_01_15` | Date → Jan 15, 2024 |
97
+ | `[DevOps]` | `[DevOps]` | Category → DevOps |
98
+ | `deploy_pipeline` | `deploy_pipeline` | Title → Deploy Pipeline |
99
+
100
+ **Full example:**
101
+ ```
102
+ 2024_01_15_[DevOps]_deploy_pipeline.md
103
+ 2024_03_20_[Frontend]_react_hooks_guide.md
104
+ 2023_11_03_[Backend]_api_versioning_strategy.md
105
+ ```
106
+
107
+ Files that don't match the pattern are still shown — they appear under **Uncategorized** with the filename as the title.
108
+
109
+ ---
110
+
111
+ ## Config file
112
+
113
+ A `.living-doc.json` file is created automatically in your docs folder on first run:
114
+
115
+ ```json
116
+ {
117
+ "docsFolder": "/absolute/path/to/docs",
118
+ "filenamePattern": "YYYY_MM_DD_[Category]_title",
119
+ "title": "Living Documentation",
120
+ "theme": "system",
121
+ "port": 4321
122
+ }
123
+ ```
124
+
125
+ You can edit it manually or use the **Admin panel** at [http://localhost:4321/admin](http://localhost:4321/admin).
126
+
127
+ ---
128
+
129
+ ## Project structure
130
+
131
+ ```
132
+ living-documentation/
133
+ ├── bin/
134
+ │ └── cli.ts CLI entry point
135
+ ├── src/
136
+ │ ├── server.ts Express app
137
+ │ ├── routes/
138
+ │ │ ├── documents.ts Documents API
139
+ │ │ └── config.ts Config API
140
+ │ ├── lib/
141
+ │ │ ├── parser.ts Filename parser
142
+ │ │ └── config.ts Config management
143
+ │ └── frontend/
144
+ │ ├── index.html Main viewer
145
+ │ └── admin.html Admin panel
146
+ ├── scripts/
147
+ │ └── copy-assets.js Build helper (copies HTML to dist/)
148
+ ├── package.json
149
+ └── tsconfig.json
150
+ ```
151
+
152
+ ---
153
+
154
+ ## API reference
155
+
156
+ | Method | Endpoint | Description |
157
+ |--------|----------|-------------|
158
+ | `GET` | `/api/documents` | List all documents with metadata |
159
+ | `GET` | `/api/documents/:id` | Get document content + rendered HTML |
160
+ | `GET` | `/api/documents/search?q=` | Full-text search |
161
+ | `GET` | `/api/config` | Read config |
162
+ | `PUT` | `/api/config` | Update config (`title`, `theme`, `filenamePattern`) |
163
+
164
+ ---
165
+
166
+ ## Build
167
+
168
+ ```bash
169
+ npm run build # compiles TypeScript → dist/ and copies HTML assets
170
+ ```
171
+
172
+ The compiled package is self-contained inside `dist/`. Only `dist/` is included in the npm publish.
173
+
174
+ ---
175
+
176
+ ## License
177
+
178
+ MIT
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../bin/cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const commander_1 = require("commander");
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const server_1 = require("../src/server");
11
+ const program = new commander_1.Command();
12
+ program
13
+ .name('living-documentation')
14
+ .description('Serve a local Markdown documentation viewer')
15
+ .version('1.0.0')
16
+ .argument('[folder]', 'Path to documentation folder', '.')
17
+ .option('-p, --port <number>', 'Port to listen on', '4321')
18
+ .option('-o, --open', 'Open browser automatically')
19
+ .action(async (folder, options) => {
20
+ const docsPath = path_1.default.resolve(process.cwd(), folder);
21
+ if (!fs_1.default.existsSync(docsPath)) {
22
+ console.error(`\nError: Folder not found: ${docsPath}\n`);
23
+ process.exit(1);
24
+ }
25
+ const stat = fs_1.default.statSync(docsPath);
26
+ if (!stat.isDirectory()) {
27
+ console.error(`\nError: Not a directory: ${docsPath}\n`);
28
+ process.exit(1);
29
+ }
30
+ const port = parseInt(options.port, 10);
31
+ if (isNaN(port) || port < 1 || port > 65535) {
32
+ console.error('\nError: Invalid port number\n');
33
+ process.exit(1);
34
+ }
35
+ await (0, server_1.startServer)({ docsPath, port, openBrowser: options.open ?? false });
36
+ });
37
+ program.parse();
38
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../bin/cli.ts"],"names":[],"mappings":";;;;;;AAEA,yCAAoC;AACpC,gDAAwB;AACxB,4CAAoB;AACpB,0CAA4C;AAE5C,MAAM,OAAO,GAAG,IAAI,mBAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,sBAAsB,CAAC;KAC5B,WAAW,CAAC,6CAA6C,CAAC;KAC1D,OAAO,CAAC,OAAO,CAAC;KAChB,QAAQ,CAAC,UAAU,EAAE,8BAA8B,EAAE,GAAG,CAAC;KACzD,MAAM,CAAC,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,CAAC;KAC1D,MAAM,CAAC,YAAY,EAAE,4BAA4B,CAAC;KAClD,MAAM,CAAC,KAAK,EAAE,MAAc,EAAE,OAAwC,EAAE,EAAE;IACzE,MAAM,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;IAErD,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,KAAK,CAAC,8BAA8B,QAAQ,IAAI,CAAC,CAAC;QAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,IAAI,GAAG,YAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;QACxB,OAAO,CAAC,KAAK,CAAC,6BAA6B,QAAQ,IAAI,CAAC,CAAC;QACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACxC,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,KAAK,EAAE,CAAC;QAC5C,OAAO,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;QAChD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,IAAA,oBAAW,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC;AAC5E,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
@@ -0,0 +1,268 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Admin — Living Documentation</title>
7
+
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+
10
+ <script>
11
+ tailwind.config = { darkMode: 'class', theme: { extend: {} } };
12
+ </script>
13
+
14
+ <style>
15
+ .field-label { @apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1; }
16
+ .field-input {
17
+ @apply w-full px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700
18
+ bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100
19
+ placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm;
20
+ }
21
+ .field-hint { @apply mt-1 text-xs text-gray-400 dark:text-gray-500; }
22
+ </style>
23
+ </head>
24
+
25
+ <body class="h-full bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100">
26
+
27
+ <!-- ── Header ── -->
28
+ <header class="flex items-center justify-between px-6 h-14 border-b border-gray-200 dark:border-gray-800
29
+ bg-white dark:bg-gray-900 shadow-sm">
30
+ <div class="flex items-center gap-3">
31
+ <a href="/" class="text-blue-600 dark:text-blue-400 hover:underline text-sm">
32
+ &#8592; Back to docs
33
+ </a>
34
+ <span class="text-gray-300 dark:text-gray-700">|</span>
35
+ <h1 class="text-sm font-semibold">Admin Panel</h1>
36
+ </div>
37
+
38
+ <button id="dark-toggle" title="Toggle dark mode"
39
+ class="p-2 rounded-lg text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
40
+ <span id="dark-icon" class="text-lg leading-none">☾</span>
41
+ </button>
42
+ </header>
43
+
44
+ <!-- ── Page ── -->
45
+ <main class="max-w-2xl mx-auto px-6 py-10">
46
+
47
+ <!-- Title -->
48
+ <div class="mb-8">
49
+ <h2 class="text-xl font-bold text-gray-900 dark:text-gray-50">Configuration</h2>
50
+ <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
51
+ Settings are saved to <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">.living-doc.json</code>
52
+ in your docs folder.
53
+ </p>
54
+ </div>
55
+
56
+ <!-- ── Form ── -->
57
+ <form id="config-form" class="space-y-6" novalidate>
58
+
59
+ <!-- Read-only info card -->
60
+ <div class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5">
61
+ <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-4">Server Info</h3>
62
+ <dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
63
+ <div>
64
+ <dt class="text-xs text-gray-400 uppercase tracking-wide mb-0.5">Docs Folder</dt>
65
+ <dd id="info-folder" class="font-mono text-xs text-gray-700 dark:text-gray-300 break-all">—</dd>
66
+ </div>
67
+ <div>
68
+ <dt class="text-xs text-gray-400 uppercase tracking-wide mb-0.5">Port</dt>
69
+ <dd id="info-port" class="font-mono text-xs text-gray-700 dark:text-gray-300">—</dd>
70
+ </div>
71
+ </dl>
72
+ </div>
73
+
74
+ <!-- Editable settings -->
75
+ <div class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5 space-y-5">
76
+ <h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Appearance & Metadata</h3>
77
+
78
+ <!-- Site Title -->
79
+ <div>
80
+ <label class="field-label" for="field-title">Site Title</label>
81
+ <input id="field-title" name="title" type="text" class="field-input"
82
+ placeholder="Living Documentation" />
83
+ <p class="field-hint">Displayed in the browser tab and sidebar header.</p>
84
+ </div>
85
+
86
+ <!-- Theme -->
87
+ <div>
88
+ <label class="field-label" for="field-theme">Default Theme</label>
89
+ <select id="field-theme" name="theme" class="field-input">
90
+ <option value="system">System (follow OS preference)</option>
91
+ <option value="light">Light</option>
92
+ <option value="dark">Dark</option>
93
+ </select>
94
+ <p class="field-hint">Users can always override this with the toggle button.</p>
95
+ </div>
96
+
97
+ <!-- Filename Pattern -->
98
+ <div>
99
+ <label class="field-label" for="field-pattern">Filename Pattern</label>
100
+ <input id="field-pattern" name="filenamePattern" type="text" class="field-input"
101
+ placeholder="YYYY_MM_DD_[Category]_title" />
102
+ <p class="field-hint">
103
+ Documents matching this pattern are parsed for date, category, and title.
104
+ <span class="font-mono bg-gray-100 dark:bg-gray-800 px-1 rounded">YYYY_MM_DD_[Category]_title.md</span>
105
+ </p>
106
+ </div>
107
+ </div>
108
+
109
+ <!-- Pattern preview -->
110
+ <div id="pattern-preview"
111
+ class="rounded-xl border border-blue-100 dark:border-blue-900 bg-blue-50 dark:bg-blue-950/30 p-5">
112
+ <h3 class="text-sm font-semibold text-blue-700 dark:text-blue-400 mb-3">Pattern Preview</h3>
113
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-3">How your pattern parses example filenames:</p>
114
+ <div id="preview-rows" class="space-y-2 text-sm"></div>
115
+ </div>
116
+
117
+ <!-- Submit -->
118
+ <div class="flex items-center justify-between">
119
+ <div id="save-msg" class="text-sm"></div>
120
+ <button type="submit"
121
+ class="px-5 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold text-sm
122
+ transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
123
+ Save changes
124
+ </button>
125
+ </div>
126
+ </form>
127
+
128
+ </main>
129
+
130
+ <script>
131
+ // ── Boot ───────────────────────────────────────────────────
132
+ document.addEventListener('DOMContentLoaded', async () => {
133
+ applyDarkMode(loadDarkPref());
134
+ setupDarkToggle();
135
+ await loadConfig();
136
+ setupPatternPreview();
137
+ document.getElementById('config-form').addEventListener('submit', saveConfig);
138
+ });
139
+
140
+ // ── Dark mode ──────────────────────────────────────────────
141
+ function loadDarkPref() {
142
+ const saved = localStorage.getItem('ld-dark');
143
+ if (saved !== null) return saved === 'true';
144
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
145
+ }
146
+ function applyDarkMode(dark) {
147
+ document.documentElement.classList.toggle('dark', dark);
148
+ document.getElementById('dark-icon').textContent = dark ? '☀' : '☾';
149
+ }
150
+ function setupDarkToggle() {
151
+ document.getElementById('dark-toggle').addEventListener('click', () => {
152
+ const isDark = document.documentElement.classList.toggle('dark');
153
+ localStorage.setItem('ld-dark', isDark);
154
+ document.getElementById('dark-icon').textContent = isDark ? '☀' : '☾';
155
+ });
156
+ }
157
+
158
+ // ── Config ─────────────────────────────────────────────────
159
+ async function loadConfig() {
160
+ try {
161
+ const cfg = await fetch('/api/config').then(r => r.json());
162
+ document.getElementById('info-folder').textContent = cfg.docsFolder || '—';
163
+ document.getElementById('info-port').textContent = cfg.port || '—';
164
+ document.getElementById('field-title').value = cfg.title || '';
165
+ document.getElementById('field-theme').value = cfg.theme || 'system';
166
+ document.getElementById('field-pattern').value = cfg.filenamePattern || '';
167
+ updatePreview(cfg.filenamePattern);
168
+ } catch {
169
+ showMsg('Failed to load config.', 'error');
170
+ }
171
+ }
172
+
173
+ async function saveConfig(e) {
174
+ e.preventDefault();
175
+ const payload = {
176
+ title: document.getElementById('field-title').value.trim(),
177
+ theme: document.getElementById('field-theme').value,
178
+ filenamePattern: document.getElementById('field-pattern').value.trim(),
179
+ };
180
+
181
+ try {
182
+ const res = await fetch('/api/config', {
183
+ method: 'PUT',
184
+ headers: { 'Content-Type': 'application/json' },
185
+ body: JSON.stringify(payload),
186
+ });
187
+ if (!res.ok) throw new Error(await res.text());
188
+ showMsg('Saved!', 'ok');
189
+ } catch (err) {
190
+ showMsg('Save failed: ' + err.message, 'error');
191
+ }
192
+ }
193
+
194
+ function showMsg(text, type) {
195
+ const el = document.getElementById('save-msg');
196
+ el.textContent = text;
197
+ el.className = 'text-sm ' + (type === 'ok'
198
+ ? 'text-green-600 dark:text-green-400'
199
+ : 'text-red-600 dark:text-red-400');
200
+ if (type === 'ok') setTimeout(() => { el.textContent = ''; }, 3000);
201
+ }
202
+
203
+ // ── Pattern preview ────────────────────────────────────────
204
+ const EXAMPLES = [
205
+ '2024_01_15_[DevOps]_deploy_pipeline.md',
206
+ '2023_11_03_[Frontend]_react_hooks_guide.md',
207
+ '2025_06_20_meeting_notes.md',
208
+ 'readme.md',
209
+ ];
210
+
211
+ // Parse like server does
212
+ const FULL_PAT = /^(\d{4}_\d{2}_\d{2})_\[([^\]]+)\]_(.+)\.md$/i;
213
+ const DATE_ONLY = /^(\d{4}_\d{2}_\d{2})_(.+)\.md$/i;
214
+
215
+ function parsePreview(filename) {
216
+ const full = filename.match(FULL_PAT);
217
+ if (full) {
218
+ const [, d, cat, t] = full;
219
+ return { date: d.replace(/_/g,'-'), category: cat, title: titleCase(t), match: true };
220
+ }
221
+ const d = filename.match(DATE_ONLY);
222
+ if (d) {
223
+ return { date: d[1].replace(/_/g,'-'), category: 'Uncategorized', title: titleCase(d[2]), match: true };
224
+ }
225
+ return { date: null, category: 'Uncategorized', title: filename.replace('.md',''), match: false };
226
+ }
227
+
228
+ function titleCase(s) {
229
+ return s.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
230
+ }
231
+
232
+ function updatePreview() {
233
+ const rows = document.getElementById('preview-rows');
234
+ rows.innerHTML = EXAMPLES.map(f => {
235
+ const p = parsePreview(f);
236
+ return `
237
+ <div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-3">
238
+ <p class="font-mono text-xs text-gray-500 dark:text-gray-400 mb-2 break-all">${esc(f)}</p>
239
+ <div class="grid grid-cols-3 gap-2 text-xs">
240
+ <div>
241
+ <span class="text-gray-400 block mb-0.5">Date</span>
242
+ <span class="${p.date ? 'text-green-600 dark:text-green-400' : 'text-gray-400'}">${p.date || 'none'}</span>
243
+ </div>
244
+ <div>
245
+ <span class="text-gray-400 block mb-0.5">Category</span>
246
+ <span class="text-blue-600 dark:text-blue-400">${esc(p.category)}</span>
247
+ </div>
248
+ <div>
249
+ <span class="text-gray-400 block mb-0.5">Title</span>
250
+ <span class="text-gray-700 dark:text-gray-300 truncate block">${esc(p.title)}</span>
251
+ </div>
252
+ </div>
253
+ </div>`;
254
+ }).join('');
255
+ }
256
+
257
+ function setupPatternPreview() {
258
+ document.getElementById('field-pattern').addEventListener('input', updatePreview);
259
+ }
260
+
261
+ function esc(s) {
262
+ return String(s)
263
+ .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
264
+ .replace(/"/g,'&quot;').replace(/'/g,'&#39;');
265
+ }
266
+ </script>
267
+ </body>
268
+ </html>