frob 0.0.0__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.
Files changed (118) hide show
  1. frob/__init__.py +0 -0
  2. frob/__main__.py +296 -0
  3. frob/_compat.py +18 -0
  4. frob/app/__init__.py +4 -0
  5. frob/app/app.py +68 -0
  6. frob/app/arch_runner.py +43 -0
  7. frob/app/bind_runner.py +73 -0
  8. frob/app/bundle_runner.py +28 -0
  9. frob/app/config.py +211 -0
  10. frob/app/cycle_runner.py +67 -0
  11. frob/app/docs_runner.py +79 -0
  12. frob/app/dup_runner.py +36 -0
  13. frob/app/init_runner.py +31 -0
  14. frob/app/inspect_runner.py +85 -0
  15. frob/app/map_runner.py +18 -0
  16. frob/app/outline_runner.py +26 -0
  17. frob/app/parse_runner.py +84 -0
  18. frob/app/stub_runner.py +28 -0
  19. frob/app/tokens_runner.py +18 -0
  20. frob/app/xref_runner.py +29 -0
  21. frob/arch/__init__.py +566 -0
  22. frob/ast/__init__.py +0 -0
  23. frob/ast/avr.py +0 -0
  24. frob/ast/common.py +49 -0
  25. frob/ast/cpp.py +155 -0
  26. frob/ast/python.py +201 -0
  27. frob/bind/__init__.py +95 -0
  28. frob/bundle/__init__.py +226 -0
  29. frob/cycle/__init__.py +0 -0
  30. frob/cycle/graph.py +70 -0
  31. frob/docs/__init__.py +270 -0
  32. frob/dup/__init__.py +644 -0
  33. frob/init/__init__.py +0 -0
  34. frob/init/data/README.md.j2 +1 -0
  35. frob/init/data/cpp_library.CMakeLists.txt.j2 +51 -0
  36. frob/init/data/cpp_library.cmake.Config.cmake.in.j2 +4 -0
  37. frob/init/data/cpp_library.include.h.j2 +3 -0
  38. frob/init/data/cpp_library.src.cpp.j2 +3 -0
  39. frob/init/data/cpp_library.tests.CMakeLists.txt.j2 +16 -0
  40. frob/init/data/cpp_library.tests.cpp.j2 +7 -0
  41. frob/init/data/cpp_shared.Makefile.j2 +72 -0
  42. frob/init/data/cpp_shared.README.md.j2 +16 -0
  43. frob/init/data/cpp_shared.cmake.toolchain-linux-arm64.cmake.j2 +9 -0
  44. frob/init/data/cpp_shared.docs.index.md.j2 +30 -0
  45. frob/init/data/cpp_shared.github.branch-protection.yml.j2 +34 -0
  46. frob/init/data/cpp_shared.github.ci.yml.j2 +71 -0
  47. frob/init/data/cpp_shared.github.release.yml.j2 +99 -0
  48. frob/init/data/cpp_shared.gitignore.j2 +11 -0
  49. frob/init/data/cpp_shared.tests.CMakeLists.txt.j2 +41 -0
  50. frob/init/data/cpp_tool.CMakeLists.txt.j2 +22 -0
  51. frob/init/data/cpp_tool.include.h.j2 +3 -0
  52. frob/init/data/cpp_tool.src.cpp.j2 +3 -0
  53. frob/init/data/cpp_tool.src.main.cpp.j2 +7 -0
  54. frob/init/data/cpp_tool.tests.CMakeLists.txt.j2 +16 -0
  55. frob/init/data/cpp_tool.tests.cpp.j2 +7 -0
  56. frob/init/data/pybind11_library.CMakeLists.txt.j2 +16 -0
  57. frob/init/data/pybind11_library.Makefile.j2 +21 -0
  58. frob/init/data/pybind11_library.github.ci.yml.j2 +30 -0
  59. frob/init/data/pybind11_library.include.h.j2 +3 -0
  60. frob/init/data/pybind11_library.pyproject.toml.j2 +21 -0
  61. frob/init/data/pybind11_library.python.__init__.py.j2 +3 -0
  62. frob/init/data/pybind11_library.src.bindings.cpp.j2 +12 -0
  63. frob/init/data/pybind11_library.src.cpp.j2 +5 -0
  64. frob/init/data/pybind11_library.tests.test_bindings.py.j2 +6 -0
  65. frob/init/data/pybind11_shared.gitignore.j2 +12 -0
  66. frob/init/data/pyo3_library.Cargo.toml.j2 +11 -0
  67. frob/init/data/pyo3_library.Makefile.j2 +22 -0
  68. frob/init/data/pyo3_library.github.ci.yml.j2 +36 -0
  69. frob/init/data/pyo3_library.pyproject.toml.j2 +23 -0
  70. frob/init/data/pyo3_library.python.__init__.py.j2 +3 -0
  71. frob/init/data/pyo3_library.src.lib.rs.j2 +13 -0
  72. frob/init/data/pyo3_library.tests.test_bindings.py.j2 +6 -0
  73. frob/init/data/pyo3_shared.gitignore.j2 +7 -0
  74. frob/init/data/pyproject.toml.j2 +50 -0
  75. frob/init/data/python_lib.__init__.py.j2 +1 -0
  76. frob/init/data/python_shared.Makefile.j2 +67 -0
  77. frob/init/data/python_shared.docs.index.md.j2 +25 -0
  78. frob/init/data/python_shared.github.branch-protection.yml.j2 +32 -0
  79. frob/init/data/python_shared.github.ci.yml.j2 +39 -0
  80. frob/init/data/python_shared.gitignore.j2 +11 -0
  81. frob/init/data/python_shared.logging.__init__.py.j2 +3 -0
  82. frob/init/data/python_shared.logging.config.toml.j2 +30 -0
  83. frob/init/data/python_shared.logging.filter.py.j2 +14 -0
  84. frob/init/data/python_shared.logging.formatter.py.j2 +17 -0
  85. frob/init/data/python_shared.logging.logger.py.j2 +32 -0
  86. frob/init/data/python_shared.tests.conftest.py.j2 +4 -0
  87. frob/init/data/python_shared.tests.system.test_build.py.j2 +37 -0
  88. frob/init/data/python_shared.tests.unit.test_placeholder.py.j2 +2 -0
  89. frob/init/data/python_tool.__init__.py.j2 +1 -0
  90. frob/init/data/python_tool.__main__.py.j2 +18 -0
  91. frob/init/data/python_tool.app.py.j2 +10 -0
  92. frob/init/data/python_tool.config.py.j2 +26 -0
  93. frob/init/project.py +266 -0
  94. frob/inspect/__init__.py +119 -0
  95. frob/logging/__init__.py +3 -0
  96. frob/logging/config.toml +30 -0
  97. frob/logging/filter.py +12 -0
  98. frob/logging/formatter.py +18 -0
  99. frob/logging/handler.py +0 -0
  100. frob/logging/logger.py +25 -0
  101. frob/map/__init__.py +138 -0
  102. frob/outline/__init__.py +228 -0
  103. frob/process/__init__.py +34 -0
  104. frob/process/parsers/__init__.py +23 -0
  105. frob/process/parsers/clang.py +66 -0
  106. frob/process/parsers/common.py +122 -0
  107. frob/process/parsers/junit.py +86 -0
  108. frob/process/parsers/pycharm.py +148 -0
  109. frob/process/parsers/pytest.py +126 -0
  110. frob/process/parsers/ruff.py +104 -0
  111. frob/process/parsers/ty.py +92 -0
  112. frob/stub/__init__.py +59 -0
  113. frob/tokens/__init__.py +168 -0
  114. frob/xref/__init__.py +205 -0
  115. frob-0.0.0.dist-info/METADATA +206 -0
  116. frob-0.0.0.dist-info/RECORD +118 -0
  117. frob-0.0.0.dist-info/WHEEL +5 -0
  118. frob-0.0.0.dist-info/top_level.txt +1 -0
