Skip to content

Commit 7593c50

Browse files
committed
feat: credential scope wizard step in cems setup
Replace the bolted-on credential prompts with a proper wizard step: - Step 1: Select IDEs (unchanged) - Step 2: Credential scope — Global (all projects) or Project (this repo only) - Step 3: Credentials — adapts based on scope + existing state - New user: prompts for URL/key, writes to chosen location - Existing creds: offers Keep or Overwrite - Project scope: defaults to global cred values when available Non-interactive usage unchanged: cems setup --project --api-url X --api-key Y
1 parent 89b4717 commit 7593c50

File tree

1 file changed

+104
-30
lines changed

1 file changed

+104
-30
lines changed

src/cems/commands/setup.py

Lines changed: 104 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -280,16 +280,9 @@ def _setup_credentials(api_url: str | None = None, api_key: str | None = None) -
280280
cems_dir = Path.home() / ".cems"
281281
creds_file = cems_dir / "credentials"
282282

283-
# Already configured — but offer project setup if interactive
283+
# Already configured
284284
if creds_file.exists() and not api_key:
285285
console.print(f"[green]Credentials:[/green] {creds_file}")
286-
if _is_interactive():
287-
also_project = click.confirm(
288-
"Also set up project-specific credentials in this directory?",
289-
default=False,
290-
)
291-
if also_project:
292-
_setup_project_credentials(None, None)
293286
return True
294287

295288
# Non-interactive with explicit values — preserve existing custom keys
@@ -339,17 +332,6 @@ def _setup_credentials(api_url: str | None = None, api_key: str | None = None) -
339332
console.print("[red]API key is required.[/red]")
340333
return False
341334

