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.
Files changed (97) hide show
  1. rpr/__init__.py +1 -0
  2. rpr/agent/__init__.py +29 -0
  3. rpr/agent/approval.py +19 -0
  4. rpr/agent/bootstrap.py +60 -0
  5. rpr/agent/client.py +115 -0
  6. rpr/agent/control.py +16 -0
  7. rpr/agent/mock.py +158 -0
  8. rpr/agent/runtime.py +291 -0
  9. rpr/agent/session.py +91 -0
  10. rpr/agent/tools/__init__.py +10 -0
  11. rpr/agent/tools/base.py +62 -0
  12. rpr/agent/tools/mutating.py +47 -0
  13. rpr/agent/tools/readonly.py +147 -0
  14. rpr/agent/tools/registry.py +30 -0
  15. rpr/application/__init__.py +1 -0
  16. rpr/application/catalog.py +427 -0
  17. rpr/application/chat_service.py +413 -0
  18. rpr/application/checks.py +49 -0
  19. rpr/application/cli_adapter.py +36 -0
  20. rpr/application/completer.py +163 -0
  21. rpr/application/conversation_service.py +92 -0
  22. rpr/application/prompt_service.py +85 -0
  23. rpr/application/selector.py +240 -0
  24. rpr/application/shell.py +543 -0
  25. rpr/checks/__init__.py +0 -0
  26. rpr/checks/base.py +13 -0
  27. rpr/checks/instructions.py +89 -0
  28. rpr/checks/packages.py +619 -0
  29. rpr/checks/workspace.py +170 -0
  30. rpr/cli.py +78 -0
  31. rpr/commands/__init__.py +0 -0
  32. rpr/commands/add.py +166 -0
  33. rpr/commands/chat.py +48 -0
  34. rpr/commands/check.py +53 -0
  35. rpr/commands/generate/__init__.py +0 -0
  36. rpr/commands/generate/api.py +228 -0
  37. rpr/commands/generate/domain.py +383 -0
  38. rpr/commands/generate/engine.py +148 -0
  39. rpr/commands/generate/storybook.py +442 -0
  40. rpr/commands/generate/ui.py +414 -0
  41. rpr/commands/init.py +822 -0
  42. rpr/commands/map.py +113 -0
  43. rpr/commands/settings.py +102 -0
  44. rpr/commands/sync.py +97 -0
  45. rpr/context.py +203 -0
  46. rpr/generators/__init__.py +0 -0
  47. rpr/generators/base.py +110 -0
  48. rpr/generators/claude.py +33 -0
  49. rpr/generators/copilot.py +36 -0
  50. rpr/generators/cursor.py +38 -0
  51. rpr/generators/gemini.py +33 -0
  52. rpr/map/__init__.py +0 -0
  53. rpr/map/architecture.py +495 -0
  54. rpr/map/chains.py +317 -0
  55. rpr/map/classifier.py +170 -0
  56. rpr/map/coverage.py +200 -0
  57. rpr/map/dependencies.py +243 -0
  58. rpr/map/extractor.py +223 -0
  59. rpr/map/graph.py +318 -0
  60. rpr/map/output.py +1030 -0
  61. rpr/map/responsibility.py +345 -0
  62. rpr/map/topology.py +327 -0
  63. rpr/map/walker.py +151 -0
  64. rpr/scaffolds/domain/base_entity.md +30 -0
  65. rpr/scaffolds/domain/base_repo.md +48 -0
  66. rpr/scaffolds/domain/container.md +76 -0
  67. rpr/scaffolds/domain/settings.md +57 -0
  68. rpr/scaffolds/instructions/all.instructions.md +50 -0
  69. rpr/scaffolds/instructions/api.instructions.md +42 -0
  70. rpr/scaffolds/instructions/domain.instructions.md +93 -0
  71. rpr/scaffolds/instructions/frontend.instructions.md +97 -0
  72. rpr/scaffolds/instructions/rust-engine.instructions.md +40 -0
  73. rpr/scaffolds/instructions/setup-guide.instructions.md +86 -0
  74. rpr/scaffolds/instructions/tooling-setup.instructions.md +97 -0
  75. rpr/scaffolds/instructions/tooling.instructions.md +42 -0
  76. rpr/scaffolds/js_special_files/fetch.service.md +222 -0
  77. rpr/scaffolds/js_special_files/sticky-navigation.md +164 -0
  78. rpr/scaffolds/special_files/domain_container.md +76 -0
  79. rpr/scaffolds/special_files/domain_settings.md +57 -0
  80. rpr/scaffolds/special_files/dto_util.md +62 -0
  81. rpr/scaffolds/special_files/encrypted_column.md +98 -0
  82. rpr/scaffolds/special_files/mapper_util.md +159 -0
  83. rpr/scaffolds/special_files/partial_update.md +61 -0
  84. rpr/templates/__init__.py +0 -0
  85. rpr/templates/registry.py +81 -0
  86. rpr/ui/__init__.py +0 -0
  87. rpr/ui/console.py +32 -0
  88. rpr/ui/markdown.py +59 -0
  89. rpr/ui/prompt_session.py +430 -0
  90. rpr/ui/renderers.py +167 -0
  91. rpr/ui/theme.py +286 -0
  92. rpr/workspace.py +131 -0
  93. rpr_cli-0.1.1.dist-info/METADATA +201 -0
  94. rpr_cli-0.1.1.dist-info/RECORD +97 -0
  95. rpr_cli-0.1.1.dist-info/WHEEL +4 -0
  96. rpr_cli-0.1.1.dist-info/entry_points.txt +2 -0
  97. 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
+ )