codecrate 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl

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/_version.py CHANGED
@@ -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.1'
32
- __version_tuple__ = version_tuple = (0, 1, 1)
31
+ __version__ = version = '0.1.2'
32
+ __version_tuple__ = version_tuple = (0, 1, 2)
33
33
 
34
34
  __commit_id__ = commit_id = None
codecrate/cli.py CHANGED
@@ -1,9 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ from dataclasses import dataclass
4
5
  from pathlib import Path
5
6
 
6
- from .config import load_config
7
+ from .config import Config, load_config
7
8
  from .diffgen import generate_patch_markdown
8
9
  from .discover import discover_files
9
10
  from .markdown import render_markdown
@@ -22,8 +23,22 @@ def build_parser() -> argparse.ArgumentParser:
22
23
  sub = p.add_subparsers(dest="cmd", required=True)
23
24
 
24
25
  # 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")
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
+ )
27
42
  pack.add_argument(
28
43
  "-o",
29
44
  "--output",
@@ -123,6 +138,135 @@ def build_parser() -> argparse.ArgumentParser:
123
138
  return p
124
139
 
125
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
+
126
270
  def _extract_diff_blocks(md_text: str) -> str:
127
271
  """
128
272
  Extract only diff fences from markdown and concatenate to a unified diff string.
@@ -140,73 +284,111 @@ def _extract_diff_blocks(md_text: str) -> str:
140
284
  return "\n".join(out) + "\n"
141
285
 
142
286
 
143
- def main(argv: list[str] | None = None) -> None:
287
+ def main(argv: list[str] | None = None) -> None: # noqa: C901
144
288
  parser = build_parser()
145
289
  args = parser.parse_args(argv)
146
290
 
147
291
  if args.cmd == "pack":
148
- root: Path = args.root.resolve()
149
- cfg = load_config(root)
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()]
150
302
 
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
303
+ used_labels: set[str] = set()
304
+ used_slugs: set[str] = set()
305
+ pack_runs: list[PackRun] = []
153
306
 
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()
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
177
345
  )
178
- if args.output is not None:
179
- out_path = args.output
346
+ if len(pack_runs) == 1:
347
+ md = pack_runs[0].markdown
180
348
  else:
181
- out_path = Path(getattr(cfg, "output", "context.md"))
182
- if not out_path.is_absolute():
183
- out_path = root / out_path
184
- disc = discover_files(
185
- root=root,
186
- include=include,
187
- exclude=exclude,
188
- respect_gitignore=respect_gitignore,
189
- )
190
- pack, canonical = pack_repo(
191
- disc.root, disc.files, keep_docstrings=keep_docstrings, dedupe=dedupe
192
- )
193
- md = render_markdown(
194
- pack, canonical, layout=layout, include_manifest=include_manifest
195
- )
349
+ md = _combine_pack_markdown(pack_runs)
350
+
196
351
  # Always write the canonical, unsplit pack
197
352
  # for machine parsing (unpack/validate).
198
353
  out_path.write_text(md, encoding="utf-8")
199
354
 
200
- # Additionally, write split parts for LLM consumption, if requested.
201
- parts = split_by_max_chars(md, out_path, split_max_chars)
202
- extra = [p for p in parts if p.path != out_path]
203
- for part in extra:
204
- part.path.write_text(part.content, encoding="utf-8")
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)
205
378
 
206
- if extra:
207
- print(f"Wrote {out_path} and {len(extra)} split part file(s).")
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
+ )
208
387
  else:
209
- print(f"Wrote {out_path}.")
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.")
210
392
  elif args.cmd == "unpack":
211
393
  md_text = args.markdown.read_text(encoding="utf-8", errors="replace")
212
394
  unpack_to_dir(md_text, args.out_dir)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecrate
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Pack Python codebases into Markdown optimized for LLM context delivery (pack/unpack/patch/apply)
5
5
  Author-email: Holger Nahrstaedt <nahrstaedt@gmail.com>
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  codecrate/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- codecrate/_version.py,sha256=m8HxkqoKGw_wAJtc4ZokpJKNLXqp4zwnNhbnfDtro7w,704
3
- codecrate/cli.py,sha256=0QCw4y4NJsiEM_dKOdF6qmJXPXeEoRBYC6cAH5rMKqg,8377
2
+ codecrate/_version.py,sha256=Ok5oAXdWgR9aghaFXTafTeDW6sYO3uVe6d2Nket57R4,704
3
+ codecrate/cli.py,sha256=AXURc8QeFzR0HvbhittK38g4N1xr4al564uCBq0PTlY,13644
4
4
  codecrate/config.py,sha256=8VOmpjHC3am6LRFnyHUY3376947XTvSFGp3fUxSZgOk,3523
5
5
  codecrate/diffgen.py,sha256=xMH-wJeMox_xoMLrGck2o7RP5mjlCJjndSHPHql9CJg,3432
6
6
  codecrate/discover.py,sha256=eWebPPbPHy5WrtbCX-RA1hKiPyQau6buUm5OcjpVi9U,2847
@@ -16,9 +16,9 @@ codecrate/token_budget.py,sha256=0vG9h7l5yNn909QQEItrYQJIjMT2h5LcM9iZf4zu-3c,129
16
16
  codecrate/udiff.py,sha256=bMvwhgqXZw2pIW5VcRRATUoDspM2XGQU3nInD_A7WWo,6249
17
17
  codecrate/unpacker.py,sha256=soh6qwRE6sxoBAbr3UbXk1O5aiwdnfUhkqfFdSvU0_I,5063
18
18
  codecrate/validate.py,sha256=-KVU7RQ8zJG-YJoD8kLCYgE0OWWdy-yYzl0gyp-3mWo,4250
19
- codecrate-0.1.1.dist-info/licenses/LICENSE,sha256=O6yVC2oL8vUoAA3oPEvLSbvxzmwtOOPiY7dOZzgrxi0,1074
20
- codecrate-0.1.1.dist-info/METADATA,sha256=xFktVoakgBrUyCAMZpcecQMjAyXMYOYt3fv80n0H9MI,9394
21
- codecrate-0.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
22
- codecrate-0.1.1.dist-info/entry_points.txt,sha256=DcY9tib-PzdLebck4B2RYJ0CGH6cqAJMCHPU3MdA0Dk,49
23
- codecrate-0.1.1.dist-info/top_level.txt,sha256=-jD2a_aH1iQN4atRhGw7ZhEYnOWe88nfmkz6sPZ6WEg,10
24
- codecrate-0.1.1.dist-info/RECORD,,
19
+ codecrate-0.1.2.dist-info/licenses/LICENSE,sha256=O6yVC2oL8vUoAA3oPEvLSbvxzmwtOOPiY7dOZzgrxi0,1074
20
+ codecrate-0.1.2.dist-info/METADATA,sha256=9GAqz6NFaQ1g9uRKKYw6hn4RVhmO5lemKle54PVDoK4,9394
21
+ codecrate-0.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
22
+ codecrate-0.1.2.dist-info/entry_points.txt,sha256=DcY9tib-PzdLebck4B2RYJ0CGH6cqAJMCHPU3MdA0Dk,49
23
+ codecrate-0.1.2.dist-info/top_level.txt,sha256=-jD2a_aH1iQN4atRhGw7ZhEYnOWe88nfmkz6sPZ6WEg,10
24
+ codecrate-0.1.2.dist-info/RECORD,,