Skip to content

Commit 3d50f85

Browse files
fix(cli): clarify exception diagnostics (#2602)
Consolidate the CLI diagnostic plan, implementation, and test hardening into one reviewable change. The CLI now reports phase and target context for broad failure paths while preserving existing fail-fast behavior for real setup failures and warning-only behavior for optional best-effort work. The workflow unit tests also avoid discovering real local agent CLIs, so developer machines with tools such as gemini installed do not hang pytest during metadata-only assertions. Constraint: CLI setup failures must remain fail-fast, while optional preset and cleanup paths should continue with clear warnings. Rejected: Replace broad handlers across the whole codebase in one pass | too broad for a targeted CLI diagnostic fix Rejected: Add runtime timeouts to workflow agent dispatch | dispatch may legitimately be long-running and the observed hang was test isolation Confidence: high Scope-risk: moderate Directive: Keep future best-effort CLI warnings tied to the failed phase and target so users can diagnose setup state. Tested: uvx ruff check src/; uv run pytest tests/integrations/test_cli.py -v; uv run pytest tests/test_workflows.py::TestCommandStep::test_step_override_integration tests/test_workflows.py::TestPromptStep::test_execute_with_step_integration tests/test_workflows.py::TestPromptStep::test_execute_with_model -vv; uv run pytest Not-tested: Real Nacos/PG/Redis-style external service failure injection; real interactive workflow dispatch against installed gemini CLI
1 parent 0b9bd90 commit 3d50f85

3 files changed

Lines changed: 289 additions & 22 deletions

File tree

src/specify_cli/__init__.py

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,35 @@ def _get_skills_dir(project_path: Path, selected_ai: str) -> Path:
427427
return project_path / ".agents" / "skills"
428428

429429

430+
def _cli_error_detail(exc: BaseException) -> str:
431+
"""Return a compact one-line exception detail for CLI output."""
432+
detail = str(exc).replace("\n", " ").strip()
433+
return detail or exc.__class__.__name__
434+
435+
436+
def _cli_phase_label(phase: str, target_kind: str, target: str | None = None) -> str:
437+
"""Format a stable operation label for user-visible diagnostics."""
438+
label = f"{phase} {target_kind}".strip()
439+
if target:
440+
label = f"{label} '{target}'"
441+
return label
442+
443+
444+
def _print_cli_warning(
445+
phase: str,
446+
target_kind: str,
447+
target: str | None,
448+
exc: BaseException,
449+
*,
450+
continuing: str | None = None,
451+
) -> None:
452+
"""Print a warning that names the failed CLI phase and target."""
453+
label = _cli_phase_label(phase, target_kind, target)
454+
console.print(f"[yellow]Warning:[/yellow] Failed to {label}: {_cli_error_detail(exc)}")
455+
if continuing:
456+
console.print(f"[dim]{continuing}[/dim]")
457+
458+
430459
# Constants kept for backward compatibility with presets and extensions.
431460
DEFAULT_SKILLS_DIR = ".agents/skills"
432461
SKILL_DESCRIPTIONS = {
@@ -859,9 +888,8 @@ def init(
859888
git_messages.append("bundled extension not found")
860889
except Exception as ext_err:
861890
git_has_error = True
862-
sanitized_ext = str(ext_err).replace('\n', ' ').strip()
863891
git_messages.append(
864-
f"extension install failed: {sanitized_ext[:120]}"
892+
f"extension install failed during optional git setup: {_cli_error_detail(ext_err)[:120]}"
865893
)
866894
summary = "; ".join(git_messages)
867895
if git_has_error:
@@ -899,8 +927,10 @@ def init(
899927
else:
900928
tracker.skip("workflow", "bundled workflow not found")
901929
except Exception as wf_err:
902-
sanitized_wf = str(wf_err).replace('\n', ' ').strip()
903-
tracker.error("workflow", f"install failed: {sanitized_wf[:120]}")
930+
tracker.error(
931+
"workflow",
932+
f"install bundled workflow 'speckit' failed: {_cli_error_detail(wf_err)[:120]}",
933+
)
904934

905935
# Fix permissions after all installs (scripts + extensions)
906936
ensure_executable_scripts(project_path, tracker=tracker)
@@ -962,7 +992,13 @@ def init(
962992
zip_path = preset_catalog.download_pack(preset)
963993
preset_manager.install_from_zip(zip_path, speckit_ver)
964994
except PresetError as preset_err:
965-
console.print(f"[yellow]Warning:[/yellow] Failed to install preset '{preset}': {preset_err}")
995+
_print_cli_warning(
996+
"install",
997+
"preset",
998+
preset,
999+
preset_err,
1000+
continuing="Continuing without the optional preset.",
1001+
)
9661002
finally:
9671003
if zip_path is not None:
9681004
# Clean up downloaded ZIP to avoid cache accumulation
@@ -972,7 +1008,14 @@ def init(
9721008
# Best-effort cleanup; failure to delete is non-fatal
9731009
pass
9741010
except Exception as preset_err:
975-
console.print(f"[yellow]Warning:[/yellow] Failed to install preset: {preset_err}")
1011+
# Optional preset install must not abort project initialization.
1012+
_print_cli_warning(
1013+
"install",
1014+
"preset",
1015+
preset,
1016+
preset_err,
1017+
continuing="Continuing without the optional preset.",
1018+
)
9761019

9771020
tracker.complete("final", "project ready")
9781021
except (typer.Exit, SystemExit):
@@ -1693,20 +1736,29 @@ def integration_install(
16931736
if new_default == integration.key:
16941737
_update_init_options_for_integration(project_root, integration, script_type=selected_script)
16951738

1696-
except Exception as e:
1739+
except Exception as exc:
16971740
# Attempt rollback of any files written by setup
16981741
try:
16991742
integration.teardown(project_root, manifest, force=True)
17001743
except Exception as rollback_err:
17011744
# Suppress so the original setup error remains the primary failure
1702-
console.print(f"[yellow]Warning:[/yellow] Failed to roll back integration changes: {rollback_err}")
1745+
_print_cli_warning(
1746+
"rollback",
1747+
"integration",
1748+
key,
1749+
rollback_err,
1750+
continuing="The original install failure is still the primary error.",
1751+
)
17031752
if installed_keys:
17041753
_write_integration_json(
17051754
project_root, default_key, installed_keys, _integration_settings(current)
17061755
)
17071756
else:
17081757
_remove_integration_json(project_root)
1709-
console.print(f"[red]Error:[/red] Failed to install integration: {e}")
1758+
console.print(
1759+
f"[red]Error:[/red] Failed to {_cli_phase_label('install', 'integration', key)}: "
1760+
f"{_cli_error_detail(exc)}"
1761+
)
17101762
raise typer.Exit(1)
17111763

17121764
name = (integration.config or {}).get("name", key)
@@ -2070,9 +2122,12 @@ def integration_switch(
20702122
ext_mgr = ExtensionManager(project_root)
20712123
ext_mgr.unregister_agent_artifacts(installed_key)
20722124
except Exception as ext_err:
2073-
console.print(
2074-
f"[yellow]Warning:[/yellow] Could not clean up extension artifacts "
2075-
f"(commands, skills, registry entries) for '{installed_key}': {ext_err}"
2125+
_print_cli_warning(
2126+
"clean up extension artifacts for",
2127+
"integration",
2128+
installed_key,
2129+
ext_err,
2130+
continuing="Continuing with integration switch; old extension artifacts may need manual cleanup.",
20762131
)
20772132

20782133
# Clear metadata so a failed Phase 2 doesn't leave stale references
@@ -2167,18 +2222,27 @@ def integration_switch(
21672222
ext_mgr = ExtensionManager(project_root)
21682223
ext_mgr.register_enabled_extensions_for_agent(target)
21692224
except Exception as ext_err:
2170-
console.print(
2171-
f"[yellow]Warning:[/yellow] Could not register extension commands, skills, "
2172-
f"or related artifacts for '{target}': {ext_err}"
2225+
_print_cli_warning(
2226+
"register extension artifacts for",
2227+
"integration",
2228+
target,
2229+
ext_err,
2230+
continuing="The integration switch succeeded, but installed extensions may need re-registration.",
21732231
)
21742232

2175-
except Exception as e:
2233+
except Exception as exc:
21762234
# Attempt rollback of any files written by setup
21772235
try:
21782236
target_integration.teardown(project_root, manifest, force=True)
21792237
except Exception as rollback_err:
21802238
# Suppress so the original setup error remains the primary failure
2181-
console.print(f"[yellow]Warning:[/yellow] Failed to roll back integration '{target}': {rollback_err}")
2239+
_print_cli_warning(
2240+
"rollback",
2241+
"integration",
2242+
target,
2243+
rollback_err,
2244+
continuing="The original switch failure is still the primary error.",
2245+
)
21822246
if installed_keys:
21832247
fallback_key = installed_keys[0]
21842248
fallback_integration = get_integration(fallback_key)
@@ -2207,7 +2271,10 @@ def integration_switch(
22072271
)
22082272
else:
22092273
_remove_integration_json(project_root)
2210-
console.print(f"[red]Error:[/red] Failed to install integration '{target}': {e}")
2274+
console.print(
2275+
f"[red]Error:[/red] Failed to {_cli_phase_label('install', 'integration', target)} "
2276+
f"during switch: {_cli_error_detail(exc)}"
2277+
)
22112278
raise typer.Exit(1)
22122279

22132280
name = (target_integration.config or {}).get("name", target)
@@ -2342,7 +2409,8 @@ def integration_upgrade(
23422409
except Exception as exc:
23432410
# Don't teardown — setup overwrites in-place, so teardown would
23442411
# delete files that were working before the upgrade. Just report.
2345-
console.print(f"[red]Error:[/red] Failed to upgrade integration: {exc}")
2412+
console.print(f"[red]Error:[/red] Failed to {_cli_phase_label('upgrade', 'integration', key)}.")
2413+
console.print(f"[dim]Details:[/dim] {_cli_error_detail(exc)}")
23462414
console.print("[yellow]The previous integration files may still be in place.[/yellow]")
23472415
raise typer.Exit(1)
23482416

0 commit comments

Comments
 (0)