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/checks/workspace.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import tomllib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from rpr.checks.base import CheckResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_workspace(project_dir: Path) -> list[CheckResult]:
|
|
11
|
+
results: list[CheckResult] = []
|
|
12
|
+
|
|
13
|
+
# NX workspace
|
|
14
|
+
nx_json = project_dir / "nx.json"
|
|
15
|
+
nx_data: dict[str, object] = {}
|
|
16
|
+
nx_ok = False
|
|
17
|
+
nx_cloud_disabled = False
|
|
18
|
+
if nx_json.exists():
|
|
19
|
+
try:
|
|
20
|
+
nx_data = json.loads(nx_json.read_text())
|
|
21
|
+
nx_ok = True
|
|
22
|
+
nx_cloud_disabled = nx_data.get("neverConnectToCloud") is True
|
|
23
|
+
except (OSError, json.JSONDecodeError):
|
|
24
|
+
nx_ok = False
|
|
25
|
+
|
|
26
|
+
results.append(
|
|
27
|
+
CheckResult(
|
|
28
|
+
group="Workspace",
|
|
29
|
+
area="NX workspace",
|
|
30
|
+
status=nx_ok,
|
|
31
|
+
path=str(nx_json) if nx_ok else "nx.json missing",
|
|
32
|
+
suggestion="rpr init <name>",
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
results.append(
|
|
36
|
+
CheckResult(
|
|
37
|
+
group="Workspace",
|
|
38
|
+
area="NX Cloud disabled",
|
|
39
|
+
status=nx_cloud_disabled,
|
|
40
|
+
path=str(nx_json)
|
|
41
|
+
if nx_cloud_disabled
|
|
42
|
+
else "nx.json missing neverConnectToCloud=true",
|
|
43
|
+
suggestion="rpr init <name>",
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# UV workspace
|
|
48
|
+
pyproject = project_dir / "pyproject.toml"
|
|
49
|
+
pyproject_data: dict[str, object] = {}
|
|
50
|
+
uv_ok = False
|
|
51
|
+
if pyproject.exists():
|
|
52
|
+
try:
|
|
53
|
+
pyproject_data = tomllib.loads(pyproject.read_text())
|
|
54
|
+
tool = pyproject_data.get("tool", {})
|
|
55
|
+
uv_ok = "uv" in tool and "workspace" in tool["uv"]
|
|
56
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
57
|
+
uv_ok = False
|
|
58
|
+
|
|
59
|
+
results.append(
|
|
60
|
+
CheckResult(
|
|
61
|
+
group="Workspace",
|
|
62
|
+
area="UV workspace",
|
|
63
|
+
status=uv_ok,
|
|
64
|
+
path=str(pyproject)
|
|
65
|
+
if uv_ok
|
|
66
|
+
else "pyproject.toml with [tool.uv.workspace] missing",
|
|
67
|
+
suggestion="rpr init <name>",
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
tool = pyproject_data.get("tool", {}) if pyproject_data else {}
|
|
72
|
+
uv_require_version = False
|
|
73
|
+
pytest_config = False
|
|
74
|
+
dev_dependencies_ok = False
|
|
75
|
+
if tool:
|
|
76
|
+
uv_require_version = "require-version" in tool.get("uv", {})
|
|
77
|
+
pytest_config = "pytest" in tool and "ini_options" in tool["pytest"]
|
|
78
|
+
|
|
79
|
+
dependency_groups = pyproject_data.get("dependency-groups", {}) if pyproject_data else {}
|
|
80
|
+
dev_dependencies = dependency_groups.get("dev", []) if isinstance(dependency_groups, dict) else []
|
|
81
|
+
if isinstance(dev_dependencies, list):
|
|
82
|
+
required = {
|
|
83
|
+
"pytest",
|
|
84
|
+
"pytest-asyncio",
|
|
85
|
+
"pytest-cov",
|
|
86
|
+
"ipykernel",
|
|
87
|
+
"pre-commit",
|
|
88
|
+
"pyright",
|
|
89
|
+
"ruff",
|
|
90
|
+
"maturin",
|
|
91
|
+
}
|
|
92
|
+
dev_dependencies_ok = required.issubset(set(dev_dependencies))
|
|
93
|
+
|
|
94
|
+
results.append(
|
|
95
|
+
CheckResult(
|
|
96
|
+
group="Workspace",
|
|
97
|
+
area="Root Python tooling",
|
|
98
|
+
status=uv_require_version and pytest_config and dev_dependencies_ok,
|
|
99
|
+
path=str(pyproject)
|
|
100
|
+
if uv_require_version and pytest_config and dev_dependencies_ok
|
|
101
|
+
else "pyproject.toml missing guide-aligned UV/Python sections",
|
|
102
|
+
suggestion="rpr init <name>",
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
package_json = project_dir / "package.json"
|
|
107
|
+
node_workspace_ok = False
|
|
108
|
+
if package_json.exists():
|
|
109
|
+
try:
|
|
110
|
+
package_data = json.loads(package_json.read_text())
|
|
111
|
+
workspaces = package_data.get("workspaces", [])
|
|
112
|
+
node_workspace_ok = isinstance(workspaces, list) and {
|
|
113
|
+
"apps/*",
|
|
114
|
+
"packages/node/*",
|
|
115
|
+
}.issubset(set(workspaces))
|
|
116
|
+
except (OSError, json.JSONDecodeError):
|
|
117
|
+
node_workspace_ok = False
|
|
118
|
+
|
|
119
|
+
results.append(
|
|
120
|
+
CheckResult(
|
|
121
|
+
group="Workspace",
|
|
122
|
+
area="Node workspace",
|
|
123
|
+
status=node_workspace_ok,
|
|
124
|
+
path=str(package_json)
|
|
125
|
+
if node_workspace_ok
|
|
126
|
+
else "package.json missing npm workspaces",
|
|
127
|
+
suggestion="rpr init <name>",
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
pre_commit = project_dir / ".pre-commit-config.yaml"
|
|
132
|
+
pre_commit_ok = False
|
|
133
|
+
if pre_commit.exists():
|
|
134
|
+
content = pre_commit.read_text()
|
|
135
|
+
pre_commit_ok = all(
|
|
136
|
+
needle in content
|
|
137
|
+
for needle in (
|
|
138
|
+
"pre-commit-hooks",
|
|
139
|
+
"nbstripout",
|
|
140
|
+
"uv run ruff format",
|
|
141
|
+
"uv run ruff check --fix",
|
|
142
|
+
"npx nx run-many --target=lint --all --fix --exclude=engine",
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
results.append(
|
|
147
|
+
CheckResult(
|
|
148
|
+
group="Workspace",
|
|
149
|
+
area="Pre-commit config",
|
|
150
|
+
status=pre_commit_ok,
|
|
151
|
+
path=str(pre_commit)
|
|
152
|
+
if pre_commit_ok
|
|
153
|
+
else ".pre-commit-config.yaml missing guide-aligned hooks",
|
|
154
|
+
suggestion="rpr init <name>",
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# .rpr.yaml
|
|
159
|
+
rpr_yaml = project_dir / ".rpr.yaml"
|
|
160
|
+
results.append(
|
|
161
|
+
CheckResult(
|
|
162
|
+
group="Workspace",
|
|
163
|
+
area=".rpr.yaml config",
|
|
164
|
+
status=rpr_yaml.exists(),
|
|
165
|
+
path=str(rpr_yaml) if rpr_yaml.exists() else ".rpr.yaml missing",
|
|
166
|
+
suggestion="rpr init <name>",
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return results
|
rpr/cli.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rpr import __version__
|
|
7
|
+
from rpr.ui.console import console
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(
|
|
10
|
+
name="rpr",
|
|
11
|
+
help="RPR CLI -- React Python Rust monorepo scaffolding orchestrator.",
|
|
12
|
+
no_args_is_help=False,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# --- Command Imports (must be after app is defined) ---
|
|
16
|
+
from rpr.commands import init as _init_mod # noqa: E402, F401
|
|
17
|
+
from rpr.commands import check as _check_mod # noqa: E402, F401
|
|
18
|
+
from rpr.commands import settings as _settings_mod # noqa: E402, F401
|
|
19
|
+
from rpr.commands import map as _map_mod # noqa: E402, F401
|
|
20
|
+
from rpr.commands import chat as _chat_mod # noqa: E402, F401
|
|
21
|
+
|
|
22
|
+
generate_app = typer.Typer(
|
|
23
|
+
name="generate",
|
|
24
|
+
help="Scaffold apps and packages (api, ui, domain, engine, storybook).",
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
)
|
|
27
|
+
app.add_typer(generate_app, name="generate")
|
|
28
|
+
|
|
29
|
+
sync_app = typer.Typer(
|
|
30
|
+
name="sync",
|
|
31
|
+
help="Sync AI instructions to Cursor, Copilot, and Claude Code.",
|
|
32
|
+
no_args_is_help=True,
|
|
33
|
+
)
|
|
34
|
+
app.add_typer(sync_app, name="sync")
|
|
35
|
+
|
|
36
|
+
add_app = typer.Typer(
|
|
37
|
+
name="add",
|
|
38
|
+
help="Add code templates to the current project.",
|
|
39
|
+
no_args_is_help=True,
|
|
40
|
+
)
|
|
41
|
+
app.add_typer(add_app, name="add")
|
|
42
|
+
|
|
43
|
+
# Import subcommands
|
|
44
|
+
from rpr.commands.generate import domain as _domain_mod # noqa: E402, F401
|
|
45
|
+
from rpr.commands.generate import api as _api_mod # noqa: E402, F401
|
|
46
|
+
from rpr.commands.generate import ui as _ui_mod # noqa: E402, F401
|
|
47
|
+
from rpr.commands.generate import engine as _engine_mod # noqa: E402, F401
|
|
48
|
+
from rpr.commands.generate import storybook as _storybook_mod # noqa: E402, F401
|
|
49
|
+
from rpr.commands import sync as _sync_mod # noqa: E402, F401
|
|
50
|
+
from rpr.commands import add as _add_mod # noqa: E402, F401
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _version_callback(value: bool) -> None:
|
|
54
|
+
if value:
|
|
55
|
+
typer.echo(f"rpr {__version__}")
|
|
56
|
+
raise typer.Exit()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.callback(invoke_without_command=True)
|
|
60
|
+
def main(
|
|
61
|
+
ctx: typer.Context,
|
|
62
|
+
version: Annotated[
|
|
63
|
+
bool,
|
|
64
|
+
typer.Option(
|
|
65
|
+
"--version",
|
|
66
|
+
"-v",
|
|
67
|
+
help="Show the version and exit.",
|
|
68
|
+
callback=_version_callback,
|
|
69
|
+
is_eager=True,
|
|
70
|
+
),
|
|
71
|
+
] = False,
|
|
72
|
+
) -> None:
|
|
73
|
+
if ctx.invoked_subcommand is None:
|
|
74
|
+
ctx.invoke(_chat_mod.chat)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
app()
|
rpr/commands/__init__.py
ADDED
|
File without changes
|
rpr/commands/add.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from rpr.cli import add_app
|
|
11
|
+
from rpr.context import RunContext
|
|
12
|
+
from rpr.generators.base import SyncConfig, interpolate
|
|
13
|
+
from rpr.templates.registry import TEMPLATES
|
|
14
|
+
from rpr.workspace import derive_variables
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _extract_code_blocks(markdown: str, language: str) -> list[str]:
|
|
20
|
+
"""Extract fenced code blocks from markdown matching the given language."""
|
|
21
|
+
pattern = rf"```{language}\s*\n(.*?)```"
|
|
22
|
+
matches = re.findall(pattern, markdown, re.DOTALL)
|
|
23
|
+
if not matches:
|
|
24
|
+
pattern = r"```\w*\s*\n(.*?)```"
|
|
25
|
+
matches = re.findall(pattern, markdown, re.DOTALL)
|
|
26
|
+
return matches
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@add_app.command("template")
|
|
30
|
+
def template(
|
|
31
|
+
template_id: Annotated[
|
|
32
|
+
str | None,
|
|
33
|
+
typer.Argument(help="Template ID to add (e.g., mapper_util, dto_util)."),
|
|
34
|
+
] = None,
|
|
35
|
+
list_templates: Annotated[
|
|
36
|
+
bool, typer.Option("--list", "-l", help="List all available templates.")
|
|
37
|
+
] = False,
|
|
38
|
+
force: Annotated[
|
|
39
|
+
bool,
|
|
40
|
+
typer.Option(
|
|
41
|
+
"--force", "-f", help="Overwrite existing files without prompting."
|
|
42
|
+
),
|
|
43
|
+
] = False,
|
|
44
|
+
name: Annotated[
|
|
45
|
+
str | None,
|
|
46
|
+
typer.Option("--name", "-n", help="App name for variable interpolation."),
|
|
47
|
+
] = None,
|
|
48
|
+
source: Annotated[
|
|
49
|
+
str | None,
|
|
50
|
+
typer.Option("--source", "-s", help="Path to the rules source directory."),
|
|
51
|
+
] = None,
|
|
52
|
+
dry_run: Annotated[
|
|
53
|
+
bool, typer.Option("--dry-run", help="Preview changes without writing.")
|
|
54
|
+
] = False,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Add a code template file to the current project."""
|
|
57
|
+
ctx = RunContext(dry_run=dry_run, project_dir=Path.cwd())
|
|
58
|
+
config_path = ctx.project_dir / ".rpr.yaml"
|
|
59
|
+
config = SyncConfig.load(config_path)
|
|
60
|
+
|
|
61
|
+
if name:
|
|
62
|
+
variables = derive_variables(name)
|
|
63
|
+
elif config.variables:
|
|
64
|
+
variables = config.variables
|
|
65
|
+
else:
|
|
66
|
+
# For list command, we don't necessarily want to prompt
|
|
67
|
+
if list_templates:
|
|
68
|
+
variables = {}
|
|
69
|
+
else:
|
|
70
|
+
project_name = typer.prompt("Project name", default=ctx.project_dir.name)
|
|
71
|
+
variables = derive_variables(project_name)
|
|
72
|
+
|
|
73
|
+
if list_templates:
|
|
74
|
+
console.print("\n[bold]Available templates:[/]\n")
|
|
75
|
+
for tid, entry in TEMPLATES.items():
|
|
76
|
+
console.print(f" [bold cyan]{tid:<20}[/] {entry.description}")
|
|
77
|
+
path_str = entry.target_path
|
|
78
|
+
if variables:
|
|
79
|
+
path_str = interpolate(path_str, variables)
|
|
80
|
+
console.print(f" {'':20} -> {path_str}")
|
|
81
|
+
console.print()
|
|
82
|
+
raise typer.Exit(0)
|
|
83
|
+
|
|
84
|
+
if not template_id:
|
|
85
|
+
template_id = typer.prompt("Template ID")
|
|
86
|
+
|
|
87
|
+
if template_id not in TEMPLATES:
|
|
88
|
+
console.print(f"[bold red]Error:[/] Unknown template: {template_id}")
|
|
89
|
+
console.print("Run 'rpr add template --list' to see available templates.")
|
|
90
|
+
raise typer.Exit(1)
|
|
91
|
+
|
|
92
|
+
entry = TEMPLATES[template_id]
|
|
93
|
+
|
|
94
|
+
source_dir = (
|
|
95
|
+
Path(source).expanduser().resolve() if source else _find_source_dir(config)
|
|
96
|
+
)
|
|
97
|
+
if not source_dir:
|
|
98
|
+
console.print(
|
|
99
|
+
"[bold red]Error:[/] Cannot find rules source. Use --source or create .rpr.yaml"
|
|
100
|
+
)
|
|
101
|
+
raise typer.Exit(1)
|
|
102
|
+
|
|
103
|
+
source_file = source_dir / entry.source_subdir / entry.source_filename
|
|
104
|
+
if not source_file.exists():
|
|
105
|
+
console.print(f"[bold red]Error:[/] Source file not found: {source_file}")
|
|
106
|
+
raise typer.Exit(1)
|
|
107
|
+
|
|
108
|
+
markdown = source_file.read_text()
|
|
109
|
+
|
|
110
|
+
lang_hint = "python" if entry.language == "python" else "ts"
|
|
111
|
+
code_blocks = _extract_code_blocks(markdown, lang_hint)
|
|
112
|
+
|
|
113
|
+
if not code_blocks:
|
|
114
|
+
console.print(f"[bold red]Error:[/] No code blocks found in {source_file}")
|
|
115
|
+
raise typer.Exit(1)
|
|
116
|
+
|
|
117
|
+
code = code_blocks[0].strip() + "\n"
|
|
118
|
+
|
|
119
|
+
# Robust interpolation
|
|
120
|
+
code = interpolate(code, variables)
|
|
121
|
+
# Legacy support for older templates that might use these placeholders
|
|
122
|
+
name_val = variables.get("name", "")
|
|
123
|
+
name_underscore_val = variables.get("name_underscore", "")
|
|
124
|
+
code = code.replace("your_app_domain", f"{name_underscore_val}_domain")
|
|
125
|
+
code = code.replace("your-app-domain", f"{name_val}-domain")
|
|
126
|
+
code = code.replace("my_app_domain", f"{name_underscore_val}_domain")
|
|
127
|
+
code = code.replace("my-app-domain", f"{name_val}-domain")
|
|
128
|
+
|
|
129
|
+
target_path_str = interpolate(entry.target_path, variables)
|
|
130
|
+
target_path = ctx.project_dir / target_path_str
|
|
131
|
+
|
|
132
|
+
if not dry_run and target_path.exists() and not force:
|
|
133
|
+
console.print(f"[bold yellow]Warning:[/] File already exists: {target_path}")
|
|
134
|
+
overwrite = typer.confirm("Overwrite?")
|
|
135
|
+
if not overwrite:
|
|
136
|
+
console.print("Skipped.")
|
|
137
|
+
raise typer.Exit(0)
|
|
138
|
+
|
|
139
|
+
console.print(f"\n[bold]Adding template:[/] {template_id}\n")
|
|
140
|
+
ctx.write_file(target_path, code)
|
|
141
|
+
|
|
142
|
+
console.print(f"\n[bold green]Done![/] Template written to {target_path}")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _find_source_dir(config: SyncConfig) -> Path | None:
|
|
146
|
+
"""Try to find the source directory from SyncConfig or common locations."""
|
|
147
|
+
if config.source:
|
|
148
|
+
p = Path(config.source).expanduser().resolve()
|
|
149
|
+
if p.exists():
|
|
150
|
+
# If the source points to 'instructions', we should look at its parent
|
|
151
|
+
# as templates are usually sibling directories to instructions.
|
|
152
|
+
if p.name == "instructions" and (p.parent / "special_files").exists():
|
|
153
|
+
return p.parent
|
|
154
|
+
return p
|
|
155
|
+
|
|
156
|
+
# Try relative to this file (development mode)
|
|
157
|
+
# src/rpr/commands/add.py -> parent x 2 is src/rpr/ -> / scaffolds
|
|
158
|
+
scaffold_root = Path(__file__).resolve().parent.parent / "scaffolds"
|
|
159
|
+
if (scaffold_root / "special_files").exists():
|
|
160
|
+
return scaffold_root
|
|
161
|
+
|
|
162
|
+
common = Path.home() / "development" / "project-getting-started"
|
|
163
|
+
if common.exists():
|
|
164
|
+
return common
|
|
165
|
+
|
|
166
|
+
return None
|
rpr/commands/chat.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from rpr.agent.bootstrap import resolve_agent_client
|
|
9
|
+
from rpr.agent.client import AgentClient
|
|
10
|
+
from rpr.application.chat_service import ChatCommandService
|
|
11
|
+
from rpr.cli import app
|
|
12
|
+
from rpr.context import RunContext
|
|
13
|
+
from rpr.ui.theme import settings_manager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_agent_client(provider: str | None, model: str | None) -> AgentClient:
|
|
17
|
+
return resolve_agent_client(provider, model)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command()
|
|
21
|
+
def chat(
|
|
22
|
+
provider: Annotated[
|
|
23
|
+
str | None, typer.Option(help="Override the AI provider to use.")
|
|
24
|
+
] = None,
|
|
25
|
+
model: Annotated[
|
|
26
|
+
str | None, typer.Option(help="Override the model to use.")
|
|
27
|
+
] = None,
|
|
28
|
+
approval_mode: Annotated[
|
|
29
|
+
str | None,
|
|
30
|
+
typer.Option(
|
|
31
|
+
help="Override the approval mode for this session.", case_sensitive=False
|
|
32
|
+
),
|
|
33
|
+
] = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Start an interactive chat session with the AI agent."""
|
|
36
|
+
try:
|
|
37
|
+
asyncio.run(
|
|
38
|
+
ChatCommandService.instance().run(
|
|
39
|
+
provider=provider,
|
|
40
|
+
model=model,
|
|
41
|
+
approval_mode_override=approval_mode,
|
|
42
|
+
client_factory=_get_agent_client,
|
|
43
|
+
context_factory=RunContext,
|
|
44
|
+
settings=settings_manager,
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
except ValueError as exc:
|
|
48
|
+
raise typer.BadParameter(str(exc)) from exc
|
rpr/commands/check.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rpr.application.checks import collect_check_report, serialize_check_results
|
|
9
|
+
from rpr.cli import app
|
|
10
|
+
from rpr.ui.console import console
|
|
11
|
+
from rpr.ui.renderers import render_heading, render_status_row
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def check(
|
|
16
|
+
group: Annotated[
|
|
17
|
+
str | None,
|
|
18
|
+
typer.Option(
|
|
19
|
+
"--group",
|
|
20
|
+
help="Run only a specific group: workspace, instructions, packages.",
|
|
21
|
+
),
|
|
22
|
+
] = None,
|
|
23
|
+
as_json: Annotated[
|
|
24
|
+
bool, typer.Option("--json", help="Emit machine-readable JSON output.")
|
|
25
|
+
] = False,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Inspect the current project and report setup status."""
|
|
28
|
+
project_dir = Path.cwd()
|
|
29
|
+
report = collect_check_report(project_dir, group=group)
|
|
30
|
+
|
|
31
|
+
if as_json:
|
|
32
|
+
typer.echo(json.dumps(serialize_check_results(report.results)))
|
|
33
|
+
if any(not result.status for result in report.results):
|
|
34
|
+
raise typer.Exit(1)
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
render_heading(f"rpr check — project: {report.project_name}")
|
|
38
|
+
|
|
39
|
+
current_group = None
|
|
40
|
+
for result in report.results:
|
|
41
|
+
if result.group != current_group:
|
|
42
|
+
current_group = result.group
|
|
43
|
+
console.print(f"[heading]{current_group}[/]")
|
|
44
|
+
|
|
45
|
+
icon_char = "✓" if result.status else "✗"
|
|
46
|
+
render_status_row(
|
|
47
|
+
icon_char, result.area, str(result.path), result.status, result.suggestion
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
console.print()
|
|
51
|
+
|
|
52
|
+
if any(not result.status for result in report.results):
|
|
53
|
+
raise typer.Exit(1)
|
|
File without changes
|