codecrate 0.1.0__tar.gz → 0.1.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.
Potentially problematic release.
This version of codecrate might be problematic. Click here for more details.
- {codecrate-0.1.0 → codecrate-0.1.2}/.gitignore +1 -0
- codecrate-0.1.2/AGENTS.md +159 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/PKG-INFO +1 -1
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/_version.py +3 -3
- codecrate-0.1.2/codecrate/cli.py +433 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/config.py +13 -4
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/parse.py +3 -1
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate.egg-info/PKG-INFO +1 -1
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate.egg-info/SOURCES.txt +2 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/docs/cli.rst +15 -6
- {codecrate-0.1.0 → codecrate-0.1.2}/docs/quickstart.rst +6 -0
- codecrate-0.1.2/tests/test_cli_pack_multi.py +39 -0
- codecrate-0.1.0/codecrate/cli.py +0 -250
- {codecrate-0.1.0 → codecrate-0.1.2}/.github/pytest.ini +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/.github/workflows/codecov.yml +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/.github/workflows/pre-commit.yml +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/.github/workflows/python-publish.yml +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/.github/workflows/tests.yml +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/.pre-commit-config.yaml +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/.readthedocs.yaml +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/.ruff.toml +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/LICENSE +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/README.md +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/__init__.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/diffgen.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/discover.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/ids.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/manifest.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/markdown.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/mdparse.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/model.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/packer.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/stubber.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/token_budget.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/udiff.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/unpacker.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate/validate.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate.egg-info/dependency_links.txt +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate.egg-info/entry_points.txt +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate.egg-info/requires.txt +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate.egg-info/top_level.txt +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/codecrate.toml +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/docs/api.rst +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/docs/conf.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/docs/format.rst +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/docs/index.rst +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/docs/make.bat +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/docs/make.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/docs/requirements.txt +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/pyproject.toml +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/requirements-test.txt +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/setup.cfg +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/setup.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/tests/__init__.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/tests/test_config.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/tests/test_discover.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/tests/test_ids.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/tests/test_markdown_line_numbers.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/tests/test_model.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/tests/test_pack_unpack_roundtrip.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/tests/test_parse.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/tests/test_patch_apply_roundtrip.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/tests/test_smoke.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/tests/test_split_codecrate_pack.py +0 -0
- {codecrate-0.1.0 → codecrate-0.1.2}/tests/test_token_budget.py +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file summarizes how to work in this repository for agentic tooling.
|
|
4
|
+
Follow these conventions unless a task explicitly requires otherwise.
|
|
5
|
+
|
|
6
|
+
## Repository Overview
|
|
7
|
+
|
|
8
|
+
- Project: `codecrate` (Python library + CLI)
|
|
9
|
+
- Package directory: `codecrate/`
|
|
10
|
+
- Tests: `tests/` with pytest
|
|
11
|
+
- Python support: 3.10+ (see `pyproject.toml`)
|
|
12
|
+
- Lint/format tooling: ruff + ruff-format (`.ruff.toml`)
|
|
13
|
+
- Pre-commit hooks: ruff, ruff-format, plus general hooks
|
|
14
|
+
|
|
15
|
+
## Key Config Files
|
|
16
|
+
|
|
17
|
+
- `pyproject.toml`: build metadata, optional deps, mypy config
|
|
18
|
+
- `.ruff.toml`: lint + format rules (target version, import order, complexity)
|
|
19
|
+
- `.pre-commit-config.yaml`: formatting and linting hooks
|
|
20
|
+
- `.github/workflows/tests.yml`: CI test commands
|
|
21
|
+
- `.github/pytest.ini`: pytest defaults (`-rw`, ignore dirs)
|
|
22
|
+
- `codecrate.toml`: runtime config for pack/unpack behavior
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
Common install paths used in CI and docs:
|
|
27
|
+
|
|
28
|
+
- `uv pip install -e .`
|
|
29
|
+
|
|
30
|
+
## Build, Lint, Format, Test Commands
|
|
31
|
+
|
|
32
|
+
Run these from the repository root:
|
|
33
|
+
|
|
34
|
+
- Format: `ruff format .`
|
|
35
|
+
- Lint: `ruff check .`
|
|
36
|
+
- Lint + autofix: `ruff check --fix .`
|
|
37
|
+
- All tests: `pytest`
|
|
38
|
+
- Pre-commit hooks: `pre-commit run --all-files`
|
|
39
|
+
|
|
40
|
+
## Running a Single Test
|
|
41
|
+
|
|
42
|
+
Typical pytest patterns:
|
|
43
|
+
|
|
44
|
+
- One file: `pytest tests/test_parse.py`
|
|
45
|
+
- One test: `pytest tests/test_parse.py::test_parse_basic`
|
|
46
|
+
- By keyword: `pytest -k line_numbers`
|
|
47
|
+
- Show stdout: `pytest -s tests/test_smoke.py::test_cli_pack`
|
|
48
|
+
|
|
49
|
+
Pytest defaults (`.github/pytest.ini`) include:
|
|
50
|
+
|
|
51
|
+
- `addopts = -rw`
|
|
52
|
+
- `norecursedirs = .git .* *.egg* old dist build`
|
|
53
|
+
|
|
54
|
+
## Formatting and Linting Rules
|
|
55
|
+
|
|
56
|
+
Ruff is the source of truth for formatting and linting.
|
|
57
|
+
Configuration lives in `.ruff.toml`.
|
|
58
|
+
|
|
59
|
+
- Target Python: 3.10 (`target-version = "py310"`)
|
|
60
|
+
- Line length: ruff E501 defaults (88 unless configured otherwise)
|
|
61
|
+
- Import sorting: ruff isort with sections
|
|
62
|
+
- Complexity limit: McCabe max 22
|
|
63
|
+
|
|
64
|
+
When editing code, prefer running:
|
|
65
|
+
|
|
66
|
+
- `ruff format .`
|
|
67
|
+
- `ruff check --fix .`
|
|
68
|
+
|
|
69
|
+
## Import Conventions
|
|
70
|
+
|
|
71
|
+
Follow the existing import layout:
|
|
72
|
+
|
|
73
|
+
- `from __future__ import annotations` at the top of each module
|
|
74
|
+
- Standard library imports first
|
|
75
|
+
- Third-party imports next (rare here)
|
|
76
|
+
- First-party imports last (`codecrate.*` or relative)
|
|
77
|
+
- Keep imports sorted by ruff/isort
|
|
78
|
+
|
|
79
|
+
`__init__.py` files may intentionally have unused imports; ruff ignores
|
|
80
|
+
F401/I001 there (see `.ruff.toml`).
|
|
81
|
+
|
|
82
|
+
## Type Hints and Static Analysis
|
|
83
|
+
|
|
84
|
+
Type hints are expected on public functions and helpers.
|
|
85
|
+
The mypy config is strict, so avoid untyped defs when possible.
|
|
86
|
+
|
|
87
|
+
- Use `list[str]`, `dict[str, str]`, etc. (PEP 585 style)
|
|
88
|
+
- Prefer explicit `Optional[T]` via `T | None`
|
|
89
|
+
- Avoid implicit `Any` in new code
|
|
90
|
+
|
|
91
|
+
## Naming Conventions
|
|
92
|
+
|
|
93
|
+
- Modules, functions, variables: `snake_case`
|
|
94
|
+
- Classes, dataclasses: `PascalCase`
|
|
95
|
+
- Constants: `UPPER_SNAKE_CASE`
|
|
96
|
+
- Private helpers: `_leading_underscore`
|
|
97
|
+
|
|
98
|
+
Follow existing naming in each module; avoid inventing new prefixes.
|
|
99
|
+
|
|
100
|
+
## File and Path Handling
|
|
101
|
+
|
|
102
|
+
The codebase favors `pathlib.Path`:
|
|
103
|
+
|
|
104
|
+
- Use `Path` instead of `os.path`
|
|
105
|
+
- For file IO, use `read_text`/`write_text`
|
|
106
|
+
- When reading text, pass `encoding="utf-8"` and `errors="replace"`
|
|
107
|
+
- Use `as_posix()` when storing relative paths
|
|
108
|
+
|
|
109
|
+
## Error Handling and Warnings
|
|
110
|
+
|
|
111
|
+
Keep error handling explicit and narrow:
|
|
112
|
+
|
|
113
|
+
- Raise `ValueError` for invalid user input
|
|
114
|
+
- Use `warnings.warn(..., RuntimeWarning)` for recoverable issues
|
|
115
|
+
- Avoid bare `except:` and broad `except Exception:` unless needed
|
|
116
|
+
- Prefer returning empty values over crashing when data is missing
|
|
117
|
+
|
|
118
|
+
## General Coding Practices
|
|
119
|
+
|
|
120
|
+
- Prefer small, focused helpers with explicit inputs/outputs
|
|
121
|
+
- Keep side effects near the CLI or IO boundaries
|
|
122
|
+
- Use dataclasses for structured data (see `codecrate/model.py`)
|
|
123
|
+
- Preserve existing section ordering in generated markdown
|
|
124
|
+
|
|
125
|
+
## Markdown and Output Formatting
|
|
126
|
+
|
|
127
|
+
Generated Markdown is line-sensitive. When modifying output:
|
|
128
|
+
|
|
129
|
+
- Preserve existing headings and section order
|
|
130
|
+
- Keep code fences exact (` ```python ` or ` ```diff `)
|
|
131
|
+
- Avoid adding trailing whitespace
|
|
132
|
+
|
|
133
|
+
## Tests and Fixtures
|
|
134
|
+
|
|
135
|
+
Tests are pytest-based and generally use `tmp_path`.
|
|
136
|
+
Keep tests deterministic and avoid external IO or network access.
|
|
137
|
+
|
|
138
|
+
- Use `tmp_path` for temp repos
|
|
139
|
+
- Use `Path.write_text(..., encoding="utf-8")`
|
|
140
|
+
- Prefer exact string comparisons for rendered output
|
|
141
|
+
|
|
142
|
+
## CLI and Commands
|
|
143
|
+
|
|
144
|
+
The CLI entrypoint is `codecrate.cli:main`.
|
|
145
|
+
When adding CLI flags, update both the parser and README if needed.
|
|
146
|
+
|
|
147
|
+
Quick reference:
|
|
148
|
+
|
|
149
|
+
- Pack: `codecrate pack . -o context.md`
|
|
150
|
+
- Unpack: `codecrate unpack context.md -o out_dir/`
|
|
151
|
+
- Patch: `codecrate patch baseline.md . -o changes.md`
|
|
152
|
+
- Apply: `codecrate apply changes.md .`
|
|
153
|
+
- Validate: `codecrate validate-pack context.md`
|
|
154
|
+
|
|
155
|
+
## Docs
|
|
156
|
+
|
|
157
|
+
Sphinx config lives under `docs/` (see `docs/conf.py`).
|
|
158
|
+
No automated doc build is configured in CI, but keep docs consistent
|
|
159
|
+
with CLI and configuration behavior.
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.2'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 2)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g3ede7bcd7'
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .config import Config, load_config
|
|
8
|
+
from .diffgen import generate_patch_markdown
|
|
9
|
+
from .discover import discover_files
|
|
10
|
+
from .markdown import render_markdown
|
|
11
|
+
from .packer import pack_repo
|
|
12
|
+
from .token_budget import split_by_max_chars
|
|
13
|
+
from .udiff import apply_file_diffs, parse_unified_diff
|
|
14
|
+
from .unpacker import unpack_to_dir
|
|
15
|
+
from .validate import validate_pack_markdown
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
19
|
+
p = argparse.ArgumentParser(
|
|
20
|
+
prog="codecrate",
|
|
21
|
+
description="Pack/unpack/patch/apply for repositories (Python + text files).",
|
|
22
|
+
)
|
|
23
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
24
|
+
|
|
25
|
+
# pack
|
|
26
|
+
pack = sub.add_parser(
|
|
27
|
+
"pack", help="Pack one or more repositories/directories into Markdown."
|
|
28
|
+
)
|
|
29
|
+
pack.add_argument(
|
|
30
|
+
"root",
|
|
31
|
+
type=Path,
|
|
32
|
+
nargs="?",
|
|
33
|
+
help="Root directory to scan (omit when using --repo)",
|
|
34
|
+
)
|
|
35
|
+
pack.add_argument(
|
|
36
|
+
"--repo",
|
|
37
|
+
action="append",
|
|
38
|
+
default=None,
|
|
39
|
+
type=Path,
|
|
40
|
+
help="Additional repo root to pack (repeatable; use instead of ROOT)",
|
|
41
|
+
)
|
|
42
|
+
pack.add_argument(
|
|
43
|
+
"-o",
|
|
44
|
+
"--output",
|
|
45
|
+
type=Path,
|
|
46
|
+
default=None,
|
|
47
|
+
help="Output markdown path (default: config 'output' or context.md)",
|
|
48
|
+
)
|
|
49
|
+
pack.add_argument(
|
|
50
|
+
"--dedupe", action="store_true", help="Deduplicate identical function bodies"
|
|
51
|
+
)
|
|
52
|
+
pack.add_argument(
|
|
53
|
+
"--layout",
|
|
54
|
+
choices=["auto", "stubs", "full"],
|
|
55
|
+
default=None,
|
|
56
|
+
help="Output layout: auto|stubs|full (default: auto via config)",
|
|
57
|
+
)
|
|
58
|
+
pack.add_argument(
|
|
59
|
+
"--keep-docstrings",
|
|
60
|
+
action=argparse.BooleanOptionalAction,
|
|
61
|
+
default=None,
|
|
62
|
+
help="Keep docstrings in stubbed file view (default: true via config)",
|
|
63
|
+
)
|
|
64
|
+
pack.add_argument(
|
|
65
|
+
"--respect-gitignore",
|
|
66
|
+
action=argparse.BooleanOptionalAction,
|
|
67
|
+
default=None,
|
|
68
|
+
help="Respect .gitignore (default: true via config)",
|
|
69
|
+
)
|
|
70
|
+
pack.add_argument(
|
|
71
|
+
"--manifest",
|
|
72
|
+
action=argparse.BooleanOptionalAction,
|
|
73
|
+
default=None,
|
|
74
|
+
help="Include Manifest section (default: true via config)",
|
|
75
|
+
)
|
|
76
|
+
pack.add_argument(
|
|
77
|
+
"--include", action="append", default=None, help="Include glob (repeatable)"
|
|
78
|
+
)
|
|
79
|
+
pack.add_argument(
|
|
80
|
+
"--exclude", action="append", default=None, help="Exclude glob (repeatable)"
|
|
81
|
+
)
|
|
82
|
+
pack.add_argument(
|
|
83
|
+
"--split-max-chars",
|
|
84
|
+
type=int,
|
|
85
|
+
default=None,
|
|
86
|
+
help="Split output into .partN.md files",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# unpack
|
|
90
|
+
unpack = sub.add_parser(
|
|
91
|
+
"unpack", help="Reconstruct files from a packed context Markdown."
|
|
92
|
+
)
|
|
93
|
+
unpack.add_argument("markdown", type=Path, help="Packed Markdown file from `pack`")
|
|
94
|
+
unpack.add_argument(
|
|
95
|
+
"-o",
|
|
96
|
+
"--out-dir",
|
|
97
|
+
type=Path,
|
|
98
|
+
required=True,
|
|
99
|
+
help="Output directory for reconstructed files",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# patch
|
|
103
|
+
patch = sub.add_parser(
|
|
104
|
+
"patch",
|
|
105
|
+
help="Generate a diff-only patch Markdown from old pack + current repo.",
|
|
106
|
+
)
|
|
107
|
+
patch.add_argument(
|
|
108
|
+
"old_markdown", type=Path, help="Older packed Markdown (baseline)"
|
|
109
|
+
)
|
|
110
|
+
patch.add_argument("root", type=Path, help="Current repo root to compare against")
|
|
111
|
+
patch.add_argument(
|
|
112
|
+
"-o",
|
|
113
|
+
"--output",
|
|
114
|
+
type=Path,
|
|
115
|
+
default=Path("patch.md"),
|
|
116
|
+
help="Output patch markdown",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# apply
|
|
120
|
+
apply = sub.add_parser("apply", help="Apply a diff-only patch Markdown to a repo.")
|
|
121
|
+
apply.add_argument(
|
|
122
|
+
"patch_markdown", type=Path, help="Patch Markdown containing ```diff blocks"
|
|
123
|
+
)
|
|
124
|
+
apply.add_argument("root", type=Path, help="Repo root to apply patch to")
|
|
125
|
+
# validate-pack
|
|
126
|
+
vpack = sub.add_parser(
|
|
127
|
+
"validate-pack",
|
|
128
|
+
help="Validate a packed context Markdown (sha/markers/canonical consistency).",
|
|
129
|
+
)
|
|
130
|
+
vpack.add_argument("markdown", type=Path, help="Packed Markdown to validate")
|
|
131
|
+
vpack.add_argument(
|
|
132
|
+
"--root",
|
|
133
|
+
type=Path,
|
|
134
|
+
default=None,
|
|
135
|
+
help="Optional repo root to compare reconstructed files against",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return p
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True)
|
|
142
|
+
class PackOptions:
|
|
143
|
+
include: list[str] | None
|
|
144
|
+
exclude: list[str] | None
|
|
145
|
+
keep_docstrings: bool
|
|
146
|
+
include_manifest: bool
|
|
147
|
+
respect_gitignore: bool
|
|
148
|
+
dedupe: bool
|
|
149
|
+
split_max_chars: int
|
|
150
|
+
layout: str
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True)
|
|
154
|
+
class PackRun:
|
|
155
|
+
root: Path
|
|
156
|
+
label: str
|
|
157
|
+
slug: str
|
|
158
|
+
markdown: str
|
|
159
|
+
options: PackOptions
|
|
160
|
+
default_output: Path
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _resolve_pack_options(cfg: Config, args: argparse.Namespace) -> PackOptions:
|
|
164
|
+
include = args.include if args.include is not None else cfg.include
|
|
165
|
+
exclude = args.exclude if args.exclude is not None else cfg.exclude
|
|
166
|
+
keep_docstrings = (
|
|
167
|
+
cfg.keep_docstrings
|
|
168
|
+
if args.keep_docstrings is None
|
|
169
|
+
else bool(args.keep_docstrings)
|
|
170
|
+
)
|
|
171
|
+
include_manifest = cfg.manifest if args.manifest is None else bool(args.manifest)
|
|
172
|
+
respect_gitignore = (
|
|
173
|
+
cfg.respect_gitignore
|
|
174
|
+
if args.respect_gitignore is None
|
|
175
|
+
else bool(args.respect_gitignore)
|
|
176
|
+
)
|
|
177
|
+
dedupe = bool(args.dedupe) or bool(cfg.dedupe)
|
|
178
|
+
split_max_chars = (
|
|
179
|
+
cfg.split_max_chars
|
|
180
|
+
if args.split_max_chars is None
|
|
181
|
+
else int(args.split_max_chars or 0)
|
|
182
|
+
)
|
|
183
|
+
layout = (
|
|
184
|
+
str(args.layout).strip().lower()
|
|
185
|
+
if args.layout is not None
|
|
186
|
+
else str(getattr(cfg, "layout", "auto")).strip().lower()
|
|
187
|
+
)
|
|
188
|
+
return PackOptions(
|
|
189
|
+
include=include,
|
|
190
|
+
exclude=exclude,
|
|
191
|
+
keep_docstrings=keep_docstrings,
|
|
192
|
+
include_manifest=include_manifest,
|
|
193
|
+
respect_gitignore=respect_gitignore,
|
|
194
|
+
dedupe=dedupe,
|
|
195
|
+
split_max_chars=split_max_chars,
|
|
196
|
+
layout=layout,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _resolve_output_path(cfg: Config, args: argparse.Namespace, root: Path) -> Path:
|
|
201
|
+
if args.output is not None:
|
|
202
|
+
return args.output
|
|
203
|
+
out_path = Path(getattr(cfg, "output", "context.md"))
|
|
204
|
+
if not out_path.is_absolute():
|
|
205
|
+
out_path = root / out_path
|
|
206
|
+
return out_path
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _default_repo_label(root: Path) -> str:
|
|
210
|
+
cwd = Path.cwd().resolve()
|
|
211
|
+
resolved = root.resolve()
|
|
212
|
+
try:
|
|
213
|
+
rel = resolved.relative_to(cwd).as_posix()
|
|
214
|
+
return rel or resolved.name or resolved.as_posix()
|
|
215
|
+
except ValueError:
|
|
216
|
+
return root.name or resolved.name or resolved.as_posix()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _unique_label(root: Path, used: set[str]) -> str:
|
|
220
|
+
base = _default_repo_label(root)
|
|
221
|
+
label = base
|
|
222
|
+
idx = 2
|
|
223
|
+
while label in used:
|
|
224
|
+
label = f"{base}-{idx}"
|
|
225
|
+
idx += 1
|
|
226
|
+
used.add(label)
|
|
227
|
+
return label
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _slugify(label: str) -> str:
|
|
231
|
+
safe: list[str] = []
|
|
232
|
+
for ch in label:
|
|
233
|
+
if ch.isalnum() or ch in {"-", "_"}:
|
|
234
|
+
safe.append(ch)
|
|
235
|
+
else:
|
|
236
|
+
safe.append("-")
|
|
237
|
+
slug = "".join(safe).strip("-")
|
|
238
|
+
while "--" in slug:
|
|
239
|
+
slug = slug.replace("--", "-")
|
|
240
|
+
return slug or "repo"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _unique_slug(label: str, used: set[str]) -> str:
|
|
244
|
+
base = _slugify(label)
|
|
245
|
+
slug = base
|
|
246
|
+
idx = 2
|
|
247
|
+
while slug in used:
|
|
248
|
+
slug = f"{base}-{idx}"
|
|
249
|
+
idx += 1
|
|
250
|
+
used.add(slug)
|
|
251
|
+
return slug
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _prefix_repo_header(text: str, label: str) -> str:
|
|
255
|
+
header = f"# Repository: {label}\n\n"
|
|
256
|
+
if text.startswith(header):
|
|
257
|
+
return text
|
|
258
|
+
return header + text
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _combine_pack_markdown(packs: list[PackRun]) -> str:
|
|
262
|
+
out: list[str] = []
|
|
263
|
+
for i, pack in enumerate(packs):
|
|
264
|
+
if i:
|
|
265
|
+
out.append("\n\n")
|
|
266
|
+
out.append(_prefix_repo_header(pack.markdown.rstrip() + "\n", pack.label))
|
|
267
|
+
return "".join(out).rstrip() + "\n"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _extract_diff_blocks(md_text: str) -> str:
|
|
271
|
+
"""
|
|
272
|
+
Extract only diff fences from markdown and concatenate to a unified diff string.
|
|
273
|
+
"""
|
|
274
|
+
lines = md_text.splitlines()
|
|
275
|
+
out: list[str] = []
|
|
276
|
+
i = 0
|
|
277
|
+
while i < len(lines):
|
|
278
|
+
if lines[i].strip() == "```diff":
|
|
279
|
+
i += 1
|
|
280
|
+
while i < len(lines) and lines[i].strip() != "```":
|
|
281
|
+
out.append(lines[i])
|
|
282
|
+
i += 1
|
|
283
|
+
i += 1
|
|
284
|
+
return "\n".join(out) + "\n"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def main(argv: list[str] | None = None) -> None: # noqa: C901
|
|
288
|
+
parser = build_parser()
|
|
289
|
+
args = parser.parse_args(argv)
|
|
290
|
+
|
|
291
|
+
if args.cmd == "pack":
|
|
292
|
+
if args.repo:
|
|
293
|
+
if args.root is not None:
|
|
294
|
+
parser.error(
|
|
295
|
+
"pack: specify either ROOT or --repo (repeatable), not both"
|
|
296
|
+
)
|
|
297
|
+
roots = [r.resolve() for r in args.repo]
|
|
298
|
+
else:
|
|
299
|
+
if args.root is None:
|
|
300
|
+
parser.error("pack: ROOT is required when --repo is not used")
|
|
301
|
+
roots = [args.root.resolve()]
|
|
302
|
+
|
|
303
|
+
used_labels: set[str] = set()
|
|
304
|
+
used_slugs: set[str] = set()
|
|
305
|
+
pack_runs: list[PackRun] = []
|
|
306
|
+
|
|
307
|
+
for root in roots:
|
|
308
|
+
cfg = load_config(root)
|
|
309
|
+
options = _resolve_pack_options(cfg, args)
|
|
310
|
+
label = _unique_label(root, used_labels)
|
|
311
|
+
slug = _unique_slug(label, used_slugs)
|
|
312
|
+
|
|
313
|
+
disc = discover_files(
|
|
314
|
+
root=root,
|
|
315
|
+
include=options.include,
|
|
316
|
+
exclude=options.exclude,
|
|
317
|
+
respect_gitignore=options.respect_gitignore,
|
|
318
|
+
)
|
|
319
|
+
pack, canonical = pack_repo(
|
|
320
|
+
disc.root,
|
|
321
|
+
disc.files,
|
|
322
|
+
keep_docstrings=options.keep_docstrings,
|
|
323
|
+
dedupe=options.dedupe,
|
|
324
|
+
)
|
|
325
|
+
md = render_markdown(
|
|
326
|
+
pack,
|
|
327
|
+
canonical,
|
|
328
|
+
layout=options.layout,
|
|
329
|
+
include_manifest=options.include_manifest,
|
|
330
|
+
)
|
|
331
|
+
default_output = _resolve_output_path(cfg, args, root)
|
|
332
|
+
pack_runs.append(
|
|
333
|
+
PackRun(
|
|
334
|
+
root=root,
|
|
335
|
+
label=label,
|
|
336
|
+
slug=slug,
|
|
337
|
+
markdown=md,
|
|
338
|
+
options=options,
|
|
339
|
+
default_output=default_output,
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
out_path = (
|
|
344
|
+
args.output if args.output is not None else pack_runs[0].default_output
|
|
345
|
+
)
|
|
346
|
+
if len(pack_runs) == 1:
|
|
347
|
+
md = pack_runs[0].markdown
|
|
348
|
+
else:
|
|
349
|
+
md = _combine_pack_markdown(pack_runs)
|
|
350
|
+
|
|
351
|
+
# Always write the canonical, unsplit pack
|
|
352
|
+
# for machine parsing (unpack/validate).
|
|
353
|
+
out_path.write_text(md, encoding="utf-8")
|
|
354
|
+
|
|
355
|
+
extra_count = 0
|
|
356
|
+
if len(pack_runs) == 1:
|
|
357
|
+
split_max_chars = pack_runs[0].options.split_max_chars
|
|
358
|
+
parts = split_by_max_chars(md, out_path, split_max_chars)
|
|
359
|
+
extra = [p for p in parts if p.path != out_path]
|
|
360
|
+
for part in extra:
|
|
361
|
+
part.path.write_text(part.content, encoding="utf-8")
|
|
362
|
+
extra_count += len(extra)
|
|
363
|
+
else:
|
|
364
|
+
for pack in pack_runs:
|
|
365
|
+
if pack.options.split_max_chars <= 0:
|
|
366
|
+
continue
|
|
367
|
+
repo_base = out_path.with_name(
|
|
368
|
+
f"{out_path.stem}.{pack.slug}{out_path.suffix}"
|
|
369
|
+
)
|
|
370
|
+
parts = split_by_max_chars(
|
|
371
|
+
pack.markdown, repo_base, pack.options.split_max_chars
|
|
372
|
+
)
|
|
373
|
+
extra = [p for p in parts if p.path != repo_base]
|
|
374
|
+
for part in extra:
|
|
375
|
+
content = _prefix_repo_header(part.content, pack.label)
|
|
376
|
+
part.path.write_text(content, encoding="utf-8")
|
|
377
|
+
extra_count += len(extra)
|
|
378
|
+
|
|
379
|
+
if extra_count:
|
|
380
|
+
if len(pack_runs) == 1:
|
|
381
|
+
print(f"Wrote {out_path} and {extra_count} split part file(s).")
|
|
382
|
+
else:
|
|
383
|
+
print(
|
|
384
|
+
f"Wrote {out_path} and {extra_count} split part file(s) for "
|
|
385
|
+
f"{len(pack_runs)} repos."
|
|
386
|
+
)
|
|
387
|
+
else:
|
|
388
|
+
if len(pack_runs) == 1:
|
|
389
|
+
print(f"Wrote {out_path}.")
|
|
390
|
+
else:
|
|
391
|
+
print(f"Wrote {out_path} for {len(pack_runs)} repos.")
|
|
392
|
+
elif args.cmd == "unpack":
|
|
393
|
+
md_text = args.markdown.read_text(encoding="utf-8", errors="replace")
|
|
394
|
+
unpack_to_dir(md_text, args.out_dir)
|
|
395
|
+
print(f"Unpacked into {args.out_dir}")
|
|
396
|
+
|
|
397
|
+
elif args.cmd == "patch":
|
|
398
|
+
old_md = args.old_markdown.read_text(encoding="utf-8", errors="replace")
|
|
399
|
+
cfg = load_config(args.root)
|
|
400
|
+
patch_md = generate_patch_markdown(
|
|
401
|
+
old_md,
|
|
402
|
+
args.root,
|
|
403
|
+
include=cfg.include,
|
|
404
|
+
exclude=cfg.exclude,
|
|
405
|
+
respect_gitignore=cfg.respect_gitignore,
|
|
406
|
+
)
|
|
407
|
+
args.output.write_text(patch_md, encoding="utf-8")
|
|
408
|
+
print(f"Wrote {args.output}")
|
|
409
|
+
|
|
410
|
+
elif args.cmd == "validate-pack":
|
|
411
|
+
md_text = args.markdown.read_text(encoding="utf-8", errors="replace")
|
|
412
|
+
report = validate_pack_markdown(md_text, root=args.root)
|
|
413
|
+
if report.warnings:
|
|
414
|
+
print("Warnings:")
|
|
415
|
+
for w in report.warnings:
|
|
416
|
+
print(f"- {w}")
|
|
417
|
+
if report.errors:
|
|
418
|
+
print("Errors:")
|
|
419
|
+
for e in report.errors:
|
|
420
|
+
print(f"- {e}")
|
|
421
|
+
raise SystemExit(1)
|
|
422
|
+
print("OK: pack is internally consistent.")
|
|
423
|
+
|
|
424
|
+
elif args.cmd == "apply":
|
|
425
|
+
md_text = args.patch_markdown.read_text(encoding="utf-8", errors="replace")
|
|
426
|
+
diff_text = _extract_diff_blocks(md_text)
|
|
427
|
+
diffs = parse_unified_diff(diff_text)
|
|
428
|
+
changed = apply_file_diffs(diffs, args.root)
|
|
429
|
+
print(f"Applied patch to {len(changed)} file(s).")
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
if __name__ == "__main__":
|
|
433
|
+
main()
|
|
@@ -61,10 +61,19 @@ def load_config(root: Path) -> Config:
|
|
|
61
61
|
return Config()
|
|
62
62
|
|
|
63
63
|
data = tomllib.loads(cfg_path.read_text(encoding="utf-8"))
|
|
64
|
-
section: dict[str, Any] =
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
section: dict[str, Any] = {}
|
|
65
|
+
if isinstance(data, dict):
|
|
66
|
+
# Preferred: [codecrate]
|
|
67
|
+
cc = data.get("codecrate")
|
|
68
|
+
if isinstance(cc, dict):
|
|
69
|
+
section = cc
|
|
70
|
+
else:
|
|
71
|
+
# Also accept: [tool.codecrate] (common convention from pyproject.toml)
|
|
72
|
+
tool = data.get("tool")
|
|
73
|
+
if isinstance(tool, dict):
|
|
74
|
+
cc2 = tool.get("codecrate")
|
|
75
|
+
if isinstance(cc2, dict):
|
|
76
|
+
section = cc2
|
|
68
77
|
cfg = Config()
|
|
69
78
|
out = section.get("output", cfg.output)
|
|
70
79
|
if isinstance(out, str) and out.strip():
|
|
@@ -127,7 +127,9 @@ class _Visitor(ast.NodeVisitor):
|
|
|
127
127
|
def parse_symbols(
|
|
128
128
|
path: Path, root: Path, text: str
|
|
129
129
|
) -> tuple[list[ClassRef], list[DefRef]]:
|
|
130
|
-
|
|
130
|
+
# Pass filename so SyntaxWarnings (e.g. invalid escape sequences) point to
|
|
131
|
+
# the real file instead of "<unknown>".
|
|
132
|
+
tree = ast.parse(text, filename=path.as_posix())
|
|
131
133
|
v = _Visitor(path=path, root=root)
|
|
132
134
|
v.visit(tree)
|
|
133
135
|
return v.classes, v.defs
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
.pre-commit-config.yaml
|
|
3
3
|
.readthedocs.yaml
|
|
4
4
|
.ruff.toml
|
|
5
|
+
AGENTS.md
|
|
5
6
|
LICENSE
|
|
6
7
|
README.md
|
|
7
8
|
codecrate.toml
|
|
@@ -47,6 +48,7 @@ docs/make.py
|
|
|
47
48
|
docs/quickstart.rst
|
|
48
49
|
docs/requirements.txt
|
|
49
50
|
tests/__init__.py
|
|
51
|
+
tests/test_cli_pack_multi.py
|
|
50
52
|
tests/test_config.py
|
|
51
53
|
tests/test_discover.py
|
|
52
54
|
tests/test_ids.py
|
|
@@ -26,7 +26,7 @@ Overview
|
|
|
26
26
|
|
|
27
27
|
.. code-block:: console
|
|
28
28
|
|
|
29
|
-
codecrate pack ROOT [options]
|
|
29
|
+
codecrate pack [ROOT] [--repo REPO ...] [options]
|
|
30
30
|
codecrate unpack PACK.md -o OUT_DIR
|
|
31
31
|
codecrate patch OLD_PACK.md ROOT [-o patch.md]
|
|
32
32
|
codecrate apply PATCH.md ROOT
|
|
@@ -36,11 +36,14 @@ Overview
|
|
|
36
36
|
pack
|
|
37
37
|
----
|
|
38
38
|
|
|
39
|
-
Create a packed Markdown context file from
|
|
39
|
+
Create a packed Markdown context file from one or more repositories.
|
|
40
40
|
|
|
41
41
|
.. code-block:: console
|
|
42
42
|
|
|
43
43
|
codecrate pack . -o context.md
|
|
44
|
+
codecrate pack --repo /path/to/repo1 --repo /path/to/repo2 -o multi.md
|
|
45
|
+
|
|
46
|
+
When using ``--repo``, omit the positional ``ROOT``. Specifying both is an error.
|
|
44
47
|
|
|
45
48
|
Useful flags:
|
|
46
49
|
|
|
@@ -52,7 +55,8 @@ Useful flags:
|
|
|
52
55
|
* ``--include GLOB`` (repeatable): include patterns
|
|
53
56
|
* ``--exclude GLOB`` (repeatable): exclude patterns
|
|
54
57
|
* ``--split-max-chars N``: additionally emit ``.partN.md`` files for LLMs (the
|
|
55
|
-
main output stays
|
|
58
|
+
main output stays unsplit). For multi-repo packs, parts are named
|
|
59
|
+
``output.<repo>.partN.md``
|
|
56
60
|
* ``-o/--output PATH``: output path (defaults to config ``output`` or ``context.md``)
|
|
57
61
|
|
|
58
62
|
|
|
@@ -107,7 +111,7 @@ Overview
|
|
|
107
111
|
|
|
108
112
|
.. code-block:: console
|
|
109
113
|
|
|
110
|
-
codecrate pack ROOT [options]
|
|
114
|
+
codecrate pack [ROOT] [--repo REPO ...] [options]
|
|
111
115
|
codecrate unpack PACK.md -o OUT_DIR
|
|
112
116
|
codecrate patch OLD_PACK.md ROOT [-o patch.md]
|
|
113
117
|
codecrate apply PATCH.md ROOT
|
|
@@ -117,11 +121,14 @@ Overview
|
|
|
117
121
|
pack
|
|
118
122
|
----
|
|
119
123
|
|
|
120
|
-
Create a packed Markdown context file from
|
|
124
|
+
Create a packed Markdown context file from one or more repositories.
|
|
121
125
|
|
|
122
126
|
.. code-block:: console
|
|
123
127
|
|
|
124
128
|
codecrate pack . -o context.md
|
|
129
|
+
codecrate pack --repo /path/to/repo1 --repo /path/to/repo2 -o multi.md
|
|
130
|
+
|
|
131
|
+
When using ``--repo``, omit the positional ``ROOT``. Specifying both is an error.
|
|
125
132
|
|
|
126
133
|
Useful flags:
|
|
127
134
|
|
|
@@ -131,7 +138,9 @@ Useful flags:
|
|
|
131
138
|
* ``--respect-gitignore / --no-respect-gitignore``: include ignored files or not
|
|
132
139
|
* ``--include GLOB`` (repeatable): include patterns
|
|
133
140
|
* ``--exclude GLOB`` (repeatable): exclude patterns
|
|
134
|
-
* ``--split-max-chars N``:
|
|
141
|
+
* ``--split-max-chars N``: additionally emit ``.partN.md`` files for LLMs (the
|
|
142
|
+
main output stays unsplit). For multi-repo packs, parts are named
|
|
143
|
+
``output.<repo>.partN.md``
|
|
135
144
|
|
|
136
145
|
|
|
137
146
|
unpack
|
|
@@ -41,6 +41,12 @@ Pack a repository into ``context.md``:
|
|
|
41
41
|
|
|
42
42
|
codecrate pack /path/to/repo -o context.md
|
|
43
43
|
|
|
44
|
+
Pack multiple repositories into a single output:
|
|
45
|
+
|
|
46
|
+
.. code-block:: console
|
|
47
|
+
|
|
48
|
+
codecrate pack --repo /path/to/repo1 --repo /path/to/repo2 -o multi.md
|
|
49
|
+
|
|
44
50
|
Common options:
|
|
45
51
|
|
|
46
52
|
* ``--dedupe``: deduplicate identical function bodies (enables stub layout when effective)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from codecrate.cli import main
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _write_repo(root: Path, filename: str, content: str) -> None:
|
|
11
|
+
root.mkdir()
|
|
12
|
+
(root / filename).write_text(content, encoding="utf-8")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_pack_multi_repos(tmp_path: Path) -> None:
|
|
16
|
+
repo1 = tmp_path / "repo1"
|
|
17
|
+
repo2 = tmp_path / "repo2"
|
|
18
|
+
_write_repo(repo1, "a.py", "def alpha():\n return 1\n")
|
|
19
|
+
_write_repo(repo2, "b.py", "def beta():\n return 2\n")
|
|
20
|
+
|
|
21
|
+
out_path = tmp_path / "multi.md"
|
|
22
|
+
main(["pack", "--repo", str(repo1), "--repo", str(repo2), "-o", str(out_path)])
|
|
23
|
+
|
|
24
|
+
text = out_path.read_text(encoding="utf-8")
|
|
25
|
+
assert "# Repository: repo1" in text
|
|
26
|
+
assert "# Repository: repo2" in text
|
|
27
|
+
assert "def alpha()" in text
|
|
28
|
+
assert "def beta()" in text
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_pack_rejects_root_and_repo(tmp_path: Path) -> None:
|
|
32
|
+
repo = tmp_path / "repo"
|
|
33
|
+
_write_repo(repo, "a.py", "def alpha():\n return 1\n")
|
|
34
|
+
|
|
35
|
+
out_path = tmp_path / "multi.md"
|
|
36
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
37
|
+
main(["pack", str(repo), "--repo", str(repo), "-o", str(out_path)])
|
|
38
|
+
|
|
39
|
+
assert excinfo.value.code == 2
|
codecrate-0.1.0/codecrate/cli.py
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import argparse
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
from .config import load_config
|
|
7
|
-
from .diffgen import generate_patch_markdown
|
|
8
|
-
from .discover import discover_files
|
|
9
|
-
from .markdown import render_markdown
|
|
10
|
-
from .packer import pack_repo
|
|
11
|
-
from .token_budget import split_by_max_chars
|
|
12
|
-
from .udiff import apply_file_diffs, parse_unified_diff
|
|
13
|
-
from .unpacker import unpack_to_dir
|
|
14
|
-
from .validate import validate_pack_markdown
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def build_parser() -> argparse.ArgumentParser:
|
|
18
|
-
p = argparse.ArgumentParser(
|
|
19
|
-
prog="codecrate",
|
|
20
|
-
description="Pack/unpack/patch/apply for repositories (Python + text files).",
|
|
21
|
-
)
|
|
22
|
-
sub = p.add_subparsers(dest="cmd", required=True)
|
|
23
|
-
|
|
24
|
-
# pack
|
|
25
|
-
pack = sub.add_parser("pack", help="Pack a repository/directory into Markdown.")
|
|
26
|
-
pack.add_argument("root", type=Path, help="Root directory to scan")
|
|
27
|
-
pack.add_argument(
|
|
28
|
-
"-o",
|
|
29
|
-
"--output",
|
|
30
|
-
type=Path,
|
|
31
|
-
default=None,
|
|
32
|
-
help="Output markdown path (default: config 'output' or context.md)",
|
|
33
|
-
)
|
|
34
|
-
pack.add_argument(
|
|
35
|
-
"--dedupe", action="store_true", help="Deduplicate identical function bodies"
|
|
36
|
-
)
|
|
37
|
-
pack.add_argument(
|
|
38
|
-
"--layout",
|
|
39
|
-
choices=["auto", "stubs", "full"],
|
|
40
|
-
default=None,
|
|
41
|
-
help="Output layout: auto|stubs|full (default: auto via config)",
|
|
42
|
-
)
|
|
43
|
-
pack.add_argument(
|
|
44
|
-
"--keep-docstrings",
|
|
45
|
-
action=argparse.BooleanOptionalAction,
|
|
46
|
-
default=None,
|
|
47
|
-
help="Keep docstrings in stubbed file view (default: true via config)",
|
|
48
|
-
)
|
|
49
|
-
pack.add_argument(
|
|
50
|
-
"--respect-gitignore",
|
|
51
|
-
action=argparse.BooleanOptionalAction,
|
|
52
|
-
default=None,
|
|
53
|
-
help="Respect .gitignore (default: true via config)",
|
|
54
|
-
)
|
|
55
|
-
pack.add_argument(
|
|
56
|
-
"--manifest",
|
|
57
|
-
action=argparse.BooleanOptionalAction,
|
|
58
|
-
default=None,
|
|
59
|
-
help="Include Manifest section (default: true via config)",
|
|
60
|
-
)
|
|
61
|
-
pack.add_argument(
|
|
62
|
-
"--include", action="append", default=None, help="Include glob (repeatable)"
|
|
63
|
-
)
|
|
64
|
-
pack.add_argument(
|
|
65
|
-
"--exclude", action="append", default=None, help="Exclude glob (repeatable)"
|
|
66
|
-
)
|
|
67
|
-
pack.add_argument(
|
|
68
|
-
"--split-max-chars",
|
|
69
|
-
type=int,
|
|
70
|
-
default=None,
|
|
71
|
-
help="Split output into .partN.md files",
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
# unpack
|
|
75
|
-
unpack = sub.add_parser(
|
|
76
|
-
"unpack", help="Reconstruct files from a packed context Markdown."
|
|
77
|
-
)
|
|
78
|
-
unpack.add_argument("markdown", type=Path, help="Packed Markdown file from `pack`")
|
|
79
|
-
unpack.add_argument(
|
|
80
|
-
"-o",
|
|
81
|
-
"--out-dir",
|
|
82
|
-
type=Path,
|
|
83
|
-
required=True,
|
|
84
|
-
help="Output directory for reconstructed files",
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
# patch
|
|
88
|
-
patch = sub.add_parser(
|
|
89
|
-
"patch",
|
|
90
|
-
help="Generate a diff-only patch Markdown from old pack + current repo.",
|
|
91
|
-
)
|
|
92
|
-
patch.add_argument(
|
|
93
|
-
"old_markdown", type=Path, help="Older packed Markdown (baseline)"
|
|
94
|
-
)
|
|
95
|
-
patch.add_argument("root", type=Path, help="Current repo root to compare against")
|
|
96
|
-
patch.add_argument(
|
|
97
|
-
"-o",
|
|
98
|
-
"--output",
|
|
99
|
-
type=Path,
|
|
100
|
-
default=Path("patch.md"),
|
|
101
|
-
help="Output patch markdown",
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
# apply
|
|
105
|
-
apply = sub.add_parser("apply", help="Apply a diff-only patch Markdown to a repo.")
|
|
106
|
-
apply.add_argument(
|
|
107
|
-
"patch_markdown", type=Path, help="Patch Markdown containing ```diff blocks"
|
|
108
|
-
)
|
|
109
|
-
apply.add_argument("root", type=Path, help="Repo root to apply patch to")
|
|
110
|
-
# validate-pack
|
|
111
|
-
vpack = sub.add_parser(
|
|
112
|
-
"validate-pack",
|
|
113
|
-
help="Validate a packed context Markdown (sha/markers/canonical consistency).",
|
|
114
|
-
)
|
|
115
|
-
vpack.add_argument("markdown", type=Path, help="Packed Markdown to validate")
|
|
116
|
-
vpack.add_argument(
|
|
117
|
-
"--root",
|
|
118
|
-
type=Path,
|
|
119
|
-
default=None,
|
|
120
|
-
help="Optional repo root to compare reconstructed files against",
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
return p
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def _extract_diff_blocks(md_text: str) -> str:
|
|
127
|
-
"""
|
|
128
|
-
Extract only diff fences from markdown and concatenate to a unified diff string.
|
|
129
|
-
"""
|
|
130
|
-
lines = md_text.splitlines()
|
|
131
|
-
out: list[str] = []
|
|
132
|
-
i = 0
|
|
133
|
-
while i < len(lines):
|
|
134
|
-
if lines[i].strip() == "```diff":
|
|
135
|
-
i += 1
|
|
136
|
-
while i < len(lines) and lines[i].strip() != "```":
|
|
137
|
-
out.append(lines[i])
|
|
138
|
-
i += 1
|
|
139
|
-
i += 1
|
|
140
|
-
return "\n".join(out) + "\n"
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def main(argv: list[str] | None = None) -> None:
|
|
144
|
-
parser = build_parser()
|
|
145
|
-
args = parser.parse_args(argv)
|
|
146
|
-
|
|
147
|
-
if args.cmd == "pack":
|
|
148
|
-
root: Path = args.root.resolve()
|
|
149
|
-
cfg = load_config(root)
|
|
150
|
-
|
|
151
|
-
include = args.include if args.include is not None else cfg.include
|
|
152
|
-
exclude = args.exclude if args.exclude is not None else cfg.exclude
|
|
153
|
-
|
|
154
|
-
keep_docstrings = (
|
|
155
|
-
cfg.keep_docstrings
|
|
156
|
-
if args.keep_docstrings is None
|
|
157
|
-
else bool(args.keep_docstrings)
|
|
158
|
-
)
|
|
159
|
-
include_manifest = (
|
|
160
|
-
cfg.manifest if args.manifest is None else bool(args.manifest)
|
|
161
|
-
)
|
|
162
|
-
respect_gitignore = (
|
|
163
|
-
cfg.respect_gitignore
|
|
164
|
-
if args.respect_gitignore is None
|
|
165
|
-
else bool(args.respect_gitignore)
|
|
166
|
-
)
|
|
167
|
-
dedupe = bool(args.dedupe) or bool(cfg.dedupe)
|
|
168
|
-
split_max_chars = (
|
|
169
|
-
cfg.split_max_chars
|
|
170
|
-
if args.split_max_chars is None
|
|
171
|
-
else int(args.split_max_chars or 0)
|
|
172
|
-
)
|
|
173
|
-
layout = (
|
|
174
|
-
str(args.layout).strip().lower()
|
|
175
|
-
if args.layout is not None
|
|
176
|
-
else str(getattr(cfg, "layout", "auto")).strip().lower()
|
|
177
|
-
)
|
|
178
|
-
out_path = (
|
|
179
|
-
args.output
|
|
180
|
-
if args.output is not None
|
|
181
|
-
else Path(getattr(cfg, "output", "context.md"))
|
|
182
|
-
)
|
|
183
|
-
disc = discover_files(
|
|
184
|
-
root=root,
|
|
185
|
-
include=include,
|
|
186
|
-
exclude=exclude,
|
|
187
|
-
respect_gitignore=respect_gitignore,
|
|
188
|
-
)
|
|
189
|
-
pack, canonical = pack_repo(
|
|
190
|
-
disc.root, disc.files, keep_docstrings=keep_docstrings, dedupe=dedupe
|
|
191
|
-
)
|
|
192
|
-
md = render_markdown(
|
|
193
|
-
pack, canonical, layout=layout, include_manifest=include_manifest
|
|
194
|
-
)
|
|
195
|
-
# Always write the canonical, unsplit pack
|
|
196
|
-
# for machine parsing (unpack/validate).
|
|
197
|
-
out_path.write_text(md, encoding="utf-8")
|
|
198
|
-
|
|
199
|
-
# Additionally, write split parts for LLM consumption, if requested.
|
|
200
|
-
parts = split_by_max_chars(md, out_path, split_max_chars)
|
|
201
|
-
extra = [p for p in parts if p.path != out_path]
|
|
202
|
-
for part in extra:
|
|
203
|
-
part.path.write_text(part.content, encoding="utf-8")
|
|
204
|
-
|
|
205
|
-
if extra:
|
|
206
|
-
print(f"Wrote {out_path} and {len(extra)} split part file(s).")
|
|
207
|
-
else:
|
|
208
|
-
print(f"Wrote {out_path}.")
|
|
209
|
-
elif args.cmd == "unpack":
|
|
210
|
-
md_text = args.markdown.read_text(encoding="utf-8", errors="replace")
|
|
211
|
-
unpack_to_dir(md_text, args.out_dir)
|
|
212
|
-
print(f"Unpacked into {args.out_dir}")
|
|
213
|
-
|
|
214
|
-
elif args.cmd == "patch":
|
|
215
|
-
old_md = args.old_markdown.read_text(encoding="utf-8", errors="replace")
|
|
216
|
-
cfg = load_config(args.root)
|
|
217
|
-
patch_md = generate_patch_markdown(
|
|
218
|
-
old_md,
|
|
219
|
-
args.root,
|
|
220
|
-
include=cfg.include,
|
|
221
|
-
exclude=cfg.exclude,
|
|
222
|
-
respect_gitignore=cfg.respect_gitignore,
|
|
223
|
-
)
|
|
224
|
-
args.output.write_text(patch_md, encoding="utf-8")
|
|
225
|
-
print(f"Wrote {args.output}")
|
|
226
|
-
|
|
227
|
-
elif args.cmd == "validate-pack":
|
|
228
|
-
md_text = args.markdown.read_text(encoding="utf-8", errors="replace")
|
|
229
|
-
report = validate_pack_markdown(md_text, root=args.root)
|
|
230
|
-
if report.warnings:
|
|
231
|
-
print("Warnings:")
|
|
232
|
-
for w in report.warnings:
|
|
233
|
-
print(f"- {w}")
|
|
234
|
-
if report.errors:
|
|
235
|
-
print("Errors:")
|
|
236
|
-
for e in report.errors:
|
|
237
|
-
print(f"- {e}")
|
|
238
|
-
raise SystemExit(1)
|
|
239
|
-
print("OK: pack is internally consistent.")
|
|
240
|
-
|
|
241
|
-
elif args.cmd == "apply":
|
|
242
|
-
md_text = args.patch_markdown.read_text(encoding="utf-8", errors="replace")
|
|
243
|
-
diff_text = _extract_diff_blocks(md_text)
|
|
244
|
-
diffs = parse_unified_diff(diff_text)
|
|
245
|
-
changed = apply_file_diffs(diffs, args.root)
|
|
246
|
-
print(f"Applied patch to {len(changed)} file(s).")
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if __name__ == "__main__":
|
|
250
|
-
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|