@oddessentials/odd-docs 2.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.
@@ -0,0 +1,1481 @@
1
+ // src/cli/index.ts
2
+ import { Command } from "commander";
3
+
4
+ // src/cli/commands/generate.ts
5
+ import { writeFile, mkdir } from "fs/promises";
6
+ import { join as join2, resolve } from "path";
7
+
8
+ // src/core/parser/manifestParser.ts
9
+ import { readFile } from "fs/promises";
10
+ import { join } from "path";
11
+ async function parseManifest(repoPath) {
12
+ const manifestPath = join(repoPath, "manifest.json");
13
+ let content;
14
+ try {
15
+ content = await readFile(manifestPath, "utf-8");
16
+ } catch {
17
+ throw new Error(`Manifest not found: ${manifestPath}`);
18
+ }
19
+ let manifest;
20
+ try {
21
+ manifest = JSON.parse(content);
22
+ } catch {
23
+ throw new Error(`Invalid JSON in manifest: ${manifestPath}`);
24
+ }
25
+ if (!manifest.tool_id) {
26
+ throw new Error("Manifest missing required field: tool_id");
27
+ }
28
+ if (!manifest.version) {
29
+ throw new Error("Manifest missing required field: version");
30
+ }
31
+ const capabilities = [];
32
+ if (manifest.capabilities) {
33
+ for (const [type, values] of Object.entries(manifest.capabilities)) {
34
+ if (Array.isArray(values) && values.length > 0) {
35
+ capabilities.push({ type, values, provenance: "manifest" });
36
+ }
37
+ }
38
+ }
39
+ const result = {
40
+ entity: {
41
+ type: "tool",
42
+ id: manifest.tool_id,
43
+ version: manifest.version,
44
+ description: manifest.description
45
+ },
46
+ inputs: {
47
+ schema: manifest.parameters,
48
+ provenance: "manifest"
49
+ }
50
+ };
51
+ if (capabilities.length > 0 || manifest.timeout_ms || manifest.resource_limits) {
52
+ result.constraints = {
53
+ capabilities: capabilities.length > 0 ? capabilities : void 0,
54
+ timeoutMs: manifest.timeout_ms,
55
+ resourceLimits: manifest.resource_limits ? {
56
+ memoryMb: manifest.resource_limits.memory_mb,
57
+ cpuCores: manifest.resource_limits.cpu_cores
58
+ } : void 0
59
+ };
60
+ }
61
+ if (manifest.deprecation) {
62
+ result.lifecycle = {
63
+ version: manifest.version,
64
+ deprecation: {
65
+ deprecatedAt: manifest.deprecation.deprecated_at,
66
+ sunsetDate: manifest.deprecation.sunset_date,
67
+ migrationUrl: manifest.deprecation.migration_url
68
+ }
69
+ };
70
+ }
71
+ return result;
72
+ }
73
+
74
+ // src/core/ir/builder.ts
75
+ import { createHash } from "crypto";
76
+
77
+ // src/core/parser/schemaParser.ts
78
+ function parseSchema(schema) {
79
+ if (!schema) return [];
80
+ const jsonSchema = schema;
81
+ const parameters = [];
82
+ const required = new Set(jsonSchema.required ?? []);
83
+ if (jsonSchema.properties) {
84
+ for (const [name, propSchema] of Object.entries(jsonSchema.properties)) {
85
+ parameters.push(parseProperty(name, propSchema, required.has(name)));
86
+ }
87
+ }
88
+ return parameters;
89
+ }
90
+ function parseProperty(name, schema, isRequired) {
91
+ const param = {
92
+ name,
93
+ type: resolveType(schema),
94
+ required: isRequired
95
+ };
96
+ if (schema.description) {
97
+ param.description = schema.description;
98
+ }
99
+ if (schema.default !== void 0) {
100
+ param.default = schema.default;
101
+ }
102
+ if (schema.enum) {
103
+ param.enum = schema.enum;
104
+ }
105
+ const constraints = buildConstraints(schema);
106
+ if (constraints) {
107
+ param.constraints = constraints;
108
+ }
109
+ return param;
110
+ }
111
+ function resolveType(schema) {
112
+ if (schema.$ref) {
113
+ const refParts = schema.$ref.split("/");
114
+ return refParts[refParts.length - 1];
115
+ }
116
+ if (schema.enum) {
117
+ return "enum";
118
+ }
119
+ if (schema.type === "array" && schema.items) {
120
+ return `${resolveType(schema.items)}[]`;
121
+ }
122
+ return schema.type ?? "unknown";
123
+ }
124
+ function buildConstraints(schema) {
125
+ const parts = [];
126
+ if (schema.minimum !== void 0) {
127
+ parts.push(`min: ${schema.minimum}`);
128
+ }
129
+ if (schema.maximum !== void 0) {
130
+ parts.push(`max: ${schema.maximum}`);
131
+ }
132
+ if (schema.minLength !== void 0) {
133
+ parts.push(`minLength: ${schema.minLength}`);
134
+ }
135
+ if (schema.maxLength !== void 0) {
136
+ parts.push(`maxLength: ${schema.maxLength}`);
137
+ }
138
+ if (schema.pattern) {
139
+ parts.push(`pattern: ${schema.pattern}`);
140
+ }
141
+ return parts.length > 0 ? parts.join(", ") : void 0;
142
+ }
143
+ function enrichSchemaSection(section) {
144
+ return {
145
+ ...section,
146
+ parameters: parseSchema(section.schema)
147
+ };
148
+ }
149
+
150
+ // src/core/ir/builder.ts
151
+ function buildDocIR(manifest) {
152
+ const enrichedInputs = enrichSchemaSection(manifest.inputs);
153
+ const ir = {
154
+ version: "1.0.0",
155
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
156
+ determinismKey: "",
157
+ // Computed below
158
+ entity: manifest.entity,
159
+ inputs: enrichedInputs,
160
+ constraints: manifest.constraints,
161
+ lifecycle: manifest.lifecycle,
162
+ provenance: {
163
+ entity: "manifest",
164
+ inputs: "manifest"
165
+ }
166
+ };
167
+ ir.determinismKey = computeDeterminismKey(ir);
168
+ return ir;
169
+ }
170
+ function computeDeterminismKey(ir) {
171
+ const canonical = JSON.stringify({
172
+ entity: ir.entity,
173
+ inputs: ir.inputs,
174
+ constraints: ir.constraints,
175
+ lifecycle: ir.lifecycle
176
+ });
177
+ const hash = createHash("sha256").update(canonical).digest("hex");
178
+ return `sha256:${hash}`;
179
+ }
180
+
181
+ // src/core/renderer/markdownRenderer.ts
182
+ function renderMarkdown(ir) {
183
+ const lines = [];
184
+ lines.push(`# ${ir.entity.id}`);
185
+ lines.push("");
186
+ lines.push(`**Version:** ${ir.entity.version} `);
187
+ lines.push(`**Type:** ${ir.entity.type} `);
188
+ if (ir.entity.description) {
189
+ lines.push("");
190
+ lines.push(ir.entity.description);
191
+ }
192
+ lines.push("");
193
+ if (ir.overview) {
194
+ lines.push("## Overview");
195
+ lines.push("");
196
+ if (ir.overview.intent) {
197
+ lines.push(`**Intent:** ${ir.overview.intent}`);
198
+ lines.push("");
199
+ }
200
+ if (ir.overview.useCases?.length) {
201
+ lines.push("### Use Cases");
202
+ lines.push("");
203
+ for (const useCase of ir.overview.useCases) {
204
+ lines.push(`- ${useCase}`);
205
+ }
206
+ lines.push("");
207
+ }
208
+ if (ir.overview.sideEffects?.length) {
209
+ lines.push("### Side Effects");
210
+ lines.push("");
211
+ for (const effect of ir.overview.sideEffects) {
212
+ lines.push(
213
+ `- **${effect.type}**: ${effect.description} ${provenanceBadge(effect.provenance)}`
214
+ );
215
+ }
216
+ lines.push("");
217
+ }
218
+ }
219
+ if (ir.inputs?.parameters?.length) {
220
+ lines.push("## Inputs");
221
+ lines.push("");
222
+ lines.push(provenanceBadge(ir.inputs.provenance));
223
+ lines.push("");
224
+ lines.push(renderParameterTable(ir.inputs.parameters));
225
+ lines.push("");
226
+ }
227
+ if (ir.outputs?.parameters?.length) {
228
+ lines.push("## Outputs");
229
+ lines.push("");
230
+ lines.push(provenanceBadge(ir.outputs.provenance));
231
+ lines.push("");
232
+ lines.push(renderParameterTable(ir.outputs.parameters));
233
+ lines.push("");
234
+ }
235
+ if (ir.constraints) {
236
+ lines.push("## Constraints");
237
+ lines.push("");
238
+ if (ir.constraints.capabilities?.length) {
239
+ lines.push("### Capabilities");
240
+ lines.push("");
241
+ for (const cap of ir.constraints.capabilities) {
242
+ const values = cap.values?.join(", ") ?? "enabled";
243
+ lines.push(`- **${cap.type}**: ${values} ${provenanceBadge(cap.provenance)}`);
244
+ }
245
+ lines.push("");
246
+ }
247
+ if (ir.constraints.timeoutMs) {
248
+ lines.push(`**Timeout:** ${ir.constraints.timeoutMs}ms`);
249
+ lines.push("");
250
+ }
251
+ if (ir.constraints.resourceLimits) {
252
+ lines.push("### Resource Limits");
253
+ lines.push("");
254
+ if (ir.constraints.resourceLimits.memoryMb) {
255
+ lines.push(`- Memory: ${ir.constraints.resourceLimits.memoryMb} MB`);
256
+ }
257
+ if (ir.constraints.resourceLimits.cpuCores) {
258
+ lines.push(`- CPU: ${ir.constraints.resourceLimits.cpuCores} cores`);
259
+ }
260
+ lines.push("");
261
+ }
262
+ }
263
+ if (ir.errors?.length) {
264
+ lines.push("## Errors");
265
+ lines.push("");
266
+ lines.push("| Code | Description | Recovery |");
267
+ lines.push("|------|-------------|----------|");
268
+ for (const err of ir.errors) {
269
+ lines.push(`| \`${err.code}\` | ${err.description ?? ""} | ${err.recovery ?? ""} |`);
270
+ }
271
+ lines.push("");
272
+ }
273
+ if (ir.lifecycle?.deprecation) {
274
+ lines.push("## Lifecycle");
275
+ lines.push("");
276
+ lines.push("> [!WARNING]");
277
+ lines.push(`> This tool is deprecated as of ${ir.lifecycle.deprecation.deprecatedAt}.`);
278
+ lines.push(`> Sunset date: ${ir.lifecycle.deprecation.sunsetDate}`);
279
+ if (ir.lifecycle.deprecation.migrationUrl) {
280
+ lines.push(`> Migration guide: ${ir.lifecycle.deprecation.migrationUrl}`);
281
+ }
282
+ lines.push("");
283
+ }
284
+ if (ir.narrative?.examples?.length) {
285
+ lines.push("## Examples");
286
+ lines.push("");
287
+ for (const example of ir.narrative.examples) {
288
+ if (example.title) {
289
+ lines.push(`### ${example.title}`);
290
+ lines.push("");
291
+ }
292
+ if (example.description) {
293
+ lines.push(example.description);
294
+ lines.push("");
295
+ }
296
+ if (example.input) {
297
+ lines.push("**Input:**");
298
+ lines.push("```json");
299
+ lines.push(JSON.stringify(example.input, null, 2));
300
+ lines.push("```");
301
+ lines.push("");
302
+ }
303
+ if (example.output) {
304
+ lines.push("**Output:**");
305
+ lines.push("```json");
306
+ lines.push(JSON.stringify(example.output, null, 2));
307
+ lines.push("```");
308
+ lines.push("");
309
+ }
310
+ }
311
+ }
312
+ if (ir.narrative?.notes?.length) {
313
+ lines.push("## Notes");
314
+ lines.push("");
315
+ lines.push("*[author notes]*");
316
+ lines.push("");
317
+ for (const note of ir.narrative.notes) {
318
+ lines.push(note);
319
+ lines.push("");
320
+ }
321
+ }
322
+ lines.push("---");
323
+ lines.push("");
324
+ lines.push(`*Generated at ${ir.generatedAt}* `);
325
+ lines.push(`*Determinism key: \`${ir.determinismKey}\`*`);
326
+ return lines.join("\n");
327
+ }
328
+ function renderParameterTable(params) {
329
+ const lines = [];
330
+ lines.push("| Name | Type | Required | Default | Description |");
331
+ lines.push("|------|------|----------|---------|-------------|");
332
+ for (const param of params) {
333
+ const required = param.required ? "\u2713" : "";
334
+ const defaultVal = param.default !== void 0 ? `\`${JSON.stringify(param.default)}\`` : "";
335
+ const desc = param.description ?? "";
336
+ lines.push(`| \`${param.name}\` | \`${param.type}\` | ${required} | ${defaultVal} | ${desc} |`);
337
+ }
338
+ return lines.join("\n");
339
+ }
340
+ function provenanceBadge(source) {
341
+ if (!source) return "";
342
+ const badges = {
343
+ introspection: "`[from introspection]`",
344
+ manifest: "`[from schema]`",
345
+ overlay: "`[from overlay]`",
346
+ narrative: "`[author notes]`"
347
+ };
348
+ return badges[source] ?? "";
349
+ }
350
+
351
+ // src/core/renderer/htmlRenderer.ts
352
+ import { marked } from "marked";
353
+ function renderHTML(ir) {
354
+ const markdown = renderMarkdown(ir);
355
+ const htmlContent = marked.parse(markdown);
356
+ return `<!DOCTYPE html>
357
+ <html lang="en" data-theme="light">
358
+ <head>
359
+ <meta charset="UTF-8">
360
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
361
+ <meta name="description" content="${escapeHtml(ir.entity.description ?? `Documentation for ${ir.entity.id}`)}">
362
+ <title>${escapeHtml(ir.entity.id)} - odd-docs</title>
363
+ <style>
364
+ ${getThemeStyles()}
365
+ </style>
366
+ </head>
367
+ <body>
368
+ <div class="container">
369
+ <header>
370
+ <nav class="breadcrumb">
371
+ <a href="index.html">Home</a> / <span>${escapeHtml(ir.entity.id)}</span>
372
+ </nav>
373
+ </header>
374
+ <main class="content">
375
+ ${htmlContent}
376
+ </main>
377
+ <footer>
378
+ <p class="meta">
379
+ Generated by <a href="https://github.com/oddessentials/odd-docs">odd-docs</a> at ${ir.generatedAt}
380
+ </p>
381
+ <p class="determinism">
382
+ <code>${ir.determinismKey}</code>
383
+ </p>
384
+ </footer>
385
+ </div>
386
+ <script>
387
+ ${getThemeScript()}
388
+ </script>
389
+ </body>
390
+ </html>`;
391
+ }
392
+ function escapeHtml(str) {
393
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
394
+ }
395
+ function getThemeStyles() {
396
+ return `
397
+ /* CSS Variables for theming */
398
+ :root {
399
+ /* Colors - neutral palette */
400
+ --color-bg: #ffffff;
401
+ --color-bg-secondary: #f8f9fa;
402
+ --color-text: #1a1a2e;
403
+ --color-text-muted: #6c757d;
404
+ --color-border: #dee2e6;
405
+ --color-link: #0066cc;
406
+ --color-link-hover: #004499;
407
+ --color-accent: #0066cc;
408
+ --color-success: #28a745;
409
+ --color-warning: #ffc107;
410
+ --color-error: #dc3545;
411
+
412
+ /* Typography */
413
+ --font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
414
+ --font-mono: 'SF Mono', Monaco, 'Cascadia Code', monospace;
415
+ --font-size-base: 16px;
416
+ --line-height: 1.6;
417
+
418
+ /* Spacing */
419
+ --space-xs: 0.25rem;
420
+ --space-sm: 0.5rem;
421
+ --space-md: 1rem;
422
+ --space-lg: 1.5rem;
423
+ --space-xl: 2rem;
424
+
425
+ /* Layout */
426
+ --max-width: 800px;
427
+ --border-radius: 4px;
428
+ }
429
+
430
+ [data-theme="dark"] {
431
+ --color-bg: #1a1a2e;
432
+ --color-bg-secondary: #16213e;
433
+ --color-text: #e8e8e8;
434
+ --color-text-muted: #a0a0a0;
435
+ --color-border: #3a3a5e;
436
+ --color-link: #66b3ff;
437
+ --color-link-hover: #99ccff;
438
+ }
439
+
440
+ /* Reset */
441
+ *, *::before, *::after {
442
+ box-sizing: border-box;
443
+ }
444
+
445
+ body {
446
+ margin: 0;
447
+ padding: 0;
448
+ font-family: var(--font-sans);
449
+ font-size: var(--font-size-base);
450
+ line-height: var(--line-height);
451
+ color: var(--color-text);
452
+ background: var(--color-bg);
453
+ }
454
+
455
+ /* Container */
456
+ .container {
457
+ max-width: var(--max-width);
458
+ margin: 0 auto;
459
+ padding: var(--space-lg);
460
+ }
461
+
462
+ /* Typography */
463
+ h1, h2, h3, h4, h5, h6 {
464
+ margin-top: var(--space-xl);
465
+ margin-bottom: var(--space-md);
466
+ line-height: 1.3;
467
+ }
468
+
469
+ h1 { font-size: 2rem; }
470
+ h2 { font-size: 1.5rem; border-bottom: 1px solid var(--color-border); padding-bottom: var(--space-sm); }
471
+ h3 { font-size: 1.25rem; }
472
+
473
+ p { margin: var(--space-md) 0; }
474
+
475
+ a {
476
+ color: var(--color-link);
477
+ text-decoration: none;
478
+ }
479
+
480
+ a:hover {
481
+ color: var(--color-link-hover);
482
+ text-decoration: underline;
483
+ }
484
+
485
+ /* Code */
486
+ code {
487
+ font-family: var(--font-mono);
488
+ font-size: 0.9em;
489
+ background: var(--color-bg-secondary);
490
+ padding: var(--space-xs) var(--space-sm);
491
+ border-radius: var(--border-radius);
492
+ }
493
+
494
+ pre {
495
+ background: var(--color-bg-secondary);
496
+ padding: var(--space-md);
497
+ border-radius: var(--border-radius);
498
+ overflow-x: auto;
499
+ }
500
+
501
+ pre code {
502
+ background: none;
503
+ padding: 0;
504
+ }
505
+
506
+ /* Tables */
507
+ table {
508
+ width: 100%;
509
+ border-collapse: collapse;
510
+ margin: var(--space-md) 0;
511
+ }
512
+
513
+ th, td {
514
+ padding: var(--space-sm) var(--space-md);
515
+ border: 1px solid var(--color-border);
516
+ text-align: left;
517
+ }
518
+
519
+ th {
520
+ background: var(--color-bg-secondary);
521
+ font-weight: 600;
522
+ }
523
+
524
+ /* Provenance badges */
525
+ code[class*="from"] {
526
+ font-size: 0.75em;
527
+ color: var(--color-text-muted);
528
+ background: transparent;
529
+ }
530
+
531
+ /* Blockquotes (for warnings/notes) */
532
+ blockquote {
533
+ margin: var(--space-md) 0;
534
+ padding: var(--space-md);
535
+ border-left: 4px solid var(--color-warning);
536
+ background: var(--color-bg-secondary);
537
+ }
538
+
539
+ blockquote p:first-child { margin-top: 0; }
540
+ blockquote p:last-child { margin-bottom: 0; }
541
+
542
+ /* Lists */
543
+ ul, ol {
544
+ padding-left: var(--space-lg);
545
+ }
546
+
547
+ li { margin: var(--space-sm) 0; }
548
+
549
+ /* Doc list (index page) */
550
+ .doc-list {
551
+ list-style: none;
552
+ padding: 0;
553
+ }
554
+
555
+ .doc-list li {
556
+ padding: var(--space-md);
557
+ border: 1px solid var(--color-border);
558
+ border-radius: var(--border-radius);
559
+ margin-bottom: var(--space-md);
560
+ }
561
+
562
+ .doc-list a {
563
+ display: flex;
564
+ align-items: center;
565
+ gap: var(--space-sm);
566
+ }
567
+
568
+ .doc-list .version {
569
+ color: var(--color-text-muted);
570
+ font-size: 0.875em;
571
+ }
572
+
573
+ .doc-list p {
574
+ margin: var(--space-sm) 0 0;
575
+ color: var(--color-text-muted);
576
+ }
577
+
578
+ /* Header */
579
+ header {
580
+ margin-bottom: var(--space-lg);
581
+ }
582
+
583
+ .breadcrumb {
584
+ font-size: 0.875em;
585
+ color: var(--color-text-muted);
586
+ }
587
+
588
+ .breadcrumb a {
589
+ color: inherit;
590
+ }
591
+
592
+ /* Footer */
593
+ footer {
594
+ margin-top: var(--space-xl);
595
+ padding-top: var(--space-lg);
596
+ border-top: 1px solid var(--color-border);
597
+ font-size: 0.875em;
598
+ color: var(--color-text-muted);
599
+ }
600
+
601
+ .determinism code {
602
+ font-size: 0.75em;
603
+ }
604
+
605
+ /* Theme toggle */
606
+ .theme-toggle {
607
+ position: fixed;
608
+ top: var(--space-md);
609
+ right: var(--space-md);
610
+ padding: var(--space-sm) var(--space-md);
611
+ border: 1px solid var(--color-border);
612
+ border-radius: var(--border-radius);
613
+ background: var(--color-bg);
614
+ cursor: pointer;
615
+ font-size: 0.875em;
616
+ }
617
+
618
+ /* Responsive */
619
+ @media (max-width: 600px) {
620
+ .container { padding: var(--space-md); }
621
+ h1 { font-size: 1.5rem; }
622
+ h2 { font-size: 1.25rem; }
623
+ table { font-size: 0.875em; }
624
+ }
625
+ `;
626
+ }
627
+ function getThemeScript() {
628
+ return `
629
+ // Theme toggle
630
+ const toggle = document.createElement('button');
631
+ toggle.className = 'theme-toggle';
632
+ toggle.textContent = '\u{1F319}';
633
+ toggle.onclick = () => {
634
+ const html = document.documentElement;
635
+ const isDark = html.dataset.theme === 'dark';
636
+ html.dataset.theme = isDark ? 'light' : 'dark';
637
+ toggle.textContent = isDark ? '\u{1F319}' : '\u2600\uFE0F';
638
+ localStorage.setItem('theme', html.dataset.theme);
639
+ };
640
+ document.body.appendChild(toggle);
641
+
642
+ // Restore saved theme
643
+ const saved = localStorage.getItem('theme');
644
+ if (saved) {
645
+ document.documentElement.dataset.theme = saved;
646
+ toggle.textContent = saved === 'dark' ? '\u2600\uFE0F' : '\u{1F319}';
647
+ } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
648
+ document.documentElement.dataset.theme = 'dark';
649
+ toggle.textContent = '\u2600\uFE0F';
650
+ }
651
+ `;
652
+ }
653
+
654
+ // src/cli/commands/generate.ts
655
+ async function generate(repoPath, options = {}) {
656
+ const absPath = resolve(repoPath);
657
+ const format = options.format ?? "md";
658
+ const outputDir = options.output ? resolve(options.output) : join2(absPath, "docs", "generated");
659
+ console.log(`Generating documentation for: ${absPath}`);
660
+ const manifestResult = await parseManifest(absPath);
661
+ console.log(` Entity: ${manifestResult.entity.id}@${manifestResult.entity.version}`);
662
+ const ir = buildDocIR(manifestResult);
663
+ console.log(` Determinism key: ${ir.determinismKey}`);
664
+ await mkdir(outputDir, { recursive: true });
665
+ if (format === "md" || format === "both") {
666
+ const markdown = renderMarkdown(ir);
667
+ const mdPath = join2(outputDir, `${ir.entity.id}.md`);
668
+ await writeFile(mdPath, markdown, "utf-8");
669
+ console.log(` \u2713 Markdown: ${mdPath}`);
670
+ }
671
+ if (format === "html" || format === "both") {
672
+ const html = renderHTML(ir);
673
+ const htmlPath = join2(outputDir, `${ir.entity.id}.html`);
674
+ await writeFile(htmlPath, html, "utf-8");
675
+ console.log(` \u2713 HTML: ${htmlPath}`);
676
+ }
677
+ const irPath = join2(outputDir, `${ir.entity.id}.ir.json`);
678
+ await writeFile(irPath, JSON.stringify(ir, null, 2), "utf-8");
679
+ console.log(` \u2713 Doc IR: ${irPath}`);
680
+ console.log("Done!");
681
+ }
682
+
683
+ // src/cli/commands/validate.ts
684
+ import { resolve as resolve2 } from "path";
685
+
686
+ // src/core/capabilities.ts
687
+ var KNOWN_CAPABILITIES = [
688
+ "network",
689
+ "filesystem",
690
+ "secrets",
691
+ "exec",
692
+ "subprocess",
693
+ "database",
694
+ "queue"
695
+ ];
696
+ var SAFETY_AFFECTING_PATTERNS = ["network", "exec", "subprocess", "write", "delete"];
697
+ function isKnownCapability(capability) {
698
+ return KNOWN_CAPABILITIES.includes(capability);
699
+ }
700
+ function isSafetyAffecting(capability) {
701
+ return SAFETY_AFFECTING_PATTERNS.some(
702
+ (pattern) => capability.toLowerCase().includes(pattern.toLowerCase())
703
+ );
704
+ }
705
+
706
+ // src/core/ir/validator.ts
707
+ function validateDocIR(ir, options = {}) {
708
+ const issues = [];
709
+ if (!ir.entity.id) {
710
+ issues.push({
711
+ severity: "error",
712
+ code: "MISSING_TOOL_ID",
713
+ message: "Tool ID is required",
714
+ path: "entity.id"
715
+ });
716
+ }
717
+ if (!ir.entity.version) {
718
+ issues.push({
719
+ severity: "error",
720
+ code: "MISSING_VERSION",
721
+ message: "Version is required",
722
+ path: "entity.version"
723
+ });
724
+ }
725
+ if (!ir.inputs?.schema && !ir.inputs?.parameters?.length) {
726
+ issues.push({
727
+ severity: "error",
728
+ code: "MISSING_PARAMETERS_SCHEMA",
729
+ message: "Parameters schema is required",
730
+ path: "inputs"
731
+ });
732
+ }
733
+ if (ir.constraints?.capabilities) {
734
+ for (const cap of ir.constraints.capabilities) {
735
+ if (!isKnownCapability(cap.type)) {
736
+ const isSafety = isSafetyAffecting(cap.type);
737
+ issues.push({
738
+ severity: options.strict && isSafety ? "error" : "warning",
739
+ code: "UNKNOWN_CAPABILITY",
740
+ message: `Unknown capability: ${cap.type}${isSafety ? " (safety-affecting)" : ""}`,
741
+ path: `constraints.capabilities.${cap.type}`,
742
+ provenance: cap.provenance
743
+ });
744
+ }
745
+ }
746
+ }
747
+ issues.push(...checkStructuralContradictions(ir));
748
+ const hasErrors = issues.some((i) => i.severity === "error");
749
+ return {
750
+ valid: !hasErrors,
751
+ issues
752
+ };
753
+ }
754
+ function checkStructuralContradictions(ir) {
755
+ const issues = [];
756
+ if (ir.narrative?.notes && ir.inputs?.parameters) {
757
+ const schemaParams = new Set(ir.inputs.parameters.map((p) => p.name.toLowerCase()));
758
+ for (const note of ir.narrative.notes) {
759
+ const paramMentions = note.match(/(?:parameter|param)\s+[`"']?(\w+)[`"']?/gi) ?? [];
760
+ const theParamMentions = note.match(/the\s+[`"']?(\w+)[`"']?\s+parameter/gi) ?? [];
761
+ for (const match of [...paramMentions, ...theParamMentions]) {
762
+ const paramName = match.replace(/.*?[`"']?(\w+)[`"']?.*/i, "$1").toLowerCase();
763
+ if (paramName && !schemaParams.has(paramName) && paramName !== "parameter") {
764
+ issues.push({
765
+ severity: "error",
766
+ code: "NARRATIVE_REFERENCES_UNDEFINED_PARAM",
767
+ message: `Narrative references parameter "${paramName}" not in schema`,
768
+ path: "narrative.notes",
769
+ provenance: "narrative"
770
+ });
771
+ }
772
+ }
773
+ }
774
+ }
775
+ if (ir.narrative?.examples && ir.inputs?.parameters) {
776
+ const schemaParams = new Set(ir.inputs.parameters.map((p) => p.name));
777
+ for (const example of ir.narrative.examples) {
778
+ if (example.input && typeof example.input === "object") {
779
+ for (const key of Object.keys(example.input)) {
780
+ if (!schemaParams.has(key)) {
781
+ issues.push({
782
+ severity: "warning",
783
+ code: "EXAMPLE_USES_UNDEFINED_PARAM",
784
+ message: `Example uses parameter "${key}" not in schema`,
785
+ path: `narrative.examples.${example.title ?? "unnamed"}`,
786
+ provenance: "narrative"
787
+ });
788
+ }
789
+ }
790
+ }
791
+ }
792
+ }
793
+ if (ir.overview?.sideEffects && ir.constraints?.capabilities) {
794
+ const capTypes = new Set(ir.constraints.capabilities.map((c) => c.type));
795
+ for (const effect of ir.overview.sideEffects) {
796
+ const expectedCap = mapSideEffectToCapability(effect.type);
797
+ if (expectedCap && !capTypes.has(expectedCap)) {
798
+ issues.push({
799
+ severity: "error",
800
+ code: "SIDE_EFFECT_CAPABILITY_MISMATCH",
801
+ message: `Side effect "${effect.type}" requires capability "${expectedCap}" which is not declared`,
802
+ path: `overview.sideEffects.${effect.type}`,
803
+ provenance: effect.provenance
804
+ });
805
+ }
806
+ }
807
+ }
808
+ return issues;
809
+ }
810
+ function mapSideEffectToCapability(effectType) {
811
+ const mapping = {
812
+ filesystem: "filesystem",
813
+ network: "network",
814
+ secrets: "secrets",
815
+ exec: "exec",
816
+ subprocess: "subprocess",
817
+ database: "database",
818
+ queue: "queue"
819
+ };
820
+ return mapping[effectType] ?? null;
821
+ }
822
+ function formatValidationResult(result, repoPath) {
823
+ const lines = [];
824
+ lines.push(`Validating: ${repoPath}`);
825
+ lines.push("");
826
+ if (result.valid) {
827
+ lines.push("\u2713 Validation passed");
828
+ } else {
829
+ lines.push("\u2717 Validation failed");
830
+ }
831
+ lines.push("");
832
+ const errors = result.issues.filter((i) => i.severity === "error");
833
+ const warnings = result.issues.filter((i) => i.severity === "warning");
834
+ if (errors.length > 0) {
835
+ lines.push(`Errors (${errors.length}):`);
836
+ for (const issue of errors) {
837
+ lines.push(` \u2717 [${issue.code}] ${issue.message}`);
838
+ if (issue.path) {
839
+ lines.push(` at ${issue.path}`);
840
+ }
841
+ }
842
+ lines.push("");
843
+ }
844
+ if (warnings.length > 0) {
845
+ lines.push(`Warnings (${warnings.length}):`);
846
+ for (const issue of warnings) {
847
+ lines.push(` \u26A0 [${issue.code}] ${issue.message}`);
848
+ if (issue.path) {
849
+ lines.push(` at ${issue.path}`);
850
+ }
851
+ }
852
+ lines.push("");
853
+ }
854
+ return lines.join("\n");
855
+ }
856
+
857
+ // src/cli/commands/validate.ts
858
+ async function validate(repoPath, options = {}) {
859
+ const absPath = resolve2(repoPath);
860
+ console.log(`Validating: ${absPath}`);
861
+ console.log(`Mode: ${options.strict ? "strict" : "normal"}`);
862
+ console.log("");
863
+ try {
864
+ const manifestResult = await parseManifest(absPath);
865
+ const ir = buildDocIR(manifestResult);
866
+ const result = validateDocIR(ir, { strict: options.strict });
867
+ console.log(formatValidationResult(result, absPath));
868
+ return result.valid;
869
+ } catch (error) {
870
+ console.error("\u2717 Validation failed");
871
+ console.error("");
872
+ console.error(`Error: ${error instanceof Error ? error.message : error}`);
873
+ return false;
874
+ }
875
+ }
876
+
877
+ // src/cli/commands/serve.ts
878
+ import { existsSync } from "fs";
879
+ import { join as join4, resolve as resolve3 } from "path";
880
+
881
+ // src/server/index.ts
882
+ import { createServer } from "http";
883
+
884
+ // src/server/fileServer.ts
885
+ import { createReadStream, statSync } from "fs";
886
+ import { realpath } from "fs/promises";
887
+ import { join as join3, extname, relative, isAbsolute } from "path";
888
+ import { createHash as createHash2 } from "crypto";
889
+ var MAX_RESPONSE_SIZE = 50 * 1024 * 1024;
890
+ var MIME_TYPES = {
891
+ ".html": "text/html; charset=utf-8",
892
+ ".css": "text/css; charset=utf-8",
893
+ ".js": "application/javascript; charset=utf-8",
894
+ ".json": "application/json; charset=utf-8",
895
+ ".md": "text/markdown; charset=utf-8",
896
+ ".png": "image/png",
897
+ ".jpg": "image/jpeg",
898
+ ".jpeg": "image/jpeg",
899
+ ".gif": "image/gif",
900
+ ".svg": "image/svg+xml",
901
+ ".ico": "image/x-icon",
902
+ ".woff": "font/woff",
903
+ ".woff2": "font/woff2",
904
+ ".ttf": "font/ttf"
905
+ };
906
+ var FileServer = class {
907
+ rootDir;
908
+ resolvedRoot = null;
909
+ constructor(rootDir) {
910
+ this.rootDir = rootDir;
911
+ }
912
+ async serve(req, res) {
913
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
914
+ let pathname = decodeURIComponent(url.pathname);
915
+ if (pathname === "/" || pathname.endsWith("/")) {
916
+ pathname = join3(pathname, "index.html");
917
+ }
918
+ const filePath = await this.resolvePath(pathname);
919
+ if (!filePath) {
920
+ res.statusCode = 403;
921
+ res.end("Forbidden: Path traversal blocked");
922
+ return;
923
+ }
924
+ let stats;
925
+ try {
926
+ stats = statSync(filePath);
927
+ } catch {
928
+ res.statusCode = 404;
929
+ res.end("Not Found");
930
+ return;
931
+ }
932
+ if (!stats.isFile()) {
933
+ res.statusCode = 404;
934
+ res.end("Not Found");
935
+ return;
936
+ }
937
+ if (stats.size > MAX_RESPONSE_SIZE) {
938
+ res.statusCode = 413;
939
+ res.end("File too large");
940
+ return;
941
+ }
942
+ const etag = this.computeETag(filePath, stats.mtime, stats.size);
943
+ const ifNoneMatch = req.headers["if-none-match"];
944
+ if (ifNoneMatch === etag) {
945
+ res.statusCode = 304;
946
+ res.end();
947
+ return;
948
+ }
949
+ const ext = extname(filePath).toLowerCase();
950
+ const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
951
+ res.setHeader("Content-Type", contentType);
952
+ res.setHeader("Content-Length", stats.size);
953
+ res.setHeader("ETag", etag);
954
+ res.setHeader("Cache-Control", "no-cache");
955
+ const stream = createReadStream(filePath);
956
+ stream.pipe(res);
957
+ stream.on("error", () => {
958
+ res.statusCode = 500;
959
+ res.end("Error reading file");
960
+ });
961
+ }
962
+ /**
963
+ * Resolve path with traversal protection
964
+ */
965
+ async resolvePath(pathname) {
966
+ if (!this.resolvedRoot) {
967
+ try {
968
+ this.resolvedRoot = await realpath(this.rootDir);
969
+ } catch {
970
+ return null;
971
+ }
972
+ }
973
+ const targetPath = join3(this.rootDir, pathname);
974
+ let resolvedTarget;
975
+ try {
976
+ resolvedTarget = await realpath(targetPath);
977
+ } catch {
978
+ return null;
979
+ }
980
+ const rel = relative(this.resolvedRoot, resolvedTarget);
981
+ if (rel.startsWith("..") || isAbsolute(rel)) {
982
+ console.warn(`[odd-docs] Path traversal blocked: ${pathname}`);
983
+ return null;
984
+ }
985
+ return resolvedTarget;
986
+ }
987
+ computeETag(path, mtime, size) {
988
+ const hash = createHash2("md5").update(`${path}:${mtime.getTime()}:${size}`).digest("hex").slice(0, 16);
989
+ return `"${hash}"`;
990
+ }
991
+ };
992
+
993
+ // src/server/livereload.ts
994
+ import { watch } from "fs";
995
+ var LiveReload = class {
996
+ options;
997
+ watcher = null;
998
+ sseClients = /* @__PURE__ */ new Set();
999
+ lastChangeTime = Date.now();
1000
+ debounceTimer = null;
1001
+ constructor(options) {
1002
+ this.options = {
1003
+ pollInterval: 5e3,
1004
+ ...options
1005
+ };
1006
+ }
1007
+ async start() {
1008
+ const usePolling = process.env.CHOKIDAR_USEPOLLING === "1" || this.options.mode === "poll";
1009
+ this.watcher = watch(
1010
+ this.options.outputDir,
1011
+ { recursive: true, persistent: true },
1012
+ (_eventType, filename) => {
1013
+ if (filename) {
1014
+ this.onFileChange(filename);
1015
+ }
1016
+ }
1017
+ );
1018
+ if (usePolling) {
1019
+ console.log("[odd-docs] Using poll-based file watching");
1020
+ }
1021
+ console.log(`[odd-docs] Live reload started (mode: ${this.options.mode})`);
1022
+ }
1023
+ async stop() {
1024
+ if (this.debounceTimer) {
1025
+ clearTimeout(this.debounceTimer);
1026
+ }
1027
+ if (this.watcher) {
1028
+ this.watcher.close();
1029
+ }
1030
+ for (const client of this.sseClients) {
1031
+ client.end();
1032
+ }
1033
+ this.sseClients.clear();
1034
+ }
1035
+ onFileChange(filename) {
1036
+ if (this.debounceTimer) {
1037
+ clearTimeout(this.debounceTimer);
1038
+ }
1039
+ this.debounceTimer = setTimeout(() => {
1040
+ this.lastChangeTime = Date.now();
1041
+ console.log(`[odd-docs] Change detected: ${filename}`);
1042
+ this.notifyClients();
1043
+ }, 100);
1044
+ }
1045
+ notifyClients() {
1046
+ const message = JSON.stringify({ type: "reload", time: this.lastChangeTime });
1047
+ for (const client of this.sseClients) {
1048
+ client.write(`data: ${message}
1049
+
1050
+ `);
1051
+ }
1052
+ }
1053
+ async handleRequest(req, res) {
1054
+ const mode = this.options.mode;
1055
+ if (mode === "sse") {
1056
+ res.setHeader("Content-Type", "text/event-stream");
1057
+ res.setHeader("Cache-Control", "no-cache");
1058
+ res.setHeader("Connection", "keep-alive");
1059
+ res.setHeader("X-Accel-Buffering", "no");
1060
+ res.write(": ping\n\n");
1061
+ this.sseClients.add(res);
1062
+ req.on("close", () => {
1063
+ this.sseClients.delete(res);
1064
+ });
1065
+ } else if (mode === "poll") {
1066
+ res.setHeader("Content-Type", "application/json");
1067
+ res.end(JSON.stringify({ lastChange: this.lastChangeTime }));
1068
+ } else {
1069
+ res.statusCode = 404;
1070
+ res.end("Live reload not available in this mode");
1071
+ }
1072
+ }
1073
+ /**
1074
+ * Get client-side script for live reload
1075
+ */
1076
+ static getClientScript(mode, pollInterval = 5e3) {
1077
+ if (mode === "none") return "";
1078
+ if (mode === "sse") {
1079
+ return `
1080
+ <script>
1081
+ (function() {
1082
+ const es = new EventSource('/__livereload');
1083
+ es.onmessage = function(e) {
1084
+ const data = JSON.parse(e.data);
1085
+ if (data.type === 'reload') {
1086
+ console.log('[odd-docs] Reloading...');
1087
+ location.reload();
1088
+ }
1089
+ };
1090
+ es.onerror = function() {
1091
+ console.log('[odd-docs] SSE connection lost, will retry...');
1092
+ };
1093
+ })();
1094
+ </script>`;
1095
+ }
1096
+ if (mode === "poll") {
1097
+ return `
1098
+ <script>
1099
+ (function() {
1100
+ let lastChange = 0;
1101
+ setInterval(async () => {
1102
+ try {
1103
+ const res = await fetch('/__livereload');
1104
+ const data = await res.json();
1105
+ if (lastChange && data.lastChange > lastChange) {
1106
+ console.log('[odd-docs] Reloading...');
1107
+ location.reload();
1108
+ }
1109
+ lastChange = data.lastChange;
1110
+ } catch (e) {}
1111
+ }, ${pollInterval});
1112
+ })();
1113
+ </script>`;
1114
+ }
1115
+ return `
1116
+ <script>
1117
+ (function() {
1118
+ const ws = new WebSocket('ws://' + location.host + '/__ws');
1119
+ ws.onmessage = function(e) {
1120
+ const data = JSON.parse(e.data);
1121
+ if (data.type === 'reload') {
1122
+ console.log('[odd-docs] Reloading...');
1123
+ location.reload();
1124
+ }
1125
+ };
1126
+ ws.onclose = function() {
1127
+ console.log('[odd-docs] WebSocket closed, will retry in 3s...');
1128
+ setTimeout(() => location.reload(), 3000);
1129
+ };
1130
+ })();
1131
+ </script>`;
1132
+ }
1133
+ };
1134
+
1135
+ // src/server/api.ts
1136
+ var MAX_IR_SIZE = 10 * 1024 * 1024;
1137
+ var ApiHandler = class {
1138
+ state;
1139
+ options;
1140
+ constructor(state, options) {
1141
+ this.state = state;
1142
+ this.options = options;
1143
+ }
1144
+ async handle(req, res) {
1145
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
1146
+ const path = url.pathname;
1147
+ const method = req.method ?? "GET";
1148
+ switch (path) {
1149
+ case "/api/health":
1150
+ return this.handleHealth(res);
1151
+ case "/api/ready":
1152
+ return this.handleReady(res);
1153
+ case "/api/introspection":
1154
+ return this.handleIntrospection(res);
1155
+ case "/api/docs":
1156
+ return this.handleDocs(res);
1157
+ case "/api/ir":
1158
+ return this.handleIR(res);
1159
+ case "/api/capabilities":
1160
+ return this.handleCapabilities(res);
1161
+ case "/api/regenerate":
1162
+ if (method === "POST") {
1163
+ return this.handleRegenerate(req, res);
1164
+ }
1165
+ return this.methodNotAllowed(res);
1166
+ default:
1167
+ return this.notFound(res);
1168
+ }
1169
+ }
1170
+ /**
1171
+ * GET /api/health - Liveness probe
1172
+ * Returns 200 if process is running
1173
+ */
1174
+ handleHealth(res) {
1175
+ this.json(res, {
1176
+ status: "ok",
1177
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1178
+ });
1179
+ }
1180
+ /**
1181
+ * GET /api/ready - Readiness probe
1182
+ * Returns 200 if last generate succeeded and output is accessible
1183
+ */
1184
+ handleReady(res) {
1185
+ const ready = this.state.lastGenerateSuccess && this.state.outputDirAccessible;
1186
+ if (!ready) {
1187
+ res.statusCode = 503;
1188
+ }
1189
+ this.json(res, {
1190
+ ready,
1191
+ lastGenerateSuccess: this.state.lastGenerateSuccess,
1192
+ lastGenerateTime: this.state.lastGenerateTime,
1193
+ outputDirAccessible: this.state.outputDirAccessible
1194
+ });
1195
+ }
1196
+ /**
1197
+ * GET /api/introspection - Introspection status
1198
+ */
1199
+ handleIntrospection(res) {
1200
+ this.json(res, {
1201
+ connected: this.state.introspectionConnected,
1202
+ lastPollTime: this.state.lastIntrospectionTime ?? null
1203
+ });
1204
+ }
1205
+ /**
1206
+ * GET /api/docs - List documentation sections
1207
+ */
1208
+ handleDocs(res) {
1209
+ this.json(res, {
1210
+ sections: [],
1211
+ message: "Not implemented yet"
1212
+ });
1213
+ }
1214
+ /**
1215
+ * GET /api/ir - Full IR dump (capped at 10MB)
1216
+ */
1217
+ handleIR(res) {
1218
+ const ir = { tools: [], prompts: [], resources: [] };
1219
+ const irJson = JSON.stringify(ir);
1220
+ if (irJson.length > MAX_IR_SIZE) {
1221
+ this.json(res, {
1222
+ truncated: true,
1223
+ message: `IR exceeds ${MAX_IR_SIZE / 1024 / 1024}MB limit`,
1224
+ partial: null
1225
+ });
1226
+ return;
1227
+ }
1228
+ res.setHeader("Content-Type", "application/json");
1229
+ res.end(irJson);
1230
+ }
1231
+ /**
1232
+ * GET /api/capabilities - Tool capabilities summary
1233
+ */
1234
+ handleCapabilities(res) {
1235
+ this.json(res, {
1236
+ serve: {
1237
+ host: this.options.host,
1238
+ port: this.options.port,
1239
+ reloadMode: this.options.reloadMode,
1240
+ mutationsEnabled: this.options.enableMutations
1241
+ }
1242
+ });
1243
+ }
1244
+ /**
1245
+ * POST /api/regenerate - Trigger manual regeneration
1246
+ * Requires --enable-mutations and valid token
1247
+ */
1248
+ handleRegenerate(req, res) {
1249
+ if (!this.options.enableMutations) {
1250
+ res.statusCode = 403;
1251
+ this.json(res, {
1252
+ error: "Mutations disabled",
1253
+ message: "Start server with --enable-mutations to use this endpoint"
1254
+ });
1255
+ return;
1256
+ }
1257
+ const token = req.headers["x-mutation-token"];
1258
+ if (token !== this.options.mutationToken) {
1259
+ res.statusCode = 401;
1260
+ this.json(res, {
1261
+ error: "Unauthorized",
1262
+ message: "Invalid or missing X-Mutation-Token header"
1263
+ });
1264
+ return;
1265
+ }
1266
+ this.json(res, {
1267
+ status: "queued",
1268
+ message: "Regeneration triggered",
1269
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1270
+ });
1271
+ }
1272
+ json(res, data) {
1273
+ res.setHeader("Content-Type", "application/json");
1274
+ res.end(JSON.stringify(data, null, 2));
1275
+ }
1276
+ notFound(res) {
1277
+ res.statusCode = 404;
1278
+ this.json(res, { error: "Not Found" });
1279
+ }
1280
+ methodNotAllowed(res) {
1281
+ res.statusCode = 405;
1282
+ this.json(res, { error: "Method Not Allowed" });
1283
+ }
1284
+ };
1285
+
1286
+ // src/server/index.ts
1287
+ var DEFAULT_OPTIONS = {
1288
+ port: 3e3,
1289
+ host: "localhost",
1290
+ watch: true,
1291
+ reloadMode: "ws",
1292
+ enableMutations: false,
1293
+ requestTimeout: 3e4
1294
+ };
1295
+ var OddDocsServer = class {
1296
+ server = null;
1297
+ fileServer;
1298
+ liveReload = null;
1299
+ apiHandler;
1300
+ options;
1301
+ state;
1302
+ constructor(options) {
1303
+ this.options = {
1304
+ ...DEFAULT_OPTIONS,
1305
+ ...options,
1306
+ // Auto-switch to SSE for non-localhost binding (safer for K8s)
1307
+ reloadMode: options.reloadMode ?? (options.host && options.host !== "localhost" ? "sse" : DEFAULT_OPTIONS.reloadMode)
1308
+ };
1309
+ this.options.host = process.env.ODD_DOCS_HOST ?? this.options.host;
1310
+ this.options.port = parseInt(process.env.ODD_DOCS_PORT ?? String(this.options.port), 10);
1311
+ this.options.mutationToken = process.env.ODD_DOCS_MUTATION_TOKEN ?? this.options.mutationToken;
1312
+ this.state = {
1313
+ lastGenerateSuccess: true,
1314
+ lastGenerateTime: (/* @__PURE__ */ new Date()).toISOString(),
1315
+ outputDirAccessible: true,
1316
+ introspectionConnected: false
1317
+ };
1318
+ this.fileServer = new FileServer(this.options.outputDir);
1319
+ this.apiHandler = new ApiHandler(this.state, this.options);
1320
+ }
1321
+ async start() {
1322
+ if (this.options.enableMutations && !this.options.mutationToken) {
1323
+ throw new Error(
1324
+ "Mutation token required when --enable-mutations is set. Set ODD_DOCS_MUTATION_TOKEN or --mutation-token"
1325
+ );
1326
+ }
1327
+ this.server = createServer((req, res) => this.handleRequest(req, res));
1328
+ this.server.timeout = this.options.requestTimeout;
1329
+ if (this.options.watch && this.options.reloadMode !== "none") {
1330
+ this.liveReload = new LiveReload({
1331
+ outputDir: this.options.outputDir,
1332
+ mode: this.options.reloadMode,
1333
+ server: this.server
1334
+ });
1335
+ await this.liveReload.start();
1336
+ }
1337
+ return new Promise((resolve4, reject) => {
1338
+ this.server.listen(this.options.port, this.options.host, () => {
1339
+ console.log(
1340
+ `[odd-docs] Server started at http://${this.options.host}:${this.options.port}`
1341
+ );
1342
+ console.log(`[odd-docs] Serving: ${this.options.outputDir}`);
1343
+ if (this.liveReload) {
1344
+ console.log(`[odd-docs] Live reload: ${this.options.reloadMode}`);
1345
+ }
1346
+ resolve4();
1347
+ });
1348
+ this.server.on("error", reject);
1349
+ });
1350
+ }
1351
+ async stop() {
1352
+ if (this.liveReload) {
1353
+ await this.liveReload.stop();
1354
+ }
1355
+ if (this.server) {
1356
+ return new Promise((resolve4) => {
1357
+ this.server.close(() => resolve4());
1358
+ });
1359
+ }
1360
+ }
1361
+ async handleRequest(req, res) {
1362
+ const url = req.url ?? "/";
1363
+ try {
1364
+ if (url.startsWith("/api/")) {
1365
+ await this.apiHandler.handle(req, res);
1366
+ return;
1367
+ }
1368
+ if (this.liveReload && url === "/__livereload") {
1369
+ await this.liveReload.handleRequest(req, res);
1370
+ return;
1371
+ }
1372
+ await this.fileServer.serve(req, res);
1373
+ } catch (error) {
1374
+ console.error("[odd-docs] Request error:", error);
1375
+ res.statusCode = 500;
1376
+ res.end("Internal Server Error");
1377
+ }
1378
+ }
1379
+ updateState(updates) {
1380
+ Object.assign(this.state, updates);
1381
+ }
1382
+ };
1383
+
1384
+ // src/cli/commands/serve.ts
1385
+ async function serve(repoPath, options = {}) {
1386
+ const resolvedPath = resolve3(repoPath);
1387
+ const outputDir = options.output ? resolve3(options.output) : join4(resolvedPath, "docs", "generated");
1388
+ if (!existsSync(outputDir)) {
1389
+ console.log("[odd-docs] Documentation not found, generating...");
1390
+ await generate(repoPath, { output: outputDir, format: "html" });
1391
+ }
1392
+ let reloadMode = options.reload ?? "ws";
1393
+ if (options.watchMode === "poll" || process.env.CHOKIDAR_USEPOLLING === "1") {
1394
+ reloadMode = "poll";
1395
+ console.log("[odd-docs] Using poll-based watching (CPU intensive)");
1396
+ }
1397
+ const host = options.host ?? "localhost";
1398
+ const port = options.port ?? 3e3;
1399
+ if (host !== "localhost" && options.reload === void 0) {
1400
+ reloadMode = "sse";
1401
+ console.log("[odd-docs] Using SSE for non-localhost binding");
1402
+ }
1403
+ const serverOptions = {
1404
+ outputDir,
1405
+ host,
1406
+ port,
1407
+ watch: options.watch !== false,
1408
+ reloadMode,
1409
+ enableMutations: options.enableMutations ?? false,
1410
+ mutationToken: options.mutationToken
1411
+ };
1412
+ const server = new OddDocsServer(serverOptions);
1413
+ const shutdown = async () => {
1414
+ console.log("\n[odd-docs] Shutting down...");
1415
+ await server.stop();
1416
+ process.exit(0);
1417
+ };
1418
+ process.on("SIGINT", shutdown);
1419
+ process.on("SIGTERM", shutdown);
1420
+ try {
1421
+ await server.start();
1422
+ if (options.open !== false && host === "localhost") {
1423
+ const url = `http://${host}:${port}`;
1424
+ try {
1425
+ const { default: open } = await import("open");
1426
+ await open(url);
1427
+ } catch {
1428
+ console.log(`[odd-docs] Open browser manually: ${url}`);
1429
+ }
1430
+ }
1431
+ } catch (error) {
1432
+ console.error("[odd-docs] Failed to start server:", error);
1433
+ process.exit(1);
1434
+ }
1435
+ }
1436
+
1437
+ // src/cli/index.ts
1438
+ var program = new Command();
1439
+ program.name("odd-docs").description("MCP-native documentation generator").version("0.1.0");
1440
+ program.command("generate").description("Generate documentation for an MCP repo").argument("<repo-path>", "Path to the repository").option("-f, --format <format>", "Output format: md, html, or both", "md").option("-o, --output <dir>", "Output directory").option("--introspect <url>", "MCP server URL for live introspection").action(async (repoPath, options) => {
1441
+ try {
1442
+ await generate(repoPath, {
1443
+ format: options.format,
1444
+ output: options.output,
1445
+ introspect: options.introspect
1446
+ });
1447
+ } catch (error) {
1448
+ console.error("Error:", error instanceof Error ? error.message : error);
1449
+ process.exit(1);
1450
+ }
1451
+ });
1452
+ program.command("validate").description("Validate documentation inputs for an MCP repo").argument("<repo-path>", "Path to the repository").option("-s, --strict", "Fail on unknown safety-affecting capabilities").action(async (repoPath, options) => {
1453
+ try {
1454
+ const valid = await validate(repoPath, { strict: options.strict });
1455
+ process.exit(valid ? 0 : 1);
1456
+ } catch (error) {
1457
+ console.error("Error:", error instanceof Error ? error.message : error);
1458
+ process.exit(1);
1459
+ }
1460
+ });
1461
+ program.command("serve").description("Start a local dev server for generated documentation").argument("<repo-path>", "Path to the repository").option("-p, --port <port>", "Port to listen on", "3000").option("-H, --host <host>", "Host to bind (default: localhost for safety)", "localhost").option("-o, --output <dir>", "Documentation output directory").option("--no-watch", "Disable file watching").option("--watch-mode <mode>", "Watch mode: auto or poll (use poll for Docker/NFS)", "auto").option("--reload <mode>", "Reload mechanism: ws, sse, poll, none", "ws").option("--no-open", "Do not open browser on start").option("--introspect <target>", "MCP target: http://host:port or stdio:<cmd>").option("--enable-mutations", "Enable mutation API endpoints (requires token)").option("--mutation-token <token>", "Token for mutation endpoints (or ODD_DOCS_MUTATION_TOKEN)").action(async (repoPath, options) => {
1462
+ try {
1463
+ await serve(repoPath, {
1464
+ port: parseInt(options.port, 10),
1465
+ host: options.host,
1466
+ output: options.output,
1467
+ watch: options.watch,
1468
+ watchMode: options.watchMode,
1469
+ reload: options.reload,
1470
+ open: options.open,
1471
+ introspect: options.introspect,
1472
+ enableMutations: options.enableMutations,
1473
+ mutationToken: options.mutationToken
1474
+ });
1475
+ } catch (error) {
1476
+ console.error("Error:", error instanceof Error ? error.message : error);
1477
+ process.exit(1);
1478
+ }
1479
+ });
1480
+ program.parse();
1481
+ //# sourceMappingURL=index.js.map