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/ui/theme.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from rich.theme import Theme
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class UIStyles:
|
|
12
|
+
success: str = "green"
|
|
13
|
+
warning: str = "yellow"
|
|
14
|
+
error: str = "red"
|
|
15
|
+
info: str = "blue"
|
|
16
|
+
muted: str = "dim"
|
|
17
|
+
heading: str = "bold"
|
|
18
|
+
command: str = "bold blue"
|
|
19
|
+
dry_run: str = "bold yellow"
|
|
20
|
+
path: str = "cyan"
|
|
21
|
+
line_number: str = "dim"
|
|
22
|
+
border: str = "dim"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_rich_theme(styles: UIStyles) -> Theme:
|
|
26
|
+
return Theme(
|
|
27
|
+
{
|
|
28
|
+
"success": styles.success,
|
|
29
|
+
"warning": styles.warning,
|
|
30
|
+
"error": styles.error,
|
|
31
|
+
"info": styles.info,
|
|
32
|
+
"muted": styles.muted,
|
|
33
|
+
"heading": styles.heading,
|
|
34
|
+
"command": styles.command,
|
|
35
|
+
"dry_run": styles.dry_run,
|
|
36
|
+
"path": styles.path,
|
|
37
|
+
"line_number": styles.line_number,
|
|
38
|
+
"border": styles.border,
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
THEMES = {
|
|
44
|
+
"standard": UIStyles(),
|
|
45
|
+
"monochrome": UIStyles(
|
|
46
|
+
success="bold white",
|
|
47
|
+
warning="white",
|
|
48
|
+
error="bold white",
|
|
49
|
+
info="white",
|
|
50
|
+
muted="dim",
|
|
51
|
+
heading="bold",
|
|
52
|
+
command="bold",
|
|
53
|
+
dry_run="bold",
|
|
54
|
+
path="white",
|
|
55
|
+
),
|
|
56
|
+
"vibrant": UIStyles(
|
|
57
|
+
success="bold bright_green",
|
|
58
|
+
warning="bold bright_yellow",
|
|
59
|
+
error="bold bright_red",
|
|
60
|
+
info="bold bright_blue",
|
|
61
|
+
command="bold bright_magenta",
|
|
62
|
+
dry_run="bold bright_yellow",
|
|
63
|
+
path="bright_cyan",
|
|
64
|
+
),
|
|
65
|
+
"ocean": UIStyles(
|
|
66
|
+
success="green",
|
|
67
|
+
warning="yellow",
|
|
68
|
+
error="red",
|
|
69
|
+
info="blue",
|
|
70
|
+
command="cyan",
|
|
71
|
+
dry_run="bright_blue",
|
|
72
|
+
path="bright_cyan",
|
|
73
|
+
heading="bold blue",
|
|
74
|
+
),
|
|
75
|
+
"dracula": UIStyles(
|
|
76
|
+
success="#50fa7b",
|
|
77
|
+
warning="#f1fa8c",
|
|
78
|
+
error="#ff5555",
|
|
79
|
+
info="#8be9fd",
|
|
80
|
+
command="#bd93f9",
|
|
81
|
+
dry_run="#f1fa8c",
|
|
82
|
+
path="#8be9fd",
|
|
83
|
+
heading="bold #ff79c6",
|
|
84
|
+
muted="#6272a4",
|
|
85
|
+
border="#6272a4",
|
|
86
|
+
),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(slots=True, frozen=True)
|
|
91
|
+
class SettingDefinition:
|
|
92
|
+
key: str
|
|
93
|
+
description: str
|
|
94
|
+
kind: str
|
|
95
|
+
default: str | bool
|
|
96
|
+
choices: tuple[str, ...] = ()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
SETTING_DEFINITIONS = {
|
|
100
|
+
"theme": SettingDefinition(
|
|
101
|
+
key="theme",
|
|
102
|
+
description="Terminal theme for Rich output.",
|
|
103
|
+
kind="choice",
|
|
104
|
+
default="standard",
|
|
105
|
+
choices=tuple(THEMES),
|
|
106
|
+
),
|
|
107
|
+
"approval_mode": SettingDefinition(
|
|
108
|
+
key="approval_mode",
|
|
109
|
+
description="Default approval mode for chat tool calls.",
|
|
110
|
+
kind="choice",
|
|
111
|
+
default="ask",
|
|
112
|
+
choices=("ask", "auto-readonly", "auto-all"),
|
|
113
|
+
),
|
|
114
|
+
"startup_help": SettingDefinition(
|
|
115
|
+
key="startup_help",
|
|
116
|
+
description="Show slash-command guidance when chat starts.",
|
|
117
|
+
kind="bool",
|
|
118
|
+
default=True,
|
|
119
|
+
),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _coerce_bool(value: object) -> bool:
|
|
124
|
+
if isinstance(value, bool):
|
|
125
|
+
return value
|
|
126
|
+
if isinstance(value, str):
|
|
127
|
+
normalized = value.strip().lower()
|
|
128
|
+
if normalized in {"1", "true", "t", "yes", "y", "on"}:
|
|
129
|
+
return True
|
|
130
|
+
if normalized in {"0", "false", "f", "no", "n", "off"}:
|
|
131
|
+
return False
|
|
132
|
+
raise ValueError("Expected a boolean value: true/false, yes/no, or on/off.")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class SettingsManager:
|
|
136
|
+
_instance: SettingsManager | None = None
|
|
137
|
+
_current_theme_name: str = "standard"
|
|
138
|
+
_approval_mode_name: str = "ask"
|
|
139
|
+
_startup_help: bool = True
|
|
140
|
+
_config_path: Path = Path.home() / ".rpr" / "settings.json"
|
|
141
|
+
|
|
142
|
+
def __new__(cls):
|
|
143
|
+
if cls._instance is None:
|
|
144
|
+
cls._instance = super(SettingsManager, cls).__new__(cls)
|
|
145
|
+
cls._instance._load_settings()
|
|
146
|
+
return cls._instance
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def definitions() -> dict[str, SettingDefinition]:
|
|
150
|
+
return SETTING_DEFINITIONS
|
|
151
|
+
|
|
152
|
+
def _default_data(self) -> dict[str, str | bool]:
|
|
153
|
+
return {
|
|
154
|
+
key: definition.default for key, definition in SETTING_DEFINITIONS.items()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
def _load_settings(self):
|
|
158
|
+
data = self._default_data()
|
|
159
|
+
if self._config_path.exists():
|
|
160
|
+
try:
|
|
161
|
+
loaded = json.loads(self._config_path.read_text())
|
|
162
|
+
except json.JSONDecodeError, OSError:
|
|
163
|
+
loaded = {}
|
|
164
|
+
if isinstance(loaded, dict):
|
|
165
|
+
data.update(loaded)
|
|
166
|
+
|
|
167
|
+
self._current_theme_name = self._coerce_value(
|
|
168
|
+
"theme", data.get("theme", "standard")
|
|
169
|
+
)
|
|
170
|
+
self._approval_mode_name = self._coerce_value(
|
|
171
|
+
"approval_mode",
|
|
172
|
+
data.get("approval_mode", "ask"),
|
|
173
|
+
)
|
|
174
|
+
self._startup_help = self._coerce_value(
|
|
175
|
+
"startup_help", data.get("startup_help", True)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def _save_settings(self):
|
|
179
|
+
self._config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
try:
|
|
181
|
+
self._config_path.write_text(
|
|
182
|
+
json.dumps(
|
|
183
|
+
{
|
|
184
|
+
"theme": self._current_theme_name,
|
|
185
|
+
"approval_mode": self._approval_mode_name,
|
|
186
|
+
"startup_help": self._startup_help,
|
|
187
|
+
},
|
|
188
|
+
indent=2,
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
except OSError:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
def _coerce_value(self, key: str, value: object) -> str | bool:
|
|
195
|
+
definition = SETTING_DEFINITIONS[key]
|
|
196
|
+
if definition.kind == "bool":
|
|
197
|
+
return _coerce_bool(value)
|
|
198
|
+
|
|
199
|
+
if not isinstance(value, str):
|
|
200
|
+
raise ValueError(f"Expected a string value for {key}.")
|
|
201
|
+
|
|
202
|
+
if definition.choices and value not in definition.choices:
|
|
203
|
+
allowed = ", ".join(definition.choices)
|
|
204
|
+
if key == "theme":
|
|
205
|
+
raise ValueError(f"Unknown theme: {value}")
|
|
206
|
+
raise ValueError(f"Unknown value for {key}: {value}. Valid: {allowed}")
|
|
207
|
+
return value
|
|
208
|
+
|
|
209
|
+
def get_value(self, key: str) -> str | bool:
|
|
210
|
+
if key not in SETTING_DEFINITIONS:
|
|
211
|
+
raise KeyError(key)
|
|
212
|
+
if key == "theme":
|
|
213
|
+
return self._current_theme_name
|
|
214
|
+
if key == "approval_mode":
|
|
215
|
+
return self._approval_mode_name
|
|
216
|
+
if key == "startup_help":
|
|
217
|
+
return self._startup_help
|
|
218
|
+
raise KeyError(key)
|
|
219
|
+
|
|
220
|
+
def set_value(self, key: str, value: object) -> str | bool:
|
|
221
|
+
if key not in SETTING_DEFINITIONS:
|
|
222
|
+
raise KeyError(key)
|
|
223
|
+
|
|
224
|
+
coerced = self._coerce_value(key, value)
|
|
225
|
+
if key == "theme":
|
|
226
|
+
self._current_theme_name = coerced
|
|
227
|
+
elif key == "approval_mode":
|
|
228
|
+
self._approval_mode_name = coerced
|
|
229
|
+
elif key == "startup_help":
|
|
230
|
+
self._startup_help = coerced
|
|
231
|
+
else:
|
|
232
|
+
raise KeyError(key)
|
|
233
|
+
|
|
234
|
+
self._save_settings()
|
|
235
|
+
return coerced
|
|
236
|
+
|
|
237
|
+
def describe_settings(self) -> list[tuple[SettingDefinition, str]]:
|
|
238
|
+
rows: list[tuple[SettingDefinition, str]] = []
|
|
239
|
+
for key, definition in SETTING_DEFINITIONS.items():
|
|
240
|
+
value = self.get_value(key)
|
|
241
|
+
if isinstance(value, bool):
|
|
242
|
+
display = "true" if value else "false"
|
|
243
|
+
else:
|
|
244
|
+
display = value
|
|
245
|
+
rows.append((definition, display))
|
|
246
|
+
return rows
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def current_theme_name(self) -> str:
|
|
250
|
+
return self._current_theme_name
|
|
251
|
+
|
|
252
|
+
@current_theme_name.setter
|
|
253
|
+
def current_theme_name(self, name: str):
|
|
254
|
+
self.set_value("theme", name)
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def approval_mode_name(self) -> str:
|
|
258
|
+
return self._approval_mode_name
|
|
259
|
+
|
|
260
|
+
@approval_mode_name.setter
|
|
261
|
+
def approval_mode_name(self, name: str) -> None:
|
|
262
|
+
self.set_value("approval_mode", name)
|
|
263
|
+
|
|
264
|
+
@property
|
|
265
|
+
def approval_mode(self):
|
|
266
|
+
from rpr.agent.approval import ApprovalMode
|
|
267
|
+
|
|
268
|
+
return ApprovalMode(self._approval_mode_name)
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def startup_help(self) -> bool:
|
|
272
|
+
return self._startup_help
|
|
273
|
+
|
|
274
|
+
@startup_help.setter
|
|
275
|
+
def startup_help(self, enabled: bool) -> None:
|
|
276
|
+
self.set_value("startup_help", enabled)
|
|
277
|
+
|
|
278
|
+
def get_theme(self) -> Theme:
|
|
279
|
+
return create_rich_theme(THEMES[self._current_theme_name])
|
|
280
|
+
|
|
281
|
+
def get_styles(self) -> UIStyles:
|
|
282
|
+
return THEMES[self._current_theme_name]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
settings_manager = SettingsManager()
|
|
286
|
+
theme_manager = settings_manager
|
rpr/workspace.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from rpr.context import RunContext
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
DEFAULT_NPM_WORKSPACES = ["apps/*", "packages/node/*"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def add_workspace_member(ctx: RunContext, member: str) -> None:
|
|
16
|
+
"""Add a UV workspace member to the root pyproject.toml."""
|
|
17
|
+
pyproject_path = ctx.project_dir / "pyproject.toml"
|
|
18
|
+
if ctx.dry_run:
|
|
19
|
+
console.print(
|
|
20
|
+
f"[bold yellow][DRY RUN][/] Would add '{member}' to workspace members in {pyproject_path}",
|
|
21
|
+
soft_wrap=True,
|
|
22
|
+
)
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
import tomllib
|
|
26
|
+
|
|
27
|
+
with open(pyproject_path, "rb") as f:
|
|
28
|
+
data = tomllib.load(f)
|
|
29
|
+
|
|
30
|
+
members = data.get("tool", {}).get("uv", {}).get("workspace", {}).get("members", [])
|
|
31
|
+
if member in members:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
members.append(member)
|
|
35
|
+
members_str = ", ".join(f'"{m}"' for m in members)
|
|
36
|
+
new_members_line = f"members = [{members_str}]"
|
|
37
|
+
|
|
38
|
+
content = pyproject_path.read_text()
|
|
39
|
+
content = re.sub(
|
|
40
|
+
r"members\s*=\s*\[.*?\]", new_members_line, content, flags=re.DOTALL
|
|
41
|
+
)
|
|
42
|
+
pyproject_path.write_text(content)
|
|
43
|
+
console.print(f"[bold green]Updated:[/] Added '{member}' to workspace members")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def derive_variables(project_name: str) -> dict[str, str]:
|
|
47
|
+
"""Derive the four canonical token variants from the raw project name."""
|
|
48
|
+
name_underscore = project_name.replace("-", "_")
|
|
49
|
+
pascal = "".join(w.capitalize() for w in re.split(r"[-_]+", project_name) if w)
|
|
50
|
+
return {
|
|
51
|
+
"name": project_name,
|
|
52
|
+
"name-of-app": project_name,
|
|
53
|
+
"name_underscore": name_underscore,
|
|
54
|
+
"NamePascal": pascal,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def write_package_json(
|
|
59
|
+
ctx: RunContext,
|
|
60
|
+
path: Path,
|
|
61
|
+
*,
|
|
62
|
+
defaults: dict[str, object] | None = None,
|
|
63
|
+
dependencies: dict[str, str] | None = None,
|
|
64
|
+
dev_dependencies: dict[str, str] | None = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Create or update a package.json while preserving existing package pins."""
|
|
67
|
+
package: dict[str, object]
|
|
68
|
+
existed = path.exists()
|
|
69
|
+
|
|
70
|
+
if existed:
|
|
71
|
+
try:
|
|
72
|
+
package = json.loads(path.read_text())
|
|
73
|
+
except json.JSONDecodeError:
|
|
74
|
+
package = {}
|
|
75
|
+
else:
|
|
76
|
+
package = {}
|
|
77
|
+
|
|
78
|
+
for key, value in (defaults or {}).items():
|
|
79
|
+
package.setdefault(key, value)
|
|
80
|
+
|
|
81
|
+
for field_name, additions in (
|
|
82
|
+
("dependencies", dependencies or {}),
|
|
83
|
+
("devDependencies", dev_dependencies or {}),
|
|
84
|
+
):
|
|
85
|
+
field = package.setdefault(field_name, {})
|
|
86
|
+
if not isinstance(field, dict):
|
|
87
|
+
field = {}
|
|
88
|
+
package[field_name] = field
|
|
89
|
+
for dep_name, version in additions.items():
|
|
90
|
+
field.setdefault(dep_name, version)
|
|
91
|
+
|
|
92
|
+
content = json.dumps(package, indent=2) + "\n"
|
|
93
|
+
if existed:
|
|
94
|
+
ctx.update_file(path, content)
|
|
95
|
+
else:
|
|
96
|
+
ctx.write_file(path, content)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def ensure_npm_workspaces(ctx: RunContext, *, package_name: str | None = None) -> None:
|
|
100
|
+
"""Ensure the root package.json declares the standard npm workspace globs."""
|
|
101
|
+
package_path = ctx.project_dir / "package.json"
|
|
102
|
+
package: dict[str, object]
|
|
103
|
+
existed = package_path.exists()
|
|
104
|
+
|
|
105
|
+
if existed:
|
|
106
|
+
try:
|
|
107
|
+
package = json.loads(package_path.read_text())
|
|
108
|
+
except json.JSONDecodeError:
|
|
109
|
+
package = {}
|
|
110
|
+
else:
|
|
111
|
+
package = {}
|
|
112
|
+
|
|
113
|
+
if package_name:
|
|
114
|
+
package.setdefault("name", package_name)
|
|
115
|
+
package["private"] = True
|
|
116
|
+
|
|
117
|
+
workspaces = package.get("workspaces", [])
|
|
118
|
+
if not isinstance(workspaces, list):
|
|
119
|
+
workspaces = []
|
|
120
|
+
|
|
121
|
+
for workspace in DEFAULT_NPM_WORKSPACES:
|
|
122
|
+
if workspace not in workspaces:
|
|
123
|
+
workspaces.append(workspace)
|
|
124
|
+
|
|
125
|
+
package["workspaces"] = workspaces
|
|
126
|
+
|
|
127
|
+
content = json.dumps(package, indent=2) + "\n"
|
|
128
|
+
if existed:
|
|
129
|
+
ctx.update_file(package_path, content)
|
|
130
|
+
else:
|
|
131
|
+
ctx.write_file(package_path, content)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rpr-cli
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: RPR CLI -- React Python Rust monorepo scaffolding orchestrator
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.14
|
|
8
|
+
Requires-Dist: jinja2>=3.1.5
|
|
9
|
+
Requires-Dist: pathspec>=1.0.4
|
|
10
|
+
Requires-Dist: prompt-toolkit>=3.0.52
|
|
11
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
12
|
+
Requires-Dist: rich>=14.3.3
|
|
13
|
+
Requires-Dist: typer>=0.24.1
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# RPR CLI
|
|
17
|
+
|
|
18
|
+
React Python Rust monorepo scaffolding orchestrator.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Published package name: `rpr-cli`
|
|
23
|
+
|
|
24
|
+
Once published to PyPI, install by name with either:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv add rpr-cli
|
|
28
|
+
uv tool install rpr-cli
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Both install the `rpr` command.
|
|
32
|
+
|
|
33
|
+
For development, editable install is fine:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv tool install -e .
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For an actual distributable install you can use in other projects, build the wheel first and install the built artifact:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv build
|
|
43
|
+
uv tool install dist/rpr_cli-*.whl
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
From another local project you can also install the built artifact directly:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv add ../project-getting-started/dist/rpr_cli-*.whl
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
### 🚀 Scaffolding & Generation
|
|
55
|
+
|
|
56
|
+
Scaffold a complete monorepo or add new apps and libraries.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
rpr init my-app # Scaffold a new monorepo (NX + UV)
|
|
60
|
+
rpr generate domain my-app # Add a domain package (shared logic & types)
|
|
61
|
+
rpr generate api my-app # Add a FastAPI application
|
|
62
|
+
rpr generate ui my-app # Add a React UI library + application
|
|
63
|
+
rpr generate engine my-app # Add a Rust/PyO3 computation engine
|
|
64
|
+
rpr generate storybook # Add Storybook for UI component development
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 🧠 AI Instructions & Code Templates
|
|
68
|
+
|
|
69
|
+
Sync project context to your favorite AI tools and add common code patterns.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
rpr sync rules # Sync AI instructions to Cursor, Copilot, Claude Code, and Gemini
|
|
73
|
+
rpr add template mapper_util # Add a common code template to your project
|
|
74
|
+
rpr add template --list # List all available code templates
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 💬 Interactive Shell
|
|
78
|
+
|
|
79
|
+
`rpr chat` is the primary interactive shell for the product. It runs a Rich-based session with slash commands for both chat controls and existing CLI workflows.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
rpr chat # Start the interactive shell
|
|
83
|
+
rpr chat --approval-mode auto-readonly # Override the session approval mode
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Inside the shell:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
/help List supported slash commands
|
|
90
|
+
/settings Inspect and update persisted settings in-shell
|
|
91
|
+
/init Run workspace bootstrap from chat
|
|
92
|
+
/generate Run scaffolding commands from chat
|
|
93
|
+
/sync Sync AI instructions from chat
|
|
94
|
+
/add Add a registered template from chat
|
|
95
|
+
/check Run workspace health checks from chat
|
|
96
|
+
/map Generate a code map from chat
|
|
97
|
+
/exit End the session
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
There is no separate `--tui` mode. The unified shell is the default chat experience.
|
|
101
|
+
|
|
102
|
+
### 🛠️ Maintenance & Insights
|
|
103
|
+
|
|
104
|
+
Keep your project healthy and explore its structure.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
rpr check # Inspect project health and report setup status
|
|
108
|
+
rpr map # Generate a structured code map of your project
|
|
109
|
+
rpr settings # Inspect persisted shell settings
|
|
110
|
+
rpr settings set theme ocean # Update the terminal theme
|
|
111
|
+
rpr settings set approval_mode auto-all
|
|
112
|
+
rpr settings set startup_help false
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Persisted settings currently include:
|
|
116
|
+
|
|
117
|
+
- `theme`: Rich console theme
|
|
118
|
+
- `approval_mode`: default chat approval mode (`ask`, `auto-readonly`, `auto-all`)
|
|
119
|
+
- `startup_help`: whether chat shows slash-command guidance on startup
|
|
120
|
+
|
|
121
|
+
### ⚙️ Common Flags
|
|
122
|
+
|
|
123
|
+
Most commands support these global flags:
|
|
124
|
+
|
|
125
|
+
- `--dry-run`: Preview changes without writing any files.
|
|
126
|
+
- `--help`: Show detailed help for any command or subcommand.
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
rpr generate api my-app --dry-run
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The `check` command also supports:
|
|
135
|
+
|
|
136
|
+
- `--json`: Output machine-readable JSON for CI/CD integration.
|
|
137
|
+
|
|
138
|
+
The `chat` command also supports:
|
|
139
|
+
|
|
140
|
+
- `--provider`: Override the AI provider. The mock provider is the only supported provider today.
|
|
141
|
+
- `--model`: Override the active model name.
|
|
142
|
+
- `--approval-mode`: Override the session approval mode without changing persisted settings.
|
|
143
|
+
|
|
144
|
+
### Project Development
|
|
145
|
+
|
|
146
|
+
#### Setup
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# Install Python dependencies
|
|
150
|
+
uv sync
|
|
151
|
+
|
|
152
|
+
# Build the Rust extension (rpr_parser) — required for rpr map acceleration
|
|
153
|
+
cd rpr-parser && uv run maturin develop && cd ..
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
> **Note:** `uv tool install dist/rpr-*.whl` uses the root hatchling build backend and does **not** build the
|
|
157
|
+
> Rust extension. You must run `maturin develop` separately before using `rpr map` with the Rust
|
|
158
|
+
> backend. Without it, `rpr map` falls back to the pure-Python implementation automatically.
|
|
159
|
+
|
|
160
|
+
#### Running Tests
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# Full test suite (Python)
|
|
164
|
+
uv run pytest
|
|
165
|
+
|
|
166
|
+
# Single test file
|
|
167
|
+
uv run pytest tests/test_map_extractor.py
|
|
168
|
+
|
|
169
|
+
# Single test
|
|
170
|
+
uv run pytest tests/test_map_extractor.py::test_fallback_returns_correct_shape
|
|
171
|
+
|
|
172
|
+
# Rust unit tests (rpr_parser crate)
|
|
173
|
+
cd rpr-parser && cargo test && cd ..
|
|
174
|
+
|
|
175
|
+
# Lint and format
|
|
176
|
+
uv run ruff check .
|
|
177
|
+
uv run ruff format .
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The Rust-path tests in `tests/test_map_extractor.py` are automatically skipped if the Rust
|
|
181
|
+
extension hasn't been built yet — run `maturin develop` first to enable them.
|
|
182
|
+
|
|
183
|
+
### Publishing To PyPI
|
|
184
|
+
|
|
185
|
+
This repo is set up to publish the package `rpr-cli` to PyPI from GitHub Actions.
|
|
186
|
+
|
|
187
|
+
One-time setup:
|
|
188
|
+
|
|
189
|
+
1. Create the PyPI project `rpr-cli`.
|
|
190
|
+
2. Add a repository secret named `PYPI_API_TOKEN` with an API token that can upload that project.
|
|
191
|
+
|
|
192
|
+
Release flow:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# update src/rpr/__init__.py with the new version
|
|
196
|
+
git commit -am "Release 0.1.2"
|
|
197
|
+
git tag v0.1.2
|
|
198
|
+
git push origin main --tags
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The workflow in `.github/workflows/publish-pypi.yml` builds the sdist and wheel, checks that the tag matches the package version, and uploads `dist/*` to PyPI.
|