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/commands/init.py
ADDED
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from contextlib import chdir
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import tomllib
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Annotated, Literal
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from rpr.cli import app
|
|
15
|
+
from rpr.context import RunContext
|
|
16
|
+
from rpr.generators.base import SyncConfig, load_rules
|
|
17
|
+
from rpr.generators.claude import generate_claude_rules
|
|
18
|
+
from rpr.generators.copilot import generate_copilot_rules
|
|
19
|
+
from rpr.generators.cursor import generate_cursor_rules
|
|
20
|
+
from rpr.generators.gemini import generate_gemini_rules
|
|
21
|
+
from rpr.workspace import ensure_npm_workspaces
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
NxState = Literal["new", "existing_non_nx", "already_nx"]
|
|
26
|
+
ScaffoldName = Literal["instructions", "domain", "api", "ui", "web", "storybook", "engine"]
|
|
27
|
+
|
|
28
|
+
_ALL_TARGETS = ["cursor", "copilot", "claude", "gemini"]
|
|
29
|
+
_TARGET_GENERATORS = {
|
|
30
|
+
"cursor": generate_cursor_rules,
|
|
31
|
+
"copilot": generate_copilot_rules,
|
|
32
|
+
"claude": generate_claude_rules,
|
|
33
|
+
"gemini": generate_gemini_rules,
|
|
34
|
+
}
|
|
35
|
+
_NX_BOOTSTRAP_ENV = {"NX_NO_CLOUD": "true"}
|
|
36
|
+
_ROOT_NODE_DEV_DEPENDENCIES = {
|
|
37
|
+
"@eslint/js": "latest",
|
|
38
|
+
"@nx/eslint": "latest",
|
|
39
|
+
"@nx/eslint-plugin": "latest",
|
|
40
|
+
"eslint": "latest",
|
|
41
|
+
"eslint-plugin-import": "latest",
|
|
42
|
+
"eslint-plugin-jsx-a11y": "latest",
|
|
43
|
+
"eslint-plugin-react": "latest",
|
|
44
|
+
"eslint-plugin-react-hooks": "latest",
|
|
45
|
+
"globals": "latest",
|
|
46
|
+
"prettier": "latest",
|
|
47
|
+
"typescript-eslint": "latest",
|
|
48
|
+
}
|
|
49
|
+
_ROOT_NODE_SCRIPTS = {
|
|
50
|
+
"format": "npx prettier --write .",
|
|
51
|
+
"format:check": "npx prettier --check .",
|
|
52
|
+
"lint": "npx nx run-many --target=lint --all",
|
|
53
|
+
"lint:fix": "npx nx run-many --target=lint --all -- --fix",
|
|
54
|
+
"typecheck": "npx nx run-many --target=typecheck --all",
|
|
55
|
+
}
|
|
56
|
+
_SCAFFOLD_DEPENDENCIES: dict[ScaffoldName, tuple[ScaffoldName, ...]] = {
|
|
57
|
+
"instructions": (),
|
|
58
|
+
"domain": (),
|
|
59
|
+
"api": ("domain",),
|
|
60
|
+
"ui": (),
|
|
61
|
+
"web": ("ui",),
|
|
62
|
+
"storybook": ("ui",),
|
|
63
|
+
"engine": (),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Detection helpers
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _detect_nx_state(project_dir: Path) -> NxState:
|
|
73
|
+
"""Classify the target directory as new, existing-non-NX, or already-NX."""
|
|
74
|
+
if not project_dir.exists() or not any(project_dir.iterdir()):
|
|
75
|
+
return "new"
|
|
76
|
+
if (project_dir / "nx.json").exists() or (project_dir / "project.json").exists():
|
|
77
|
+
return "already_nx"
|
|
78
|
+
return "existing_non_nx"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _detect_uv_state(project_dir: Path) -> bool:
|
|
82
|
+
"""Return True when the directory already has a UV-managed pyproject.toml."""
|
|
83
|
+
pyproject = project_dir / "pyproject.toml"
|
|
84
|
+
if not pyproject.exists():
|
|
85
|
+
return False
|
|
86
|
+
try:
|
|
87
|
+
data = tomllib.loads(pyproject.read_text())
|
|
88
|
+
except tomllib.TOMLDecodeError:
|
|
89
|
+
return False
|
|
90
|
+
tool = data.get("tool", {})
|
|
91
|
+
return "uv" in tool
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Variable derivation
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _derive_variables(project_name: str) -> dict[str, str]:
|
|
100
|
+
"""Derive the four canonical token variants from the raw project name."""
|
|
101
|
+
name_underscore = project_name.replace("-", "_")
|
|
102
|
+
pascal = "".join(w.capitalize() for w in re.split(r"[-_]+", project_name) if w)
|
|
103
|
+
return {
|
|
104
|
+
"name": project_name,
|
|
105
|
+
"name-of-app": project_name,
|
|
106
|
+
"name_underscore": name_underscore,
|
|
107
|
+
"NamePascal": pascal,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _normalize_scaffold_name(value: str) -> ScaffoldName:
|
|
112
|
+
normalized = value.strip().lower()
|
|
113
|
+
if normalized not in _SCAFFOLD_DEPENDENCIES:
|
|
114
|
+
allowed = ", ".join(_SCAFFOLD_DEPENDENCIES)
|
|
115
|
+
raise typer.BadParameter(
|
|
116
|
+
f"Unknown scaffold '{value}'. Choose from: {allowed}."
|
|
117
|
+
)
|
|
118
|
+
return normalized # type: ignore[return-value]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _resolve_scaffolds(scaffolds: list[ScaffoldName] | None) -> list[ScaffoldName]:
|
|
122
|
+
requested = scaffolds or ["instructions"]
|
|
123
|
+
resolved: list[ScaffoldName] = []
|
|
124
|
+
seen: set[ScaffoldName] = set()
|
|
125
|
+
|
|
126
|
+
def visit(name: ScaffoldName) -> None:
|
|
127
|
+
if name in seen:
|
|
128
|
+
return
|
|
129
|
+
seen.add(name)
|
|
130
|
+
for dependency in _SCAFFOLD_DEPENDENCIES[name]:
|
|
131
|
+
visit(dependency)
|
|
132
|
+
resolved.append(name)
|
|
133
|
+
|
|
134
|
+
for scaffold_name in requested:
|
|
135
|
+
visit(scaffold_name)
|
|
136
|
+
|
|
137
|
+
return resolved
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _run_requested_scaffolds(
|
|
141
|
+
project_name: str,
|
|
142
|
+
project_dir: Path,
|
|
143
|
+
dry_run: bool,
|
|
144
|
+
scaffolds: list[ScaffoldName],
|
|
145
|
+
domain_migrations: bool,
|
|
146
|
+
) -> None:
|
|
147
|
+
from rpr.commands.generate.api import api as generate_api
|
|
148
|
+
from rpr.commands.generate.domain import domain as generate_domain
|
|
149
|
+
from rpr.commands.generate.engine import engine as generate_engine
|
|
150
|
+
from rpr.commands.generate.storybook import storybook as generate_storybook
|
|
151
|
+
from rpr.commands.generate.ui import ui as generate_ui
|
|
152
|
+
|
|
153
|
+
include_library = "ui" in scaffolds
|
|
154
|
+
include_web = "web" in scaffolds
|
|
155
|
+
|
|
156
|
+
if not scaffolds:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
console.print(
|
|
160
|
+
"\n[bold]Scaffolding requested project components[/]"
|
|
161
|
+
f" ({', '.join(scaffolds)})\n"
|
|
162
|
+
)
|
|
163
|
+
with chdir(project_dir):
|
|
164
|
+
scaffold_generators = {
|
|
165
|
+
"domain": lambda: generate_domain(
|
|
166
|
+
name=project_name,
|
|
167
|
+
dry_run=dry_run,
|
|
168
|
+
migrations=domain_migrations,
|
|
169
|
+
),
|
|
170
|
+
"api": lambda: generate_api(name=project_name, dry_run=dry_run),
|
|
171
|
+
"storybook": lambda: generate_storybook(dry_run=dry_run),
|
|
172
|
+
"engine": lambda: generate_engine(name=project_name, dry_run=dry_run),
|
|
173
|
+
}
|
|
174
|
+
ui_handled = False
|
|
175
|
+
for scaffold in scaffolds:
|
|
176
|
+
if scaffold == "instructions":
|
|
177
|
+
continue
|
|
178
|
+
if scaffold in {"ui", "web"}:
|
|
179
|
+
if ui_handled:
|
|
180
|
+
continue
|
|
181
|
+
generate_ui(
|
|
182
|
+
name=project_name,
|
|
183
|
+
dry_run=dry_run,
|
|
184
|
+
include_library=include_library,
|
|
185
|
+
include_web=include_web,
|
|
186
|
+
)
|
|
187
|
+
ui_handled = True
|
|
188
|
+
continue
|
|
189
|
+
scaffold_generators[scaffold]()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# Package-bundled instruction templates
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _bundled_instructions_dir() -> Path:
|
|
198
|
+
"""Absolute path to the instruction templates bundled with the rpr package."""
|
|
199
|
+
# src/rpr/commands/init.py → parent = commands/ → parent = rpr/
|
|
200
|
+
return Path(__file__).resolve().parent.parent / "scaffolds" / "instructions"
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# AI instruction scaffolding
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _scaffold_ai_instructions(
|
|
209
|
+
ctx: RunContext,
|
|
210
|
+
project_dir: Path,
|
|
211
|
+
variables: dict[str, str],
|
|
212
|
+
) -> None:
|
|
213
|
+
instructions_dir = _bundled_instructions_dir()
|
|
214
|
+
if not instructions_dir.exists():
|
|
215
|
+
console.print(
|
|
216
|
+
f"[bold yellow]Warning:[/] Bundled instructions not found at {instructions_dir}. "
|
|
217
|
+
"Skipping AI instruction scaffolding."
|
|
218
|
+
)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
rules = load_rules(instructions_dir)
|
|
222
|
+
if not rules:
|
|
223
|
+
console.print(
|
|
224
|
+
"[bold yellow]Warning:[/] No instruction rules found. Skipping scaffolding."
|
|
225
|
+
)
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
console.print(f"\n[bold]Scaffolding AI instructions[/] ({len(rules)} rules)\n")
|
|
229
|
+
for target in _ALL_TARGETS:
|
|
230
|
+
console.print(f" [bold]→[/] {target}")
|
|
231
|
+
_TARGET_GENERATORS[target](rules, project_dir, variables, ctx)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# ---------------------------------------------------------------------------
|
|
235
|
+
# .rpr.yaml
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _write_rpr_yaml(
|
|
240
|
+
ctx: RunContext,
|
|
241
|
+
project_dir: Path,
|
|
242
|
+
variables: dict[str, str],
|
|
243
|
+
) -> None:
|
|
244
|
+
instructions_dir = _bundled_instructions_dir()
|
|
245
|
+
config = SyncConfig(
|
|
246
|
+
source=str(instructions_dir),
|
|
247
|
+
targets=list(_ALL_TARGETS),
|
|
248
|
+
include=[],
|
|
249
|
+
exclude=[],
|
|
250
|
+
variables=variables,
|
|
251
|
+
)
|
|
252
|
+
config_path = project_dir / ".rpr.yaml"
|
|
253
|
+
|
|
254
|
+
if ctx.dry_run:
|
|
255
|
+
import yaml
|
|
256
|
+
|
|
257
|
+
console.print(f"\n[bold yellow][DRY RUN][/] Would write: {config_path}")
|
|
258
|
+
preview = yaml.dump(
|
|
259
|
+
{
|
|
260
|
+
"source": config.source,
|
|
261
|
+
"targets": config.targets,
|
|
262
|
+
"include": config.include,
|
|
263
|
+
"exclude": config.exclude,
|
|
264
|
+
"variables": config.variables,
|
|
265
|
+
},
|
|
266
|
+
default_flow_style=False,
|
|
267
|
+
sort_keys=False,
|
|
268
|
+
)
|
|
269
|
+
console.print(f"[dim]{'─' * 60}[/]")
|
|
270
|
+
for i, line in enumerate(preview.splitlines(), 1):
|
|
271
|
+
console.print(f" [dim]{i:>4}[/] │ {line}")
|
|
272
|
+
console.print(f"[dim]{'─' * 60}[/]")
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
config.save(config_path)
|
|
276
|
+
console.print(f"[bold green]Created:[/] {config_path}")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
# Root config files
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _write_nx_json(ctx: RunContext) -> None:
|
|
285
|
+
content = """{
|
|
286
|
+
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
|
287
|
+
"neverConnectToCloud": true,
|
|
288
|
+
"plugins": [
|
|
289
|
+
{
|
|
290
|
+
"plugin": "@nx/eslint/plugin",
|
|
291
|
+
"options": {
|
|
292
|
+
"targetName": "lint"
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
],
|
|
296
|
+
"targetDefaults": {
|
|
297
|
+
"build": {
|
|
298
|
+
"dependsOn": ["^build"],
|
|
299
|
+
"cache": true
|
|
300
|
+
},
|
|
301
|
+
"format": {
|
|
302
|
+
"cache": true
|
|
303
|
+
},
|
|
304
|
+
"test": {
|
|
305
|
+
"cache": true
|
|
306
|
+
},
|
|
307
|
+
"lint": {
|
|
308
|
+
"cache": true
|
|
309
|
+
},
|
|
310
|
+
"typecheck": {
|
|
311
|
+
"cache": true
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
"defaultBase": "main"
|
|
315
|
+
}
|
|
316
|
+
"""
|
|
317
|
+
ctx.write_file(ctx.project_dir / "nx.json", content)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _write_root_pyproject(
|
|
321
|
+
ctx: RunContext, project_name: str, python_version: str
|
|
322
|
+
) -> None:
|
|
323
|
+
uv_require_version = _detect_uv_require_version()
|
|
324
|
+
content = f"""[project]
|
|
325
|
+
name = "{project_name}"
|
|
326
|
+
version = "0.1.0"
|
|
327
|
+
description = "A monorepo combining NX and UV"
|
|
328
|
+
readme = "README.md"
|
|
329
|
+
requires-python = ">={python_version}"
|
|
330
|
+
dependencies = []
|
|
331
|
+
|
|
332
|
+
[tool.uv]
|
|
333
|
+
require-version = "{uv_require_version}"
|
|
334
|
+
|
|
335
|
+
[tool.uv.workspace]
|
|
336
|
+
members = []
|
|
337
|
+
|
|
338
|
+
[dependency-groups]
|
|
339
|
+
dev = [
|
|
340
|
+
"pytest",
|
|
341
|
+
"pytest-asyncio",
|
|
342
|
+
"pytest-cov",
|
|
343
|
+
"ipykernel",
|
|
344
|
+
"pre-commit",
|
|
345
|
+
"pyright",
|
|
346
|
+
"ruff",
|
|
347
|
+
"maturin",
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
[tool.ruff]
|
|
351
|
+
line-length = 88
|
|
352
|
+
target-version = "py{python_version.replace(".", "")}"
|
|
353
|
+
exclude = ["**/migrations/**"]
|
|
354
|
+
|
|
355
|
+
[tool.ruff.lint]
|
|
356
|
+
select = ["E", "F", "W", "I001"]
|
|
357
|
+
ignore = ["E501", "B008", "C901"]
|
|
358
|
+
|
|
359
|
+
[tool.ruff.lint.per-file-ignores]
|
|
360
|
+
"__init__.py" = ["F401"]
|
|
361
|
+
"tests/**/*.py" = ["F841"]
|
|
362
|
+
|
|
363
|
+
[tool.ruff.format]
|
|
364
|
+
quote-style = "double"
|
|
365
|
+
indent-style = "space"
|
|
366
|
+
line-ending = "lf"
|
|
367
|
+
skip-magic-trailing-comma = false
|
|
368
|
+
|
|
369
|
+
[tool.pytest.ini_options]
|
|
370
|
+
pythonpath = []
|
|
371
|
+
testpaths = []
|
|
372
|
+
"""
|
|
373
|
+
ctx.update_file(ctx.project_dir / "pyproject.toml", content)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _detect_uv_require_version() -> str:
|
|
377
|
+
try:
|
|
378
|
+
result = subprocess.run(
|
|
379
|
+
["uv", "--version"],
|
|
380
|
+
capture_output=True,
|
|
381
|
+
text=True,
|
|
382
|
+
check=True,
|
|
383
|
+
)
|
|
384
|
+
except (OSError, subprocess.CalledProcessError):
|
|
385
|
+
return ">=0"
|
|
386
|
+
|
|
387
|
+
match = re.search(r"(\d+\.\d+(?:\.\d+)?)", result.stdout)
|
|
388
|
+
if not match:
|
|
389
|
+
return ">=0"
|
|
390
|
+
return f">={match.group(1)}"
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _write_root_package_json(ctx: RunContext, project_name: str) -> None:
|
|
394
|
+
ensure_npm_workspaces(ctx, package_name=project_name)
|
|
395
|
+
|
|
396
|
+
package_path = ctx.project_dir / "package.json"
|
|
397
|
+
package = json.loads(package_path.read_text())
|
|
398
|
+
package.setdefault("name", project_name)
|
|
399
|
+
package["private"] = True
|
|
400
|
+
|
|
401
|
+
scripts = package.setdefault("scripts", {})
|
|
402
|
+
if not isinstance(scripts, dict):
|
|
403
|
+
scripts = {}
|
|
404
|
+
package["scripts"] = scripts
|
|
405
|
+
scripts.update(_ROOT_NODE_SCRIPTS)
|
|
406
|
+
|
|
407
|
+
dev_dependencies = package.setdefault("devDependencies", {})
|
|
408
|
+
if not isinstance(dev_dependencies, dict):
|
|
409
|
+
dev_dependencies = {}
|
|
410
|
+
package["devDependencies"] = dev_dependencies
|
|
411
|
+
|
|
412
|
+
for name, version in _ROOT_NODE_DEV_DEPENDENCIES.items():
|
|
413
|
+
dev_dependencies.setdefault(name, version)
|
|
414
|
+
|
|
415
|
+
ctx.update_file(package_path, json.dumps(package, indent=2) + "\n")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _write_root_eslint_config(ctx: RunContext) -> None:
|
|
419
|
+
content = """import js from \"@eslint/js\";
|
|
420
|
+
import nxPlugin from \"@nx/eslint-plugin\";
|
|
421
|
+
import globals from \"globals\";
|
|
422
|
+
import importPlugin from \"eslint-plugin-import\";
|
|
423
|
+
import jsxA11yPlugin from \"eslint-plugin-jsx-a11y\";
|
|
424
|
+
import reactPlugin from \"eslint-plugin-react\";
|
|
425
|
+
import reactHooksPlugin from \"eslint-plugin-react-hooks\";
|
|
426
|
+
import tseslint from \"typescript-eslint\";
|
|
427
|
+
|
|
428
|
+
export default [
|
|
429
|
+
{
|
|
430
|
+
ignores: [\"build/**\", \"coverage/**\", \"dist/**\", \"node_modules/**\", ".nx/**"],
|
|
431
|
+
},
|
|
432
|
+
js.configs.recommended,
|
|
433
|
+
...tseslint.configs.recommended,
|
|
434
|
+
{
|
|
435
|
+
files: [\"**/*.{js,jsx,ts,tsx}\"],
|
|
436
|
+
plugins: {
|
|
437
|
+
\"@nx\": nxPlugin,
|
|
438
|
+
import: importPlugin,
|
|
439
|
+
\"jsx-a11y\": jsxA11yPlugin,
|
|
440
|
+
react: reactPlugin,
|
|
441
|
+
\"react-hooks\": reactHooksPlugin,
|
|
442
|
+
},
|
|
443
|
+
languageOptions: {
|
|
444
|
+
parser: tseslint.parser,
|
|
445
|
+
parserOptions: {
|
|
446
|
+
ecmaFeatures: {
|
|
447
|
+
jsx: true,
|
|
448
|
+
},
|
|
449
|
+
ecmaVersion: \"latest\",
|
|
450
|
+
sourceType: \"module\",
|
|
451
|
+
},
|
|
452
|
+
globals: {
|
|
453
|
+
...globals.browser,
|
|
454
|
+
...globals.node,
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
settings: {
|
|
458
|
+
react: {
|
|
459
|
+
version: \"detect\",
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
rules: {
|
|
463
|
+
\"@nx/enforce-module-boundaries\": [
|
|
464
|
+
\"error\",
|
|
465
|
+
{
|
|
466
|
+
enforceBuildableLibDependency: true,
|
|
467
|
+
allow: [],
|
|
468
|
+
depConstraints: [
|
|
469
|
+
{
|
|
470
|
+
sourceTag: \"*\",
|
|
471
|
+
onlyDependOnLibsWithTags: [\"*\"],
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
\"react-hooks/exhaustive-deps\": \"warn\",
|
|
477
|
+
\"react-hooks/rules-of-hooks\": \"error\",
|
|
478
|
+
\"react/jsx-uses-react\": \"off\",
|
|
479
|
+
\"react/react-in-jsx-scope\": \"off\",
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
];
|
|
483
|
+
"""
|
|
484
|
+
ctx.write_file(ctx.project_dir / "eslint.config.mjs", content)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _write_prettier_config(ctx: RunContext) -> None:
|
|
488
|
+
content = """{
|
|
489
|
+
\"singleQuote\": true,
|
|
490
|
+
\"printWidth\": 100,
|
|
491
|
+
\"semi\": true,
|
|
492
|
+
\"tabWidth\": 2,
|
|
493
|
+
\"trailingComma\": \"es5\"
|
|
494
|
+
}
|
|
495
|
+
"""
|
|
496
|
+
ctx.write_file(ctx.project_dir / ".prettierrc", content)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _write_prettierignore(ctx: RunContext) -> None:
|
|
500
|
+
content = """build/
|
|
501
|
+
coverage/
|
|
502
|
+
dist/
|
|
503
|
+
node_modules/
|
|
504
|
+
.nx/
|
|
505
|
+
"""
|
|
506
|
+
ctx.write_file(ctx.project_dir / ".prettierignore", content)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _write_pre_commit_config(ctx: RunContext) -> None:
|
|
510
|
+
content = """repos:
|
|
511
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
512
|
+
rev: v6.0.0
|
|
513
|
+
hooks:
|
|
514
|
+
- id: check-merge-conflict
|
|
515
|
+
- id: end-of-file-fixer
|
|
516
|
+
- id: trailing-whitespace
|
|
517
|
+
args: [--markdown-linebreak-ext=md]
|
|
518
|
+
- id: fix-byte-order-marker
|
|
519
|
+
- id: mixed-line-ending
|
|
520
|
+
args: [--fix=lf]
|
|
521
|
+
exclude: \\.ipynb$
|
|
522
|
+
- id: detect-private-key
|
|
523
|
+
|
|
524
|
+
- repo: https://github.com/kynan/nbstripout
|
|
525
|
+
rev: v0.8.2
|
|
526
|
+
hooks:
|
|
527
|
+
- id: nbstripout
|
|
528
|
+
|
|
529
|
+
- repo: local
|
|
530
|
+
hooks:
|
|
531
|
+
- id: ruff-format
|
|
532
|
+
name: Ruff format
|
|
533
|
+
entry: uv run ruff format
|
|
534
|
+
language: system
|
|
535
|
+
types: [python]
|
|
536
|
+
pass_filenames: false
|
|
537
|
+
|
|
538
|
+
- id: ruff-lint
|
|
539
|
+
name: Ruff lint
|
|
540
|
+
entry: uv run ruff check --fix
|
|
541
|
+
language: system
|
|
542
|
+
types: [python]
|
|
543
|
+
pass_filenames: false
|
|
544
|
+
|
|
545
|
+
- id: prettier
|
|
546
|
+
name: Prettier format
|
|
547
|
+
entry: npx prettier --write --log-level=warn
|
|
548
|
+
language: system
|
|
549
|
+
types_or: [javascript, jsx, ts, tsx, json, css, scss, html, markdown]
|
|
550
|
+
exclude: \\.ipynb$
|
|
551
|
+
pass_filenames: true
|
|
552
|
+
|
|
553
|
+
- id: eslint
|
|
554
|
+
name: ESLint (NX)
|
|
555
|
+
entry: npx nx run-many --target=lint --all --fix --exclude=engine
|
|
556
|
+
language: system
|
|
557
|
+
types_or: [javascript, jsx, ts, tsx]
|
|
558
|
+
pass_filenames: false
|
|
559
|
+
"""
|
|
560
|
+
ctx.write_file(ctx.project_dir / ".pre-commit-config.yaml", content)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _write_tsconfig_base(ctx: RunContext) -> None:
|
|
564
|
+
content = """{
|
|
565
|
+
"compilerOptions": {
|
|
566
|
+
"target": "ES2022",
|
|
567
|
+
"module": "ESNext",
|
|
568
|
+
"moduleResolution": "bundler",
|
|
569
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
570
|
+
"jsx": "react-jsx",
|
|
571
|
+
"strict": true,
|
|
572
|
+
"esModuleInterop": true,
|
|
573
|
+
"skipLibCheck": true,
|
|
574
|
+
"forceConsistentCasingInFileNames": true,
|
|
575
|
+
"resolveJsonModule": true,
|
|
576
|
+
"isolatedModules": true,
|
|
577
|
+
"noEmit": true,
|
|
578
|
+
"declaration": true,
|
|
579
|
+
"declarationMap": true,
|
|
580
|
+
"sourceMap": true,
|
|
581
|
+
"baseUrl": ".",
|
|
582
|
+
"paths": {}
|
|
583
|
+
},
|
|
584
|
+
"exclude": ["node_modules", "dist", "build"]
|
|
585
|
+
}
|
|
586
|
+
"""
|
|
587
|
+
ctx.write_file(ctx.project_dir / "tsconfig.base.json", content)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _write_gitignore(ctx: RunContext) -> None:
|
|
591
|
+
content = """# Node
|
|
592
|
+
node_modules/
|
|
593
|
+
dist/
|
|
594
|
+
build/
|
|
595
|
+
.next/
|
|
596
|
+
*.tsbuildinfo
|
|
597
|
+
|
|
598
|
+
# Python
|
|
599
|
+
__pycache__/
|
|
600
|
+
*.py[cod]
|
|
601
|
+
*.egg-info/
|
|
602
|
+
.venv/
|
|
603
|
+
*.egg
|
|
604
|
+
|
|
605
|
+
# Rust
|
|
606
|
+
target/
|
|
607
|
+
|
|
608
|
+
# Environment
|
|
609
|
+
.env
|
|
610
|
+
.env.local
|
|
611
|
+
.env.*.local
|
|
612
|
+
|
|
613
|
+
# IDE
|
|
614
|
+
.idea/
|
|
615
|
+
.vscode/
|
|
616
|
+
*.swp
|
|
617
|
+
*.swo
|
|
618
|
+
|
|
619
|
+
# OS
|
|
620
|
+
.DS_Store
|
|
621
|
+
Thumbs.db
|
|
622
|
+
|
|
623
|
+
# NX
|
|
624
|
+
.nx/
|
|
625
|
+
|
|
626
|
+
# UV
|
|
627
|
+
uv.lock
|
|
628
|
+
"""
|
|
629
|
+
ctx.write_file(ctx.project_dir / ".gitignore", content)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _write_env_example(ctx: RunContext) -> None:
|
|
633
|
+
content = """# Database
|
|
634
|
+
DATABASE_URL=
|
|
635
|
+
|
|
636
|
+
# Auth
|
|
637
|
+
OAUTH_CLIENT_ID=
|
|
638
|
+
OAUTH_CLIENT_SECRET=
|
|
639
|
+
|
|
640
|
+
# Encryption
|
|
641
|
+
FERNET_KEY=
|
|
642
|
+
|
|
643
|
+
# API
|
|
644
|
+
API_BASE_URL=http://localhost:8000
|
|
645
|
+
"""
|
|
646
|
+
ctx.write_file(ctx.project_dir / ".env.example", content)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
# ---------------------------------------------------------------------------
|
|
650
|
+
# Command
|
|
651
|
+
# ---------------------------------------------------------------------------
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
@app.command()
|
|
655
|
+
def init(
|
|
656
|
+
project_name: Annotated[
|
|
657
|
+
str | None, typer.Argument(help="Name of the project to create.")
|
|
658
|
+
] = None,
|
|
659
|
+
dry_run: Annotated[
|
|
660
|
+
bool, typer.Option("--dry-run", help="Preview changes without writing.")
|
|
661
|
+
] = False,
|
|
662
|
+
python_version: Annotated[
|
|
663
|
+
str, typer.Option("--python", help="Python version to use.")
|
|
664
|
+
] = "3.14",
|
|
665
|
+
integrated: Annotated[
|
|
666
|
+
bool, typer.Option("--integrated", help="Use NX integrated monorepo layout.")
|
|
667
|
+
] = False,
|
|
668
|
+
scaffold: Annotated[
|
|
669
|
+
list[str] | None,
|
|
670
|
+
typer.Option(
|
|
671
|
+
"--scaffold",
|
|
672
|
+
help=(
|
|
673
|
+
"Repeat to scaffold additional components: instructions, domain, "
|
|
674
|
+
"api, ui, web, storybook, engine. Defaults to instructions only."
|
|
675
|
+
),
|
|
676
|
+
),
|
|
677
|
+
] = None,
|
|
678
|
+
domain_migrations: Annotated[
|
|
679
|
+
bool,
|
|
680
|
+
typer.Option(
|
|
681
|
+
"--domain-migrations",
|
|
682
|
+
help=(
|
|
683
|
+
"Scaffold Alembic migrations when the selected scaffolds include "
|
|
684
|
+
"the domain package, directly or through a dependency such as api."
|
|
685
|
+
),
|
|
686
|
+
),
|
|
687
|
+
] = False,
|
|
688
|
+
) -> None:
|
|
689
|
+
"""Scaffold a complete NX + UV monorepo with AI instructions."""
|
|
690
|
+
if not project_name:
|
|
691
|
+
project_name = typer.prompt("Project name")
|
|
692
|
+
|
|
693
|
+
selected_scaffolds = _resolve_scaffolds(
|
|
694
|
+
[_normalize_scaffold_name(value) for value in scaffold or []]
|
|
695
|
+
)
|
|
696
|
+
if domain_migrations and "domain" not in selected_scaffolds:
|
|
697
|
+
raise typer.BadParameter(
|
|
698
|
+
"--domain-migrations requires a scaffold that generates the domain "
|
|
699
|
+
"package, such as --scaffold domain or --scaffold api."
|
|
700
|
+
)
|
|
701
|
+
variables = _derive_variables(project_name)
|
|
702
|
+
project_dir = Path.cwd() / project_name
|
|
703
|
+
ctx = RunContext(dry_run=dry_run, project_dir=project_dir)
|
|
704
|
+
|
|
705
|
+
console.print(f"\n[bold]Initializing RPR monorepo:[/] {project_name}\n")
|
|
706
|
+
|
|
707
|
+
# ------------------------------------------------------------------
|
|
708
|
+
# Step 1 – NX setup
|
|
709
|
+
# ------------------------------------------------------------------
|
|
710
|
+
nx_state = _detect_nx_state(project_dir)
|
|
711
|
+
|
|
712
|
+
if nx_state == "already_nx":
|
|
713
|
+
console.print("[bold yellow]Already an NX project.[/] Skipping NX setup.")
|
|
714
|
+
elif nx_state == "new":
|
|
715
|
+
nx_args = [
|
|
716
|
+
f"--name={project_name}",
|
|
717
|
+
"--preset=apps",
|
|
718
|
+
"--nxCloud=never",
|
|
719
|
+
"--interactive=false",
|
|
720
|
+
]
|
|
721
|
+
if integrated:
|
|
722
|
+
nx_args.append("--integrated")
|
|
723
|
+
ctx.run_command(
|
|
724
|
+
["npx", "--yes", "create-nx-workspace@latest"] + nx_args,
|
|
725
|
+
cwd=Path.cwd(),
|
|
726
|
+
env=_NX_BOOTSTRAP_ENV,
|
|
727
|
+
)
|
|
728
|
+
else: # existing_non_nx
|
|
729
|
+
if not dry_run:
|
|
730
|
+
console.print(
|
|
731
|
+
f"[bold yellow]Warning:[/] Directory {project_dir} already exists."
|
|
732
|
+
)
|
|
733
|
+
if not typer.confirm("Continue with initialization in existing directory?"):
|
|
734
|
+
raise typer.Exit(0)
|
|
735
|
+
ctx.run_command(["git", "init"], cwd=project_dir)
|
|
736
|
+
nx_cmd = [
|
|
737
|
+
"npx",
|
|
738
|
+
"--yes",
|
|
739
|
+
"nx",
|
|
740
|
+
"init",
|
|
741
|
+
"--interactive=false",
|
|
742
|
+
"--nxCloud=false",
|
|
743
|
+
]
|
|
744
|
+
if integrated:
|
|
745
|
+
nx_cmd.append("--integrated")
|
|
746
|
+
ctx.run_command(nx_cmd, cwd=project_dir, env=_NX_BOOTSTRAP_ENV)
|
|
747
|
+
|
|
748
|
+
# ------------------------------------------------------------------
|
|
749
|
+
# Step 2 – UV setup
|
|
750
|
+
# ------------------------------------------------------------------
|
|
751
|
+
if _detect_uv_state(project_dir):
|
|
752
|
+
console.print("[bold yellow]Already a UV project.[/] Skipping uv init.")
|
|
753
|
+
else:
|
|
754
|
+
ctx.run_command(
|
|
755
|
+
[
|
|
756
|
+
"uv",
|
|
757
|
+
"init",
|
|
758
|
+
"--no-readme",
|
|
759
|
+
"--no-pin-python",
|
|
760
|
+
"--python",
|
|
761
|
+
python_version,
|
|
762
|
+
],
|
|
763
|
+
cwd=project_dir,
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
# ------------------------------------------------------------------
|
|
767
|
+
# Step 3 – Standard directories
|
|
768
|
+
# ------------------------------------------------------------------
|
|
769
|
+
ctx.ensure_dir(project_dir / "apps")
|
|
770
|
+
ctx.ensure_dir(project_dir / "packages" / "node")
|
|
771
|
+
ctx.ensure_dir(project_dir / "packages" / "python")
|
|
772
|
+
|
|
773
|
+
# ------------------------------------------------------------------
|
|
774
|
+
# Step 4 – Root config files
|
|
775
|
+
# ------------------------------------------------------------------
|
|
776
|
+
_write_nx_json(ctx)
|
|
777
|
+
_write_root_pyproject(ctx, project_name, python_version)
|
|
778
|
+
_write_root_package_json(ctx, project_name)
|
|
779
|
+
_write_tsconfig_base(ctx)
|
|
780
|
+
_write_root_eslint_config(ctx)
|
|
781
|
+
_write_prettier_config(ctx)
|
|
782
|
+
_write_prettierignore(ctx)
|
|
783
|
+
_write_pre_commit_config(ctx)
|
|
784
|
+
_write_gitignore(ctx)
|
|
785
|
+
_write_env_example(ctx)
|
|
786
|
+
|
|
787
|
+
# ------------------------------------------------------------------
|
|
788
|
+
# Step 5 – AI instruction files
|
|
789
|
+
# ------------------------------------------------------------------
|
|
790
|
+
if "instructions" in selected_scaffolds:
|
|
791
|
+
_scaffold_ai_instructions(ctx, project_dir, variables)
|
|
792
|
+
|
|
793
|
+
# ------------------------------------------------------------------
|
|
794
|
+
# Step 6 – .rpr.yaml (source path + variables block)
|
|
795
|
+
# ------------------------------------------------------------------
|
|
796
|
+
_write_rpr_yaml(ctx, project_dir, variables)
|
|
797
|
+
|
|
798
|
+
# ------------------------------------------------------------------
|
|
799
|
+
# Step 7 – Sync tooling dependencies
|
|
800
|
+
# ------------------------------------------------------------------
|
|
801
|
+
ctx.run_command(["npm", "install"], cwd=project_dir)
|
|
802
|
+
|
|
803
|
+
# ------------------------------------------------------------------
|
|
804
|
+
# Step 8 – Sync Python dependencies
|
|
805
|
+
# ------------------------------------------------------------------
|
|
806
|
+
ctx.run_command(["uv", "sync"], cwd=project_dir)
|
|
807
|
+
|
|
808
|
+
# ------------------------------------------------------------------
|
|
809
|
+
# Step 9 – Requested project scaffolds
|
|
810
|
+
# ------------------------------------------------------------------
|
|
811
|
+
_run_requested_scaffolds(
|
|
812
|
+
project_name,
|
|
813
|
+
project_dir,
|
|
814
|
+
dry_run,
|
|
815
|
+
selected_scaffolds,
|
|
816
|
+
domain_migrations,
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
console.print(f"\n[bold green]Done![/] Project created at {project_dir}")
|
|
820
|
+
console.print(f" cd {project_name}")
|
|
821
|
+
if len(selected_scaffolds) == 1 and selected_scaffolds[0] == "instructions":
|
|
822
|
+
console.print(f" rpr generate domain {project_name}")
|