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.
Files changed (29) hide show
  1. {codigest-0.3.0 → codigest-0.3.2}/.github/workflows/publish.yml +3 -5
  2. {codigest-0.3.0 → codigest-0.3.2}/PKG-INFO +46 -15
  3. {codigest-0.3.0 → codigest-0.3.2}/README.md +45 -14
  4. {codigest-0.3.0 → codigest-0.3.2}/pyproject.toml +1 -1
  5. codigest-0.3.2/src/codigest/__main__.py +4 -0
  6. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/commands/diff.py +10 -1
  7. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/commands/digest.py +1 -5
  8. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/commands/init.py +11 -1
  9. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/commands/scan.py +43 -10
  10. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/commands/semdiff.py +20 -16
  11. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/prompts.py +89 -78
  12. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/scanner.py +40 -8
  13. codigest-0.3.2/src/codigest/core/tags.py +38 -0
  14. codigest-0.3.2/src/codigest/main.py +65 -0
  15. {codigest-0.3.0 → codigest-0.3.2}/uv.lock +1 -35
  16. codigest-0.3.0/.github/workflows/main.yml +0 -30
  17. codigest-0.3.0/src/codigest/core/tags.py +0 -75
  18. codigest-0.3.0/src/codigest/main.py +0 -34
  19. {codigest-0.3.0 → codigest-0.3.2}/.gitignore +0 -0
  20. {codigest-0.3.0 → codigest-0.3.2}/.python-version +0 -0
  21. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/__init__.py +0 -0
  22. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/commands/tree.py +0 -0
  23. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/config.py +0 -0
  24. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/git_ops.py +0 -0
  25. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/processor.py +0 -0
  26. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/semdiff.py +0 -0
  27. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/shadow.py +0 -0
  28. {codigest-0.3.0 → codigest-0.3.2}/src/codigest/core/structure.py +0 -0
  29. {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
- # [변경] uv 설치 (가장 빠르고 확실한 방법)
22
+ # uv 설치 (가장 빠르고 확실한 방법)
25
23
  - name: Install uv
26
24
  uses: astral-sh/setup-uv@v5
27
25
 
28
- # [변경] Python 3.14 설정 (uv 이용)
26
+ # Python 3.14 설정 (uv 이용)
29
27
  - name: Set up Python
30
28
  run: uv python install 3.14
31
29
 
32
- # [변경] 의존성 설치 및 빌드 (uv build 하나로 해결)
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.0
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
- * **Python 3.14 Native**: Built using modern Python features including PEP 750 Tag Strings for safe XML generation.
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
- Requires **Python 3.14+**.
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
- # Install via uv
45
- git clone https://github.com/SJB7777/codigest.git
46
- cd codigest
47
- uv tool install .
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 --m " Python 3.14 project. Do NOT use typing. List/Dict."
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
- * **Python 3.14 Native**: Built using modern Python features including PEP 750 Tag Strings for safe XML generation.
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
- Requires **Python 3.14+**.
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
- # Install via uv
20
- git clone https://github.com/SJB7777/codigest.git
21
- cd codigest
22
- uv tool install .
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 --m " Python 3.14 project. Do NOT use typing. List/Dict."
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`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "codigest"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Human-Centered Codebase Context Manager for LLMs"
5
5
  authors = [{name = "Codigest Team", email = "sngd024@google.com"}]
6
6
  readme = "README.md"
@@ -0,0 +1,4 @@
1
+ from .main import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -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 = Path.cwd()
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.xml(t"""
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 = Path.cwd()
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
- target: Path = typer.Argument(Path.cwd(), help="Target directory"),
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
- [Legacy Mode] Scans the codebase using the PromptEngine.
57
+ Scans the codebase.
58
+ If TARGETS provided, only scans those paths within the project.
41
59
  """
42
- root_path = target.resolve()
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(root_path, extensions, extra_ignores)
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
- block = tags.xml(t"""
93
- <file path="{rel_path}">
94
- {content}
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: new_code = ""
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": icon = "➕ [ADDED] "
78
- elif ch.change_type == "REMOVED": icon = "➖ [REMOVED] "
79
- elif ch.change_type == "MODIFIED": icon = "⚠️ [SIGNATURE]" # 중요: 시그니처 변경
80
- else: icon = "✏️ [LOGIC] " # 내부 로직 변경
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: file_status = " (DELETED)"
89
- elif not old_code: file_status = " (NEW)"
90
-
91
- block = tags.xml(t"""
92
- <file path="{rel_path.as_posix()}{file_status}">
93
- {chr(10).join(change_lines)}
94
- </file>
95
- """)
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
- Philosophy: Defaults are Code (t-strings), Overrides are Data (TOML).
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
- <user_instruction>
19
- {instruction}
20
- </user_instruction>
21
- """)
22
-
23
- return tags.dedent(t"""
24
- [SYSTEM: CODIGEST - INITIAL CONTEXT]
25
- You are an expert AI developer. I am providing the full context of a project named "{project_name}".
26
- Please digest this structure and code to build your internal mental model.
27
- {instruction_block}
28
-
29
- <project_root>
30
- {project_name}
31
- </project_root>
32
-
33
- <project_structure>
34
- {tree_structure}
35
- </project_structure>
36
-
37
- <source_code>
38
- {source_code}
39
- </source_code>
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
- <user_instruction>
48
- {instruction}
49
- </user_instruction>
50
- """)
50
+ <user_instruction>
51
+ {safe_instruction}
52
+ </user_instruction>
53
+ """)
51
54
 
52
- return tags.dedent(t"""
53
- [SYSTEM: CODIGEST - INCREMENTAL UPDATE]
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
- Focus on these modifications to update your context.
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
- <git_diff>
61
- {diff_content}
62
- </git_diff>
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
- <user_instruction>
71
- {instruction}
72
- </user_instruction>
73
- """)
74
-
75
- return tags.dedent(t"""
76
- [SYSTEM: CODIGEST - SEMANTIC ANALYSIS]
77
- Structural changes detected in "{project_name}".
78
- {context_message}.
79
- {instruction_block}
80
-
81
- This report focuses on High-Level Architecture changes (Classes, Functions, Signatures).
82
-
83
- <semantic_diff>
84
- {semdiff_content}
85
- </semantic_diff>
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) - [Missing Part Fix]
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
- <user_instruction>
94
- {instruction}
95
- </user_instruction>
96
- """)
97
-
98
- return tags.dedent(t"""
99
- [SYSTEM: CODIGEST - ARCHITECTURE DIGEST]
100
- Here is the high-level architecture of "{project_name}".
101
- This contains ONLY definitions (Classes, Functions), no implementation details.
102
- Use this to understand the project structure and relationships.
103
- {instruction_block}
104
-
105
- <project_structure>
106
- {tree_structure}
107
- </project_structure>
108
-
109
- <code_digest>
110
- {digest_content}
111
- </code_digest>
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__(self, root_path: Path, extensions: Optional[set[str]] = None, extra_ignores: Optional[list[str]] = None):
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 = [self.root_path]
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
- def scan_project(root_path: Path, extensions: set[str] = None, extra_ignores: list[str] = None) -> list[Path]:
98
- scanner = ProjectScanner(root_path, extensions, extra_ignores)
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("</", "&lt;/")
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.2.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("</", "&lt;/")
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