frob/__init__.py ADDED
File without changes
frob/__main__.py ADDED
@@ -0,0 +1,296 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ from frob.app import App, AppConfig
7
+
8
+
9
+ def _build_parser() -> argparse.ArgumentParser:
10
+ p = argparse.ArgumentParser(
11
+ prog="frob",
12
+ description="Developer workflow tools -- optimized for agentic use",
13
+ )
14
+ sub = p.add_subparsers(dest="subcommand")
15
+
16
+ # -- init ----------------------------------------------------------------
17
+ init_p = sub.add_parser("init", help="scaffold a new project from a template")
18
+ init_sub = init_p.add_subparsers(dest="init_command")
19
+ init_sub.add_parser("list", help="list registered project types")
20
+ new_p = init_sub.add_parser("new", help="create a new project")
21
+ new_p.add_argument(
22
+ "init_type", metavar="type", help="project type (e.g. python-tool)"
23
+ )
24
+ new_p.add_argument("init_name", metavar="name", help="project name")
25
+ new_p.add_argument("--output", dest="init_output", metavar="DIR")
26
+ new_p.add_argument(
27
+ "--force",
28
+ dest="init_force",
29
+ action="store_true",
30
+ help="overwrite existing files",
31
+ )
32
+
33
+ # -- cycle ---------------------------------------------------------------
34
+ cycle_p = sub.add_parser("cycle", help="detect dependency cycles")
35
+ cycle_p.add_argument("cycle_path", metavar="path")
36
+ cycle_p.add_argument("--lang", dest="cycle_lang", choices=["python", "cpp", "c"])
37
+ cycle_p.add_argument("--suggest", dest="cycle_suggest", action="store_true")
38
+
39
+ # -- stub ----------------------------------------------------------------
40
+ stub_p = sub.add_parser(
41
+ "stub", help="emit a source file stubbed to a single target"
42
+ )
43
+ stub_p.add_argument("stub_file", metavar="file")
44
+ stub_p.add_argument(
45
+ "stub_target",
46
+ metavar="target",
47
+ help="function or ClassName.method to keep intact",
48
+ )
49
+ stub_p.add_argument("--output", dest="stub_output", metavar="FILE")
50
+
51
+ # -- outline -------------------------------------------------------------
52
+ outline_p = sub.add_parser(
53
+ "outline",
54
+ help="show structural skeleton of a file (classes, functions, line numbers)",
55
+ )
56
+ outline_p.add_argument("outline_file", metavar="file")
57
+ outline_p.add_argument("--json", dest="outline_json", action="store_true")
58
+
59
+ # -- map -----------------------------------------------------------------
60
+ map_p = sub.add_parser(
61
+ "map",
62
+ help="show whole-project structural map (symbols + line counts)",
63
+ )
64
+ map_p.add_argument("map_path", metavar="path", nargs="?", default=".")
65
+ map_p.add_argument("--json", dest="map_json", action="store_true")
66
+ map_p.add_argument("--depth", dest="map_depth", type=int, metavar="N")
67
+
68
+ # -- xref ----------------------------------------------------------------
69
+ xref_p = sub.add_parser(
70
+ "xref",
71
+ help="find where a symbol is defined and every file that uses it",
72
+ )
73
+ xref_p.add_argument("xref_symbol", metavar="symbol")
74
+ xref_p.add_argument("xref_path", metavar="path", nargs="?", default=".")
75
+ xref_p.add_argument("--lang", dest="xref_lang", choices=["python", "cpp", "c"])
76
+ xref_p.add_argument("--json", dest="xref_json", action="store_true")
77
+
78
+ # -- tokens --------------------------------------------------------------
79
+ tokens_p = sub.add_parser(
80
+ "tokens",
81
+ help="estimate token cost of files before reading them",
82
+ )
83
+ tokens_p.add_argument("tokens_paths", metavar="path", nargs="+")
84
+ tokens_p.add_argument(
85
+ "--detail",
86
+ dest="tokens_detail",
87
+ action="store_true",
88
+ help="break down by function/class region",
89
+ )
90
+ tokens_p.add_argument("--json", dest="tokens_json", action="store_true")
91
+
92
+ # -- parse ---------------------------------------------------------------
93
+ parse_p = sub.add_parser(
94
+ "parse",
95
+ help="parse tool output (pytest/ruff/ty/clang/junit) into compact summary",
96
+ )
97
+ parse_p.add_argument(
98
+ "parse_tool",
99
+ metavar="tool",
100
+ choices=[
101
+ "pytest",
102
+ "ruff",
103
+ "ty",
104
+ "clang",
105
+ "clang++",
106
+ "gcc",
107
+ "g++",
108
+ "junit",
109
+ "gtest",
110
+ "catch2",
111
+ "pycharm",
112
+ ],
113
+ )
114
+ parse_p.add_argument(
115
+ "parse_input", metavar="file", nargs="?", help="input file (default: stdin)"
116
+ )
117
+ parse_p.add_argument(
118
+ "--exit-code",
119
+ dest="parse_exit_code",
120
+ type=int,
121
+ default=0,
122
+ metavar="N",
123
+ help="exit code the tool returned (affects pass/fail)",
124
+ )
125
+ parse_p.add_argument("--json", dest="parse_json", action="store_true")
126
+ parse_p.add_argument(
127
+ "--verbose",
128
+ dest="parse_verbose",
129
+ action="store_true",
130
+ help="show passing tests and notes too",
131
+ )
132
+ parse_p.add_argument(
133
+ "--passthrough",
134
+ dest="parse_passthrough",
135
+ action="store_true",
136
+ help="exit non-zero if the tool failed (useful in pipelines)",
137
+ )
138
+
139
+ # -- bundle --------------------------------------------------------------
140
+ bundle_p = sub.add_parser(
141
+ "bundle",
142
+ help="assemble minimal context for a subagent (stubbed file + import sigs)",
143
+ )
144
+ bundle_p.add_argument("bundle_file", metavar="file")
145
+ bundle_p.add_argument("bundle_target", metavar="target")
146
+ bundle_p.add_argument(
147
+ "--depth",
148
+ dest="bundle_depth",
149
+ type=int,
150
+ default=1,
151
+ metavar="N",
152
+ help="how many import levels to inline (default: 1)",
153
+ )
154
+ bundle_p.add_argument(
155
+ "--format",
156
+ dest="bundle_format",
157
+ choices=["markdown", "json"],
158
+ default="markdown",
159
+ )
160
+
161
+ # -- dup -----------------------------------------------------------------
162
+ dup_p = sub.add_parser(
163
+ "dup",
164
+ help="detect duplicate/clone code segments (Type 1 exact, Type 2 renamed)",
165
+ )
166
+ dup_p.add_argument("dup_path", metavar="path", nargs="?", default=".")
167
+ dup_p.add_argument(
168
+ "--min-lines",
169
+ dest="dup_min_lines",
170
+ type=int,
171
+ default=6,
172
+ metavar="N",
173
+ help="minimum function body size to consider (default: 6)",
174
+ )
175
+ dup_p.add_argument("--json", dest="dup_json", action="store_true")
176
+
177
+ # -- inspect -------------------------------------------------------------
178
+ inspect_p = sub.add_parser(
179
+ "inspect",
180
+ help="run PyCharm headless inspection and parse results",
181
+ )
182
+ inspect_p.add_argument(
183
+ "inspect_project", metavar="project_dir", help="path to the project to inspect"
184
+ )
185
+ inspect_p.add_argument(
186
+ "--pycharm",
187
+ dest="inspect_pycharm",
188
+ metavar="PATH",
189
+ help="path to PyCharm inspect.bat",
190
+ )
191
+ inspect_p.add_argument(
192
+ "--profile",
193
+ dest="inspect_profile",
194
+ metavar="PATH",
195
+ help="path to inspection profile XML",
196
+ )
197
+ inspect_p.add_argument(
198
+ "--output-dir",
199
+ dest="inspect_output_dir",
200
+ metavar="DIR",
201
+ help="directory for inspection output (default: temp dir)",
202
+ )
203
+ inspect_p.add_argument(
204
+ "--scope",
205
+ dest="inspect_scope",
206
+ metavar="DIR",
207
+ help="subdirectory scope for inspection (e.g. src)",
208
+ )
209
+ inspect_p.add_argument("--json", dest="inspect_json", action="store_true")
210
+
211
+ # -- arch ----------------------------------------------------------------
212
+ arch_p = sub.add_parser(
213
+ "arch",
214
+ help="arch analysis: long functions, god classes, coupling",
215
+ )
216
+ arch_p.add_argument("arch_path", metavar="path", nargs="?", default=".")
217
+ arch_p.add_argument("--json", dest="arch_json", action="store_true")
218
+ arch_p.add_argument(
219
+ "--max-function-lines",
220
+ dest="arch_max_function_lines",
221
+ type=int,
222
+ default=30,
223
+ metavar="N",
224
+ )
225
+ arch_p.add_argument(
226
+ "--max-class-methods",
227
+ dest="arch_max_class_methods",
228
+ type=int,
229
+ default=12,
230
+ metavar="N",
231
+ )
232
+
233
+ # -- docs ----------------------------------------------------------------
234
+ docs_p = sub.add_parser(
235
+ "docs",
236
+ help="extract docstrings or search docs/ for a file/symbol",
237
+ )
238
+ docs_p.add_argument(
239
+ "docs_path", metavar="path", help="file or directory to inspect"
240
+ )
241
+ docs_p.add_argument(
242
+ "docs_symbol",
243
+ metavar="symbol",
244
+ nargs="?",
245
+ default=None,
246
+ help="class or function name (optional)",
247
+ )
248
+ docs_p.add_argument(
249
+ "--overview",
250
+ dest="docs_overview",
251
+ action="store_true",
252
+ help="show relevant docs/ headings and summaries",
253
+ )
254
+ docs_p.add_argument(
255
+ "--search",
256
+ dest="docs_search",
257
+ metavar="QUERY",
258
+ help="full-text search through docs/",
259
+ )
260
+ docs_p.add_argument("--json", dest="docs_json", action="store_true")
261
+
262
+ # -- bind ----------------------------------------------------------------
263
+ bind_p = sub.add_parser(
264
+ "bind",
265
+ help="verify binding declarations match source signatures",
266
+ )
267
+ bind_p.add_argument("bind_path", metavar="path", help="project root to scan")
268
+ bind_p.add_argument(
269
+ "--list-bindings",
270
+ dest="bind_list_bindings",
271
+ action="store_true",
272
+ help="list all BIND declarations",
273
+ )
274
+ bind_p.add_argument(
275
+ "--list-sources",
276
+ dest="bind_list_sources",
277
+ action="store_true",
278
+ help="list all detected source signatures",
279
+ )
280
+ bind_p.add_argument("--json", dest="bind_json", action="store_true")
281
+
282
+ return p
283
+
284
+
285
+ if __name__ == "__main__":
286
+ import sys as _sys
287
+
288
+ if len(_sys.argv) > 1 and _sys.argv[1] == "bind":
289
+ from frob.app.bind_runner import run as _bind_run
290
+
291
+ _bind_run(_sys.argv[2:])
292
+ else:
293
+ parser = _build_parser()
294
+ args = parser.parse_args()
295
+ cfg = AppConfig.from_external(args, Path("pyproject.toml"))
296
+ App(cfg)()
frob/_compat.py ADDED
@@ -0,0 +1,18 @@
1
+ import sys
2
+
3
+ if sys.version_info < (3, 11):
4
+ from typing_extensions import Self
5
+ else:
6
+ from typing import Self
7
+
8
+ # tomllib is stdlib on 3.11+; fall back to tomli or toml on older Python.
9
+ # Unconditional try/except so ty only evaluates the first branch.
10
+ try:
11
+ import tomllib as toml # type: ignore[import-not-found,no-redef] # ty: ignore[unresolved-import]
12
+ except ImportError:
13
+ try:
14
+ import tomli as toml # type: ignore[import-not-found,no-redef]
15
+ except ImportError:
16
+ import toml # type: ignore[import-not-found,no-redef] # ty: ignore[unresolved-import]
17
+
18
+ __all__ = ["toml", "Self"]
frob/app/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from frob.app.app import App
2
+ from frob.app.config import AppConfig
3
+
4
+ __all__ = ["App", "AppConfig"]
frob/app/app.py ADDED
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from frob.app.config import AppConfig, Subcommand
6
+ from frob.logging import get_logger
7
+
8
+ _log = get_logger(__name__)
9
+
10
+
11
+ class App:
12
+ def __init__(self, cfg: AppConfig) -> None:
13
+ self._cfg = cfg
14
+
15
+ def __call__(self) -> None:
16
+ from frob.app import (
17
+ arch_runner,
18
+ bind_runner,
19
+ bundle_runner,
20
+ cycle_runner,
21
+ docs_runner,
22
+ dup_runner,
23
+ init_runner,
24
+ inspect_runner,
25
+ map_runner,
26
+ outline_runner,
27
+ parse_runner,
28
+ stub_runner,
29
+ tokens_runner,
30
+ xref_runner,
31
+ )
32
+
33
+ match self._cfg.subcommand:
34
+ case Subcommand.init:
35
+ init_runner.run(self._cfg)
36
+ case Subcommand.cycle:
37
+ cycle_runner.run(self._cfg)
38
+ case Subcommand.stub:
39
+ stub_runner.run(self._cfg)
40
+ case Subcommand.outline:
41
+ outline_runner.run(self._cfg)
42
+ case Subcommand.map:
43
+ map_runner.run(self._cfg)
44
+ case Subcommand.xref:
45
+ xref_runner.run(self._cfg)
46
+ case Subcommand.tokens:
47
+ tokens_runner.run(self._cfg)
48
+ case Subcommand.bundle:
49
+ bundle_runner.run(self._cfg)
50
+ case Subcommand.parse:
51
+ parse_runner.run(self._cfg)
52
+ case Subcommand.dup:
53
+ dup_runner.run(self._cfg)
54
+ case Subcommand.arch:
55
+ arch_runner.run(self._cfg)
56
+ case Subcommand.inspect:
57
+ inspect_runner.run(self._cfg)
58
+ case Subcommand.docs:
59
+ docs_runner.run(self._cfg)
60
+ case Subcommand.bind:
61
+ bind_runner.run([])
62
+ case _:
63
+ _log.error(
64
+ "usage: frob "
65
+ "<init|cycle|stub|outline|map|xref|tokens|bundle|parse|dup|arch|inspect|docs|bind>"
66
+ " ..."
67
+ )
68
+ sys.exit(1)
@@ -0,0 +1,43 @@
1
+ """
2
+ arch_runner: run the architectural linter.
3
+
4
+ AppConfig fields used:
5
+ arch_path (Path | None) -- root directory to analyze (required)
6
+ arch_json (bool) -- emit JSON instead of plain text
7
+ arch_max_function_lines (int | None) -- override max lines per function
8
+ arch_max_class_methods (int | None) -- override max methods per class
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+
15
+ from frob.app.config import AppConfig
16
+ from frob.logging import get_logger
17
+
18
+ _log = get_logger(__name__)
19
+
20
+
21
+ def run(cfg: AppConfig) -> None:
22
+ from frob.arch import analyze_project
23
+
24
+ root = getattr(cfg, "arch_path", None)
25
+ if root is None:
26
+ _log.error("frob arch requires <path>")
27
+ sys.exit(1)
28
+
29
+ kwargs: dict = {}
30
+ max_fn = getattr(cfg, "arch_max_function_lines", None)
31
+ if max_fn is not None:
32
+ kwargs["max_function_lines"] = int(max_fn)
33
+ max_cls = getattr(cfg, "arch_max_class_methods", None)
34
+ if max_cls is not None:
35
+ kwargs["max_class_methods"] = int(max_cls)
36
+
37
+ result = analyze_project(root, **kwargs)
38
+
39
+ use_json = getattr(cfg, "arch_json", False)
40
+ if use_json:
41
+ _log.info("%s", result.as_json())
42
+ else:
43
+ _log.info("%s", result.as_text())
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from frob.bind import check, scan_bindings, scan_sources
9
+
10
+
11
+ def run(argv=None):
12
+ p = argparse.ArgumentParser(
13
+ prog="frob bind",
14
+ description="Verify that binding declarations match source signatures",
15
+ )
16
+ p.add_argument("path", help="Project root to scan")
17
+ p.add_argument("--json", action="store_true", help="Output JSON")
18
+ p.add_argument(
19
+ "--list-bindings", action="store_true", help="List all BIND declarations"
20
+ )
21
+ p.add_argument(
22
+ "--list-sources",
23
+ action="store_true",
24
+ help="List all detected source signatures",
25
+ )
26
+ args = p.parse_args(argv)
27
+
28
+ root = Path(args.path)
29
+ if not root.exists():
30
+ print(f"error: {root} does not exist", file=sys.stderr)
31
+ sys.exit(1)
32
+
33
+ if args.list_bindings:
34
+ items = scan_bindings(root)
35
+ if args.json:
36
+ print(json.dumps([vars(i) for i in items], indent=2))
37
+ else:
38
+ for b in items:
39
+ print(f"{b.file}:{b.line} {b.signature} [{b.kind}]")
40
+ return
41
+
42
+ if args.list_sources:
43
+ items = scan_sources(root)
44
+ if args.json:
45
+ print(json.dumps([vars(i) for i in items], indent=2))
46
+ else:
47
+ for s in items:
48
+ print(f"{s.file}:{s.line} {s.signature} [{s.kind}]")
49
+ return
50
+
51
+ mismatches = check(root)
52
+ if args.json:
53
+ out = {
54
+ "root": str(root),
55
+ "ok": len(mismatches) == 0,
56
+ "mismatches": [
57
+ {
58
+ "file": m.binding.file,
59
+ "line": m.binding.line,
60
+ "signature": m.binding.signature,
61
+ "issue": m.issue,
62
+ }
63
+ for m in mismatches
64
+ ],
65
+ }
66
+ print(json.dumps(out, indent=2))
67
+ else:
68
+ if not mismatches:
69
+ print("ok: all bindings match source declarations")
70
+ else:
71
+ for m in mismatches:
72
+ print(f"{m.binding.file}:{m.binding.line}: {m.issue}")
73
+ sys.exit(1)
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from frob.app.config import AppConfig
6
+ from frob.bundle import build_bundle
7
+ from frob.logging import get_logger
8
+
9
+ _log = get_logger(__name__)
10
+
11
+
12
+ def run(cfg: AppConfig) -> None:
13
+ if cfg.bundle_file is None or cfg.bundle_target is None:
14
+ _log.error("frob bundle requires <file> and <target>")
15
+ sys.exit(1)
16
+
17
+ result = build_bundle(cfg.bundle_file, cfg.bundle_target, depth=cfg.bundle_depth)
18
+
19
+ if result.is_err:
20
+ _log.error(result.danger_err.value)
21
+ sys.exit(1)
22
+
23
+ bundle = result.danger_ok
24
+ fmt = cfg.bundle_format
25
+ if fmt == "json":
26
+ _log.info(bundle.as_json())
27
+ else:
28
+ _log.info(bundle.as_markdown())