RepoPackPy 0.1.1__tar.gz → 0.1.8__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.
@@ -1,8 +1,9 @@
1
1
  name: Publish to PyPI
2
2
 
3
3
  on:
4
- release:
5
- types: [published]
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
6
7
 
7
8
  jobs:
8
9
  verify-tag:
@@ -23,11 +23,13 @@ jobs:
23
23
 
24
24
  - name: Install project and audit tools
25
25
  run: |
26
- pip install -e ".[dev]"
26
+ pip install ".[dev]"
27
27
  pip install pip-audit bandit
28
28
 
29
29
  - name: pip-audit (known CVEs)
30
- run: pip-audit --strict
30
+ run: |
31
+ pip freeze | grep -Eiv "^(repopackpy|repopack-py|-e )" > /tmp/audit-reqs.txt
32
+ pip-audit --strict -r /tmp/audit-reqs.txt
31
33
 
32
34
  - name: bandit (SAST)
33
35
  run: bandit -r repopack/ -ll -ii
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: RepoPackPy
3
- Version: 0.1.1
3
+ Version: 0.1.8
4
4
  Summary: Pack/unpack any source workspace into portable JSON, with MCP server support.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -27,18 +27,45 @@ Pack/unpack **any source workspace regardless of technology stack** (Python, Nod
27
27
 
28
28
  ## Installation
29
29
 
30
+ ### From PyPI (recommended)
31
+
32
+ Install the latest stable release:
33
+
30
34
  ```bash
31
35
  pip install RepoPackPy
32
36
  ```
33
37
 
34
- For development:
38
+ Pin to a specific version:
35
39
 
36
40
  ```bash
37
- pip install -e ".[dev]"
41
+ pip install RepoPackPy==0.1.1
38
42
  ```
39
43
 
40
44
  Requires Python 3.10+.
41
45
 
46
+ ### Quick start after install
47
+
48
+ ```bash
49
+ # Verify the install
50
+ repopack --help
51
+
52
+ # Pack your project
53
+ repopack pack . -o my-workspace.json
54
+
55
+ # Restore it somewhere else
56
+ repopack unpack my-workspace.json -t ./restored
57
+ ```
58
+
59
+ ### For development
60
+
61
+ Clone the repo and install in editable mode with test dependencies:
62
+
63
+ ```bash
64
+ git clone https://github.com/ShanKonduru/RepoPack.git
65
+ cd RepoPack
66
+ pip install -e ".[dev]"
67
+ ```
68
+
42
69
  ## CLI Usage
43
70
 
44
71
  ```bash
@@ -13,18 +13,45 @@ Pack/unpack **any source workspace regardless of technology stack** (Python, Nod
13
13
 
14
14
  ## Installation
15
15
 
16
+ ### From PyPI (recommended)
17
+
18
+ Install the latest stable release:
19
+
16
20
  ```bash
17
21
  pip install RepoPackPy
18
22
  ```
19
23
 
20
- For development:
24
+ Pin to a specific version:
21
25
 
22
26
  ```bash
23
- pip install -e ".[dev]"
27
+ pip install RepoPackPy==0.1.1
24
28
  ```
25
29
 
26
30
  Requires Python 3.10+.
27
31
 
32
+ ### Quick start after install
33
+
34
+ ```bash
35
+ # Verify the install
36
+ repopack --help
37
+
38
+ # Pack your project
39
+ repopack pack . -o my-workspace.json
40
+
41
+ # Restore it somewhere else
42
+ repopack unpack my-workspace.json -t ./restored
43
+ ```
44
+
45
+ ### For development
46
+
47
+ Clone the repo and install in editable mode with test dependencies:
48
+
49
+ ```bash
50
+ git clone https://github.com/ShanKonduru/RepoPack.git
51
+ cd RepoPack
52
+ pip install -e ".[dev]"
53
+ ```
54
+
28
55
  ## CLI Usage
29
56
 
30
57
  ```bash
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "RepoPackPy"
7
- version = "0.1.1"
7
+ version = "0.1.8"
8
8
  description = "Pack/unpack any source workspace into portable JSON, with MCP server support."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1 @@
1
+ __version__ = "0.1.8"
@@ -0,0 +1,119 @@
1
+ import json
2
+ import http.client
3
+ from typing import Optional
4
+ import typer
5
+ from pathlib import Path
6
+
7
+ from repopack import __version__
8
+ from repopack.packer import pack_workspace
9
+ from repopack.unpacker import unpack_workspace
10
+ from repopack.mcp_server import run_server
11
+
12
+ _PYPI_HOST = "pypi.org"
13
+ _PYPI_PATH = "/pypi/RepoPackPy/json"
14
+
15
+
16
+ def _check_latest_version() -> Optional[str]:
17
+ try:
18
+ conn = http.client.HTTPSConnection(_PYPI_HOST, timeout=3)
19
+ conn.request("GET", _PYPI_PATH)
20
+ resp = conn.getresponse()
21
+ if resp.status == 200:
22
+ data = json.loads(resp.read().decode())
23
+ return data["info"]["version"]
24
+ return None
25
+ except Exception:
26
+ return None
27
+
28
+
29
+ def _version_callback(value: bool) -> None:
30
+ if value:
31
+ typer.echo(f"repopack {__version__}")
32
+ latest = _check_latest_version()
33
+ if latest and latest != __version__:
34
+ typer.echo(
35
+ f"A new version is available: {latest} "
36
+ f"(run: pip install --upgrade RepoPackPy)",
37
+ err=True,
38
+ )
39
+ raise typer.Exit()
40
+
41
+
42
+ app = typer.Typer(name="repopack", help="Pack/unpack any workspace into portable JSON.")
43
+
44
+
45
+ @app.callback()
46
+ def main_callback(
47
+ version: Optional[bool] = typer.Option(
48
+ None, "--version", "-V",
49
+ callback=_version_callback,
50
+ is_eager=True,
51
+ help="Show version and exit.",
52
+ ),
53
+ ) -> None:
54
+ pass
55
+
56
+
57
+ @app.command()
58
+ def pack(
59
+ directory: str = typer.Argument(".", help="The workspace to pack."),
60
+ output: str = typer.Option(None, "-o", "--output", help="Output JSON file path."),
61
+ include_binary: bool = typer.Option(False, "--include-binary", help="Include binary files."),
62
+ custom_ignore: str = typer.Option(None, "--custom-ignore", help="Comma-separated extra ignore patterns."),
63
+ dry_run: bool = typer.Option(False, "--dry-run", help="Report files that would be packed without writing output."),
64
+ ):
65
+ try:
66
+ result = pack_workspace(Path(directory), include_binary, custom_ignore, dry_run=dry_run)
67
+ if dry_run:
68
+ typer.echo(f"Dry run — would pack {result['total_files']} files ({result['total_bytes']} bytes)")
69
+ typer.echo(f"Root : {result['root_directory']}")
70
+ typer.echo(f"Stack : {', '.join(result['detected_stack']) or 'unknown'}")
71
+ for f in result["files"]:
72
+ typer.echo(f" {f['encoding']:6} {f['size']:>10} {f['path']}")
73
+ return
74
+ if output:
75
+ with open(output, "w", encoding="utf-8") as f:
76
+ json.dump(result, f, indent=2)
77
+ total_files = result.get("metadata", {}).get("total_files", 0)
78
+ total_bytes = result.get("metadata", {}).get("total_bytes", 0)
79
+ typer.echo(f"Packed {total_files} files ({total_bytes} bytes) -> {output}", err=True)
80
+ else:
81
+ print(json.dumps(result, indent=2))
82
+ except Exception as e:
83
+ typer.echo(str(e), err=True)
84
+ raise typer.Exit(1)
85
+
86
+
87
+ @app.command()
88
+ def unpack(
89
+ input: str = typer.Argument(..., help="File path or raw JSON string to unpack."),
90
+ target: str = typer.Option(".", "-t", "--target", help="Output directory."),
91
+ force: bool = typer.Option(False, "--force/--no-force", help="Overwrite existing files."),
92
+ dry_run: bool = typer.Option(False, "--dry-run", help="Report files that would be unpacked without writing anything."),
93
+ ):
94
+ try:
95
+ result = unpack_workspace(input, Path(target), overwrite=force, dry_run=dry_run)
96
+ if dry_run:
97
+ typer.echo(f"Dry run — destination: {result['destination']}")
98
+ typer.echo(f" Would extract : {result['would_extract']}")
99
+ typer.echo(f" Would skip : {result['would_skip']}")
100
+ for f in result["files"]:
101
+ typer.echo(f" {f['action']:20} {f['path']}")
102
+ return
103
+ extracted = result.get("extracted", 0)
104
+ destination = result.get("destination", target)
105
+ skipped = result.get("skipped", 0)
106
+ typer.echo(f"Extracted {extracted} files to {destination} ({skipped} skipped)")
107
+ except Exception as e:
108
+ typer.echo(str(e), err=True)
109
+ raise typer.Exit(1)
110
+
111
+
112
+ @app.command()
113
+ def serve():
114
+ typer.echo("Starting RepoPack MCP server (stdio)...", err=True)
115
+ run_server()
116
+
117
+
118
+ def main():
119
+ app()
@@ -14,8 +14,16 @@ def export_workspace(
14
14
  workspace_path: str = ".",
15
15
  output_json_path: str | None = None,
16
16
  include_binary: bool = False,
17
+ dry_run: bool = False,
17
18
  ) -> str:
18
- result = pack_workspace(Path(workspace_path), include_binary=include_binary)
19
+ result = pack_workspace(Path(workspace_path), include_binary=include_binary, dry_run=dry_run)
20
+ if dry_run:
21
+ lines = [
22
+ f"Dry run — would pack {result['total_files']} files ({result['total_bytes']} bytes)",
23
+ f"Root : {result['root_directory']}",
24
+ f"Stack : {', '.join(result['detected_stack']) or 'unknown'}",
25
+ ] + [f" {f['encoding']:6} {f['size']:>10} {f['path']}" for f in result["files"]]
26
+ return "\n".join(lines)
19
27
  if output_json_path is not None:
20
28
  with open(output_json_path, "w", encoding="utf-8") as f:
21
29
  json.dump(result, f, indent=2)
@@ -28,8 +36,16 @@ def import_workspace(
28
36
  json_input: str,
29
37
  destination_path: str,
30
38
  overwrite: bool = False,
39
+ dry_run: bool = False,
31
40
  ) -> str:
32
- summary = unpack_workspace(json_input, Path(destination_path), overwrite=overwrite)
41
+ summary = unpack_workspace(json_input, Path(destination_path), overwrite=overwrite, dry_run=dry_run)
42
+ if dry_run:
43
+ lines = [
44
+ f"Dry run — destination: {summary['destination']}",
45
+ f" Would extract : {summary['would_extract']}",
46
+ f" Would skip : {summary['would_skip']}",
47
+ ] + [f" {f['action']:20} {f['path']}" for f in summary["files"]]
48
+ return "\n".join(lines)
33
49
  return f"Extracted {summary['extracted']} files to {summary['destination']} ({summary['skipped']} skipped)"
34
50
 
35
51
 
@@ -13,6 +13,7 @@ def pack_workspace(
13
13
  directory: Path,
14
14
  include_binary: bool = False,
15
15
  custom_ignore: str | None = None,
16
+ dry_run: bool = False,
16
17
  ) -> dict:
17
18
  directory = directory.resolve()
18
19
 
@@ -58,21 +59,25 @@ def pack_workspace(
58
59
  if binary and not include_binary:
59
60
  continue
60
61
 
62
+ encoding = "base64" if binary else "utf-8"
63
+ total_bytes += file_size
64
+
65
+ if dry_run:
66
+ files.append({"path": rel_path.as_posix(), "encoding": encoding, "size": file_size})
67
+ continue
68
+
61
69
  if binary:
62
70
  try:
63
71
  raw = file_path.read_bytes()
64
72
  except OSError:
65
73
  continue
66
74
  content = base64.b64encode(raw).decode("ascii")
67
- encoding = "base64"
68
75
  else:
69
76
  try:
70
77
  content = file_path.read_text(encoding="utf-8", errors="replace")
71
78
  except OSError:
72
79
  continue
73
- encoding = "utf-8"
74
80
 
75
- total_bytes += file_size
76
81
  files.append(
77
82
  {
78
83
  "path": rel_path.as_posix(),
@@ -83,6 +88,16 @@ def pack_workspace(
83
88
 
84
89
  detected_stack = detect_stack(directory)
85
90
 
91
+ if dry_run:
92
+ return {
93
+ "dry_run": True,
94
+ "root_directory": str(directory),
95
+ "detected_stack": detected_stack,
96
+ "total_files": len(files),
97
+ "total_bytes": total_bytes,
98
+ "files": files,
99
+ }
100
+
86
101
  return {
87
102
  "version": "1.0",
88
103
  "metadata": {
@@ -11,6 +11,7 @@ def unpack_workspace(
11
11
  destination_path: Path,
12
12
  overwrite: bool = False,
13
13
  force: bool = False,
14
+ dry_run: bool = False,
14
15
  ) -> dict:
15
16
  if Path(json_input).is_file():
16
17
  with open(json_input, "r", encoding="utf-8") as f:
@@ -25,6 +26,7 @@ def unpack_workspace(
25
26
 
26
27
  extracted = 0
27
28
  skipped = 0
29
+ dry_run_files: list[dict] = []
28
30
 
29
31
  for entry in data["files"]:
30
32
  rel = entry.get("path", "")
@@ -40,8 +42,20 @@ def unpack_workspace(
40
42
  raise ValueError(f"Path traversal detected: {rel}")
41
43
 
42
44
  output_path = destination_path / rel_path
45
+ would_skip = output_path.exists() and not overwrite
46
+
47
+ if dry_run:
48
+ dry_run_files.append({
49
+ "path": rel,
50
+ "action": "skip (exists)" if would_skip else "create",
51
+ })
52
+ if would_skip:
53
+ skipped += 1
54
+ else:
55
+ extracted += 1
56
+ continue
43
57
 
44
- if output_path.exists() and not overwrite:
58
+ if would_skip:
45
59
  skipped += 1
46
60
  continue
47
61
 
@@ -57,4 +71,13 @@ def unpack_workspace(
57
71
 
58
72
  extracted += 1
59
73
 
74
+ if dry_run:
75
+ return {
76
+ "dry_run": True,
77
+ "destination": str(destination_path),
78
+ "would_extract": extracted,
79
+ "would_skip": skipped,
80
+ "files": dry_run_files,
81
+ }
82
+
60
83
  return {"extracted": extracted, "skipped": skipped, "destination": str(destination_path)}
@@ -1 +0,0 @@
1
- __version__ = "0.1.1"
@@ -1,58 +0,0 @@
1
- import json
2
- import typer
3
- from pathlib import Path
4
-
5
- from repopack.packer import pack_workspace
6
- from repopack.unpacker import unpack_workspace
7
- from repopack.mcp_server import run_server
8
-
9
- app = typer.Typer(name="repopack", help="Pack/unpack any workspace into portable JSON.")
10
-
11
-
12
- @app.command()
13
- def pack(
14
- directory: str = typer.Argument(".", help="The workspace to pack."),
15
- output: str = typer.Option(None, "-o", "--output", help="Output JSON file path."),
16
- include_binary: bool = typer.Option(False, "--include-binary", help="Include binary files."),
17
- custom_ignore: str = typer.Option(None, "--custom-ignore", help="Comma-separated extra ignore patterns."),
18
- ):
19
- try:
20
- result = pack_workspace(Path(directory), include_binary, custom_ignore)
21
- if output:
22
- with open(output, "w", encoding="utf-8") as f:
23
- json.dump(result, f, indent=2)
24
- total_files = result.get("total_files", 0)
25
- total_bytes = result.get("total_bytes", 0)
26
- typer.echo(f"Packed {total_files} files ({total_bytes} bytes) → {output}", err=True)
27
- else:
28
- print(json.dumps(result, indent=2))
29
- except Exception as e:
30
- typer.echo(str(e), err=True)
31
- raise typer.Exit(1)
32
-
33
-
34
- @app.command()
35
- def unpack(
36
- input: str = typer.Argument(..., help="File path or raw JSON string to unpack."),
37
- target: str = typer.Option(".", "-t", "--target", help="Output directory."),
38
- force: bool = typer.Option(False, "--force/--no-force", help="Overwrite existing files."),
39
- ):
40
- try:
41
- result = unpack_workspace(input, Path(target), overwrite=force)
42
- extracted = result.get("extracted", 0)
43
- destination = result.get("destination", target)
44
- skipped = result.get("skipped", 0)
45
- typer.echo(f"Extracted {extracted} files to {destination} ({skipped} skipped)")
46
- except Exception as e:
47
- typer.echo(str(e), err=True)
48
- raise typer.Exit(1)
49
-
50
-
51
- @app.command()
52
- def serve():
53
- typer.echo("Starting RepoPack MCP server (stdio)...", err=True)
54
- run_server()
55
-
56
-
57
- def main():
58
- app()
File without changes
File without changes
File without changes
File without changes
File without changes