342-
# Ask where to store credentials
343-
location = click.prompt(
344-
"Store credentials",
345-
type=click.Choice(["global", "project"], case_sensitive=False),
346-
default="global",
347-
show_default=True,
348-
)
349-
if location == "project":
350-
_setup_project_credentials(api_url, api_key)
351-
return True
352-
353335
cems_dir.mkdir(parents=True, exist_ok=True)
354336
creds_file.write_text(
355337
f"# CEMS credentials — generated by cems setup\n"
@@ -967,19 +949,37 @@ def _setup_project_credentials(api_url: str | None, api_key: str | None) -> None
967949
console.print("Usage: cems setup --project --api-url URL --api-key KEY")
968950
raise click.Abort()
969951

952+
# Use existing global credentials as defaults for project setup
953+
existing_creds = _read_credentials() if (Path.home() / ".cems" / "credentials").exists() else {}
954+
default_url = existing_creds.get("CEMS_API_URL", "http://localhost:8765")
955+
default_key = existing_creds.get("CEMS_API_KEY", "")
956+
970957
console.print()
971958
console.print("[bold]Project Credentials Setup[/bold]")
972-
console.print("Get these from your CEMS admin.")
959+
if default_key:
960+
console.print("Using global credentials as defaults. Press enter to keep, or type new values.")
961+
else:
962+
console.print("Get these from your CEMS admin.")
973963
console.print()
974964

975965
if not api_url:
976966
api_url = click.prompt(
977967
"CEMS API URL",
978-
default="http://localhost:8765",
968+
default=default_url,
979969
show_default=True,
980970
)
981971
if not api_key:
982-
api_key = click.prompt("CEMS API Key", hide_input=True)
972+
if default_key:
973+
use_same = click.confirm(
974+
f"Use same API key as global ({default_key[:8]}...)?",
975+
default=True,
976+
)
977+
if use_same:
978+
api_key = default_key
979+
else:
980+
api_key = click.prompt("CEMS API Key", hide_input=True)
981+
else:
982+
api_key = click.prompt("CEMS API Key", hide_input=True)
983983

984984
if not api_key:
985985
console.print("[red]API key is required.[/red]")
@@ -1100,17 +1100,18 @@ def setup(install_claude: bool, install_cursor: bool, install_codex: bool, insta
11001100
cems setup --claude --api-url URL --api-key KEY # Non-interactive
11011101
cems setup --project --api-url URL --api-key KEY # Per-project config
11021102
"""
1103-
# Per-project setup — completely separate flow
1104-
if install_project:
1103+
# Non-interactive --project flag — separate flow
1104+
if install_project and not _is_interactive():
11051105
_setup_project_credentials(api_url, api_key)
11061106
return
1107+
11071108
console.print()
11081109
console.print("[bold]CEMS Setup[/bold]")
11091110
console.print()
11101111

11111112
data_path = _get_data_path()
11121113

1113-
# Determine what to install
1114+
# Step 1: Select IDEs
11141115
if not install_claude and not install_cursor and not install_codex and not install_goose:
11151116
if _is_interactive():
11161117
selected = _multiselect(
@@ -1131,14 +1132,84 @@ def setup(install_claude: bool, install_cursor: bool, install_codex: bool, insta
11311132
console.print("[yellow]Non-interactive mode: installing Claude Code hooks (use --cursor/--codex/--goose for others)[/yellow]")
11321133
install_claude = True
11331134

1134-
# Credentials
1135-
if not _setup_credentials(api_url=api_url, api_key=api_key):
1136-
raise click.Abort()
1135+
# Step 2: Credential scope
1136+
credential_scope = "global"
1137+
if _is_interactive() and not install_project:
1138+
console.print()
1139+
credential_scope = _single_select(
1140+
"Credential scope",
1141+
[
1142+
("global", "Global — CEMS accesses all your projects"),
1143+
("project", "Project — CEMS accesses only this project's data"),
1144+
],
1145+
default=0,
1146+
)
1147+
install_project = credential_scope == "project"
1148+
elif install_project:
1149+
credential_scope = "project"
1150+
1151+
# Step 3: Credentials (adapts based on scope + existing state)
1152+
import os as _os
1153+
global_creds_file = Path.home() / ".cems" / "credentials"
1154+
project_creds_file = Path(_os.getcwd()) / ".cems" / "credentials"
1155+
1156+
if credential_scope == "project":
1157+
# Project scope — write to CWD/.cems/credentials
1158+
if project_creds_file.exists() and _is_interactive() and not api_key:
1159+
console.print()
1160+
action = _single_select(
1161+
f"Project credentials found ({project_creds_file})",
1162+
[
1163+
("keep", "Keep existing"),
1164+
("overwrite", "Overwrite with new values"),
1165+
],
1166+
default=0,
1167+
)
1168+
if action == "overwrite":
1169+
_setup_project_credentials(api_url, api_key)
1170+
elif not project_creds_file.exists():
1171+
_setup_project_credentials(api_url, api_key)
1172+
else:
1173+
console.print(f"[green]Project credentials:[/green] {project_creds_file}")
1174+
1175+
# Also ensure global creds exist for hooks/MCP that need them
1176+
if not global_creds_file.exists() and not api_key:
1177+
# Read the project creds we just wrote to populate global as well
1178+
pass # Global is optional in project-only mode
1179+
else:
1180+
# Global scope — write to ~/.cems/credentials
1181+
if global_creds_file.exists() and _is_interactive() and not api_key:
1182+
console.print()
1183+
action = _single_select(
1184+
f"Global credentials found ({global_creds_file})",
1185+
[
1186+
("keep", "Keep existing"),
1187+
("overwrite", "Overwrite with new values"),
1188+
],
1189+
default=0,
1190+
)
1191+
if action == "overwrite":
1192+
if not _setup_credentials(api_url=None, api_key=None):
1193+
raise click.Abort()
1194+
else:
1195+
console.print(f"[green]Credentials:[/green] {global_creds_file}")
1196+
elif not _setup_credentials(api_url=api_url, api_key=api_key):
1197+
raise click.Abort()
11371198

11381199
console.print()
11391200

11401201
# Resolve API URL and key for team discovery
1141-
creds = _read_credentials()
1202+
# Read from whichever credential file we just wrote/kept
1203+
if credential_scope == "project" and project_creds_file.exists():
1204+
from cems.shared.credentials import parse_credentials_file
1205+
creds = parse_credentials_file(str(project_creds_file))
1206+
# Merge with global if project creds are sparse
1207+
if global_creds_file.exists():
1208+
global_creds = _read_credentials()
1209+
for k, v in global_creds.items():
1210+
creds.setdefault(k, v)
1211+
else:
1212+
creds = _read_credentials()
11421213
resolved_url = api_url or creds.get("CEMS_API_URL", "http://localhost:8765")
11431214
resolved_key = api_key or creds.get("CEMS_API_KEY", "")
11441215

@@ -1212,7 +1283,10 @@ def setup(install_claude: bool, install_cursor: bool, install_codex: bool, insta
12121283
table = Table(show_header=False, box=None, padding=(0, 2))
12131284
table.add_column(style="cyan")
12141285
table.add_column(style="white")
1215-
table.add_row("Credentials", str(Path.home() / ".cems" / "credentials"))
1286+
if credential_scope == "project" and project_creds_file.exists():
1287+
table.add_row("Project creds", str(project_creds_file))
1288+
if global_creds_file.exists():
1289+
table.add_row("Global creds", str(global_creds_file))
12161290
if install_claude:
12171291
table.add_row("Claude hooks", str(Path.home() / ".claude" / "hooks"))
12181292
table.add_row("Claude skills", str(Path.home() / ".claude" / "skills" / "cems"))

0 commit comments

Comments
 (0)