docgenie-cli 1.1.0__py3-none-any.whl

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.
docgenie/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """
2
+ DocGenie - Auto-documentation tool for codebases
3
+
4
+ A powerful Python library that automatically generates comprehensive README documentation
5
+ for any codebase by analyzing source code, dependencies, and project structure.
6
+ """
7
+
8
+ __version__ = "1.1.0"
9
+ __author__ = "DocGenie Team"
10
+ __email__ = "contact@docgenie.dev"
11
+
12
+ from .core import CodebaseAnalyzer
13
+ from .generator import ReadmeGenerator
14
+
15
+ __all__ = ["CodebaseAnalyzer", "ReadmeGenerator"]
docgenie/__main__.py ADDED
@@ -0,0 +1,13 @@
1
+ """Module entry point for `python -m docgenie`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .cli import app
6
+
7
+
8
+ def main() -> None:
9
+ app(prog_name="docgenie")
10
+
11
+
12
+ if __name__ == "__main__":
13
+ main()
docgenie/cli.py ADDED
@@ -0,0 +1,499 @@
1
+ """Command-line interface for DocGenie."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import webbrowser
8
+ from pathlib import Path
9
+
10
+ import typer
11
+ import yaml
12
+ from rich.console import Console
13
+ from rich.progress import Progress
14
+ from rich.table import Table
15
+
16
+ from .config import load_config
17
+ from .core import CodebaseAnalyzer
18
+ from .generator import ReadmeGenerator
19
+ from .html_generator import HTMLGenerator
20
+ from .index_store import IndexStore
21
+ from .logging import configure_logging, get_logger
22
+
23
+ app = typer.Typer(add_completion=False, help="DocGenie - Auto-documentation for any codebase.")
24
+ index_app = typer.Typer(add_completion=False, help="Manage persistent DocGenie index store.")
25
+ app.add_typer(index_app, name="index")
26
+ console = Console()
27
+
28
+ OutputSpec = tuple[str, Path]
29
+
30
+
31
+ def _print_summary(analysis_data: dict, target_formats: str) -> None:
32
+ table = Table(title="DocGenie Summary", show_lines=True)
33
+ table.add_column("Metric")
34
+ table.add_column("Value")
35
+ table.add_row("Project", analysis_data.get("project_name", "unknown"))
36
+ table.add_row("Files", str(analysis_data.get("files_analyzed", 0)))
37
+ table.add_row("Languages", ", ".join(analysis_data.get("languages", {}).keys()))
38
+ table.add_row("Functions", str(len(analysis_data.get("functions", []))))
39
+ table.add_row("Classes", str(len(analysis_data.get("classes", []))))
40
+ table.add_row("Format", target_formats)
41
+ repo = analysis_data.get("git_info", {}).get("remote_url")
42
+ if repo:
43
+ table.add_row("Repository", repo)
44
+ console.print(table)
45
+
46
+
47
+ def _validate_format(fmt: str) -> str:
48
+ target_formats = fmt.lower()
49
+ if target_formats not in {"markdown", "html", "both"}:
50
+ typer.echo("Invalid format. Choose markdown, html, or both.")
51
+ raise typer.Exit(code=1)
52
+ return target_formats
53
+
54
+
55
+ def _run_analysis(
56
+ path: Path,
57
+ ignore: list[str],
58
+ tree_sitter: bool,
59
+ verbose: bool,
60
+ config_overrides: dict | None = None,
61
+ ) -> dict:
62
+ config = load_config(path)
63
+ if config_overrides:
64
+ for key, value in config_overrides.items():
65
+ if isinstance(value, dict) and isinstance(config.get(key), dict):
66
+ config[key].update(value)
67
+ else:
68
+ config[key] = value
69
+ # Merge CLI ignore patterns with config ignore patterns
70
+ config_ignore = config.get("ignore_patterns", [])
71
+ combined_ignore = list(set(ignore + config_ignore))
72
+
73
+ analyzer = CodebaseAnalyzer(
74
+ str(path),
75
+ combined_ignore,
76
+ enable_tree_sitter=tree_sitter,
77
+ config=config,
78
+ )
79
+ with Progress(console=console, transient=True) as progress:
80
+ task = progress.add_task("Analyzing codebase...", total=100)
81
+ analysis_data = analyzer.analyze()
82
+ progress.update(task, completed=100)
83
+ if verbose:
84
+ console.log("Analysis complete")
85
+ return analysis_data
86
+
87
+
88
+ def _content_hash(text: str) -> str:
89
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
90
+
91
+
92
+ def _section_hashes(content: str) -> dict[str, str]:
93
+ hashes: dict[str, str] = {}
94
+ for block in content.split("\n## "):
95
+ if not block.strip():
96
+ continue
97
+ title = block.splitlines()[0].lstrip("# ").strip()
98
+ hashes[title or "document"] = _content_hash(block)
99
+ return hashes
100
+
101
+
102
+ def _record_artifact(path: Path, target: str, content: str, root: Path) -> None:
103
+ try:
104
+ store = IndexStore(root)
105
+ run_id = store.latest_run_id()
106
+ if run_id is not None:
107
+ store.add_doc_artifact(
108
+ run_id=run_id,
109
+ artifact_path=str(path),
110
+ target=target,
111
+ content_hash=_content_hash(content),
112
+ section_hashes=_section_hashes(content),
113
+ )
114
+ store.commit()
115
+ store.close()
116
+ except OSError:
117
+ pass
118
+
119
+
120
+ def _build_outputs(target_formats: str, output: Path | None, base: Path) -> list[OutputSpec]:
121
+ outputs: list[OutputSpec] = []
122
+ if target_formats in {"markdown", "both"}:
123
+ outputs.append(("markdown", _resolve_output(output, base, "README.md")))
124
+ if target_formats in {"html", "both"}:
125
+ outputs.append(("html", _resolve_output(output, base, "docs.html")))
126
+ return outputs
127
+
128
+
129
+ def _confirm_overwrite(outputs: list[OutputSpec], *, preview: bool, force: bool) -> None:
130
+ if preview or force:
131
+ return
132
+ for _, out_path in outputs:
133
+ if out_path.exists() and not typer.confirm(f"{out_path.name} exists. Overwrite?"):
134
+ typer.echo("Operation cancelled.")
135
+ raise typer.Exit(code=1)
136
+
137
+
138
+ def _render_outputs(outputs: list[OutputSpec], analysis_data: dict, *, preview: bool) -> None:
139
+ for output_format, output_path in outputs:
140
+ if output_format == "markdown":
141
+ generator = ReadmeGenerator()
142
+ content = generator.generate(analysis_data, None if preview else str(output_path))
143
+ if preview:
144
+ console.rule("README Preview")
145
+ typer.echo(content)
146
+ else:
147
+ console.log(f"[green]README generated:[/green] {output_path}")
148
+ else:
149
+ html_generator = HTMLGenerator()
150
+ content = html_generator.generate_from_analysis(
151
+ analysis_data, None if preview else str(output_path)
152
+ )
153
+ if preview:
154
+ console.rule("HTML Preview (truncated)")
155
+ typer.echo("\n".join(content.splitlines()[:80]))
156
+ else:
157
+ console.log(f"[green]HTML generated:[/green] {output_path}")
158
+
159
+
160
+ @app.command("generate")
161
+ def generate( # noqa: PLR0913
162
+ path: Path = typer.Argument(
163
+ Path("."), exists=True, file_okay=False, dir_okay=True, resolve_path=True
164
+ ),
165
+ output: Path | None = typer.Option(
166
+ None, "--output", "-o", help="Output path for documentation."
167
+ ),
168
+ fmt: str = typer.Option(
169
+ "markdown",
170
+ "--format",
171
+ "--fmt",
172
+ help="Output format",
173
+ case_sensitive=False,
174
+ rich_help_panel="Output",
175
+ ),
176
+ ignore: list[str] = typer.Option([], "--ignore", "-i", help="Additional ignore patterns"),
177
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
178
+ preview: bool = typer.Option(False, "--preview", "-p", help="Preview without saving"),
179
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
180
+ tree_sitter: bool = typer.Option(
181
+ True,
182
+ "--tree-sitter/--no-tree-sitter",
183
+ help="Enable tree-sitter parsing when available",
184
+ ),
185
+ mode: str = typer.Option("auto", "--mode", help="Output mode: auto|single|package"),
186
+ engine: str = typer.Option("hybrid", "--engine", help="Engine: hybrid|stateless"),
187
+ incremental: bool = typer.Option(
188
+ True,
189
+ "--incremental/--no-incremental",
190
+ help="Enable incremental analysis",
191
+ ),
192
+ max_file_size_kb: int = typer.Option(
193
+ 512, "--max-file-size-kb", help="Skip files larger than this size in KB"
194
+ ),
195
+ redaction_mode: str = typer.Option(
196
+ "strict",
197
+ "--redaction-mode",
198
+ help="Redaction mode: strict|balanced|open",
199
+ ),
200
+ json_logs: bool = typer.Option(False, "--json-logs", help="Output structured logs as JSON"),
201
+ ) -> None:
202
+ """Generate README and/or HTML docs for a codebase."""
203
+ configure_logging(verbose=verbose, json_output=json_logs)
204
+ logger = get_logger(__name__)
205
+
206
+ target_formats = _validate_format(fmt)
207
+ console.rule("[bold cyan]DocGenie")
208
+ logger.info("Starting documentation generation", path=str(path), format=target_formats)
209
+
210
+ config_overrides = {
211
+ "analysis": {
212
+ "engine": "hybrid_index" if engine == "hybrid" else "stateless",
213
+ "incremental": incremental,
214
+ "max_file_size_kb": max_file_size_kb,
215
+ },
216
+ "monorepo": {"mode": mode},
217
+ "safety": {"redaction_mode": redaction_mode},
218
+ }
219
+ analysis_data = _run_analysis(
220
+ path, ignore, tree_sitter, verbose, config_overrides=config_overrides
221
+ )
222
+ outputs = _build_outputs(target_formats, output, path)
223
+ _confirm_overwrite(outputs, preview=preview, force=force)
224
+ monorepo_config = (
225
+ analysis_data.get("config", {}).get("monorepo", {})
226
+ if isinstance(analysis_data.get("config"), dict)
227
+ else {}
228
+ )
229
+ package_mode = mode == "package" or (
230
+ mode == "auto" and len(analysis_data.get("packages", [])) > 1
231
+ )
232
+ generate_root_doc = bool(monorepo_config.get("root_doc", True)) or not package_mode
233
+ per_package_docs = bool(monorepo_config.get("per_package_docs", True))
234
+ package_output_dir = Path(str(monorepo_config.get("package_output_dir", ".docgenie/packages")))
235
+
236
+ for output_format, output_path in outputs:
237
+ if output_format == "markdown":
238
+ generator = ReadmeGenerator()
239
+ if generate_root_doc:
240
+ content = generator.generate(analysis_data, None if preview else str(output_path))
241
+ if preview:
242
+ console.rule("README Preview")
243
+ typer.echo(content)
244
+ else:
245
+ console.log(f"[green]README generated:[/green] {output_path}")
246
+ _record_artifact(output_path, "root", content, path)
247
+ if package_mode and per_package_docs and not preview:
248
+ pkg_artifacts = generator.generate_package_docs(
249
+ analysis_data, path / package_output_dir
250
+ )
251
+ for pkg_name, pkg_content in pkg_artifacts.items():
252
+ artifact_path = path / package_output_dir / pkg_name / "README.md"
253
+ _record_artifact(artifact_path, pkg_name, pkg_content, path)
254
+ else:
255
+ html_generator = HTMLGenerator()
256
+ if generate_root_doc:
257
+ content = html_generator.generate_from_analysis(
258
+ analysis_data, None if preview else str(output_path)
259
+ )
260
+ if preview:
261
+ console.rule("HTML Preview (truncated)")
262
+ typer.echo("\n".join(content.splitlines()[:80]))
263
+ else:
264
+ console.log(f"[green]HTML generated:[/green] {output_path}")
265
+ _record_artifact(output_path, "root_html", content, path)
266
+
267
+ if not preview:
268
+ _print_summary(analysis_data, target_formats)
269
+
270
+
271
+ def _resolve_output(output: Path | None, base: Path, default_name: str) -> Path:
272
+ if output is None:
273
+ return base / default_name
274
+ if output.is_dir():
275
+ return output / default_name
276
+ return output
277
+
278
+
279
+ @app.command("analyze")
280
+ def analyze(
281
+ path: Path = typer.Argument(Path("."), exists=True, resolve_path=True),
282
+ fmt: str = typer.Option("text", "--format", "-f", help="Output format"),
283
+ tree_sitter: bool = typer.Option(
284
+ True,
285
+ "--tree-sitter/--no-tree-sitter",
286
+ help="Enable tree-sitter parsing when available",
287
+ ),
288
+ metrics_json: Path | None = typer.Option(
289
+ None, "--metrics-json", help="Optional path to write run metrics as JSON"
290
+ ),
291
+ engine: str = typer.Option("hybrid", "--engine", help="Engine: hybrid|stateless"),
292
+ incremental: bool = typer.Option(True, "--incremental/--no-incremental"),
293
+ ) -> None:
294
+ """Analyze a codebase and print structured results."""
295
+ analysis_data = _run_analysis(
296
+ path,
297
+ ignore=[],
298
+ tree_sitter=tree_sitter,
299
+ verbose=False,
300
+ config_overrides={
301
+ "analysis": {
302
+ "engine": "hybrid_index" if engine == "hybrid" else "stateless",
303
+ "incremental": incremental,
304
+ }
305
+ },
306
+ )
307
+
308
+ if metrics_json is not None:
309
+ metrics_json.write_text(
310
+ json.dumps(analysis_data.get("run_metrics", {}), indent=2, sort_keys=True),
311
+ encoding="utf-8",
312
+ )
313
+
314
+ if fmt == "json":
315
+ typer.echo(json.dumps(analysis_data, indent=2))
316
+ elif fmt == "yaml":
317
+ typer.echo(yaml.dump(analysis_data, default_flow_style=False))
318
+ else:
319
+ typer.echo("Codebase Analysis Results")
320
+ typer.echo(f"Path: {analysis_data.get('root_path')}")
321
+ typer.echo(f"Files analyzed: {analysis_data['files_analyzed']}")
322
+ typer.echo(f"Languages: {', '.join(analysis_data['languages'].keys())}")
323
+ typer.echo(f"Functions: {len(analysis_data['functions'])}")
324
+ typer.echo(f"Classes: {len(analysis_data['classes'])}")
325
+
326
+
327
+ @app.command("init")
328
+ def init_project_config(
329
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
330
+ ) -> None:
331
+ """Create a starter .docgenie.yaml configuration file."""
332
+ config_path = Path(".docgenie.yaml")
333
+ if config_path.exists() and not force:
334
+ typer.echo("Config already exists. Use --force to overwrite.")
335
+ raise typer.Exit(code=1)
336
+
337
+ template = """# DocGenie configuration
338
+ ignore_patterns:
339
+ - "*.log"
340
+ - "build/"
341
+ - "dist/"
342
+
343
+ template_customizations:
344
+ include_api_docs: true
345
+ include_directory_tree: true
346
+ max_functions_documented: 25
347
+
348
+ analysis:
349
+ use_gitignore: true
350
+ exclude_generated: true
351
+ include_hidden: false
352
+ max_file_size_kb: 512
353
+ generated_patterns: []
354
+ engine: hybrid_index
355
+ incremental: true
356
+ parallelism: auto
357
+ hard_file_cap: 300000
358
+ full_rescan_interval_runs: 20
359
+
360
+ monorepo:
361
+ mode: auto
362
+ root_doc: true
363
+ per_package_docs: true
364
+ package_output_dir: ".docgenie/packages"
365
+
366
+ quality:
367
+ confidence_enabled: true
368
+ include_warnings: true
369
+ min_confidence_for_api_docs: low
370
+
371
+ safety:
372
+ redaction_mode: strict
373
+ redact_patterns: []
374
+ """
375
+ config_path.write_text(template, encoding="utf-8")
376
+ console.log(f"[green]Created {config_path}[/green]")
377
+
378
+
379
+ @app.command("html")
380
+ def html_command( # noqa: PLR0913
381
+ input_path: Path = typer.Argument(..., exists=True, resolve_path=True),
382
+ output: Path | None = typer.Option(None, "--output", "-o", help="Output HTML path"),
383
+ source: str = typer.Option("readme", "--source", "-s", help="readme or codebase"),
384
+ title: str | None = typer.Option(None, "--title", "-t", help="Custom HTML title"),
385
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
386
+ open_browser: bool = typer.Option(
387
+ False,
388
+ "--open-browser",
389
+ "--open",
390
+ help="Open generated HTML in browser",
391
+ ),
392
+ verbose: bool = typer.Option(False, "--verbose", "-v"),
393
+ tree_sitter: bool = typer.Option(True, "--tree-sitter/--no-tree-sitter"),
394
+ ) -> None:
395
+ """Convert README to HTML or generate HTML from codebase analysis."""
396
+ html_generator = HTMLGenerator()
397
+ output_path = output
398
+ if not output_path:
399
+ output_path = (input_path.parent if source == "readme" else input_path) / "docs.html"
400
+ elif output_path.is_dir():
401
+ output_path = output_path / "docs.html"
402
+
403
+ if (
404
+ output_path.exists()
405
+ and (not force)
406
+ and (not typer.confirm(f"{output_path} exists. Overwrite?"))
407
+ ):
408
+ raise typer.Exit(code=1)
409
+
410
+ if source == "readme":
411
+ if not input_path.is_file() or input_path.suffix.lower() != ".md":
412
+ typer.echo("Input must be a markdown file when --source readme")
413
+ raise typer.Exit(code=1)
414
+ readme_content = input_path.read_text(encoding="utf-8")
415
+ project_name = title or _extract_title(readme_content) or input_path.stem
416
+ html_generator.generate_from_readme(readme_content, str(output_path), project_name)
417
+ else:
418
+ analyzer = CodebaseAnalyzer(str(input_path), enable_tree_sitter=tree_sitter)
419
+ if verbose:
420
+ console.log(f"Analyzing codebase at {input_path}")
421
+ analysis_data = analyzer.analyze()
422
+ html_generator.generate_from_analysis(analysis_data, str(output_path))
423
+
424
+ console.log(f"[green]HTML generated:[/green] {output_path}")
425
+ if open_browser:
426
+ webbrowser.open(output_path.resolve().as_uri())
427
+
428
+
429
+ def _extract_title(content: str) -> str | None:
430
+ for line in content.splitlines():
431
+ if line.startswith("# "):
432
+ return line[2:].strip()
433
+ return None
434
+
435
+
436
+ @index_app.command("rebuild")
437
+ def index_rebuild(
438
+ path: Path = typer.Argument(Path("."), exists=True, file_okay=False, resolve_path=True),
439
+ ) -> None:
440
+ """Rebuild persistent index for a repository."""
441
+ store = IndexStore(path)
442
+ store.clear_all()
443
+ store.commit()
444
+ store.close()
445
+ typer.echo(f"Index rebuilt at {path / '.docgenie' / 'index.db'}")
446
+
447
+
448
+ @index_app.command("stats")
449
+ def index_stats(
450
+ path: Path = typer.Argument(Path("."), exists=True, file_okay=False, resolve_path=True),
451
+ ) -> None:
452
+ """Print index statistics."""
453
+ store = IndexStore(path)
454
+ stats = store.stats()
455
+ store.close()
456
+ typer.echo(json.dumps(stats, indent=2, sort_keys=True))
457
+
458
+
459
+ @app.command("diff")
460
+ def diff_command(
461
+ path: Path = typer.Argument(Path("."), exists=True, file_okay=False, resolve_path=True),
462
+ since: str = typer.Option(..., "--since", help="Run ID or git ref"),
463
+ ) -> None:
464
+ """Show documentation impact since a prior indexed run/git ref."""
465
+ store = IndexStore(path)
466
+ latest = store.latest_run_id()
467
+ if latest is None:
468
+ typer.echo("No runs in index yet.")
469
+ store.close()
470
+ raise typer.Exit(code=1)
471
+
472
+ # Git ref mode falls back to previous run for compatibility.
473
+ base_id = int(since) if since.isdigit() else max(1, latest - 1)
474
+
475
+ latest_artifacts = {a["artifact_path"]: a for a in store.list_artifacts_for_run(latest)}
476
+ base_artifacts = {a["artifact_path"]: a for a in store.list_artifacts_for_run(base_id)}
477
+ changed: list[str] = []
478
+ for artifact_path, latest_art in latest_artifacts.items():
479
+ base_art = base_artifacts.get(artifact_path)
480
+ if not base_art or base_art.get("content_hash") != latest_art.get("content_hash"):
481
+ changed.append(artifact_path)
482
+
483
+ typer.echo(
484
+ json.dumps(
485
+ {
486
+ "latest_run_id": latest,
487
+ "base_run_id": base_id,
488
+ "changed_artifacts": sorted(changed),
489
+ "changed_count": len(changed),
490
+ },
491
+ indent=2,
492
+ sort_keys=True,
493
+ )
494
+ )
495
+ store.close()
496
+
497
+
498
+ if __name__ == "__main__":
499
+ app()
docgenie/config.py ADDED
@@ -0,0 +1,89 @@
1
+ """Configuration management for DocGenie."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+
11
+ def load_config(root_path: Path) -> dict[str, Any]:
12
+ """
13
+ Load configuration from .docgenie.yaml in the project root.
14
+ Returns a default configuration if the file doesn't exist.
15
+ """
16
+ config_path = root_path / ".docgenie.yaml"
17
+ if not config_path.exists():
18
+ return get_default_config()
19
+
20
+ try:
21
+ with open(config_path, encoding="utf-8") as f:
22
+ user_config = yaml.safe_load(f) or {}
23
+ return merge_configs(get_default_config(), user_config)
24
+ except (yaml.YAMLError, OSError):
25
+ # Return default config if loading fails
26
+ return get_default_config()
27
+
28
+
29
+ def get_default_config() -> dict[str, Any]:
30
+ """Return the default configuration."""
31
+ return {
32
+ "ignore_patterns": [
33
+ "*.log",
34
+ "build/",
35
+ "dist/",
36
+ "*.egg-info",
37
+ "__pycache__",
38
+ ".git",
39
+ ".idea",
40
+ ".vscode",
41
+ "node_modules",
42
+ "venv",
43
+ ".venv",
44
+ "env",
45
+ ],
46
+ "analysis": {
47
+ "use_gitignore": True,
48
+ "exclude_generated": True,
49
+ "include_hidden": False,
50
+ "max_file_size_kb": 512,
51
+ "generated_patterns": [],
52
+ "engine": "hybrid_index",
53
+ "incremental": True,
54
+ "parallelism": "auto",
55
+ "hard_file_cap": 300000,
56
+ "full_rescan_interval_runs": 20,
57
+ },
58
+ "monorepo": {
59
+ "mode": "auto",
60
+ "root_doc": True,
61
+ "per_package_docs": True,
62
+ "package_output_dir": ".docgenie/packages",
63
+ },
64
+ "quality": {
65
+ "confidence_enabled": True,
66
+ "include_warnings": True,
67
+ "min_confidence_for_api_docs": "low",
68
+ },
69
+ "safety": {
70
+ "redaction_mode": "strict",
71
+ "redact_patterns": [],
72
+ },
73
+ "template_customizations": {
74
+ "include_api_docs": True,
75
+ "include_directory_tree": True,
76
+ "max_functions_documented": 10,
77
+ },
78
+ }
79
+
80
+
81
+ def merge_configs(default: dict[str, Any], user: dict[str, Any]) -> dict[str, Any]:
82
+ """Deep merge user config into default config."""
83
+ result = default.copy()
84
+ for key, value in user.items():
85
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
86
+ result[key] = merge_configs(result[key], value)
87
+ else:
88
+ result[key] = value
89
+ return result
@@ -0,0 +1,19 @@
1
+ """`docgenie-html` console-script entrypoint.
2
+
3
+ This provides backwards-compatible access to the `docgenie html ...` subcommand.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+
10
+ from .cli import app
11
+
12
+
13
+ def main() -> None:
14
+ # Forward all args to the Typer subcommand.
15
+ app(args=["html", *sys.argv[1:]], prog_name="docgenie-html")
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()