codigest 0.3.3__tar.gz → 0.3.6__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 (35) hide show
  1. {codigest-0.3.3 → codigest-0.3.6}/PKG-INFO +42 -7
  2. {codigest-0.3.3 → codigest-0.3.6}/README.md +41 -6
  3. {codigest-0.3.3 → codigest-0.3.6}/pyproject.toml +2 -2
  4. codigest-0.3.6/src/codigest/commands/clean.py +45 -0
  5. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/commands/diff.py +39 -29
  6. codigest-0.3.6/src/codigest/commands/digest.py +110 -0
  7. codigest-0.3.6/src/codigest/commands/scan.py +182 -0
  8. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/commands/semdiff.py +59 -44
  9. codigest-0.3.6/src/codigest/commands/status.py +151 -0
  10. codigest-0.3.6/src/codigest/commands/tree.py +82 -0
  11. codigest-0.3.6/src/codigest/core/cache.py +78 -0
  12. codigest-0.3.6/src/codigest/core/common.py +118 -0
  13. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/prompts.py +2 -2
  14. codigest-0.3.6/src/codigest/core/resolver.py +151 -0
  15. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/scanner.py +3 -3
  16. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/semdiff.py +2 -2
  17. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/shadow.py +63 -19
  18. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/main.py +3 -1
  19. {codigest-0.3.3 → codigest-0.3.6}/uv.lock +1 -1
  20. codigest-0.3.3/src/codigest/commands/digest.py +0 -84
  21. codigest-0.3.3/src/codigest/commands/scan.py +0 -230
  22. codigest-0.3.3/src/codigest/commands/tree.py +0 -108
  23. codigest-0.3.3/src/codigest/core/resolver.py +0 -107
  24. {codigest-0.3.3 → codigest-0.3.6}/.github/workflows/publish.yml +0 -0
  25. {codigest-0.3.3 → codigest-0.3.6}/.gitignore +0 -0
  26. {codigest-0.3.3 → codigest-0.3.6}/.python-version +0 -0
  27. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/__init__.py +0 -0
  28. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/__main__.py +0 -0
  29. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/commands/init.py +0 -0
  30. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/config.py +0 -0
  31. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/git_ops.py +0 -0
  32. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/processor.py +0 -0
  33. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/structure.py +0 -0
  34. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/tags.py +0 -0
  35. {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/tokenizer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codigest
3
- Version: 0.3.3
3
+ Version: 0.3.6
4
4
  Summary: Human-Centered Codebase Context Manager for LLMs
5
5
  Project-URL: Homepage, https://github.com/SJB777/codigest
6
6
  Project-URL: Repository, https://github.com/SJB777/codigest
@@ -92,13 +92,12 @@ cdg init
92
92
 
93
93
  Scans the codebase and generates a structured XML snapshot. Includes a **Pre-flight Check** to prevent accidental token overflow.
94
94
 
95
- * **Output:** `.codigest/snapshot.xml`
95
+ * **Output:** `.codigest/snapshot.txt` (or stdout with `-s`)
96
96
  * **Features:**
97
97
  * **Smart Confirmation:** Automatically skips confirmation for small contexts, but warns you for large ones (>30k tokens).
98
98
  * **Dependency Resolution (`-r`):** Automatically finds and includes local files imported by your target files.
99
99
  * **Scope Control:** You can specify folders or files to scan.
100
-
101
-
100
+ * **Terminal Output (`-s` / `--stdout`):** Output directly to stdout instead of saving to file.
102
101
 
103
102
  ```bash
104
103
  # Basic scan (Interactive confirmation if large)
@@ -109,30 +108,39 @@ cdg scan src/main.py -r
109
108
 
110
109
  # Force execution without confirmation (Good for CI/CD)
111
110
  cdg scan -y --message "Automated snapshot"
111
+
112
+ # Output directly to stdout
113
+ cdg scan -s
112
114
  ```
113
115
 
114
116
  ### 3. Incremental Changes (`diff`)
115
117
 
116
118
  Tracks text-based changes between the last `scan` and the current working tree.
117
119
 
118
- * **Output:** `.codigest/changes.diff`
120
+ * **Output:** `.codigest/changes.diff` (or stdout with `-s`)
119
121
  * **Use Case:** "I modified 3 files. Here is exactly what changed since the last snapshot."
120
122
  * **Note:** Checks against the internal Shadow Git, enabling tracking without committing to the real Git.
121
123
 
122
124
  ```bash
123
125
  cdg diff
126
+
127
+ # Output directly to stdout
128
+ cdg diff -s
124
129
  ```
125
130
 
126
131
  ### 4. Semantic Analysis (`semdiff`)
127
132
 
128
133
  Analyzes **structural changes** (AST-based) rather than line-by-line text differences.
129
134
 
130
- * **Output:** `.codigest/semdiff.xml`
135
+ * **Output:** `.codigest/semdiff.txt` (or stdout with `-s`)
131
136
  * **Use Case:** "I refactored the API. Show me added/removed functions or signature changes."
132
137
  * **Benefit:** Reduces token usage by ignoring formatting/comment changes.
133
138
 
134
139
  ```bash
135
140
  cdg semdiff
141
+
142
+ # Output directly to stdout
143
+ cdg semdiff -s
136
144
  ```
137
145
 
138
146
  ### 5. Project Tree (`tree`)
@@ -150,11 +158,38 @@ cdg tree
150
158
 
151
159
  Generates a high-level outline of the project structure (Classes, Functions, Methods only).
152
160
 
153
- * **Output:** `.codigest/digest.xml`
161
+ * **Output:** `.codigest/digest.txt` (or stdout with `-s`)
154
162
  * **Use Case:** "Don't read the implementation details. Just understand the class hierarchy."
155
163
 
156
164
  ```bash
157
165
  cdg digest
166
+
167
+ # Output directly to stdout
168
+ cdg digest -s
169
+ ```
170
+
171
+ ### 7. Environment Status (`status`)
172
+
173
+ Displays the health of `.codigest`, anchor snapshot baseline, pending working tree changes, and generated artifacts.
174
+
175
+ * **Use Case:** "Check what files have changed since my last snapshot and inspect the state of generated context artifacts."
176
+
177
+ ```bash
178
+ cdg status
179
+ ```
180
+
181
+ ### 8. Environment Cleanup (`clean`)
182
+
183
+ Removes the `.codigest/` environment and resets all context anchors and artifacts.
184
+
185
+ * **Use Case:** "Clean up context files and resets Codigest tracking."
186
+
187
+ ```bash
188
+ # Interactive confirmation
189
+ cdg clean
190
+
191
+ # Force clean without prompt
192
+ cdg clean -y
158
193
  ```
159
194
 
160
195
  ---
@@ -67,13 +67,12 @@ cdg init
67
67
 
68
68
  Scans the codebase and generates a structured XML snapshot. Includes a **Pre-flight Check** to prevent accidental token overflow.
69
69
 
70
- * **Output:** `.codigest/snapshot.xml`
70
+ * **Output:** `.codigest/snapshot.txt` (or stdout with `-s`)
71
71
  * **Features:**
72
72
  * **Smart Confirmation:** Automatically skips confirmation for small contexts, but warns you for large ones (>30k tokens).
73
73
  * **Dependency Resolution (`-r`):** Automatically finds and includes local files imported by your target files.
74
74
  * **Scope Control:** You can specify folders or files to scan.
75
-
76
-
75
+ * **Terminal Output (`-s` / `--stdout`):** Output directly to stdout instead of saving to file.
77
76
 
78
77
  ```bash
79
78
  # Basic scan (Interactive confirmation if large)
@@ -84,30 +83,39 @@ cdg scan src/main.py -r
84
83
 
85
84
  # Force execution without confirmation (Good for CI/CD)
86
85
  cdg scan -y --message "Automated snapshot"
86
+
87
+ # Output directly to stdout
88
+ cdg scan -s
87
89
  ```
88
90
 
89
91
  ### 3. Incremental Changes (`diff`)
90
92
 
91
93
  Tracks text-based changes between the last `scan` and the current working tree.
92
94
 
93
- * **Output:** `.codigest/changes.diff`
95
+ * **Output:** `.codigest/changes.diff` (or stdout with `-s`)
94
96
  * **Use Case:** "I modified 3 files. Here is exactly what changed since the last snapshot."
95
97
  * **Note:** Checks against the internal Shadow Git, enabling tracking without committing to the real Git.
96
98
 
97
99
  ```bash
98
100
  cdg diff
101
+
102
+ # Output directly to stdout
103
+ cdg diff -s
99
104
  ```
100
105
 
101
106
  ### 4. Semantic Analysis (`semdiff`)
102
107
 
103
108
  Analyzes **structural changes** (AST-based) rather than line-by-line text differences.
104
109
 
105
- * **Output:** `.codigest/semdiff.xml`
110
+ * **Output:** `.codigest/semdiff.txt` (or stdout with `-s`)
106
111
  * **Use Case:** "I refactored the API. Show me added/removed functions or signature changes."
107
112
  * **Benefit:** Reduces token usage by ignoring formatting/comment changes.
108
113
 
109
114
  ```bash
110
115
  cdg semdiff
116
+
117
+ # Output directly to stdout
118
+ cdg semdiff -s
111
119
  ```
112
120
 
113
121
  ### 5. Project Tree (`tree`)
@@ -125,11 +133,38 @@ cdg tree
125
133
 
126
134
  Generates a high-level outline of the project structure (Classes, Functions, Methods only).
127
135
 
128
- * **Output:** `.codigest/digest.xml`
136
+ * **Output:** `.codigest/digest.txt` (or stdout with `-s`)
129
137
  * **Use Case:** "Don't read the implementation details. Just understand the class hierarchy."
130
138
 
131
139
  ```bash
132
140
  cdg digest
141
+
142
+ # Output directly to stdout
143
+ cdg digest -s
144
+ ```
145
+
146
+ ### 7. Environment Status (`status`)
147
+
148
+ Displays the health of `.codigest`, anchor snapshot baseline, pending working tree changes, and generated artifacts.
149
+
150
+ * **Use Case:** "Check what files have changed since my last snapshot and inspect the state of generated context artifacts."
151
+
152
+ ```bash
153
+ cdg status
154
+ ```
155
+
156
+ ### 8. Environment Cleanup (`clean`)
157
+
158
+ Removes the `.codigest/` environment and resets all context anchors and artifacts.
159
+
160
+ * **Use Case:** "Clean up context files and resets Codigest tracking."
161
+
162
+ ```bash
163
+ # Interactive confirmation
164
+ cdg clean
165
+
166
+ # Force clean without prompt
167
+ cdg clean -y
133
168
  ```
134
169
 
135
170
  ---
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codigest"
3
- version = "0.3.3"
3
+ version = "0.3.6"
4
4
  description = "Human-Centered Codebase Context Manager for LLMs"
5
5
  authors = [{name = "Codigest Team", email = "sngd024@google.com"}]
6
6
  readme = "README.md"
@@ -36,4 +36,4 @@ cdg = "codigest.main:app"
36
36
 
37
37
  [build-system]
38
38
  requires = ["hatchling"]
39
- build-backend = "hatchling.build"
39
+ build-backend = "hatchling.build"
@@ -0,0 +1,45 @@
1
+ import typer
2
+ import shutil
3
+ from pathlib import Path
4
+ from rich.console import Console
5
+
6
+ from ..core import common
7
+
8
+ app = typer.Typer()
9
+ console = Console()
10
+
11
+ @app.callback(invoke_without_command=True)
12
+ def handle(
13
+ target: Path = typer.Argument(
14
+ Path.cwd(),
15
+ help="Target directory to clean",
16
+ exists=True,
17
+ file_okay=False,
18
+ dir_okay=True,
19
+ resolve_path=True
20
+ ),
21
+ yes: bool = typer.Option(False, "-y", "--yes", "-f", "--force", help="Skip confirmation prompt"),
22
+ ):
23
+ """
24
+ [Cleanup] Removes the .codigest environment and clears all context anchors and artifacts.
25
+ """
26
+ ctx = common.get_context(target)
27
+ root_path = ctx.root_path
28
+ codigest_dir = root_path / ".codigest"
29
+
30
+ if not codigest_dir.exists():
31
+ console.print(f"[yellow]No .codigest directory found in {root_path}[/yellow]")
32
+ return
33
+
34
+ if not yes:
35
+ console.print(f"[yellow]Warning: This will remove [bold]{codigest_dir}[/bold] and all stored context/anchors.[/yellow]")
36
+ if not typer.confirm("Are you sure you want to remove .codigest?"):
37
+ console.print("[red]Aborted.[/red]")
38
+ raise typer.Exit()
39
+
40
+ try:
41
+ shutil.rmtree(codigest_dir)
42
+ console.print(f"[bold green]✔ Successfully removed .codigest environment from {root_path}[/bold green]")
43
+ except Exception as e:
44
+ console.print(f"[bold red]Error removing .codigest:[/bold red] {e}")
45
+ raise typer.Exit(1)
@@ -4,15 +4,14 @@ from pathlib import Path
4
4
  from rich.console import Console
5
5
  from rich.progress import Progress, SpinnerColumn, TextColumn
6
6
 
7
- from ..core import scanner, prompts, shadow
8
- from .scan import _load_config_filters
7
+ from ..core import prompts, shadow, tags, common
9
8
 
10
9
  app = typer.Typer()
11
10
  console = Console()
11
+ err_console = Console(stderr=True)
12
12
 
13
13
  @app.callback(invoke_without_command=True)
14
14
  def handle(
15
- # target 인자 추가
16
15
  target: Path = typer.Argument(
17
16
  Path.cwd(),
18
17
  help="Target directory to check diff",
@@ -23,49 +22,54 @@ def handle(
23
22
  ),
24
23
  copy: bool = typer.Option(True, help="Auto-copy to clipboard"),
25
24
  save: bool = typer.Option(True, help="Save to .codigest/changes.diff"),
25
+ stdout: bool = typer.Option(False, "-s", "--stdout", help="Print output to terminal (stdout) instead of file"),
26
26
  message: str = typer.Option("", "--message", "-m", help="Add specific instruction context"),
27
+ # [추가] resolve 옵션
28
+ resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports"),
27
29
  ):
28
30
  """
29
31
  [Context Update] Shows changes since the last 'codigest scan'.
30
32
  Useful for updating LLM context without re-uploading everything.
31
33
  """
32
- root_path = target
33
- anchor = shadow.ContextAnchor(root_path)
34
+ log_console = err_console if stdout else console
35
+
36
+ # [1] Context Setup
37
+ ctx = common.get_context(target)
38
+ root_path = ctx.root_path
34
39
 
35
- # 1. Check Baseline Existence
40
+ anchor = shadow.ContextAnchor(root_path)
41
+
42
+ # Check Baseline
36
43
  last_update = anchor.get_last_update_time()
37
44
  if last_update == "Never":
38
- console.print("[yellow]No scan history found.[/yellow]")
39
- console.print(" Run [bold cyan]codigest scan[/bold cyan] first to establish a baseline.")
45
+ log_console.print("[yellow]⚠️ No scan history found.[/yellow]")
46
+ log_console.print(" Run [bold cyan]cdg scan[/bold cyan] first to establish a baseline.")
40
47
  raise typer.Exit(1)
41
48
 
42
- console.print(f"[dim]Checking changes since last scan ({last_update})...[/dim]")
49
+ log_console.print(f"[dim]Checking changes since last scan ({last_update})...[/dim]")
43
50
 
44
- # 2. Calculate Diff
51
+ # [2] Calculate Diff via Context
45
52
  with Progress(
46
53
  SpinnerColumn(),
47
54
  TextColumn("[bold blue]Analyzing changes...[/bold blue]"),
48
55
  transient=True,
49
- console=console
56
+ console=log_console
50
57
  ) as progress:
51
58
  task = progress.add_task("diff", total=None)
52
-
53
- # Load same filters as scan
54
- extensions, extra_ignores = _load_config_filters(root_path)
55
-
56
- # Get current valid files (Working Tree)
57
- current_files = scanner.scan_project(root_path, extensions, extra_ignores)
58
-
59
+
60
+ # ★ Use common context to get files (handles config & resolve)
61
+ current_files = ctx.get_target_files(resolve_deps=resolve)
62
+
59
63
  # Compare Working Tree vs Anchor
60
64
  diff_content = anchor.get_changes(current_files)
61
-
65
+
62
66
  progress.update(task, completed=100)
63
67
 
64
68
  if not diff_content.strip():
65
- console.print("[green]✨ No changes detected since last scan.[/green]")
69
+ log_console.print("[green]No changes detected since last scan.[/green]")
66
70
  return
67
71
 
68
- # 3. Render
72
+ # [3] Render
69
73
  prompt_engine = prompts.get_engine(root_path)
70
74
  try:
71
75
  formatted_diff = prompt_engine.render(
@@ -76,18 +80,24 @@ def handle(
76
80
  instruction=message
77
81
  )
78
82
  except Exception:
79
- # Fallback if template fails
80
- formatted_diff = f"<diff>\n{diff_content}\n</diff>"
83
+ # Fallback if template fails (using safe escaping via tags)
84
+ safe_content = tags.escape_xml_value(diff_content)
85
+ formatted_diff = f"<diff>\n{safe_content}\n</diff>"
81
86
 
82
- # 4. Output
83
- console.print(f"[bold green]✔ Changes Detected![/bold green] ({len(formatted_diff)} chars)")
87
+ # [4] Output
88
+ log_console.print(f"[bold green]Changes Detected![/bold green] ({len(formatted_diff)} chars)")
84
89
 
85
90
  if copy:
86
- pyperclip.copy(formatted_diff)
87
- console.print("[dim]Copied to clipboard[/dim]")
91
+ try:
92
+ pyperclip.copy(formatted_diff)
93
+ log_console.print("[dim]Clipboard copied[/dim]")
94
+ except Exception:
95
+ log_console.print("[dim]⚠️ Clipboard unavailable (skipped copy)[/dim]")
88
96
 
89
- if save:
97
+ if stdout:
98
+ print(formatted_diff)
99
+ elif save:
90
100
  out_path = root_path / ".codigest" / "changes.diff"
91
101
  out_path.parent.mkdir(exist_ok=True)
92
102
  out_path.write_text(formatted_diff, encoding="utf-8")
93
- console.print(f"[dim]Saved to {out_path}[/dim]")
103
+ log_console.print(f"[dim]Saved to {out_path}[/dim]")
@@ -0,0 +1,110 @@
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 structure, prompts, semdiff, tags, tokenizer, common, cache
8
+
9
+ app = typer.Typer()
10
+ console = Console()
11
+ err_console = Console(stderr=True)
12
+
13
+ @app.callback(invoke_without_command=True)
14
+ def handle(
15
+ target: Path = typer.Argument(Path.cwd(), help="Target directory"),
16
+ copy: bool = typer.Option(True, help="Auto-copy to clipboard"),
17
+ save: bool = typer.Option(True, help="Save to .codigest/digest.txt"),
18
+ stdout: bool = typer.Option(False, "-s", "--stdout", help="Print output to terminal (stdout) instead of file"),
19
+ message: str = typer.Option("", "--message", "-m", help="Add specific instruction"),
20
+ # [추가]
21
+ resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports"),
22
+ ):
23
+ """
24
+ [Architectural View] Summarizes the codebase structure (Classes/Functions only).
25
+ """
26
+ log_console = err_console if stdout else console
27
+
28
+ # [1] Context Setup
29
+ ctx = common.get_context(target)
30
+ root_path = ctx.root_path
31
+
32
+ prompt_engine = prompts.get_engine(root_path)
33
+
34
+ with Progress(
35
+ SpinnerColumn(),
36
+ TextColumn("[bold blue]Digesting Architecture...[/bold blue]"),
37
+ transient=True,
38
+ console=log_console
39
+ ) as progress:
40
+
41
+ task = progress.add_task("digest", total=None)
42
+
43
+ # [2] File Discovery via Context
44
+ files = ctx.get_target_files(ignore_config=False, resolve_deps=resolve)
45
+
46
+ tree_str = structure.generate_ascii_tree(files, root_path)
47
+
48
+ content_cache = cache.ContentCache(root_path)
49
+ summary_blocks = []
50
+
51
+ for file_path in files:
52
+ if file_path.suffix in (".py", ".pyi"):
53
+ try:
54
+ rel_path = file_path.relative_to(root_path).as_posix()
55
+ except ValueError:
56
+ rel_path = f"[EXTERNAL]/{file_path.name}"
57
+
58
+ blob_hash = cache.get_file_blob_hash(file_path)
59
+ summary = None
60
+ if blob_hash:
61
+ summary = content_cache.get_summary(rel_path, blob_hash)
62
+
63
+ if summary is None:
64
+ try:
65
+ content = file_path.read_text(encoding="utf-8")
66
+ summary = semdiff.summarize(content)
67
+ if summary and blob_hash:
68
+ content_cache.set_summary(rel_path, blob_hash, summary)
69
+ except Exception:
70
+ continue
71
+
72
+ if summary:
73
+ block = tags.file(rel_path, summary)
74
+ summary_blocks.append(block)
75
+
76
+ content_cache.save()
77
+
78
+ summary_blob = "\n".join(summary_blocks)
79
+
80
+ try:
81
+ digest_content = prompt_engine.render(
82
+ "digest",
83
+ project_name=root_path.name,
84
+ tree_structure=tree_str,
85
+ digest_content=summary_blob,
86
+ instruction=message
87
+ )
88
+ except Exception as e:
89
+ log_console.print(f"[red]❌ Rendering Failed:[/red] {e}")
90
+ raise typer.Exit(1)
91
+
92
+ progress.update(task, completed=100)
93
+
94
+ token_count = tokenizer.estimate_tokens(digest_content)
95
+ log_console.print(f"[bold green]Digest Generated![/bold green] ([bold cyan]~{token_count:,} Tokens[/bold cyan])")
96
+
97
+ if copy:
98
+ try:
99
+ pyperclip.copy(digest_content)
100
+ log_console.print("[dim]📋 Copied to clipboard[/dim]")
101
+ except Exception:
102
+ log_console.print("[dim]⚠️ Clipboard unavailable (skipped copy)[/dim]")
103
+
104
+ if stdout:
105
+ print(digest_content)
106
+ elif save:
107
+ out_path = root_path / ".codigest" / "digest.txt"
108
+ out_path.parent.mkdir(exist_ok=True)
109
+ out_path.write_text(digest_content, encoding="utf-8")
110
+ log_console.print(f"[dim]💾 Saved to {out_path}[/dim]")
@@ -0,0 +1,182 @@
1
+ import sys
2
+ import typer
3
+ from pathlib import Path
4
+ from rich.console import Console
5
+ from rich.progress import Progress, SpinnerColumn, TextColumn
6
+ from rich.panel import Panel
7
+ from rich.filesize import decimal
8
+
9
+ from ..core import structure, tags, prompts, processor, shadow, tokenizer, common, cache
10
+
11
+ app = typer.Typer()
12
+ console = Console()
13
+ err_console = Console(stderr=True)
14
+
15
+ @app.callback(invoke_without_command=True)
16
+ def handle(
17
+ targets: list[Path] = typer.Argument(
18
+ None,
19
+ help="Specific files or directories to scan (Scope)",
20
+ exists=True,
21
+ resolve_path=True
22
+ ),
23
+ output: str = typer.Option("snapshot.txt", "-o", "--output", help="Output filename inside .codigest/"),
24
+ stdout: bool = typer.Option(False, "-s", "--stdout", help="Print output to terminal (stdout) instead of file"),
25
+ all: bool = typer.Option(False, "--all", "-a", help="Ignore config filters"),
26
+ message: str = typer.Option("", "--message", "-m", help="Add specific instruction"),
27
+ line_numbers: bool = typer.Option(False, "--lines", "-l", help="Add line numbers to code blocks"),
28
+ yes: bool = typer.Option(False, "-y", "--yes", help="Skip confirmation prompt"),
29
+ resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports for local files"),
30
+ ):
31
+ """
32
+ Scans the codebase.
33
+ If TARGETS provided, only scans those paths within the project.
34
+ """
35
+ log_console = err_console if stdout else console
36
+
37
+ # [1] Context Setup (Centralized)
38
+ ctx = common.get_context(targets)
39
+ root_path = ctx.root_path
40
+
41
+ # Init check
42
+ artifact_dir = root_path / ".codigest"
43
+ if not artifact_dir.exists():
44
+ log_console.print(f"[yellow][Warning] .codigest directory missing in {root_path.name}. Running init...[/yellow]")
45
+ try:
46
+ artifact_dir.mkdir(exist_ok=True)
47
+ except PermissionError:
48
+ log_console.print(f"[red][Error] Cannot create .codigest at {root_path}[/red]")
49
+ raise typer.Exit(1)
50
+
51
+ output_path = artifact_dir / output
52
+ prompt_engine = prompts.get_engine(root_path)
53
+ anchor = shadow.ContextAnchor(root_path)
54
+
55
+ # [2] File Discovery via Context
56
+ with Progress(
57
+ SpinnerColumn(),
58
+ TextColumn("[bold blue]Scanning...[/bold blue]"),
59
+ transient=True,
60
+ console=log_console
61
+ ) as progress:
62
+ task = progress.add_task("scanning", total=None)
63
+
64
+ # ★ All logic delegated to common.py
65
+ files = ctx.get_target_files(
66
+ targets=targets,
67
+ ignore_config=all,
68
+ resolve_deps=resolve
69
+ )
70
+
71
+ progress.update(task, completed=100)
72
+
73
+ # [3] Pre-flight Check
74
+ total_files = len(files)
75
+ total_size = sum(f.stat().st_size for f in files)
76
+ est_tokens = int(total_size / 4)
77
+
78
+ log_console.print(Panel(f"""[bold]Scan Plan[/bold]
79
+ Target: [cyan]{root_path}[/cyan]
80
+ Scope: {total_files} files
81
+ Est. Size: {decimal(total_size)}
82
+ Est. Tokens: ~{est_tokens:,}""", expand=False))
83
+
84
+ TOKEN_THRESHOLD = 30000
85
+ FILE_COUNT_THRESHOLD = 100
86
+ is_large_context = est_tokens > TOKEN_THRESHOLD or total_files > FILE_COUNT_THRESHOLD
87
+
88
+ if yes:
89
+ pass
90
+ elif is_large_context:
91
+ log_console.print(f"[yellow][Warning] Large context detected (> {TOKEN_THRESHOLD:,} tokens or > {FILE_COUNT_THRESHOLD} files).[/yellow]")
92
+ if not typer.confirm("Proceed with digestion?"):
93
+ log_console.print("[red]Aborted.[/red]")
94
+ raise typer.Exit()
95
+ else:
96
+ log_console.print("[dim]Small context detected. Automatically proceeding...[/dim]")
97
+
98
+ # [4] Execution
99
+ with Progress(
100
+ SpinnerColumn(),
101
+ TextColumn("[bold blue]Generating Snapshot...[/bold blue]"),
102
+ transient=True,
103
+ console=log_console
104
+ ) as progress:
105
+
106
+ if anchor.has_history():
107
+ try:
108
+ diff_content = anchor.get_changes(files)
109
+ if diff_content.strip():
110
+ pre_diff_path = artifact_dir / "previous_changes.diff"
111
+ pre_diff_path.write_text(diff_content, encoding="utf-8")
112
+ except Exception:
113
+ pass
114
+
115
+ tree_str = structure.generate_ascii_tree(files, root_path)
116
+
117
+ content_cache = cache.ContentCache(root_path)
118
+ file_blocks = []
119
+
120
+ for file_path in files:
121
+ try:
122
+ rel_path = file_path.relative_to(root_path).as_posix()
123
+ except ValueError:
124
+ rel_path = f"[EXTERNAL]/{file_path.name}"
125
+
126
+ blob_hash = cache.get_file_blob_hash(file_path)
127
+ cached_block = None
128
+ if blob_hash:
129
+ cached_block = content_cache.get_file_block(rel_path, blob_hash, line_numbers)
130
+
131
+ if cached_block is not None:
132
+ file_blocks.append(cached_block)
133
+ else:
134
+ try:
135
+ content = processor.read_file_content(file_path, add_line_numbers=line_numbers)
136
+ block = tags.file(rel_path, content)
137
+ file_blocks.append(block)
138
+ if blob_hash:
139
+ content_cache.set_file_block(rel_path, blob_hash, line_numbers, block)
140
+ except Exception:
141
+ continue
142
+
143
+ content_cache.save()
144
+
145
+ source_code_blob = "\n\n".join(file_blocks)
146
+
147
+ try:
148
+ snapshot_content = prompt_engine.render(
149
+ "snapshot",
150
+ project_name=root_path.name,
151
+ tree_structure=tree_str,
152
+ source_code=source_code_blob,
153
+ instruction=message
154
+ )
155
+ except Exception as e:
156
+ log_console.print(f"[red][Error] Template Rendering Failed:[/red] {e}")
157
+ raise typer.Exit(1)
158
+
159
+ try:
160
+ anchor.update(files)
161
+ except Exception as e:
162
+ log_console.print(f"[yellow][Warning] Failed to update context anchor: {e}[/yellow]")
163
+
164
+ if stdout:
165
+ print(snapshot_content)
166
+ else:
167
+ try:
168
+ output_path.write_text(snapshot_content, encoding="utf-8")
169
+ final_token_count = tokenizer.estimate_tokens(snapshot_content)
170
+
171
+ log_console.print("[bold green]Snapshot Saved![/bold green]")
172
+ log_console.print(f" Path: [underline]{output_path}[/underline]")
173
+ log_console.print(f" Final Tokens: [bold cyan]~{final_token_count:,}[/bold cyan]")
174
+
175
+ if anchor.has_history():
176
+ pre_diff_path = artifact_dir / "previous_changes.diff"
177
+ if pre_diff_path.exists() and pre_diff_path.stat().st_size > 0:
178
+ log_console.print(f" [dim]Changes before this scan saved to: {pre_diff_path.name}[/dim]")
179
+
180
+ except Exception as e:
181
+ log_console.print(f"[bold red][Error] Save Failed:[/bold red] {e}")
182
+ raise typer.Exit(1)