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/__init__.py +7 -0
- wikigen/cli.py +690 -0
- wikigen/config.py +526 -0
- wikigen/defaults.py +78 -0
- wikigen/flows/__init__.py +1 -0
- wikigen/flows/flow.py +38 -0
- wikigen/formatter/help_formatter.py +194 -0
- wikigen/formatter/init_formatter.py +56 -0
- wikigen/formatter/output_formatter.py +290 -0
- wikigen/mcp/__init__.py +12 -0
- wikigen/mcp/chunking.py +127 -0
- wikigen/mcp/embeddings.py +69 -0
- wikigen/mcp/output_resources.py +65 -0
- wikigen/mcp/search_index.py +826 -0
- wikigen/mcp/server.py +232 -0
- wikigen/mcp/vector_index.py +297 -0
- wikigen/metadata/__init__.py +35 -0
- wikigen/metadata/logo.py +28 -0
- wikigen/metadata/project.py +28 -0
- wikigen/metadata/version.py +17 -0
- wikigen/nodes/__init__.py +1 -0
- wikigen/nodes/nodes.py +1080 -0
- wikigen/utils/__init__.py +0 -0
- wikigen/utils/adjust_headings.py +72 -0
- wikigen/utils/call_llm.py +271 -0
- wikigen/utils/crawl_github_files.py +450 -0
- wikigen/utils/crawl_local_files.py +151 -0
- wikigen/utils/llm_providers.py +101 -0
- wikigen/utils/version_check.py +84 -0
- wikigen-1.0.0.dist-info/METADATA +352 -0
- wikigen-1.0.0.dist-info/RECORD +35 -0
- wikigen-1.0.0.dist-info/WHEEL +5 -0
- wikigen-1.0.0.dist-info/entry_points.txt +2 -0
- wikigen-1.0.0.dist-info/licenses/LICENSE +21 -0
- wikigen-1.0.0.dist-info/top_level.txt +1 -0
wikigen/__init__.py
ADDED
wikigen/cli.py
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for WikiGen.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
import argparse
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
from .config import (
|
|
11
|
+
init_config,
|
|
12
|
+
load_config,
|
|
13
|
+
merge_config_with_args,
|
|
14
|
+
check_config_exists,
|
|
15
|
+
save_config,
|
|
16
|
+
should_check_for_updates,
|
|
17
|
+
update_last_check_timestamp,
|
|
18
|
+
)
|
|
19
|
+
from .defaults import DEFAULT_INCLUDE_PATTERNS, DEFAULT_EXCLUDE_PATTERNS
|
|
20
|
+
from .flows.flow import create_wiki_flow
|
|
21
|
+
from .formatter.output_formatter import (
|
|
22
|
+
print_info,
|
|
23
|
+
print_final_success,
|
|
24
|
+
print_error_missing_api_key,
|
|
25
|
+
print_error_invalid_api_key,
|
|
26
|
+
print_error_rate_limit,
|
|
27
|
+
print_error_network,
|
|
28
|
+
print_error_general,
|
|
29
|
+
print_update_notification,
|
|
30
|
+
)
|
|
31
|
+
from .metadata.logo import print_logo
|
|
32
|
+
from .metadata import DESCRIPTION, CLI_ENTRY_POINT
|
|
33
|
+
from .metadata.version import get_version
|
|
34
|
+
from .formatter.help_formatter import print_enhanced_help
|
|
35
|
+
from .utils.version_check import check_for_update
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _is_url(source: str) -> bool:
|
|
39
|
+
"""Detect if source is a URL (GitHub/GitLab) or local path."""
|
|
40
|
+
if not source:
|
|
41
|
+
return False
|
|
42
|
+
return (
|
|
43
|
+
source.startswith("http://")
|
|
44
|
+
or source.startswith("https://")
|
|
45
|
+
or source.startswith("git@")
|
|
46
|
+
or source.startswith("ssh://")
|
|
47
|
+
or "github.com" in source
|
|
48
|
+
or "gitlab.com" in source
|
|
49
|
+
or "bitbucket.org" in source
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _run_documentation_generation(repo_url, local_dir, args, config):
|
|
54
|
+
"""Shared logic for running documentation generation."""
|
|
55
|
+
# Detect CI environment
|
|
56
|
+
is_ci = getattr(args, "ci", False) or os.environ.get("CI", "").lower() in (
|
|
57
|
+
"true",
|
|
58
|
+
"1",
|
|
59
|
+
"yes",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Get GitHub token from argument, config, or environment variable
|
|
63
|
+
github_token = None
|
|
64
|
+
if repo_url:
|
|
65
|
+
github_token = (
|
|
66
|
+
args.token or config.get("github_token") or os.environ.get("GITHUB_TOKEN")
|
|
67
|
+
)
|
|
68
|
+
if not github_token and not is_ci:
|
|
69
|
+
print(
|
|
70
|
+
"⚠ Warning: No GitHub token provided.\n"
|
|
71
|
+
" • For public repos: Optional, but you may hit rate limits (60 requests/hour)\n"
|
|
72
|
+
" • For private repos: Required for access\n"
|
|
73
|
+
f" • To add a token: Run '{CLI_ENTRY_POINT} config update-github-token'"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Merge config with CLI args (CLI takes precedence)
|
|
77
|
+
final_config = merge_config_with_args(config, args)
|
|
78
|
+
|
|
79
|
+
# Handle custom output path (for CI workflows)
|
|
80
|
+
output_dir = final_config["output_dir"]
|
|
81
|
+
if hasattr(args, "output_path") and args.output_path:
|
|
82
|
+
# Custom output path specified (e.g., 'docs/', 'documentation/')
|
|
83
|
+
output_dir = args.output_path
|
|
84
|
+
|
|
85
|
+
# Initialize the shared dictionary with inputs
|
|
86
|
+
shared = {
|
|
87
|
+
"repo_url": repo_url,
|
|
88
|
+
"local_dir": local_dir,
|
|
89
|
+
"project_name": args.name, # Can be None, FetchRepo will derive it
|
|
90
|
+
"github_token": github_token,
|
|
91
|
+
"output_dir": output_dir, # Base directory for CombineWiki output
|
|
92
|
+
# Add include/exclude patterns and max file size
|
|
93
|
+
"include_patterns": (
|
|
94
|
+
set(final_config["include_patterns"])
|
|
95
|
+
if final_config.get("include_patterns")
|
|
96
|
+
else DEFAULT_INCLUDE_PATTERNS
|
|
97
|
+
),
|
|
98
|
+
"exclude_patterns": (
|
|
99
|
+
set(final_config["exclude_patterns"])
|
|
100
|
+
if final_config.get("exclude_patterns")
|
|
101
|
+
else DEFAULT_EXCLUDE_PATTERNS
|
|
102
|
+
),
|
|
103
|
+
"max_file_size": final_config["max_file_size"],
|
|
104
|
+
# Add language for multi-language support
|
|
105
|
+
"language": final_config["language"],
|
|
106
|
+
# Add use_cache flag (inverse of no-cache flag)
|
|
107
|
+
"use_cache": final_config["use_cache"],
|
|
108
|
+
# Add max_abstraction_num parameter
|
|
109
|
+
"max_abstraction_num": final_config["max_abstractions"],
|
|
110
|
+
# Add documentation_mode parameter
|
|
111
|
+
"documentation_mode": final_config.get("documentation_mode", "minimal"),
|
|
112
|
+
# CI-specific flags
|
|
113
|
+
"ci_mode": is_ci,
|
|
114
|
+
"update_mode": getattr(args, "update", False),
|
|
115
|
+
"check_changes": getattr(args, "check_changes", False),
|
|
116
|
+
# Outputs will be populated by the nodes
|
|
117
|
+
"files": [],
|
|
118
|
+
"abstractions": [],
|
|
119
|
+
"relationships": {},
|
|
120
|
+
"component_order": [],
|
|
121
|
+
"components": [],
|
|
122
|
+
"final_output_dir": None,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
# Display logo and starting message with repository/directory and language
|
|
126
|
+
if not is_ci:
|
|
127
|
+
print_logo()
|
|
128
|
+
print_info("Repository", repo_url or local_dir)
|
|
129
|
+
print_info("Language", final_config["language"].capitalize())
|
|
130
|
+
print_info("LLM caching", "Enabled" if final_config["use_cache"] else "Disabled")
|
|
131
|
+
if is_ci:
|
|
132
|
+
print_info("CI Mode", "Enabled")
|
|
133
|
+
if hasattr(args, "output_path") and args.output_path:
|
|
134
|
+
print_info("Output Path", args.output_path)
|
|
135
|
+
|
|
136
|
+
# Create the flow instance
|
|
137
|
+
wiki_flow = create_wiki_flow()
|
|
138
|
+
|
|
139
|
+
# Run the flow
|
|
140
|
+
start_time = time.time()
|
|
141
|
+
try:
|
|
142
|
+
wiki_flow.run(shared)
|
|
143
|
+
total_time = time.time() - start_time
|
|
144
|
+
|
|
145
|
+
# Print final success message
|
|
146
|
+
print_final_success(
|
|
147
|
+
"Success! Documents generated", total_time, shared["final_output_dir"]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Check for updates (non-blocking, only if 24 hours have passed)
|
|
151
|
+
if not is_ci:
|
|
152
|
+
_check_for_updates_quietly()
|
|
153
|
+
|
|
154
|
+
# Handle change detection for CI
|
|
155
|
+
if shared.get("check_changes"):
|
|
156
|
+
# If docs were changed, exit with code 1
|
|
157
|
+
if shared.get("docs_changed", False):
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
else:
|
|
160
|
+
sys.exit(0)
|
|
161
|
+
except ValueError as e:
|
|
162
|
+
# Handle missing/invalid API key
|
|
163
|
+
# Check for missing API key errors (provider-agnostic)
|
|
164
|
+
error_str = str(e).lower()
|
|
165
|
+
if "not found" in error_str and (
|
|
166
|
+
"api key" in error_str or "api_key" in error_str
|
|
167
|
+
):
|
|
168
|
+
from .config import get_llm_provider
|
|
169
|
+
from .utils.llm_providers import get_display_name
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
provider = get_llm_provider()
|
|
173
|
+
provider_display = get_display_name(provider)
|
|
174
|
+
print_error_missing_api_key(provider_display)
|
|
175
|
+
except Exception:
|
|
176
|
+
print_error_missing_api_key()
|
|
177
|
+
else:
|
|
178
|
+
print_error_general(e)
|
|
179
|
+
sys.exit(1)
|
|
180
|
+
except (IOError, OSError, ConnectionError, TimeoutError) as e:
|
|
181
|
+
# Check error type and show appropriate message
|
|
182
|
+
error_str = str(e).lower()
|
|
183
|
+
if (
|
|
184
|
+
"401" in error_str
|
|
185
|
+
or "unauthorized" in error_str
|
|
186
|
+
or "invalid api key" in error_str
|
|
187
|
+
):
|
|
188
|
+
print_error_invalid_api_key()
|
|
189
|
+
elif "rate limit" in error_str or "429" in error_str:
|
|
190
|
+
print_error_rate_limit()
|
|
191
|
+
elif (
|
|
192
|
+
"connection" in error_str
|
|
193
|
+
or "timeout" in error_str
|
|
194
|
+
or "network" in error_str
|
|
195
|
+
):
|
|
196
|
+
print_error_network()
|
|
197
|
+
else:
|
|
198
|
+
print_error_general(e)
|
|
199
|
+
sys.exit(1)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def main():
|
|
203
|
+
"""Main CLI entry point."""
|
|
204
|
+
# Handle 'init' subcommand
|
|
205
|
+
if len(sys.argv) > 1 and sys.argv[1] == "init":
|
|
206
|
+
init_config()
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
# Handle 'config' subcommand
|
|
210
|
+
if len(sys.argv) > 1 and sys.argv[1] == "config":
|
|
211
|
+
handle_config_command()
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
# Handle 'mcp' subcommand
|
|
215
|
+
if len(sys.argv) > 1 and sys.argv[1] == "mcp":
|
|
216
|
+
from .mcp.server import run_mcp_server
|
|
217
|
+
|
|
218
|
+
run_mcp_server()
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
# Handle 'run' subcommand
|
|
222
|
+
if len(sys.argv) > 1 and sys.argv[1] == "run":
|
|
223
|
+
# Extract source argument (if provided, and not a flag)
|
|
224
|
+
source = None
|
|
225
|
+
remaining_args = []
|
|
226
|
+
|
|
227
|
+
if len(sys.argv) > 2 and not sys.argv[2].startswith("-"):
|
|
228
|
+
source = sys.argv[2]
|
|
229
|
+
remaining_args = sys.argv[3:] # Arguments after 'run source'
|
|
230
|
+
else:
|
|
231
|
+
remaining_args = sys.argv[2:] # Arguments after 'run' (no source)
|
|
232
|
+
|
|
233
|
+
# Determine repo_url or local_dir based on source
|
|
234
|
+
if source:
|
|
235
|
+
if _is_url(source):
|
|
236
|
+
repo_url = source
|
|
237
|
+
local_dir = None
|
|
238
|
+
else:
|
|
239
|
+
repo_url = None
|
|
240
|
+
local_dir = source
|
|
241
|
+
else:
|
|
242
|
+
# No source provided, use current directory
|
|
243
|
+
repo_url = None
|
|
244
|
+
local_dir = os.getcwd()
|
|
245
|
+
|
|
246
|
+
# Temporarily modify sys.argv to parse remaining arguments
|
|
247
|
+
original_argv = sys.argv[:]
|
|
248
|
+
try:
|
|
249
|
+
sys.argv = [sys.argv[0]] + remaining_args
|
|
250
|
+
|
|
251
|
+
# Check if config exists, if not, prompt user to run init
|
|
252
|
+
if not check_config_exists():
|
|
253
|
+
print("✘ WikiGen is not configured yet.")
|
|
254
|
+
print(
|
|
255
|
+
f"Please run '{CLI_ENTRY_POINT} init' to set up your configuration first."
|
|
256
|
+
)
|
|
257
|
+
sys.exit(1)
|
|
258
|
+
|
|
259
|
+
# Load saved configuration
|
|
260
|
+
config = load_config()
|
|
261
|
+
|
|
262
|
+
# Parse remaining arguments with enhanced help
|
|
263
|
+
parser = argparse.ArgumentParser(
|
|
264
|
+
description=DESCRIPTION,
|
|
265
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
266
|
+
add_help=False, # Disable default help to use our custom one
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
_add_common_arguments(parser, config)
|
|
270
|
+
|
|
271
|
+
args = parser.parse_args()
|
|
272
|
+
|
|
273
|
+
# Handle help display
|
|
274
|
+
if args.help:
|
|
275
|
+
print_enhanced_help()
|
|
276
|
+
sys.exit(0)
|
|
277
|
+
|
|
278
|
+
# Call shared function with categorized repo_url/local_dir
|
|
279
|
+
_run_documentation_generation(repo_url, local_dir, args, config)
|
|
280
|
+
finally:
|
|
281
|
+
# Always restore original sys.argv
|
|
282
|
+
sys.argv = original_argv
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
# Check if config exists, if not, prompt user to run init
|
|
286
|
+
if not check_config_exists():
|
|
287
|
+
print("✘ WikiGen is not configured yet.")
|
|
288
|
+
print(
|
|
289
|
+
f"Please run '{CLI_ENTRY_POINT} init' to set up your configuration first."
|
|
290
|
+
)
|
|
291
|
+
sys.exit(1)
|
|
292
|
+
|
|
293
|
+
# Load saved configuration
|
|
294
|
+
config = load_config()
|
|
295
|
+
|
|
296
|
+
# Parse arguments with enhanced help
|
|
297
|
+
parser = argparse.ArgumentParser(
|
|
298
|
+
description=DESCRIPTION,
|
|
299
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
300
|
+
add_help=False, # Disable default help to use our custom one
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
_add_common_arguments(parser, config)
|
|
304
|
+
|
|
305
|
+
# Create mutually exclusive group for source
|
|
306
|
+
source_group = parser.add_mutually_exclusive_group(required=False)
|
|
307
|
+
source_group.add_argument("--repo", help="URL of the public GitHub repository.")
|
|
308
|
+
source_group.add_argument("--dir", help="Path to local directory.")
|
|
309
|
+
|
|
310
|
+
args = parser.parse_args()
|
|
311
|
+
|
|
312
|
+
# Handle help display
|
|
313
|
+
if args.help:
|
|
314
|
+
print_enhanced_help()
|
|
315
|
+
sys.exit(0)
|
|
316
|
+
|
|
317
|
+
# Validate that either --repo or --dir is provided
|
|
318
|
+
if not args.repo and not args.dir:
|
|
319
|
+
print("Error: One of --repo or --dir is required.")
|
|
320
|
+
print("Use --help for more information.")
|
|
321
|
+
sys.exit(1)
|
|
322
|
+
|
|
323
|
+
# Call shared function with args.repo/args.dir
|
|
324
|
+
_run_documentation_generation(args.repo, args.dir, args, config)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _add_common_arguments(parser, config):
|
|
328
|
+
"""Add common arguments to the parser."""
|
|
329
|
+
# Add custom help option
|
|
330
|
+
parser.add_argument(
|
|
331
|
+
"-h",
|
|
332
|
+
"--help",
|
|
333
|
+
action="store_true",
|
|
334
|
+
help="Show enhanced help message and exit",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Add version option
|
|
338
|
+
parser.add_argument(
|
|
339
|
+
"-v",
|
|
340
|
+
"--version",
|
|
341
|
+
action="version",
|
|
342
|
+
version=f"wikigen {get_version()}",
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
parser.add_argument(
|
|
346
|
+
"-n",
|
|
347
|
+
"--name",
|
|
348
|
+
help="Project name (optional, derived from repo/directory if omitted).",
|
|
349
|
+
)
|
|
350
|
+
parser.add_argument(
|
|
351
|
+
"-t",
|
|
352
|
+
"--token",
|
|
353
|
+
help="GitHub personal access token (optional, reads from GITHUB_TOKEN env var if not provided).",
|
|
354
|
+
)
|
|
355
|
+
parser.add_argument(
|
|
356
|
+
"-o",
|
|
357
|
+
"--output",
|
|
358
|
+
default=config.get("output_dir", "output"),
|
|
359
|
+
help="Base directory for output (default: from config).",
|
|
360
|
+
)
|
|
361
|
+
parser.add_argument(
|
|
362
|
+
"-i",
|
|
363
|
+
"--include",
|
|
364
|
+
nargs="+",
|
|
365
|
+
help="Include file patterns (e.g. '*.py' '*.js'). Defaults to common code files if not specified.",
|
|
366
|
+
)
|
|
367
|
+
parser.add_argument(
|
|
368
|
+
"-e",
|
|
369
|
+
"--exclude",
|
|
370
|
+
nargs="+",
|
|
371
|
+
help="Exclude file patterns (e.g. 'tests/*' 'docs/*'). Defaults to test/build directories if not specified.",
|
|
372
|
+
)
|
|
373
|
+
parser.add_argument(
|
|
374
|
+
"-s",
|
|
375
|
+
"--max-size",
|
|
376
|
+
type=int,
|
|
377
|
+
default=config.get("max_file_size", 100000),
|
|
378
|
+
help="Maximum file size in bytes (default: from config).",
|
|
379
|
+
)
|
|
380
|
+
# Add language parameter for multi-language support
|
|
381
|
+
parser.add_argument(
|
|
382
|
+
"--language",
|
|
383
|
+
default=config.get("language", "english"),
|
|
384
|
+
help="Language for the generated wiki (default: from config)",
|
|
385
|
+
)
|
|
386
|
+
# Add use_cache parameter to control LLM caching
|
|
387
|
+
parser.add_argument(
|
|
388
|
+
"--no-cache",
|
|
389
|
+
action="store_true",
|
|
390
|
+
help="Disable LLM response caching (default: caching enabled)",
|
|
391
|
+
)
|
|
392
|
+
# Add max_abstraction_num parameter to control the number of abstractions
|
|
393
|
+
parser.add_argument(
|
|
394
|
+
"--max-abstractions",
|
|
395
|
+
type=int,
|
|
396
|
+
default=config.get("max_abstractions", 10),
|
|
397
|
+
help="Maximum number of abstractions to identify (default: from config)",
|
|
398
|
+
)
|
|
399
|
+
# Add documentation mode parameter
|
|
400
|
+
parser.add_argument(
|
|
401
|
+
"--mode",
|
|
402
|
+
choices=["minimal", "comprehensive"],
|
|
403
|
+
default=None,
|
|
404
|
+
help="Documentation mode (default: from config)",
|
|
405
|
+
)
|
|
406
|
+
# CI/CD specific flags
|
|
407
|
+
parser.add_argument(
|
|
408
|
+
"--ci",
|
|
409
|
+
action="store_true",
|
|
410
|
+
help="Enable CI mode (non-interactive, uses defaults, better error messages)",
|
|
411
|
+
)
|
|
412
|
+
parser.add_argument(
|
|
413
|
+
"--update",
|
|
414
|
+
action="store_true",
|
|
415
|
+
help="Update existing documentation instead of overwriting (merges with existing docs)",
|
|
416
|
+
)
|
|
417
|
+
parser.add_argument(
|
|
418
|
+
"--output-path",
|
|
419
|
+
help="Custom output path for documentation (e.g., 'docs/', 'documentation/')",
|
|
420
|
+
)
|
|
421
|
+
parser.add_argument(
|
|
422
|
+
"--check-changes",
|
|
423
|
+
action="store_true",
|
|
424
|
+
help="Exit with code 1 if docs changed, 0 if unchanged (useful for conditional PR creation)",
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _check_for_updates_quietly():
|
|
429
|
+
"""
|
|
430
|
+
Check for updates in the background without blocking the CLI.
|
|
431
|
+
Only checks if 24 hours have passed since last check.
|
|
432
|
+
Silently fails on any errors to not interrupt user workflow.
|
|
433
|
+
"""
|
|
434
|
+
try:
|
|
435
|
+
# Only check if 24 hours have passed
|
|
436
|
+
if not should_check_for_updates():
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
current_version = get_version()
|
|
440
|
+
latest_version = check_for_update(current_version, timeout=5.0)
|
|
441
|
+
|
|
442
|
+
# Update timestamp after attempting check (prevents excessive API calls)
|
|
443
|
+
# Even if network fails, we update to avoid retrying immediately
|
|
444
|
+
update_last_check_timestamp()
|
|
445
|
+
|
|
446
|
+
# If update is available, show notification
|
|
447
|
+
if latest_version:
|
|
448
|
+
print_update_notification(current_version, latest_version)
|
|
449
|
+
except Exception:
|
|
450
|
+
# Silently fail - don't interrupt user workflow
|
|
451
|
+
# Catch all exceptions to ensure update checks never break the CLI
|
|
452
|
+
pass
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def handle_config_command():
|
|
456
|
+
"""Handle wikigen config commands."""
|
|
457
|
+
if len(sys.argv) < 3:
|
|
458
|
+
print("Usage: wikigen config <command>")
|
|
459
|
+
print("Commands:")
|
|
460
|
+
print(" show - Show current configuration")
|
|
461
|
+
print(" set <key> <value> - Set a configuration value")
|
|
462
|
+
print(
|
|
463
|
+
" update-api-key <provider> - Update API key for a provider (interactive)"
|
|
464
|
+
)
|
|
465
|
+
print(
|
|
466
|
+
" update-github-token [token] - Update GitHub token (interactive if no token provided)"
|
|
467
|
+
)
|
|
468
|
+
print("\nExamples:")
|
|
469
|
+
print(" wikigen config set llm-provider openai")
|
|
470
|
+
print(" wikigen config set llm-model gpt-4o-mini")
|
|
471
|
+
print(" wikigen config update-api-key gemini")
|
|
472
|
+
return
|
|
473
|
+
|
|
474
|
+
command = sys.argv[2]
|
|
475
|
+
|
|
476
|
+
if command == "show":
|
|
477
|
+
show_config()
|
|
478
|
+
elif command == "set":
|
|
479
|
+
if len(sys.argv) < 5:
|
|
480
|
+
print("Usage: wikigen config set <key> <value>")
|
|
481
|
+
print("Example: wikigen config set language spanish")
|
|
482
|
+
print("Example: wikigen config set llm-provider openai")
|
|
483
|
+
return
|
|
484
|
+
key = sys.argv[3]
|
|
485
|
+
value = sys.argv[4]
|
|
486
|
+
set_config_value(key, value)
|
|
487
|
+
elif command == "update-api-key":
|
|
488
|
+
if len(sys.argv) < 4:
|
|
489
|
+
print("Usage: wikigen config update-api-key <provider>")
|
|
490
|
+
print("Providers: gemini, openai, anthropic, openrouter")
|
|
491
|
+
return
|
|
492
|
+
provider = sys.argv[3]
|
|
493
|
+
update_api_key(provider)
|
|
494
|
+
elif command == "update-gemini-key":
|
|
495
|
+
# Legacy command, redirect to update-api-key
|
|
496
|
+
update_api_key("gemini")
|
|
497
|
+
elif command == "update-github-token":
|
|
498
|
+
if len(sys.argv) > 3:
|
|
499
|
+
# Token provided as argument
|
|
500
|
+
new_token = sys.argv[3]
|
|
501
|
+
update_github_token_direct(new_token)
|
|
502
|
+
else:
|
|
503
|
+
# Interactive mode
|
|
504
|
+
update_github_token()
|
|
505
|
+
else:
|
|
506
|
+
print(f"Unknown command: {command}")
|
|
507
|
+
print("Run 'wikigen config' to see available commands")
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def show_config():
|
|
511
|
+
"""Show current configuration."""
|
|
512
|
+
if not check_config_exists():
|
|
513
|
+
print(f"✘ No configuration found. Run '{CLI_ENTRY_POINT} init' first.")
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
config = load_config()
|
|
517
|
+
print(" Current WikiGen Configuration:")
|
|
518
|
+
print(f" LLM Provider: {config.get('llm_provider', 'Not set')}")
|
|
519
|
+
print(f" LLM Model: {config.get('llm_model', 'Not set')}")
|
|
520
|
+
print(f" Output Directory: {config.get('output_dir', 'Not set')}")
|
|
521
|
+
print(f" Language: {config.get('language', 'Not set')}")
|
|
522
|
+
print(f" Max Abstractions: {config.get('max_abstractions', 'Not set')}")
|
|
523
|
+
print(f" Max File Size: {config.get('max_file_size', 'Not set')}")
|
|
524
|
+
print(f" Use Cache: {config.get('use_cache', 'Not set')}")
|
|
525
|
+
print(f" Documentation Mode: {config.get('documentation_mode', 'Not set')}")
|
|
526
|
+
|
|
527
|
+
# Check if API keys are available
|
|
528
|
+
try:
|
|
529
|
+
from .config import get_api_key, get_github_token, get_llm_provider
|
|
530
|
+
from .utils.llm_providers import get_display_name
|
|
531
|
+
|
|
532
|
+
provider = get_llm_provider()
|
|
533
|
+
provider_display = get_display_name(provider)
|
|
534
|
+
api_key = get_api_key()
|
|
535
|
+
github_token = get_github_token()
|
|
536
|
+
print(f" {provider_display} API Key: {'✓ Set' if api_key else '✘ Not set'}")
|
|
537
|
+
print(f" GitHub Token: {'✓ Set' if github_token else '✘ Not set'}")
|
|
538
|
+
except (IOError, OSError, ValueError, ImportError) as e:
|
|
539
|
+
print(f" API Key: Unable to check ({e})")
|
|
540
|
+
print(f" GitHub Token: Unable to check ({e})")
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def set_config_value(key, value):
|
|
544
|
+
"""Set a configuration value."""
|
|
545
|
+
if not check_config_exists():
|
|
546
|
+
print(f"✘ No configuration found. Run '{CLI_ENTRY_POINT} init' first.")
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
config = load_config()
|
|
550
|
+
|
|
551
|
+
# Map CLI keys to config keys
|
|
552
|
+
key_mapping = {
|
|
553
|
+
"llm-provider": "llm_provider",
|
|
554
|
+
"llm-model": "llm_model",
|
|
555
|
+
}
|
|
556
|
+
config_key = key_mapping.get(key, key)
|
|
557
|
+
|
|
558
|
+
# Validate provider/model if setting those
|
|
559
|
+
if config_key == "llm_provider":
|
|
560
|
+
from .utils.llm_providers import get_provider_list
|
|
561
|
+
|
|
562
|
+
if value not in get_provider_list():
|
|
563
|
+
print(f"✘ Invalid provider: {value}")
|
|
564
|
+
print(f"Available providers: {', '.join(get_provider_list())}")
|
|
565
|
+
return
|
|
566
|
+
|
|
567
|
+
# Handle different value types
|
|
568
|
+
if config_key in ["max_abstractions", "max_file_size"]:
|
|
569
|
+
try:
|
|
570
|
+
value = int(value)
|
|
571
|
+
except ValueError:
|
|
572
|
+
print(f"✘ {key} must be a number")
|
|
573
|
+
return
|
|
574
|
+
elif config_key == "use_cache":
|
|
575
|
+
value = value.lower() in ["true", "1", "yes", "on"]
|
|
576
|
+
elif config_key in ["include_patterns", "exclude_patterns"]:
|
|
577
|
+
value = [pattern.strip() for pattern in value.split(",")]
|
|
578
|
+
|
|
579
|
+
config[config_key] = value
|
|
580
|
+
save_config(config)
|
|
581
|
+
print(f"✓ Updated {key} to {value}")
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _update_secret(
|
|
585
|
+
secret_key: str,
|
|
586
|
+
secret_value: str,
|
|
587
|
+
display_name: str,
|
|
588
|
+
allow_empty: bool = False,
|
|
589
|
+
) -> None:
|
|
590
|
+
"""
|
|
591
|
+
Common function to update a secret in keyring or config file.
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
secret_key: The keyring service/key name and config key (e.g., "gemini_api_key")
|
|
595
|
+
secret_value: The secret value to store (empty string removes it if allow_empty=True)
|
|
596
|
+
display_name: Human-readable name for messages (e.g., "Gemini API key")
|
|
597
|
+
allow_empty: If True, empty value removes the secret; if False, empty is invalid
|
|
598
|
+
"""
|
|
599
|
+
try:
|
|
600
|
+
import keyring
|
|
601
|
+
|
|
602
|
+
KEYRING_AVAILABLE = True
|
|
603
|
+
except ImportError:
|
|
604
|
+
KEYRING_AVAILABLE = False
|
|
605
|
+
|
|
606
|
+
if KEYRING_AVAILABLE:
|
|
607
|
+
try:
|
|
608
|
+
if secret_value:
|
|
609
|
+
keyring.set_password("wikigen", secret_key, secret_value)
|
|
610
|
+
print(f"✓ {display_name} updated securely in keyring")
|
|
611
|
+
return
|
|
612
|
+
elif allow_empty:
|
|
613
|
+
keyring.delete_password("wikigen", secret_key)
|
|
614
|
+
print(f"✓ {display_name} removed from keyring")
|
|
615
|
+
return
|
|
616
|
+
else:
|
|
617
|
+
print(f"✘ {display_name} cannot be empty")
|
|
618
|
+
return
|
|
619
|
+
except (OSError, RuntimeError, AttributeError) as e:
|
|
620
|
+
print(f"✘ Failed to update keyring: {e}")
|
|
621
|
+
# Fall through to config file fallback
|
|
622
|
+
|
|
623
|
+
# Fallback to config file if keyring not available or failed
|
|
624
|
+
print("⚠ Keyring not available, updating config file (less secure)")
|
|
625
|
+
config = load_config()
|
|
626
|
+
if secret_value:
|
|
627
|
+
config[secret_key] = secret_value
|
|
628
|
+
save_config(config)
|
|
629
|
+
print(f"✓ {display_name} updated in config file")
|
|
630
|
+
elif allow_empty:
|
|
631
|
+
config.pop(secret_key, None)
|
|
632
|
+
save_config(config)
|
|
633
|
+
print(f"✓ {display_name} removed from config file")
|
|
634
|
+
else:
|
|
635
|
+
print(f"✘ {display_name} cannot be empty")
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def update_api_key(provider: str):
|
|
639
|
+
"""Update API key for a provider (interactive)."""
|
|
640
|
+
import getpass
|
|
641
|
+
|
|
642
|
+
from .utils.llm_providers import (
|
|
643
|
+
get_provider_info,
|
|
644
|
+
get_display_name,
|
|
645
|
+
requires_api_key,
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
try:
|
|
649
|
+
provider_info = get_provider_info(provider)
|
|
650
|
+
provider_display = get_display_name(provider)
|
|
651
|
+
|
|
652
|
+
if not requires_api_key(provider):
|
|
653
|
+
print(f"✘ {provider_display} does not require an API key")
|
|
654
|
+
return
|
|
655
|
+
|
|
656
|
+
keyring_key = provider_info.get("keyring_key")
|
|
657
|
+
key_name = f"{provider_display} API Key"
|
|
658
|
+
|
|
659
|
+
print(f"+ Update {key_name}")
|
|
660
|
+
new_key = getpass.getpass(f"Enter new {key_name}: ").strip()
|
|
661
|
+
|
|
662
|
+
if not new_key:
|
|
663
|
+
print("✘ API key cannot be empty")
|
|
664
|
+
return
|
|
665
|
+
|
|
666
|
+
_update_secret(keyring_key, new_key, key_name, allow_empty=False)
|
|
667
|
+
except ValueError as e:
|
|
668
|
+
print(f"✘ {e}")
|
|
669
|
+
print("Available providers: gemini, openai, anthropic, openrouter")
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def update_github_token():
|
|
673
|
+
"""Update GitHub token (interactive)."""
|
|
674
|
+
import getpass
|
|
675
|
+
|
|
676
|
+
print("+ Update GitHub Token")
|
|
677
|
+
new_token = getpass.getpass(
|
|
678
|
+
"Enter new GitHub token (or press Enter to remove): "
|
|
679
|
+
).strip()
|
|
680
|
+
|
|
681
|
+
update_github_token_direct(new_token)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def update_github_token_direct(new_token: str) -> None:
|
|
685
|
+
"""Update GitHub token directly."""
|
|
686
|
+
_update_secret("github_token", new_token, "GitHub token", allow_empty=True)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
if __name__ == "__main__":
|
|
690
|
+
main()
|