mirror of
https://github.com/akadeepesh/contextzip.git
synced 2026-10-20 08:36:20 +02:00
first commit
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
# contextzip
|
||||
|
||||
> Intelligently package your codebase for AI tools — no noise, just signal.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
# or once published:
|
||||
pip install contextzip
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Run from your project root
|
||||
contextzip
|
||||
|
||||
# Preview what would be included (no ZIP created)
|
||||
contextzip --dry-run
|
||||
|
||||
# See every file decision
|
||||
contextzip --dry-run --verbose
|
||||
|
||||
# Only include specific folders
|
||||
contextzip --include src app routes
|
||||
|
||||
# Add extra exclusions on top of auto-rules
|
||||
contextzip --exclude "*.log" "*.sqlite" temp.js
|
||||
|
||||
# Save ZIP to a specific path
|
||||
contextzip --output ~/Desktop/myproject.zip
|
||||
|
||||
# Create ZIP without clipboard copy
|
||||
contextzip --no-clipboard
|
||||
```
|
||||
|
||||
## What gets auto-excluded
|
||||
|
||||
### Always (base rules)
|
||||
`.git/`, `.env`, `*.log`, editor files, OS artifacts, binaries, media
|
||||
|
||||
### Node.js / Next.js
|
||||
`node_modules/`, `.next/`, `dist/`, `package-lock.json`, `yarn.lock`
|
||||
|
||||
### Python
|
||||
`__pycache__/`, `.venv/`, `.pytest_cache/`, `*.pyc`, `migrations/`
|
||||
|
||||
### Rust
|
||||
`target/`, `Cargo.lock`
|
||||
|
||||
### Go
|
||||
`vendor/`, `go.sum`
|
||||
|
||||
## Status
|
||||
|
||||
| Phase | Feature | Status |
|
||||
|-------|---------|--------|
|
||||
| 1+2 | Scaffold + Detection | ✅ Done |
|
||||
| 3 | ZIP creation | 🔜 Next |
|
||||
| 4 | Clipboard integration | 🔜 Planned |
|
||||
| 5 | Rich output polish | 🔜 Planned |
|
||||
@@ -0,0 +1,3 @@
|
||||
"""contextzip — intelligent codebase packager for AI tools."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,248 @@
|
||||
"""
|
||||
cli.py — contextzip entry point.
|
||||
|
||||
Phase 1 + 2: scaffold, detection, dry-run, and rich output.
|
||||
Packaging (Phase 3) and clipboard (Phase 4) are stubbed with clear TODOs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
from rich import box
|
||||
from rich.text import Text
|
||||
|
||||
from contextzip.detector import detect
|
||||
from contextzip.filters import build_spec, resolve_files, summarise_exclusions
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI definition
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@click.command(context_settings={"help_option_names": ["-h", "--help"]})
|
||||
@click.option(
|
||||
"--include", "-i",
|
||||
multiple=True,
|
||||
metavar="PATH",
|
||||
help="Only include files under these paths (relative to project root). "
|
||||
"Can be repeated: --include src --include app",
|
||||
)
|
||||
@click.option(
|
||||
"--exclude", "-e",
|
||||
multiple=True,
|
||||
metavar="PATTERN",
|
||||
help="Extra exclusion patterns on top of auto-rules (gitignore syntax). "
|
||||
"Can be repeated: --exclude '*.log' --exclude temp.js",
|
||||
)
|
||||
@click.option(
|
||||
"--dry-run", "-n",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Show what would be included without creating the ZIP.",
|
||||
)
|
||||
@click.option(
|
||||
"--output", "-o",
|
||||
default=None,
|
||||
metavar="FILE",
|
||||
help="Path for the output ZIP file. "
|
||||
"Defaults to <project_name>_context_<timestamp>.zip in the system temp dir.",
|
||||
)
|
||||
@click.option(
|
||||
"--no-clipboard",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Skip clipboard copy after creating the ZIP.",
|
||||
)
|
||||
@click.option(
|
||||
"--verbose", "-v",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Show every included and excluded file.",
|
||||
)
|
||||
@click.version_option(version="0.1.0", prog_name="contextzip")
|
||||
def main(
|
||||
include: tuple[str, ...],
|
||||
exclude: tuple[str, ...],
|
||||
dry_run: bool,
|
||||
output: str | None,
|
||||
no_clipboard: bool,
|
||||
verbose: bool,
|
||||
) -> None:
|
||||
"""
|
||||
\b
|
||||
contextzip — package your codebase for AI tools.
|
||||
|
||||
Run from your project root to produce a smart, lightweight ZIP
|
||||
ready to paste directly into Claude, ChatGPT, or any AI interface.
|
||||
"""
|
||||
|
||||
project_dir = Path(os.getcwd()).resolve()
|
||||
|
||||
# ── Header ──────────────────────────────────────────────────────────────
|
||||
console.print()
|
||||
console.print(
|
||||
Panel.fit(
|
||||
f"[bold cyan]contextzip[/] [dim]v0.1.0[/]\n"
|
||||
f"[dim]Project:[/] [white]{project_dir}[/]",
|
||||
border_style="cyan",
|
||||
padding=(0, 2),
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
|
||||
# ── Detection ───────────────────────────────────────────────────────────
|
||||
with console.status("[cyan]Detecting project ecosystem…[/]", spinner="dots"):
|
||||
detection = detect(project_dir)
|
||||
|
||||
_print_detection(detection)
|
||||
|
||||
# ── Build exclusion spec ────────────────────────────────────────────────
|
||||
with console.status("[cyan]Building exclusion rules…[/]", spinner="dots"):
|
||||
spec = build_spec(
|
||||
rule_modules=detection.rule_modules,
|
||||
extra_exclude=list(exclude) if exclude else None,
|
||||
)
|
||||
|
||||
# ── Resolve files ────────────────────────────────────────────────────────
|
||||
with console.status("[cyan]Scanning project files…[/]", spinner="dots"):
|
||||
included, excluded = resolve_files(
|
||||
project_dir=project_dir,
|
||||
spec=spec,
|
||||
include_only=list(include) if include else None,
|
||||
)
|
||||
|
||||
# ── Results ─────────────────────────────────────────────────────────────
|
||||
_print_results(included, excluded, project_dir, verbose)
|
||||
|
||||
if dry_run:
|
||||
console.print()
|
||||
console.print(
|
||||
Panel.fit(
|
||||
"[yellow]Dry run — no ZIP created.[/]\n"
|
||||
"[dim]Remove --dry-run to produce the archive.[/]",
|
||||
border_style="yellow",
|
||||
padding=(0, 2),
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if not included:
|
||||
console.print(
|
||||
"\n[red]Nothing to package.[/] All files were excluded — "
|
||||
"try [cyan]--include[/] to override."
|
||||
)
|
||||
return
|
||||
|
||||
# ── TODO Phase 3: Create ZIP ─────────────────────────────────────────────
|
||||
console.print()
|
||||
console.print(
|
||||
Panel.fit(
|
||||
"[bold yellow]⚙ Phase 3 coming next[/]\n"
|
||||
"[dim]ZIP creation will be wired in the next phase.[/]",
|
||||
border_style="yellow",
|
||||
padding=(0, 2),
|
||||
)
|
||||
)
|
||||
|
||||
# ── TODO Phase 4: Clipboard ──────────────────────────────────────────────
|
||||
if not no_clipboard:
|
||||
pass # clipboard logic arrives in Phase 4
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Display helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _print_detection(detection) -> None:
|
||||
"""Render a detection summary panel."""
|
||||
if detection.is_unknown:
|
||||
ecosystem_line = "[yellow]Unknown[/] — applying base rules only"
|
||||
else:
|
||||
colours = {
|
||||
"Next.js": "bright_blue",
|
||||
"Node.js": "green",
|
||||
"Python": "yellow",
|
||||
"Django": "green",
|
||||
"FastAPI": "cyan",
|
||||
"Rust": "red",
|
||||
"Go": "cyan",
|
||||
"Ruby": "red",
|
||||
}
|
||||
parts = []
|
||||
for name in detection.ecosystems:
|
||||
colour = colours.get(name, "white")
|
||||
parts.append(f"[{colour}]{name}[/]")
|
||||
ecosystem_line = " [dim]+[/] ".join(parts)
|
||||
|
||||
confidence_colour = {"high": "green", "medium": "yellow", "low": "dim"}.get(
|
||||
detection.confidence, "dim"
|
||||
)
|
||||
rules_str = ", ".join(detection.rule_modules)
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
f" [dim]Ecosystem :[/] {ecosystem_line}\n"
|
||||
f" [dim]Confidence:[/] [{confidence_colour}]{detection.confidence}[/]\n"
|
||||
f" [dim]Rules :[/] [dim]{rules_str}[/]",
|
||||
title="[bold]Detection[/]",
|
||||
border_style="blue",
|
||||
padding=(0, 1),
|
||||
)
|
||||
)
|
||||
console.print()
|
||||
|
||||
|
||||
def _print_results(
|
||||
included: list[Path],
|
||||
excluded: list[Path],
|
||||
project_dir: Path,
|
||||
verbose: bool,
|
||||
) -> None:
|
||||
"""Render file inclusion/exclusion summary."""
|
||||
|
||||
total = len(included) + len(excluded)
|
||||
included_size = sum(p.stat().st_size for p in included if p.exists())
|
||||
|
||||
# Summary table
|
||||
table = Table(box=box.ROUNDED, show_header=False, padding=(0, 2))
|
||||
table.add_column(style="dim")
|
||||
table.add_column()
|
||||
table.add_row("Files scanned", str(total))
|
||||
table.add_row(
|
||||
"To be included",
|
||||
f"[green]{len(included)}[/] [dim]({_human_size(included_size)})[/]",
|
||||
)
|
||||
table.add_row("Excluded", f"[red]{len(excluded)}[/]")
|
||||
console.print(table)
|
||||
|
||||
# Verbose: show every included file
|
||||
if verbose and included:
|
||||
console.print()
|
||||
console.print("[bold]Included files:[/]")
|
||||
for p in included:
|
||||
rel = p.relative_to(project_dir).as_posix()
|
||||
console.print(f" [green]✓[/] {rel}")
|
||||
|
||||
# Always show top excluded buckets
|
||||
if excluded:
|
||||
console.print()
|
||||
buckets = summarise_exclusions(excluded, project_dir)
|
||||
console.print("[bold]Top excluded directories / files:[/]")
|
||||
for label, count in list(buckets.items())[:8]:
|
||||
console.print(f" [red]✗[/] [dim]{label}[/] [dim]({count} file{'s' if count != 1 else ''})[/]")
|
||||
|
||||
|
||||
def _human_size(n: int) -> str:
|
||||
for unit in ("B", "KB", "MB", "GB"):
|
||||
if n < 1024:
|
||||
return f"{n:.0f} {unit}"
|
||||
n /= 1024
|
||||
return f"{n:.1f} TB"
|
||||
@@ -0,0 +1,218 @@
|
||||
"""
|
||||
detector.py — Analyses a project directory and identifies the ecosystem(s) present.
|
||||
|
||||
Detection is additive: base rules always apply, and each detected framework
|
||||
stacks its own rules on top. A monorepo can match multiple ecosystems at once.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Data model
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class DetectionResult:
|
||||
"""Everything the detector knows about a project directory."""
|
||||
|
||||
ecosystems: list[str] = field(default_factory=list) # e.g. ["Node.js", "Next.js"]
|
||||
rule_modules: list[str] = field(default_factory=list) # e.g. ["base", "node"]
|
||||
confidence: str = "low" # "low" | "medium" | "high"
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
if not self.ecosystems:
|
||||
return "Unknown"
|
||||
return " + ".join(self.ecosystems)
|
||||
|
||||
@property
|
||||
def is_unknown(self) -> bool:
|
||||
return not self.ecosystems
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Detection rules — ordered from most specific to least specific
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class _Rule:
|
||||
"""A single detection rule."""
|
||||
name: str # Human-readable ecosystem name e.g. "Next.js"
|
||||
module: str # Rule module key e.g. "node"
|
||||
check: Callable[[Path], bool]
|
||||
weight: int = 1 # Higher = stronger signal
|
||||
|
||||
|
||||
_RULES: list[_Rule] = [
|
||||
# ── Next.js (must come before generic Node.js) ────────────────────────
|
||||
_Rule(
|
||||
name="Next.js",
|
||||
module="node",
|
||||
check=lambda p: (
|
||||
_file_exists(p, "next.config.js")
|
||||
or _file_exists(p, "next.config.ts")
|
||||
or _file_exists(p, "next.config.mjs")
|
||||
or _dep_present(p, "next")
|
||||
),
|
||||
weight=3,
|
||||
),
|
||||
|
||||
# ── Node.js / generic JS+TS ───────────────────────────────────────────
|
||||
_Rule(
|
||||
name="Node.js",
|
||||
module="node",
|
||||
check=lambda p: _file_exists(p, "package.json"),
|
||||
weight=2,
|
||||
),
|
||||
|
||||
# ── Django (must come before generic Python) ──────────────────────────
|
||||
_Rule(
|
||||
name="Django",
|
||||
module="python",
|
||||
check=lambda p: _file_exists(p, "manage.py") or _dep_present_py(p, "django"),
|
||||
weight=3,
|
||||
),
|
||||
|
||||
# ── FastAPI ───────────────────────────────────────────────────────────
|
||||
_Rule(
|
||||
name="FastAPI",
|
||||
module="python",
|
||||
check=lambda p: _dep_present_py(p, "fastapi"),
|
||||
weight=3,
|
||||
),
|
||||
|
||||
# ── Generic Python ────────────────────────────────────────────────────
|
||||
_Rule(
|
||||
name="Python",
|
||||
module="python",
|
||||
check=lambda p: (
|
||||
_file_exists(p, "requirements.txt")
|
||||
or _file_exists(p, "pyproject.toml")
|
||||
or _file_exists(p, "setup.py")
|
||||
or _file_exists(p, "setup.cfg")
|
||||
or _file_exists(p, "Pipfile")
|
||||
),
|
||||
weight=2,
|
||||
),
|
||||
|
||||
# ── Rust ──────────────────────────────────────────────────────────────
|
||||
_Rule(
|
||||
name="Rust",
|
||||
module="rust",
|
||||
check=lambda p: _file_exists(p, "Cargo.toml"),
|
||||
weight=3,
|
||||
),
|
||||
|
||||
# ── Go ────────────────────────────────────────────────────────────────
|
||||
_Rule(
|
||||
name="Go",
|
||||
module="go",
|
||||
check=lambda p: _file_exists(p, "go.mod"),
|
||||
weight=3,
|
||||
),
|
||||
|
||||
# ── Ruby ─────────────────────────────────────────────────────────────
|
||||
_Rule(
|
||||
name="Ruby",
|
||||
module="base", # no dedicated rule module yet; base covers it
|
||||
check=lambda p: _file_exists(p, "Gemfile"),
|
||||
weight=2,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def detect(project_dir: Path) -> DetectionResult:
|
||||
"""
|
||||
Analyse *project_dir* and return a :class:`DetectionResult`.
|
||||
|
||||
Always includes "base" in rule_modules.
|
||||
"""
|
||||
result = DetectionResult(rule_modules=["base"])
|
||||
|
||||
seen_modules: set[str] = {"base"}
|
||||
seen_names: set[str] = set()
|
||||
total_weight = 0
|
||||
|
||||
for rule in _RULES:
|
||||
try:
|
||||
matched = rule.check(project_dir)
|
||||
except Exception:
|
||||
matched = False
|
||||
|
||||
if matched:
|
||||
if rule.name not in seen_names:
|
||||
result.ecosystems.append(rule.name)
|
||||
seen_names.add(rule.name)
|
||||
if rule.module not in seen_modules:
|
||||
result.rule_modules.append(rule.module)
|
||||
seen_modules.add(rule.module)
|
||||
total_weight += rule.weight
|
||||
|
||||
# Confidence based on cumulative signal weight
|
||||
if total_weight >= 4:
|
||||
result.confidence = "high"
|
||||
elif total_weight >= 2:
|
||||
result.confidence = "medium"
|
||||
elif total_weight >= 1:
|
||||
result.confidence = "low"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _file_exists(project_dir: Path, filename: str) -> bool:
|
||||
return (project_dir / filename).exists()
|
||||
|
||||
|
||||
def _dep_present(project_dir: Path, dep: str) -> bool:
|
||||
"""Check if *dep* appears in package.json dependencies."""
|
||||
pkg = project_dir / "package.json"
|
||||
if not pkg.exists():
|
||||
return False
|
||||
try:
|
||||
import json
|
||||
data = json.loads(pkg.read_text(encoding="utf-8", errors="ignore"))
|
||||
all_deps = {
|
||||
**data.get("dependencies", {}),
|
||||
**data.get("devDependencies", {}),
|
||||
**data.get("peerDependencies", {}),
|
||||
}
|
||||
return dep in all_deps
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _dep_present_py(project_dir: Path, dep: str) -> bool:
|
||||
"""Check if *dep* appears in requirements.txt or pyproject.toml."""
|
||||
req = project_dir / "requirements.txt"
|
||||
if req.exists():
|
||||
try:
|
||||
content = req.read_text(encoding="utf-8", errors="ignore").lower()
|
||||
if dep.lower() in content:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ppt = project_dir / "pyproject.toml"
|
||||
if ppt.exists():
|
||||
try:
|
||||
content = ppt.read_text(encoding="utf-8", errors="ignore").lower()
|
||||
if dep.lower() in content:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return False
|
||||
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
filters.py — Loads exclusion patterns from rule modules and resolves
|
||||
which files in a project directory should be included or excluded.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
|
||||
import pathspec
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rule registry — maps module key → importable module path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_RULE_REGISTRY: dict[str, str] = {
|
||||
"base": "contextzip.rules.base",
|
||||
"node": "contextzip.rules.node",
|
||||
"python": "contextzip.rules.python",
|
||||
"rust": "contextzip.rules.rust",
|
||||
"go": "contextzip.rules.go",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def build_spec(
|
||||
rule_modules: list[str],
|
||||
extra_exclude: list[str] | None = None,
|
||||
) -> pathspec.PathSpec:
|
||||
"""
|
||||
Combine patterns from the given *rule_modules* (plus optional
|
||||
*extra_exclude* patterns) into a single :class:`pathspec.PathSpec`.
|
||||
|
||||
The spec can then be used to test whether a path should be excluded.
|
||||
"""
|
||||
patterns: list[str] = []
|
||||
|
||||
for key in rule_modules:
|
||||
module_path = _RULE_REGISTRY.get(key)
|
||||
if module_path is None:
|
||||
continue
|
||||
try:
|
||||
mod = importlib.import_module(module_path)
|
||||
patterns.extend(getattr(mod, "PATTERNS", []))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if extra_exclude:
|
||||
patterns.extend(extra_exclude)
|
||||
|
||||
return pathspec.PathSpec.from_lines("gitwildmatch", patterns)
|
||||
|
||||
|
||||
def resolve_files(
|
||||
project_dir: Path,
|
||||
spec: pathspec.PathSpec,
|
||||
include_only: list[str] | None = None,
|
||||
) -> tuple[list[Path], list[Path]]:
|
||||
"""
|
||||
Walk *project_dir* and split every file into included / excluded lists.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
project_dir:
|
||||
Root of the project to scan.
|
||||
spec:
|
||||
The exclusion spec built by :func:`build_spec`.
|
||||
include_only:
|
||||
If provided, only files whose relative path starts with one of
|
||||
these prefixes will be considered (after exclusions are applied).
|
||||
|
||||
Returns
|
||||
-------
|
||||
(included, excluded)
|
||||
Both lists contain absolute :class:`Path` objects.
|
||||
"""
|
||||
included: list[Path] = []
|
||||
excluded: list[Path] = []
|
||||
|
||||
for abs_path in sorted(project_dir.rglob("*")):
|
||||
if not abs_path.is_file():
|
||||
continue
|
||||
|
||||
rel = abs_path.relative_to(project_dir)
|
||||
rel_str = rel.as_posix()
|
||||
|
||||
# Check directory-level exclusion — match any path component
|
||||
if _any_parent_excluded(rel, spec):
|
||||
excluded.append(abs_path)
|
||||
continue
|
||||
|
||||
# Check file-level exclusion
|
||||
if spec.match_file(rel_str):
|
||||
excluded.append(abs_path)
|
||||
continue
|
||||
|
||||
# Apply --include filter if set
|
||||
if include_only:
|
||||
if not any(rel_str.startswith(prefix.rstrip("/")) for prefix in include_only):
|
||||
excluded.append(abs_path)
|
||||
continue
|
||||
|
||||
included.append(abs_path)
|
||||
|
||||
return included, excluded
|
||||
|
||||
|
||||
def summarise_exclusions(excluded: list[Path], project_dir: Path) -> dict[str, int]:
|
||||
"""
|
||||
Group excluded files by their top-level directory / filename for display.
|
||||
Returns a dict of { label: count } sorted by count descending.
|
||||
"""
|
||||
buckets: dict[str, int] = {}
|
||||
for p in excluded:
|
||||
rel = p.relative_to(project_dir)
|
||||
top = rel.parts[0] if rel.parts else str(rel)
|
||||
buckets[top] = buckets.get(top, 0) + 1
|
||||
return dict(sorted(buckets.items(), key=lambda x: x[1], reverse=True))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _any_parent_excluded(rel: Path, spec: pathspec.PathSpec) -> bool:
|
||||
"""
|
||||
Return True if any *directory* component of *rel* is matched by *spec*.
|
||||
This catches patterns like ``node_modules/`` that should exclude the
|
||||
entire subtree, not just the top-level directory entry.
|
||||
"""
|
||||
parts = rel.parts[:-1] # all directory components, no filename
|
||||
for i in range(len(parts)):
|
||||
dir_path = "/".join(parts[: i + 1]) + "/"
|
||||
if spec.match_file(dir_path):
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Universal exclusion rules applied to every project regardless of language.
|
||||
"""
|
||||
|
||||
PATTERNS = [
|
||||
# Version control
|
||||
".git/",
|
||||
".gitignore",
|
||||
".svn/",
|
||||
".hg/",
|
||||
|
||||
# OS artifacts
|
||||
".DS_Store",
|
||||
"Thumbs.db",
|
||||
"desktop.ini",
|
||||
|
||||
# Editor & IDE
|
||||
".vscode/",
|
||||
".idea/",
|
||||
"*.swp",
|
||||
"*.swo",
|
||||
"*.swn",
|
||||
".vim/",
|
||||
|
||||
# Logs
|
||||
"*.log",
|
||||
"logs/",
|
||||
"log/",
|
||||
|
||||
# Temp & cache
|
||||
".cache/",
|
||||
"tmp/",
|
||||
"temp/",
|
||||
"*.tmp",
|
||||
"*.bak",
|
||||
"*.orig",
|
||||
|
||||
# Secrets & env
|
||||
".env",
|
||||
".env.*",
|
||||
"*.pem",
|
||||
"*.key",
|
||||
"*.p12",
|
||||
"*.pfx",
|
||||
|
||||
# Binaries & media (rarely useful for AI context)
|
||||
"*.exe",
|
||||
"*.dll",
|
||||
"*.so",
|
||||
"*.dylib",
|
||||
"*.bin",
|
||||
"*.png",
|
||||
"*.jpg",
|
||||
"*.jpeg",
|
||||
"*.gif",
|
||||
"*.svg",
|
||||
"*.ico",
|
||||
"*.mp4",
|
||||
"*.mp3",
|
||||
"*.zip",
|
||||
"*.tar",
|
||||
"*.gz",
|
||||
"*.rar",
|
||||
|
||||
# The output zip itself (avoid recursion)
|
||||
"*_context_*.zip",
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Go module exclusion rules.
|
||||
"""
|
||||
|
||||
PATTERNS = [
|
||||
"vendor/",
|
||||
"*.test",
|
||||
"go.sum",
|
||||
"bin/",
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Node.js, Next.js, and general JS/TS framework exclusion rules.
|
||||
"""
|
||||
|
||||
PATTERNS = [
|
||||
"node_modules/",
|
||||
".next/",
|
||||
".nuxt/",
|
||||
"dist/",
|
||||
"build/",
|
||||
"out/",
|
||||
".turbo/",
|
||||
".vercel/",
|
||||
".netlify/",
|
||||
"coverage/",
|
||||
".nyc_output/",
|
||||
"*.min.js",
|
||||
"*.min.css",
|
||||
"package-lock.json",
|
||||
"yarn.lock",
|
||||
"pnpm-lock.yaml",
|
||||
".pnp.js",
|
||||
".pnp.cjs",
|
||||
".yarn/",
|
||||
"storybook-static/",
|
||||
".storybook/",
|
||||
"*.d.ts", # generated type declarations
|
||||
"tsconfig.tsbuildinfo",
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Python, Django, FastAPI, and general Python project exclusion rules.
|
||||
"""
|
||||
|
||||
PATTERNS = [
|
||||
"__pycache__/",
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*.pyd",
|
||||
".Python",
|
||||
".venv/",
|
||||
"venv/",
|
||||
"env/",
|
||||
".env/",
|
||||
"ENV/",
|
||||
"*.egg-info/",
|
||||
"*.egg",
|
||||
"dist/",
|
||||
"build/",
|
||||
".pytest_cache/",
|
||||
".mypy_cache/",
|
||||
".ruff_cache/",
|
||||
".hypothesis/",
|
||||
"htmlcov/",
|
||||
"coverage.xml",
|
||||
".coverage",
|
||||
"*.sqlite3",
|
||||
"*.db",
|
||||
"site/", # mkdocs build output
|
||||
".tox/",
|
||||
"poetry.lock",
|
||||
"uv.lock",
|
||||
"migrations/", # Django migrations (auto-generated)
|
||||
"staticfiles/", # Django collected static
|
||||
"media/", # Django uploaded media
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Rust (Cargo) exclusion rules.
|
||||
"""
|
||||
|
||||
PATTERNS = [
|
||||
"target/",
|
||||
"Cargo.lock", # exclude for libraries; keep for binaries — we exclude for AI context
|
||||
"*.rlib",
|
||||
"*.rmeta",
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=68", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "contextzip"
|
||||
version = "0.1.0"
|
||||
description = "Intelligently package your codebase for AI tools"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"click>=8.1",
|
||||
"rich>=13.0",
|
||||
"pathspec>=0.12",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
contextzip = "contextzip.cli:main"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["contextzip*"]
|
||||
Reference in New Issue
Block a user