@voodocs/cli 0.1.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.
Files changed (52) hide show
  1. package/LICENSE +37 -0
  2. package/README.md +153 -0
  3. package/USAGE.md +314 -0
  4. package/cli.py +1340 -0
  5. package/examples/.cursorrules +437 -0
  6. package/examples/instructions/.claude/instructions.md +372 -0
  7. package/examples/instructions/.cursorrules +437 -0
  8. package/examples/instructions/.windsurfrules +437 -0
  9. package/examples/instructions/VOODOCS_INSTRUCTIONS.md +437 -0
  10. package/examples/math_example.py +41 -0
  11. package/examples/phase2_test.py +24 -0
  12. package/examples/test_compound_conditions.py +40 -0
  13. package/examples/test_math_example.py +186 -0
  14. package/lib/darkarts/README.md +115 -0
  15. package/lib/darkarts/__init__.py +16 -0
  16. package/lib/darkarts/annotations/__init__.py +34 -0
  17. package/lib/darkarts/annotations/parser.py +618 -0
  18. package/lib/darkarts/annotations/types.py +181 -0
  19. package/lib/darkarts/cli.py +128 -0
  20. package/lib/darkarts/core/__init__.py +32 -0
  21. package/lib/darkarts/core/interface.py +256 -0
  22. package/lib/darkarts/core/loader.py +231 -0
  23. package/lib/darkarts/core/plugin.py +215 -0
  24. package/lib/darkarts/core/registry.py +146 -0
  25. package/lib/darkarts/exceptions.py +51 -0
  26. package/lib/darkarts/parsers/typescript/dist/cli.d.ts +9 -0
  27. package/lib/darkarts/parsers/typescript/dist/cli.d.ts.map +1 -0
  28. package/lib/darkarts/parsers/typescript/dist/cli.js +69 -0
  29. package/lib/darkarts/parsers/typescript/dist/cli.js.map +1 -0
  30. package/lib/darkarts/parsers/typescript/dist/parser.d.ts +111 -0
  31. package/lib/darkarts/parsers/typescript/dist/parser.d.ts.map +1 -0
  32. package/lib/darkarts/parsers/typescript/dist/parser.js +365 -0
  33. package/lib/darkarts/parsers/typescript/dist/parser.js.map +1 -0
  34. package/lib/darkarts/parsers/typescript/package-lock.json +51 -0
  35. package/lib/darkarts/parsers/typescript/package.json +19 -0
  36. package/lib/darkarts/parsers/typescript/src/cli.ts +41 -0
  37. package/lib/darkarts/parsers/typescript/src/parser.ts +408 -0
  38. package/lib/darkarts/parsers/typescript/tsconfig.json +19 -0
  39. package/lib/darkarts/plugins/voodocs/__init__.py +379 -0
  40. package/lib/darkarts/plugins/voodocs/ai_native_plugin.py +151 -0
  41. package/lib/darkarts/plugins/voodocs/annotation_validator.py +280 -0
  42. package/lib/darkarts/plugins/voodocs/api_spec_generator.py +486 -0
  43. package/lib/darkarts/plugins/voodocs/documentation_generator.py +610 -0
  44. package/lib/darkarts/plugins/voodocs/html_exporter.py +260 -0
  45. package/lib/darkarts/plugins/voodocs/instruction_generator.py +706 -0
  46. package/lib/darkarts/plugins/voodocs/pdf_exporter.py +66 -0
  47. package/lib/darkarts/plugins/voodocs/test_generator.py +636 -0
  48. package/package.json +70 -0
  49. package/requirements.txt +13 -0
  50. package/templates/ci/github-actions.yml +73 -0
  51. package/templates/ci/gitlab-ci.yml +35 -0
  52. package/templates/ci/pre-commit-hook.sh +26 -0
