project-brain-cli 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.
- project_brain/__init__.py +1 -0
- project_brain/cli.py +644 -0
- project_brain/cli_help.py +52 -0
- project_brain/cli_ui.py +69 -0
- project_brain/config/__init__.py +0 -0
- project_brain/core/__init__.py +0 -0
- project_brain/core/analyzer.py +134 -0
- project_brain/core/config_loader.py +193 -0
- project_brain/core/differ.py +160 -0
- project_brain/core/doctor.py +29 -0
- project_brain/core/doctor_checks/analysis.py +110 -0
- project_brain/core/doctor_checks/environment.py +92 -0
- project_brain/core/doctor_checks/exports.py +70 -0
- project_brain/core/doctor_checks/llm.py +107 -0
- project_brain/core/doctor_checks/models.py +10 -0
- project_brain/core/doctor_checks/repository.py +102 -0
- project_brain/core/explainer.py +334 -0
- project_brain/core/explainer_file.py +136 -0
- project_brain/core/exporter.py +340 -0
- project_brain/core/logger.py +31 -0
- project_brain/core/results.py +163 -0
- project_brain/core/summarizer.py +108 -0
- project_brain/llm/__init__.py +0 -0
- project_brain/llm/provider.py +211 -0
- project_brain/storage/__init__.py +0 -0
- project_brain/storage/storage.py +12 -0
- project_brain_cli-1.0.0.dist-info/METADATA +1185 -0
- project_brain_cli-1.0.0.dist-info/RECORD +32 -0
- project_brain_cli-1.0.0.dist-info/WHEEL +5 -0
- project_brain_cli-1.0.0.dist-info/entry_points.txt +3 -0
- project_brain_cli-1.0.0.dist-info/licenses/LICENSE +11 -0
- project_brain_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
project_brain/cli.py
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
import webbrowser
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
import typer.rich_utils
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
|
|
14
|
+
from project_brain.core.analyzer import analyze_project
|
|
15
|
+
from project_brain.core.config_loader import (DEFAULT_CONFIG, dump_config,
|
|
16
|
+
load_config)
|
|
17
|
+
from project_brain.core.differ import compute_diff, is_git_repo, run_git_command
|
|
18
|
+
from project_brain.core.doctor import run_doctor
|
|
19
|
+
from project_brain.core.explainer import explain_diff
|
|
20
|
+
from project_brain.core.explainer_file import explain_file, explain_function
|
|
21
|
+
from project_brain.core.exporter import (add_code_dir, add_code_file,
|
|
22
|
+
export_code_changes, export_full_code)
|
|
23
|
+
from project_brain.core.results import generate_html
|
|
24
|
+
from project_brain.core.summarizer import format_summary, load_data
|
|
25
|
+
from project_brain.llm.provider import call_llm
|
|
26
|
+
|
|
27
|
+
from project_brain.cli_help import (
|
|
28
|
+
ROOT_HELP,
|
|
29
|
+
PROJECT_HELP,
|
|
30
|
+
DIFF_HELP,
|
|
31
|
+
EXPORT_HELP,
|
|
32
|
+
LLM_HELP,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from project_brain.cli_ui import (
|
|
36
|
+
console,
|
|
37
|
+
section,
|
|
38
|
+
success,
|
|
39
|
+
error,
|
|
40
|
+
info,
|
|
41
|
+
key_value_table,
|
|
42
|
+
doctor_panel,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
typer.rich_utils.STYLE_HELPTEXT = "bold"
|
|
46
|
+
typer.rich_utils.STYLE_OPTIONS_PANEL_BORDER = "cyan"
|
|
47
|
+
typer.rich_utils.STYLE_COMMANDS_PANEL_BORDER = "cyan"
|
|
48
|
+
|
|
49
|
+
def configure_output_encoding():
|
|
50
|
+
for stream in (sys.stdout, sys.stderr):
|
|
51
|
+
if hasattr(stream, "reconfigure"):
|
|
52
|
+
stream.reconfigure(encoding="utf-8") # type: ignore[attr-defined]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
configure_output_encoding()
|
|
56
|
+
|
|
57
|
+
app = typer.Typer(
|
|
58
|
+
help=ROOT_HELP,
|
|
59
|
+
rich_markup_mode="rich",
|
|
60
|
+
no_args_is_help=True,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
project_app = typer.Typer(
|
|
64
|
+
help=PROJECT_HELP,
|
|
65
|
+
no_args_is_help=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
diff_app = typer.Typer(
|
|
69
|
+
help=DIFF_HELP,
|
|
70
|
+
no_args_is_help=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
export_app = typer.Typer(
|
|
74
|
+
help=EXPORT_HELP,
|
|
75
|
+
no_args_is_help=True,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
llm_app = typer.Typer(
|
|
79
|
+
help=LLM_HELP,
|
|
80
|
+
no_args_is_help=True,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
app.add_typer(project_app, name="project")
|
|
84
|
+
app.add_typer(diff_app, name="diff")
|
|
85
|
+
app.add_typer(export_app, name="export")
|
|
86
|
+
app.add_typer(llm_app, name="testllm")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def version_callback(value: bool):
|
|
90
|
+
if value:
|
|
91
|
+
try:
|
|
92
|
+
version = importlib.metadata.version("project-brain")
|
|
93
|
+
except Exception:
|
|
94
|
+
version = "unknown"
|
|
95
|
+
typer.echo(f"project-brain version: {version}")
|
|
96
|
+
raise typer.Exit()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.callback()
|
|
100
|
+
def main_callback(
|
|
101
|
+
version: bool = typer.Option(
|
|
102
|
+
False,
|
|
103
|
+
"--version",
|
|
104
|
+
"-v",
|
|
105
|
+
callback=version_callback,
|
|
106
|
+
is_eager=True,
|
|
107
|
+
help="Show version and exit",
|
|
108
|
+
)
|
|
109
|
+
):
|
|
110
|
+
""" project-brain CLI entrypoint """
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def require_git_repo(root: Path):
|
|
114
|
+
if not is_git_repo(root):
|
|
115
|
+
error(
|
|
116
|
+
"Current directory is not a git repository",
|
|
117
|
+
suggestion="""
|
|
118
|
+
Initialize git:
|
|
119
|
+
|
|
120
|
+
git init
|
|
121
|
+
|
|
122
|
+
Then create first commit:
|
|
123
|
+
|
|
124
|
+
git add .
|
|
125
|
+
git commit -m "initial commit"
|
|
126
|
+
""",
|
|
127
|
+
)
|
|
128
|
+
raise typer.Exit(code=1)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def create_file(path: Path, content: str):
|
|
132
|
+
if path.exists():
|
|
133
|
+
typer.echo(f"⚠️ Skipped (already exists): {path}")
|
|
134
|
+
return False
|
|
135
|
+
path.write_text(content)
|
|
136
|
+
typer.echo(f"✅ Created: {path}")
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@project_app.command()
|
|
141
|
+
def init():
|
|
142
|
+
"""Initialize project-brain in the current directory"""
|
|
143
|
+
cwd = Path.cwd()
|
|
144
|
+
|
|
145
|
+
brain_yaml = cwd / "brain.yaml"
|
|
146
|
+
brain_dir = cwd / ".brain"
|
|
147
|
+
data_json = brain_dir / "data.json"
|
|
148
|
+
index_json = brain_dir / "index.json"
|
|
149
|
+
cache_dir = brain_dir / "cache"
|
|
150
|
+
|
|
151
|
+
created_anything = False
|
|
152
|
+
|
|
153
|
+
if not brain_dir.exists():
|
|
154
|
+
brain_dir.mkdir()
|
|
155
|
+
created_anything = True
|
|
156
|
+
typer.echo(f"✅ Created: {brain_dir}")
|
|
157
|
+
else:
|
|
158
|
+
typer.echo(f"⚠️ Exists: {brain_dir}")
|
|
159
|
+
|
|
160
|
+
if not cache_dir.exists():
|
|
161
|
+
cache_dir.mkdir()
|
|
162
|
+
created_anything = True
|
|
163
|
+
typer.echo(f"✅ Created: {cache_dir}")
|
|
164
|
+
else:
|
|
165
|
+
typer.echo(f"⚠️ Exists: {cache_dir}")
|
|
166
|
+
|
|
167
|
+
created_brain_yaml = create_file(
|
|
168
|
+
brain_yaml, dump_config(DEFAULT_CONFIG)
|
|
169
|
+
)
|
|
170
|
+
created_data_json = create_file(data_json, json.dumps({}, indent=2))
|
|
171
|
+
created_index_json = create_file(index_json, json.dumps({}, indent=2))
|
|
172
|
+
created_anything = (
|
|
173
|
+
created_anything
|
|
174
|
+
or created_brain_yaml
|
|
175
|
+
or created_data_json
|
|
176
|
+
or created_index_json
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if created_anything:
|
|
180
|
+
success(
|
|
181
|
+
"project-brain initialized successfully",
|
|
182
|
+
next_step="brain project analyze .",
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
info("Project already initialized")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@project_app.command()
|
|
189
|
+
def analyze(
|
|
190
|
+
path: str = typer.Argument(
|
|
191
|
+
".",
|
|
192
|
+
help="Repository path to analyze",
|
|
193
|
+
)
|
|
194
|
+
):
|
|
195
|
+
"""
|
|
196
|
+
Analyze repository structure using AST parsing.
|
|
197
|
+
|
|
198
|
+
Extracts:
|
|
199
|
+
- files
|
|
200
|
+
- functions
|
|
201
|
+
- classes
|
|
202
|
+
- metadata
|
|
203
|
+
|
|
204
|
+
Stores results inside:
|
|
205
|
+
.brain/data.json
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
brain project analyze .
|
|
209
|
+
"""
|
|
210
|
+
root = Path(path)
|
|
211
|
+
|
|
212
|
+
section("Project Analysis")
|
|
213
|
+
info(f"Analyzing: {root}")
|
|
214
|
+
|
|
215
|
+
config = load_config(root)
|
|
216
|
+
|
|
217
|
+
analysis_cfg = config.get("analysis", {})
|
|
218
|
+
|
|
219
|
+
ignore = analysis_cfg.get("ignore", [])
|
|
220
|
+
include_tests = analysis_cfg.get("include_tests", False)
|
|
221
|
+
|
|
222
|
+
data, files_path = analyze_project(
|
|
223
|
+
root, ignore_patterns=ignore, include_tests=include_tests
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
brain_dir = root / ".brain"
|
|
227
|
+
brain_dir.mkdir(exist_ok=True)
|
|
228
|
+
|
|
229
|
+
data_path = brain_dir / "data.json"
|
|
230
|
+
data_path.write_text(json.dumps(data, indent=2))
|
|
231
|
+
|
|
232
|
+
formatted_paths = "\n\t\t".join(str(p) for p in files_path)
|
|
233
|
+
typer.echo(f"📋 File Paths: {formatted_paths}")
|
|
234
|
+
success(
|
|
235
|
+
"Analysis complete",
|
|
236
|
+
next_step="brain project summary",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@project_app.command()
|
|
241
|
+
def summary():
|
|
242
|
+
"""Summarize the analyzed data"""
|
|
243
|
+
root = Path.cwd()
|
|
244
|
+
data = load_data(root)
|
|
245
|
+
|
|
246
|
+
if not data:
|
|
247
|
+
error(
|
|
248
|
+
"Project has not been analyzed yet",
|
|
249
|
+
suggestion="""
|
|
250
|
+
Run:
|
|
251
|
+
|
|
252
|
+
brain project analyze .
|
|
253
|
+
""",
|
|
254
|
+
)
|
|
255
|
+
raise typer.Exit(code=1)
|
|
256
|
+
|
|
257
|
+
config = load_config(root)
|
|
258
|
+
fmt = config.get("output", {}).get("format", "text")
|
|
259
|
+
|
|
260
|
+
if fmt == "json":
|
|
261
|
+
|
|
262
|
+
typer.echo(json.dumps(data, indent=2))
|
|
263
|
+
typer.echo(
|
|
264
|
+
"✅ Summary complete (JSON format), its already saved in .brain/data.json"
|
|
265
|
+
)
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
output = format_summary(root, data)
|
|
269
|
+
typer.echo(output)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@diff_app.callback(invoke_without_command=True)
|
|
273
|
+
def diff(ctx: typer.Context):
|
|
274
|
+
"""
|
|
275
|
+
Diff command group
|
|
276
|
+
"""
|
|
277
|
+
if ctx.invoked_subcommand is None:
|
|
278
|
+
typer.echo("❌ Missing subcommand")
|
|
279
|
+
typer.echo("👉 Use: brain diff show or brain diff review")
|
|
280
|
+
raise typer.Exit(code=1)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@diff_app.command()
|
|
284
|
+
def show(
|
|
285
|
+
from_ref: str = typer.Argument(None),
|
|
286
|
+
to_ref: str = typer.Argument(None),
|
|
287
|
+
):
|
|
288
|
+
|
|
289
|
+
# Defaults
|
|
290
|
+
if not from_ref and not to_ref:
|
|
291
|
+
from_ref, to_ref = "HEAD~1", "HEAD"
|
|
292
|
+
|
|
293
|
+
elif from_ref and not to_ref:
|
|
294
|
+
to_ref = "HEAD"
|
|
295
|
+
|
|
296
|
+
root = Path.cwd()
|
|
297
|
+
|
|
298
|
+
require_git_repo(root)
|
|
299
|
+
|
|
300
|
+
config = load_config(root)
|
|
301
|
+
mode = config.get("diff", {}).get("mode", "function")
|
|
302
|
+
|
|
303
|
+
def validate_ref(ref: str):
|
|
304
|
+
return run_git_command(["rev-parse", "--verify", ref], root) is not None
|
|
305
|
+
|
|
306
|
+
if not validate_ref(from_ref) or not validate_ref(to_ref):
|
|
307
|
+
error(
|
|
308
|
+
f"Invalid git reference: {from_ref} {to_ref}",
|
|
309
|
+
suggestion="""
|
|
310
|
+
brain diff show HEAD~1 HEAD
|
|
311
|
+
brain diff show main dev
|
|
312
|
+
|
|
313
|
+
Check refs using:
|
|
314
|
+
git log --oneline
|
|
315
|
+
""",
|
|
316
|
+
)
|
|
317
|
+
raise typer.Exit(code=1)
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
result = compute_diff(from_ref, to_ref, root)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
typer.echo(f"❌ Diff failed: {str(e)}")
|
|
323
|
+
raise typer.Exit(code=1)
|
|
324
|
+
|
|
325
|
+
if result is None:
|
|
326
|
+
typer.echo("❌ Failed to compute diff")
|
|
327
|
+
raise typer.Exit(code=1)
|
|
328
|
+
|
|
329
|
+
added = result["added"]
|
|
330
|
+
modified = result["modified"]
|
|
331
|
+
deleted = result["deleted"]
|
|
332
|
+
|
|
333
|
+
typer.echo(f"Files Changed: {len(added) + len(modified) + len(deleted)}\n")
|
|
334
|
+
|
|
335
|
+
section("Modified Files")
|
|
336
|
+
|
|
337
|
+
if modified:
|
|
338
|
+
for f in modified:
|
|
339
|
+
console.print(f"[yellow]•[/yellow] {f}")
|
|
340
|
+
else:
|
|
341
|
+
info("No modified files")
|
|
342
|
+
|
|
343
|
+
section("Added Files")
|
|
344
|
+
if added:
|
|
345
|
+
for f in added:
|
|
346
|
+
console.print(f"[green]•[/green] {f}")
|
|
347
|
+
else:
|
|
348
|
+
info("No added files")
|
|
349
|
+
|
|
350
|
+
section("Deleted Files")
|
|
351
|
+
if deleted:
|
|
352
|
+
for f in deleted:
|
|
353
|
+
console.print(f"[red]•[/red] {f}")
|
|
354
|
+
else:
|
|
355
|
+
info("No deleted files")
|
|
356
|
+
|
|
357
|
+
typer.echo("* None")
|
|
358
|
+
|
|
359
|
+
if mode == "file":
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
# Function-level diff
|
|
363
|
+
for fd in result["function_diffs"]:
|
|
364
|
+
typer.echo(f"\nFile: {fd['file']}\n")
|
|
365
|
+
|
|
366
|
+
typer.echo("Functions Added:\n")
|
|
367
|
+
for fn in fd["added"]:
|
|
368
|
+
typer.echo(f"* {fn}")
|
|
369
|
+
if not fd["added"]:
|
|
370
|
+
typer.echo("* None")
|
|
371
|
+
|
|
372
|
+
typer.echo("\nFunctions Removed:\n")
|
|
373
|
+
for fn in fd["removed"]:
|
|
374
|
+
typer.echo(f"* {fn}")
|
|
375
|
+
if not fd["removed"]:
|
|
376
|
+
typer.echo("* None")
|
|
377
|
+
|
|
378
|
+
typer.echo("\nFunctions Modified:\n")
|
|
379
|
+
for fn in fd["modified"]:
|
|
380
|
+
typer.echo(f"* {fn}")
|
|
381
|
+
if not fd["modified"]:
|
|
382
|
+
typer.echo("* None")
|
|
383
|
+
|
|
384
|
+
typer.echo("")
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@diff_app.command()
|
|
388
|
+
def review(
|
|
389
|
+
from_ref: str = typer.Argument(None),
|
|
390
|
+
to_ref: str = typer.Argument(None),
|
|
391
|
+
):
|
|
392
|
+
"""
|
|
393
|
+
Explain code changes using LLM
|
|
394
|
+
"""
|
|
395
|
+
if not from_ref and not to_ref:
|
|
396
|
+
from_ref, to_ref = "HEAD~1", "HEAD"
|
|
397
|
+
|
|
398
|
+
elif from_ref and not to_ref:
|
|
399
|
+
to_ref = "HEAD"
|
|
400
|
+
|
|
401
|
+
root = Path.cwd()
|
|
402
|
+
|
|
403
|
+
require_git_repo(root)
|
|
404
|
+
def validate_ref(ref: str):
|
|
405
|
+
return run_git_command(["rev-parse", "--verify", ref], root) is not None
|
|
406
|
+
|
|
407
|
+
if not validate_ref(from_ref) or not validate_ref(to_ref):
|
|
408
|
+
error(
|
|
409
|
+
f"Invalid git reference: {from_ref} {to_ref}",
|
|
410
|
+
suggestion="""
|
|
411
|
+
brain diff show HEAD~1 HEAD
|
|
412
|
+
brain diff show main dev
|
|
413
|
+
|
|
414
|
+
Check refs using:
|
|
415
|
+
git log --oneline
|
|
416
|
+
""",
|
|
417
|
+
)
|
|
418
|
+
typer.echo(f" From: {from_ref}")
|
|
419
|
+
typer.echo(f" To: {to_ref}")
|
|
420
|
+
raise typer.Exit(code=1)
|
|
421
|
+
results = explain_diff(from_ref, to_ref, root)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
if not results:
|
|
425
|
+
typer.echo("❌ Failed to compute explain-diff")
|
|
426
|
+
raise typer.Exit(code=1)
|
|
427
|
+
|
|
428
|
+
reports_dir = root / ".brain" / "reports"
|
|
429
|
+
reports_dir.mkdir(parents=True, exist_ok=True)
|
|
430
|
+
|
|
431
|
+
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M")
|
|
432
|
+
|
|
433
|
+
json_path = reports_dir / f"diff_{timestamp}.json"
|
|
434
|
+
html_path = reports_dir / f"diff_{timestamp}.html"
|
|
435
|
+
json_path.write_text(json.dumps(results, indent=2), encoding="utf-8")
|
|
436
|
+
html_path.write_text(generate_html(results), encoding="utf-8")
|
|
437
|
+
|
|
438
|
+
typer.echo("\n✅ Analysis complete\n")
|
|
439
|
+
typer.echo(f"📄 JSON: {json_path}")
|
|
440
|
+
typer.echo(f"🌐 HTML: {html_path}")
|
|
441
|
+
|
|
442
|
+
webbrowser.open(str(html_path))
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@project_app.command()
|
|
447
|
+
def doctor():
|
|
448
|
+
"""
|
|
449
|
+
Repository diagnostics and environment health checks.
|
|
450
|
+
"""
|
|
451
|
+
|
|
452
|
+
root = Path.cwd()
|
|
453
|
+
|
|
454
|
+
checks, final_status = run_doctor(root)
|
|
455
|
+
|
|
456
|
+
console.print(
|
|
457
|
+
Panel.fit(
|
|
458
|
+
"[bold cyan]🩺 Project Brain Diagnostics[/bold cyan]",
|
|
459
|
+
border_style="cyan",
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
grouped = {}
|
|
464
|
+
|
|
465
|
+
for check in checks:
|
|
466
|
+
grouped.setdefault(check.category, []).append(check)
|
|
467
|
+
|
|
468
|
+
for category, items in grouped.items():
|
|
469
|
+
rows = []
|
|
470
|
+
|
|
471
|
+
for item in items:
|
|
472
|
+
|
|
473
|
+
if item.status == "pass":
|
|
474
|
+
status = "[green]PASS[/green]"
|
|
475
|
+
elif item.status == "warn":
|
|
476
|
+
status = "[yellow]WARN[/yellow]"
|
|
477
|
+
elif item.status == "fail":
|
|
478
|
+
status = "[red]FAIL[/red]"
|
|
479
|
+
else:
|
|
480
|
+
status = "[cyan]INFO[/cyan]"
|
|
481
|
+
|
|
482
|
+
detail = item.message
|
|
483
|
+
|
|
484
|
+
if item.fix:
|
|
485
|
+
detail += f"\n[dim]{item.fix}[/dim]"
|
|
486
|
+
|
|
487
|
+
rows.append(
|
|
488
|
+
(
|
|
489
|
+
item.name,
|
|
490
|
+
status,
|
|
491
|
+
detail,
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
doctor_panel(category, rows)
|
|
496
|
+
|
|
497
|
+
if final_status == "READY":
|
|
498
|
+
success(
|
|
499
|
+
"Repository is fully operational",
|
|
500
|
+
next_step="brain diff review",
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
elif final_status == "PARTIAL":
|
|
504
|
+
info("Repository partially configured")
|
|
505
|
+
|
|
506
|
+
else:
|
|
507
|
+
error(
|
|
508
|
+
"Repository is not ready",
|
|
509
|
+
suggestion="""
|
|
510
|
+
brain project init
|
|
511
|
+
brain project analyze .
|
|
512
|
+
""",
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
@export_app.command()
|
|
516
|
+
def full_code():
|
|
517
|
+
"""
|
|
518
|
+
Export entire codebase into structured file
|
|
519
|
+
"""
|
|
520
|
+
root = Path.cwd()
|
|
521
|
+
|
|
522
|
+
count, output_path, files_path = export_full_code(root)
|
|
523
|
+
|
|
524
|
+
success(
|
|
525
|
+
f"Exported {count} files",
|
|
526
|
+
next_step="brain export code_changes HEAD~1 HEAD",
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
info(f"Output: {output_path}")
|
|
530
|
+
formatted_paths = "\n\t\t".join(files_path)
|
|
531
|
+
typer.echo(f"📋 File Paths: {formatted_paths}")
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@export_app.command("file")
|
|
535
|
+
def add_code_file_cmd(path: str):
|
|
536
|
+
"""
|
|
537
|
+
Manually add a single file to export
|
|
538
|
+
"""
|
|
539
|
+
root = Path.cwd()
|
|
540
|
+
target = Path(path)
|
|
541
|
+
|
|
542
|
+
count, output_path, msg = add_code_file(root, target)
|
|
543
|
+
|
|
544
|
+
if msg:
|
|
545
|
+
typer.echo(msg)
|
|
546
|
+
|
|
547
|
+
typer.echo(f"📦 Files added: {count}")
|
|
548
|
+
typer.echo(f"📄 Output: {output_path}")
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@export_app.command("dir")
|
|
552
|
+
def add_code_dir_cmd(path: str):
|
|
553
|
+
"""
|
|
554
|
+
Manually add a directory to export
|
|
555
|
+
"""
|
|
556
|
+
root = Path.cwd()
|
|
557
|
+
target = Path(path)
|
|
558
|
+
|
|
559
|
+
count, output_path, msg = add_code_dir(root, target)
|
|
560
|
+
|
|
561
|
+
if msg:
|
|
562
|
+
typer.echo(msg)
|
|
563
|
+
|
|
564
|
+
typer.echo(f"📦 Files added: {count}")
|
|
565
|
+
typer.echo(f"📄 Output: {output_path}")
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
@export_app.command()
|
|
569
|
+
def code_changes(from_ref: str, to_ref: str):
|
|
570
|
+
"""
|
|
571
|
+
Export changed code between two git references
|
|
572
|
+
"""
|
|
573
|
+
root = Path.cwd()
|
|
574
|
+
|
|
575
|
+
count, output_path = export_code_changes(root, from_ref, to_ref)
|
|
576
|
+
|
|
577
|
+
typer.echo(f"📦 Files processed: {count}")
|
|
578
|
+
typer.echo(f"📄 Output: {output_path}")
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@diff_app.command()
|
|
582
|
+
def explain(target: str):
|
|
583
|
+
"""
|
|
584
|
+
Explain a file or function
|
|
585
|
+
"""
|
|
586
|
+
root = Path.cwd()
|
|
587
|
+
|
|
588
|
+
if ":" in target:
|
|
589
|
+
file_path, func_name = target.split(":", 1)
|
|
590
|
+
output = explain_function(root, file_path, func_name)
|
|
591
|
+
else:
|
|
592
|
+
output = explain_file(root, target)
|
|
593
|
+
|
|
594
|
+
typer.echo(output)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
@llm_app.command()
|
|
598
|
+
def test():
|
|
599
|
+
root = Path.cwd()
|
|
600
|
+
config = load_config(root)
|
|
601
|
+
|
|
602
|
+
llm = config.get("llm", {})
|
|
603
|
+
provider = llm.get("provider", "none")
|
|
604
|
+
model = llm.get("model", "")
|
|
605
|
+
timeout = llm.get("timeout_sec", 60)
|
|
606
|
+
|
|
607
|
+
if provider == "none":
|
|
608
|
+
info("LLM disabled (provider=none)")
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
result = call_llm(
|
|
612
|
+
provider,
|
|
613
|
+
model,
|
|
614
|
+
"What is 2 + 2?",
|
|
615
|
+
api_key="",
|
|
616
|
+
include_models=True,
|
|
617
|
+
timeout=timeout
|
|
618
|
+
)
|
|
619
|
+
print(f"LLM Call - Provider: {provider}, Model: {model}, Timeout: {timeout}s")
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
if result["error"]:
|
|
623
|
+
error(
|
|
624
|
+
f"LLM test failed: {result['error']}",
|
|
625
|
+
suggestion="""
|
|
626
|
+
Check:
|
|
627
|
+
- provider name
|
|
628
|
+
- model name
|
|
629
|
+
- API keys
|
|
630
|
+
- internet connectivity
|
|
631
|
+
""",
|
|
632
|
+
)
|
|
633
|
+
return
|
|
634
|
+
|
|
635
|
+
typer.echo(f"✅ Output: {result['output']}")
|
|
636
|
+
typer.echo(f"📦 Models: {result['models'][:5]}")
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def main():
|
|
640
|
+
app()
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
if __name__ == "__main__":
|
|
644
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
ROOT_HELP = """
|
|
2
|
+
🧠 project-brain
|
|
3
|
+
|
|
4
|
+
Developer intelligence CLI for understanding repositories,
|
|
5
|
+
Git changes, and AI-assisted code analysis.
|
|
6
|
+
|
|
7
|
+
COMMON WORKFLOW
|
|
8
|
+
|
|
9
|
+
1. Initialize project
|
|
10
|
+
brain project init
|
|
11
|
+
|
|
12
|
+
2. Analyze repository
|
|
13
|
+
brain project analyze .
|
|
14
|
+
|
|
15
|
+
3. View project summary
|
|
16
|
+
brain project summary
|
|
17
|
+
|
|
18
|
+
4. Inspect code changes
|
|
19
|
+
brain diff show
|
|
20
|
+
|
|
21
|
+
5. Generate semantic review
|
|
22
|
+
brain diff review
|
|
23
|
+
|
|
24
|
+
POPULAR COMMANDS
|
|
25
|
+
|
|
26
|
+
project Repository analysis and diagnostics
|
|
27
|
+
diff Git-aware change intelligence
|
|
28
|
+
export AI-friendly code exports
|
|
29
|
+
testllm Validate LLM connectivity
|
|
30
|
+
|
|
31
|
+
EXAMPLES
|
|
32
|
+
|
|
33
|
+
brain diff show HEAD~3 HEAD
|
|
34
|
+
brain export full_code
|
|
35
|
+
brain diff explain src/api.py:create_user
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
PROJECT_HELP = """
|
|
39
|
+
Project analysis and repository management commands.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
DIFF_HELP = """
|
|
43
|
+
Git-aware repository change analysis.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
EXPORT_HELP = """
|
|
47
|
+
Export repository content into AI-friendly formats.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
LLM_HELP = """
|
|
51
|
+
LLM provider testing and diagnostics.
|
|
52
|
+
"""
|