gwc-pybundle 0.4.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.
- gwc_pybundle-0.4.2.dist-info/METADATA +476 -0
- gwc_pybundle-0.4.2.dist-info/RECORD +34 -0
- gwc_pybundle-0.4.2.dist-info/WHEEL +5 -0
- gwc_pybundle-0.4.2.dist-info/entry_points.txt +2 -0
- gwc_pybundle-0.4.2.dist-info/licenses/LICENSE.md +25 -0
- gwc_pybundle-0.4.2.dist-info/top_level.txt +1 -0
- pybundle/__init__.py +0 -0
- pybundle/__main__.py +4 -0
- pybundle/cli.py +228 -0
- pybundle/context.py +232 -0
- pybundle/doctor.py +101 -0
- pybundle/manifest.py +78 -0
- pybundle/packaging.py +41 -0
- pybundle/policy.py +176 -0
- pybundle/profiles.py +146 -0
- pybundle/roadmap_model.py +38 -0
- pybundle/roadmap_scan.py +262 -0
- pybundle/root_detect.py +14 -0
- pybundle/runner.py +72 -0
- pybundle/steps/base.py +20 -0
- pybundle/steps/compileall.py +76 -0
- pybundle/steps/context_expand.py +272 -0
- pybundle/steps/copy_pack.py +300 -0
- pybundle/steps/error_refs.py +204 -0
- pybundle/steps/handoff_md.py +166 -0
- pybundle/steps/mypy.py +60 -0
- pybundle/steps/pytest.py +66 -0
- pybundle/steps/repro_md.py +161 -0
- pybundle/steps/rg_scans.py +78 -0
- pybundle/steps/roadmap.py +158 -0
- pybundle/steps/ruff.py +111 -0
- pybundle/steps/shell.py +67 -0
- pybundle/steps/tree.py +136 -0
- pybundle/tools.py +7 -0
pybundle/steps/shell.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .base import StepResult
|
|
9
|
+
from ..context import BundleContext
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ShellStep:
|
|
14
|
+
name: str
|
|
15
|
+
outfile_rel: str
|
|
16
|
+
cmd: list[str]
|
|
17
|
+
cwd_is_root: bool = True
|
|
18
|
+
allow_fail: bool = True
|
|
19
|
+
require_cmd: str | None = None
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def out_rel(self) -> str:
|
|
23
|
+
return self.outfile_rel
|
|
24
|
+
|
|
25
|
+
def run(self, ctx: BundleContext) -> StepResult:
|
|
26
|
+
if self.require_cmd and not getattr(ctx.tools, self.require_cmd, None):
|
|
27
|
+
out = ctx.workdir / self.outfile_rel
|
|
28
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
out.write_text(
|
|
30
|
+
f"{self.require_cmd} not found; skipping\n", encoding="utf-8"
|
|
31
|
+
)
|
|
32
|
+
return StepResult(self.name, "SKIP", 0, f"missing {self.require_cmd}")
|
|
33
|
+
|
|
34
|
+
out = ctx.workdir / self.outfile_rel
|
|
35
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
|
|
37
|
+
start = time.time()
|
|
38
|
+
header = (
|
|
39
|
+
f"## PWD: {ctx.root if self.cwd_is_root else Path.cwd()}\n"
|
|
40
|
+
f"## CMD: {' '.join(self.cmd)}\n\n"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
cp = subprocess.run(
|
|
45
|
+
self.cmd,
|
|
46
|
+
cwd=str(ctx.root) if self.cwd_is_root else None,
|
|
47
|
+
text=True,
|
|
48
|
+
capture_output=True,
|
|
49
|
+
check=False,
|
|
50
|
+
)
|
|
51
|
+
text = header + (cp.stdout or "") + ("\n" + cp.stderr if cp.stderr else "")
|
|
52
|
+
out.write_text(ctx.redact_text(text), encoding="utf-8")
|
|
53
|
+
status = (
|
|
54
|
+
"PASS"
|
|
55
|
+
if cp.returncode == 0
|
|
56
|
+
else ("FAIL" if not self.allow_fail else "PASS")
|
|
57
|
+
)
|
|
58
|
+
note = "" if cp.returncode == 0 else f"exit={cp.returncode}"
|
|
59
|
+
except Exception as e:
|
|
60
|
+
out.write_text(
|
|
61
|
+
ctx.redact_text(header + f"\nEXCEPTION: {e}\n"), encoding="utf-8"
|
|
62
|
+
)
|
|
63
|
+
status = "FAIL" if not self.allow_fail else "PASS"
|
|
64
|
+
note = str(e)
|
|
65
|
+
|
|
66
|
+
dur = int(time.time() - start)
|
|
67
|
+
return StepResult(self.name, status, dur, note)
|
pybundle/steps/tree.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .base import StepResult
|
|
9
|
+
from pybundle.context import BundleContext
|
|
10
|
+
from pybundle.tools import which
|
|
11
|
+
from pybundle.policy import AIContextPolicy, PathFilter
|
|
12
|
+
|
|
13
|
+
BIN_EXTS = {
|
|
14
|
+
".appimage", ".deb", ".rpm", ".exe", ".msi", ".dmg", ".pkg",
|
|
15
|
+
".so", ".dll", ".dylib",
|
|
16
|
+
}
|
|
17
|
+
DB_EXTS = {".db", ".sqlite", ".sqlite3"}
|
|
18
|
+
ARCHIVE_EXTS = {".zip", ".tar", ".gz", ".tgz", ".bz2", ".xz", ".7z"}
|
|
19
|
+
|
|
20
|
+
DEFAULT_EXCLUDES = [
|
|
21
|
+
".git",
|
|
22
|
+
".venv",
|
|
23
|
+
".mypy_cache",
|
|
24
|
+
".ruff_cache",
|
|
25
|
+
".pytest_cache",
|
|
26
|
+
"__pycache__",
|
|
27
|
+
"node_modules",
|
|
28
|
+
"dist",
|
|
29
|
+
"build",
|
|
30
|
+
"artifacts",
|
|
31
|
+
".cache",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
def _is_excluded(rel: Path, excludes: set[str]) -> bool:
|
|
35
|
+
# Exclude if any path component matches
|
|
36
|
+
for part in rel.parts:
|
|
37
|
+
if part in excludes:
|
|
38
|
+
return True
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class TreeStep:
|
|
43
|
+
name: str = "tree (filtered)"
|
|
44
|
+
max_depth: int = 4
|
|
45
|
+
excludes: list[str] | None = None
|
|
46
|
+
policy: AIContextPolicy | None = None
|
|
47
|
+
|
|
48
|
+
def run(self, ctx: BundleContext) -> StepResult:
|
|
49
|
+
start = time.time()
|
|
50
|
+
policy = self.policy or AIContextPolicy()
|
|
51
|
+
|
|
52
|
+
# allow overrides
|
|
53
|
+
exclude_dirs = set(self.excludes) if self.excludes else set(policy.exclude_dirs)
|
|
54
|
+
filt = PathFilter(exclude_dirs=exclude_dirs, exclude_file_exts=set(policy.exclude_file_exts))
|
|
55
|
+
|
|
56
|
+
out = ctx.metadir / "10_tree.txt"
|
|
57
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
root = ctx.root
|
|
60
|
+
lines: list[str] = []
|
|
61
|
+
|
|
62
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
63
|
+
dp = Path(dirpath)
|
|
64
|
+
rel_dp = dp.relative_to(root)
|
|
65
|
+
depth = 0 if rel_dp == Path(".") else len(rel_dp.parts)
|
|
66
|
+
|
|
67
|
+
if depth > self.max_depth:
|
|
68
|
+
dirnames[:] = []
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
# prune dirs (name + venv-structure)
|
|
72
|
+
kept = []
|
|
73
|
+
for d in dirnames:
|
|
74
|
+
if filt.should_prune_dir(dp, d):
|
|
75
|
+
continue
|
|
76
|
+
kept.append(d)
|
|
77
|
+
dirnames[:] = kept
|
|
78
|
+
|
|
79
|
+
for fn in filenames:
|
|
80
|
+
p = dp / fn
|
|
81
|
+
if not filt.should_include_file(root, p):
|
|
82
|
+
continue
|
|
83
|
+
lines.append(str(p.relative_to(root)))
|
|
84
|
+
|
|
85
|
+
lines.sort()
|
|
86
|
+
out.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
|
|
87
|
+
dur = int(time.time() - start)
|
|
88
|
+
return StepResult(self.name, "PASS", dur, "python-walk")
|
|
89
|
+
|
|
90
|
+
@dataclass
|
|
91
|
+
class LargestFilesStep:
|
|
92
|
+
name: str = "largest files"
|
|
93
|
+
limit: int = 80
|
|
94
|
+
excludes: list[str] | None = None
|
|
95
|
+
policy: AIContextPolicy | None = None
|
|
96
|
+
|
|
97
|
+
def run(self, ctx: BundleContext) -> StepResult:
|
|
98
|
+
start = time.time()
|
|
99
|
+
policy = self.policy or AIContextPolicy()
|
|
100
|
+
|
|
101
|
+
exclude_dirs = set(self.excludes) if self.excludes else set(policy.exclude_dirs)
|
|
102
|
+
filt = PathFilter(exclude_dirs=exclude_dirs, exclude_file_exts=set(policy.exclude_file_exts))
|
|
103
|
+
|
|
104
|
+
out = ctx.metadir / "11_largest_files.txt"
|
|
105
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
|
|
107
|
+
files: list[tuple[int, str]] = []
|
|
108
|
+
root = ctx.root
|
|
109
|
+
|
|
110
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
111
|
+
dp = Path(dirpath)
|
|
112
|
+
|
|
113
|
+
kept = []
|
|
114
|
+
for d in dirnames:
|
|
115
|
+
if filt.should_prune_dir(dp, d):
|
|
116
|
+
continue
|
|
117
|
+
kept.append(d)
|
|
118
|
+
dirnames[:] = kept
|
|
119
|
+
|
|
120
|
+
for fn in filenames:
|
|
121
|
+
p = dp / fn
|
|
122
|
+
if not filt.should_include_file(root, p):
|
|
123
|
+
continue
|
|
124
|
+
try:
|
|
125
|
+
size = p.stat().st_size
|
|
126
|
+
except OSError:
|
|
127
|
+
continue
|
|
128
|
+
files.append((size, str(p.relative_to(root))))
|
|
129
|
+
|
|
130
|
+
files.sort(key=lambda x: x[0], reverse=True)
|
|
131
|
+
lines = [f"{size}\t{path}" for size, path in files[: self.limit]]
|
|
132
|
+
out.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
|
|
133
|
+
|
|
134
|
+
dur = int(time.time() - start)
|
|
135
|
+
return StepResult(self.name, "PASS", dur, f"count={len(files)}")
|
|
136
|
+
|