wikigen 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
wikigen/config.py ADDED
@@ -0,0 +1,526 @@
1
+ """
2
+ Configuration management for WikiGen.
3
+ Handles loading, saving, and merging configuration with CLI arguments.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import sys
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Dict, Any, Optional
12
+
13
+ try:
14
+ import keyring
15
+
16
+ KEYRING_AVAILABLE = True
17
+ except ImportError:
18
+ KEYRING_AVAILABLE = False
19
+
20
+ from .defaults import DEFAULT_CONFIG
21
+
22
+
23
+ def _get_platform_config_base() -> Path:
24
+ """
25
+ Return the OS-appropriate user config base directory.
26
+
27
+ - macOS: ~/Library/Application Support
28
+ - Windows: %APPDATA%
29
+ - Linux/other: $XDG_CONFIG_HOME or ~/.config
30
+ """
31
+ home = Path.home()
32
+ if sys.platform.startswith("win"):
33
+ appdata = os.environ.get("APPDATA")
34
+ return Path(appdata) if appdata else home / "AppData" / "Roaming"
35
+ elif sys.platform == "darwin":
36
+ # Follow XDG-style on macOS per project preference
37
+ xdg = os.environ.get("XDG_CONFIG_HOME")
38
+ return Path(xdg) if xdg else home / ".config"
39
+ else:
40
+ xdg = os.environ.get("XDG_CONFIG_HOME")
41
+ return Path(xdg) if xdg else home / ".config"
42
+
43
+
44
+ def _get_new_config_dir() -> Path:
45
+ """Return the new config directory for wikigen under the platform base."""
46
+ return _get_platform_config_base() / "wikigen"
47
+
48
+
49
+ def _get_legacy_config_dir() -> Path:
50
+ """Return the previous Documents-based config directory (for migration)."""
51
+ return Path.home() / "Documents" / "WikiGen" / ".salt"
52
+
53
+
54
+ # Configuration paths
55
+ CONFIG_DIR = _get_new_config_dir()
56
+ CONFIG_FILE = CONFIG_DIR / "config.json"
57
+ DEFAULT_OUTPUT_DIR = Path.home() / "Documents" / "WikiGen"
58
+
59
+
60
+ def _migrate_legacy_config_if_needed() -> None:
61
+ """
62
+ If a legacy config exists in the old Documents path and the new config
63
+ doesn't exist yet, migrate the file and directory.
64
+ """
65
+ legacy_dir = _get_legacy_config_dir()
66
+ legacy_file = legacy_dir / "config.json"
67
+ if CONFIG_FILE.exists():
68
+ return
69
+ if legacy_file.exists():
70
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
71
+ try:
72
+ # Copy then remove legacy to be safe
73
+ with open(legacy_file, "r", encoding="utf-8") as f:
74
+ data = f.read()
75
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
76
+ f.write(data)
77
+ # best-effort cleanup of empty legacy dir
78
+ try:
79
+ legacy_file.unlink(missing_ok=True)
80
+ except Exception:
81
+ pass
82
+ try:
83
+ # remove legacy dir if empty
84
+ legacy_dir.rmdir()
85
+ except Exception:
86
+ pass
87
+ except Exception:
88
+ # Ignore migration errors — user can re-init
89
+ pass
90
+
91
+
92
+ def init_config() -> None:
93
+ """Interactive setup wizard for init command."""
94
+ import getpass
95
+
96
+ from .formatter.init_formatter import (
97
+ print_init_header,
98
+ print_section_start,
99
+ print_input_prompt,
100
+ print_init_complete,
101
+ )
102
+ from .formatter.output_formatter import Colors, Icons, Tree
103
+ from .utils.llm_providers import (
104
+ get_provider_list,
105
+ get_display_name,
106
+ get_recommended_models,
107
+ get_provider_info,
108
+ requires_api_key,
109
+ )
110
+
111
+ # Create directories
112
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
113
+ DEFAULT_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
114
+
115
+ print_init_header()
116
+
117
+ # LLM Provider Selection section
118
+ print_section_start("LLM Provider", Icons.INFO)
119
+
120
+ # Show provider list
121
+ providers = get_provider_list()
122
+ print(
123
+ f"{Colors.LIGHT_GRAY}{Tree.VERTICAL} {Colors.LIGHT_GRAY}{Tree.MIDDLE} "
124
+ f"{Colors.MEDIUM_GRAY}Available providers:{Colors.RESET}"
125
+ )
126
+ for i, provider_id in enumerate(providers, 1):
127
+ display_name = get_display_name(provider_id)
128
+ print(
129
+ f"{Colors.LIGHT_GRAY}{Tree.VERTICAL} {Colors.LIGHT_GRAY}{Tree.VERTICAL} "
130
+ f"{Colors.MEDIUM_GRAY}{i}) {display_name}{Colors.RESET}"
131
+ )
132
+
133
+ # Provider selection
134
+ print_input_prompt(
135
+ "Select LLM provider (enter number)", Icons.ANALYZING, is_required=True
136
+ )
137
+ provider_choice = input().strip()
138
+
139
+ try:
140
+ provider_index = int(provider_choice) - 1
141
+ if provider_index < 0 or provider_index >= len(providers):
142
+ print(f"✘ Invalid provider selection: {provider_choice}")
143
+ sys.exit(1)
144
+ llm_provider = providers[provider_index]
145
+ except ValueError:
146
+ print(f"✘ Invalid provider selection: {provider_choice}")
147
+ sys.exit(1)
148
+
149
+ provider_info = get_provider_info(llm_provider)
150
+ provider_display = get_display_name(llm_provider)
151
+
152
+ # Model Selection
153
+ print_section_start("Model Selection", Icons.INFO)
154
+
155
+ # Show recommended models
156
+ recommended_models = get_recommended_models(llm_provider)
157
+ print(
158
+ f"{Colors.LIGHT_GRAY}{Tree.VERTICAL} {Colors.LIGHT_GRAY}{Tree.MIDDLE} "
159
+ f"{Colors.MEDIUM_GRAY}Recommended models for {provider_display}:{Colors.RESET}"
160
+ )
161
+ for i, model in enumerate(recommended_models, 1):
162
+ print(
163
+ f"{Colors.LIGHT_GRAY}{Tree.VERTICAL} {Colors.LIGHT_GRAY}{Tree.VERTICAL} "
164
+ f"{Colors.MEDIUM_GRAY}{i}) {model}{Colors.RESET}"
165
+ )
166
+ print(
167
+ f"{Colors.LIGHT_GRAY}{Tree.VERTICAL} {Colors.LIGHT_GRAY}{Tree.VERTICAL} "
168
+ f"{Colors.MEDIUM_GRAY}{len(recommended_models) + 1}) Enter custom model name{Colors.RESET}"
169
+ )
170
+
171
+ print_input_prompt(
172
+ f"Select model for {provider_display} (enter number or custom name)",
173
+ Icons.ANALYZING,
174
+ is_required=True,
175
+ )
176
+ model_choice = input().strip()
177
+
178
+ # Parse model selection
179
+ try:
180
+ model_index = int(model_choice) - 1
181
+ if model_index == len(recommended_models):
182
+ # Custom model
183
+ print_input_prompt(
184
+ "Enter custom model name", Icons.CONFIG, is_required=True
185
+ )
186
+ llm_model = input().strip()
187
+ if not llm_model:
188
+ print("✘ Model name cannot be empty!")
189
+ sys.exit(1)
190
+ elif 0 <= model_index < len(recommended_models):
191
+ llm_model = recommended_models[model_index]
192
+ else:
193
+ print(f"✘ Invalid model selection: {model_choice}")
194
+ sys.exit(1)
195
+ except ValueError:
196
+ # Custom model name entered directly
197
+ llm_model = model_choice
198
+
199
+ # API Keys section
200
+ print_section_start("API Keys", Icons.INFO)
201
+
202
+ # Get API key if required
203
+ api_key = None
204
+ custom_url = None
205
+ if requires_api_key(llm_provider):
206
+ env_var = provider_info.get("api_key_env")
207
+ key_name = env_var or f"{provider_display} API Key"
208
+
209
+ print_input_prompt(key_name, Icons.ANALYZING, is_required=True)
210
+ api_key = getpass.getpass().strip()
211
+ if not api_key:
212
+ print(f"✘ {key_name} is required!")
213
+ sys.exit(1)
214
+ else:
215
+ # Ollama - just show base URL
216
+ base_url = provider_info.get("base_url", "http://localhost:11434")
217
+ print_input_prompt(
218
+ "Ollama Base URL", Icons.CONFIG, is_required=False, default_value=base_url
219
+ )
220
+ custom_url = input().strip()
221
+ # For Ollama, use default if empty
222
+ if not custom_url:
223
+ custom_url = base_url
224
+
225
+ # GitHub Token
226
+ print_input_prompt(
227
+ "GitHub Token", Icons.ANALYZING, is_required=False, default_value="skip"
228
+ )
229
+ github_token = input().strip()
230
+
231
+ # Store in keyring if available, otherwise save to config
232
+ keyring_available = KEYRING_AVAILABLE
233
+ if keyring_available:
234
+ try:
235
+ if api_key:
236
+ keyring.set_password("wikigen", provider_info["keyring_key"], api_key)
237
+ if github_token:
238
+ keyring.set_password("wikigen", "github_token", github_token)
239
+ except (OSError, RuntimeError, AttributeError):
240
+ keyring_available = False
241
+
242
+ # Preferences section
243
+ print_section_start("Preferences", Icons.INFO)
244
+
245
+ # Output Directory
246
+ print_input_prompt(
247
+ "Output Directory",
248
+ Icons.CONFIG,
249
+ is_required=False,
250
+ default_value=str(DEFAULT_OUTPUT_DIR),
251
+ )
252
+ output_dir = input().strip()
253
+ if not output_dir:
254
+ output_dir = str(DEFAULT_OUTPUT_DIR)
255
+
256
+ # Language
257
+ print_input_prompt(
258
+ "Language", Icons.CONFIG, is_required=False, default_value="english"
259
+ )
260
+ language = input().strip()
261
+ if not language:
262
+ language = "english"
263
+
264
+ # Max Abstractions
265
+ print_input_prompt(
266
+ "Max Abstractions", Icons.CONFIG, is_required=False, default_value="5"
267
+ )
268
+ max_abstractions_input = input().strip()
269
+ if not max_abstractions_input:
270
+ max_abstractions = 5
271
+ else:
272
+ try:
273
+ max_abstractions = int(max_abstractions_input)
274
+ except ValueError:
275
+ max_abstractions = 5
276
+
277
+ # Documentation Mode
278
+ print_input_prompt(
279
+ "Documentation Mode (minimal/comprehensive)",
280
+ Icons.CONFIG,
281
+ is_required=False,
282
+ default_value="minimal",
283
+ )
284
+ documentation_mode_input = input().strip().lower()
285
+ if not documentation_mode_input:
286
+ documentation_mode = "minimal"
287
+ elif documentation_mode_input in ["minimal", "comprehensive"]:
288
+ documentation_mode = documentation_mode_input
289
+ else:
290
+ print(f"⚠ Warning: Invalid mode '{documentation_mode_input}'. Using 'minimal'.")
291
+ documentation_mode = "minimal"
292
+
293
+ # Build configuration
294
+ config = {
295
+ "llm_provider": llm_provider,
296
+ "llm_model": llm_model,
297
+ "output_dir": output_dir,
298
+ "language": language,
299
+ "max_abstractions": max_abstractions,
300
+ "max_file_size": DEFAULT_CONFIG["max_file_size"],
301
+ "use_cache": DEFAULT_CONFIG["use_cache"],
302
+ "include_patterns": DEFAULT_CONFIG["include_patterns"],
303
+ "exclude_patterns": DEFAULT_CONFIG["exclude_patterns"],
304
+ "documentation_mode": documentation_mode,
305
+ }
306
+
307
+ # Store Ollama base URL if custom
308
+ if llm_provider == "ollama" and custom_url and custom_url != base_url:
309
+ config["ollama_base_url"] = custom_url
310
+
311
+ # Add API keys to config if keyring not available
312
+ if not keyring_available:
313
+ if api_key:
314
+ config[provider_info["keyring_key"]] = api_key
315
+ if github_token:
316
+ config["github_token"] = github_token
317
+
318
+ # Save configuration
319
+ save_config(config)
320
+
321
+ # Print completion message
322
+ print_init_complete(CONFIG_FILE, output_dir, keyring_available)
323
+
324
+
325
+ def load_config() -> Dict[str, Any]:
326
+ """Load configuration from file and keyring."""
327
+ config = DEFAULT_CONFIG.copy()
328
+
329
+ # Attempt migration from legacy location
330
+ _migrate_legacy_config_if_needed()
331
+
332
+ # Load from file if it exists
333
+ if CONFIG_FILE.exists():
334
+ try:
335
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
336
+ file_config = json.load(f)
337
+ config.update(file_config)
338
+ except (json.JSONDecodeError, IOError) as e:
339
+ print(f"⚠ Warning: Could not load config file: {e}")
340
+
341
+ # Load API keys from keyring if available
342
+ # Load all provider API keys dynamically
343
+ if KEYRING_AVAILABLE:
344
+ try:
345
+ from .utils.llm_providers import LLM_PROVIDERS
346
+
347
+ for provider_id, provider_info in LLM_PROVIDERS.items():
348
+ keyring_key = provider_info.get("keyring_key")
349
+ if keyring_key:
350
+ api_key = keyring.get_password("wikigen", keyring_key)
351
+ if api_key:
352
+ config[keyring_key] = api_key
353
+
354
+ github_token = keyring.get_password("wikigen", "github_token")
355
+ if github_token:
356
+ config["github_token"] = github_token
357
+ except (OSError, RuntimeError, AttributeError) as e:
358
+ print(f"⚠ Warning: Could not load from keyring: {e}")
359
+
360
+ return config
361
+
362
+
363
+ def save_config(config: Dict[str, Any]) -> None:
364
+ """Save configuration to file."""
365
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
366
+
367
+ # Don't save API keys to file if keyring is available
368
+ config_to_save = config.copy()
369
+ if KEYRING_AVAILABLE:
370
+ # Remove all provider API keys from config file
371
+ from .utils.llm_providers import LLM_PROVIDERS
372
+
373
+ for provider_info in LLM_PROVIDERS.values():
374
+ keyring_key = provider_info.get("keyring_key")
375
+ if keyring_key:
376
+ config_to_save.pop(keyring_key, None)
377
+ config_to_save.pop("github_token", None)
378
+
379
+ try:
380
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
381
+ json.dump(config_to_save, f, indent=2)
382
+ except IOError as e:
383
+ print(f"✘ Error saving config: {e}")
384
+ sys.exit(1)
385
+
386
+
387
+ def merge_config_with_args(config: Dict[str, Any], args) -> Dict[str, Any]:
388
+ """Merge configuration with CLI arguments (CLI takes precedence)."""
389
+ merged = config.copy()
390
+
391
+ # Map argparse attributes to config keys
392
+ arg_mapping = {
393
+ "output": "output_dir",
394
+ "language": "language",
395
+ "max_abstractions": "max_abstractions",
396
+ "max_size": "max_file_size",
397
+ "no_cache": "use_cache", # Note: inverted logic
398
+ "include": "include_patterns",
399
+ "exclude": "exclude_patterns",
400
+ "token": "github_token",
401
+ "mode": "documentation_mode",
402
+ }
403
+
404
+ for arg_name, config_key in arg_mapping.items():
405
+ if hasattr(args, arg_name):
406
+ value = getattr(args, arg_name)
407
+ if arg_name == "no_cache":
408
+ # For store_true flags, only override if explicitly provided (True)
409
+ # Invert the logic: no_cache=True means use_cache=False
410
+ if value is True:
411
+ merged[config_key] = False
412
+ elif value is not None:
413
+ if arg_name in ["include", "exclude"]:
414
+ # Convert to list if it's a set
415
+ if isinstance(value, set):
416
+ merged[config_key] = list(value)
417
+ else:
418
+ merged[config_key] = value
419
+ else:
420
+ merged[config_key] = value
421
+
422
+ return merged
423
+
424
+
425
+ def check_config_exists() -> bool:
426
+ """Check if configuration file exists."""
427
+ return CONFIG_FILE.exists()
428
+
429
+
430
+ def get_llm_provider() -> str:
431
+ """Get LLM provider from config, defaulting to gemini."""
432
+ config = load_config()
433
+ return config.get("llm_provider", "gemini")
434
+
435
+
436
+ def get_llm_model() -> str:
437
+ """Get LLM model from config, defaulting to gemini-2.5-flash."""
438
+ config = load_config()
439
+ return config.get("llm_model", "gemini-2.5-flash")
440
+
441
+
442
+ def get_api_key() -> Optional[str]:
443
+ """Get API key from config or environment based on current provider."""
444
+ from .utils.llm_providers import get_provider_info, requires_api_key
445
+
446
+ config = load_config()
447
+ provider = get_llm_provider()
448
+
449
+ # Check if provider requires API key
450
+ if not requires_api_key(provider):
451
+ return None
452
+
453
+ provider_info = get_provider_info(provider)
454
+ keyring_key = provider_info.get("keyring_key")
455
+ env_var = provider_info.get("api_key_env")
456
+
457
+ # Try keyring first, then env var
458
+ api_key = None
459
+ if keyring_key and KEYRING_AVAILABLE:
460
+ try:
461
+ api_key = keyring.get_password("wikigen", keyring_key)
462
+ except (OSError, RuntimeError, AttributeError):
463
+ pass
464
+
465
+ if not api_key:
466
+ # Try config file fallback
467
+ api_key = config.get(keyring_key or "")
468
+
469
+ if not api_key and env_var:
470
+ # Fallback to environment variable
471
+ api_key = os.getenv(env_var)
472
+
473
+ return api_key
474
+
475
+
476
+ def get_github_token() -> Optional[str]:
477
+ """Get GitHub token from config or environment."""
478
+ config = load_config()
479
+ return config.get("github_token") or os.getenv("GITHUB_TOKEN")
480
+
481
+
482
+ def should_check_for_updates() -> bool:
483
+ """
484
+ Check if 24 hours have passed since last update check.
485
+
486
+ Returns:
487
+ True if update check should be performed, False otherwise
488
+ """
489
+ config = load_config()
490
+ last_check = config.get("last_update_check")
491
+
492
+ # If never checked, return True
493
+ if last_check is None:
494
+ return True
495
+
496
+ # Check if 24 hours (86400 seconds) have passed
497
+ current_time = time.time()
498
+ time_since_last_check = current_time - last_check
499
+
500
+ return time_since_last_check >= 86400
501
+
502
+
503
+ def update_last_check_timestamp() -> None:
504
+ """Update the last update check timestamp to current time."""
505
+ config = load_config()
506
+ config["last_update_check"] = time.time()
507
+ save_config(config)
508
+
509
+
510
+ def get_output_dir() -> Path:
511
+ """
512
+ Get the output directory from config or use default.
513
+
514
+ Returns:
515
+ Path to the output directory
516
+ """
517
+ try:
518
+ config = load_config()
519
+ output_dir_str = config.get("output_dir")
520
+ if output_dir_str:
521
+ return Path(output_dir_str).expanduser()
522
+ except Exception:
523
+ # If config loading fails, fallback to default
524
+ pass
525
+
526
+ return DEFAULT_OUTPUT_DIR
wikigen/defaults.py ADDED
@@ -0,0 +1,78 @@
1
+ """
2
+ Default configuration values for WikiGen.
3
+ """
4
+
5
+ # Default file patterns for inclusion
6
+ DEFAULT_INCLUDE_PATTERNS = {
7
+ "*.py",
8
+ "*.js",
9
+ "*.jsx",
10
+ "*.ts",
11
+ "*.tsx",
12
+ "*.go",
13
+ "*.java",
14
+ "*.pyi",
15
+ "*.pyx",
16
+ "*.c",
17
+ "*.cc",
18
+ "*.cpp",
19
+ "*.h",
20
+ "*.md",
21
+ "*.rst",
22
+ "*Dockerfile",
23
+ "*Makefile",
24
+ "*.yaml",
25
+ "*.yml",
26
+ }
27
+
28
+ # Default file patterns for exclusion
29
+ DEFAULT_EXCLUDE_PATTERNS = {
30
+ "assets/*",
31
+ "data/*",
32
+ "images/*",
33
+ "public/*",
34
+ "static/*",
35
+ "temp/*",
36
+ "*docs/*",
37
+ "*venv/*",
38
+ "*.venv/*",
39
+ "*test*",
40
+ "*tests/*",
41
+ "*examples/*",
42
+ "v1/*",
43
+ "*dist/*",
44
+ "*build/*",
45
+ "*experimental/*",
46
+ "*deprecated/*",
47
+ "*misc/*",
48
+ "*legacy/*",
49
+ ".git/*",
50
+ ".github/*",
51
+ ".next/*",
52
+ ".vscode/*",
53
+ "*obj/*",
54
+ "*bin/*",
55
+ "*node_modules/*",
56
+ "*.log",
57
+ }
58
+
59
+ # Default configuration values
60
+ DEFAULT_CONFIG = {
61
+ "output_dir": "~/Documents/WikiGen",
62
+ "language": "english",
63
+ "max_abstractions": 10,
64
+ "max_file_size": 100000,
65
+ "use_cache": True,
66
+ "include_patterns": list(DEFAULT_INCLUDE_PATTERNS),
67
+ "exclude_patterns": list(DEFAULT_EXCLUDE_PATTERNS),
68
+ "last_update_check": None, # Timestamp of last update check (None means never checked)
69
+ "llm_provider": "gemini",
70
+ "llm_model": "gemini-2.5-flash",
71
+ "documentation_mode": "minimal",
72
+ # Semantic search configuration
73
+ "semantic_search_enabled": True,
74
+ "chunk_size": 1000, # tokens (increased for better context)
75
+ "chunk_overlap": 200, # tokens (reduced overlap to avoid tiny fragments)
76
+ "embedding_model": "all-MiniLM-L6-v2", # lightweight, fast
77
+ "max_chunks_per_file": 5, # limit chunks returned per file
78
+ }
@@ -0,0 +1 @@
1
+ """Flows module for WikiGen."""
wikigen/flows/flow.py ADDED
@@ -0,0 +1,38 @@
1
+ from pocketflow import Flow
2
+
3
+ # Import all node classes from nodes.py
4
+ from wikigen.nodes.nodes import (
5
+ FetchRepo,
6
+ IdentifyAbstractions,
7
+ AnalyzeRelationships,
8
+ OrderComponents,
9
+ WriteComponents,
10
+ GenerateDocContent,
11
+ WriteDocFiles,
12
+ )
13
+
14
+
15
+ def create_wiki_flow():
16
+ """Creates and returns the codebase wiki generation flow."""
17
+
18
+ # Instantiate nodes
19
+ fetch_repo = FetchRepo()
20
+ identify_abstractions = IdentifyAbstractions(max_retries=5, wait=20)
21
+ analyze_relationships = AnalyzeRelationships(max_retries=5, wait=20)
22
+ order_components = OrderComponents(max_retries=5, wait=20)
23
+ write_components = WriteComponents(max_retries=5, wait=20) # This is a BatchNode
24
+ generate_doc_content = GenerateDocContent()
25
+ write_doc_files = WriteDocFiles()
26
+
27
+ # Connect nodes in sequence based on the design
28
+ fetch_repo >> identify_abstractions
29
+ identify_abstractions >> analyze_relationships
30
+ analyze_relationships >> order_components
31
+ order_components >> write_components
32
+ write_components >> generate_doc_content
33
+ generate_doc_content >> write_doc_files
34
+
35
+ # Create the flow starting with FetchRepo
36
+ wiki_flow = Flow(start=fetch_repo)
37
+
38
+ return wiki_flow