codigest 0.3.5__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.5 → codigest-0.3.6}/PKG-INFO +42 -7
- {codigest-0.3.5 → codigest-0.3.6}/README.md +41 -6
- {codigest-0.3.5 → codigest-0.3.6}/pyproject.toml +1 -1
- codigest-0.3.6/src/codigest/commands/clean.py +45 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/commands/diff.py +19 -10
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/commands/digest.py +43 -22
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/commands/scan.py +55 -33
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/commands/semdiff.py +21 -12
- codigest-0.3.6/src/codigest/commands/status.py +151 -0
- codigest-0.3.6/src/codigest/core/cache.py +78 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/shadow.py +35 -18
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/main.py +3 -1
- {codigest-0.3.5 → codigest-0.3.6}/uv.lock +1 -5
- {codigest-0.3.5 → codigest-0.3.6}/.github/workflows/publish.yml +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/.gitignore +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/.python-version +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/__init__.py +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/__main__.py +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/commands/init.py +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/commands/tree.py +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/config.py +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/common.py +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/git_ops.py +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/processor.py +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/prompts.py +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/resolver.py +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/scanner.py +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/semdiff.py +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/structure.py +0 -0
- {codigest-0.3.5 → codigest-0.3.6}/src/codigest/core/tags.py +0 -0
- {codigest-0.3.5 → 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
|
---
|
|
@@ -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)
|
|
@@ -8,6 +8,7 @@ from ..core import prompts, shadow, tags, common
|
|
|
8
8
|
|
|
9
9
|
app = typer.Typer()
|
|
10
10
|
console = Console()
|
|
11
|
+
err_console = Console(stderr=True)
|
|
11
12
|
|
|
12
13
|
@app.callback(invoke_without_command=True)
|
|
13
14
|
def handle(
|
|
@@ -21,6 +22,7 @@ def handle(
|
|
|
21
22
|
),
|
|
22
23
|
copy: bool = typer.Option(True, help="Auto-copy to clipboard"),
|
|
23
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"),
|
|
24
26
|
message: str = typer.Option("", "--message", "-m", help="Add specific instruction context"),
|
|
25
27
|
# [추가] resolve 옵션
|
|
26
28
|
resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports"),
|
|
@@ -29,6 +31,8 @@ def handle(
|
|
|
29
31
|
[Context Update] Shows changes since the last 'codigest scan'.
|
|
30
32
|
Useful for updating LLM context without re-uploading everything.
|
|
31
33
|
"""
|
|
34
|
+
log_console = err_console if stdout else console
|
|
35
|
+
|
|
32
36
|
# [1] Context Setup
|
|
33
37
|
ctx = common.get_context(target)
|
|
34
38
|
root_path = ctx.root_path
|
|
@@ -38,18 +42,18 @@ def handle(
|
|
|
38
42
|
# Check Baseline
|
|
39
43
|
last_update = anchor.get_last_update_time()
|
|
40
44
|
if last_update == "Never":
|
|
41
|
-
|
|
42
|
-
|
|
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.")
|
|
43
47
|
raise typer.Exit(1)
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
log_console.print(f"[dim]Checking changes since last scan ({last_update})...[/dim]")
|
|
46
50
|
|
|
47
51
|
# [2] Calculate Diff via Context
|
|
48
52
|
with Progress(
|
|
49
53
|
SpinnerColumn(),
|
|
50
54
|
TextColumn("[bold blue]Analyzing changes...[/bold blue]"),
|
|
51
55
|
transient=True,
|
|
52
|
-
console=
|
|
56
|
+
console=log_console
|
|
53
57
|
) as progress:
|
|
54
58
|
task = progress.add_task("diff", total=None)
|
|
55
59
|
|
|
@@ -62,7 +66,7 @@ def handle(
|
|
|
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
72
|
# [3] Render
|
|
@@ -81,14 +85,19 @@ def handle(
|
|
|
81
85
|
formatted_diff = f"<diff>\n{safe_content}\n</diff>"
|
|
82
86
|
|
|
83
87
|
# [4] Output
|
|
84
|
-
|
|
88
|
+
log_console.print(f"[bold green]Changes Detected![/bold green] ({len(formatted_diff)} chars)")
|
|
85
89
|
|
|
86
90
|
if copy:
|
|
87
|
-
|
|
88
|
-
|
|
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]")
|
|
89
96
|
|
|
90
|
-
if
|
|
97
|
+
if stdout:
|
|
98
|
+
print(formatted_diff)
|
|
99
|
+
elif save:
|
|
91
100
|
out_path = root_path / ".codigest" / "changes.diff"
|
|
92
101
|
out_path.parent.mkdir(exist_ok=True)
|
|
93
102
|
out_path.write_text(formatted_diff, encoding="utf-8")
|
|
94
|
-
|
|
103
|
+
log_console.print(f"[dim]Saved to {out_path}[/dim]")
|
|
@@ -4,16 +4,18 @@ 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 structure, prompts, semdiff, tags, tokenizer, common
|
|
7
|
+
from ..core import structure, prompts, semdiff, tags, tokenizer, common, cache
|
|
8
8
|
|
|
9
9
|
app = typer.Typer()
|
|
10
10
|
console = Console()
|
|
11
|
+
err_console = Console(stderr=True)
|
|
11
12
|
|
|
12
13
|
@app.callback(invoke_without_command=True)
|
|
13
14
|
def handle(
|
|
14
15
|
target: Path = typer.Argument(Path.cwd(), help="Target directory"),
|
|
15
16
|
copy: bool = typer.Option(True, help="Auto-copy to clipboard"),
|
|
16
|
-
save: bool = typer.Option(True, help="Save to .codigest/digest.
|
|
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"),
|
|
17
19
|
message: str = typer.Option("", "--message", "-m", help="Add specific instruction"),
|
|
18
20
|
# [추가]
|
|
19
21
|
resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports"),
|
|
@@ -21,6 +23,8 @@ def handle(
|
|
|
21
23
|
"""
|
|
22
24
|
[Architectural View] Summarizes the codebase structure (Classes/Functions only).
|
|
23
25
|
"""
|
|
26
|
+
log_console = err_console if stdout else console
|
|
27
|
+
|
|
24
28
|
# [1] Context Setup
|
|
25
29
|
ctx = common.get_context(target)
|
|
26
30
|
root_path = ctx.root_path
|
|
@@ -31,7 +35,7 @@ def handle(
|
|
|
31
35
|
SpinnerColumn(),
|
|
32
36
|
TextColumn("[bold blue]Digesting Architecture...[/bold blue]"),
|
|
33
37
|
transient=True,
|
|
34
|
-
console=
|
|
38
|
+
console=log_console
|
|
35
39
|
) as progress:
|
|
36
40
|
|
|
37
41
|
task = progress.add_task("digest", total=None)
|
|
@@ -41,23 +45,35 @@ def handle(
|
|
|
41
45
|
|
|
42
46
|
tree_str = structure.generate_ascii_tree(files, root_path)
|
|
43
47
|
|
|
48
|
+
content_cache = cache.ContentCache(root_path)
|
|
44
49
|
summary_blocks = []
|
|
50
|
+
|
|
45
51
|
for file_path in files:
|
|
46
52
|
if file_path.suffix in (".py", ".pyi"):
|
|
47
53
|
try:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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)
|
|
56
62
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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()
|
|
61
77
|
|
|
62
78
|
summary_blob = "\n".join(summary_blocks)
|
|
63
79
|
|
|
@@ -70,20 +86,25 @@ def handle(
|
|
|
70
86
|
instruction=message
|
|
71
87
|
)
|
|
72
88
|
except Exception as e:
|
|
73
|
-
|
|
89
|
+
log_console.print(f"[red]❌ Rendering Failed:[/red] {e}")
|
|
74
90
|
raise typer.Exit(1)
|
|
75
91
|
|
|
76
92
|
progress.update(task, completed=100)
|
|
77
93
|
|
|
78
94
|
token_count = tokenizer.estimate_tokens(digest_content)
|
|
79
|
-
|
|
95
|
+
log_console.print(f"[bold green]Digest Generated![/bold green] ([bold cyan]~{token_count:,} Tokens[/bold cyan])")
|
|
80
96
|
|
|
81
97
|
if copy:
|
|
82
|
-
|
|
83
|
-
|
|
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]")
|
|
84
103
|
|
|
85
|
-
if
|
|
86
|
-
|
|
104
|
+
if stdout:
|
|
105
|
+
print(digest_content)
|
|
106
|
+
elif save:
|
|
107
|
+
out_path = root_path / ".codigest" / "digest.txt"
|
|
87
108
|
out_path.parent.mkdir(exist_ok=True)
|
|
88
109
|
out_path.write_text(digest_content, encoding="utf-8")
|
|
89
|
-
|
|
110
|
+
log_console.print(f"[dim]💾 Saved to {out_path}[/dim]")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
import typer
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from rich.console import Console
|
|
@@ -5,10 +6,11 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
|
5
6
|
from rich.panel import Panel
|
|
6
7
|
from rich.filesize import decimal
|
|
7
8
|
|
|
8
|
-
from ..core import structure, tags, prompts, processor, shadow, tokenizer, common
|
|
9
|
+
from ..core import structure, tags, prompts, processor, shadow, tokenizer, common, cache
|
|
9
10
|
|
|
10
11
|
app = typer.Typer()
|
|
11
12
|
console = Console()
|
|
13
|
+
err_console = Console(stderr=True)
|
|
12
14
|
|
|
13
15
|
@app.callback(invoke_without_command=True)
|
|
14
16
|
def handle(
|
|
@@ -18,7 +20,8 @@ def handle(
|
|
|
18
20
|
exists=True,
|
|
19
21
|
resolve_path=True
|
|
20
22
|
),
|
|
21
|
-
output: str = typer.Option("snapshot.
|
|
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"),
|
|
22
25
|
all: bool = typer.Option(False, "--all", "-a", help="Ignore config filters"),
|
|
23
26
|
message: str = typer.Option("", "--message", "-m", help="Add specific instruction"),
|
|
24
27
|
line_numbers: bool = typer.Option(False, "--lines", "-l", help="Add line numbers to code blocks"),
|
|
@@ -29,6 +32,8 @@ def handle(
|
|
|
29
32
|
Scans the codebase.
|
|
30
33
|
If TARGETS provided, only scans those paths within the project.
|
|
31
34
|
"""
|
|
35
|
+
log_console = err_console if stdout else console
|
|
36
|
+
|
|
32
37
|
# [1] Context Setup (Centralized)
|
|
33
38
|
ctx = common.get_context(targets)
|
|
34
39
|
root_path = ctx.root_path
|
|
@@ -36,11 +41,11 @@ def handle(
|
|
|
36
41
|
# Init check
|
|
37
42
|
artifact_dir = root_path / ".codigest"
|
|
38
43
|
if not artifact_dir.exists():
|
|
39
|
-
|
|
44
|
+
log_console.print(f"[yellow][Warning] .codigest directory missing in {root_path.name}. Running init...[/yellow]")
|
|
40
45
|
try:
|
|
41
46
|
artifact_dir.mkdir(exist_ok=True)
|
|
42
47
|
except PermissionError:
|
|
43
|
-
|
|
48
|
+
log_console.print(f"[red][Error] Cannot create .codigest at {root_path}[/red]")
|
|
44
49
|
raise typer.Exit(1)
|
|
45
50
|
|
|
46
51
|
output_path = artifact_dir / output
|
|
@@ -52,7 +57,7 @@ def handle(
|
|
|
52
57
|
SpinnerColumn(),
|
|
53
58
|
TextColumn("[bold blue]Scanning...[/bold blue]"),
|
|
54
59
|
transient=True,
|
|
55
|
-
console=
|
|
60
|
+
console=log_console
|
|
56
61
|
) as progress:
|
|
57
62
|
task = progress.add_task("scanning", total=None)
|
|
58
63
|
|
|
@@ -70,7 +75,7 @@ def handle(
|
|
|
70
75
|
total_size = sum(f.stat().st_size for f in files)
|
|
71
76
|
est_tokens = int(total_size / 4)
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
log_console.print(Panel(f"""[bold]Scan Plan[/bold]
|
|
74
79
|
Target: [cyan]{root_path}[/cyan]
|
|
75
80
|
Scope: {total_files} files
|
|
76
81
|
Est. Size: {decimal(total_size)}
|
|
@@ -83,19 +88,19 @@ def handle(
|
|
|
83
88
|
if yes:
|
|
84
89
|
pass
|
|
85
90
|
elif is_large_context:
|
|
86
|
-
|
|
91
|
+
log_console.print(f"[yellow][Warning] Large context detected (> {TOKEN_THRESHOLD:,} tokens or > {FILE_COUNT_THRESHOLD} files).[/yellow]")
|
|
87
92
|
if not typer.confirm("Proceed with digestion?"):
|
|
88
|
-
|
|
93
|
+
log_console.print("[red]Aborted.[/red]")
|
|
89
94
|
raise typer.Exit()
|
|
90
95
|
else:
|
|
91
|
-
|
|
96
|
+
log_console.print("[dim]Small context detected. Automatically proceeding...[/dim]")
|
|
92
97
|
|
|
93
98
|
# [4] Execution
|
|
94
99
|
with Progress(
|
|
95
100
|
SpinnerColumn(),
|
|
96
101
|
TextColumn("[bold blue]Generating Snapshot...[/bold blue]"),
|
|
97
102
|
transient=True,
|
|
98
|
-
console=
|
|
103
|
+
console=log_console
|
|
99
104
|
) as progress:
|
|
100
105
|
|
|
101
106
|
if anchor.has_history():
|
|
@@ -109,19 +114,33 @@ def handle(
|
|
|
109
114
|
|
|
110
115
|
tree_str = structure.generate_ascii_tree(files, root_path)
|
|
111
116
|
|
|
117
|
+
content_cache = cache.ContentCache(root_path)
|
|
112
118
|
file_blocks = []
|
|
119
|
+
|
|
113
120
|
for file_path in files:
|
|
114
121
|
try:
|
|
115
122
|
rel_path = file_path.relative_to(root_path).as_posix()
|
|
116
123
|
except ValueError:
|
|
117
124
|
rel_path = f"[EXTERNAL]/{file_path.name}"
|
|
118
125
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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()
|
|
125
144
|
|
|
126
145
|
source_code_blob = "\n\n".join(file_blocks)
|
|
127
146
|
|
|
@@ -134,27 +153,30 @@ def handle(
|
|
|
134
153
|
instruction=message
|
|
135
154
|
)
|
|
136
155
|
except Exception as e:
|
|
137
|
-
|
|
156
|
+
log_console.print(f"[red][Error] Template Rendering Failed:[/red] {e}")
|
|
138
157
|
raise typer.Exit(1)
|
|
139
158
|
|
|
140
159
|
try:
|
|
141
160
|
anchor.update(files)
|
|
142
161
|
except Exception as e:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
try:
|
|
146
|
-
output_path.write_text(snapshot_content, encoding="utf-8")
|
|
147
|
-
final_token_count = tokenizer.estimate_tokens(snapshot_content)
|
|
162
|
+
log_console.print(f"[yellow][Warning] Failed to update context anchor: {e}[/yellow]")
|
|
148
163
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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]")
|
|
157
179
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
180
|
+
except Exception as e:
|
|
181
|
+
log_console.print(f"[bold red][Error] Save Failed:[/bold red] {e}")
|
|
182
|
+
raise typer.Exit(1)
|
|
@@ -8,12 +8,14 @@ from ..core import prompts, shadow, semdiff, tags, tokenizer, common
|
|
|
8
8
|
|
|
9
9
|
app = typer.Typer()
|
|
10
10
|
console = Console()
|
|
11
|
+
err_console = Console(stderr=True)
|
|
11
12
|
|
|
12
13
|
@app.callback(invoke_without_command=True)
|
|
13
14
|
def handle(
|
|
14
15
|
target: Path = typer.Argument(Path.cwd(), help="Target directory"),
|
|
15
16
|
copy: bool = typer.Option(True, help="Auto-copy to clipboard"),
|
|
16
|
-
save: bool = typer.Option(True, help="Save to .codigest/semdiff.
|
|
17
|
+
save: bool = typer.Option(True, help="Save to .codigest/semdiff.txt"),
|
|
18
|
+
stdout: bool = typer.Option(False, "-s", "--stdout", help="Print output to terminal (stdout) instead of file"),
|
|
17
19
|
message: str = typer.Option("", "--message", "-m", help="Add specific instruction"),
|
|
18
20
|
# [추가] resolve 옵션
|
|
19
21
|
resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports"),
|
|
@@ -22,6 +24,8 @@ def handle(
|
|
|
22
24
|
[Advanced] Generates a Semantic Diff (AST-based) report.
|
|
23
25
|
Shows ADDED/REMOVED/MODIFIED functions & classes instead of raw text lines.
|
|
24
26
|
"""
|
|
27
|
+
log_console = err_console if stdout else console
|
|
28
|
+
|
|
25
29
|
# [1] Context Setup
|
|
26
30
|
ctx = common.get_context(target)
|
|
27
31
|
root_path = ctx.root_path
|
|
@@ -30,10 +34,10 @@ def handle(
|
|
|
30
34
|
|
|
31
35
|
last_update = anchor.get_last_update_time()
|
|
32
36
|
if last_update == "Never":
|
|
33
|
-
|
|
37
|
+
log_console.print("[yellow]⚠️ No scan history found. Run [bold]cdg scan[/bold] first.[/yellow]")
|
|
34
38
|
raise typer.Exit(1)
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
log_console.print(f"[dim]Analyzing structural changes since ({last_update})...[/dim]")
|
|
37
41
|
|
|
38
42
|
reports = []
|
|
39
43
|
|
|
@@ -41,7 +45,7 @@ def handle(
|
|
|
41
45
|
SpinnerColumn(),
|
|
42
46
|
TextColumn("[bold blue]Parsing AST...[/bold blue]"),
|
|
43
47
|
transient=True,
|
|
44
|
-
console=
|
|
48
|
+
console=log_console
|
|
45
49
|
) as progress:
|
|
46
50
|
task = progress.add_task("semdiff", total=None)
|
|
47
51
|
|
|
@@ -108,7 +112,7 @@ def handle(
|
|
|
108
112
|
progress.update(task, completed=100)
|
|
109
113
|
|
|
110
114
|
if not reports:
|
|
111
|
-
|
|
115
|
+
log_console.print("[green]No structural (AST) changes detected.[/green]")
|
|
112
116
|
return
|
|
113
117
|
|
|
114
118
|
# [4] Render
|
|
@@ -124,18 +128,23 @@ def handle(
|
|
|
124
128
|
instruction=message
|
|
125
129
|
)
|
|
126
130
|
except Exception as e:
|
|
127
|
-
|
|
131
|
+
log_console.print(f"[red]Template Error:[/red] {e}")
|
|
128
132
|
raise typer.Exit(1)
|
|
129
133
|
|
|
130
134
|
token_count = tokenizer.estimate_tokens(final_output)
|
|
131
|
-
|
|
135
|
+
log_console.print(f"[bold green]SemDiff Generated![/bold green] ([bold cyan]~{token_count:,} Tokens[/bold cyan])")
|
|
132
136
|
|
|
133
137
|
if copy:
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
try:
|
|
139
|
+
pyperclip.copy(final_output)
|
|
140
|
+
log_console.print("[dim]Clipboard copied[/dim]")
|
|
141
|
+
except Exception:
|
|
142
|
+
log_console.print("[dim]⚠️ Clipboard unavailable (skipped copy)[/dim]")
|
|
136
143
|
|
|
137
|
-
if
|
|
138
|
-
|
|
144
|
+
if stdout:
|
|
145
|
+
print(final_output)
|
|
146
|
+
elif save:
|
|
147
|
+
out_path = root_path / ".codigest" / "semdiff.txt"
|
|
139
148
|
out_path.parent.mkdir(exist_ok=True)
|
|
140
149
|
out_path.write_text(final_output, encoding="utf-8")
|
|
141
|
-
|
|
150
|
+
log_console.print(f"[dim]Saved to {out_path}[/dim]")
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
from rich.filesize import decimal
|
|
7
|
+
|
|
8
|
+
from ..core import common, shadow, tokenizer
|
|
9
|
+
|
|
10
|
+
app = typer.Typer()
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
@app.callback(invoke_without_command=True)
|
|
14
|
+
def handle(
|
|
15
|
+
target: Path = typer.Argument(
|
|
16
|
+
Path.cwd(),
|
|
17
|
+
help="Target directory to check status",
|
|
18
|
+
exists=True,
|
|
19
|
+
file_okay=False,
|
|
20
|
+
dir_okay=True,
|
|
21
|
+
resolve_path=True
|
|
22
|
+
),
|
|
23
|
+
resolve: bool = typer.Option(False, "-r", "--resolve", help="Recursively resolve imports when checking context files"),
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
[Environment Status] Displays information about the .codigest setup, anchor baseline, changes, and artifacts.
|
|
27
|
+
"""
|
|
28
|
+
ctx = common.get_context(target)
|
|
29
|
+
root_path = ctx.root_path
|
|
30
|
+
codigest_dir = root_path / ".codigest"
|
|
31
|
+
|
|
32
|
+
is_initialized = codigest_dir.exists() and codigest_dir.is_dir()
|
|
33
|
+
|
|
34
|
+
# Header Panel
|
|
35
|
+
init_status_str = "[bold green]Initialized[/bold green]" if is_initialized else "[bold yellow]Not Initialized[/bold yellow]"
|
|
36
|
+
header_content = (
|
|
37
|
+
f"🦁 [bold cyan]Codigest Environment Status[/bold cyan]\n"
|
|
38
|
+
f"📂 [bold]Root:[/bold] {root_path}\n"
|
|
39
|
+
f"⚙️ [bold]Status:[/bold] {init_status_str} "
|
|
40
|
+
f"({'[dim].codigest/ present[/dim]' if is_initialized else '[dim]run cdg init to set up[/dim]'})"
|
|
41
|
+
)
|
|
42
|
+
console.print(Panel(header_content, expand=False))
|
|
43
|
+
|
|
44
|
+
if not is_initialized:
|
|
45
|
+
console.print("\n[yellow]💡 Tip: Run [bold cyan]cdg init[/bold cyan] to initialize context tracking for this repository.[/yellow]")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
anchor = shadow.ContextAnchor(root_path)
|
|
49
|
+
has_anchor = anchor.has_history()
|
|
50
|
+
last_update = anchor.get_last_update_time() if has_anchor else "Never"
|
|
51
|
+
|
|
52
|
+
# Fetch target files
|
|
53
|
+
current_files = ctx.get_target_files(resolve_deps=resolve)
|
|
54
|
+
total_files = len(current_files)
|
|
55
|
+
|
|
56
|
+
# 1. Baseline Section
|
|
57
|
+
console.print("\n[bold blue]📍 Context Anchor Baseline[/bold blue]")
|
|
58
|
+
console.print(f" • [bold]Last Snapshot:[/bold] {last_update}")
|
|
59
|
+
console.print(f" • [bold]Tracked Source Files:[/bold] {total_files} files")
|
|
60
|
+
|
|
61
|
+
# 2. Pending Working Tree Changes
|
|
62
|
+
console.print("\n[bold blue]📝 Pending Working Tree Changes[/bold blue] [dim](since last scan)[/dim]")
|
|
63
|
+
if not has_anchor:
|
|
64
|
+
console.print(" [yellow]⚠️ No baseline scan found. Run [bold cyan]cdg scan[/bold cyan] to create the initial snapshot.[/yellow]")
|
|
65
|
+
else:
|
|
66
|
+
diff_content = anchor.get_changes(current_files)
|
|
67
|
+
changed_files = anchor.get_changed_files(current_files)
|
|
68
|
+
|
|
69
|
+
if not diff_content.strip():
|
|
70
|
+
console.print(" [green]✔ Working tree clean (No pending changes detected)[/green]")
|
|
71
|
+
else:
|
|
72
|
+
diff_tokens = tokenizer.estimate_tokens(diff_content)
|
|
73
|
+
console.print(f" • [bold]Changed Files:[/bold] [yellow]{len(changed_files)} files[/yellow]")
|
|
74
|
+
console.print(f" • [bold]Est. Diff Size:[/bold] [cyan]~{diff_tokens:,} tokens[/cyan] ({len(diff_content):,} chars)")
|
|
75
|
+
|
|
76
|
+
console.print(" • [bold]Modified / New Files:[/bold]")
|
|
77
|
+
max_show = 10
|
|
78
|
+
for idx, p in enumerate(changed_files):
|
|
79
|
+
if idx >= max_show:
|
|
80
|
+
remaining = len(changed_files) - max_show
|
|
81
|
+
console.print(f" [dim]... and {remaining} more file(s)[/dim]")
|
|
82
|
+
break
|
|
83
|
+
try:
|
|
84
|
+
rel_p = p.relative_to(root_path).as_posix()
|
|
85
|
+
except ValueError:
|
|
86
|
+
rel_p = str(p)
|
|
87
|
+
|
|
88
|
+
# Check if new file or modified
|
|
89
|
+
try:
|
|
90
|
+
rel_path_obj = p.relative_to(root_path)
|
|
91
|
+
anchor_file_content = anchor.read_anchor_file(rel_path_obj)
|
|
92
|
+
except Exception:
|
|
93
|
+
anchor_file_content = ""
|
|
94
|
+
|
|
95
|
+
status_label = "[green][NEW] [/green]" if not anchor_file_content else "[yellow][MODIFIED][/yellow]"
|
|
96
|
+
console.print(f" {status_label} {rel_p}")
|
|
97
|
+
|
|
98
|
+
# 3. Artifacts Summary Table
|
|
99
|
+
console.print("\n[bold blue]📦 Context Artifacts[/bold blue] [dim](.codigest/)[/dim]")
|
|
100
|
+
|
|
101
|
+
artifacts = [
|
|
102
|
+
("snapshot.txt", "Full Context Snapshot", "cdg scan"),
|
|
103
|
+
("changes.diff", "Incremental Diff", "cdg diff"),
|
|
104
|
+
("semdiff.txt", "Semantic AST Diff", "cdg semdiff"),
|
|
105
|
+
("digest.txt", "Architecture Digest", "cdg digest"),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
table = Table(box=None, header_style="bold cyan", padding=(0, 2))
|
|
109
|
+
table.add_column("Artifact", style="bold white")
|
|
110
|
+
table.add_column("Description", style="dim")
|
|
111
|
+
table.add_column("Status")
|
|
112
|
+
table.add_column("Size", justify="right")
|
|
113
|
+
table.add_column("Tokens", justify="right", style="cyan")
|
|
114
|
+
|
|
115
|
+
for filename, desc, cmd_hint in artifacts:
|
|
116
|
+
art_path = codigest_dir / filename
|
|
117
|
+
if art_path.exists() and art_path.is_file():
|
|
118
|
+
stat = art_path.stat()
|
|
119
|
+
size_str = decimal(stat.st_size)
|
|
120
|
+
try:
|
|
121
|
+
text_content = art_path.read_text(encoding="utf-8", errors="replace")
|
|
122
|
+
tokens = tokenizer.estimate_tokens(text_content)
|
|
123
|
+
tokens_str = f"~{tokens:,}"
|
|
124
|
+
except Exception:
|
|
125
|
+
tokens_str = "N/A"
|
|
126
|
+
status_str = "[bold green]Ready[/bold green]"
|
|
127
|
+
else:
|
|
128
|
+
size_str = "-"
|
|
129
|
+
tokens_str = "-"
|
|
130
|
+
status_str = f"[dim]Missing ({cmd_hint})[/dim]"
|
|
131
|
+
|
|
132
|
+
table.add_row(filename, desc, status_str, size_str, tokens_str)
|
|
133
|
+
|
|
134
|
+
console.print(table)
|
|
135
|
+
|
|
136
|
+
# 4. Configuration Summary
|
|
137
|
+
console.print("\n[bold blue]⚙️ Configuration[/bold blue] [dim](.codigest/config.toml)[/dim]")
|
|
138
|
+
config_file = codigest_dir / "config.toml"
|
|
139
|
+
if config_file.exists():
|
|
140
|
+
console.print(" • [bold]Config File:[/bold] [green]Found[/green]")
|
|
141
|
+
else:
|
|
142
|
+
console.print(" • [bold]Config File:[/bold] [yellow]Default settings (no config.toml)[/yellow]")
|
|
143
|
+
|
|
144
|
+
if ctx.config_extensions:
|
|
145
|
+
exts_str = ", ".join(sorted(ctx.config_extensions))
|
|
146
|
+
console.print(f" • [bold]Filter Extensions:[/bold] [cyan]{exts_str}[/cyan]")
|
|
147
|
+
else:
|
|
148
|
+
console.print(" • [bold]Filter Extensions:[/bold] [dim]All non-ignored extensions[/dim]")
|
|
149
|
+
|
|
150
|
+
if ctx.config_ignores:
|
|
151
|
+
console.print(f" • [bold]Extra Ignores:[/bold] {len(ctx.config_ignores)} custom patterns")
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Blob Hash-based Content & AST Cache.
|
|
3
|
+
Provides fast caching for scan snapshot blocks and digest AST summaries.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import hashlib
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional, Dict, Any
|
|
9
|
+
|
|
10
|
+
def compute_git_blob_hash(data: bytes) -> str:
|
|
11
|
+
"""Computes Git-compatible SHA-1 blob hash for raw bytes."""
|
|
12
|
+
header = f"blob {len(data)}\0".encode("utf-8")
|
|
13
|
+
return hashlib.sha1(header + data).hexdigest()
|
|
14
|
+
|
|
15
|
+
def get_file_blob_hash(file_path: Path) -> Optional[str]:
|
|
16
|
+
"""Reads file and computes its Git blob hash."""
|
|
17
|
+
try:
|
|
18
|
+
data = file_path.read_bytes()
|
|
19
|
+
return compute_git_blob_hash(data)
|
|
20
|
+
except Exception:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
class ContentCache:
|
|
24
|
+
def __init__(self, root_path: Path):
|
|
25
|
+
self.root_path = root_path
|
|
26
|
+
self.cache_file = root_path / ".codigest" / "cache.json"
|
|
27
|
+
self._data: Dict[str, Any] = {}
|
|
28
|
+
self.hits = 0
|
|
29
|
+
self.misses = 0
|
|
30
|
+
self._load()
|
|
31
|
+
|
|
32
|
+
def _load(self):
|
|
33
|
+
if self.cache_file.exists():
|
|
34
|
+
try:
|
|
35
|
+
with open(self.cache_file, "r", encoding="utf-8") as f:
|
|
36
|
+
self._data = json.load(f)
|
|
37
|
+
except Exception:
|
|
38
|
+
self._data = {}
|
|
39
|
+
else:
|
|
40
|
+
self._data = {}
|
|
41
|
+
|
|
42
|
+
def save(self):
|
|
43
|
+
try:
|
|
44
|
+
self.cache_file.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
with open(self.cache_file, "w", encoding="utf-8") as f:
|
|
46
|
+
json.dump(self._data, f, ensure_ascii=False, indent=2)
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def get_file_block(self, rel_path: str, blob_hash: str, line_numbers: bool) -> Optional[str]:
|
|
51
|
+
cache_key = f"{rel_path}:{blob_hash}:lines={line_numbers}"
|
|
52
|
+
val = self._data.get("file_blocks", {}).get(cache_key)
|
|
53
|
+
if val is not None:
|
|
54
|
+
self.hits += 1
|
|
55
|
+
else:
|
|
56
|
+
self.misses += 1
|
|
57
|
+
return val
|
|
58
|
+
|
|
59
|
+
def set_file_block(self, rel_path: str, blob_hash: str, line_numbers: bool, block: str):
|
|
60
|
+
if "file_blocks" not in self._data:
|
|
61
|
+
self._data["file_blocks"] = {}
|
|
62
|
+
cache_key = f"{rel_path}:{blob_hash}:lines={line_numbers}"
|
|
63
|
+
self._data["file_blocks"][cache_key] = block
|
|
64
|
+
|
|
65
|
+
def get_summary(self, rel_path: str, blob_hash: str) -> Optional[str]:
|
|
66
|
+
cache_key = f"{rel_path}:{blob_hash}"
|
|
67
|
+
val = self._data.get("summaries", {}).get(cache_key)
|
|
68
|
+
if val is not None:
|
|
69
|
+
self.hits += 1
|
|
70
|
+
else:
|
|
71
|
+
self.misses += 1
|
|
72
|
+
return val
|
|
73
|
+
|
|
74
|
+
def set_summary(self, rel_path: str, blob_hash: str, summary: str):
|
|
75
|
+
if "summaries" not in self._data:
|
|
76
|
+
self._data["summaries"] = {}
|
|
77
|
+
cache_key = f"{rel_path}:{blob_hash}"
|
|
78
|
+
self._data["summaries"][cache_key] = summary
|
|
@@ -10,17 +10,16 @@ from loguru import logger
|
|
|
10
10
|
|
|
11
11
|
class ContextAnchor:
|
|
12
12
|
def __init__(self, root_path: Path):
|
|
13
|
-
self.root = root_path
|
|
14
|
-
self.anchor_dir =
|
|
15
|
-
|
|
16
|
-
self.git_dir = self.anchor_dir / ".shadow_git"
|
|
13
|
+
self.root = root_path.resolve()
|
|
14
|
+
self.anchor_dir = (self.root / ".codigest" / "anchor").resolve()
|
|
15
|
+
self.git_dir = (self.anchor_dir / ".shadow_git").resolve()
|
|
17
16
|
|
|
18
17
|
def has_history(self) -> bool:
|
|
19
18
|
"""Checks if a valid anchor (git repo with commits) exists."""
|
|
20
19
|
return self.git_dir.exists() and (self.git_dir / "HEAD").exists()
|
|
21
20
|
|
|
22
21
|
def _run_git(self, args: list[str], cwd: Path | None = None, check=True) -> str:
|
|
23
|
-
|
|
22
|
+
self.anchor_dir.mkdir(parents=True, exist_ok=True)
|
|
24
23
|
base_cmd = [
|
|
25
24
|
"git",
|
|
26
25
|
"--git-dir", str(self.git_dir),
|
|
@@ -42,11 +41,16 @@ class ContextAnchor:
|
|
|
42
41
|
return (result.stdout or "").strip()
|
|
43
42
|
|
|
44
43
|
def update(self, source_files: list[Path]):
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
self.anchor_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
(self.anchor_dir / ".gitignore").write_text(".shadow_git\n", encoding="utf-8")
|
|
46
|
+
if not self.git_dir.exists() or not (self.git_dir / "HEAD").exists():
|
|
47
|
+
if self.git_dir.exists():
|
|
48
|
+
shutil.rmtree(self.git_dir)
|
|
49
|
+
subprocess.run(
|
|
50
|
+
["git", "init", "--bare", str(self.git_dir)],
|
|
51
|
+
capture_output=True, check=False
|
|
52
|
+
)
|
|
53
|
+
self._run_git(["config", "core.bare", "false"])
|
|
50
54
|
self._run_git(["config", "user.email", "codigest@ai"])
|
|
51
55
|
self._run_git(["config", "user.name", "Context Manager"])
|
|
52
56
|
self._run_git(["config", "core.autocrlf", "false"])
|
|
@@ -63,7 +67,7 @@ class ContextAnchor:
|
|
|
63
67
|
source_rel_paths = set()
|
|
64
68
|
|
|
65
69
|
for src in source_files:
|
|
66
|
-
if ".git" in src.parts:
|
|
70
|
+
if ".git" in src.parts or ".codigest" in src.parts:
|
|
67
71
|
continue
|
|
68
72
|
try:
|
|
69
73
|
rel = src.relative_to(self.root)
|
|
@@ -80,6 +84,7 @@ class ContextAnchor:
|
|
|
80
84
|
if src_stat.st_size == dest_stat.st_size and dest_stat.st_mtime >= src_stat.st_mtime:
|
|
81
85
|
should_copy = False
|
|
82
86
|
|
|
87
|
+
source_rel_paths.add(rel)
|
|
83
88
|
if should_copy:
|
|
84
89
|
shutil.copy2(src, dest)
|
|
85
90
|
except Exception:
|
|
@@ -87,7 +92,7 @@ class ContextAnchor:
|
|
|
87
92
|
|
|
88
93
|
for anchor_file in self.anchor_dir.rglob("*"):
|
|
89
94
|
if anchor_file.is_file():
|
|
90
|
-
if ".
|
|
95
|
+
if ".shadow_git" in anchor_file.parts:
|
|
91
96
|
continue
|
|
92
97
|
|
|
93
98
|
try:
|
|
@@ -99,7 +104,10 @@ class ContextAnchor:
|
|
|
99
104
|
continue
|
|
100
105
|
|
|
101
106
|
self._run_git(["add", "."])
|
|
102
|
-
if self._run_git(["
|
|
107
|
+
if not self._run_git(["rev-parse", "--verify", "HEAD"], check=False):
|
|
108
|
+
self._run_git(["commit", "-m", f"Snapshot: {int(time.time())}"])
|
|
109
|
+
logger.info("Initial context anchor created.")
|
|
110
|
+
elif self._run_git(["diff-index", "--quiet", "HEAD", "--"], check=False) != "":
|
|
103
111
|
self._run_git(["commit", "-m", f"Snapshot: {int(time.time())}"])
|
|
104
112
|
logger.info("Context anchor updated.")
|
|
105
113
|
|
|
@@ -118,7 +126,7 @@ class ContextAnchor:
|
|
|
118
126
|
try:
|
|
119
127
|
current_rel_paths = set()
|
|
120
128
|
for src in current_files:
|
|
121
|
-
if ".git" in src.parts:
|
|
129
|
+
if ".git" in src.parts or ".codigest" in src.parts:
|
|
122
130
|
continue
|
|
123
131
|
try:
|
|
124
132
|
rel = src.relative_to(self.root)
|
|
@@ -149,7 +157,15 @@ class ContextAnchor:
|
|
|
149
157
|
|
|
150
158
|
diff_text = (result.stdout or "")
|
|
151
159
|
diff_text = diff_text.replace("temp_diff_baseline/", "").replace("temp_diff_current/", "")
|
|
152
|
-
|
|
160
|
+
|
|
161
|
+
clean_lines = []
|
|
162
|
+
skip = False
|
|
163
|
+
for line in diff_text.splitlines():
|
|
164
|
+
if line.startswith("diff --git"):
|
|
165
|
+
skip = any(x in line for x in [".shadow_git", ".codigest", ".git"])
|
|
166
|
+
if not skip:
|
|
167
|
+
clean_lines.append(line)
|
|
168
|
+
return "\n".join(clean_lines)
|
|
153
169
|
|
|
154
170
|
finally:
|
|
155
171
|
for d in [temp_current, temp_baseline]:
|
|
@@ -158,7 +174,7 @@ class ContextAnchor:
|
|
|
158
174
|
|
|
159
175
|
def _prune_ignored_files(self, baseline_dir: Path, valid_rel_paths: set[Path]):
|
|
160
176
|
for file_path in baseline_dir.rglob("*"):
|
|
161
|
-
if file_path.is_file() and ".git" not in file_path.parts:
|
|
177
|
+
if file_path.is_file() and ".git" not in file_path.parts and ".codigest" not in file_path.parts and ".shadow_git" not in file_path.parts:
|
|
162
178
|
try:
|
|
163
179
|
rel_path = file_path.relative_to(baseline_dir)
|
|
164
180
|
if rel_path in valid_rel_paths:
|
|
@@ -209,5 +225,6 @@ class ContextAnchor:
|
|
|
209
225
|
if len(parts) >= 4:
|
|
210
226
|
p = parts[-1]
|
|
211
227
|
if p.startswith("b/") or p.startswith("a/"): p = p[2:]
|
|
212
|
-
|
|
213
|
-
|
|
228
|
+
if not any(part in (".codigest", ".git", ".shadow_git") for part in Path(p).parts):
|
|
229
|
+
paths.add(self.root / p)
|
|
230
|
+
return sorted(list(paths))
|
|
@@ -6,7 +6,7 @@ import typer
|
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
from importlib.metadata import version, PackageNotFoundError
|
|
8
8
|
|
|
9
|
-
from .commands import init, scan, tree, digest, diff, semdiff
|
|
9
|
+
from .commands import init, scan, tree, digest, diff, semdiff, status, clean
|
|
10
10
|
|
|
11
11
|
console = Console()
|
|
12
12
|
|
|
@@ -18,6 +18,8 @@ app = typer.Typer(
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
app.add_typer(init.app, name="init")
|
|
21
|
+
app.add_typer(status.app, name="status")
|
|
22
|
+
app.add_typer(clean.app, name="clean")
|
|
21
23
|
app.add_typer(scan.app, name="scan")
|
|
22
24
|
app.add_typer(tree.app, name="tree")
|
|
23
25
|
app.add_typer(digest.app, name="digest")
|
|
@@ -16,11 +16,7 @@ wheels = [
|
|
|
16
16
|
|
|
17
17
|
[[package]]
|
|
18
18
|
name = "codigest"
|
|
19
|
-
|
|
20
|
-
version = "0.3.2"
|
|
21
|
-
=======
|
|
22
|
-
version = "0.3.5"
|
|
23
|
-
>>>>>>> 03f5ec5087ff49559e917b3620b915c9ace496c2
|
|
19
|
+
version = "0.3.6"
|
|
24
20
|
source = { editable = "." }
|
|
25
21
|
dependencies = [
|
|
26
22
|
{ name = "loguru" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|