rpr-cli 0.1.1__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.
- rpr/__init__.py +1 -0
- rpr/agent/__init__.py +29 -0
- rpr/agent/approval.py +19 -0
- rpr/agent/bootstrap.py +60 -0
- rpr/agent/client.py +115 -0
- rpr/agent/control.py +16 -0
- rpr/agent/mock.py +158 -0
- rpr/agent/runtime.py +291 -0
- rpr/agent/session.py +91 -0
- rpr/agent/tools/__init__.py +10 -0
- rpr/agent/tools/base.py +62 -0
- rpr/agent/tools/mutating.py +47 -0
- rpr/agent/tools/readonly.py +147 -0
- rpr/agent/tools/registry.py +30 -0
- rpr/application/__init__.py +1 -0
- rpr/application/catalog.py +427 -0
- rpr/application/chat_service.py +413 -0
- rpr/application/checks.py +49 -0
- rpr/application/cli_adapter.py +36 -0
- rpr/application/completer.py +163 -0
- rpr/application/conversation_service.py +92 -0
- rpr/application/prompt_service.py +85 -0
- rpr/application/selector.py +240 -0
- rpr/application/shell.py +543 -0
- rpr/checks/__init__.py +0 -0
- rpr/checks/base.py +13 -0
- rpr/checks/instructions.py +89 -0
- rpr/checks/packages.py +619 -0
- rpr/checks/workspace.py +170 -0
- rpr/cli.py +78 -0
- rpr/commands/__init__.py +0 -0
- rpr/commands/add.py +166 -0
- rpr/commands/chat.py +48 -0
- rpr/commands/check.py +53 -0
- rpr/commands/generate/__init__.py +0 -0
- rpr/commands/generate/api.py +228 -0
- rpr/commands/generate/domain.py +383 -0
- rpr/commands/generate/engine.py +148 -0
- rpr/commands/generate/storybook.py +442 -0
- rpr/commands/generate/ui.py +414 -0
- rpr/commands/init.py +822 -0
- rpr/commands/map.py +113 -0
- rpr/commands/settings.py +102 -0
- rpr/commands/sync.py +97 -0
- rpr/context.py +203 -0
- rpr/generators/__init__.py +0 -0
- rpr/generators/base.py +110 -0
- rpr/generators/claude.py +33 -0
- rpr/generators/copilot.py +36 -0
- rpr/generators/cursor.py +38 -0
- rpr/generators/gemini.py +33 -0
- rpr/map/__init__.py +0 -0
- rpr/map/architecture.py +495 -0
- rpr/map/chains.py +317 -0
- rpr/map/classifier.py +170 -0
- rpr/map/coverage.py +200 -0
- rpr/map/dependencies.py +243 -0
- rpr/map/extractor.py +223 -0
- rpr/map/graph.py +318 -0
- rpr/map/output.py +1030 -0
- rpr/map/responsibility.py +345 -0
- rpr/map/topology.py +327 -0
- rpr/map/walker.py +151 -0
- rpr/scaffolds/domain/base_entity.md +30 -0
- rpr/scaffolds/domain/base_repo.md +48 -0
- rpr/scaffolds/domain/container.md +76 -0
- rpr/scaffolds/domain/settings.md +57 -0
- rpr/scaffolds/instructions/all.instructions.md +50 -0
- rpr/scaffolds/instructions/api.instructions.md +42 -0
- rpr/scaffolds/instructions/domain.instructions.md +93 -0
- rpr/scaffolds/instructions/frontend.instructions.md +97 -0
- rpr/scaffolds/instructions/rust-engine.instructions.md +40 -0
- rpr/scaffolds/instructions/setup-guide.instructions.md +86 -0
- rpr/scaffolds/instructions/tooling-setup.instructions.md +97 -0
- rpr/scaffolds/instructions/tooling.instructions.md +42 -0
- rpr/scaffolds/js_special_files/fetch.service.md +222 -0
- rpr/scaffolds/js_special_files/sticky-navigation.md +164 -0
- rpr/scaffolds/special_files/domain_container.md +76 -0
- rpr/scaffolds/special_files/domain_settings.md +57 -0
- rpr/scaffolds/special_files/dto_util.md +62 -0
- rpr/scaffolds/special_files/encrypted_column.md +98 -0
- rpr/scaffolds/special_files/mapper_util.md +159 -0
- rpr/scaffolds/special_files/partial_update.md +61 -0
- rpr/templates/__init__.py +0 -0
- rpr/templates/registry.py +81 -0
- rpr/ui/__init__.py +0 -0
- rpr/ui/console.py +32 -0
- rpr/ui/markdown.py +59 -0
- rpr/ui/prompt_session.py +430 -0
- rpr/ui/renderers.py +167 -0
- rpr/ui/theme.py +286 -0
- rpr/workspace.py +131 -0
- rpr_cli-0.1.1.dist-info/METADATA +201 -0
- rpr_cli-0.1.1.dist-info/RECORD +97 -0
- rpr_cli-0.1.1.dist-info/WHEEL +4 -0
- rpr_cli-0.1.1.dist-info/entry_points.txt +2 -0
- rpr_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
rpr/map/graph.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import PurePosixPath
|
|
5
|
+
from typing import Callable, Literal
|
|
6
|
+
|
|
7
|
+
from rpr.map.walker import Language, SourceFile
|
|
8
|
+
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
# Domain aggregate
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class DependencyGraph:
|
|
16
|
+
nodes: list[SourceFile]
|
|
17
|
+
edges: dict[str, list[str]] # rel_path → [dep rel_path, ...]
|
|
18
|
+
reverse_edges: dict[str, list[str]] # rel_path → [importer rel_path, ...]
|
|
19
|
+
cycles: list[list[str]]
|
|
20
|
+
backend: Literal["rust", "python-fallback"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Private language resolvers (one per language — SRP)
|
|
25
|
+
# All share the same signature so they can be stored in _RESOLVERS (OCP).
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _resolve_python(
|
|
30
|
+
raw: str,
|
|
31
|
+
importer: SourceFile,
|
|
32
|
+
known: dict[str, SourceFile],
|
|
33
|
+
) -> str | None:
|
|
34
|
+
"""Resolve a raw Python import string to a known rel_path key, or None."""
|
|
35
|
+
if not raw:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
if raw.startswith("."):
|
|
39
|
+
# Relative import: count leading dots to determine traversal depth.
|
|
40
|
+
# "." means same package, ".." means one level up, etc.
|
|
41
|
+
dots = len(raw) - len(raw.lstrip("."))
|
|
42
|
+
remainder = raw.lstrip(".")
|
|
43
|
+
|
|
44
|
+
base = PurePosixPath(str(importer.rel_path.parent))
|
|
45
|
+
for _ in range(dots - 1):
|
|
46
|
+
base = base.parent
|
|
47
|
+
|
|
48
|
+
if remainder:
|
|
49
|
+
candidate_base = base / PurePosixPath(remainder.replace(".", "/"))
|
|
50
|
+
else:
|
|
51
|
+
candidate_base = base
|
|
52
|
+
|
|
53
|
+
for suffix in (f"{candidate_base}.py", f"{candidate_base}/__init__.py"):
|
|
54
|
+
if suffix in known:
|
|
55
|
+
return suffix
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
# Absolute-dotted module (e.g. "rpr.context", "src.utils.helpers").
|
|
59
|
+
# Single-segment names with no dots are stdlib/external — drop them.
|
|
60
|
+
if "." not in raw:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
# Convert dotted form to path and suffix-match against known keys.
|
|
64
|
+
path_form = raw.replace(".", "/")
|
|
65
|
+
for suffix in (f"{path_form}.py", f"{path_form}/__init__.py"):
|
|
66
|
+
for key in known:
|
|
67
|
+
if key == suffix or key.endswith(f"/{suffix}"):
|
|
68
|
+
return key
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _normalize_posix(path: str) -> str:
|
|
73
|
+
"""Resolve `.` and `..` components in a posix path string."""
|
|
74
|
+
parts: list[str] = []
|
|
75
|
+
for part in path.split("/"):
|
|
76
|
+
if part == "..":
|
|
77
|
+
if parts:
|
|
78
|
+
parts.pop()
|
|
79
|
+
elif part and part != ".":
|
|
80
|
+
parts.append(part)
|
|
81
|
+
return "/".join(parts)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _resolve_ts_js(
|
|
85
|
+
raw: str,
|
|
86
|
+
importer: SourceFile,
|
|
87
|
+
known: dict[str, SourceFile],
|
|
88
|
+
) -> str | None:
|
|
89
|
+
"""Resolve a TS/JS import specifier to a known rel_path key, or None."""
|
|
90
|
+
if not raw.startswith("./") and not raw.startswith("../"):
|
|
91
|
+
return None # External package — drop.
|
|
92
|
+
|
|
93
|
+
# Strip any existing extension so probing below works uniformly.
|
|
94
|
+
base_raw = raw
|
|
95
|
+
for ext in (".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"):
|
|
96
|
+
if raw.endswith(ext):
|
|
97
|
+
base_raw = raw[: -len(ext)]
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
importer_dir = str(importer.rel_path.parent).replace("\\", "/")
|
|
101
|
+
raw_posix = f"{importer_dir}/{base_raw}" if importer_dir != "." else base_raw
|
|
102
|
+
candidate_base = _normalize_posix(raw_posix)
|
|
103
|
+
|
|
104
|
+
probes = [
|
|
105
|
+
f"{candidate_base}.ts",
|
|
106
|
+
f"{candidate_base}.tsx",
|
|
107
|
+
f"{candidate_base}.js",
|
|
108
|
+
f"{candidate_base}.jsx",
|
|
109
|
+
f"{candidate_base}.mjs",
|
|
110
|
+
f"{candidate_base}.cjs",
|
|
111
|
+
f"{candidate_base}/index.ts",
|
|
112
|
+
f"{candidate_base}/index.tsx",
|
|
113
|
+
f"{candidate_base}/index.js",
|
|
114
|
+
f"{candidate_base}/index.jsx",
|
|
115
|
+
]
|
|
116
|
+
for probe in probes:
|
|
117
|
+
if probe in known:
|
|
118
|
+
return probe
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _resolve_rust(
|
|
123
|
+
raw: str,
|
|
124
|
+
importer: SourceFile,
|
|
125
|
+
known: dict[str, SourceFile],
|
|
126
|
+
) -> str | None:
|
|
127
|
+
"""Resolve a raw Rust use/mod string to a known rel_path key, or None."""
|
|
128
|
+
if not raw:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
# extern crate / std / super / self — always external or non-file.
|
|
132
|
+
if raw.startswith("extern::") or raw.startswith("std::"):
|
|
133
|
+
return None
|
|
134
|
+
if raw.startswith("super::") or raw.startswith("self::"):
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
# mod declaration: bare name, no "::", resolved adjacent to importer.
|
|
138
|
+
if "::" not in raw:
|
|
139
|
+
importer_dir = str(importer.rel_path.parent)
|
|
140
|
+
prefix = "" if importer_dir == "." else f"{importer_dir}/"
|
|
141
|
+
for probe in (f"{prefix}{raw}.rs", f"{prefix}{raw}/mod.rs"):
|
|
142
|
+
if probe in known:
|
|
143
|
+
return probe
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
# use crate::X::Y — strip "crate::" prefix.
|
|
147
|
+
path = raw.removeprefix("crate::")
|
|
148
|
+
# Drop glob / brace suffixes (e.g. "::*", "::{Foo, Bar}").
|
|
149
|
+
if path.endswith("::*"):
|
|
150
|
+
path = path[:-3]
|
|
151
|
+
brace = path.find("::{")
|
|
152
|
+
if brace != -1:
|
|
153
|
+
path = path[:brace]
|
|
154
|
+
|
|
155
|
+
path_form = path.replace("::", "/")
|
|
156
|
+
for suffix in (f"{path_form}.rs", f"{path_form}/mod.rs"):
|
|
157
|
+
for key in known:
|
|
158
|
+
if key == suffix or key.endswith(f"/{suffix}"):
|
|
159
|
+
return key
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _resolve_cpp(
|
|
164
|
+
raw: str,
|
|
165
|
+
importer: SourceFile,
|
|
166
|
+
known: dict[str, SourceFile],
|
|
167
|
+
) -> str | None:
|
|
168
|
+
"""Resolve a C++ #include string to a known rel_path key, or None."""
|
|
169
|
+
if not raw:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
# Angle-bracket includes are system/external — drop.
|
|
173
|
+
if raw.startswith("<") and raw.endswith(">"):
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
# Strip surrounding quotes from quoted includes.
|
|
177
|
+
header = raw.strip('"')
|
|
178
|
+
|
|
179
|
+
# 1. Resolve relative to the importer's directory.
|
|
180
|
+
importer_dir = PurePosixPath(str(importer.rel_path.parent))
|
|
181
|
+
candidate = str((importer_dir / header).as_posix())
|
|
182
|
+
if candidate.startswith("./"):
|
|
183
|
+
candidate = candidate[2:]
|
|
184
|
+
if candidate in known:
|
|
185
|
+
return candidate
|
|
186
|
+
|
|
187
|
+
# 2. Fallback: search for "include/<header>" within known paths.
|
|
188
|
+
include_suffix = f"include/{header}"
|
|
189
|
+
for key in known:
|
|
190
|
+
if key == include_suffix or key.endswith(f"/{include_suffix}"):
|
|
191
|
+
return key
|
|
192
|
+
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Dispatch dict: adding a new language requires only a new entry here (OCP).
|
|
197
|
+
_RESOLVERS: dict[
|
|
198
|
+
Language, Callable[[str, SourceFile, dict[str, SourceFile]], str | None]
|
|
199
|
+
] = {
|
|
200
|
+
"python": _resolve_python,
|
|
201
|
+
"typescript": _resolve_ts_js,
|
|
202
|
+
"javascript": _resolve_ts_js,
|
|
203
|
+
"rust": _resolve_rust,
|
|
204
|
+
"cpp": _resolve_cpp,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
# Graph construction helpers
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _build_reverse_edges(edges: dict[str, list[str]]) -> dict[str, list[str]]:
|
|
214
|
+
reverse: dict[str, list[str]] = {}
|
|
215
|
+
for src, deps in edges.items():
|
|
216
|
+
for dep in deps:
|
|
217
|
+
reverse.setdefault(dep, []).append(src)
|
|
218
|
+
# Sort for determinism.
|
|
219
|
+
return {k: sorted(v) for k, v in reverse.items()}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _detect_cycles(edges: dict[str, list[str]]) -> list[list[str]]:
|
|
223
|
+
"""Iterative DFS with white/gray/black coloring — no recursion."""
|
|
224
|
+
WHITE, GRAY, BLACK = 0, 1, 2
|
|
225
|
+
all_nodes = set(edges.keys()) | {dep for deps in edges.values() for dep in deps}
|
|
226
|
+
color: dict[str, int] = {n: WHITE for n in all_nodes}
|
|
227
|
+
|
|
228
|
+
found: list[list[str]] = []
|
|
229
|
+
seen_canonical: set[tuple[str, ...]] = set()
|
|
230
|
+
|
|
231
|
+
for start in sorted(all_nodes):
|
|
232
|
+
if color[start] != WHITE:
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
path_stack: list[str] = [start]
|
|
236
|
+
dfs_stack: list[tuple[str, int, list[str]]] = [(start, 0, edges.get(start, []))]
|
|
237
|
+
color[start] = GRAY
|
|
238
|
+
|
|
239
|
+
while dfs_stack:
|
|
240
|
+
node, idx, neighbors = dfs_stack[-1]
|
|
241
|
+
|
|
242
|
+
if idx < len(neighbors):
|
|
243
|
+
dfs_stack[-1] = (node, idx + 1, neighbors)
|
|
244
|
+
neighbor = neighbors[idx]
|
|
245
|
+
|
|
246
|
+
if color.get(neighbor, WHITE) == GRAY:
|
|
247
|
+
# Back edge — reconstruct cycle from path_stack.
|
|
248
|
+
cycle_start = path_stack.index(neighbor)
|
|
249
|
+
cycle = path_stack[cycle_start:] + [neighbor]
|
|
250
|
+
canonical = _canonical_cycle(cycle)
|
|
251
|
+
if canonical not in seen_canonical:
|
|
252
|
+
seen_canonical.add(canonical)
|
|
253
|
+
found.append(cycle)
|
|
254
|
+
|
|
255
|
+
elif color.get(neighbor, WHITE) == WHITE:
|
|
256
|
+
color[neighbor] = GRAY
|
|
257
|
+
path_stack.append(neighbor)
|
|
258
|
+
dfs_stack.append((neighbor, 0, edges.get(neighbor, [])))
|
|
259
|
+
else:
|
|
260
|
+
dfs_stack.pop()
|
|
261
|
+
path_stack.pop()
|
|
262
|
+
color[node] = BLACK
|
|
263
|
+
|
|
264
|
+
return found
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _canonical_cycle(cycle: list[str]) -> tuple[str, ...]:
|
|
268
|
+
"""Return a rotation of the cycle starting at the lexicographically smallest node."""
|
|
269
|
+
body = cycle[:-1] # drop the closing duplicate
|
|
270
|
+
min_idx = body.index(min(body))
|
|
271
|
+
rotated = body[min_idx:] + body[:min_idx]
|
|
272
|
+
return tuple(rotated)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
# Public entry point
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def build_graph(
|
|
281
|
+
files: list[SourceFile],
|
|
282
|
+
imports_by_file: dict[str, list[str]],
|
|
283
|
+
) -> DependencyGraph:
|
|
284
|
+
"""Build a dependency graph from discovered files and their raw imports.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
files: All source files discovered in the project.
|
|
288
|
+
imports_by_file: Maps each file's rel_path string to the raw import
|
|
289
|
+
strings extracted from that file.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
A fully populated DependencyGraph with backend="python-fallback".
|
|
293
|
+
"""
|
|
294
|
+
known: dict[str, SourceFile] = {
|
|
295
|
+
str(f.rel_path).replace("\\", "/"): f for f in files
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
edges: dict[str, list[str]] = {}
|
|
299
|
+
for f in files:
|
|
300
|
+
key = str(f.rel_path).replace("\\", "/")
|
|
301
|
+
resolver = _RESOLVERS[f.language]
|
|
302
|
+
resolved: set[str] = set()
|
|
303
|
+
for raw in imports_by_file.get(key, []):
|
|
304
|
+
target = resolver(raw, f, known)
|
|
305
|
+
if target is not None and target != key:
|
|
306
|
+
resolved.add(target)
|
|
307
|
+
edges[key] = sorted(resolved)
|
|
308
|
+
|
|
309
|
+
reverse_edges = _build_reverse_edges(edges)
|
|
310
|
+
cycles = _detect_cycles(edges)
|
|
311
|
+
|
|
312
|
+
return DependencyGraph(
|
|
313
|
+
nodes=files,
|
|
314
|
+
edges=edges,
|
|
315
|
+
reverse_edges=reverse_edges,
|
|
316
|
+
cycles=cycles,
|
|
317
|
+
backend="python-fallback",
|
|
318
|
+
)
|