docwright 0.1.5__tar.gz → 0.1.7__tar.gz
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.
- {docwright-0.1.5 → docwright-0.1.7}/PKG-INFO +1 -1
- docwright-0.1.7/docwright/cli.py +347 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/config.py +17 -5
- docwright-0.1.7/docwright/outputs/factory.py +24 -0
- docwright-0.1.7/docwright/outputs/gitlab_wiki.py +36 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/scaffolder.py +11 -11
- {docwright-0.1.5 → docwright-0.1.7}/pyproject.toml +1 -1
- docwright-0.1.5/docwright/cli.py +0 -136
- docwright-0.1.5/docwright/outputs/factory.py +0 -12
- {docwright-0.1.5 → docwright-0.1.7}/LICENSE +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/README.md +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/__init__.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/analyzer.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/__init__.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/readme/__init__.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/readme/default.md.j2 +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/__init__.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/adr.md.j2 +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/api-contracts.md.j2 +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/architecture.md.j2 +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/data-model.md.j2 +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/db-schema.md.j2 +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/development-guide.md.j2 +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/home.md.j2 +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/integrations.md.j2 +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/operations.md.j2 +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/security.md.j2 +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/troubleshooting.md.j2 +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/engine.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/outputs/__init__.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/outputs/base.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/outputs/direct.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/outputs/pull_request.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/providers/__init__.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/providers/base.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/providers/claude.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/providers/factory.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/providers/ollama.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/providers/openai.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/registry.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/renderer.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/reporters/__init__.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/reporters/html.py +0 -0
- {docwright-0.1.5 → docwright-0.1.7}/docwright/reporters/terminal.py +0 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import yaml
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from docwright.config import Config
|
|
14
|
+
from docwright.engine import DocsEngine
|
|
15
|
+
from docwright.outputs.factory import build_output
|
|
16
|
+
from docwright.providers.factory import build_provider
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_repo_root() -> Path:
|
|
22
|
+
return Path.cwd()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_engine(repo_root: Path) -> DocsEngine:
|
|
26
|
+
config = Config.load(repo_root)
|
|
27
|
+
provider = build_provider(config.provider)
|
|
28
|
+
output = build_output(config.output, repo_root)
|
|
29
|
+
return DocsEngine(repo_root=repo_root, provider=provider, output=output)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def ensure_gitignore_has_env(repo_root: Path) -> None:
|
|
33
|
+
gitignore = repo_root / ".gitignore"
|
|
34
|
+
if gitignore.exists():
|
|
35
|
+
content = gitignore.read_text()
|
|
36
|
+
if ".env" in content.splitlines():
|
|
37
|
+
return
|
|
38
|
+
gitignore.write_text(content.rstrip("\n") + "\n.env\n")
|
|
39
|
+
else:
|
|
40
|
+
gitignore.write_text(".env\n")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def append_env_var(repo_root: Path, key: str, value: str) -> None:
|
|
44
|
+
env_file = repo_root / ".env"
|
|
45
|
+
existing = env_file.read_text() if env_file.exists() else ""
|
|
46
|
+
lines = existing.splitlines()
|
|
47
|
+
filtered = [ln for ln in lines if not ln.startswith(f"{key}=")]
|
|
48
|
+
filtered.append(f"{key}={value}")
|
|
49
|
+
env_file.write_text("\n".join(filtered) + "\n")
|
|
50
|
+
ensure_gitignore_has_env(repo_root)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def ask_int_choice(prompt: str, max_choice: int, default: int = 1) -> int:
|
|
54
|
+
while True:
|
|
55
|
+
raw = input(f"{prompt} [{default}]: ").strip()
|
|
56
|
+
if raw == "":
|
|
57
|
+
return default
|
|
58
|
+
if raw.isdigit() and 1 <= int(raw) <= max_choice:
|
|
59
|
+
return int(raw)
|
|
60
|
+
console.print(f"[red]✗ Enter a number from 1 to {max_choice}.[/red]")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run_interactive_setup(repo_root: Path) -> None:
|
|
64
|
+
"""Run interactive setup wizard and write .docwright/docwright.yml."""
|
|
65
|
+
console.print()
|
|
66
|
+
console.print(
|
|
67
|
+
"[bold green]Welcome to docwright!"
|
|
68
|
+
" Let's set up documentation for this project.[/bold green]"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# ── Step 1: Provider ──────────────────────────────────────────────────────
|
|
72
|
+
console.rule("[bold]Step 1 of 4: AI Provider[/bold]")
|
|
73
|
+
console.print("\nWhich AI provider do you want to use?\n")
|
|
74
|
+
console.print(" 1. OpenAI (GPT-4o) — recommended")
|
|
75
|
+
console.print(" 2. Claude (Anthropic)")
|
|
76
|
+
console.print(" 3. Ollama (local or corporate server)\n")
|
|
77
|
+
|
|
78
|
+
provider_choice = ask_int_choice("Choose", 3, default=1)
|
|
79
|
+
provider_map = {1: "openai", 2: "claude", 3: "ollama"}
|
|
80
|
+
provider = provider_map[provider_choice]
|
|
81
|
+
|
|
82
|
+
# ── Step 2: API Key / Base URL ────────────────────────────────────────────
|
|
83
|
+
console.rule("[bold]Step 2 of 4: API credentials[/bold]")
|
|
84
|
+
|
|
85
|
+
api_key_env: str | None = None
|
|
86
|
+
base_url: str | None = None
|
|
87
|
+
|
|
88
|
+
if provider == "openai":
|
|
89
|
+
console.print("\nYou need an OpenAI API key.\n")
|
|
90
|
+
console.print(" Get it here → [link]https://platform.openai.com/api-keys[/link]")
|
|
91
|
+
console.print(" (Sign in → API Keys → Create new secret key)\n")
|
|
92
|
+
while True:
|
|
93
|
+
key = input("Paste your API key: ").strip()
|
|
94
|
+
if key.startswith("sk-"):
|
|
95
|
+
break
|
|
96
|
+
console.print(
|
|
97
|
+
"[red]✗ That doesn't look like an OpenAI key"
|
|
98
|
+
" (should start with sk-). Try again:[/red]"
|
|
99
|
+
)
|
|
100
|
+
append_env_var(repo_root, "OPENAI_API_KEY", key)
|
|
101
|
+
console.print("[green]✓ Key saved to .env (this file will NOT be committed to git)[/green]")
|
|
102
|
+
api_key_env = "OPENAI_API_KEY"
|
|
103
|
+
model = "gpt-4o"
|
|
104
|
+
|
|
105
|
+
elif provider == "claude":
|
|
106
|
+
console.print("\nYou need an Anthropic API key.\n")
|
|
107
|
+
console.print(" Get it here → [link]https://console.anthropic.com/settings/keys[/link]")
|
|
108
|
+
console.print(" (Sign in → API Keys → Create new secret key)\n")
|
|
109
|
+
while True:
|
|
110
|
+
key = input("Paste your API key: ").strip()
|
|
111
|
+
if key.startswith("sk-ant-"):
|
|
112
|
+
break
|
|
113
|
+
console.print(
|
|
114
|
+
"[red]✗ That doesn't look like an Anthropic key"
|
|
115
|
+
" (should start with sk-ant-). Try again:[/red]"
|
|
116
|
+
)
|
|
117
|
+
append_env_var(repo_root, "ANTHROPIC_API_KEY", key)
|
|
118
|
+
console.print("[green]✓ Key saved to .env (this file will NOT be committed to git)[/green]")
|
|
119
|
+
api_key_env = "ANTHROPIC_API_KEY"
|
|
120
|
+
model = "claude-sonnet-4-6"
|
|
121
|
+
|
|
122
|
+
else: # ollama
|
|
123
|
+
console.print()
|
|
124
|
+
raw_url = input("Base URL [http://localhost:11434]: ").strip()
|
|
125
|
+
base_url = raw_url or "http://localhost:11434"
|
|
126
|
+
raw_model = input("Model name [llama3]: ").strip()
|
|
127
|
+
model = raw_model or "llama3"
|
|
128
|
+
api_key_env = None
|
|
129
|
+
|
|
130
|
+
# ── Step 3: Language ──────────────────────────────────────────────────────
|
|
131
|
+
console.rule("[bold]Step 3 of 4: Documentation language[/bold]")
|
|
132
|
+
console.print()
|
|
133
|
+
console.print(" 1. English")
|
|
134
|
+
console.print(" 2. Russian")
|
|
135
|
+
console.print(" 3. Other (enter manually)\n")
|
|
136
|
+
|
|
137
|
+
lang_choice = ask_int_choice("Choose", 3, default=1)
|
|
138
|
+
if lang_choice == 1:
|
|
139
|
+
language = "en"
|
|
140
|
+
elif lang_choice == 2:
|
|
141
|
+
language = "ru"
|
|
142
|
+
else:
|
|
143
|
+
language = input("Enter language code (e.g. de, fr, es): ").strip() or "en"
|
|
144
|
+
|
|
145
|
+
# ── Step 4: Output mode ───────────────────────────────────────────────────
|
|
146
|
+
console.rule("[bold]Step 4 of 4: How to publish docs?[/bold]")
|
|
147
|
+
console.print()
|
|
148
|
+
console.print(" 1. Commit directly to branch (simplest, good for solo projects)")
|
|
149
|
+
console.print(" 2. Create a Pull/Merge Request (requires human review)")
|
|
150
|
+
console.print(" 3. Push to GitLab Wiki\n")
|
|
151
|
+
|
|
152
|
+
output_choice = ask_int_choice("Choose", 3, default=1)
|
|
153
|
+
output_mode_map = {1: "direct", 2: "pull_request", 3: "gitlab_wiki"}
|
|
154
|
+
output_mode = output_mode_map[output_choice]
|
|
155
|
+
|
|
156
|
+
wiki_url: str | None = None
|
|
157
|
+
gitlab_token_env: str | None = None
|
|
158
|
+
|
|
159
|
+
if output_mode == "gitlab_wiki":
|
|
160
|
+
console.print("\nGitLab Wiki URL:")
|
|
161
|
+
console.print(" → Take your project URL and add .wiki.git at the end")
|
|
162
|
+
console.print(" → Example: https://gitlab.com/mygroup/myproject.wiki.git\n")
|
|
163
|
+
|
|
164
|
+
while True:
|
|
165
|
+
wiki_url = input("URL: ").strip()
|
|
166
|
+
if wiki_url.endswith(".wiki.git"):
|
|
167
|
+
break
|
|
168
|
+
console.print(
|
|
169
|
+
"[red]✗ URL should end with .wiki.git. "
|
|
170
|
+
"Example: https://gitlab.com/group/project.wiki.git[/red]"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
parsed = urlparse(wiki_url)
|
|
174
|
+
gitlab_host = f"{parsed.scheme}://{parsed.netloc}"
|
|
175
|
+
console.print("\nGitLab Personal Access Token:")
|
|
176
|
+
console.print(f" Get it here → {gitlab_host}/-/user_settings/personal_access_tokens")
|
|
177
|
+
console.print(" Required scopes: read_repository write_repository\n")
|
|
178
|
+
|
|
179
|
+
while True:
|
|
180
|
+
token = input("Paste your token: ").strip()
|
|
181
|
+
if token:
|
|
182
|
+
break
|
|
183
|
+
console.print("[red]✗ Token cannot be empty.[/red]")
|
|
184
|
+
|
|
185
|
+
append_env_var(repo_root, "GITLAB_TOKEN", token)
|
|
186
|
+
console.print(
|
|
187
|
+
"[green]✓ Token saved to .env (this file will NOT be committed to git)[/green]"
|
|
188
|
+
)
|
|
189
|
+
gitlab_token_env = "GITLAB_TOKEN"
|
|
190
|
+
|
|
191
|
+
# ── Write config ──────────────────────────────────────────────────────────
|
|
192
|
+
from docwright.config import CONFIG_DIR, CONFIG_FILE
|
|
193
|
+
|
|
194
|
+
config_dir = repo_root / CONFIG_DIR
|
|
195
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
config_path = config_dir / CONFIG_FILE
|
|
197
|
+
|
|
198
|
+
provider_section: dict[str, object] = {
|
|
199
|
+
"type": provider,
|
|
200
|
+
"model": model,
|
|
201
|
+
}
|
|
202
|
+
if api_key_env:
|
|
203
|
+
provider_section["api_key_env"] = api_key_env
|
|
204
|
+
if base_url:
|
|
205
|
+
provider_section["base_url"] = base_url
|
|
206
|
+
|
|
207
|
+
output_section: dict[str, object] = {"mode": output_mode}
|
|
208
|
+
if wiki_url:
|
|
209
|
+
output_section["wiki_url"] = wiki_url
|
|
210
|
+
if gitlab_token_env:
|
|
211
|
+
output_section["token_env"] = gitlab_token_env
|
|
212
|
+
|
|
213
|
+
config_data: dict[str, object] = {
|
|
214
|
+
"provider": provider_section,
|
|
215
|
+
"output": output_section,
|
|
216
|
+
"language": language,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
config_path.write_text(yaml.dump(config_data, default_flow_style=False, allow_unicode=True))
|
|
220
|
+
|
|
221
|
+
console.print()
|
|
222
|
+
console.rule()
|
|
223
|
+
console.print(f"[green]✓ Config saved to {config_path.relative_to(repo_root)}[/green]")
|
|
224
|
+
console.print("\nStarting documentation generation...")
|
|
225
|
+
console.rule()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@click.group()
|
|
229
|
+
def cli() -> None:
|
|
230
|
+
"""AI-powered documentation agent."""
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@cli.command()
|
|
234
|
+
def init() -> None:
|
|
235
|
+
"""Generate documentation from scratch."""
|
|
236
|
+
repo_root = get_repo_root()
|
|
237
|
+
Config.migrate_if_needed(repo_root)
|
|
238
|
+
from docwright.config import CONFIG_DIR, CONFIG_FILE
|
|
239
|
+
|
|
240
|
+
config_exists = (repo_root / CONFIG_DIR / CONFIG_FILE).exists()
|
|
241
|
+
if not config_exists:
|
|
242
|
+
run_interactive_setup(repo_root)
|
|
243
|
+
engine = build_engine(repo_root)
|
|
244
|
+
asyncio.run(engine.init())
|
|
245
|
+
click.echo("Documentation initialized.")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@cli.command()
|
|
249
|
+
@click.option("--dry-run", is_flag=True, help="Show what would change without writing files.")
|
|
250
|
+
def run(dry_run: bool) -> None:
|
|
251
|
+
"""Incrementally update documentation based on recent git diff."""
|
|
252
|
+
repo_root = get_repo_root()
|
|
253
|
+
Config.migrate_if_needed(repo_root)
|
|
254
|
+
base_sha = os.environ.get("AI_DOCGEN_BASE_SHA", "HEAD~1")
|
|
255
|
+
try:
|
|
256
|
+
diff_text = subprocess.check_output(
|
|
257
|
+
["git", "diff", f"{base_sha}..HEAD"], cwd=repo_root
|
|
258
|
+
).decode()
|
|
259
|
+
except subprocess.CalledProcessError:
|
|
260
|
+
diff_text = ""
|
|
261
|
+
if dry_run:
|
|
262
|
+
config = Config.load(repo_root)
|
|
263
|
+
triggers = config.triggers
|
|
264
|
+
from docwright.analyzer import DiffAnalyzer
|
|
265
|
+
|
|
266
|
+
analyzer = DiffAnalyzer(
|
|
267
|
+
diff_text=diff_text,
|
|
268
|
+
trigger_paths=triggers.paths if triggers else [],
|
|
269
|
+
ignore_paths=triggers.ignore if triggers else [],
|
|
270
|
+
)
|
|
271
|
+
if analyzer.has_relevant_changes():
|
|
272
|
+
click.echo("Relevant changes detected — documentation would be updated.")
|
|
273
|
+
else:
|
|
274
|
+
click.echo("No relevant changes — documentation would be skipped.")
|
|
275
|
+
return
|
|
276
|
+
engine = build_engine(repo_root)
|
|
277
|
+
skipped = asyncio.run(engine.run(diff_text=diff_text))
|
|
278
|
+
click.echo(
|
|
279
|
+
"No relevant changes — documentation up to date." if skipped else "Documentation updated."
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@cli.command()
|
|
284
|
+
def sync() -> None:
|
|
285
|
+
"""Force re-sync all documentation against current templates."""
|
|
286
|
+
repo_root = get_repo_root()
|
|
287
|
+
Config.migrate_if_needed(repo_root)
|
|
288
|
+
engine = build_engine(repo_root)
|
|
289
|
+
asyncio.run(engine.sync())
|
|
290
|
+
click.echo("Documentation synced.")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@cli.command("install")
|
|
294
|
+
@click.option("--auto", is_flag=True, help="Non-interactive mode with auto-detected defaults.")
|
|
295
|
+
@click.option("--provider", default=None, type=click.Choice(["claude", "openai", "ollama"]))
|
|
296
|
+
@click.option("--output", "output_mode", default=None, type=click.Choice(["pr", "direct"]))
|
|
297
|
+
def install(auto: bool, provider: str | None, output_mode: str | None) -> None:
|
|
298
|
+
"""Bootstrap this repo with docwright configuration."""
|
|
299
|
+
from docwright.scaffolder import Scaffolder
|
|
300
|
+
|
|
301
|
+
repo_root = get_repo_root()
|
|
302
|
+
scaffolder = Scaffolder(repo_root=repo_root)
|
|
303
|
+
profile = scaffolder.detect_profile()
|
|
304
|
+
|
|
305
|
+
if auto:
|
|
306
|
+
final_provider = provider or "claude"
|
|
307
|
+
final_output = output_mode or "pr"
|
|
308
|
+
else:
|
|
309
|
+
click.echo(
|
|
310
|
+
f"Detected: {profile.language} project '{profile.service_name}', CI: {profile.ci}"
|
|
311
|
+
)
|
|
312
|
+
final_provider = provider or click.prompt(
|
|
313
|
+
"LLM provider",
|
|
314
|
+
type=click.Choice(["claude", "openai", "ollama"]),
|
|
315
|
+
default="claude",
|
|
316
|
+
)
|
|
317
|
+
final_output = output_mode or click.prompt(
|
|
318
|
+
"Output mode",
|
|
319
|
+
type=click.Choice(["pr", "direct"]),
|
|
320
|
+
default="pr",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
scaffolder.generate(profile, provider_type=final_provider, output_mode=final_output)
|
|
324
|
+
click.echo(f"Installed docwright for '{profile.service_name}'.")
|
|
325
|
+
click.echo("Next: set your API key env var, then run 'make docs'.")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@cli.command()
|
|
329
|
+
@click.option("--registry", "registry_path", default=None, help="Path to registry.yml")
|
|
330
|
+
def dashboard(registry_path: str | None) -> None:
|
|
331
|
+
"""Show status of all registered projects."""
|
|
332
|
+
from docwright.reporters.terminal import render_dashboard
|
|
333
|
+
|
|
334
|
+
path = Path(registry_path) if registry_path else Path.cwd() / ".docwright" / "registry.yml"
|
|
335
|
+
render_dashboard(path)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@cli.command()
|
|
339
|
+
@click.option("--registry", "registry_path", default=None, help="Path to registry.yml")
|
|
340
|
+
@click.option("--output", "output_file", default="docwright-report.html", help="Output HTML file")
|
|
341
|
+
def report(registry_path: str | None, output_file: str) -> None:
|
|
342
|
+
"""Generate a static HTML status report."""
|
|
343
|
+
from docwright.reporters.html import render_html_report
|
|
344
|
+
|
|
345
|
+
reg_path = Path(registry_path) if registry_path else Path.cwd() / ".docwright" / "registry.yml"
|
|
346
|
+
render_html_report(reg_path, Path(output_file))
|
|
347
|
+
click.echo(f"Report saved to {output_file}")
|
|
@@ -6,8 +6,8 @@ from typing import Literal
|
|
|
6
6
|
import yaml
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
9
|
-
CONFIG_DIR = ".
|
|
10
|
-
CONFIG_FILE = "
|
|
9
|
+
CONFIG_DIR = ".docwright"
|
|
10
|
+
CONFIG_FILE = "docwright.yml"
|
|
11
11
|
INITIALIZED_MARKER = ".initialized"
|
|
12
12
|
|
|
13
13
|
|
|
@@ -19,14 +19,16 @@ class ProviderConfig(BaseModel):
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class OutputConfig(BaseModel):
|
|
22
|
-
mode:
|
|
22
|
+
mode: str = "direct"
|
|
23
23
|
pr_title: str = "docs: auto-update documentation"
|
|
24
24
|
branch_prefix: str = "docs/auto-"
|
|
25
|
+
wiki_url: str | None = None
|
|
26
|
+
token_env: str | None = None
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
class TemplatesConfig(BaseModel):
|
|
28
30
|
source: Literal["builtin", "local"] = "builtin"
|
|
29
|
-
local_path: str = ".
|
|
31
|
+
local_path: str = ".docwright/templates"
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
class TriggersConfig(BaseModel):
|
|
@@ -41,7 +43,7 @@ class DocumentConfig(BaseModel):
|
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
class RegistryConfig(BaseModel):
|
|
44
|
-
path: str = "../.
|
|
46
|
+
path: str = "../.docwright/registry.yml"
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
class Config(BaseModel):
|
|
@@ -53,6 +55,16 @@ class Config(BaseModel):
|
|
|
53
55
|
registry: RegistryConfig = Field(default_factory=RegistryConfig)
|
|
54
56
|
language: str = "en"
|
|
55
57
|
|
|
58
|
+
@classmethod
|
|
59
|
+
def migrate_if_needed(cls, repo_root: Path) -> None:
|
|
60
|
+
old = repo_root / ".ai-docgen"
|
|
61
|
+
new = repo_root / ".docwright"
|
|
62
|
+
if old.exists() and not new.exists():
|
|
63
|
+
old.rename(new)
|
|
64
|
+
from rich.console import Console
|
|
65
|
+
|
|
66
|
+
Console().print("[yellow]⚠ Migrated .ai-docgen → .docwright[/yellow]")
|
|
67
|
+
|
|
56
68
|
@classmethod
|
|
57
69
|
def load(cls, repo_root: Path) -> Config:
|
|
58
70
|
config_path = repo_root / CONFIG_DIR / CONFIG_FILE
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from docwright.config import OutputConfig
|
|
7
|
+
from docwright.outputs.base import Output
|
|
8
|
+
from docwright.outputs.direct import DirectOutput
|
|
9
|
+
from docwright.outputs.pull_request import PullRequestOutput
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_output(config: OutputConfig, repo_root: Path) -> Output:
|
|
13
|
+
if config.mode == "direct":
|
|
14
|
+
return DirectOutput(repo_root=repo_root)
|
|
15
|
+
elif config.mode == "gitlab_wiki":
|
|
16
|
+
token = os.getenv(config.token_env or "CI_JOB_TOKEN", "")
|
|
17
|
+
if not token:
|
|
18
|
+
raise OSError("GitLab token not found. Set CI_JOB_TOKEN or configure token_env.")
|
|
19
|
+
if not config.wiki_url:
|
|
20
|
+
raise ValueError("output.wiki_url is required for gitlab_wiki mode.")
|
|
21
|
+
from docwright.outputs.gitlab_wiki import GitLabWikiOutput
|
|
22
|
+
|
|
23
|
+
return GitLabWikiOutput(wiki_url=config.wiki_url, token=token, repo_root=repo_root)
|
|
24
|
+
return PullRequestOutput(repo_root=repo_root, config=config)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from urllib.parse import urlparse, urlunparse
|
|
7
|
+
|
|
8
|
+
from git import Repo
|
|
9
|
+
|
|
10
|
+
from docwright.outputs.base import Output
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GitLabWikiOutput(Output):
|
|
14
|
+
def __init__(self, wiki_url: str, token: str, repo_root: Path) -> None:
|
|
15
|
+
self.wiki_url = wiki_url
|
|
16
|
+
self.token = token
|
|
17
|
+
self.repo_root = repo_root
|
|
18
|
+
|
|
19
|
+
def build_auth_url(self) -> str:
|
|
20
|
+
parsed = urlparse(self.wiki_url)
|
|
21
|
+
auth_netloc = f"oauth2:{self.token}@{parsed.netloc}"
|
|
22
|
+
return urlunparse(parsed._replace(netloc=auth_netloc))
|
|
23
|
+
|
|
24
|
+
def apply(self, changed_files: list[Path], message: str) -> None:
|
|
25
|
+
auth_url = self.build_auth_url()
|
|
26
|
+
tmp_dir = tempfile.mkdtemp()
|
|
27
|
+
try:
|
|
28
|
+
repo = Repo.clone_from(auth_url, tmp_dir)
|
|
29
|
+
for file in changed_files:
|
|
30
|
+
dest = Path(tmp_dir) / file.name
|
|
31
|
+
dest.write_bytes(file.read_bytes())
|
|
32
|
+
repo.index.add("*")
|
|
33
|
+
repo.index.commit(message)
|
|
34
|
+
repo.remotes.origin.push()
|
|
35
|
+
finally:
|
|
36
|
+
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
@@ -28,8 +28,8 @@ jobs:
|
|
|
28
28
|
- uses: actions/setup-python@v5
|
|
29
29
|
with:
|
|
30
30
|
python-version: "3.12"
|
|
31
|
-
- run: pip install
|
|
32
|
-
- run:
|
|
31
|
+
- run: pip install docwright
|
|
32
|
+
- run: docwright run
|
|
33
33
|
env:
|
|
34
34
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
35
35
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
@@ -43,21 +43,21 @@ docs:
|
|
|
43
43
|
stage: docs
|
|
44
44
|
image: python:3.12
|
|
45
45
|
script:
|
|
46
|
-
- pip install
|
|
47
|
-
-
|
|
46
|
+
- pip install docwright
|
|
47
|
+
- docwright run
|
|
48
48
|
variables:
|
|
49
49
|
AI_DOCGEN_BASE_SHA: $CI_MERGE_REQUEST_DIFF_BASE_SHA
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
52
|
MAKEFILE_TARGETS = """
|
|
53
53
|
docs: ## init if not initialized, otherwise update
|
|
54
|
-
\
|
|
54
|
+
\tdocwright run
|
|
55
55
|
|
|
56
56
|
docs-sync: ## force re-sync all docs against current templates
|
|
57
|
-
\
|
|
57
|
+
\tdocwright sync
|
|
58
58
|
|
|
59
59
|
docs-check: ## dry-run: show what would change
|
|
60
|
-
\
|
|
60
|
+
\tdocwright run --dry-run
|
|
61
61
|
"""
|
|
62
62
|
|
|
63
63
|
|
|
@@ -114,9 +114,9 @@ class Scaffolder:
|
|
|
114
114
|
self.write_ci_workflow(profile)
|
|
115
115
|
|
|
116
116
|
def write_config(self, provider_type: str, output_mode: str) -> None:
|
|
117
|
-
config_dir = self.repo_root / ".
|
|
117
|
+
config_dir = self.repo_root / ".docwright"
|
|
118
118
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
119
|
-
config_file = config_dir / "
|
|
119
|
+
config_file = config_dir / "docwright.yml"
|
|
120
120
|
if config_file.exists():
|
|
121
121
|
return
|
|
122
122
|
api_key_env = {
|
|
@@ -138,7 +138,7 @@ class Scaffolder:
|
|
|
138
138
|
"documents": [
|
|
139
139
|
{"type": "readme", "template": "readme/default", "target": "README.md"},
|
|
140
140
|
],
|
|
141
|
-
"registry": {"path": "../.
|
|
141
|
+
"registry": {"path": "../.docwright/registry.yml"},
|
|
142
142
|
}
|
|
143
143
|
config_file.write_text(yaml.dump(config, default_flow_style=False, allow_unicode=True))
|
|
144
144
|
|
|
@@ -159,5 +159,5 @@ class Scaffolder:
|
|
|
159
159
|
elif profile.ci == "gitlab":
|
|
160
160
|
gitlab_ci = self.repo_root / ".gitlab-ci.yml"
|
|
161
161
|
content = gitlab_ci.read_text() if gitlab_ci.exists() else ""
|
|
162
|
-
if "
|
|
162
|
+
if "docwright run" not in content:
|
|
163
163
|
gitlab_ci.write_text(content + GITLAB_CI_BLOCK)
|
docwright-0.1.5/docwright/cli.py
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import os
|
|
5
|
-
import subprocess
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
import click
|
|
9
|
-
|
|
10
|
-
from docwright.config import Config
|
|
11
|
-
from docwright.engine import DocsEngine
|
|
12
|
-
from docwright.outputs.factory import build_output
|
|
13
|
-
from docwright.providers.factory import build_provider
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def get_repo_root() -> Path:
|
|
17
|
-
return Path.cwd()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def build_engine(repo_root: Path) -> DocsEngine:
|
|
21
|
-
config = Config.load(repo_root)
|
|
22
|
-
provider = build_provider(config.provider)
|
|
23
|
-
output = build_output(config.output, repo_root)
|
|
24
|
-
return DocsEngine(repo_root=repo_root, provider=provider, output=output)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@click.group()
|
|
28
|
-
def cli() -> None:
|
|
29
|
-
"""AI-powered documentation agent."""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@cli.command()
|
|
33
|
-
def init() -> None:
|
|
34
|
-
"""Generate documentation from scratch."""
|
|
35
|
-
engine = build_engine(get_repo_root())
|
|
36
|
-
asyncio.run(engine.init())
|
|
37
|
-
click.echo("Documentation initialized.")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@cli.command()
|
|
41
|
-
@click.option("--dry-run", is_flag=True, help="Show what would change without writing files.")
|
|
42
|
-
def run(dry_run: bool) -> None:
|
|
43
|
-
"""Incrementally update documentation based on recent git diff."""
|
|
44
|
-
repo_root = get_repo_root()
|
|
45
|
-
base_sha = os.environ.get("AI_DOCGEN_BASE_SHA", "HEAD~1")
|
|
46
|
-
try:
|
|
47
|
-
diff_text = subprocess.check_output(
|
|
48
|
-
["git", "diff", f"{base_sha}..HEAD"], cwd=repo_root
|
|
49
|
-
).decode()
|
|
50
|
-
except subprocess.CalledProcessError:
|
|
51
|
-
diff_text = ""
|
|
52
|
-
if dry_run:
|
|
53
|
-
config = Config.load(repo_root)
|
|
54
|
-
triggers = config.triggers
|
|
55
|
-
from docwright.analyzer import DiffAnalyzer
|
|
56
|
-
|
|
57
|
-
analyzer = DiffAnalyzer(
|
|
58
|
-
diff_text=diff_text,
|
|
59
|
-
trigger_paths=triggers.paths if triggers else [],
|
|
60
|
-
ignore_paths=triggers.ignore if triggers else [],
|
|
61
|
-
)
|
|
62
|
-
if analyzer.has_relevant_changes():
|
|
63
|
-
click.echo("Relevant changes detected — documentation would be updated.")
|
|
64
|
-
else:
|
|
65
|
-
click.echo("No relevant changes — documentation would be skipped.")
|
|
66
|
-
return
|
|
67
|
-
engine = build_engine(repo_root)
|
|
68
|
-
skipped = asyncio.run(engine.run(diff_text=diff_text))
|
|
69
|
-
click.echo(
|
|
70
|
-
"No relevant changes — documentation up to date." if skipped else "Documentation updated."
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@cli.command()
|
|
75
|
-
def sync() -> None:
|
|
76
|
-
"""Force re-sync all documentation against current templates."""
|
|
77
|
-
engine = build_engine(get_repo_root())
|
|
78
|
-
asyncio.run(engine.sync())
|
|
79
|
-
click.echo("Documentation synced.")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@cli.command("install")
|
|
83
|
-
@click.option("--auto", is_flag=True, help="Non-interactive mode with auto-detected defaults.")
|
|
84
|
-
@click.option("--provider", default=None, type=click.Choice(["claude", "openai", "ollama"]))
|
|
85
|
-
@click.option("--output", "output_mode", default=None, type=click.Choice(["pr", "direct"]))
|
|
86
|
-
def install(auto: bool, provider: str | None, output_mode: str | None) -> None:
|
|
87
|
-
"""Bootstrap this repo with ai-docgen configuration."""
|
|
88
|
-
from docwright.scaffolder import Scaffolder
|
|
89
|
-
|
|
90
|
-
repo_root = get_repo_root()
|
|
91
|
-
scaffolder = Scaffolder(repo_root=repo_root)
|
|
92
|
-
profile = scaffolder.detect_profile()
|
|
93
|
-
|
|
94
|
-
if auto:
|
|
95
|
-
final_provider = provider or "claude"
|
|
96
|
-
final_output = output_mode or "pr"
|
|
97
|
-
else:
|
|
98
|
-
click.echo(
|
|
99
|
-
f"Detected: {profile.language} project '{profile.service_name}', CI: {profile.ci}"
|
|
100
|
-
)
|
|
101
|
-
final_provider = provider or click.prompt(
|
|
102
|
-
"LLM provider",
|
|
103
|
-
type=click.Choice(["claude", "openai", "ollama"]),
|
|
104
|
-
default="claude",
|
|
105
|
-
)
|
|
106
|
-
final_output = output_mode or click.prompt(
|
|
107
|
-
"Output mode",
|
|
108
|
-
type=click.Choice(["pr", "direct"]),
|
|
109
|
-
default="pr",
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
scaffolder.generate(profile, provider_type=final_provider, output_mode=final_output)
|
|
113
|
-
click.echo(f"Installed ai-docgen for '{profile.service_name}'.")
|
|
114
|
-
click.echo("Next: set your API key env var, then run 'make docs'.")
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
@cli.command()
|
|
118
|
-
@click.option("--registry", "registry_path", default=None, help="Path to registry.yml")
|
|
119
|
-
def dashboard(registry_path: str | None) -> None:
|
|
120
|
-
"""Show status of all registered projects."""
|
|
121
|
-
from docwright.reporters.terminal import render_dashboard
|
|
122
|
-
|
|
123
|
-
path = Path(registry_path) if registry_path else Path.cwd() / ".ai-docgen" / "registry.yml"
|
|
124
|
-
render_dashboard(path)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
@cli.command()
|
|
128
|
-
@click.option("--registry", "registry_path", default=None, help="Path to registry.yml")
|
|
129
|
-
@click.option("--output", "output_file", default="ai-docgen-report.html", help="Output HTML file")
|
|
130
|
-
def report(registry_path: str | None, output_file: str) -> None:
|
|
131
|
-
"""Generate a static HTML status report."""
|
|
132
|
-
from docwright.reporters.html import render_html_report
|
|
133
|
-
|
|
134
|
-
reg_path = Path(registry_path) if registry_path else Path.cwd() / ".ai-docgen" / "registry.yml"
|
|
135
|
-
render_html_report(reg_path, Path(output_file))
|
|
136
|
-
click.echo(f"Report saved to {output_file}")
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
from docwright.config import OutputConfig
|
|
4
|
-
from docwright.outputs.base import Output
|
|
5
|
-
from docwright.outputs.direct import DirectOutput
|
|
6
|
-
from docwright.outputs.pull_request import PullRequestOutput
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def build_output(config: OutputConfig, repo_root: Path) -> Output:
|
|
10
|
-
if config.mode == "direct":
|
|
11
|
-
return DirectOutput(repo_root=repo_root)
|
|
12
|
-
return PullRequestOutput(repo_root=repo_root, config=config)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/development-guide.md.j2
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|