contextzip 0.3.6__tar.gz → 0.3.7__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.
- {contextzip-0.3.6 → contextzip-0.3.7}/PKG-INFO +34 -2
- {contextzip-0.3.6 → contextzip-0.3.7}/README.md +33 -1
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/__init__.py +1 -1
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/cli.py +95 -1
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/config.py +30 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/filters.py +145 -2
- contextzip-0.3.7/contextzip/webui/__init__.py +9 -0
- contextzip-0.3.7/contextzip/webui/assets.py +1012 -0
- contextzip-0.3.7/contextzip/webui/persist.py +59 -0
- contextzip-0.3.7/contextzip/webui/server.py +426 -0
- contextzip-0.3.7/contextzip/webui/suggestions.py +136 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip.egg-info/PKG-INFO +34 -2
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip.egg-info/SOURCES.txt +6 -1
- {contextzip-0.3.6 → contextzip-0.3.7}/pyproject.toml +1 -1
- {contextzip-0.3.6 → contextzip-0.3.7}/LICENSE +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/ai/__init__.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/ai/gemini.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/ai/heuristic.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/ai/selector.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/api.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/cli_ai.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/cli_display.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/cli_onboard.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/clipboard.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/detector.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/error_parser.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/git.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/packager.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/project_config.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/rules/__init__.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/rules/base.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/rules/errors/__init__.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/rules/errors/node.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/rules/errors/python.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/rules/go.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/rules/node.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/rules/python.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/rules/ruby.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/rules/rust.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip/watcher.py +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip.egg-info/dependency_links.txt +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip.egg-info/entry_points.txt +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip.egg-info/requires.txt +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/contextzip.egg-info/top_level.txt +0 -0
- {contextzip-0.3.6 → contextzip-0.3.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: contextzip
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.7
|
|
4
4
|
Summary: Intelligently package your codebase for AI tools
|
|
5
5
|
Author-email: Deepesh <akadeepesh@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -53,6 +53,7 @@ contextzip eliminates that entirely. Run it from your project root — it detect
|
|
|
53
53
|
- **AI-powered file selection** — describe your task in plain English with `--prompt` and Gemini selects the minimum relevant files automatically, no manual hunting required
|
|
54
54
|
- **Terminal error watcher** — wrap any dev server with `contextzip watch` to auto-detect errors and package a ready-to-upload debug context in one keypress
|
|
55
55
|
- **Configurable workspace location** — `.contextzip/` lives at the git root by default, but you can pin it elsewhere per-machine (`contextzip config --set-workspace`) or for the whole team via a committed `.contextzip/config.json`
|
|
56
|
+
- **Visual config UI** — `contextzip config --ui` opens a local browser tab to set include/exclude by clicking through your actual file tree, with live counts and one-click suggestions for PDFs, media, and other non-code files — nothing leaves your machine
|
|
56
57
|
- **Persistent workspace** — all generated ZIPs land in `.contextzip/output/`, discoverable, reusable, and git-ignored automatically
|
|
57
58
|
- **Warns before it's a problem** — flags large (≥ 1 MB) and binary files that AI tools can't read, before you waste an upload
|
|
58
59
|
- **Handles edge cases** — dangling symlinks, unreadable files, and paths outside the project tree are caught and reported, never silently dropped
|
|
@@ -342,7 +343,7 @@ All keys are optional — start with just the ones you need.
|
|
|
342
343
|
|
|
343
344
|
`always_include`/`always_exclude` are additive on top of `--include`/`--exclude` for that run — an explicit `contextzip include PATH` still has the final say over what's actually packaged. Persistent preferences belong in `config.json` rather than an ever-growing list of CLI flags; CLI flags stay for one-off, explicit actions (`--dry-run`, `--prompt "…"`, `--output FILE`, etc.).
|
|
344
345
|
|
|
345
|
-
|
|
346
|
+
You don't have to hand-write `config.json` — see [Visual config UI](#visual-config-ui) below for a point-and-click way to produce it.
|
|
346
347
|
|
|
347
348
|
### Deprecation: `.contextzip.json`
|
|
348
349
|
|
|
@@ -352,6 +353,37 @@ Earlier versions of contextzip read a `.contextzip.json` file at the project roo
|
|
|
352
353
|
|
|
353
354
|
---
|
|
354
355
|
|
|
356
|
+
## Visual config UI
|
|
357
|
+
|
|
358
|
+
```
|
|
359
|
+
contextzip config --ui
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Opens a local browser tab for setting `always_include`/`always_exclude` by clicking through your actual file tree instead of hand-writing patterns — live file counts and packed size update as you go, and one-click chips suggest excluding things like PDFs, office docs, fonts, media, or anything over 1MB that isn't already covered by contextzip's default rules.
|
|
363
|
+
|
|
364
|
+
It's also offered automatically the **first time** you run `contextzip` in a project that has no config at all:
|
|
365
|
+
|
|
366
|
+
```
|
|
367
|
+
$ contextzip
|
|
368
|
+
╭─────────────────────────╮
|
|
369
|
+
│ contextzip v0.3.5 │
|
|
370
|
+
╰─────────────────────────╯
|
|
371
|
+
...
|
|
372
|
+
╭─ First run ─────────────────────────────────────╮
|
|
373
|
+
│ No project config found yet. │
|
|
374
|
+
│ contextzip can open a local browser tab... │
|
|
375
|
+
╰───────────────────────────────────────────────────╯
|
|
376
|
+
Set up include/exclude visually now? [Y/n]:
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Decline once and it won't ask again (run `contextzip config --ui` any time you want it). It's also skipped automatically for non-interactive runs, when `CI` is set, or when `--prompt`/`--output` are passed — it only ever offers on a plain, interactive, first-ever run.
|
|
380
|
+
|
|
381
|
+
**Nothing about your project leaves your machine.** The server binds to `127.0.0.1` only (never `0.0.0.0`), every request needs a random per-session token embedded in the URL (the same approach Jupyter Notebook uses), and the page itself makes no calls anywhere except back to that local server — no CDN scripts, no web fonts, no analytics. Saving writes straight to `.contextzip/config.json` on disk and the server shuts itself down; if you close the tab without saving, it also shuts down after a short idle period so a forgotten session doesn't linger as an open port.
|
|
382
|
+
|
|
383
|
+
If you accept the first-run offer, the same `contextzip` invocation picks up whatever you saved and finishes packaging immediately — no need to run it again.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
355
387
|
## Contributing
|
|
356
388
|
|
|
357
389
|
Contributions are welcome — especially new framework rule sets, edge case fixes, and platform-specific clipboard improvements.
|
|
@@ -24,6 +24,7 @@ contextzip eliminates that entirely. Run it from your project root — it detect
|
|
|
24
24
|
- **AI-powered file selection** — describe your task in plain English with `--prompt` and Gemini selects the minimum relevant files automatically, no manual hunting required
|
|
25
25
|
- **Terminal error watcher** — wrap any dev server with `contextzip watch` to auto-detect errors and package a ready-to-upload debug context in one keypress
|
|
26
26
|
- **Configurable workspace location** — `.contextzip/` lives at the git root by default, but you can pin it elsewhere per-machine (`contextzip config --set-workspace`) or for the whole team via a committed `.contextzip/config.json`
|
|
27
|
+
- **Visual config UI** — `contextzip config --ui` opens a local browser tab to set include/exclude by clicking through your actual file tree, with live counts and one-click suggestions for PDFs, media, and other non-code files — nothing leaves your machine
|
|
27
28
|
- **Persistent workspace** — all generated ZIPs land in `.contextzip/output/`, discoverable, reusable, and git-ignored automatically
|
|
28
29
|
- **Warns before it's a problem** — flags large (≥ 1 MB) and binary files that AI tools can't read, before you waste an upload
|
|
29
30
|
- **Handles edge cases** — dangling symlinks, unreadable files, and paths outside the project tree are caught and reported, never silently dropped
|
|
@@ -313,7 +314,7 @@ All keys are optional — start with just the ones you need.
|
|
|
313
314
|
|
|
314
315
|
`always_include`/`always_exclude` are additive on top of `--include`/`--exclude` for that run — an explicit `contextzip include PATH` still has the final say over what's actually packaged. Persistent preferences belong in `config.json` rather than an ever-growing list of CLI flags; CLI flags stay for one-off, explicit actions (`--dry-run`, `--prompt "…"`, `--output FILE`, etc.).
|
|
315
316
|
|
|
316
|
-
|
|
317
|
+
You don't have to hand-write `config.json` — see [Visual config UI](#visual-config-ui) below for a point-and-click way to produce it.
|
|
317
318
|
|
|
318
319
|
### Deprecation: `.contextzip.json`
|
|
319
320
|
|
|
@@ -323,6 +324,37 @@ Earlier versions of contextzip read a `.contextzip.json` file at the project roo
|
|
|
323
324
|
|
|
324
325
|
---
|
|
325
326
|
|
|
327
|
+
## Visual config UI
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
contextzip config --ui
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Opens a local browser tab for setting `always_include`/`always_exclude` by clicking through your actual file tree instead of hand-writing patterns — live file counts and packed size update as you go, and one-click chips suggest excluding things like PDFs, office docs, fonts, media, or anything over 1MB that isn't already covered by contextzip's default rules.
|
|
334
|
+
|
|
335
|
+
It's also offered automatically the **first time** you run `contextzip` in a project that has no config at all:
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
$ contextzip
|
|
339
|
+
╭─────────────────────────╮
|
|
340
|
+
│ contextzip v0.3.5 │
|
|
341
|
+
╰─────────────────────────╯
|
|
342
|
+
...
|
|
343
|
+
╭─ First run ─────────────────────────────────────╮
|
|
344
|
+
│ No project config found yet. │
|
|
345
|
+
│ contextzip can open a local browser tab... │
|
|
346
|
+
╰───────────────────────────────────────────────────╯
|
|
347
|
+
Set up include/exclude visually now? [Y/n]:
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Decline once and it won't ask again (run `contextzip config --ui` any time you want it). It's also skipped automatically for non-interactive runs, when `CI` is set, or when `--prompt`/`--output` are passed — it only ever offers on a plain, interactive, first-ever run.
|
|
351
|
+
|
|
352
|
+
**Nothing about your project leaves your machine.** The server binds to `127.0.0.1` only (never `0.0.0.0`), every request needs a random per-session token embedded in the URL (the same approach Jupyter Notebook uses), and the page itself makes no calls anywhere except back to that local server — no CDN scripts, no web fonts, no analytics. Saving writes straight to `.contextzip/config.json` on disk and the server shuts itself down; if you close the tab without saving, it also shuts down after a short idle period so a forgotten session doesn't linger as an open port.
|
|
353
|
+
|
|
354
|
+
If you accept the first-run offer, the same `contextzip` invocation picks up whatever you saved and finishes packaging immediately — no need to run it again.
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
326
358
|
## Contributing
|
|
327
359
|
|
|
328
360
|
Contributions are welcome — especially new framework rule sets, edge case fixes, and platform-specific clipboard improvements.
|
|
@@ -409,11 +409,20 @@ def cmd_watch(command: tuple[str, ...]) -> None:
|
|
|
409
409
|
default=False,
|
|
410
410
|
help="Print the path to the config file and exit.",
|
|
411
411
|
)
|
|
412
|
+
@click.option(
|
|
413
|
+
"--ui",
|
|
414
|
+
"launch_ui",
|
|
415
|
+
is_flag=True,
|
|
416
|
+
default=False,
|
|
417
|
+
help="Open a local browser UI to set include/exclude preferences visually "
|
|
418
|
+
"and write .contextzip/config.json.",
|
|
419
|
+
)
|
|
412
420
|
def cmd_config(
|
|
413
421
|
reset_key: bool,
|
|
414
422
|
set_workspace: str | None,
|
|
415
423
|
reset_workspace: bool,
|
|
416
424
|
show_key_path: bool,
|
|
425
|
+
launch_ui: bool,
|
|
417
426
|
) -> None:
|
|
418
427
|
"""
|
|
419
428
|
Manage contextzip configuration.
|
|
@@ -426,6 +435,7 @@ def cmd_config(
|
|
|
426
435
|
contextzip config --set-workspace ~/zips # a fixed custom location
|
|
427
436
|
contextzip config --reset-workspace # clear the personal override
|
|
428
437
|
contextzip config --show-key-path # print config file location
|
|
438
|
+
contextzip config --ui # visually set include/exclude
|
|
429
439
|
|
|
430
440
|
\b
|
|
431
441
|
A workspace location can also be pinned for the whole team by committing
|
|
@@ -434,6 +444,16 @@ def cmd_config(
|
|
|
434
444
|
Project config (if present) takes priority over this personal setting —
|
|
435
445
|
see the README for the full precedence order.
|
|
436
446
|
"""
|
|
447
|
+
if launch_ui:
|
|
448
|
+
from contextzip.detector import detect as _detect
|
|
449
|
+
from contextzip.webui.server import launch_config_ui
|
|
450
|
+
|
|
451
|
+
project_dir = Path(os.getcwd()).resolve()
|
|
452
|
+
with console.status("[cyan]Detecting project ecosystem…[/]", spinner="dots"):
|
|
453
|
+
detection = _detect(project_dir)
|
|
454
|
+
launch_config_ui(project_dir, detection, con=console)
|
|
455
|
+
return
|
|
456
|
+
|
|
437
457
|
if show_key_path:
|
|
438
458
|
console.print(f"\n [dim]Config file:[/] [cyan]{config_path()}[/]\n")
|
|
439
459
|
return
|
|
@@ -528,7 +548,9 @@ def cmd_config(
|
|
|
528
548
|
" [dim]Run [cyan]contextzip config --set-workspace <location>[/] to set "
|
|
529
549
|
"a personal default,\n"
|
|
530
550
|
" or commit a [cyan].contextzip/config.json[/] at the project root to "
|
|
531
|
-
"share one with your team
|
|
551
|
+
"share one with your team.\n"
|
|
552
|
+
" Run [cyan]contextzip config --ui[/] to set include/exclude "
|
|
553
|
+
"preferences visually.[/]",
|
|
532
554
|
title="[bold]contextzip config[/]",
|
|
533
555
|
border_style="cyan",
|
|
534
556
|
padding=(0, 2),
|
|
@@ -605,6 +627,69 @@ def _enforce_ai_config(ai_cfg) -> None:
|
|
|
605
627
|
raise SystemExit(1)
|
|
606
628
|
|
|
607
629
|
|
|
630
|
+
def _maybe_offer_config_ui(
|
|
631
|
+
project_dir: Path,
|
|
632
|
+
detection,
|
|
633
|
+
*,
|
|
634
|
+
prompt: str | None,
|
|
635
|
+
output: str | None,
|
|
636
|
+
) -> bool:
|
|
637
|
+
"""
|
|
638
|
+
On a project's very first run — no .contextzip/config.json and no
|
|
639
|
+
legacy .contextzip.json at all — offer to open the local config UI
|
|
640
|
+
instead of silently proceeding with bare defaults.
|
|
641
|
+
|
|
642
|
+
Deliberately conservative about when to ask:
|
|
643
|
+
- never if --prompt or --output were given (the user is mid-task,
|
|
644
|
+
not exploring — don't interrupt with a browser tab)
|
|
645
|
+
- never outside an interactive terminal, or with CI set (scripted/
|
|
646
|
+
automated runs must never block on a prompt)
|
|
647
|
+
- never again once declined once (persisted personally, not per
|
|
648
|
+
project — see config.get_config_ui_dismissed)
|
|
649
|
+
|
|
650
|
+
Returns True if the user saved a config during the offered session
|
|
651
|
+
(the caller should reload project config before continuing).
|
|
652
|
+
"""
|
|
653
|
+
import sys
|
|
654
|
+
|
|
655
|
+
if prompt or output:
|
|
656
|
+
return False
|
|
657
|
+
if not (sys.stdin.isatty() and sys.stdout.isatty()):
|
|
658
|
+
return False
|
|
659
|
+
if os.environ.get("CI"):
|
|
660
|
+
return False
|
|
661
|
+
|
|
662
|
+
from contextzip.config import get_config_ui_dismissed, save_config_ui_dismissed
|
|
663
|
+
|
|
664
|
+
if get_config_ui_dismissed():
|
|
665
|
+
return False
|
|
666
|
+
|
|
667
|
+
console.print(
|
|
668
|
+
Panel.fit(
|
|
669
|
+
"[bold]No project config found yet.[/]\n\n"
|
|
670
|
+
"[dim]contextzip can open a local browser tab where you pick "
|
|
671
|
+
"exactly what\nto include/exclude — with live file counts and "
|
|
672
|
+
"one-click suggestions\nfor things like PDFs or other non-code "
|
|
673
|
+
"files. Nothing leaves your machine.[/]",
|
|
674
|
+
title="[cyan]First run[/]",
|
|
675
|
+
border_style="cyan",
|
|
676
|
+
padding=(0, 2),
|
|
677
|
+
)
|
|
678
|
+
)
|
|
679
|
+
console.print()
|
|
680
|
+
|
|
681
|
+
if not click.confirm(" Set up include/exclude visually now?", default=True):
|
|
682
|
+
save_config_ui_dismissed()
|
|
683
|
+
console.print(
|
|
684
|
+
" [dim]No problem — run [cyan]contextzip config --ui[/] anytime.[/]\n"
|
|
685
|
+
)
|
|
686
|
+
return False
|
|
687
|
+
|
|
688
|
+
from contextzip.webui.server import launch_config_ui
|
|
689
|
+
|
|
690
|
+
return launch_config_ui(project_dir, detection, con=console)
|
|
691
|
+
|
|
692
|
+
|
|
608
693
|
def _run(
|
|
609
694
|
*,
|
|
610
695
|
extra_exclude: list[str] | None,
|
|
@@ -652,6 +737,15 @@ def _run(
|
|
|
652
737
|
|
|
653
738
|
print_detection(detection)
|
|
654
739
|
|
|
740
|
+
# ── First-run offer: visual config UI ────────────────────────────────────
|
|
741
|
+
if project_cfg.source_path is None:
|
|
742
|
+
saved = _maybe_offer_config_ui(
|
|
743
|
+
project_dir, detection, prompt=prompt, output=output
|
|
744
|
+
)
|
|
745
|
+
if saved:
|
|
746
|
+
project_cfg = load_project_config(project_dir)
|
|
747
|
+
console.print()
|
|
748
|
+
|
|
655
749
|
# ── Git-changes mode ─────────────────────────────────────────────────────
|
|
656
750
|
if git_changes:
|
|
657
751
|
with console.status("[cyan]Querying git for changed files…[/]", spinner="dots"):
|
|
@@ -201,6 +201,36 @@ def delete_workspace_location() -> bool:
|
|
|
201
201
|
return False
|
|
202
202
|
|
|
203
203
|
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
# "Don't offer the local config UI again" preference
|
|
206
|
+
#
|
|
207
|
+
# Set the first time a user declines the first-run offer to launch
|
|
208
|
+
# `contextzip config --ui` (see cli.py's _maybe_offer_config_ui), so the
|
|
209
|
+
# prompt genuinely asks only once per machine rather than nagging on every
|
|
210
|
+
# run of every config-less project. Running `contextzip config --ui`
|
|
211
|
+
# explicitly is never affected by this — it's just the unsolicited offer.
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def get_config_ui_dismissed() -> bool:
|
|
216
|
+
"""True if the user has previously declined the first-run config UI offer."""
|
|
217
|
+
try:
|
|
218
|
+
return bool(_read_config().get("config_ui_prompt_dismissed", False))
|
|
219
|
+
except Exception:
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def save_config_ui_dismissed() -> None:
|
|
224
|
+
"""Record that the user declined the first-run config UI offer."""
|
|
225
|
+
_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
data = _read_config()
|
|
227
|
+
data["config_ui_prompt_dismissed"] = True
|
|
228
|
+
_CONFIG_FILE.write_text(
|
|
229
|
+
json.dumps(data, indent=2) + "\n",
|
|
230
|
+
encoding="utf-8",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
204
234
|
def _read_config() -> dict:
|
|
205
235
|
"""Read and parse the config file. Returns {} if missing or malformed."""
|
|
206
236
|
if not _CONFIG_FILE.is_file():
|
|
@@ -26,6 +26,7 @@ Force-include (Phase 7):
|
|
|
26
26
|
from __future__ import annotations
|
|
27
27
|
|
|
28
28
|
import importlib
|
|
29
|
+
import os
|
|
29
30
|
from pathlib import Path
|
|
30
31
|
from dataclasses import dataclass, field
|
|
31
32
|
|
|
@@ -51,6 +52,40 @@ LARGE_FILE_WARN_BYTES = 1 * 1024 * 1024 # 1 MB
|
|
|
51
52
|
# Peek this many bytes to detect binary files
|
|
52
53
|
_BINARY_PEEK = 512
|
|
53
54
|
|
|
55
|
+
# Directory names scan_all_files() prunes during the walk itself, before
|
|
56
|
+
# even reading their contents. These are always dependency/build/VCS
|
|
57
|
+
# noise that no ecosystem's rule module would ever include, and — for
|
|
58
|
+
# .venv/venv especially — often contain symlinks to files well outside
|
|
59
|
+
# the project tree (e.g. bin/python -> the system interpreter), which is
|
|
60
|
+
# exactly the kind of thing that must never reach classify_scanned_files.
|
|
61
|
+
# Kept intentionally small and generic (not ecosystem-specific — that's
|
|
62
|
+
# what rules/*.py is for) since this list affects every project's scan.
|
|
63
|
+
_ALWAYS_PRUNED_DIRNAMES = frozenset(
|
|
64
|
+
{
|
|
65
|
+
".git",
|
|
66
|
+
".hg",
|
|
67
|
+
".svn",
|
|
68
|
+
"node_modules",
|
|
69
|
+
".venv",
|
|
70
|
+
"venv",
|
|
71
|
+
"env",
|
|
72
|
+
"__pycache__",
|
|
73
|
+
".tox",
|
|
74
|
+
".mypy_cache",
|
|
75
|
+
".pytest_cache",
|
|
76
|
+
".ruff_cache",
|
|
77
|
+
"dist",
|
|
78
|
+
"build",
|
|
79
|
+
".next",
|
|
80
|
+
".nuxt",
|
|
81
|
+
".cache",
|
|
82
|
+
"target",
|
|
83
|
+
"vendor",
|
|
84
|
+
"site-packages",
|
|
85
|
+
".contextzip",
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
54
89
|
|
|
55
90
|
# ---------------------------------------------------------------------------
|
|
56
91
|
# Result model
|
|
@@ -99,7 +134,9 @@ def build_spec(
|
|
|
99
134
|
encoding="utf-8", errors="ignore"
|
|
100
135
|
).splitlines()
|
|
101
136
|
# Strip comments and blank lines; pathspec handles the rest
|
|
102
|
-
patterns.extend(
|
|
137
|
+
patterns.extend(
|
|
138
|
+
line for line in lines if line.strip() and not line.startswith("#")
|
|
139
|
+
)
|
|
103
140
|
except OSError:
|
|
104
141
|
pass
|
|
105
142
|
|
|
@@ -122,6 +159,111 @@ def build_force_include_spec(patterns: list[str] | None) -> pathspec.PathSpec |
|
|
|
122
159
|
return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
|
|
123
160
|
|
|
124
161
|
|
|
162
|
+
def scan_all_files(project_dir: Path) -> list[tuple[Path, int]]:
|
|
163
|
+
"""
|
|
164
|
+
Lightweight full-tree walk that returns every regular file under
|
|
165
|
+
*project_dir* once, as (abs_path, size_bytes) tuples, without
|
|
166
|
+
classifying anything.
|
|
167
|
+
|
|
168
|
+
Meant for tools that need to classify the same file list against
|
|
169
|
+
several different specs without re-touching the filesystem each time
|
|
170
|
+
(the local config UI — see webui/server.py — scans once at startup,
|
|
171
|
+
then reclassifies purely in memory on every checkbox toggle). Pair
|
|
172
|
+
with classify_scanned_files().
|
|
173
|
+
|
|
174
|
+
Two things this does that a plain rglob() wouldn't:
|
|
175
|
+
|
|
176
|
+
- Prunes a fixed list of known-huge, always-irrelevant directories
|
|
177
|
+
(.venv, node_modules, .git, __pycache__, build output, etc.) during
|
|
178
|
+
the walk itself, not just at classification time. These can easily
|
|
179
|
+
be tens of thousands of files (a .venv's site-packages, in
|
|
180
|
+
particular) that would never end up in a ZIP anyway — walking into
|
|
181
|
+
them at all makes the scan needlessly slow and floods the UI's tree
|
|
182
|
+
with noise nobody wants to look at.
|
|
183
|
+
- Rejects symlinks whose real target resolves outside project_dir.
|
|
184
|
+
A virtualenv's bin/python is a classic example (symlinks to the
|
|
185
|
+
system interpreter) — without this check, relative_to() calls
|
|
186
|
+
downstream (see webui/server.py's _rel()) raise ValueError and
|
|
187
|
+
crash the request entirely.
|
|
188
|
+
|
|
189
|
+
Deliberately simpler than resolve_files()'s walk (no skipped/binary
|
|
190
|
+
bookkeeping) since callers here only need "does this file exist, and
|
|
191
|
+
how big is it" — resolve_files() remains the source of truth for an
|
|
192
|
+
actual packaging run, and still sees everything within the pruned
|
|
193
|
+
dirs is excluded via the normal rules (rules/base.py etc.) whether or
|
|
194
|
+
not the config UI ever looked at them.
|
|
195
|
+
"""
|
|
196
|
+
files: list[tuple[Path, int]] = []
|
|
197
|
+
|
|
198
|
+
for dirpath, dirnames, filenames in os.walk(project_dir, topdown=True):
|
|
199
|
+
dirnames[:] = [d for d in dirnames if d not in _ALWAYS_PRUNED_DIRNAMES]
|
|
200
|
+
|
|
201
|
+
for filename in filenames:
|
|
202
|
+
abs_path = Path(dirpath) / filename
|
|
203
|
+
|
|
204
|
+
if abs_path.is_symlink():
|
|
205
|
+
try:
|
|
206
|
+
real = abs_path.resolve(strict=True)
|
|
207
|
+
except (OSError, RuntimeError):
|
|
208
|
+
continue # dangling symlink — skip silently, this is best-effort
|
|
209
|
+
try:
|
|
210
|
+
real.relative_to(project_dir)
|
|
211
|
+
except ValueError:
|
|
212
|
+
continue # resolves outside the project tree — never include
|
|
213
|
+
if not real.is_file():
|
|
214
|
+
continue
|
|
215
|
+
abs_path = real
|
|
216
|
+
elif not abs_path.is_file():
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
size = abs_path.stat().st_size
|
|
221
|
+
except OSError:
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
files.append((abs_path, size))
|
|
225
|
+
|
|
226
|
+
return files
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def classify_scanned_files(
|
|
230
|
+
project_dir: Path,
|
|
231
|
+
files: list[tuple[Path, int]],
|
|
232
|
+
spec: pathspec.PathSpec,
|
|
233
|
+
force_include: pathspec.PathSpec | None = None,
|
|
234
|
+
) -> dict[Path, bool]:
|
|
235
|
+
"""
|
|
236
|
+
Classify each (abs_path, size) from scan_all_files() against *spec*
|
|
237
|
+
(and optional *force_include*) without touching the filesystem again.
|
|
238
|
+
|
|
239
|
+
Mirrors resolve_files()'s force_include-then-spec precedence exactly,
|
|
240
|
+
reusing the same matching primitives, but works off an already-scanned
|
|
241
|
+
file list. Returns {abs_path: is_included}.
|
|
242
|
+
"""
|
|
243
|
+
result: dict[Path, bool] = {}
|
|
244
|
+
for abs_path, _size in files:
|
|
245
|
+
try:
|
|
246
|
+
rel = abs_path.relative_to(project_dir)
|
|
247
|
+
except ValueError:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
rel_str = rel.as_posix()
|
|
251
|
+
|
|
252
|
+
forced = force_include is not None and (
|
|
253
|
+
force_include.match_file(rel_str)
|
|
254
|
+
or _any_parent_excluded(rel, force_include)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if forced:
|
|
258
|
+
result[abs_path] = True
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
excluded = _any_parent_excluded(rel, spec) or spec.match_file(rel_str)
|
|
262
|
+
result[abs_path] = not excluded
|
|
263
|
+
|
|
264
|
+
return result
|
|
265
|
+
|
|
266
|
+
|
|
125
267
|
def resolve_files(
|
|
126
268
|
project_dir: Path,
|
|
127
269
|
spec: pathspec.PathSpec,
|
|
@@ -176,7 +318,8 @@ def resolve_files(
|
|
|
176
318
|
|
|
177
319
|
# ── force_include short-circuits the exclusion checks below ─────────
|
|
178
320
|
forced = force_include is not None and (
|
|
179
|
-
force_include.match_file(rel_str)
|
|
321
|
+
force_include.match_file(rel_str)
|
|
322
|
+
or _any_parent_excluded(rel, force_include)
|
|
180
323
|
)
|
|
181
324
|
|
|
182
325
|
if not forced:
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
webui — Local-only browser UI for setting up .contextzip/config.json.
|
|
3
|
+
|
|
4
|
+
Entry point: `launch_config_ui()` in server.py, invoked by
|
|
5
|
+
`contextzip config --ui` and offered automatically on a project's first
|
|
6
|
+
run (see cli.py's _maybe_offer_config_ui). Everything here runs a plain
|
|
7
|
+
stdlib HTTP server bound to 127.0.0.1 — no new dependency, and no data
|
|
8
|
+
about the project ever leaves the machine.
|
|
9
|
+
"""
|