@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.
- package/LICENSE +37 -0
- package/README.md +153 -0
- package/USAGE.md +314 -0
- package/cli.py +1340 -0
- package/examples/.cursorrules +437 -0
- package/examples/instructions/.claude/instructions.md +372 -0
- package/examples/instructions/.cursorrules +437 -0
- package/examples/instructions/.windsurfrules +437 -0
- package/examples/instructions/VOODOCS_INSTRUCTIONS.md +437 -0
- package/examples/math_example.py +41 -0
- package/examples/phase2_test.py +24 -0
- package/examples/test_compound_conditions.py +40 -0
- package/examples/test_math_example.py +186 -0
- package/lib/darkarts/README.md +115 -0
- package/lib/darkarts/__init__.py +16 -0
- package/lib/darkarts/annotations/__init__.py +34 -0
- package/lib/darkarts/annotations/parser.py +618 -0
- package/lib/darkarts/annotations/types.py +181 -0
- package/lib/darkarts/cli.py +128 -0
- package/lib/darkarts/core/__init__.py +32 -0
- package/lib/darkarts/core/interface.py +256 -0
- package/lib/darkarts/core/loader.py +231 -0
- package/lib/darkarts/core/plugin.py +215 -0
- package/lib/darkarts/core/registry.py +146 -0
- package/lib/darkarts/exceptions.py +51 -0
- package/lib/darkarts/parsers/typescript/dist/cli.d.ts +9 -0
- package/lib/darkarts/parsers/typescript/dist/cli.d.ts.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/cli.js +69 -0
- package/lib/darkarts/parsers/typescript/dist/cli.js.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/parser.d.ts +111 -0
- package/lib/darkarts/parsers/typescript/dist/parser.d.ts.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/parser.js +365 -0
- package/lib/darkarts/parsers/typescript/dist/parser.js.map +1 -0
- package/lib/darkarts/parsers/typescript/package-lock.json +51 -0
- package/lib/darkarts/parsers/typescript/package.json +19 -0
- package/lib/darkarts/parsers/typescript/src/cli.ts +41 -0
- package/lib/darkarts/parsers/typescript/src/parser.ts +408 -0
- package/lib/darkarts/parsers/typescript/tsconfig.json +19 -0
- package/lib/darkarts/plugins/voodocs/__init__.py +379 -0
- package/lib/darkarts/plugins/voodocs/ai_native_plugin.py +151 -0
- package/lib/darkarts/plugins/voodocs/annotation_validator.py +280 -0
- package/lib/darkarts/plugins/voodocs/api_spec_generator.py +486 -0
- package/lib/darkarts/plugins/voodocs/documentation_generator.py +610 -0
- package/lib/darkarts/plugins/voodocs/html_exporter.py +260 -0
- package/lib/darkarts/plugins/voodocs/instruction_generator.py +706 -0
- package/lib/darkarts/plugins/voodocs/pdf_exporter.py +66 -0
- package/lib/darkarts/plugins/voodocs/test_generator.py +636 -0
- package/package.json +70 -0
- package/requirements.txt +13 -0
- package/templates/ci/github-actions.yml +73 -0
- package/templates/ci/gitlab-ci.yml +35 -0
- 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()
|