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.
- {codigest-0.3.3 → codigest-0.3.6}/PKG-INFO +42 -7
- {codigest-0.3.3 → codigest-0.3.6}/README.md +41 -6
- {codigest-0.3.3 → codigest-0.3.6}/pyproject.toml +2 -2
- codigest-0.3.6/src/codigest/commands/clean.py +45 -0
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/commands/diff.py +39 -29
- codigest-0.3.6/src/codigest/commands/digest.py +110 -0
- codigest-0.3.6/src/codigest/commands/scan.py +182 -0
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/commands/semdiff.py +59 -44
- codigest-0.3.6/src/codigest/commands/status.py +151 -0
- codigest-0.3.6/src/codigest/commands/tree.py +82 -0
- codigest-0.3.6/src/codigest/core/cache.py +78 -0
- codigest-0.3.6/src/codigest/core/common.py +118 -0
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/prompts.py +2 -2
- codigest-0.3.6/src/codigest/core/resolver.py +151 -0
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/scanner.py +3 -3
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/semdiff.py +2 -2
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/shadow.py +63 -19
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/main.py +3 -1
- {codigest-0.3.3 → codigest-0.3.6}/uv.lock +1 -1
- codigest-0.3.3/src/codigest/commands/digest.py +0 -84
- codigest-0.3.3/src/codigest/commands/scan.py +0 -230
- codigest-0.3.3/src/codigest/commands/tree.py +0 -108
- codigest-0.3.3/src/codigest/core/resolver.py +0 -107
- {codigest-0.3.3 → codigest-0.3.6}/.github/workflows/publish.yml +0 -0
- {codigest-0.3.3 → codigest-0.3.6}/.gitignore +0 -0
- {codigest-0.3.3 → codigest-0.3.6}/.python-version +0 -0
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/__init__.py +0 -0
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/__main__.py +0 -0
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/commands/init.py +0 -0
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/config.py +0 -0
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/git_ops.py +0 -0
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/processor.py +0 -0
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/structure.py +0 -0
- {codigest-0.3.3 → codigest-0.3.6}/src/codigest/core/tags.py +0 -0
- {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
|
+
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
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
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
49
|
+
log_console.print(f"[dim]Checking changes since last scan ({last_update})...[/dim]")
|
|
43
50
|
|
|
44
|
-
# 2
|
|
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=
|
|
56
|
+
console=log_console
|
|
50
57
|
) as progress:
|
|
51
58
|
task = progress.add_task("diff", total=None)
|
|
52
|
-
|
|
53
|
-
#
|
|
54
|
-
|
|
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
|
-
|
|
69
|
+
log_console.print("[green]No changes detected since last scan.[/green]")
|
|
66
70
|
return
|
|
67
71
|
|
|
68
|
-
# 3
|
|
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
|
-
|
|
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
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|
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
|
-
|
|
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)
|