remoro 0.1.0__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.
- remo/__init__.py +5 -0
- remo/_reminder_worker.py +32 -0
- remo/checks/__init__.py +1 -0
- remo/checks/cmd_check.py +66 -0
- remo/checks/env_check.py +90 -0
- remo/checks/file_check.py +40 -0
- remo/checks/version_check.py +110 -0
- remo/cli.py +34 -0
- remo/commands/__init__.py +1 -0
- remo/commands/check.py +146 -0
- remo/commands/init.py +225 -0
- remo/commands/log.py +120 -0
- remo/commands/remind.py +149 -0
- remo/commands/run.py +116 -0
- remo/commands/snooze.py +97 -0
- remo/commands/startup.py +242 -0
- remo/commands/status.py +113 -0
- remo/config/__init__.py +1 -0
- remo/config/global_config.py +48 -0
- remo/config/loader.py +120 -0
- remo/config/schema.py +148 -0
- remo/notifications/__init__.py +1 -0
- remo/notifications/dispatcher.py +125 -0
- remo/notifications/fallback.py +20 -0
- remo/notifications/linux.py +19 -0
- remo/notifications/macos.py +17 -0
- remo/notifications/windows.py +34 -0
- remo/ui/__init__.py +1 -0
- remo/ui/colors.py +26 -0
- remo/ui/icons.py +46 -0
- remo/ui/panels.py +56 -0
- remoro-0.1.0.dist-info/METADATA +132 -0
- remoro-0.1.0.dist-info/RECORD +35 -0
- remoro-0.1.0.dist-info/WHEEL +4 -0
- remoro-0.1.0.dist-info/entry_points.txt +2 -0
remo/__init__.py
ADDED
remo/_reminder_worker.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Detached reminder worker — run as a subprocess, sleep, then notify.
|
|
2
|
+
|
|
3
|
+
Usage (internal — spawned by remo remind):
|
|
4
|
+
python -m remo._reminder_worker <seconds> <message> [project_name]
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
"""Sleep for the specified duration then fire a notification."""
|
|
13
|
+
if len(sys.argv) < 3:
|
|
14
|
+
sys.exit(1)
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
seconds = int(sys.argv[1])
|
|
18
|
+
except ValueError:
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
message = sys.argv[2]
|
|
22
|
+
project_name = sys.argv[3] if len(sys.argv) > 3 else "remo"
|
|
23
|
+
|
|
24
|
+
time.sleep(seconds)
|
|
25
|
+
|
|
26
|
+
title = f"remo · {project_name}"
|
|
27
|
+
from remo.notifications.dispatcher import notify
|
|
28
|
+
notify(title, message)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
main()
|
remo/checks/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Checks sub-package."""
|
remo/checks/cmd_check.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Check a shell command's exit code."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from typing import NamedTuple, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CmdCheckResult(NamedTuple):
|
|
8
|
+
"""Result of a command exit-code check."""
|
|
9
|
+
|
|
10
|
+
status: str # "pass" | "fail"
|
|
11
|
+
cmd: str
|
|
12
|
+
label: str
|
|
13
|
+
exit_code: Optional[int]
|
|
14
|
+
message: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_cmd_check(cmd: str, label: Optional[str] = None) -> CmdCheckResult:
|
|
18
|
+
"""Run a shell command and check that it exits with code 0.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
cmd: Shell command string to execute.
|
|
22
|
+
label: Human-readable label for display (defaults to cmd).
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
CmdCheckResult with status and exit code.
|
|
26
|
+
"""
|
|
27
|
+
display_label = label or cmd
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
result = subprocess.run(
|
|
31
|
+
cmd,
|
|
32
|
+
shell=True,
|
|
33
|
+
capture_output=True,
|
|
34
|
+
timeout=15,
|
|
35
|
+
)
|
|
36
|
+
if result.returncode == 0:
|
|
37
|
+
return CmdCheckResult(
|
|
38
|
+
status="pass",
|
|
39
|
+
cmd=cmd,
|
|
40
|
+
label=display_label,
|
|
41
|
+
exit_code=0,
|
|
42
|
+
message="running (exit 0)",
|
|
43
|
+
)
|
|
44
|
+
return CmdCheckResult(
|
|
45
|
+
status="fail",
|
|
46
|
+
cmd=cmd,
|
|
47
|
+
label=display_label,
|
|
48
|
+
exit_code=result.returncode,
|
|
49
|
+
message=f"exited with code {result.returncode}",
|
|
50
|
+
)
|
|
51
|
+
except subprocess.TimeoutExpired:
|
|
52
|
+
return CmdCheckResult(
|
|
53
|
+
status="fail",
|
|
54
|
+
cmd=cmd,
|
|
55
|
+
label=display_label,
|
|
56
|
+
exit_code=None,
|
|
57
|
+
message="command timed out after 15s",
|
|
58
|
+
)
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
return CmdCheckResult(
|
|
61
|
+
status="fail",
|
|
62
|
+
cmd=cmd,
|
|
63
|
+
label=display_label,
|
|
64
|
+
exit_code=None,
|
|
65
|
+
message=f"error running command: {exc}",
|
|
66
|
+
)
|
remo/checks/env_check.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Check .env file against .env.example — report missing keys."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, NamedTuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EnvCheckResult(NamedTuple):
|
|
8
|
+
"""Result of an .env check."""
|
|
9
|
+
|
|
10
|
+
status: str # "pass" | "fail" | "skip"
|
|
11
|
+
missing_keys: List[str]
|
|
12
|
+
present_count: int
|
|
13
|
+
total_count: int
|
|
14
|
+
message: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _parse_env_keys(path: Path) -> List[str]:
|
|
18
|
+
"""Parse key names from a .env-style file (ignores comments and blanks)."""
|
|
19
|
+
keys: List[str] = []
|
|
20
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
21
|
+
for line in fh:
|
|
22
|
+
line = line.strip()
|
|
23
|
+
if not line or line.startswith("#"):
|
|
24
|
+
continue
|
|
25
|
+
if "=" in line:
|
|
26
|
+
key = line.split("=", 1)[0].strip()
|
|
27
|
+
if key:
|
|
28
|
+
keys.append(key)
|
|
29
|
+
return keys
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_env_check(
|
|
33
|
+
example_file: str,
|
|
34
|
+
env_file: str = ".env",
|
|
35
|
+
base_dir: Path = Path("."),
|
|
36
|
+
) -> EnvCheckResult:
|
|
37
|
+
"""Compare .env against .env.example and report missing keys.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
example_file: Path to the .env.example file (relative to base_dir).
|
|
41
|
+
env_file: Path to the .env file (relative to base_dir).
|
|
42
|
+
base_dir: Directory to resolve paths against.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
EnvCheckResult with status, missing keys, and counts.
|
|
46
|
+
"""
|
|
47
|
+
example_path = base_dir / example_file
|
|
48
|
+
env_path = base_dir / env_file
|
|
49
|
+
|
|
50
|
+
if not example_path.exists():
|
|
51
|
+
return EnvCheckResult(
|
|
52
|
+
status="skip",
|
|
53
|
+
missing_keys=[],
|
|
54
|
+
present_count=0,
|
|
55
|
+
total_count=0,
|
|
56
|
+
message=f"{example_file} not found — skipping env check",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
example_keys = _parse_env_keys(example_path)
|
|
60
|
+
total = len(example_keys)
|
|
61
|
+
|
|
62
|
+
if not env_path.exists():
|
|
63
|
+
return EnvCheckResult(
|
|
64
|
+
status="fail",
|
|
65
|
+
missing_keys=example_keys,
|
|
66
|
+
present_count=0,
|
|
67
|
+
total_count=total,
|
|
68
|
+
message=f".env not found — all {total} keys missing",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
env_keys = set(_parse_env_keys(env_path))
|
|
72
|
+
missing = [k for k in example_keys if k not in env_keys]
|
|
73
|
+
present = total - len(missing)
|
|
74
|
+
|
|
75
|
+
if missing:
|
|
76
|
+
return EnvCheckResult(
|
|
77
|
+
status="fail",
|
|
78
|
+
missing_keys=missing,
|
|
79
|
+
present_count=present,
|
|
80
|
+
total_count=total,
|
|
81
|
+
message=f"missing keys: {', '.join(missing)}",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return EnvCheckResult(
|
|
85
|
+
status="pass",
|
|
86
|
+
missing_keys=[],
|
|
87
|
+
present_count=total,
|
|
88
|
+
total_count=total,
|
|
89
|
+
message=f"all {total} keys present",
|
|
90
|
+
)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Check that a required file exists on disk."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import NamedTuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class FileCheckResult(NamedTuple):
|
|
8
|
+
"""Result of a file existence check."""
|
|
9
|
+
|
|
10
|
+
status: str # "pass" | "fail"
|
|
11
|
+
path: str
|
|
12
|
+
message: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_file_check(path: str, base_dir: Path = Path(".")) -> FileCheckResult:
|
|
16
|
+
"""Assert that a file exists.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
path: File path to check (relative to base_dir or absolute).
|
|
20
|
+
base_dir: Directory to resolve relative paths against.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
FileCheckResult with status and message.
|
|
24
|
+
"""
|
|
25
|
+
target = Path(path)
|
|
26
|
+
if not target.is_absolute():
|
|
27
|
+
target = base_dir / path
|
|
28
|
+
|
|
29
|
+
if target.exists():
|
|
30
|
+
return FileCheckResult(
|
|
31
|
+
status="pass",
|
|
32
|
+
path=str(path),
|
|
33
|
+
message="exists",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return FileCheckResult(
|
|
37
|
+
status="fail",
|
|
38
|
+
path=str(path),
|
|
39
|
+
message=f"file not found: {target}",
|
|
40
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Check that a tool meets a minimum version requirement."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import List, NamedTuple, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VersionCheckResult(NamedTuple):
|
|
10
|
+
"""Result of a version check."""
|
|
11
|
+
|
|
12
|
+
status: str # "pass" | "fail" | "missing"
|
|
13
|
+
tool: str
|
|
14
|
+
required: str
|
|
15
|
+
found: Optional[str]
|
|
16
|
+
message: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_version(version_str: str) -> Optional[Tuple[int, ...]]:
|
|
20
|
+
"""Parse a version string like '3.11.2' into a tuple of ints."""
|
|
21
|
+
match = re.search(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?", version_str)
|
|
22
|
+
if not match:
|
|
23
|
+
return None
|
|
24
|
+
parts = [int(g) for g in match.groups() if g is not None]
|
|
25
|
+
return tuple(parts)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_tool_version(tool: str) -> Optional[str]:
|
|
29
|
+
"""Run `tool --version` and return the output, or None if not found."""
|
|
30
|
+
if shutil.which(tool) is None:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
# Some tools use `--version`, some use `version`, some use `-v`
|
|
34
|
+
for flag in ["--version", "-v", "version"]:
|
|
35
|
+
try:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
[tool, flag],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
timeout=5,
|
|
41
|
+
)
|
|
42
|
+
output = (result.stdout + result.stderr).strip()
|
|
43
|
+
if output:
|
|
44
|
+
return output
|
|
45
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _compare_versions(found: Tuple[int, ...], required: Tuple[int, ...]) -> bool:
|
|
52
|
+
"""Return True if found >= required."""
|
|
53
|
+
# Pad shorter tuple with zeros
|
|
54
|
+
max_len = max(len(found), len(required))
|
|
55
|
+
found_padded = found + (0,) * (max_len - len(found))
|
|
56
|
+
req_padded = required + (0,) * (max_len - len(required))
|
|
57
|
+
return found_padded >= req_padded
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_version_check(tool: str, min_version: str) -> VersionCheckResult:
|
|
61
|
+
"""Check that `tool` is installed and meets the minimum version.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
tool: The tool name (e.g., "python", "node").
|
|
65
|
+
min_version: Minimum required version string (e.g., "3.11").
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
VersionCheckResult with status and details.
|
|
69
|
+
"""
|
|
70
|
+
version_output = _get_tool_version(tool)
|
|
71
|
+
|
|
72
|
+
if version_output is None:
|
|
73
|
+
return VersionCheckResult(
|
|
74
|
+
status="missing",
|
|
75
|
+
tool=tool,
|
|
76
|
+
required=min_version,
|
|
77
|
+
found=None,
|
|
78
|
+
message=f"{tool} not found in PATH",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
found_version = _parse_version(version_output)
|
|
82
|
+
req_version = _parse_version(min_version)
|
|
83
|
+
|
|
84
|
+
if found_version is None or req_version is None:
|
|
85
|
+
return VersionCheckResult(
|
|
86
|
+
status="fail",
|
|
87
|
+
tool=tool,
|
|
88
|
+
required=min_version,
|
|
89
|
+
found=version_output,
|
|
90
|
+
message=f"could not parse version from output: {version_output!r}",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
found_str = ".".join(str(x) for x in found_version)
|
|
94
|
+
|
|
95
|
+
if _compare_versions(found_version, req_version):
|
|
96
|
+
return VersionCheckResult(
|
|
97
|
+
status="pass",
|
|
98
|
+
tool=tool,
|
|
99
|
+
required=min_version,
|
|
100
|
+
found=found_str,
|
|
101
|
+
message=f"found {found_str} (>= {min_version})",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return VersionCheckResult(
|
|
105
|
+
status="fail",
|
|
106
|
+
tool=tool,
|
|
107
|
+
required=min_version,
|
|
108
|
+
found=found_str,
|
|
109
|
+
message=f"need >= {min_version}, found {found_str}",
|
|
110
|
+
)
|
remo/cli.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""CLI entry point — registers all remo commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from remo import __version__
|
|
5
|
+
from remo.commands.startup import show_startup
|
|
6
|
+
from remo.commands.init import init
|
|
7
|
+
from remo.commands.remind import remind
|
|
8
|
+
from remo.commands.check import check
|
|
9
|
+
from remo.commands.log import log
|
|
10
|
+
from remo.commands.run import run, shortcuts
|
|
11
|
+
from remo.commands.snooze import snooze
|
|
12
|
+
from remo.commands.status import status
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group(invoke_without_command=True)
|
|
16
|
+
@click.version_option(version=__version__, prog_name="remo")
|
|
17
|
+
@click.pass_context
|
|
18
|
+
def main(ctx: click.Context) -> None:
|
|
19
|
+
"""remo — Project-aware terminal companion for developers.
|
|
20
|
+
|
|
21
|
+
Run without arguments to show the startup panel for the current project.
|
|
22
|
+
"""
|
|
23
|
+
if ctx.invoked_subcommand is None:
|
|
24
|
+
show_startup()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
main.add_command(init)
|
|
28
|
+
main.add_command(remind)
|
|
29
|
+
main.add_command(check)
|
|
30
|
+
main.add_command(log)
|
|
31
|
+
main.add_command(run)
|
|
32
|
+
main.add_command(shortcuts)
|
|
33
|
+
main.add_command(snooze)
|
|
34
|
+
main.add_command(status)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Commands sub-package."""
|
remo/commands/check.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""remo check — run all environment checks defined in .remo config."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.rule import Rule
|
|
10
|
+
|
|
11
|
+
from remo.ui.colors import COLORS
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _run_single_check(check_def: Dict[str, Any], base_dir: Path) -> None:
|
|
17
|
+
"""Run a single check definition and print the result."""
|
|
18
|
+
check_type = check_def.get("type")
|
|
19
|
+
|
|
20
|
+
if check_type == "env":
|
|
21
|
+
from remo.checks.env_check import run_env_check
|
|
22
|
+
example_file = check_def.get("file", ".env.example")
|
|
23
|
+
result = run_env_check(example_file, base_dir=base_dir)
|
|
24
|
+
label = ".env"
|
|
25
|
+
if result.status == "pass":
|
|
26
|
+
console.print(
|
|
27
|
+
f" [{COLORS['success']}]✓[/] [bold]{label}[/]"
|
|
28
|
+
f" [dim]{result.message}[/]"
|
|
29
|
+
)
|
|
30
|
+
elif result.status == "skip":
|
|
31
|
+
console.print(
|
|
32
|
+
f" [{COLORS['warning']}]![/] [bold]{label}[/]"
|
|
33
|
+
f" [dim]{result.message}[/]"
|
|
34
|
+
)
|
|
35
|
+
else:
|
|
36
|
+
console.print(
|
|
37
|
+
f" [{COLORS['error']}]✗[/] [bold]{label}[/]"
|
|
38
|
+
f" [dim]{result.message}[/]"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
elif check_type == "version":
|
|
42
|
+
from remo.checks.version_check import run_version_check
|
|
43
|
+
tool = check_def.get("tool", "")
|
|
44
|
+
min_ver = check_def.get("min", "0")
|
|
45
|
+
result = run_version_check(tool, min_ver)
|
|
46
|
+
|
|
47
|
+
if result.status == "pass":
|
|
48
|
+
console.print(
|
|
49
|
+
f" [{COLORS['success']}]✓[/] [bold]{tool}[/]"
|
|
50
|
+
f" [dim]{result.message}[/]"
|
|
51
|
+
)
|
|
52
|
+
elif result.status == "missing":
|
|
53
|
+
console.print(
|
|
54
|
+
f" [{COLORS['warning']}]![/] [bold]{tool}[/]"
|
|
55
|
+
f" [dim]not found (shortcuts using {tool} will fail)[/]"
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
console.print(
|
|
59
|
+
f" [{COLORS['error']}]✗[/] [bold]{tool}[/]"
|
|
60
|
+
f" [dim]{result.message}[/]"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
elif check_type == "file":
|
|
64
|
+
from remo.checks.file_check import run_file_check
|
|
65
|
+
path = check_def.get("path", "")
|
|
66
|
+
result = run_file_check(path, base_dir=base_dir)
|
|
67
|
+
label = path
|
|
68
|
+
|
|
69
|
+
if result.status == "pass":
|
|
70
|
+
console.print(
|
|
71
|
+
f" [{COLORS['success']}]✓[/] [bold]{label}[/]"
|
|
72
|
+
f" [dim]{result.message}[/]"
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
console.print(
|
|
76
|
+
f" [{COLORS['error']}]✗[/] [bold]{label}[/]"
|
|
77
|
+
f" [dim]{result.message}[/]"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
elif check_type == "cmd":
|
|
81
|
+
from remo.checks.cmd_check import run_cmd_check
|
|
82
|
+
cmd = check_def.get("cmd", "")
|
|
83
|
+
label = check_def.get("label", cmd)
|
|
84
|
+
result = run_cmd_check(cmd, label=label)
|
|
85
|
+
|
|
86
|
+
if result.status == "pass":
|
|
87
|
+
console.print(
|
|
88
|
+
f" [{COLORS['success']}]✓[/] [bold]{label}[/]"
|
|
89
|
+
f" [dim]{result.message}[/]"
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
console.print(
|
|
93
|
+
f" [{COLORS['error']}]✗[/] [bold]{label}[/]"
|
|
94
|
+
f" [dim]{result.message}[/]"
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
console.print(f" [dim]? unknown check type: {check_type!r}[/]")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@click.command()
|
|
101
|
+
@click.option(
|
|
102
|
+
"--env-only",
|
|
103
|
+
is_flag=True,
|
|
104
|
+
default=False,
|
|
105
|
+
help="Only run the .env vs .env.example check.",
|
|
106
|
+
)
|
|
107
|
+
def check(env_only: bool) -> None:
|
|
108
|
+
"""Run all environment checks defined in .remo config."""
|
|
109
|
+
from remo.config.loader import load_config, ConfigError
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
config, config_dir = load_config()
|
|
113
|
+
except ConfigError as exc:
|
|
114
|
+
console.print(f"[red]✗ Error:[/red] {exc}")
|
|
115
|
+
if exc.hint:
|
|
116
|
+
console.print(f"[dim] Hint: {exc.hint}[/dim]")
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
|
|
119
|
+
checks = config.get("checks", [])
|
|
120
|
+
|
|
121
|
+
if env_only:
|
|
122
|
+
checks = [c for c in checks if c.get("type") == "env"]
|
|
123
|
+
if not checks:
|
|
124
|
+
console.print(
|
|
125
|
+
f"[{COLORS['warning']}]![/] No env checks defined in .remo."
|
|
126
|
+
)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
project_name = config.get("project", "Project")
|
|
130
|
+
console.print()
|
|
131
|
+
console.print(Rule(
|
|
132
|
+
f" [{COLORS['header']}]Environment Check — {project_name}[/] ",
|
|
133
|
+
style=COLORS["dim"],
|
|
134
|
+
))
|
|
135
|
+
|
|
136
|
+
if not checks:
|
|
137
|
+
console.print(
|
|
138
|
+
f" [dim]No checks defined in .remo. "
|
|
139
|
+
"Add a 'checks' section to get started.[/]"
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
for check_def in checks:
|
|
144
|
+
_run_single_check(check_def, config_dir)
|
|
145
|
+
|
|
146
|
+
console.print()
|