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.
Files changed (44) hide show
  1. {docwright-0.1.5 → docwright-0.1.7}/PKG-INFO +1 -1
  2. docwright-0.1.7/docwright/cli.py +347 -0
  3. {docwright-0.1.5 → docwright-0.1.7}/docwright/config.py +17 -5
  4. docwright-0.1.7/docwright/outputs/factory.py +24 -0
  5. docwright-0.1.7/docwright/outputs/gitlab_wiki.py +36 -0
  6. {docwright-0.1.5 → docwright-0.1.7}/docwright/scaffolder.py +11 -11
  7. {docwright-0.1.5 → docwright-0.1.7}/pyproject.toml +1 -1
  8. docwright-0.1.5/docwright/cli.py +0 -136
  9. docwright-0.1.5/docwright/outputs/factory.py +0 -12
  10. {docwright-0.1.5 → docwright-0.1.7}/LICENSE +0 -0
  11. {docwright-0.1.5 → docwright-0.1.7}/README.md +0 -0
  12. {docwright-0.1.5 → docwright-0.1.7}/docwright/__init__.py +0 -0
  13. {docwright-0.1.5 → docwright-0.1.7}/docwright/analyzer.py +0 -0
  14. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/__init__.py +0 -0
  15. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/readme/__init__.py +0 -0
  16. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/readme/default.md.j2 +0 -0
  17. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/__init__.py +0 -0
  18. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/adr.md.j2 +0 -0
  19. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/api-contracts.md.j2 +0 -0
  20. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/architecture.md.j2 +0 -0
  21. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/data-model.md.j2 +0 -0
  22. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/db-schema.md.j2 +0 -0
  23. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/development-guide.md.j2 +0 -0
  24. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/home.md.j2 +0 -0
  25. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/integrations.md.j2 +0 -0
  26. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/operations.md.j2 +0 -0
  27. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/security.md.j2 +0 -0
  28. {docwright-0.1.5 → docwright-0.1.7}/docwright/built_in_templates/wiki/troubleshooting.md.j2 +0 -0
  29. {docwright-0.1.5 → docwright-0.1.7}/docwright/engine.py +0 -0
  30. {docwright-0.1.5 → docwright-0.1.7}/docwright/outputs/__init__.py +0 -0
  31. {docwright-0.1.5 → docwright-0.1.7}/docwright/outputs/base.py +0 -0
  32. {docwright-0.1.5 → docwright-0.1.7}/docwright/outputs/direct.py +0 -0
  33. {docwright-0.1.5 → docwright-0.1.7}/docwright/outputs/pull_request.py +0 -0
  34. {docwright-0.1.5 → docwright-0.1.7}/docwright/providers/__init__.py +0 -0
  35. {docwright-0.1.5 → docwright-0.1.7}/docwright/providers/base.py +0 -0
  36. {docwright-0.1.5 → docwright-0.1.7}/docwright/providers/claude.py +0 -0
  37. {docwright-0.1.5 → docwright-0.1.7}/docwright/providers/factory.py +0 -0
  38. {docwright-0.1.5 → docwright-0.1.7}/docwright/providers/ollama.py +0 -0
  39. {docwright-0.1.5 → docwright-0.1.7}/docwright/providers/openai.py +0 -0
  40. {docwright-0.1.5 → docwright-0.1.7}/docwright/registry.py +0 -0
  41. {docwright-0.1.5 → docwright-0.1.7}/docwright/renderer.py +0 -0
  42. {docwright-0.1.5 → docwright-0.1.7}/docwright/reporters/__init__.py +0 -0
  43. {docwright-0.1.5 → docwright-0.1.7}/docwright/reporters/html.py +0 -0
  44. {docwright-0.1.5 → docwright-0.1.7}/docwright/reporters/terminal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docwright
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: AI-powered documentation agent: auto-generates and maintains README & wiki on every commit
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -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 = ".ai-docgen"
10
- CONFIG_FILE = "ai-docgen.yml"
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: Literal["direct", "pr"] = "pr"
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 = ".ai-docgen/templates"
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 = "../.ai-docgen/registry.yml"
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 ai-docgen
32
- - run: ai-docgen 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 ai-docgen
47
- - ai-docgen run
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
- \tai-docgen run
54
+ \tdocwright run
55
55
 
56
56
  docs-sync: ## force re-sync all docs against current templates
57
- \tai-docgen sync
57
+ \tdocwright sync
58
58
 
59
59
  docs-check: ## dry-run: show what would change
60
- \tai-docgen run --dry-run
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 / ".ai-docgen"
117
+ config_dir = self.repo_root / ".docwright"
118
118
  config_dir.mkdir(parents=True, exist_ok=True)
119
- config_file = config_dir / "ai-docgen.yml"
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": "../.ai-docgen/registry.yml"},
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 "ai-docgen run" not in content:
162
+ if "docwright run" not in content:
163
163
  gitlab_ci.write_text(content + GITLAB_CI_BLOCK)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "docwright"
3
- version = "0.1.5"
3
+ version = "0.1.7"
4
4
  description = "AI-powered documentation agent: auto-generates and maintains README & wiki on every commit"
5
5
  authors = ["Artem Gotlib <gotlib.artem.m@gmail.com>"]
6
6
  license = "MIT"
@@ -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