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/application/shell.py
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import shlex
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import ClassVar, Protocol
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from rpr.agent.bootstrap import ChatRuntimeBundle
|
|
12
|
+
from rpr.agent.control import TurnControl
|
|
13
|
+
from rpr.application.catalog import COMMAND_CATALOG, chat_commands, usage_string
|
|
14
|
+
from rpr.application.checks import collect_check_report
|
|
15
|
+
from rpr.ui.theme import SETTING_DEFINITIONS, settings_manager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ShellArgumentParser(argparse.ArgumentParser):
|
|
19
|
+
def error(self, message: str) -> None:
|
|
20
|
+
raise ValueError(message)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ShellPresenter(Protocol):
|
|
24
|
+
def flush(self) -> None: ...
|
|
25
|
+
|
|
26
|
+
def heading(self, text: str) -> None: ...
|
|
27
|
+
|
|
28
|
+
def info(self, message: str) -> None: ...
|
|
29
|
+
|
|
30
|
+
def success(self, message: str) -> None: ...
|
|
31
|
+
|
|
32
|
+
def warning(self, message: str) -> None: ...
|
|
33
|
+
|
|
34
|
+
def error(self, message: str) -> None: ...
|
|
35
|
+
|
|
36
|
+
def table(self, title: str, columns: list[str], rows: list[list[str]]) -> None: ...
|
|
37
|
+
|
|
38
|
+
def status_row(
|
|
39
|
+
self,
|
|
40
|
+
icon_char: str,
|
|
41
|
+
area: str,
|
|
42
|
+
path: str,
|
|
43
|
+
status: bool,
|
|
44
|
+
suggestion: str | None = None,
|
|
45
|
+
) -> None: ...
|
|
46
|
+
|
|
47
|
+
def on_setting_changed(self, key: str) -> None: ...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ShellInputService(Protocol):
|
|
51
|
+
async def prompt(self, label: str, *, default: str | None = None) -> str: ...
|
|
52
|
+
|
|
53
|
+
async def confirm(self, label: str, *, default: bool = False) -> bool: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(slots=True, frozen=True)
|
|
57
|
+
class SlashCommandSpec:
|
|
58
|
+
name: str
|
|
59
|
+
usage: str
|
|
60
|
+
description: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(slots=True, frozen=True)
|
|
64
|
+
class ParsedSlashCommand:
|
|
65
|
+
name: str
|
|
66
|
+
args: list[str]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CommandShellService:
|
|
70
|
+
_instance: ClassVar[CommandShellService | None] = None
|
|
71
|
+
|
|
72
|
+
def __init__(self) -> None:
|
|
73
|
+
self._bundle: ChatRuntimeBundle | None = None
|
|
74
|
+
self._presenter: ShellPresenter | None = None
|
|
75
|
+
self._prompts: ShellInputService | None = None
|
|
76
|
+
self._command_specs = {
|
|
77
|
+
cmd.name: SlashCommandSpec(
|
|
78
|
+
name=cmd.name,
|
|
79
|
+
usage=usage_string(cmd),
|
|
80
|
+
description=cmd.description,
|
|
81
|
+
)
|
|
82
|
+
for cmd in chat_commands(COMMAND_CATALOG)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def instance(cls) -> CommandShellService:
|
|
87
|
+
if cls._instance is None:
|
|
88
|
+
cls._instance = cls()
|
|
89
|
+
return cls._instance
|
|
90
|
+
|
|
91
|
+
def configure(
|
|
92
|
+
self,
|
|
93
|
+
*,
|
|
94
|
+
bundle: ChatRuntimeBundle,
|
|
95
|
+
presenter: ShellPresenter,
|
|
96
|
+
prompts: ShellInputService,
|
|
97
|
+
) -> None:
|
|
98
|
+
self._bundle = bundle
|
|
99
|
+
self._presenter = presenter
|
|
100
|
+
self._prompts = prompts
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def command_specs(self) -> list[SlashCommandSpec]:
|
|
104
|
+
return [self._command_specs[name] for name in sorted(self._command_specs)]
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def _active_bundle(self) -> ChatRuntimeBundle:
|
|
108
|
+
if self._bundle is None:
|
|
109
|
+
raise RuntimeError("Command shell service is not configured.")
|
|
110
|
+
return self._bundle
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def _active_presenter(self) -> ShellPresenter:
|
|
114
|
+
if self._presenter is None:
|
|
115
|
+
raise RuntimeError("Command shell service is not configured.")
|
|
116
|
+
return self._presenter
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def _active_prompts(self) -> ShellInputService:
|
|
120
|
+
if self._prompts is None:
|
|
121
|
+
raise RuntimeError("Command shell service is not configured.")
|
|
122
|
+
return self._prompts
|
|
123
|
+
|
|
124
|
+
async def handle_input(
|
|
125
|
+
self, user_input: str, *, control: TurnControl | None = None
|
|
126
|
+
) -> bool:
|
|
127
|
+
normalized = user_input.strip()
|
|
128
|
+
if not normalized:
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
if normalized.lower() in {"exit", "quit"}:
|
|
132
|
+
normalized = "/exit"
|
|
133
|
+
|
|
134
|
+
if not normalized.startswith("/"):
|
|
135
|
+
await self._active_bundle.runtime.execute_turn(normalized, control=control)
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
parsed = self._parse_slash_command(normalized)
|
|
139
|
+
if parsed is None:
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
handlers = {
|
|
143
|
+
"help": self._handle_help,
|
|
144
|
+
"exit": self._handle_exit,
|
|
145
|
+
"settings": self._handle_settings,
|
|
146
|
+
"init": self._handle_init,
|
|
147
|
+
"generate": self._handle_generate,
|
|
148
|
+
"sync": self._handle_sync,
|
|
149
|
+
"add": self._handle_add,
|
|
150
|
+
"check": self._handle_check,
|
|
151
|
+
"map": self._handle_map,
|
|
152
|
+
}
|
|
153
|
+
handler = handlers.get(parsed.name)
|
|
154
|
+
if handler is None:
|
|
155
|
+
self._active_presenter.error(f"Unknown slash command: /{parsed.name}")
|
|
156
|
+
self._active_presenter.info(
|
|
157
|
+
"Run /help to inspect the supported command surface."
|
|
158
|
+
)
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
return await handler(parsed)
|
|
163
|
+
except ValueError as exc:
|
|
164
|
+
self._active_presenter.error(str(exc))
|
|
165
|
+
return True
|
|
166
|
+
except Exception as exc:
|
|
167
|
+
self._active_presenter.error(f"Command failed: {exc}")
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
def _parse_slash_command(self, raw_input: str) -> ParsedSlashCommand | None:
|
|
171
|
+
command_body = raw_input[1:].strip()
|
|
172
|
+
if not command_body:
|
|
173
|
+
self._active_presenter.error(
|
|
174
|
+
"Empty slash command. Run /help to inspect the command surface."
|
|
175
|
+
)
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
tokens = shlex.split(command_body)
|
|
180
|
+
except ValueError as exc:
|
|
181
|
+
self._active_presenter.error(f"Unable to parse command: {exc}")
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
if not tokens:
|
|
185
|
+
self._active_presenter.error(
|
|
186
|
+
"Empty slash command. Run /help to inspect the command surface."
|
|
187
|
+
)
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
return ParsedSlashCommand(name=tokens[0].lower(), args=tokens[1:])
|
|
191
|
+
|
|
192
|
+
def _parse_args(
|
|
193
|
+
self, parser: ShellArgumentParser, args: list[str]
|
|
194
|
+
) -> argparse.Namespace | None:
|
|
195
|
+
try:
|
|
196
|
+
return parser.parse_args(args)
|
|
197
|
+
except ValueError as exc:
|
|
198
|
+
self._active_presenter.error(str(exc))
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
def _invoke_command(self, callback, /, *args, **kwargs) -> int:
|
|
202
|
+
try:
|
|
203
|
+
callback(*args, **kwargs)
|
|
204
|
+
except typer.Exit as exc:
|
|
205
|
+
return int(exc.exit_code or 0)
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
async def _handle_help(self, parsed: ParsedSlashCommand) -> bool:
|
|
209
|
+
del parsed
|
|
210
|
+
rows = [
|
|
211
|
+
[spec.name, spec.usage, spec.description] for spec in self.command_specs
|
|
212
|
+
]
|
|
213
|
+
self._active_presenter.table(
|
|
214
|
+
"Slash Commands", ["Command", "Usage", "Description"], rows
|
|
215
|
+
)
|
|
216
|
+
self._active_presenter.info(
|
|
217
|
+
"Session state: "
|
|
218
|
+
f"approval_mode={self._active_bundle.session.approval_mode.value}, "
|
|
219
|
+
f"project_root={self._active_bundle.ctx.project_dir}"
|
|
220
|
+
)
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
async def _handle_exit(self, parsed: ParsedSlashCommand) -> bool:
|
|
224
|
+
del parsed
|
|
225
|
+
self._active_presenter.info("Session ended.")
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
async def _handle_settings(self, parsed: ParsedSlashCommand) -> bool:
|
|
229
|
+
parser = ShellArgumentParser(prog="/settings", add_help=False)
|
|
230
|
+
parser.add_argument("action", nargs="?")
|
|
231
|
+
parser.add_argument("key", nargs="?")
|
|
232
|
+
parser.add_argument("value", nargs="?")
|
|
233
|
+
namespace = self._parse_args(parser, parsed.args)
|
|
234
|
+
if namespace is None:
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
action = None if namespace.action is None else namespace.action.lower()
|
|
238
|
+
if action in {None, "list"}:
|
|
239
|
+
self._render_settings_table()
|
|
240
|
+
if action == "list":
|
|
241
|
+
return True
|
|
242
|
+
return await self._interactive_settings_update()
|
|
243
|
+
|
|
244
|
+
if action == "get":
|
|
245
|
+
if not namespace.key:
|
|
246
|
+
raise ValueError("Usage: /settings get <key>")
|
|
247
|
+
self._ensure_setting_exists(namespace.key)
|
|
248
|
+
self._active_presenter.info(
|
|
249
|
+
f"{namespace.key}: {self._format_setting_value(namespace.key)}"
|
|
250
|
+
)
|
|
251
|
+
return True
|
|
252
|
+
|
|
253
|
+
if action == "set":
|
|
254
|
+
if not namespace.key:
|
|
255
|
+
raise ValueError("Usage: /settings set <key> <value>")
|
|
256
|
+
self._ensure_setting_exists(namespace.key)
|
|
257
|
+
value = namespace.value
|
|
258
|
+
if value is None:
|
|
259
|
+
value = await self._active_prompts.prompt(
|
|
260
|
+
f"New value for {namespace.key}",
|
|
261
|
+
default=self._format_setting_value(namespace.key),
|
|
262
|
+
)
|
|
263
|
+
return await self._apply_setting_change(namespace.key, value)
|
|
264
|
+
|
|
265
|
+
raise ValueError(
|
|
266
|
+
"Usage: /settings | /settings get <key> | /settings set <key> <value>"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
async def _handle_init(self, parsed: ParsedSlashCommand) -> bool:
|
|
270
|
+
from rpr.commands import init as init_command
|
|
271
|
+
|
|
272
|
+
parser = ShellArgumentParser(prog="/init", add_help=False)
|
|
273
|
+
parser.add_argument("project_name", nargs="?")
|
|
274
|
+
parser.add_argument("--dry-run", action="store_true")
|
|
275
|
+
parser.add_argument("--python", default="3.14")
|
|
276
|
+
parser.add_argument("--integrated", action="store_true")
|
|
277
|
+
namespace = self._parse_args(parser, parsed.args)
|
|
278
|
+
if namespace is None:
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
project_name = namespace.project_name or await self._active_prompts.prompt(
|
|
282
|
+
"Project name"
|
|
283
|
+
)
|
|
284
|
+
if not project_name.strip():
|
|
285
|
+
self._active_presenter.warning("Initialization cancelled.")
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
self._active_presenter.info(f"Running /init for {project_name}.")
|
|
289
|
+
exit_code = self._invoke_command(
|
|
290
|
+
init_command.init,
|
|
291
|
+
project_name=project_name,
|
|
292
|
+
dry_run=namespace.dry_run,
|
|
293
|
+
python_version=namespace.python,
|
|
294
|
+
integrated=namespace.integrated,
|
|
295
|
+
)
|
|
296
|
+
if exit_code:
|
|
297
|
+
self._active_presenter.error(f"/init finished with exit code {exit_code}.")
|
|
298
|
+
return True
|
|
299
|
+
|
|
300
|
+
async def _handle_generate(self, parsed: ParsedSlashCommand) -> bool:
|
|
301
|
+
from rpr.commands.generate import api as generate_api_command
|
|
302
|
+
from rpr.commands.generate import domain as generate_domain_command
|
|
303
|
+
from rpr.commands.generate import engine as generate_engine_command
|
|
304
|
+
from rpr.commands.generate import storybook as generate_storybook_command
|
|
305
|
+
from rpr.commands.generate import ui as generate_ui_command
|
|
306
|
+
|
|
307
|
+
parser = ShellArgumentParser(prog="/generate", add_help=False)
|
|
308
|
+
subparsers = parser.add_subparsers(dest="target")
|
|
309
|
+
|
|
310
|
+
for name in ("domain", "api", "ui", "engine"):
|
|
311
|
+
subparser = subparsers.add_parser(name, add_help=False)
|
|
312
|
+
subparser.add_argument("name", nargs="?")
|
|
313
|
+
subparser.add_argument("--dry-run", action="store_true")
|
|
314
|
+
|
|
315
|
+
storybook_parser = subparsers.add_parser("storybook", add_help=False)
|
|
316
|
+
storybook_parser.add_argument("--dry-run", action="store_true")
|
|
317
|
+
storybook_parser.add_argument("--ui-package")
|
|
318
|
+
|
|
319
|
+
namespace = self._parse_args(parser, parsed.args)
|
|
320
|
+
if namespace is None:
|
|
321
|
+
return True
|
|
322
|
+
if namespace.target is None:
|
|
323
|
+
raise ValueError("Usage: /generate <domain|api|ui|engine|storybook> ...")
|
|
324
|
+
|
|
325
|
+
if namespace.target == "domain":
|
|
326
|
+
name = namespace.name or await self._active_prompts.prompt("App name")
|
|
327
|
+
self._invoke_command(
|
|
328
|
+
generate_domain_command.domain, name=name, dry_run=namespace.dry_run
|
|
329
|
+
)
|
|
330
|
+
return True
|
|
331
|
+
if namespace.target == "api":
|
|
332
|
+
name = namespace.name or await self._active_prompts.prompt("App name")
|
|
333
|
+
self._invoke_command(
|
|
334
|
+
generate_api_command.api, name=name, dry_run=namespace.dry_run
|
|
335
|
+
)
|
|
336
|
+
return True
|
|
337
|
+
if namespace.target == "ui":
|
|
338
|
+
name = namespace.name or await self._active_prompts.prompt("App name")
|
|
339
|
+
self._invoke_command(
|
|
340
|
+
generate_ui_command.ui, name=name, dry_run=namespace.dry_run
|
|
341
|
+
)
|
|
342
|
+
return True
|
|
343
|
+
if namespace.target == "engine":
|
|
344
|
+
name = namespace.name or await self._active_prompts.prompt("App name")
|
|
345
|
+
self._invoke_command(
|
|
346
|
+
generate_engine_command.engine, name=name, dry_run=namespace.dry_run
|
|
347
|
+
)
|
|
348
|
+
return True
|
|
349
|
+
|
|
350
|
+
ui_package = namespace.ui_package
|
|
351
|
+
if not ui_package:
|
|
352
|
+
ui_packages = generate_storybook_command._find_ui_packages(
|
|
353
|
+
self._bundle.ctx.project_dir
|
|
354
|
+
)
|
|
355
|
+
if len(ui_packages) == 1:
|
|
356
|
+
ui_package = ui_packages[0]
|
|
357
|
+
elif len(ui_packages) > 1:
|
|
358
|
+
self._active_presenter.table(
|
|
359
|
+
"Storybook Targets",
|
|
360
|
+
["Package"],
|
|
361
|
+
[[package] for package in ui_packages],
|
|
362
|
+
)
|
|
363
|
+
ui_package = (
|
|
364
|
+
await self._active_prompts.prompt(
|
|
365
|
+
"UI package", default=ui_packages[0]
|
|
366
|
+
)
|
|
367
|
+
).strip() or ui_packages[0]
|
|
368
|
+
|
|
369
|
+
self._invoke_command(
|
|
370
|
+
generate_storybook_command.storybook,
|
|
371
|
+
dry_run=namespace.dry_run,
|
|
372
|
+
ui_package=ui_package,
|
|
373
|
+
)
|
|
374
|
+
return True
|
|
375
|
+
|
|
376
|
+
async def _handle_sync(self, parsed: ParsedSlashCommand) -> bool:
|
|
377
|
+
from rpr.commands import sync as sync_command
|
|
378
|
+
|
|
379
|
+
parser = ShellArgumentParser(prog="/sync", add_help=False)
|
|
380
|
+
parser.add_argument("action", nargs="?", default="rules")
|
|
381
|
+
parser.add_argument("--target")
|
|
382
|
+
parser.add_argument("--dry-run", action="store_true")
|
|
383
|
+
namespace = self._parse_args(parser, parsed.args)
|
|
384
|
+
if namespace is None:
|
|
385
|
+
return True
|
|
386
|
+
if namespace.action != "rules":
|
|
387
|
+
raise ValueError("Usage: /sync [rules] [--target cursor] [--dry-run]")
|
|
388
|
+
|
|
389
|
+
self._invoke_command(
|
|
390
|
+
sync_command.rules, target=namespace.target, dry_run=namespace.dry_run
|
|
391
|
+
)
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
async def _handle_add(self, parsed: ParsedSlashCommand) -> bool:
|
|
395
|
+
from rpr.commands import add as add_command
|
|
396
|
+
|
|
397
|
+
parser = ShellArgumentParser(prog="/add", add_help=False)
|
|
398
|
+
parser.add_argument("action", nargs="?", default="template")
|
|
399
|
+
parser.add_argument("template_id", nargs="?")
|
|
400
|
+
parser.add_argument("--list", action="store_true")
|
|
401
|
+
parser.add_argument("--force", action="store_true")
|
|
402
|
+
parser.add_argument("--name")
|
|
403
|
+
parser.add_argument("--source")
|
|
404
|
+
parser.add_argument("--dry-run", action="store_true")
|
|
405
|
+
namespace = self._parse_args(parser, parsed.args)
|
|
406
|
+
if namespace is None:
|
|
407
|
+
return True
|
|
408
|
+
if namespace.action != "template":
|
|
409
|
+
raise ValueError(
|
|
410
|
+
"Usage: /add template <template-id> [--name app] [--force] [--dry-run]"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
template_id = namespace.template_id
|
|
414
|
+
if template_id is None and not namespace.list:
|
|
415
|
+
template_id = await self._active_prompts.prompt("Template ID")
|
|
416
|
+
|
|
417
|
+
self._invoke_command(
|
|
418
|
+
add_command.template,
|
|
419
|
+
template_id=template_id,
|
|
420
|
+
list_templates=namespace.list,
|
|
421
|
+
force=namespace.force,
|
|
422
|
+
name=namespace.name,
|
|
423
|
+
source=namespace.source,
|
|
424
|
+
dry_run=namespace.dry_run,
|
|
425
|
+
)
|
|
426
|
+
return True
|
|
427
|
+
|
|
428
|
+
async def _handle_check(self, parsed: ParsedSlashCommand) -> bool:
|
|
429
|
+
parser = ShellArgumentParser(prog="/check", add_help=False)
|
|
430
|
+
parser.add_argument("--group")
|
|
431
|
+
namespace = self._parse_args(parser, parsed.args)
|
|
432
|
+
if namespace is None:
|
|
433
|
+
return True
|
|
434
|
+
|
|
435
|
+
report = collect_check_report(
|
|
436
|
+
self._active_bundle.ctx.project_dir, group=namespace.group
|
|
437
|
+
)
|
|
438
|
+
self._active_presenter.heading(f"rpr check — project: {report.project_name}")
|
|
439
|
+
current_group = None
|
|
440
|
+
for result in report.results:
|
|
441
|
+
if result.group != current_group:
|
|
442
|
+
current_group = result.group
|
|
443
|
+
self._active_presenter.info(current_group)
|
|
444
|
+
self._active_presenter.status_row(
|
|
445
|
+
"✓" if result.status else "✗",
|
|
446
|
+
result.area,
|
|
447
|
+
str(result.path),
|
|
448
|
+
result.status,
|
|
449
|
+
result.suggestion,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
failures = sum(1 for result in report.results if not result.status)
|
|
453
|
+
if failures:
|
|
454
|
+
self._active_presenter.error(
|
|
455
|
+
f"Check completed with {failures} failing item(s)."
|
|
456
|
+
)
|
|
457
|
+
else:
|
|
458
|
+
self._active_presenter.success("Check completed with no failing items.")
|
|
459
|
+
return True
|
|
460
|
+
|
|
461
|
+
async def _handle_map(self, parsed: ParsedSlashCommand) -> bool:
|
|
462
|
+
from rpr.commands import map as map_command
|
|
463
|
+
|
|
464
|
+
parser = ShellArgumentParser(prog="/map", add_help=False)
|
|
465
|
+
parser.add_argument("--root", default=".")
|
|
466
|
+
parser.add_argument("--output", default="code-map.md")
|
|
467
|
+
parser.add_argument("--dry-run", action="store_true")
|
|
468
|
+
namespace = self._parse_args(parser, parsed.args)
|
|
469
|
+
if namespace is None:
|
|
470
|
+
return True
|
|
471
|
+
|
|
472
|
+
exit_code = self._invoke_command(
|
|
473
|
+
map_command.map,
|
|
474
|
+
root=Path(namespace.root),
|
|
475
|
+
output=Path(namespace.output),
|
|
476
|
+
dry_run=namespace.dry_run,
|
|
477
|
+
)
|
|
478
|
+
if exit_code:
|
|
479
|
+
self._active_presenter.error(f"/map finished with exit code {exit_code}.")
|
|
480
|
+
return True
|
|
481
|
+
|
|
482
|
+
def _render_settings_table(self) -> None:
|
|
483
|
+
rows = []
|
|
484
|
+
for definition, value in settings_manager.describe_settings():
|
|
485
|
+
allowed = (
|
|
486
|
+
", ".join(definition.choices) if definition.choices else "true, false"
|
|
487
|
+
)
|
|
488
|
+
rows.append([definition.key, value, allowed, definition.description])
|
|
489
|
+
self._active_presenter.table(
|
|
490
|
+
"RPR Settings", ["Key", "Value", "Allowed", "Description"], rows
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
async def _interactive_settings_update(self) -> bool:
|
|
494
|
+
key = (
|
|
495
|
+
await self._active_prompts.prompt("Setting to edit (blank to cancel)")
|
|
496
|
+
).strip()
|
|
497
|
+
if not key:
|
|
498
|
+
self._active_presenter.info("Settings update cancelled.")
|
|
499
|
+
return True
|
|
500
|
+
|
|
501
|
+
self._ensure_setting_exists(key)
|
|
502
|
+
definition = SETTING_DEFINITIONS[key]
|
|
503
|
+
allowed = ", ".join(definition.choices) if definition.choices else "true, false"
|
|
504
|
+
self._active_presenter.info(f"Current {key}: {self._format_setting_value(key)}")
|
|
505
|
+
self._active_presenter.info(f"Allowed values: {allowed}")
|
|
506
|
+
value = await self._active_prompts.prompt(
|
|
507
|
+
f"New value for {key}",
|
|
508
|
+
default=self._format_setting_value(key),
|
|
509
|
+
)
|
|
510
|
+
return await self._apply_setting_change(key, value)
|
|
511
|
+
|
|
512
|
+
def _ensure_setting_exists(self, key: str) -> None:
|
|
513
|
+
if key not in SETTING_DEFINITIONS:
|
|
514
|
+
raise ValueError(f"Unknown setting: {key}")
|
|
515
|
+
|
|
516
|
+
def _format_setting_value(self, key: str) -> str:
|
|
517
|
+
value = settings_manager.get_value(key)
|
|
518
|
+
if isinstance(value, bool):
|
|
519
|
+
return "true" if value else "false"
|
|
520
|
+
return value
|
|
521
|
+
|
|
522
|
+
async def _apply_setting_change(self, key: str, value: str) -> bool:
|
|
523
|
+
self._ensure_setting_exists(key)
|
|
524
|
+
if not await self._active_prompts.confirm(
|
|
525
|
+
f"Persist {key} = {value}?", default=True
|
|
526
|
+
):
|
|
527
|
+
self._active_presenter.info("Settings update cancelled.")
|
|
528
|
+
return True
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
settings_manager.set_value(key, value)
|
|
532
|
+
except ValueError as exc:
|
|
533
|
+
self._active_presenter.error(str(exc))
|
|
534
|
+
return True
|
|
535
|
+
|
|
536
|
+
if key == "approval_mode":
|
|
537
|
+
self._active_bundle.session.approval_mode = settings_manager.approval_mode
|
|
538
|
+
|
|
539
|
+
self._active_presenter.on_setting_changed(key)
|
|
540
|
+
self._active_presenter.success(
|
|
541
|
+
f"Updated {key} to {self._format_setting_value(key)}."
|
|
542
|
+
)
|
|
543
|
+
return True
|
rpr/checks/__init__.py
ADDED
|
File without changes
|
rpr/checks/base.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from rpr.checks.base import CheckResult
|
|
6
|
+
from rpr.generators.base import SyncConfig, interpolate
|
|
7
|
+
from rpr.templates.registry import TEMPLATES
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_instructions(
|
|
11
|
+
project_dir: Path, variables: dict[str, str]
|
|
12
|
+
) -> list[CheckResult]:
|
|
13
|
+
results = []
|
|
14
|
+
|
|
15
|
+
# Cursor rules
|
|
16
|
+
cursor_rules = project_dir / ".cursor" / "rules"
|
|
17
|
+
cursor_ok = cursor_rules.exists() and any(cursor_rules.glob("*.mdc"))
|
|
18
|
+
results.append(
|
|
19
|
+
CheckResult(
|
|
20
|
+
group="AI Instructions & Templates",
|
|
21
|
+
area="Cursor rules",
|
|
22
|
+
status=cursor_ok,
|
|
23
|
+
path=str(cursor_rules) if cursor_ok else ".cursor/rules/ missing",
|
|
24
|
+
suggestion="rpr sync rules --target cursor",
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Copilot instructions
|
|
29
|
+
copilot_file = project_dir / ".github" / "copilot-instructions.md"
|
|
30
|
+
results.append(
|
|
31
|
+
CheckResult(
|
|
32
|
+
group="AI Instructions & Templates",
|
|
33
|
+
area="Copilot instructions",
|
|
34
|
+
status=copilot_file.exists(),
|
|
35
|
+
path=str(copilot_file)
|
|
36
|
+
if copilot_file.exists()
|
|
37
|
+
else ".github/copilot-instructions.md missing",
|
|
38
|
+
suggestion="rpr sync rules --target copilot",
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Claude instructions
|
|
43
|
+
claude_file = project_dir / "CLAUDE.md"
|
|
44
|
+
results.append(
|
|
45
|
+
CheckResult(
|
|
46
|
+
group="AI Instructions & Templates",
|
|
47
|
+
area="Claude instructions",
|
|
48
|
+
status=claude_file.exists(),
|
|
49
|
+
path=str(claude_file) if claude_file.exists() else "CLAUDE.md missing",
|
|
50
|
+
suggestion="rpr sync rules --target claude",
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Templates (only if domain package exists for Python, or UI library for JS)
|
|
55
|
+
has_domain = any(project_dir.glob("packages/python/domain"))
|
|
56
|
+
has_ui = any(project_dir.glob("packages/node/*-ui"))
|
|
57
|
+
|
|
58
|
+
template_ids = [
|
|
59
|
+
("domain_settings", "has_domain"),
|
|
60
|
+
("domain_container", "has_domain"),
|
|
61
|
+
("mapper_util", "has_domain"),
|
|
62
|
+
("dto_util", "has_domain"),
|
|
63
|
+
("partial_update", "has_domain"),
|
|
64
|
+
("encrypted_column", "has_domain"),
|
|
65
|
+
("fetch_service", "has_ui"),
|
|
66
|
+
("sticky_navigation", "has_ui"),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
for tid, prereq in template_ids:
|
|
70
|
+
if (prereq == "has_domain" and not has_domain) or (
|
|
71
|
+
prereq == "has_ui" and not has_ui
|
|
72
|
+
):
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
entry = TEMPLATES[tid]
|
|
76
|
+
target_path_str = interpolate(entry.target_path, variables)
|
|
77
|
+
target_path = project_dir / target_path_str
|
|
78
|
+
|
|
79
|
+
results.append(
|
|
80
|
+
CheckResult(
|
|
81
|
+
group="AI Instructions & Templates",
|
|
82
|
+
area=tid,
|
|
83
|
+
status=target_path.exists(),
|
|
84
|
+
path=str(target_path) if target_path.exists() else f"{tid} missing",
|
|
85
|
+
suggestion=f"rpr add template {tid}",
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return results
|