Skip to content

Commit 71dafb3

Browse files
author
jzhu
committed
update
1 parent 5882bd9 commit 71dafb3

File tree

9 files changed

+763
-189
lines changed

9 files changed

+763
-189
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ This file documents repository-level expectations and instructions intended to g
1010
rm -rf dist/*
1111
./install.sh uninstall
1212
./install.sh
13-
cp ~/.config/code-assistant-manager/settings.json.bak ~/.config/code-assistant-manager/settings.json
13+
cp ~/.config/code-assistant-manager/providers.json.bak ~/.config/code-assistant-manager/providers.json
1414
```
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""Shared helpers for CLI option validation and normalization."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
from typing import List, Optional, Sequence
7+
8+
import typer
9+
10+
from code_assistant_manager.menu.base import Colors
11+
12+
13+
def _emit_error(message: str) -> None:
14+
"""Print a formatted error message and exit."""
15+
typer.echo(f"{Colors.RED}{message}{Colors.RESET}")
16+
raise typer.Exit(1)
17+
18+
19+
def _split_values(raw: str) -> List[str]:
20+
"""Split a comma-delimited option string into individual values."""
21+
parts = [part.strip() for part in raw.split(",")]
22+
values = [part for part in parts if part]
23+
return values or ([raw.strip()] if raw.strip() else [])
24+
25+
26+
def resolve_app_targets(
27+
value: Optional[str],
28+
valid_apps: Sequence[str],
29+
*,
30+
option_label: str = "--app",
31+
default: Optional[str] = None,
32+
allow_all: bool = True,
33+
fallback_to_all_if_none: bool = False,
34+
) -> List[str]:
35+
"""Resolve an app option into a list of normalized app identifiers."""
36+
selection = value if value is not None else default
37+
if selection is None and fallback_to_all_if_none:
38+
selection = "all"
39+
if selection is None:
40+
_emit_error(f"{option_label} is required")
41+
42+
tokens = _split_values(selection)
43+
if not tokens:
44+
_emit_error(f"{option_label} cannot be empty")
45+
46+
resolved: List[str] = []
47+
for token in tokens:
48+
normalized = token.lower()
49+
if allow_all and normalized == "all":
50+
return list(valid_apps)
51+
if normalized not in valid_apps:
52+
valid_list = ", ".join(valid_apps)
53+
all_hint = " or 'all'" if allow_all else ""
54+
_emit_error(
55+
f"Invalid value '{token}' for {option_label}. Valid: {valid_list}{all_hint}"
56+
)
57+
if normalized not in resolved:
58+
resolved.append(normalized)
59+
return resolved
60+
61+
62+
def resolve_single_app(
63+
value: Optional[str],
64+
valid_apps: Sequence[str],
65+
*,
66+
option_label: str = "--app",
67+
default: Optional[str] = None,
68+
) -> str:
69+
"""Resolve an app option that must target exactly one app."""
70+
apps = resolve_app_targets(
71+
value,
72+
valid_apps,
73+
option_label=option_label,
74+
default=default,
75+
allow_all=False,
76+
)
77+
if len(apps) != 1:
78+
_emit_error(f"{option_label} accepts a single app value")
79+
return apps[0]
80+
81+
82+
def resolve_level_targets(
83+
value: Optional[str],
84+
valid_levels: Sequence[str],
85+
*,
86+
option_label: str = "--level",
87+
default: Optional[str] = None,
88+
allow_all: bool = True,
89+
) -> List[str]:
90+
"""Resolve a level option into one or more levels."""
91+
selection = value if value is not None else default
92+
if selection is None:
93+
_emit_error(f"{option_label} is required")
94+
95+
tokens = _split_values(selection)
96+
if not tokens:
97+
_emit_error(f"{option_label} cannot be empty")
98+
99+
resolved: List[str] = []
100+
for token in tokens:
101+
normalized = token.lower()
102+
if allow_all and normalized == "all":
103+
return list(valid_levels)
104+
if normalized not in valid_levels:
105+
valid_list = ", ".join(valid_levels)
106+
all_hint = " or 'all'" if allow_all else ""
107+
_emit_error(
108+
f"Invalid value '{token}' for {option_label}. Valid: {valid_list}{all_hint}"
109+
)
110+
if normalized not in resolved:
111+
resolved.append(normalized)
112+
return resolved
113+
114+
115+
def resolve_single_level(
116+
value: Optional[str],
117+
valid_levels: Sequence[str],
118+
*,
119+
option_label: str = "--level",
120+
default: Optional[str] = None,
121+
) -> str:
122+
"""Resolve a level option that must target exactly one level."""
123+
levels = resolve_level_targets(
124+
value,
125+
valid_levels,
126+
option_label=option_label,
127+
default=default,
128+
allow_all=False,
129+
)
130+
if len(levels) != 1:
131+
_emit_error(f"{option_label} accepts a single level value")
132+
return levels[0]
133+
134+
135+
def ensure_project_dir(level: str, project_dir: Optional[Path]) -> Optional[Path]:
136+
"""Ensure a project directory exists when required for the provided level."""
137+
if level == "project" and project_dir is None:
138+
return Path.cwd()
139+
return project_dir

0 commit comments

Comments
 (0)