package/cli.py ADDED
@@ -0,0 +1,1340 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ VooDocs CLI - AI-native documentation for modern development
4
+
5
+ Commands:
6
+ init - Initialize VooDocs in a project
7
+ instruct - Generate AI assistant instructions
8
+ generate - Generate docs, tests, and API specs from annotations
9
+ status - Show project status and statistics
10
+ version - Show VooDocs version
11
+ """
12
+
13
+ import sys
14
+ import argparse
15
+ import traceback
16
+ import time
17
+ from pathlib import Path
18
+ from functools import wraps
19
+
20
+ # Add lib to path (resolve symlinks for npm bin)
21
+ import os
22
+ script_path = Path(os.path.realpath(__file__)).parent
23
+ sys.path.insert(0, str(script_path / "lib"))
24
+
25
+ from darkarts.plugins.voodocs.instruction_generator import InstructionGenerator
26
+ from darkarts.plugins.voodocs.documentation_generator import DocumentationGenerator
27
+ from darkarts.plugins.voodocs.test_generator import TestGenerator
28
+ from darkarts.plugins.voodocs.api_spec_generator import APISpecGenerator
29
+ from darkarts.plugins.voodocs.annotation_validator import AnnotationValidator
30
+ from darkarts.plugins.voodocs.html_exporter import HTMLExporter
31
+ from darkarts.plugins.voodocs.pdf_exporter import PDFExporter
32
+ from darkarts.annotations.parser import AnnotationParser
33
+ from darkarts.telemetry import get_telemetry_client, track_command
34
+ from darkarts.exceptions import (
35
+ VooDocsError,
36
+ ParserError,
37
+ ParserNotBuiltError,
38
+ AnnotationError,
39
+ GeneratorError,
40
+ ConfigurationError
41
+ )
42
+
43
+ # Telemetry environment variables
44
+ os.environ.setdefault("VOODOCS_SUPABASE_URL", "https://sjatkayudkbkmipubhfy.supabase.co")
45
+ os.environ.setdefault("VOODOCS_SUPABASE_ANON_KEY", "sb_publishable_rHfzdx4jc7QH3JFSRC-sDA_6HHhzdU5")
46
+
47
+ def track_command_execution(command_name):
48
+ """Decorator to track command execution with telemetry."""
49
+ def decorator(func):
50
+ @wraps(func)
51
+ def wrapper(args):
52
+ start_time = time.time()
53
+ success = False
54
+ error_type = None
55
+
56
+ try:
57
+ result = func(args)
58
+ success = True
59
+ return result
60
+ except Exception as e:
61
+ error_type = type(e).__name__
62
+ raise
63
+ finally:
64
+ duration_ms = int((time.time() - start_time) * 1000)
65
+
66
+ # Track telemetry
67
+ try:
68
+ track_command(
69
+ command=command_name,
70
+ success=success,
71
+ duration_ms=duration_ms,
72
+ error_type=error_type
73
+ )
74
+ except:
75
+ pass # Never let telemetry break the CLI
76
+
77
+ return wrapper
78
+ return decorator
79
+
80
+
81
+ def main():
82
+ """Main CLI entry point."""
83
+ parser = argparse.ArgumentParser(
84
+ prog="voodocs",
85
+ description="""VooDocs - AI-native documentation for modern development
86
+
87
+ VooDocs teaches AI coding assistants to document code in the DarkArts language,
88
+ then automatically generates human-readable docs, tests, and API specs.
89
+ """,
90
+ epilog="""Examples:
91
+ # Initialize a new project
92
+ voodocs init
93
+
94
+ # Generate AI assistant instructions
95
+ voodocs instruct --assistant cursor
96
+
97
+ # Generate docs, tests, and API specs
98
+ voodocs generate src/
99
+ voodocs generate --docs-only src/
100
+ voodocs generate --tests-only src/
101
+
102
+ # Validate annotation quality
103
+ voodocs validate src/ --strict
104
+
105
+ # Check coverage for CI/CD
106
+ voodocs check . --min-coverage 80 --min-quality 70
107
+
108
+ # Export documentation
109
+ voodocs export docs/api.md --format html
110
+
111
+ # Watch for changes
112
+ voodocs watch src/
113
+
114
+ For more information, visit: https://voodocs.com
115
+ Documentation: https://github.com/3vilEnterprises/vooodooo-magic/tree/main/packages/voodocs
116
+ """,
117
+ formatter_class=argparse.RawDescriptionHelpFormatter
118
+ )
119
+
120
+ parser.add_argument(
121
+ "--version",
122
+ action="version",
123
+ version="VooDocs 0.1.0"
124
+ )
125
+
126
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
127
+
128
+ # init command
129
+ init_parser = subparsers.add_parser(
130
+ "init",
131
+ help="Initialize VooDocs in a project"
132
+ )
133
+ init_parser.add_argument(
134
+ "--project-name",
135
+ type=str,
136
+ help="Project name (default: current directory name)"
137
+ )
138
+ init_parser.add_argument(
139
+ "--language",
140
+ type=str,
141
+ choices=["python", "typescript", "javascript"],
142
+ default="python",
143
+ help="Primary programming language (default: python)"
144
+ )
145
+
146
+ # instruct command
147
+ instruct_parser = subparsers.add_parser(
148
+ "instruct",
149
+ help="Generate AI assistant instructions"
150
+ )
151
+ instruct_parser.add_argument(
152
+ "--assistant",
153
+ type=str,
154
+ choices=["cursor", "claude", "copilot", "windsurf", "generic"],
155
+ help="AI assistant type (auto-detected if not specified)"
156
+ )
157
+ instruct_parser.add_argument(
158
+ "--language",
159
+ type=str,
160
+ choices=["python", "typescript", "javascript"],
161
+ default="python",
162
+ help="Primary programming language (default: python)"
163
+ )
164
+ instruct_parser.add_argument(
165
+ "--output",
166
+ type=str,
167
+ help="Output file path (default: assistant's config file)"
168
+ )
169
+
170
+ # generate command
171
+ generate_parser = subparsers.add_parser(
172
+ "generate",
173
+ help="Generate docs, tests, and API specs from annotations"
174
+ )
175
+ generate_parser.add_argument(
176
+ "--debug",
177
+ action="store_true",
178
+ help="Show full stack traces on errors"
179
+ )
180
+ generate_parser.add_argument(
181
+ "paths",
182
+ nargs="+",
183
+ help="Files or directories to process"
184
+ )
185
+ generate_parser.add_argument(
186
+ "--output-dir",
187
+ type=str,
188
+ default="./voodocs-output",
189
+ help="Output directory (default: ./voodocs-output)"
190
+ )
191
+ generate_parser.add_argument(
192
+ "--docs-only",
193
+ action="store_true",
194
+ help="Generate only documentation"
195
+ )
196
+ generate_parser.add_argument(
197
+ "--tests-only",
198
+ action="store_true",
199
+ help="Generate only tests"
200
+ )
201
+ generate_parser.add_argument(
202
+ "--api-only",
203
+ action="store_true",
204
+ help="Generate only API specs"
205
+ )
206
+ generate_parser.add_argument(
207
+ "--test-framework",
208
+ type=str,
209
+ choices=["pytest", "unittest", "jest"],
210
+ default="pytest",
211
+ help="Test framework (default: pytest)"
212
+ )
213
+ generate_parser.add_argument(
214
+ "--api-format",
215
+ type=str,
216
+ choices=["openapi", "swagger", "graphql"],
217
+ default="openapi",
218
+ help="API spec format (default: openapi)"
219
+ )
220
+ generate_parser.add_argument(
221
+ "--recursive",
222
+ action="store_true",
223
+ default=True,
224
+ help="Process directories recursively (default: true)"
225
+ )
226
+ generate_parser.add_argument(
227
+ "--verbose",
228
+ action="store_true",
229
+ help="Show detailed output"
230
+ )
231
+
232
+ # status command
233
+ status_parser = subparsers.add_parser(
234
+ "status",
235
+ help="Show project status and statistics"
236
+ )
237
+ status_parser.add_argument(
238
+ "paths",
239
+ nargs="*",
240
+ default=["."],
241
+ help="Paths to analyze (default: current directory)"
242
+ )
243
+
244
+ # watch command
245
+ watch_parser = subparsers.add_parser(
246
+ "watch",
247
+ help="Watch files and automatically regenerate documentation"
248
+ )
249
+ watch_parser.add_argument(
250
+ "paths",
251
+ nargs="+",
252
+ help="Paths to watch for changes"
253
+ )
254
+ watch_parser.add_argument(
255
+ "--output-dir",
256
+ default="./voodocs-output",
257
+ help="Output directory (default: ./voodocs-output)"
258
+ )
259
+ watch_parser.add_argument(
260
+ "--docs-only",
261
+ action="store_true",
262
+ help="Generate only documentation"
263
+ )
264
+ watch_parser.add_argument(
265
+ "--tests-only",
266
+ action="store_true",
267
+ help="Generate only tests"
268
+ )
269
+ watch_parser.add_argument(
270
+ "--api-only",
271
+ action="store_true",
272
+ help="Generate only API specs"
273
+ )
274
+ watch_parser.add_argument(
275
+ "--interval",
276
+ type=int,
277
+ default=2,
278
+ help="Check interval in seconds (default: 2)"
279
+ )
280
+
281
+ # validate command
282
+ validate_parser = subparsers.add_parser(
283
+ "validate",
284
+ help="Validate annotation quality and correctness"
285
+ )
286
+ validate_parser.add_argument(
287
+ "paths",
288
+ nargs="+",
289
+ help="Paths to validate"
290
+ )
291
+ validate_parser.add_argument(
292
+ "--strict",
293
+ action="store_true",
294
+ help="Treat warnings as errors"
295
+ )
296
+ validate_parser.add_argument(
297
+ "--format",
298
+ choices=["text", "json"],
299
+ default="text",
300
+ help="Output format (default: text)"
301
+ )
302
+
303
+ # check command (for CI/CD)
304
+ check_parser = subparsers.add_parser(
305
+ "check",
306
+ help="Check annotation coverage and quality (for CI/CD)"
307
+ )
308
+ check_parser.add_argument(
309
+ "paths",
310
+ nargs="*",
311
+ default=["."],
312
+ help="Paths to check (default: current directory)"
313
+ )
314
+ check_parser.add_argument(
315
+ "--min-coverage",
316
+ type=int,
317
+ default=0,
318
+ help="Minimum annotation coverage percentage (0-100)"
319
+ )
320
+ check_parser.add_argument(
321
+ "--min-quality",
322
+ type=int,
323
+ default=0,
324
+ help="Minimum quality score (0-100)"
325
+ )
326
+
327
+ # export command
328
+ export_parser = subparsers.add_parser(
329
+ "export",
330
+ help="Export documentation to different formats"
331
+ )
332
+ export_parser.add_argument(
333
+ "input_file",
334
+ help="Input Markdown file to export"
335
+ )
336
+ export_parser.add_argument(
337
+ "--format",
338
+ choices=["html", "pdf"],
339
+ required=True,
340
+ help="Export format"
341
+ )
342
+ export_parser.add_argument(
343
+ "--output",
344
+ help="Output file path (optional)"
345
+ )
346
+
347
+ # telemetry command
348
+ telemetry_parser = subparsers.add_parser(
349
+ "telemetry",
350
+ help="Manage telemetry settings"
351
+ )
352
+ telemetry_parser.add_argument(
353
+ "action",
354
+ choices=["enable", "disable", "status"],
355
+ help="Telemetry action"
356
+ )
357
+
358
+ args = parser.parse_args()
359
+
360
+ # Execute command
361
+ if args.command == "init":
362
+ cmd_init(args)
363
+ elif args.command == "instruct":
364
+ cmd_instruct(args)
365
+ elif args.command == "generate":
366
+ cmd_generate(args)
367
+ elif args.command == "status":
368
+ cmd_status(args)
369
+ elif args.command == "watch":
370
+ cmd_watch(args)
371
+ elif args.command == "validate":
372
+ cmd_validate(args)
373
+ elif args.command == "check":
374
+ cmd_check(args)
375
+ elif args.command == "export":
376
+ cmd_export(args)
377
+ elif args.command == "telemetry":
378
+ cmd_telemetry(args)
379
+ else:
380
+ parser.print_help()
381
+ sys.exit(1)
382
+
383
+
384
+ @track_command_execution("init")
385
+ def cmd_init(args):
386
+ """Initialize VooDocs in a project."""
387
+ print("🎯 VooDocs Initialization")
388
+ print("=" * 60)
389
+ print()
390
+
391
+ try:
392
+ # Interactive prompts if not provided
393
+ if not args.project_name:
394
+ default_name = Path.cwd().name
395
+ response = input(f"Project name [{default_name}]: ").strip()
396
+ project_name = response if response else default_name
397
+ else:
398
+ project_name = args.project_name
399
+
400
+ if not args.language:
401
+ print("\nSelect primary language:")
402
+ print(" 1. Python")
403
+ print(" 2. TypeScript")
404
+ print(" 3. JavaScript")
405
+ response = input("Choice [1]: ").strip()
406
+ language_map = {"1": "python", "2": "typescript", "3": "javascript", "": "python"}
407
+ language = language_map.get(response, "python")
408
+ else:
409
+ language = args.language
410
+
411
+ print(f"\n📦 Setting up VooDocs for '{project_name}'...")
412
+
413
+ # Create .voodocs directory
414
+ voodocs_dir = Path(".voodocs")
415
+ voodocs_dir.mkdir(exist_ok=True)
416
+
417
+ # Create config file with all options
418
+ config = {
419
+ "project_name": project_name,
420
+ "language": language,
421
+ "version": "0.1.0",
422
+ "output_dir": "./voodocs-output",
423
+ "test_framework": "pytest",
424
+ "api_format": "openapi",
425
+ "exclude_patterns": [
426
+ "**/test_*.py",
427
+ "**/node_modules/**",
428
+ "**/__pycache__/**",
429
+ "**/venv/**",
430
+ "**/.venv/**"
431
+ ],
432
+ "include_patterns": [],
433
+ "annotation_coverage_threshold": 50
434
+ }
435
+
436
+ import json
437
+ config_file = voodocs_dir / "config.json"
438
+ config_file.write_text(json.dumps(config, indent=2))
439
+
440
+ print(f"✓ Created .voodocs/config.json")
441
+ print(f"✓ Project: {project_name}")
442
+ print(f"✓ Language: {language}")
443
+ print()
444
+ print("Next steps:")
445
+ print(" 1. Run 'voodocs instruct' to generate AI assistant instructions")
446
+ print(" 2. Start coding with @voodocs annotations")
447
+ print(" 3. Run 'voodocs generate <path>' to generate docs and tests")
448
+
449
+ except PermissionError:
450
+ print("❌ Permission denied: Cannot create .voodocs directory")
451
+ print(" Check directory permissions and try again.")
452
+ sys.exit(1)
453
+
454
+ except Exception as e:
455
+ print(f"❌ Failed to initialize VooDocs: {e}")
456
+ sys.exit(1)
457
+
458
+
459
+ @track_command_execution("instruct")
460
+ def cmd_instruct(args):
461
+ """Generate AI assistant instructions."""
462
+ print("📝 Generating AI assistant instructions...")
463
+
464
+ try:
465
+ # Load config
466
+ config = load_config()
467
+
468
+ # Detect or use specified assistant
469
+ assistant = args.assistant
470
+ if not assistant:
471
+ generator = InstructionGenerator()
472
+ assistant = generator.detect_assistant()
473
+ if assistant:
474
+ print(f"✓ Detected AI assistant: {assistant}")
475
+ else:
476
+ assistant = "generic"
477
+ print(f"⚠ Could not detect AI assistant, using generic")
478
+
479
+ # Generate instructions
480
+ generator = InstructionGenerator(
481
+ assistant=assistant,
482
+ project_name=config.get("project_name", "Your Project")
483
+ )
484
+
485
+ output_file = args.output
486
+ language = args.language or config.get("language", "python")
487
+
488
+ instructions = generator.generate(
489
+ output_file=output_file,
490
+ language=language
491
+ )
492
+
493
+ output_path = output_file or generator.assistant_info["config_file"]
494
+
495
+ print(f"✓ Generated instructions: {output_path}")
496
+ print(f"✓ File size: {len(instructions):,} characters")
497
+ print()
498
+ print(f"Instructions have been generated for {generator.assistant_info['name']}.")
499
+ print(f"Your AI assistant will now automatically add @voodocs annotations!")
500
+
501
+ except PermissionError as e:
502
+ print(f"❌ Permission denied: Cannot write to {output_path if 'output_path' in locals() else 'output file'}")
503
+ print(" Check file permissions and try again.")
504
+ sys.exit(1)
505
+
506
+ except Exception as e:
507
+ print(f"❌ Failed to generate instructions: {e}")
508
+ sys.exit(1)
509
+
510
+
511
+ @track_command_execution("generate")
512
+ def cmd_generate(args):
513
+ """Generate documentation, tests, and API specs from annotations."""
514
+ print("🚀 VooDocs Generate")
515
+ print("=" * 60)
516
+
517
+ # Load configuration
518
+ config = load_config()
519
+
520
+ # Use config values as defaults, CLI args override
521
+ output_dir_path = args.output_dir if hasattr(args, 'output_dir') and args.output_dir != './voodocs-output' else config.get('output_dir', './voodocs-output')
522
+ test_framework = args.test_framework if hasattr(args, 'test_framework') else config.get('test_framework', 'pytest')
523
+ api_format = args.api_format if hasattr(args, 'api_format') else config.get('api_format', 'openapi')
524
+
525
+ # Determine what to generate
526
+ generate_docs = not (args.tests_only or args.api_only)
527
+ generate_tests = not (args.docs_only or args.api_only)
528
+ generate_api = not (args.docs_only or args.tests_only)
529
+
530
+ if args.docs_only:
531
+ generate_docs = True
532
+ generate_tests = False
533
+ generate_api = False
534
+ elif args.tests_only:
535
+ generate_docs = False
536
+ generate_tests = True
537
+ generate_api = False
538
+ elif args.api_only:
539
+ generate_docs = False
540
+ generate_tests = False
541
+ generate_api = True
542
+
543
+ print(f"📁 Output directory: {output_dir_path}")
544
+ print(f"📄 Generate docs: {'✓' if generate_docs else '✗'}")
545
+ print(f"🧪 Generate tests: {'✓' if generate_tests else '✗'}")
546
+ print(f"🔌 Generate API specs: {'✓' if generate_api else '✗'}")
547
+ print()
548
+
549
+ # Create output directory
550
+ output_dir = Path(output_dir_path)
551
+ output_dir.mkdir(parents=True, exist_ok=True)
552
+
553
+ # Collect files to process
554
+ files_to_process = []
555
+ for path_str in args.paths:
556
+ path = Path(path_str)
557
+ if path.is_file():
558
+ files_to_process.append(path)
559
+ elif path.is_dir():
560
+ if args.recursive:
561
+ # Find all Python/TypeScript files
562
+ files_to_process.extend(path.rglob("*.py"))
563
+ files_to_process.extend(path.rglob("*.ts"))
564
+ files_to_process.extend(path.rglob("*.js"))
565
+ else:
566
+ files_to_process.extend(path.glob("*.py"))
567
+ files_to_process.extend(path.glob("*.ts"))
568
+ files_to_process.extend(path.glob("*.js"))
569
+
570
+ if not files_to_process:
571
+ print("❌ No files found to process")
572
+ sys.exit(1)
573
+
574
+ print(f"📋 Processing {len(files_to_process)} file(s)...")
575
+ print()
576
+
577
+ # Initialize generators
578
+ parser = AnnotationParser()
579
+ doc_generator = DocumentationGenerator() if generate_docs else None
580
+ test_generator = TestGenerator(framework=args.test_framework) if generate_tests else None
581
+ api_generator = APISpecGenerator(format=args.api_format) if generate_api else None
582
+
583
+ # Statistics
584
+ stats = {
585
+ "files_processed": 0,
586
+ "files_with_annotations": 0,
587
+ "total_functions": 0,
588
+ "total_classes": 0,
589
+ "docs_generated": 0,
590
+ "tests_generated": 0,
591
+ "api_specs_generated": 0
592
+ }
593
+
594
+ # Process each file
595
+ for file_path in files_to_process:
596
+ if args.verbose:
597
+ print(f"Processing: {file_path}")
598
+
599
+ try:
600
+ # Parse file
601
+ parsed = parser.parse_file(str(file_path))
602
+ stats["files_processed"] += 1
603
+
604
+ if not parsed.has_annotations():
605
+ if args.verbose:
606
+ print(f" ⚠ No annotations found")
607
+ continue
608
+
609
+ stats["files_with_annotations"] += 1
610
+ stats["total_functions"] += len(parsed.get_all_functions())
611
+ stats["total_classes"] += len(parsed.module.classes)
612
+
613
+ # Generate relative output path
614
+ rel_path = file_path.stem
615
+
616
+ # Generate documentation
617
+ if generate_docs:
618
+ doc_output = output_dir / "docs" / f"{rel_path}.md"
619
+ doc_output.parent.mkdir(parents=True, exist_ok=True)
620
+ doc_generator.generate(parsed, str(doc_output))
621
+ stats["docs_generated"] += 1
622
+ if args.verbose:
623
+ print(f" ✓ Documentation: {doc_output}")
624
+
625
+ # Generate tests
626
+ if generate_tests:
627
+ test_output = output_dir / "tests" / f"test_{rel_path}.py"
628
+ test_output.parent.mkdir(parents=True, exist_ok=True)
629
+ test_generator.generate(parsed, str(test_output))
630
+ stats["tests_generated"] += 1
631
+ if args.verbose:
632
+ print(f" ✓ Tests: {test_output}")
633
+
634
+ # Generate API spec
635
+ if generate_api:
636
+ api_ext = ".yaml" if args.api_format in ["openapi", "swagger"] else ".graphql"
637
+ api_output = output_dir / "api" / f"{rel_path}{api_ext}"
638
+ api_output.parent.mkdir(parents=True, exist_ok=True)
639
+ api_generator.generate(parsed, str(api_output))
640
+ stats["api_specs_generated"] += 1
641
+ if args.verbose:
642
+ print(f" ✓ API Spec: {api_output}")
643
+
644
+ except ParserNotBuiltError as e:
645
+ print(f"\n❌ {e}")
646
+ print(f"\nThe TypeScript parser requires building before use.")
647
+ print(f"Please run the following commands:")
648
+ print(f" cd {Path(__file__).parent / 'lib/darkarts/parsers/typescript'}")
649
+ print(f" npm install")
650
+ print(f" npm run build")
651
+ sys.exit(1)
652
+
653
+ except FileNotFoundError as e:
654
+ print(f" ❌ File not found: {file_path}")
655
+ if args.debug:
656
+ traceback.print_exc()
657
+ continue
658
+
659
+ except SyntaxError as e:
660
+ print(f" ❌ Syntax error in {file_path.name}: {e}")
661
+ if args.debug:
662
+ traceback.print_exc()
663
+ continue
664
+
665
+ except AnnotationError as e:
666
+ print(f" ❌ Annotation error in {file_path.name}: {e}")
667
+ if args.debug:
668
+ traceback.print_exc()
669
+ continue
670
+
671
+ except GeneratorError as e:
672
+ print(f" ❌ Generator error for {file_path.name}: {e}")
673
+ if args.debug:
674
+ traceback.print_exc()
675
+ continue
676
+
677
+ except PermissionError:
678
+ print(f" ❌ Permission denied: {file_path}")
679
+ print(f" Check file permissions and try again.")
680
+ continue
681
+
682
+ except Exception as e:
683
+ print(f" ❌ Unexpected error in {file_path.name}: {e}")
684
+ print(f" Run with --debug for full stack trace.")
685
+ if args.debug:
686
+ traceback.print_exc()
687
+ continue
688
+
689
+ # Print summary
690
+ print()
691
+ print("=" * 60)
692
+ print("📊 Generation Summary")
693
+ print("=" * 60)
694
+ print(f"Files processed: {stats['files_processed']}")
695
+ print(f"Files with annotations: {stats['files_with_annotations']}")
696
+ print(f"Total functions: {stats['total_functions']}")
697
+ print(f"Total classes: {stats['total_classes']}")
698
+ print()
699
+ if generate_docs:
700
+ print(f"✓ Documentation files: {stats['docs_generated']}")
701
+ if generate_tests:
702
+ print(f"✓ Test files: {stats['tests_generated']}")
703
+ if generate_api:
704
+ print(f"✓ API specs: {stats['api_specs_generated']}")
705
+ print()
706
+ print(f"✨ All outputs saved to: {output_dir}")
707
+
708
+
709
+ @track_command_execution("status")
710
+ def cmd_status(args):
711
+ """Show project status and statistics."""
712
+ print("📊 VooDocs Project Status")
713
+ print("=" * 60)
714
+
715
+ # Collect files
716
+ files = []
717
+ for path_str in args.paths:
718
+ path = Path(path_str)
719
+ if path.is_file():
720
+ files.append(path)
721
+ elif path.is_dir():
722
+ files.extend(path.rglob("*.py"))
723
+ files.extend(path.rglob("*.ts"))
724
+ files.extend(path.rglob("*.js"))
725
+
726
+ if not files:
727
+ print("❌ No files found")
728
+ sys.exit(1)
729
+
730
+ # Parse and analyze
731
+ parser = AnnotationParser()
732
+ stats = {
733
+ "total_files": len(files),
734
+ "files_with_annotations": 0,
735
+ "total_functions": 0,
736
+ "annotated_functions": 0,
737
+ "total_classes": 0,
738
+ "annotated_classes": 0,
739
+ "annotation_fields": {
740
+ "preconditions": 0,
741
+ "postconditions": 0,
742
+ "complexity": 0,
743
+ "error_cases": 0,
744
+ "security_implications": 0
745
+ }
746
+ }
747
+
748
+ for file_path in files:
749
+ try:
750
+ parsed = parser.parse_file(str(file_path))
751
+
752
+ if parsed.has_annotations():
753
+ stats["files_with_annotations"] += 1
754
+
755
+ all_functions = parsed.get_all_functions()
756
+ stats["total_functions"] += len(all_functions)
757
+
758
+ for func in all_functions:
759
+ if func.preconditions or func.postconditions:
760
+ stats["annotated_functions"] += 1
761
+ if func.preconditions:
762
+ stats["annotation_fields"]["preconditions"] += 1
763
+ if func.postconditions:
764
+ stats["annotation_fields"]["postconditions"] += 1
765
+ if func.complexity:
766
+ stats["annotation_fields"]["complexity"] += 1
767
+ if func.error_cases:
768
+ stats["annotation_fields"]["error_cases"] += 1
769
+ if func.security_implications:
770
+ stats["annotation_fields"]["security_implications"] += 1
771
+
772
+ stats["total_classes"] += len(parsed.module.classes)
773
+ for cls in parsed.module.classes:
774
+ if cls.class_invariants or cls.state_transitions:
775
+ stats["annotated_classes"] += 1
776
+
777
+ except Exception:
778
+ pass
779
+
780
+ # Calculate percentages
781
+ func_coverage = (stats["annotated_functions"] / stats["total_functions"] * 100) if stats["total_functions"] > 0 else 0
782
+ class_coverage = (stats["annotated_classes"] / stats["total_classes"] * 100) if stats["total_classes"] > 0 else 0
783
+ file_coverage = (stats["files_with_annotations"] / stats["total_files"] * 100) if stats["total_files"] > 0 else 0
784
+
785
+ # Calculate quality score (0-100)
786
+ quality_score = _calculate_quality_score(stats)
787
+ quality_grade = _get_quality_grade(quality_score)
788
+
789
+ # Print stats with visual indicators
790
+ print(f"Total files: {stats['total_files']}")
791
+ print(f"Files with annotations: {stats['files_with_annotations']} {_coverage_bar(file_coverage)}")
792
+ print()
793
+ print(f"Total functions: {stats['total_functions']}")
794
+ print(f"Annotated functions: {stats['annotated_functions']} {_coverage_bar(func_coverage)}")
795
+ print()
796
+ print(f"Total classes: {stats['total_classes']}")
797
+ print(f"Annotated classes: {stats['annotated_classes']} {_coverage_bar(class_coverage)}")
798
+ print()
799
+ print("Annotation Field Usage:")
800
+ for field, count in stats["annotation_fields"].items():
801
+ percentage = (count / stats['total_functions'] * 100) if stats['total_functions'] > 0 else 0
802
+ print(f" {field:25} {count:3} ({percentage:5.1f}%)")
803
+ print()
804
+
805
+ # Quality Score
806
+ print("📈 Documentation Quality")
807
+ print("=" * 60)
808
+ print(f"Overall Score: {quality_score:.1f}/100 {quality_grade}")
809
+ print(f"Coverage: {func_coverage:.1f}% {_coverage_bar(func_coverage)}")
810
+ print()
811
+
812
+ # Detailed breakdown
813
+ print("Quality Breakdown:")
814
+ print(f" Coverage Score: {_coverage_score(func_coverage):.1f}/40")
815
+ print(f" Completeness Score: {_completeness_score(stats):.1f}/30")
816
+ print(f" Richness Score: {_richness_score(stats):.1f}/30")
817
+ print()
818
+
819
+ # Recommendations
820
+ if quality_score < 50:
821
+ print("💡 Recommendations:")
822
+ print(" • Run 'voodocs instruct' to help your AI assistant add annotations")
823
+ print(" • Focus on adding preconditions and postconditions first")
824
+ print(" • Add complexity annotations to performance-critical functions")
825
+ elif quality_score < 70:
826
+ print("💡 Good progress! To improve further:")
827
+ print(" • Add error_cases to functions that can fail")
828
+ print(" • Document security_implications for sensitive functions")
829
+ print(" • Aim for 80%+ annotation coverage")
830
+ elif quality_score < 90:
831
+ print("🎉 Great work! Your documentation is solid.")
832
+ print(" • Consider adding more detailed error cases")
833
+ print(" • Document edge cases and security considerations")
834
+ else:
835
+ print("✨ Excellent! Your codebase is exceptionally well-documented with VooDocs.")
836
+
837
+
838
+ def _calculate_quality_score(stats):
839
+ """Calculate overall documentation quality score (0-100)."""
840
+ total_funcs = stats['total_functions']
841
+ if total_funcs == 0:
842
+ return 0
843
+
844
+ # Coverage score (40 points)
845
+ coverage = (stats['annotated_functions'] / total_funcs) * 40
846
+
847
+ # Completeness score (30 points) - how many have both pre and post
848
+ completeness = _completeness_score(stats)
849
+
850
+ # Richness score (30 points) - usage of advanced fields
851
+ richness = _richness_score(stats)
852
+
853
+ return min(100, coverage + completeness + richness)
854
+
855
+
856
+ def _coverage_score(coverage_pct):
857
+ """Calculate coverage score (0-40)."""
858
+ return min(40, coverage_pct * 0.4)
859
+
860
+
861
+ def _completeness_score(stats):
862
+ """Calculate completeness score (0-30)."""
863
+ total_funcs = stats['total_functions']
864
+ if total_funcs == 0:
865
+ return 0
866
+
867
+ # Functions with both preconditions and postconditions
868
+ pre = stats['annotation_fields']['preconditions']
869
+ post = stats['annotation_fields']['postconditions']
870
+ complete = min(pre, post)
871
+
872
+ return (complete / total_funcs) * 30
873
+
874
+
875
+ def _richness_score(stats):
876
+ """Calculate richness score (0-30)."""
877
+ total_funcs = stats['total_functions']
878
+ if total_funcs == 0:
879
+ return 0
880
+
881
+ # Advanced fields: complexity, error_cases, security_implications
882
+ complexity = stats['annotation_fields']['complexity']
883
+ errors = stats['annotation_fields']['error_cases']
884
+ security = stats['annotation_fields']['security_implications']
885
+
886
+ richness = (complexity + errors + security) / (total_funcs * 3) * 30
887
+ return min(30, richness)
888
+
889
+
890
+ def _get_quality_grade(score):
891
+ """Get letter grade for quality score."""
892
+ if score >= 90:
893
+ return "🏆 A+"
894
+ elif score >= 80:
895
+ return "🥇 A"
896
+ elif score >= 70:
897
+ return "🥈 B"
898
+ elif score >= 60:
899
+ return "🥉 C"
900
+ elif score >= 50:
901
+ return "📝 D"
902
+ else:
903
+ return "❌ F"
904
+
905
+
906
+ def _coverage_bar(percentage):
907
+ """Generate a visual coverage bar."""
908
+ filled = int(percentage / 5) # 20 blocks for 100%
909
+ empty = 20 - filled
910
+
911
+ bar = "█" * filled + "░" * empty
912
+
913
+ if percentage >= 80:
914
+ color = "🟢"
915
+ elif percentage >= 50:
916
+ color = "🟡"
917
+ else:
918
+ color = "🔴"
919
+
920
+ return f"{color} [{bar}] {percentage:.1f}%"
921
+
922
+
923
+ def load_config():
924
+ """Load VooDocs configuration."""
925
+ config_file = Path(".voodocs/config.json")
926
+
927
+ # Default configuration
928
+ default_config = {
929
+ "project_name": Path.cwd().name,
930
+ "language": "python",
931
+ "version": "0.1.0",
932
+ "output_dir": "./voodocs-output",
933
+ "test_framework": "pytest",
934
+ "api_format": "openapi",
935
+ "exclude_patterns": [
936
+ "**/test_*.py",
937
+ "**/node_modules/**",
938
+ "**/__pycache__/**",
939
+ "**/venv/**",
940
+ "**/.venv/**"
941
+ ],
942
+ "include_patterns": [],
943
+ "annotation_coverage_threshold": 50
944
+ }
945
+
946
+ if config_file.exists():
947
+ import json
948
+ try:
949
+ user_config = json.loads(config_file.read_text())
950
+ # Merge user config with defaults
951
+ default_config.update(user_config)
952
+ except json.JSONDecodeError:
953
+ print("⚠ Warning: Invalid config file, using defaults")
954
+
955
+ return default_config
956
+
957
+
958
+ @track_command_execution("watch")
959
+ def cmd_watch(args):
960
+ """Watch files and automatically regenerate documentation."""
961
+ import time
962
+ import hashlib
963
+
964
+ print("👀 VooDocs Watch Mode")
965
+ print("=" * 60)
966
+ print(f"📁 Watching: {', '.join(args.paths)}")
967
+ print(f"📁 Output directory: {args.output_dir}")
968
+ print(f"⏱️ Check interval: {args.interval}s")
969
+ print("\nPress Ctrl+C to stop watching...\n")
970
+
971
+ # Track file modification times and hashes
972
+ file_states = {}
973
+
974
+ def get_file_hash(filepath):
975
+ """Get MD5 hash of file content."""
976
+ try:
977
+ with open(filepath, 'rb') as f:
978
+ return hashlib.md5(f.read()).hexdigest()
979
+ except Exception:
980
+ return None
981
+
982
+ def collect_files(paths):
983
+ """Collect all relevant files from paths."""
984
+ files = []
985
+ for path_str in paths:
986
+ path = Path(path_str)
987
+ if path.is_file():
988
+ if path.suffix in ['.py', '.ts', '.js', '.tsx', '.jsx']:
989
+ files.append(path)
990
+ elif path.is_dir():
991
+ for ext in ['.py', '.ts', '.js', '.tsx', '.jsx']:
992
+ files.extend(path.rglob(f'*{ext}'))
993
+ return files
994
+
995
+ def regenerate(changed_files):
996
+ """Regenerate documentation for changed files."""
997
+ print(f"\n🔄 Changes detected in {len(changed_files)} file(s):")
998
+ for f in changed_files:
999
+ print(f" - {f}")
1000
+
1001
+ print("\n🚀 Regenerating...")
1002
+
1003
+ # Create a mock args object for cmd_generate
1004
+ class GenerateArgs:
1005
+ pass
1006
+
1007
+ gen_args = GenerateArgs()
1008
+ gen_args.paths = [str(f) for f in changed_files]
1009
+ gen_args.output_dir = args.output_dir
1010
+ gen_args.docs_only = args.docs_only
1011
+ gen_args.tests_only = args.tests_only
1012
+ gen_args.api_only = args.api_only
1013
+ gen_args.verbose = False
1014
+ gen_args.debug = False
1015
+
1016
+ try:
1017
+ cmd_generate(gen_args)
1018
+ print(f"✅ Regeneration complete at {time.strftime('%H:%M:%S')}\n")
1019
+ except Exception as e:
1020
+ print(f"❌ Error during regeneration: {e}\n")
1021
+
1022
+ try:
1023
+ # Initial scan
1024
+ all_files = collect_files(args.paths)
1025
+ print(f"📋 Watching {len(all_files)} file(s)...\n")
1026
+
1027
+ for filepath in all_files:
1028
+ file_states[str(filepath)] = get_file_hash(filepath)
1029
+
1030
+ # Watch loop
1031
+ while True:
1032
+ time.sleep(args.interval)
1033
+
1034
+ # Check for changes
1035
+ changed_files = []
1036
+ current_files = collect_files(args.paths)
1037
+
1038
+ for filepath in current_files:
1039
+ filepath_str = str(filepath)
1040
+ current_hash = get_file_hash(filepath)
1041
+
1042
+ # New file or modified file
1043
+ if filepath_str not in file_states or file_states[filepath_str] != current_hash:
1044
+ changed_files.append(filepath)
1045
+ file_states[filepath_str] = current_hash
1046
+
1047
+ # Check for deleted files
1048
+ current_file_strs = {str(f) for f in current_files}
1049
+ deleted_files = [f for f in file_states.keys() if f not in current_file_strs]
1050
+
1051
+ if deleted_files:
1052
+ print(f"\n🗑️ Deleted file(s):")
1053
+ for f in deleted_files:
1054
+ print(f" - {f}")
1055
+ del file_states[f]
1056
+ print()
1057
+
1058
+ # Regenerate if changes detected
1059
+ if changed_files:
1060
+ regenerate(changed_files)
1061
+
1062
+ except KeyboardInterrupt:
1063
+ print("\n\n👋 Watch mode stopped.")
1064
+ sys.exit(0)
1065
+ except Exception as e:
1066
+ print(f"\n❌ Watch mode error: {e}")
1067
+ if args.debug:
1068
+ traceback.print_exc()
1069
+ sys.exit(1)
1070
+
1071
+
1072
+ @track_command_execution("validate")
1073
+ def cmd_validate(args):
1074
+ """Validate annotation quality and correctness."""
1075
+ import json
1076
+
1077
+ print("🔍 VooDocs Validate")
1078
+ print("=" * 60)
1079
+
1080
+ # Collect files
1081
+ files = []
1082
+ for path_str in args.paths:
1083
+ path = Path(path_str)
1084
+ if path.is_file():
1085
+ files.append(path)
1086
+ elif path.is_dir():
1087
+ files.extend(path.rglob("*.py"))
1088
+ files.extend(path.rglob("*.ts"))
1089
+ files.extend(path.rglob("*.js"))
1090
+
1091
+ if not files:
1092
+ print("❌ No files found")
1093
+ sys.exit(1)
1094
+
1095
+ print(f"📋 Validating {len(files)} file(s)...\n")
1096
+
1097
+ # Validate each file
1098
+ parser = AnnotationParser()
1099
+ validator = AnnotationValidator()
1100
+ all_issues = []
1101
+ files_with_issues = 0
1102
+
1103
+ for file_path in files:
1104
+ try:
1105
+ parsed = parser.parse_file(str(file_path))
1106
+ issues = validator.validate(parsed)
1107
+
1108
+ if issues:
1109
+ files_with_issues += 1
1110
+ all_issues.extend(issues)
1111
+
1112
+ if args.format == "text":
1113
+ print(f"\n📄 {file_path}")
1114
+ print("-" * 60)
1115
+ for issue in issues:
1116
+ print(f" {issue}")
1117
+
1118
+ except Exception as e:
1119
+ if args.format == "text":
1120
+ print(f"\n📄 {file_path}")
1121
+ print(f" ❌ [ERROR] Failed to parse: {e}")
1122
+ all_issues.append({
1123
+ "severity": "error",
1124
+ "message": f"Failed to parse: {e}",
1125
+ "location": str(file_path)
1126
+ })
1127
+
1128
+ # Print summary
1129
+ if args.format == "text":
1130
+ print("\n" + "=" * 60)
1131
+ print("📊 Validation Summary")
1132
+ print("=" * 60)
1133
+
1134
+ summary = validator.get_summary()
1135
+ print(f"Files validated: {len(files)}")
1136
+ print(f"Files with issues: {files_with_issues}")
1137
+ print(f"Total issues: {summary['total']}")
1138
+ print(f" Errors: {summary['errors']}")
1139
+ print(f" Warnings: {summary['warnings']}")
1140
+ print(f" Info: {summary['info']}")
1141
+ print()
1142
+
1143
+ # Exit code
1144
+ if validator.has_errors():
1145
+ print("❌ Validation failed with errors")
1146
+ sys.exit(1)
1147
+ elif args.strict and summary['warnings'] > 0:
1148
+ print("❌ Validation failed (strict mode: warnings treated as errors)")
1149
+ sys.exit(1)
1150
+ elif summary['total'] > 0:
1151
+ print("⚠️ Validation passed with warnings")
1152
+ sys.exit(0)
1153
+ else:
1154
+ print("✅ Validation passed!")
1155
+ sys.exit(0)
1156
+
1157
+ else: # JSON format
1158
+ output = {
1159
+ "files_validated": len(files),
1160
+ "files_with_issues": files_with_issues,
1161
+ "summary": validator.get_summary(),
1162
+ "issues": [
1163
+ {
1164
+ "severity": issue.severity,
1165
+ "message": issue.message,
1166
+ "location": issue.location,
1167
+ "suggestion": issue.suggestion
1168
+ }
1169
+ for issue in all_issues
1170
+ ]
1171
+ }
1172
+ print(json.dumps(output, indent=2))
1173
+
1174
+ # Exit code
1175
+ if validator.has_errors():
1176
+ sys.exit(1)
1177
+ elif args.strict and validator.get_summary()['warnings'] > 0:
1178
+ sys.exit(1)
1179
+ else:
1180
+ sys.exit(0)
1181
+
1182
+
1183
+
1184
+
1185
+ @track_command_execution("check")
1186
+ def cmd_check(args):
1187
+ """Check annotation coverage and quality (for CI/CD)."""
1188
+ print("🔍 VooDocs Check (CI/CD Mode)")
1189
+ print("=" * 60)
1190
+
1191
+ # Collect files
1192
+ files = []
1193
+ for path_str in args.paths:
1194
+ path = Path(path_str)
1195
+ if path.is_file():
1196
+ files.append(path)
1197
+ elif path.is_dir():
1198
+ files.extend(path.rglob("*.py"))
1199
+ files.extend(path.rglob("*.ts"))
1200
+ files.extend(path.rglob("*.js"))
1201
+
1202
+ if not files:
1203
+ print("❌ No files found")
1204
+ sys.exit(1)
1205
+
1206
+ print(f"📋 Checking {len(files)} file(s)...\n")
1207
+
1208
+ # Parse all files and calculate metrics
1209
+ parser = AnnotationParser()
1210
+ validator = AnnotationValidator()
1211
+
1212
+ total_functions = 0
1213
+ annotated_functions = 0
1214
+ total_files = len(files)
1215
+ files_with_annotations = 0
1216
+ all_issues = []
1217
+
1218
+ for file_path in files:
1219
+ try:
1220
+ parsed = parser.parse_file(str(file_path))
1221
+ functions = parsed.get_all_functions()
1222
+ total_functions += len(functions)
1223
+
1224
+ # Count annotated functions
1225
+ for func in functions:
1226
+ if func.preconditions or func.postconditions:
1227
+ annotated_functions += 1
1228
+
1229
+ # Check if file has any annotations
1230
+ if parsed.module.module_purpose or functions:
1231
+ files_with_annotations += 1
1232
+
1233
+ # Validate
1234
+ issues = validator.validate(parsed)
1235
+ all_issues.extend(issues)
1236
+
1237
+ except Exception as e:
1238
+ print(f"⚠️ Failed to parse {file_path}: {e}")
1239
+
1240
+ # Calculate coverage
1241
+ function_coverage = (annotated_functions / total_functions * 100) if total_functions > 0 else 0
1242
+ file_coverage = (files_with_annotations / total_files * 100) if total_files > 0 else 0
1243
+
1244
+ # Calculate quality score (same as status command)
1245
+ summary = validator.get_summary()
1246
+ coverage_score = function_coverage * 0.4 # 40 points
1247
+ completeness_score = max(0, 30 - summary['warnings'] * 2) # 30 points, -2 per warning
1248
+ richness_score = max(0, 30 - summary['info']) # 30 points, -1 per info
1249
+ quality_score = coverage_score + completeness_score + richness_score
1250
+
1251
+ # Print results
1252
+ print("📊 Results")
1253
+ print("=" * 60)
1254
+ print(f"Files checked: {total_files}")
1255
+ print(f"Files with annotations: {files_with_annotations} ({file_coverage:.1f}%)")
1256
+ print(f"Functions checked: {total_functions}")
1257
+ print(f"Annotated functions: {annotated_functions} ({function_coverage:.1f}%)")
1258
+ print(f"Quality score: {quality_score:.1f}/100")
1259
+ print()
1260
+ print(f"Validation issues:")
1261
+ print(f" Errors: {summary['errors']}")
1262
+ print(f" Warnings: {summary['warnings']}")
1263
+ print(f" Info: {summary['info']}")
1264
+ print()
1265
+
1266
+ # Check thresholds
1267
+ failed = False
1268
+
1269
+ if function_coverage < args.min_coverage:
1270
+ print(f"❌ Coverage check failed: {function_coverage:.1f}% < {args.min_coverage}%")
1271
+ failed = True
1272
+ else:
1273
+ print(f"✅ Coverage check passed: {function_coverage:.1f}% >= {args.min_coverage}%")
1274
+
1275
+ if quality_score < args.min_quality:
1276
+ print(f"❌ Quality check failed: {quality_score:.1f} < {args.min_quality}")
1277
+ failed = True
1278
+ else:
1279
+ print(f"✅ Quality check passed: {quality_score:.1f} >= {args.min_quality}")
1280
+
1281
+ if validator.has_errors():
1282
+ print(f"❌ Validation failed: {summary['errors']} error(s) found")
1283
+ failed = True
1284
+ else:
1285
+ print(f"✅ Validation passed: No errors")
1286
+
1287
+ print()
1288
+
1289
+ if failed:
1290
+ print("❌ Check failed")
1291
+ sys.exit(1)
1292
+ else:
1293
+ print("✅ All checks passed!")
1294
+ sys.exit(0)
1295
+
1296
+
1297
+
1298
+ @track_command_execution("export")
1299
+ def cmd_export(args):
1300
+ """Export documentation to different formats."""
1301
+ print(f"📤 Exporting to {args.format.upper()}...")
1302
+
1303
+ try:
1304
+ if args.format == "html":
1305
+ exporter = HTMLExporter()
1306
+ output_file = exporter.export(args.input_file, args.output)
1307
+ print(f"✅ HTML exported to: {output_file}")
1308
+
1309
+ elif args.format == "pdf":
1310
+ exporter = PDFExporter()
1311
+ output_file = exporter.export(args.input_file, args.output)
1312
+ print(f"✅ PDF exported to: {output_file}")
1313
+
1314
+ except FileNotFoundError as e:
1315
+ print(f"❌ Error: {e}")
1316
+ sys.exit(1)
1317
+
1318
+ except ImportError as e:
1319
+ print(f"❌ Error: {e}")
1320
+ sys.exit(1)
1321
+
1322
+ except Exception as e:
1323
+ print(f"❌ Export failed: {e}")
1324
+ sys.exit(1)
1325
+
1326
+
1327
+ def cmd_telemetry(args):
1328
+ """Manage telemetry settings."""
1329
+ client = get_telemetry_client()
1330
+
1331
+ if args.action == "enable":
1332
+ client.enable()
1333
+ elif args.action == "disable":
1334
+ client.disable()
1335
+ elif args.action == "status":
1336
+ client.status()
1337
+
1338
+
1339
+ if __name__ == "__main__":
1340
+ main()