codigest 0.3.0__tar.gz → 0.3.2__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.0 → codigest-0.3.2}/.github/workflows/publish.yml +3 -5
- {codigest-0.3.0 → codigest-0.3.2}/PKG-INFO +46 -15
- {codigest-0.3.0 → codigest-0.3.2}/README.md +45 -14
- {codigest-0.3.0 → codigest-0.3.2}/pyproject.toml +1 -1
- codigest-0.3.2/src/codigest/__main__.py +4 -0
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/commands/diff.py +10 -1
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/commands/digest.py +1 -5
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/commands/init.py +11 -1
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/commands/scan.py +43 -10
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/commands/semdiff.py +20 -16
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/prompts.py +89 -78
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/scanner.py +40 -8
- codigest-0.3.2/src/codigest/core/tags.py +38 -0
- codigest-0.3.2/src/codigest/main.py +65 -0
- {codigest-0.3.0 → codigest-0.3.2}/uv.lock +1 -35
- codigest-0.3.0/.github/workflows/main.yml +0 -30
- codigest-0.3.0/src/codigest/core/tags.py +0 -75
- codigest-0.3.0/src/codigest/main.py +0 -34
- {codigest-0.3.0 → codigest-0.3.2}/.gitignore +0 -0
- {codigest-0.3.0 → codigest-0.3.2}/.python-version +0 -0
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/__init__.py +0 -0
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/commands/tree.py +0 -0
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/config.py +0 -0
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/git_ops.py +0 -0
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/processor.py +0 -0
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/semdiff.py +0 -0
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/shadow.py +0 -0
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/structure.py +0 -0
- {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/tokenizer.py +0 -0
|
@@ -4,8 +4,6 @@ on:
|
|
|
4
4
|
push:
|
|
5
5
|
tags:
|
|
6
6
|
- 'v*' # v0.3.0 태그 푸시 시 배포 실행
|
|
7
|
-
# branches:
|
|
8
|
-
# - main # main 브랜치 푸시 시 문서 배포 실행 (옵션)
|
|
9
7
|
|
|
10
8
|
jobs:
|
|
11
9
|
build-n-publish:
|
|
@@ -21,15 +19,15 @@ jobs:
|
|
|
21
19
|
steps:
|
|
22
20
|
- uses: actions/checkout@v4
|
|
23
21
|
|
|
24
|
-
#
|
|
22
|
+
# uv 설치 (가장 빠르고 확실한 방법)
|
|
25
23
|
- name: Install uv
|
|
26
24
|
uses: astral-sh/setup-uv@v5
|
|
27
25
|
|
|
28
|
-
#
|
|
26
|
+
# Python 3.14 설정 (uv 이용)
|
|
29
27
|
- name: Set up Python
|
|
30
28
|
run: uv python install 3.14
|
|
31
29
|
|
|
32
|
-
#
|
|
30
|
+
# 의존성 설치 및 빌드 (uv build 하나로 해결)
|
|
33
31
|
- name: Build package
|
|
34
32
|
run: uv build
|
|
35
33
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codigest
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
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
|
|
@@ -25,33 +25,61 @@ Description-Content-Type: text/markdown
|
|
|
25
25
|
|
|
26
26
|
# Codigest
|
|
27
27
|
|
|
28
|
-
Codigest is a CLI tool designed to extract, structure, and track the context of your codebase for Large Language Models (LLMs).
|
|
28
|
+
Codigest is a standalone CLI tool designed to extract, structure, and track the context of your codebase for Large Language Models (LLMs).
|
|
29
29
|
|
|
30
30
|
Unlike simple copy-paste tools, Codigest employs a **Context Anchor** system (Shadow Git) to track changes locally without polluting your main version control history. It also features **Semantic Analysis** to detect structural changes in your code.
|
|
31
31
|
|
|
32
32
|
## Core Philosophy
|
|
33
33
|
|
|
34
|
-
* **Read-Only & Safe**: Codigest never modifies your source code. It only reads, analyzes, and formats context.
|
|
35
|
-
* **Context-Aware**: Instead of dumping raw text, it structures code into XML snapshots designed for LLM comprehension.
|
|
36
|
-
* **Session-Based Tracking**: It maintains an internal anchor to track "work-in-progress" changes independently of your Git commits.
|
|
37
|
-
* **
|
|
34
|
+
* **Read-Only & Safe**: Codigest never modifies your source code. It only reads, analyzes, and formats context.
|
|
35
|
+
* **Context-Aware**: Instead of dumping raw text, it structures code into XML snapshots designed for LLM comprehension.
|
|
36
|
+
* **Session-Based Tracking**: It maintains an internal anchor to track "work-in-progress" changes independently of your Git commits.
|
|
37
|
+
* **Environment Isolated**: Runs in its own isolated Python 3.14 environment via `uv`, ensuring it never conflicts with your project's dependencies.
|
|
38
38
|
|
|
39
39
|
## Installation
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Codigest runs as a global tool. You don't need to add it to your project's `requirements.txt`.
|
|
42
|
+
We strongly recommend using **uv** for the best experience.
|
|
43
|
+
|
|
44
|
+
### Step 1: Install `uv`
|
|
45
|
+
|
|
46
|
+
If you don't have `uv` installed, use **Winget** (Windows) or curl (macOS/Linux).
|
|
47
|
+
|
|
48
|
+
```powershell
|
|
49
|
+
# Windows (via Winget)
|
|
50
|
+
winget install --id astral-sh.uv
|
|
51
|
+
|
|
52
|
+
# macOS/Linux
|
|
53
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Step 2: Install `codigest`
|
|
57
|
+
|
|
58
|
+
Install Codigest globally. `uv` will automatically manage the required Python 3.14 environment for you.
|
|
42
59
|
|
|
43
60
|
```bash
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
uv tool install codigest
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Step 3: Update Path (Important\!)
|
|
65
|
+
|
|
66
|
+
To run `codigest` from any terminal, ensure your tools directory is in your PATH.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv tool update-shell
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
*\> **Note:** Restart your terminal (or VS Code) after this step to apply changes.*
|
|
73
|
+
|
|
74
|
+
-----
|
|
49
75
|
|
|
50
76
|
## Workflow & Commands
|
|
51
77
|
|
|
78
|
+
Once installed, you can use `codigest` command anywhere.
|
|
79
|
+
|
|
52
80
|
### 1\. Initialization
|
|
53
81
|
|
|
54
|
-
Sets up the `.codigest` directory and captures the initial baseline anchor.
|
|
82
|
+
Sets up the `.codigest` directory and captures the initial baseline anchor.
|
|
55
83
|
|
|
56
84
|
```bash
|
|
57
85
|
codigest init
|
|
@@ -66,7 +94,7 @@ Scans the entire codebase and generates a structured XML snapshot.
|
|
|
66
94
|
* **Side Effect:** Updates the internal Context Anchor to the current state.
|
|
67
95
|
|
|
68
96
|
```bash
|
|
69
|
-
codigest scan --
|
|
97
|
+
codigest scan --message "Initial project context"
|
|
70
98
|
```
|
|
71
99
|
|
|
72
100
|
### 3\. Incremental Changes (`diff`)
|
|
@@ -77,6 +105,7 @@ Tracks text-based changes between the last `scan` and the current working tree.
|
|
|
77
105
|
* **Use Case:** "I modified 3 files. Here is exactly what changed since the last snapshot."
|
|
78
106
|
* **Note:** This is based on the internal Shadow Git, not your project's Git history.
|
|
79
107
|
|
|
108
|
+
|
|
80
109
|
```bash
|
|
81
110
|
codigest diff
|
|
82
111
|
```
|
|
@@ -89,6 +118,7 @@ Analyzes **structural changes** (AST-based) rather than line-by-line text differ
|
|
|
89
118
|
* **Use Case:** "I refactored the API. Show me which functions were added, removed, or had their signatures changed."
|
|
90
119
|
* **Benefit:** Significantly reduces token usage compared to raw diffs by ignoring formatting/comment changes.
|
|
91
120
|
|
|
121
|
+
|
|
92
122
|
```bash
|
|
93
123
|
codigest semdiff
|
|
94
124
|
```
|
|
@@ -100,12 +130,13 @@ Generates a high-level outline of the project structure (Classes, Functions, Met
|
|
|
100
130
|
* **Output:** `.codigest/digest.xml`
|
|
101
131
|
* **Use Case:** "Don't read the implementation details. Just understand the class hierarchy and available methods."
|
|
102
132
|
|
|
103
|
-
<!-- end list -->
|
|
104
133
|
|
|
105
134
|
```bash
|
|
106
135
|
codigest digest
|
|
107
136
|
```
|
|
108
137
|
|
|
138
|
+
-----
|
|
139
|
+
|
|
109
140
|
## Configuration
|
|
110
141
|
|
|
111
142
|
You can customize behavior in `.codigest/config.toml`.
|
|
@@ -1,32 +1,60 @@
|
|
|
1
1
|
# Codigest
|
|
2
2
|
|
|
3
|
-
Codigest is a CLI tool designed to extract, structure, and track the context of your codebase for Large Language Models (LLMs).
|
|
3
|
+
Codigest is a standalone CLI tool designed to extract, structure, and track the context of your codebase for Large Language Models (LLMs).
|
|
4
4
|
|
|
5
5
|
Unlike simple copy-paste tools, Codigest employs a **Context Anchor** system (Shadow Git) to track changes locally without polluting your main version control history. It also features **Semantic Analysis** to detect structural changes in your code.
|
|
6
6
|
|
|
7
7
|
## Core Philosophy
|
|
8
8
|
|
|
9
|
-
* **Read-Only & Safe**: Codigest never modifies your source code. It only reads, analyzes, and formats context.
|
|
10
|
-
* **Context-Aware**: Instead of dumping raw text, it structures code into XML snapshots designed for LLM comprehension.
|
|
11
|
-
* **Session-Based Tracking**: It maintains an internal anchor to track "work-in-progress" changes independently of your Git commits.
|
|
12
|
-
* **
|
|
9
|
+
* **Read-Only & Safe**: Codigest never modifies your source code. It only reads, analyzes, and formats context.
|
|
10
|
+
* **Context-Aware**: Instead of dumping raw text, it structures code into XML snapshots designed for LLM comprehension.
|
|
11
|
+
* **Session-Based Tracking**: It maintains an internal anchor to track "work-in-progress" changes independently of your Git commits.
|
|
12
|
+
* **Environment Isolated**: Runs in its own isolated Python 3.14 environment via `uv`, ensuring it never conflicts with your project's dependencies.
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Codigest runs as a global tool. You don't need to add it to your project's `requirements.txt`.
|
|
17
|
+
We strongly recommend using **uv** for the best experience.
|
|
18
|
+
|
|
19
|
+
### Step 1: Install `uv`
|
|
20
|
+
|
|
21
|
+
If you don't have `uv` installed, use **Winget** (Windows) or curl (macOS/Linux).
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
# Windows (via Winget)
|
|
25
|
+
winget install --id astral-sh.uv
|
|
26
|
+
|
|
27
|
+
# macOS/Linux
|
|
28
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Step 2: Install `codigest`
|
|
32
|
+
|
|
33
|
+
Install Codigest globally. `uv` will automatically manage the required Python 3.14 environment for you.
|
|
17
34
|
|
|
18
35
|
```bash
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
uv tool install codigest
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Step 3: Update Path (Important\!)
|
|
40
|
+
|
|
41
|
+
To run `codigest` from any terminal, ensure your tools directory is in your PATH.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv tool update-shell
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
*\> **Note:** Restart your terminal (or VS Code) after this step to apply changes.*
|
|
48
|
+
|
|
49
|
+
-----
|
|
24
50
|
|
|
25
51
|
## Workflow & Commands
|
|
26
52
|
|
|
53
|
+
Once installed, you can use `codigest` command anywhere.
|
|
54
|
+
|
|
27
55
|
### 1\. Initialization
|
|
28
56
|
|
|
29
|
-
Sets up the `.codigest` directory and captures the initial baseline anchor.
|
|
57
|
+
Sets up the `.codigest` directory and captures the initial baseline anchor.
|
|
30
58
|
|
|
31
59
|
```bash
|
|
32
60
|
codigest init
|
|
@@ -41,7 +69,7 @@ Scans the entire codebase and generates a structured XML snapshot.
|
|
|
41
69
|
* **Side Effect:** Updates the internal Context Anchor to the current state.
|
|
42
70
|
|
|
43
71
|
```bash
|
|
44
|
-
codigest scan --
|
|
72
|
+
codigest scan --message "Initial project context"
|
|
45
73
|
```
|
|
46
74
|
|
|
47
75
|
### 3\. Incremental Changes (`diff`)
|
|
@@ -52,6 +80,7 @@ Tracks text-based changes between the last `scan` and the current working tree.
|
|
|
52
80
|
* **Use Case:** "I modified 3 files. Here is exactly what changed since the last snapshot."
|
|
53
81
|
* **Note:** This is based on the internal Shadow Git, not your project's Git history.
|
|
54
82
|
|
|
83
|
+
|
|
55
84
|
```bash
|
|
56
85
|
codigest diff
|
|
57
86
|
```
|
|
@@ -64,6 +93,7 @@ Analyzes **structural changes** (AST-based) rather than line-by-line text differ
|
|
|
64
93
|
* **Use Case:** "I refactored the API. Show me which functions were added, removed, or had their signatures changed."
|
|
65
94
|
* **Benefit:** Significantly reduces token usage compared to raw diffs by ignoring formatting/comment changes.
|
|
66
95
|
|
|
96
|
+
|
|
67
97
|
```bash
|
|
68
98
|
codigest semdiff
|
|
69
99
|
```
|
|
@@ -75,12 +105,13 @@ Generates a high-level outline of the project structure (Classes, Functions, Met
|
|
|
75
105
|
* **Output:** `.codigest/digest.xml`
|
|
76
106
|
* **Use Case:** "Don't read the implementation details. Just understand the class hierarchy and available methods."
|
|
77
107
|
|
|
78
|
-
<!-- end list -->
|
|
79
108
|
|
|
80
109
|
```bash
|
|
81
110
|
codigest digest
|
|
82
111
|
```
|
|
83
112
|
|
|
113
|
+
-----
|
|
114
|
+
|
|
84
115
|
## Configuration
|
|
85
116
|
|
|
86
117
|
You can customize behavior in `.codigest/config.toml`.
|
|
@@ -12,6 +12,15 @@ console = Console()
|
|
|
12
12
|
|
|
13
13
|
@app.callback(invoke_without_command=True)
|
|
14
14
|
def handle(
|
|
15
|
+
# target 인자 추가
|
|
16
|
+
target: Path = typer.Argument(
|
|
17
|
+
Path.cwd(),
|
|
18
|
+
help="Target directory to check diff",
|
|
19
|
+
exists=True,
|
|
20
|
+
file_okay=False,
|
|
21
|
+
dir_okay=True,
|
|
22
|
+
resolve_path=True
|
|
23
|
+
),
|
|
15
24
|
copy: bool = typer.Option(True, help="Auto-copy to clipboard"),
|
|
16
25
|
save: bool = typer.Option(True, help="Save to .codigest/changes.diff"),
|
|
17
26
|
message: str = typer.Option("", "--message", "-m", help="Add specific instruction context"),
|
|
@@ -20,7 +29,7 @@ def handle(
|
|
|
20
29
|
[Context Update] Shows changes since the last 'codigest scan'.
|
|
21
30
|
Useful for updating LLM context without re-uploading everything.
|
|
22
31
|
"""
|
|
23
|
-
root_path =
|
|
32
|
+
root_path = target
|
|
24
33
|
anchor = shadow.ContextAnchor(root_path)
|
|
25
34
|
|
|
26
35
|
# 1. Check Baseline Existence
|
|
@@ -49,11 +49,7 @@ def handle(
|
|
|
49
49
|
|
|
50
50
|
if summary:
|
|
51
51
|
rel_path = file_path.relative_to(root_path).as_posix()
|
|
52
|
-
block = tags.
|
|
53
|
-
<file path="{rel_path}">
|
|
54
|
-
{summary}
|
|
55
|
-
</file>
|
|
56
|
-
""")
|
|
52
|
+
block = tags.file(rel_path, summary)
|
|
57
53
|
summary_blocks.append(block)
|
|
58
54
|
except Exception:
|
|
59
55
|
continue
|
|
@@ -35,12 +35,22 @@ structure = "toon"
|
|
|
35
35
|
|
|
36
36
|
@app.callback(invoke_without_command=True)
|
|
37
37
|
def handle(
|
|
38
|
+
# target 인자 추가 (기본값: 현재 위치)
|
|
39
|
+
target: Path = typer.Argument(
|
|
40
|
+
Path.cwd(),
|
|
41
|
+
help="Target directory to initialize",
|
|
42
|
+
exists=True,
|
|
43
|
+
file_okay=False,
|
|
44
|
+
dir_okay=True,
|
|
45
|
+
resolve_path=True
|
|
46
|
+
),
|
|
38
47
|
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing config"),
|
|
39
48
|
):
|
|
40
49
|
"""
|
|
41
50
|
Initialize the .codigest environment and capture the initial state.
|
|
42
51
|
"""
|
|
43
|
-
root_path =
|
|
52
|
+
root_path = target # Typer가 이미 resolve_path=True로 절대경로 변환해줌
|
|
53
|
+
|
|
44
54
|
config_dir = root_path / ".codigest"
|
|
45
55
|
config_file = config_dir / "config.toml"
|
|
46
56
|
gitignore_file = root_path / ".gitignore"
|
|
@@ -28,22 +28,52 @@ def _load_config_filters(root_path: Path):
|
|
|
28
28
|
|
|
29
29
|
return extensions, exclude_patterns
|
|
30
30
|
|
|
31
|
+
def _find_project_root(start_path: Path) -> Path:
|
|
32
|
+
"""
|
|
33
|
+
.codigest 폴더나 .git 폴더가 있는 상위 디렉토리를 찾습니다.
|
|
34
|
+
못 찾으면 start_path를 반환합니다.
|
|
35
|
+
"""
|
|
36
|
+
current = start_path.resolve()
|
|
37
|
+
for parent in [current] + list(current.parents):
|
|
38
|
+
if (parent / ".codigest").exists() or (parent / ".git").exists():
|
|
39
|
+
return parent
|
|
40
|
+
return start_path
|
|
41
|
+
|
|
31
42
|
@app.callback(invoke_without_command=True)
|
|
32
43
|
def handle(
|
|
33
|
-
|
|
44
|
+
# targets를 리스트로 받음 (여러 폴더 지정 가능)
|
|
45
|
+
targets: list[Path] = typer.Argument(
|
|
46
|
+
None,
|
|
47
|
+
help="Specific files or directories to scan (Scope)",
|
|
48
|
+
exists=True,
|
|
49
|
+
resolve_path=True
|
|
50
|
+
),
|
|
34
51
|
output: str = typer.Option("snapshot.xml", help="Output filename inside .codigest/"),
|
|
35
52
|
all: bool = typer.Option(False, "--all", "-a", help="Ignore config filters"),
|
|
36
53
|
message: str = typer.Option("", "--message", "-m", help="Add specific instruction"),
|
|
37
54
|
line_numbers: bool = typer.Option(False, "--lines", "-l", help="Add line numbers to code blocks"),
|
|
38
55
|
):
|
|
39
56
|
"""
|
|
40
|
-
|
|
57
|
+
Scans the codebase.
|
|
58
|
+
If TARGETS provided, only scans those paths within the project.
|
|
41
59
|
"""
|
|
42
|
-
|
|
60
|
+
# 1. 프로젝트 루트 결정 (현재 위치 기준 상위 탐색)
|
|
61
|
+
root_path = _find_project_root(Path.cwd())
|
|
62
|
+
|
|
63
|
+
# 2. Scope 설정
|
|
64
|
+
# 입력된 targets가 있으면 사용, 없으면 [root_path] 전체
|
|
65
|
+
scan_scope = targets if targets else None
|
|
66
|
+
|
|
67
|
+
# 검증: targets가 root_path 밖에 있으면 경고 (옵션)
|
|
68
|
+
if scan_scope:
|
|
69
|
+
for p in scan_scope:
|
|
70
|
+
if not p.is_relative_to(root_path):
|
|
71
|
+
console.print(f"[yellow]⚠️ Warning: {p.name} is outside project root {root_path.name}[/yellow]")
|
|
72
|
+
|
|
43
73
|
artifact_dir = root_path / ".codigest"
|
|
44
74
|
|
|
45
75
|
if not artifact_dir.exists():
|
|
46
|
-
console.print("[yellow]⚠️ .codigest directory missing. Running init...[/yellow]")
|
|
76
|
+
console.print(f"[yellow]⚠️ .codigest directory missing in {root_path.name}. Running init...[/yellow]")
|
|
47
77
|
try:
|
|
48
78
|
artifact_dir.mkdir(exist_ok=True)
|
|
49
79
|
except PermissionError:
|
|
@@ -68,7 +98,12 @@ def handle(
|
|
|
68
98
|
):
|
|
69
99
|
|
|
70
100
|
# File Discovery
|
|
71
|
-
files = scanner.scan_project(
|
|
101
|
+
files = scanner.scan_project(
|
|
102
|
+
root_path,
|
|
103
|
+
extensions,
|
|
104
|
+
extra_ignores,
|
|
105
|
+
include_paths=scan_scope
|
|
106
|
+
)
|
|
72
107
|
|
|
73
108
|
# Pre-scan Diff Report
|
|
74
109
|
if anchor.has_history():
|
|
@@ -89,11 +124,9 @@ def handle(
|
|
|
89
124
|
rel_path = file_path.relative_to(root_path).as_posix()
|
|
90
125
|
try:
|
|
91
126
|
content = processor.read_file_content(file_path, add_line_numbers=line_numbers)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
</file>
|
|
96
|
-
""")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
block = tags.file(rel_path, content)
|
|
97
130
|
file_blocks.append(block)
|
|
98
131
|
except Exception:
|
|
99
132
|
continue
|
|
@@ -61,38 +61,42 @@ def handle(
|
|
|
61
61
|
# Read Content (Handle Missing Files for Deletion)
|
|
62
62
|
try:
|
|
63
63
|
new_code = file_path.read_text(encoding="utf-8") if file_path.exists() else ""
|
|
64
|
-
except:
|
|
64
|
+
except:
|
|
65
|
+
new_code = ""
|
|
65
66
|
|
|
66
67
|
old_code = anchor.read_anchor_file(rel_path)
|
|
67
68
|
|
|
68
69
|
# Case: File Added (No old code) -> All symbols ADDED
|
|
69
70
|
# Case: File Deleted (No new code) -> All symbols REMOVED
|
|
70
71
|
# Case: Modified -> Semantic Compare
|
|
71
|
-
|
|
72
|
+
|
|
72
73
|
changes = semdiff.compare(old_code, new_code)
|
|
73
|
-
|
|
74
|
+
|
|
74
75
|
if changes:
|
|
75
76
|
change_lines = []
|
|
76
77
|
for ch in changes:
|
|
77
|
-
if ch.change_type == "ADDED":
|
|
78
|
-
|
|
79
|
-
elif ch.change_type == "
|
|
80
|
-
|
|
78
|
+
if ch.change_type == "ADDED":
|
|
79
|
+
icon = "➕ [ADDED] "
|
|
80
|
+
elif ch.change_type == "REMOVED":
|
|
81
|
+
icon = "➖ [REMOVED] "
|
|
82
|
+
elif ch.change_type == "MODIFIED":
|
|
83
|
+
icon = "⚠️ [SIGNATURE]" # 중요: 시그니처 변경
|
|
84
|
+
else:
|
|
85
|
+
icon = "✏️ [LOGIC] "
|
|
81
86
|
|
|
82
87
|
detail = f" :: {ch.details}" if ch.details else ""
|
|
83
88
|
line = f"{icon} {ch.symbol.type} {ch.symbol.name}{ch.symbol.signature}{detail}"
|
|
84
89
|
change_lines.append(line)
|
|
85
|
-
|
|
90
|
+
|
|
86
91
|
# File Deleted Tagging
|
|
87
92
|
file_status = ""
|
|
88
|
-
if not new_code:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
""")
|
|
93
|
+
if not new_code:
|
|
94
|
+
file_status = " (DELETED)"
|
|
95
|
+
elif not old_code:
|
|
96
|
+
file_status = " (NEW)"
|
|
97
|
+
|
|
98
|
+
raw_body = chr(10).join(change_lines)
|
|
99
|
+
block = tags.file(rel_path.as_posix(), raw_body, status=file_status)
|
|
96
100
|
reports.append(block)
|
|
97
101
|
|
|
98
102
|
progress.update(task, completed=100)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Prompt Management Module.
|
|
3
|
-
|
|
3
|
+
Templates are aligned flush-left to prevent indentation issues when injecting large code blocks.
|
|
4
4
|
"""
|
|
5
5
|
import tomllib
|
|
6
6
|
from pathlib import Path
|
|
@@ -14,102 +14,111 @@ RenderFunc = Callable[..., str]
|
|
|
14
14
|
def _default_snapshot(project_name: str, tree_structure: str, source_code: str, instruction: str = "") -> str:
|
|
15
15
|
instruction_block = ""
|
|
16
16
|
if instruction:
|
|
17
|
+
safe_instruction = tags.escape_xml_value(instruction)
|
|
17
18
|
instruction_block = tags.dedent(f"""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
19
|
+
<user_instruction>
|
|
20
|
+
{safe_instruction}
|
|
21
|
+
</user_instruction>
|
|
22
|
+
""")
|
|
23
|
+
|
|
24
|
+
# Note: source_code is assumed to be already formatted via tags.file()
|
|
25
|
+
return tags.dedent(f"""
|
|
26
|
+
[SYSTEM: CODIGEST - INITIAL CONTEXT]
|
|
27
|
+
You are an expert AI developer. I am providing the full context of a project named "{project_name}".
|
|
28
|
+
Please digest this structure and code to build your internal mental model.
|
|
29
|
+
{instruction_block}
|
|
30
|
+
|
|
31
|
+
<project_root>
|
|
32
|
+
{project_name}
|
|
33
|
+
</project_root>
|
|
34
|
+
|
|
35
|
+
<project_structure>
|
|
36
|
+
{tree_structure}
|
|
37
|
+
</project_structure>
|
|
38
|
+
|
|
39
|
+
<source_code>
|
|
40
|
+
{source_code}
|
|
41
|
+
</source_code>
|
|
42
|
+
""")
|
|
41
43
|
|
|
42
44
|
# 2. Diff Template (codigest diff)
|
|
43
45
|
def _default_diff(project_name: str, context_message: str, diff_content: str, instruction: str = "") -> str:
|
|
44
46
|
instruction_block = ""
|
|
45
47
|
if instruction:
|
|
48
|
+
safe_instruction = tags.escape_xml_value(instruction)
|
|
46
49
|
instruction_block = tags.dedent(f"""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
<user_instruction>
|
|
51
|
+
{safe_instruction}
|
|
52
|
+
</user_instruction>
|
|
53
|
+
""")
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Here are the latest changes for the project "{project_name}".
|
|
55
|
-
{context_message}.
|
|
56
|
-
{instruction_block}
|
|
55
|
+
# Escape raw diff content to prevent XML injection
|
|
56
|
+
safe_diff = tags.escape_xml_value(diff_content)
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
return tags.dedent(f"""
|
|
59
|
+
[SYSTEM: CODIGEST - INCREMENTAL UPDATE]
|
|
60
|
+
Here are the latest changes for the project "{project_name}".
|
|
61
|
+
{context_message}.
|
|
62
|
+
{instruction_block}
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
Focus on these modifications to update your context.
|
|
65
|
+
|
|
66
|
+
<git_diff>
|
|
67
|
+
{safe_diff}
|
|
68
|
+
</git_diff>
|
|
69
|
+
""")
|
|
64
70
|
|
|
65
71
|
# 3. SemDiff Template (codigest semdiff)
|
|
66
72
|
def _default_semdiff(project_name: str, context_message: str, semdiff_content: str, instruction: str = "") -> str:
|
|
67
73
|
instruction_block = ""
|
|
68
74
|
if instruction:
|
|
75
|
+
safe_instruction = tags.escape_xml_value(instruction)
|
|
69
76
|
instruction_block = tags.dedent(f"""
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return tags.dedent(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
77
|
+
<user_instruction>
|
|
78
|
+
{safe_instruction}
|
|
79
|
+
</user_instruction>
|
|
80
|
+
""")
|
|
81
|
+
|
|
82
|
+
return tags.dedent(f"""
|
|
83
|
+
[SYSTEM: CODIGEST - SEMANTIC ANALYSIS]
|
|
84
|
+
Structural changes detected in "{project_name}".
|
|
85
|
+
{context_message}.
|
|
86
|
+
{instruction_block}
|
|
87
|
+
|
|
88
|
+
This report focuses on High-Level Architecture changes (Classes, Functions, Signatures).
|
|
89
|
+
|
|
90
|
+
<semantic_diff>
|
|
91
|
+
{semdiff_content}
|
|
92
|
+
</semantic_diff>
|
|
93
|
+
""")
|
|
87
94
|
|
|
88
|
-
# 4. Digest Template (codigest digest)
|
|
95
|
+
# 4. Digest Template (codigest digest)
|
|
89
96
|
def _default_digest(project_name: str, tree_structure: str, digest_content: str, instruction: str = "") -> str:
|
|
90
97
|
instruction_block = ""
|
|
91
98
|
if instruction:
|
|
99
|
+
safe_instruction = tags.escape_xml_value(instruction)
|
|
92
100
|
instruction_block = tags.dedent(f"""
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return tags.dedent(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
101
|
+
<user_instruction>
|
|
102
|
+
{safe_instruction}
|
|
103
|
+
</user_instruction>
|
|
104
|
+
""")
|
|
105
|
+
|
|
106
|
+
return tags.dedent(f"""
|
|
107
|
+
[SYSTEM: CODIGEST - ARCHITECTURE DIGEST]
|
|
108
|
+
Here is the high-level architecture of "{project_name}".
|
|
109
|
+
This contains ONLY definitions (Classes, Functions), no implementation details.
|
|
110
|
+
Use this to understand the project structure and relationships.
|
|
111
|
+
{instruction_block}
|
|
112
|
+
|
|
113
|
+
<project_structure>
|
|
114
|
+
{tree_structure}
|
|
115
|
+
</project_structure>
|
|
116
|
+
|
|
117
|
+
<code_digest>
|
|
118
|
+
{digest_content}
|
|
119
|
+
</code_digest>
|
|
120
|
+
""")
|
|
121
|
+
|
|
113
122
|
|
|
114
123
|
# Registry of default implementations
|
|
115
124
|
# Ensure ALL keys correspond to prompt_engine.render calls
|
|
@@ -117,7 +126,7 @@ DEFAULT_RENDERERS: Dict[str, RenderFunc] = {
|
|
|
117
126
|
"snapshot": _default_snapshot,
|
|
118
127
|
"diff": _default_diff,
|
|
119
128
|
"semdiff": _default_semdiff,
|
|
120
|
-
"digest": _default_digest,
|
|
129
|
+
"digest": _default_digest,
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
class PromptEngine:
|
|
@@ -146,6 +155,7 @@ class PromptEngine:
|
|
|
146
155
|
# 1. Check for TOML Override (String-based)
|
|
147
156
|
if key in self.overrides:
|
|
148
157
|
raw_template = self.overrides[key]
|
|
158
|
+
# Safety: Automatically escape values when using user-defined templates
|
|
149
159
|
safe_kwargs = {
|
|
150
160
|
k: tags.escape_xml_value(v) for k, v in kwargs.items()
|
|
151
161
|
}
|
|
@@ -156,9 +166,10 @@ class PromptEngine:
|
|
|
156
166
|
|
|
157
167
|
# 2. Use Default (Code-based t-string)
|
|
158
168
|
if key in DEFAULT_RENDERERS:
|
|
169
|
+
# Default functions handle their own escaping logic internally where needed
|
|
159
170
|
return DEFAULT_RENDERERS[key](**kwargs)
|
|
160
171
|
|
|
161
172
|
return f"Error: Prompt template '{key}' not found."
|
|
162
173
|
|
|
163
174
|
def get_engine(root_path: Path) -> PromptEngine:
|
|
164
|
-
return PromptEngine(root_path)
|
|
175
|
+
return PromptEngine(root_path)
|
|
@@ -16,11 +16,18 @@ ALWAYS_IGNORE = [
|
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
class ProjectScanner:
|
|
19
|
-
def __init__(
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
root_path: Path,
|
|
22
|
+
extensions: Optional[set[str]] = None,
|
|
23
|
+
extra_ignores: Optional[list[str]] = None,
|
|
24
|
+
include_paths: Optional[list[Path]] = None
|
|
25
|
+
):
|
|
20
26
|
self.root_path = root_path
|
|
21
27
|
self.extensions = extensions
|
|
22
28
|
self.extra_ignores = extra_ignores or []
|
|
23
29
|
self.ignore_spec = self._load_gitignore()
|
|
30
|
+
self.include_paths = include_paths
|
|
24
31
|
|
|
25
32
|
def _load_gitignore(self) -> pathspec.PathSpec:
|
|
26
33
|
"""Loads .gitignore and combines with ALWAYS_IGNORE and extra_ignores."""
|
|
@@ -56,17 +63,36 @@ class ProjectScanner:
|
|
|
56
63
|
|
|
57
64
|
return self.ignore_spec.match_file(rel_path)
|
|
58
65
|
|
|
66
|
+
def _is_included(self, entry: Path) -> bool:
|
|
67
|
+
"""파일이 지정된 include_paths 범위 안에 있는지 확인"""
|
|
68
|
+
if not self.include_paths:
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
for path in self.include_paths:
|
|
72
|
+
try:
|
|
73
|
+
entry.relative_to(path)
|
|
74
|
+
return True
|
|
75
|
+
except ValueError:
|
|
76
|
+
continue
|
|
77
|
+
return False
|
|
78
|
+
|
|
59
79
|
def scan(self) -> list[Path]:
|
|
60
80
|
"""
|
|
61
81
|
Walks the directory tree and returns valid files.
|
|
62
82
|
(Modernized from old core.py's stack-based approach)
|
|
63
83
|
"""
|
|
64
84
|
valid_files = []
|
|
65
|
-
|
|
85
|
+
start_dirs = self.include_paths if self.include_paths else [self.root_path]
|
|
66
86
|
# Use rglob for simplicity, but we must manually filter dirs to respect gitignore
|
|
67
87
|
# For better performance on large repos, we use a manual walk similar to the old code
|
|
68
|
-
dirs_stack = [
|
|
69
|
-
|
|
88
|
+
dirs_stack = []
|
|
89
|
+
for p in start_dirs:
|
|
90
|
+
if p.is_file():
|
|
91
|
+
if not self.is_ignored(p):
|
|
92
|
+
valid_files.append(p)
|
|
93
|
+
elif p.is_dir():
|
|
94
|
+
dirs_stack.append(p)
|
|
95
|
+
|
|
70
96
|
while dirs_stack:
|
|
71
97
|
current = dirs_stack.pop()
|
|
72
98
|
|
|
@@ -83,6 +109,7 @@ class ProjectScanner:
|
|
|
83
109
|
|
|
84
110
|
if entry.is_dir():
|
|
85
111
|
dirs_stack.append(entry)
|
|
112
|
+
|
|
86
113
|
elif entry.is_file():
|
|
87
114
|
# Extension Filter Check
|
|
88
115
|
if self.extensions and entry.suffix.lower() not in self.extensions:
|
|
@@ -91,9 +118,14 @@ class ProjectScanner:
|
|
|
91
118
|
continue
|
|
92
119
|
|
|
93
120
|
valid_files.append(entry)
|
|
94
|
-
|
|
95
|
-
return sorted(valid_files)
|
|
96
121
|
|
|
97
|
-
|
|
98
|
-
|
|
122
|
+
return sorted(list(set(valid_files)))
|
|
123
|
+
|
|
124
|
+
def scan_project(
|
|
125
|
+
root_path: Path,
|
|
126
|
+
extensions: set[str] = None,
|
|
127
|
+
extra_ignores: list[str] = None,
|
|
128
|
+
include_paths: list[Path] = None
|
|
129
|
+
) -> list[Path]:
|
|
130
|
+
scanner = ProjectScanner(root_path, extensions, extra_ignores, include_paths)
|
|
99
131
|
return scanner.scan()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core Template Engine & XML Factory.
|
|
3
|
+
Centralizes XML escaping and block generation logic.
|
|
4
|
+
"""
|
|
5
|
+
import html
|
|
6
|
+
import textwrap
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
def escape_xml_value(value: Any) -> str:
|
|
10
|
+
"""[XML] Escapes characters for XML safety."""
|
|
11
|
+
raw_value = str(value)
|
|
12
|
+
safe_value = html.escape(raw_value, quote=False)
|
|
13
|
+
if "</" in safe_value:
|
|
14
|
+
safe_value = safe_value.replace("</", "</")
|
|
15
|
+
return safe_value
|
|
16
|
+
|
|
17
|
+
def dedent(template: str) -> str:
|
|
18
|
+
"""[Plain] Removes common leading whitespace."""
|
|
19
|
+
if not isinstance(template, str):
|
|
20
|
+
template = str(template)
|
|
21
|
+
return textwrap.dedent(template.expandtabs(4)).strip()
|
|
22
|
+
|
|
23
|
+
def xml(template: str) -> str:
|
|
24
|
+
"""[XML] Wrapper for dedent (legacy support)."""
|
|
25
|
+
return dedent(template)
|
|
26
|
+
|
|
27
|
+
# 파일 블록 생성기 (이스케이프 자동화)
|
|
28
|
+
def file(path: str, content: str, status: str = "") -> str:
|
|
29
|
+
"""
|
|
30
|
+
Generates a safe <file> block.
|
|
31
|
+
Automatically escapes the content to prevent XML injection.
|
|
32
|
+
"""
|
|
33
|
+
safe_content = escape_xml_value(content)
|
|
34
|
+
|
|
35
|
+
# f-string을 왼쪽 벽에 붙여 들여쓰기 문제 원천 차단
|
|
36
|
+
return f"""<file path="{path}{status}">
|
|
37
|
+
{safe_content}
|
|
38
|
+
</file>"""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entry Point with Global Error Handling & Version Check
|
|
3
|
+
"""
|
|
4
|
+
import sys
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
8
|
+
|
|
9
|
+
from .commands import init, scan, tree, digest, diff, semdiff
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(
|
|
14
|
+
name="codigest",
|
|
15
|
+
help="Semantic Context Manager for LLM-assisted Development",
|
|
16
|
+
add_completion=False,
|
|
17
|
+
no_args_is_help=True
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
app.add_typer(init.app, name="init")
|
|
21
|
+
app.add_typer(scan.app, name="scan")
|
|
22
|
+
app.add_typer(tree.app, name="tree")
|
|
23
|
+
app.add_typer(digest.app, name="digest")
|
|
24
|
+
app.add_typer(diff.app, name="diff")
|
|
25
|
+
app.add_typer(semdiff.app, name="semdiff")
|
|
26
|
+
|
|
27
|
+
def version_callback(value: bool):
|
|
28
|
+
"""
|
|
29
|
+
Callback function to handle --version flag.
|
|
30
|
+
"""
|
|
31
|
+
if value:
|
|
32
|
+
try:
|
|
33
|
+
pkg_version = version("codigest")
|
|
34
|
+
except PackageNotFoundError:
|
|
35
|
+
pkg_version = "0.3.0 (dev)"
|
|
36
|
+
|
|
37
|
+
console.print(f"🦁 [bold cyan]Codigest[/bold cyan] version [bold green]{pkg_version}[/bold green]")
|
|
38
|
+
raise typer.Exit()
|
|
39
|
+
|
|
40
|
+
@app.callback()
|
|
41
|
+
def common(
|
|
42
|
+
ctx: typer.Context,
|
|
43
|
+
version: bool = typer.Option(
|
|
44
|
+
None,
|
|
45
|
+
"--version", "-v",
|
|
46
|
+
help="Show the application version and exit.",
|
|
47
|
+
callback=version_callback,
|
|
48
|
+
is_eager=True
|
|
49
|
+
)
|
|
50
|
+
):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def main():
|
|
54
|
+
try:
|
|
55
|
+
app()
|
|
56
|
+
except Exception as e:
|
|
57
|
+
if isinstance(e, typer.Exit):
|
|
58
|
+
raise e
|
|
59
|
+
|
|
60
|
+
error_msg = str(e)
|
|
61
|
+
console.print(f"[bold red]❌ Error:[/bold red] {error_msg}")
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
main()
|
|
@@ -16,7 +16,7 @@ wheels = [
|
|
|
16
16
|
|
|
17
17
|
[[package]]
|
|
18
18
|
name = "codigest"
|
|
19
|
-
version = "0.
|
|
19
|
+
version = "0.3.1"
|
|
20
20
|
source = { editable = "." }
|
|
21
21
|
dependencies = [
|
|
22
22
|
{ name = "loguru" },
|
|
@@ -27,11 +27,6 @@ dependencies = [
|
|
|
27
27
|
{ name = "typer" },
|
|
28
28
|
]
|
|
29
29
|
|
|
30
|
-
[package.dev-dependencies]
|
|
31
|
-
dev = [
|
|
32
|
-
{ name = "ruff" },
|
|
33
|
-
]
|
|
34
|
-
|
|
35
30
|
[package.metadata]
|
|
36
31
|
requires-dist = [
|
|
37
32
|
{ name = "loguru" },
|
|
@@ -42,9 +37,6 @@ requires-dist = [
|
|
|
42
37
|
{ name = "typer" },
|
|
43
38
|
]
|
|
44
39
|
|
|
45
|
-
[package.metadata.requires-dev]
|
|
46
|
-
dev = [{ name = "ruff" }]
|
|
47
|
-
|
|
48
40
|
[[package]]
|
|
49
41
|
name = "colorama"
|
|
50
42
|
version = "0.4.6"
|
|
@@ -128,32 +120,6 @@ wheels = [
|
|
|
128
120
|
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
|
|
129
121
|
]
|
|
130
122
|
|
|
131
|
-
[[package]]
|
|
132
|
-
name = "ruff"
|
|
133
|
-
version = "0.14.8"
|
|
134
|
-
source = { registry = "https://pypi.org/simple" }
|
|
135
|
-
sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" }
|
|
136
|
-
wheels = [
|
|
137
|
-
{ url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" },
|
|
138
|
-
{ url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" },
|
|
139
|
-
{ url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" },
|
|
140
|
-
{ url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" },
|
|
141
|
-
{ url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" },
|
|
142
|
-
{ url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" },
|
|
143
|
-
{ url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" },
|
|
144
|
-
{ url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" },
|
|
145
|
-
{ url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" },
|
|
146
|
-
{ url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" },
|
|
147
|
-
{ url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" },
|
|
148
|
-
{ url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" },
|
|
149
|
-
{ url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" },
|
|
150
|
-
{ url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" },
|
|
151
|
-
{ url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" },
|
|
152
|
-
{ url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" },
|
|
153
|
-
{ url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" },
|
|
154
|
-
{ url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" },
|
|
155
|
-
]
|
|
156
|
-
|
|
157
123
|
[[package]]
|
|
158
124
|
name = "shellingham"
|
|
159
125
|
version = "1.5.4"
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
name: Publish to PyPI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags:
|
|
6
|
-
- 'v*'
|
|
7
|
-
workflow_dispatch:
|
|
8
|
-
jobs:
|
|
9
|
-
pypi-publish:
|
|
10
|
-
name: Build and publish Python distribution to PyPI
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
environment: github
|
|
13
|
-
permissions:
|
|
14
|
-
id-token: write
|
|
15
|
-
contents: read
|
|
16
|
-
|
|
17
|
-
steps:
|
|
18
|
-
- uses: actions/checkout@v4
|
|
19
|
-
|
|
20
|
-
- name: Install uv
|
|
21
|
-
uses: astral-sh/setup-uv@v5
|
|
22
|
-
|
|
23
|
-
- name: Set up Python
|
|
24
|
-
run: uv python install 3.14
|
|
25
|
-
|
|
26
|
-
- name: Build package
|
|
27
|
-
run: uv build
|
|
28
|
-
|
|
29
|
-
- name: Publish package distributions to PyPI
|
|
30
|
-
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Core Template Engine using Python Tag Strings.
|
|
3
|
-
Advanced Logic: Structure-Aware Dedent & XML Safety.
|
|
4
|
-
"""
|
|
5
|
-
import html
|
|
6
|
-
import textwrap
|
|
7
|
-
from typing import Any, Callable
|
|
8
|
-
|
|
9
|
-
def _get_common_indent(parts: list[str]) -> int:
|
|
10
|
-
"""Calculates minimum indentation from static template parts."""
|
|
11
|
-
min_indent = None
|
|
12
|
-
for part in parts:
|
|
13
|
-
for line in part.splitlines():
|
|
14
|
-
if not line.strip():
|
|
15
|
-
continue
|
|
16
|
-
indent = len(line) - len(line.lstrip())
|
|
17
|
-
if min_indent is None or indent < min_indent:
|
|
18
|
-
min_indent = indent
|
|
19
|
-
return min_indent or 0
|
|
20
|
-
|
|
21
|
-
def _dedent_line(line: str, indent: int) -> str:
|
|
22
|
-
"""
|
|
23
|
-
Safely removes indentation from a single line.
|
|
24
|
-
"""
|
|
25
|
-
if not line.strip():
|
|
26
|
-
return "\n" if line.endswith("\n") else ""
|
|
27
|
-
|
|
28
|
-
if line.startswith(" " * indent):
|
|
29
|
-
return line[indent:]
|
|
30
|
-
|
|
31
|
-
return line.lstrip(" ")
|
|
32
|
-
|
|
33
|
-
def escape_xml_value(value: Any) -> str:
|
|
34
|
-
"""[XML] Escapes characters for XML safety."""
|
|
35
|
-
raw_value = str(value)
|
|
36
|
-
safe_value = html.escape(raw_value, quote=False)
|
|
37
|
-
if "</" in safe_value:
|
|
38
|
-
safe_value = safe_value.replace("</", "</")
|
|
39
|
-
return safe_value
|
|
40
|
-
|
|
41
|
-
def no_escape(value: Any) -> str:
|
|
42
|
-
"""[Plain] Returns string as-is (for dedent)."""
|
|
43
|
-
return str(value)
|
|
44
|
-
|
|
45
|
-
def _render_structure_aware(template: Any, escape_fn: Callable[[Any], str]) -> str:
|
|
46
|
-
"""
|
|
47
|
-
Core Logic: Dedents based on static structure, then injects processed values.
|
|
48
|
-
"""
|
|
49
|
-
if isinstance(template, str):
|
|
50
|
-
return textwrap.dedent(template).strip()
|
|
51
|
-
|
|
52
|
-
static_parts = [p for p in template if isinstance(p, str)]
|
|
53
|
-
common_indent = _get_common_indent(static_parts)
|
|
54
|
-
|
|
55
|
-
result = []
|
|
56
|
-
|
|
57
|
-
for part in template:
|
|
58
|
-
if isinstance(part, str):
|
|
59
|
-
# Dedent static parts
|
|
60
|
-
lines = part.splitlines(keepends=True)
|
|
61
|
-
dedented_lines = [_dedent_line(line, common_indent) for line in lines]
|
|
62
|
-
result.append("".join(dedented_lines))
|
|
63
|
-
else:
|
|
64
|
-
# Inject dynamic value
|
|
65
|
-
result.append(escape_fn(part.value))
|
|
66
|
-
|
|
67
|
-
return "".join(result).lstrip()
|
|
68
|
-
|
|
69
|
-
def xml(template: Any) -> str:
|
|
70
|
-
"""[Python 3.14 Native Tag] XML Generator."""
|
|
71
|
-
return _render_structure_aware(template, escape_xml_value)
|
|
72
|
-
|
|
73
|
-
def dedent(template: Any) -> str:
|
|
74
|
-
"""[Python 3.14 Native Tag] Plain Text Generator."""
|
|
75
|
-
return _render_structure_aware(template, no_escape)
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Entry Point with Global Error Handling
|
|
3
|
-
"""
|
|
4
|
-
import sys
|
|
5
|
-
import typer
|
|
6
|
-
from rich.console import Console
|
|
7
|
-
from .commands import init, scan, tree, digest, diff, semdiff
|
|
8
|
-
|
|
9
|
-
console = Console()
|
|
10
|
-
|
|
11
|
-
app = typer.Typer(
|
|
12
|
-
name="codigest",
|
|
13
|
-
help="Semantic Context Manager for LLM-assisted Development",
|
|
14
|
-
add_completion=False,
|
|
15
|
-
no_args_is_help=True
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
app.add_typer(init.app, name="init")
|
|
19
|
-
app.add_typer(scan.app, name="scan")
|
|
20
|
-
app.add_typer(tree.app, name="tree")
|
|
21
|
-
app.add_typer(digest.app, name="digest")
|
|
22
|
-
app.add_typer(diff.app, name="diff")
|
|
23
|
-
app.add_typer(semdiff.app, name="semdiff")
|
|
24
|
-
|
|
25
|
-
def main():
|
|
26
|
-
try:
|
|
27
|
-
app()
|
|
28
|
-
except Exception as e:
|
|
29
|
-
error_msg = str(e)
|
|
30
|
-
console.print(f"[bold red]❌ Error:[/bold red] {error_msg}")
|
|
31
|
-
sys.exit(1)
|
|
32
|
-
|
|
33
|
-
if __name__ == "__main__":
|
|
34
|
-
main()
|
|
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
|