codigest 0.3.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.
codigest/__init__.py ADDED
File without changes
@@ -0,0 +1,84 @@
1
+ import typer
2
+ import pyperclip
3
+ from pathlib import Path
4
+ from rich.console import Console
5
+ from rich.progress import Progress, SpinnerColumn, TextColumn
6
+
7
+ from ..core import scanner, prompts, shadow
8
+ from .scan import _load_config_filters
9
+
10
+ app = typer.Typer()
11
+ console = Console()
12
+
13
+ @app.callback(invoke_without_command=True)
14
+ def handle(
15
+ copy: bool = typer.Option(True, help="Auto-copy to clipboard"),
16
+ save: bool = typer.Option(True, help="Save to .codigest/changes.diff"),
17
+ message: str = typer.Option("", "--message", "-m", help="Add specific instruction context"),
18
+ ):
19
+ """
20
+ [Context Update] Shows changes since the last 'codigest scan'.
21
+ Useful for updating LLM context without re-uploading everything.
22
+ """
23
+ root_path = Path.cwd()
24
+ anchor = shadow.ContextAnchor(root_path)
25
+
26
+ # 1. Check Baseline Existence
27
+ last_update = anchor.get_last_update_time()
28
+ if last_update == "Never":
29
+ console.print("[yellow]⚠️ No scan history found.[/yellow]")
30
+ console.print(" Run [bold cyan]codigest scan[/bold cyan] first to establish a baseline.")
31
+ raise typer.Exit(1)
32
+
33
+ console.print(f"[dim]Checking changes since last scan ({last_update})...[/dim]")
34
+
35
+ # 2. Calculate Diff
36
+ with Progress(
37
+ SpinnerColumn(),
38
+ TextColumn("[bold blue]Analyzing changes...[/bold blue]"),
39
+ transient=True,
40
+ console=console
41
+ ) as progress:
42
+ task = progress.add_task("diff", total=None)
43
+
44
+ # Load same filters as scan
45
+ extensions, extra_ignores = _load_config_filters(root_path)
46
+
47
+ # Get current valid files (Working Tree)
48
+ current_files = scanner.scan_project(root_path, extensions, extra_ignores)
49
+
50
+ # Compare Working Tree vs Anchor
51
+ diff_content = anchor.get_changes(current_files)
52
+
53
+ progress.update(task, completed=100)
54
+
55
+ if not diff_content.strip():
56
+ console.print("[green]✨ No changes detected since last scan.[/green]")
57
+ return
58
+
59
+ # 3. Render
60
+ prompt_engine = prompts.get_engine(root_path)
61
+ try:
62
+ formatted_diff = prompt_engine.render(
63
+ "diff",
64
+ project_name=root_path.name,
65
+ context_message=f"Changes since last scan ({last_update})",
66
+ diff_content=diff_content,
67
+ instruction=message
68
+ )
69
+ except Exception:
70
+ # Fallback if template fails
71
+ formatted_diff = f"<diff>\n{diff_content}\n</diff>"
72
+
73
+ # 4. Output
74
+ console.print(f"[bold green]✔ Changes Detected![/bold green] ({len(formatted_diff)} chars)")
75
+
76
+ if copy:
77
+ pyperclip.copy(formatted_diff)
78
+ console.print("[dim]📋 Copied to clipboard[/dim]")
79
+
80
+ if save:
81
+ out_path = root_path / ".codigest" / "changes.diff"
82
+ out_path.parent.mkdir(exist_ok=True)
83
+ out_path.write_text(formatted_diff, encoding="utf-8")
84
+ console.print(f"[dim]💾 Saved to {out_path}[/dim]")
@@ -0,0 +1,88 @@
1
+ import typer
2
+ import pyperclip
3
+ from pathlib import Path
4
+ from rich.console import Console
5
+ from rich.progress import Progress, SpinnerColumn, TextColumn
6
+
7
+ from ..core import scanner, structure, prompts, semdiff, tags, tokenizer
8
+
9
+ # Reuse config loader
10
+ from .scan import _load_config_filters
11
+
12
+ app = typer.Typer()
13
+ console = Console()
14
+
15
+ @app.callback(invoke_without_command=True)
16
+ def handle(
17
+ target: Path = typer.Argument(Path.cwd(), help="Target directory"),
18
+ copy: bool = typer.Option(True, help="Auto-copy to clipboard"),
19
+ save: bool = typer.Option(True, help="Save to .codigest/digest.xml"),
20
+ message: str = typer.Option("", "--message", "-m", help="Add specific instruction"),
21
+ ):
22
+ """
23
+ [Architectural View] Summarizes the codebase structure (Classes/Functions only).
24
+ """
25
+ root_path = target.resolve()
26
+
27
+ prompt_engine = prompts.get_engine(root_path)
28
+ extensions, extra_ignores = _load_config_filters(root_path)
29
+
30
+ with Progress(
31
+ SpinnerColumn(),
32
+ TextColumn("[bold blue]Digesting Architecture...[/bold blue]"),
33
+ transient=True,
34
+ console=console
35
+ ) as progress:
36
+
37
+ task = progress.add_task("digest", total=None)
38
+
39
+ files = scanner.scan_project(root_path, extensions, extra_ignores)
40
+ tree_str = structure.generate_ascii_tree(files, root_path)
41
+
42
+ summary_blocks = []
43
+ for file_path in files:
44
+ # Only summarize Python files via AST
45
+ if file_path.suffix in (".py", ".pyi"):
46
+ try:
47
+ content = file_path.read_text(encoding="utf-8")
48
+ summary = semdiff.summarize(content)
49
+
50
+ if summary:
51
+ rel_path = file_path.relative_to(root_path).as_posix()
52
+ block = tags.xml(t"""
53
+ <file path="{rel_path}">
54
+ {summary}
55
+ </file>
56
+ """)
57
+ summary_blocks.append(block)
58
+ except Exception:
59
+ continue
60
+
61
+ summary_blob = "\n".join(summary_blocks)
62
+
63
+ try:
64
+ digest_content = prompt_engine.render(
65
+ "digest",
66
+ project_name=root_path.name,
67
+ tree_structure=tree_str,
68
+ digest_content=summary_blob,
69
+ instruction=message
70
+ )
71
+ except Exception as e:
72
+ console.print(f"[red]❌ Rendering Failed:[/red] {e}")
73
+ raise typer.Exit(1)
74
+
75
+ progress.update(task, completed=100)
76
+
77
+ token_count = tokenizer.estimate_tokens(digest_content)
78
+ console.print(f"[bold green]✔ Digest Generated![/bold green] ([bold cyan]~{token_count:,} Tokens[/bold cyan])")
79
+
80
+ if copy:
81
+ pyperclip.copy(digest_content)
82
+ console.print("[dim]📋 Copied to clipboard[/dim]")
83
+
84
+ if save:
85
+ out_path = root_path / ".codigest" / "digest.xml"
86
+ out_path.parent.mkdir(exist_ok=True)
87
+ out_path.write_text(digest_content, encoding="utf-8")
88
+ console.print(f"[dim]💾 Saved to {out_path}[/dim]")
@@ -0,0 +1,87 @@
1
+ import typer
2
+ from pathlib import Path
3
+ from rich.console import Console
4
+ from rich.panel import Panel
5
+
6
+ from ..core import scanner, shadow
7
+
8
+ app = typer.Typer()
9
+ console = Console()
10
+
11
+ DEFAULT_CONFIG = """# Codigest Configuration File
12
+ [project]
13
+ description = "Auto-generated context configuration"
14
+
15
+ [filter]
16
+ max_file_size_kb = 100
17
+ extensions = [
18
+ ".py", ".pyi",
19
+ ".ts", ".tsx", ".js", ".jsx",
20
+ ".json", ".html", ".css",
21
+ ".md", ".toml", ".yaml", ".yml", ".xml"
22
+ ]
23
+ exclude_patterns = [
24
+ "*.lock",
25
+ "dist/",
26
+ "build/",
27
+ "node_modules/",
28
+ "__pycache__/"
29
+ ]
30
+
31
+ [output]
32
+ format = "xml"
33
+ structure = "toon"
34
+ """
35
+
36
+ @app.callback(invoke_without_command=True)
37
+ def handle(
38
+ force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
39
+ ):
40
+ """
41
+ Initialize the .codigest environment and capture the initial state.
42
+ """
43
+ root_path = Path.cwd()
44
+ config_dir = root_path / ".codigest"
45
+ config_file = config_dir / "config.toml"
46
+ gitignore_file = root_path / ".gitignore"
47
+
48
+ console.print(Panel(f"[bold blue]Initializing Codigest[/bold blue]\n📂 Location: {root_path}", expand=False))
49
+
50
+ # 1. Config Setup (기존 로직)
51
+ if not config_dir.exists():
52
+ config_dir.mkdir()
53
+ console.print(" [green]✔[/green] Created [bold].codigest/[/bold] directory")
54
+
55
+ if not config_file.exists() or force:
56
+ config_file.write_text(DEFAULT_CONFIG, encoding="utf-8")
57
+ console.print(" [green]✔[/green] Created [bold]config.toml[/bold]")
58
+ else:
59
+ console.print(" [yellow]![/yellow] Config exists. Use --force to overwrite.")
60
+
61
+ _update_gitignore(gitignore_file)
62
+
63
+ # 2. [NEW] Initial Anchoring (Shadow Git)
64
+ # "프로그램 시작 시에도 Anchor 하나 잡고 시작"
65
+ console.print("\n[dim]Creating initial context anchor...[/dim]")
66
+ try:
67
+ # Default scan settings for initialization
68
+ files = scanner.scan_project(root_path)
69
+ anchor = shadow.ContextAnchor(root_path)
70
+ anchor.update(files)
71
+ console.print(" [green]✔[/green] Baseline snapshot captured.")
72
+ except Exception as e:
73
+ console.print(f" [red]⚠️ Failed to create initial anchor: {e}[/red]")
74
+
75
+ console.print("\n[bold green]✨ Ready to digest![/bold green]")
76
+ console.print("Try running: [cyan]codigest diff[/cyan] to see changes from now on.")
77
+
78
+ def _update_gitignore(path: Path):
79
+ entry = ".codigest/"
80
+ if not path.exists():
81
+ path.write_text(f"# Git Ignore\n{entry}\n", encoding="utf-8")
82
+ return
83
+
84
+ if entry not in path.read_text(encoding="utf-8"):
85
+ with path.open("a", encoding="utf-8") as f:
86
+ f.write(f"\n{entry}\n")
87
+ console.print(" [green]✔[/green] Added [bold].codigest/[/bold] to .gitignore")
@@ -0,0 +1,142 @@
1
+ import typer
2
+ import tomllib
3
+ from pathlib import Path
4
+ from rich.console import Console
5
+ from rich.progress import Progress, SpinnerColumn, TextColumn
6
+
7
+ from ..core import scanner, structure, tags, prompts, processor, shadow, tokenizer
8
+
9
+ app = typer.Typer()
10
+ console = Console()
11
+
12
+ def _load_config_filters(root_path: Path):
13
+ config_path = root_path / ".codigest" / "config.toml"
14
+ extensions = None
15
+ exclude_patterns = []
16
+
17
+ if config_path.exists():
18
+ try:
19
+ with open(config_path, "rb") as f:
20
+ data = tomllib.load(f)
21
+ filters = data.get("filter", {})
22
+ ext_list = filters.get("extensions", [])
23
+ if ext_list:
24
+ extensions = set(ext_list)
25
+ exclude_patterns = filters.get("exclude_patterns", [])
26
+ except Exception:
27
+ pass
28
+
29
+ return extensions, exclude_patterns
30
+
31
+ @app.callback(invoke_without_command=True)
32
+ def handle(
33
+ target: Path = typer.Argument(Path.cwd(), help="Target directory"),
34
+ output: str = typer.Option("snapshot.xml", help="Output filename inside .codigest/"),
35
+ all: bool = typer.Option(False, "--all", "-a", help="Ignore config filters"),
36
+ message: str = typer.Option("", "--message", "-m", help="Add specific instruction"),
37
+ line_numbers: bool = typer.Option(False, "--lines", "-l", help="Add line numbers to code blocks"),
38
+ ):
39
+ """
40
+ [Legacy Mode] Scans the codebase using the PromptEngine.
41
+ """
42
+ root_path = target.resolve()
43
+ artifact_dir = root_path / ".codigest"
44
+
45
+ if not artifact_dir.exists():
46
+ console.print("[yellow]⚠️ .codigest directory missing. Running init...[/yellow]")
47
+ try:
48
+ artifact_dir.mkdir(exist_ok=True)
49
+ except PermissionError:
50
+ console.print(f"[red]❌ Error: Cannot create .codigest at {root_path}[/red]")
51
+ raise typer.Exit(1)
52
+
53
+ output_path = artifact_dir / output
54
+
55
+ # Initialize Engines
56
+ prompt_engine = prompts.get_engine(root_path)
57
+ anchor = shadow.ContextAnchor(root_path)
58
+
59
+ extensions, extra_ignores = (None, [])
60
+ if not all:
61
+ extensions, extra_ignores = _load_config_filters(root_path)
62
+
63
+ with Progress(
64
+ SpinnerColumn(),
65
+ TextColumn("[bold blue]Scanning Project...[/bold blue]"),
66
+ transient=True,
67
+ console=console
68
+ ):
69
+
70
+ # File Discovery
71
+ files = scanner.scan_project(root_path, extensions, extra_ignores)
72
+
73
+ # Pre-scan Diff Report
74
+ if anchor.has_history():
75
+ try:
76
+ diff_content = anchor.get_changes(files)
77
+ if diff_content.strip():
78
+ pre_diff_path = artifact_dir / "previous_changes.diff"
79
+ pre_diff_path.write_text(diff_content, encoding="utf-8")
80
+ except Exception:
81
+ pass
82
+
83
+ # Structure Generation
84
+ tree_str = structure.generate_ascii_tree(files, root_path)
85
+
86
+ # Content Processing
87
+ file_blocks = []
88
+ for file_path in files:
89
+ rel_path = file_path.relative_to(root_path).as_posix()
90
+ try:
91
+ content = processor.read_file_content(file_path, add_line_numbers=line_numbers)
92
+ block = tags.xml(t"""
93
+ <file path="{rel_path}">
94
+ {content}
95
+ </file>
96
+ """)
97
+ file_blocks.append(block)
98
+ except Exception:
99
+ continue
100
+
101
+ source_code_blob = "\n\n".join(file_blocks)
102
+
103
+ # Final Assembly
104
+ try:
105
+ snapshot_content = prompt_engine.render(
106
+ "snapshot",
107
+ project_name=root_path.name,
108
+ tree_structure=tree_str,
109
+ source_code=source_code_blob,
110
+ instruction=message
111
+ )
112
+ except Exception as e:
113
+ console.print(f"[red]❌ Template Rendering Failed:[/red] {e}")
114
+ raise typer.Exit(1)
115
+
116
+ # Update Anchor
117
+ try:
118
+ anchor.update(files)
119
+ except Exception as e:
120
+ console.print(f"[yellow]⚠️ Warning: Failed to update context anchor: {e}[/yellow]")
121
+
122
+ # Save to Disk
123
+ try:
124
+ output_path.write_text(snapshot_content, encoding="utf-8")
125
+
126
+ size_kb = len(snapshot_content) / 1024
127
+ token_count = tokenizer.estimate_tokens(snapshot_content)
128
+
129
+ # Report
130
+ console.print("[bold green]✔ Snapshot Saved![/bold green]")
131
+ console.print(f" 📄 Path: [underline]{output_path}[/underline]")
132
+ console.print(f" 📊 Size: {size_kb:.1f} KB | [bold cyan]~{token_count:,} Tokens[/bold cyan]")
133
+ console.print(f" 📂 Files: {len(files)}")
134
+
135
+ if anchor.has_history():
136
+ pre_diff_path = artifact_dir / "previous_changes.diff"
137
+ if pre_diff_path.exists() and pre_diff_path.stat().st_size > 0:
138
+ console.print(f" [dim]ℹ️ Changes before this scan saved to: {pre_diff_path.name}[/dim]")
139
+
140
+ except Exception as e:
141
+ console.print(f"[bold red]❌ Save Failed:[/bold red] {e}")
142
+ raise typer.Exit(1)
@@ -0,0 +1,131 @@
1
+ import typer
2
+ import pyperclip
3
+ from pathlib import Path
4
+ from rich.console import Console
5
+ from rich.progress import Progress, SpinnerColumn, TextColumn
6
+
7
+ from ..core import scanner, prompts, shadow, semdiff, tags, tokenizer
8
+
9
+ # Reuse config loader
10
+ from .scan import _load_config_filters
11
+
12
+ app = typer.Typer()
13
+ console = Console()
14
+
15
+ @app.callback(invoke_without_command=True)
16
+ def handle(
17
+ target: Path = typer.Argument(Path.cwd(), help="Target directory"),
18
+ copy: bool = typer.Option(True, help="Auto-copy to clipboard"),
19
+ save: bool = typer.Option(True, help="Save to .codigest/semdiff.xml"),
20
+ message: str = typer.Option("", "--message", "-m", help="Add specific instruction"),
21
+ ):
22
+ """
23
+ [Advanced] Generates a Semantic Diff (AST-based) report.
24
+ Shows ADDED/REMOVED/MODIFIED functions & classes instead of raw text lines.
25
+ """
26
+ root_path = target.resolve()
27
+ anchor = shadow.ContextAnchor(root_path)
28
+
29
+ last_update = anchor.get_last_update_time()
30
+ if last_update == "Never":
31
+ console.print("[yellow]⚠️ No scan history found. Run [bold]codigest scan[/bold] first.[/yellow]")
32
+ raise typer.Exit(1)
33
+
34
+ console.print(f"[dim]Analyzing structural changes since ({last_update})...[/dim]")
35
+
36
+ reports = []
37
+
38
+ with Progress(
39
+ SpinnerColumn(),
40
+ TextColumn("[bold blue]Parsing AST...[/bold blue]"),
41
+ transient=True,
42
+ console=console
43
+ ) as progress:
44
+ task = progress.add_task("semdiff", total=None)
45
+
46
+ # 1. Scan files
47
+ extensions, extra_ignores = _load_config_filters(root_path)
48
+ current_files = scanner.scan_project(root_path, extensions, extra_ignores)
49
+
50
+ changed_files_paths = anchor.get_changed_files(list(current_files))
51
+
52
+ target_files = []
53
+ for p in changed_files_paths:
54
+ if p.suffix in (".py", ".pyi"):
55
+ target_files.append(p)
56
+
57
+ # 2. Compare Each File
58
+ for file_path in target_files:
59
+ rel_path = file_path.relative_to(root_path)
60
+
61
+ # Read Content (Handle Missing Files for Deletion)
62
+ try:
63
+ new_code = file_path.read_text(encoding="utf-8") if file_path.exists() else ""
64
+ except: new_code = ""
65
+
66
+ old_code = anchor.read_anchor_file(rel_path)
67
+
68
+ # Case: File Added (No old code) -> All symbols ADDED
69
+ # Case: File Deleted (No new code) -> All symbols REMOVED
70
+ # Case: Modified -> Semantic Compare
71
+
72
+ changes = semdiff.compare(old_code, new_code)
73
+
74
+ if changes:
75
+ change_lines = []
76
+ for ch in changes:
77
+ if ch.change_type == "ADDED": icon = "➕ [ADDED] "
78
+ elif ch.change_type == "REMOVED": icon = "➖ [REMOVED] "
79
+ elif ch.change_type == "MODIFIED": icon = "⚠️ [SIGNATURE]" # 중요: 시그니처 변경
80
+ else: icon = "✏️ [LOGIC] " # 내부 로직 변경
81
+
82
+ detail = f" :: {ch.details}" if ch.details else ""
83
+ line = f"{icon} {ch.symbol.type} {ch.symbol.name}{ch.symbol.signature}{detail}"
84
+ change_lines.append(line)
85
+
86
+ # File Deleted Tagging
87
+ file_status = ""
88
+ if not new_code: file_status = " (DELETED)"
89
+ elif not old_code: file_status = " (NEW)"
90
+
91
+ block = tags.xml(t"""
92
+ <file path="{rel_path.as_posix()}{file_status}">
93
+ {chr(10).join(change_lines)}
94
+ </file>
95
+ """)
96
+ reports.append(block)
97
+
98
+ progress.update(task, completed=100)
99
+
100
+ if not reports:
101
+ console.print("[green]✨ No structural (AST) changes detected.[/green]")
102
+ return
103
+
104
+ # Render
105
+ report_content = "\n".join(reports)
106
+ prompt_engine = prompts.get_engine(root_path)
107
+
108
+ try:
109
+ final_output = prompt_engine.render(
110
+ "semdiff",
111
+ project_name=root_path.name,
112
+ context_message=f"Structural changes since {last_update}",
113
+ semdiff_content=report_content,
114
+ instruction=message
115
+ )
116
+ except Exception as e:
117
+ console.print(f"[red]❌ Template Error:[/red] {e}")
118
+ raise typer.Exit(1)
119
+
120
+ token_count = tokenizer.estimate_tokens(final_output)
121
+ console.print(f"[bold green]✔ SemDiff Generated![/bold green] ([bold cyan]~{token_count:,} Tokens[/bold cyan])")
122
+
123
+ if copy:
124
+ pyperclip.copy(final_output)
125
+ console.print("[dim]📋 Copied to clipboard[/dim]")
126
+
127
+ if save:
128
+ out_path = root_path / ".codigest" / "semdiff.xml"
129
+ out_path.parent.mkdir(exist_ok=True)
130
+ out_path.write_text(final_output, encoding="utf-8")
131
+ console.print(f"[dim]💾 Saved to {out_path}[/dim]")
@@ -0,0 +1,110 @@
1
+ import typer
2
+ import tomllib
3
+ from pathlib import Path
4
+ from typing import Optional
5
+ from rich.console import Console
6
+ from rich.tree import Tree
7
+ from rich.text import Text
8
+ from rich.filesize import decimal
9
+
10
+ from ..core.scanner import scan_project
11
+
12
+ app = typer.Typer()
13
+ console = Console()
14
+
15
+ def _load_extensions_from_config(root_path: Path) -> Optional[set[str]]:
16
+ """Attempts to load target extensions from .codigest/config.toml"""
17
+ config_path = root_path / ".codigest" / "config.toml"
18
+ if not config_path.exists():
19
+ return None
20
+ try:
21
+ with open(config_path, "rb") as f:
22
+ data = tomllib.load(f)
23
+ exts = data.get("filter", {}).get("extensions", [])
24
+ return set(exts) if exts else None
25
+ except Exception:
26
+ return None
27
+
28
+ def _build_rich_tree(root_path: Path, files: list[Path]) -> Tree:
29
+ """Converts a flat list of Paths into a visual Rich Tree."""
30
+ tree = Tree(
31
+ f":open_file_folder: [bold blue]{root_path.name}[/bold blue]",
32
+ guide_style="bold bright_black",
33
+ )
34
+ dir_nodes = {root_path: tree}
35
+
36
+ for path in files:
37
+ relative = path.relative_to(root_path)
38
+ parts = relative.parts
39
+ current_node = tree
40
+ current_path = root_path
41
+
42
+ for part in parts[:-1]:
43
+ current_path = current_path / part
44
+ if current_path not in dir_nodes:
45
+ dir_nodes[current_path] = current_node.add(f":folder: [bold cyan]{part}[/bold cyan]")
46
+ current_node = dir_nodes[current_path]
47
+
48
+ stat = path.stat()
49
+ size_str = decimal(stat.st_size)
50
+ filename = parts[-1]
51
+
52
+ # Icon logic
53
+ icon, style = "📄", "white"
54
+ if filename.endswith(".py"):
55
+ icon, style = "🐍", "green"
56
+ elif filename.endswith((".js", ".ts", ".tsx")):
57
+ icon, style = "📜", "yellow"
58
+ elif filename.endswith((".html", ".css")):
59
+ icon, style = "🌐", "magenta"
60
+ elif filename.endswith((".json", ".toml", ".yaml")):
61
+ icon, style = "⚙️", "blue"
62
+ elif filename.endswith(".md"):
63
+ icon, style = "📝", "cyan"
64
+
65
+ label = Text(f"{icon} {filename}", style=style)
66
+ label.append(f" ({size_str})", style="dim")
67
+ current_node.add(label)
68
+
69
+ return tree
70
+
71
+ @app.callback(invoke_without_command=True)
72
+ def handle(
73
+ target: Path = typer.Argument(Path.cwd(), help="Target directory"),
74
+ all: bool = typer.Option(False, "--all", "-a", help="Ignore config filters"),
75
+ ):
76
+ """
77
+ [Visual] Print the project directory tree (Respects .gitignore).
78
+ """
79
+ if not target.exists():
80
+ console.print(f"[red]❌ Error: Path '{target}' does not exist.[/red]")
81
+ raise typer.Exit(code=1)
82
+
83
+ root_path = target.resolve()
84
+
85
+ # 1. Determine Filters
86
+ extensions = None
87
+ if not all:
88
+ extensions = _load_extensions_from_config(root_path)
89
+ filter_status = f"[dim]Filter: {len(extensions)} extensions from config[/dim]" if extensions else "[dim]Filter: Default safelist[/dim]"
90
+ else:
91
+ filter_status = "[bold yellow]Filter: ALL (Gitignore only)[/bold yellow]"
92
+
93
+ console.print(f"[bold]🔍 Scanning:[/bold] {root_path}")
94
+ console.print(filter_status)
95
+
96
+ # 2. Scan
97
+ try:
98
+ files = scan_project(root_path, extensions)
99
+ except Exception as e:
100
+ console.print(f"[red]❌ Scan failed:[/red] {e}")
101
+ raise typer.Exit(code=1)
102
+
103
+ if not files:
104
+ console.print("[yellow]⚠️ No matching files found.[/yellow]")
105
+ return
106
+
107
+ # 3. Visualize
108
+ tree_viz = _build_rich_tree(root_path, files)
109
+ console.print(tree_viz)
110
+ console.print(f"\n[dim]Found {len(files)} files.[/dim]")
codigest/config.py ADDED
File without changes