diff --git a/cli/src/services/parse/command_runtime.rs b/cli/src/services/parse/command_runtime.rs index 2903c965..036afa32 100644 --- a/cli/src/services/parse/command_runtime.rs +++ b/cli/src/services/parse/command_runtime.rs @@ -1,4 +1,5 @@ use crate::{cli_schema, command_surface, services}; +use clap::{ArgAction, CommandFactory}; use services::command_registry::{CommandRegistry, RuntimeCommand}; use services::error::{CliError, FailureClass}; use services::observability::traits::Logger as LoggerTrait; @@ -68,6 +69,16 @@ fn handle_clap_error( )); } + if error.kind() == clap::error::ErrorKind::InvalidSubcommand { + if let Some((name, text)) = render_unknown_subcommand_help(args, error) { + return Ok(RuntimeCommand::HelpText( + services::help::command::HelpTextCommand { name, text }, + )); + } + + return registry_command(registry, services::help::NAME); + } + if error.kind() == clap::error::ErrorKind::DisplayVersion { return registry_command(registry, services::version::NAME); } @@ -108,24 +119,84 @@ fn classify_clap_error(error: &clap::Error) -> CliError { } fn render_subcommand_help_from_args(args: &[String]) -> Option<(String, String)> { - let command_name = args.get(1)?.to_owned(); - let command_path = args[1..] + let command_path = command_path_from_args(args, args.len()); + + render_help_for_command_path(&command_path) +} + +fn render_unknown_subcommand_help( + args: &[String], + error: &clap::Error, +) -> Option<(String, String)> { + let unknown = extract_quoted_value(&error.to_string())?; + let unknown_index = args .iter() - .take_while(|arg| !arg.starts_with('-')) - .map(String::as_str) - .collect::>(); + .enumerate() + .skip(1) + .find_map(|(index, arg)| (arg == &unknown).then_some(index))?; + let command_path = command_path_from_args(args, unknown_index); - if command_path.is_empty() { - return None; - } + render_help_for_command_path(&command_path) +} + +fn render_help_for_command_path(command_path: &[String]) -> Option<(String, String)> { + let command_name = command_path.first()?.clone(); - if command_path.as_slice() == [services::auth_command::NAME] { + if command_path.len() == 1 && command_path[0] == services::auth_command::NAME { return Some((command_name, cli_schema::auth_help_text())); } + let command_path = command_path.iter().map(String::as_str).collect::>(); cli_schema::render_help_for_path(&command_path).map(|text| (command_name, text)) } +fn command_path_from_args(args: &[String], end: usize) -> Vec { + let mut command = cli_schema::Cli::command(); + let mut command_path = Vec::new(); + let mut index = 1; + + while index < end { + let arg = &args[index]; + if arg == "--" { + break; + } + if arg.starts_with('-') { + if option_takes_value(&command, arg) && !arg.contains('=') { + index += 2; + } else { + index += 1; + } + continue; + } + + let Some(subcommand) = command.find_subcommand(arg) else { + break; + }; + + command_path.push(arg.clone()); + command = subcommand.clone(); + index += 1; + } + + command_path +} + +fn option_takes_value(command: &clap::Command, token: &str) -> bool { + let (long_name, short_name) = if let Some(name) = token.strip_prefix("--") { + (name.split('=').next(), None) + } else if let Some(name) = token.strip_prefix('-') { + (None, name.chars().next().filter(|_| name.len() == 1)) + } else { + return false; + }; + + command.get_arguments().any(|argument| { + let matches_name = long_name.is_some_and(|name| argument.get_long() == Some(name)) + || short_name.is_some_and(|name| argument.get_short() == Some(name)); + matches_name && matches!(argument.get_action(), ArgAction::Set | ArgAction::Append) + }) +} + fn render_missing_subcommand_help(args: &[String]) -> Option { let command_name = args.get(1)?.as_str(); @@ -466,6 +537,7 @@ fn parse_optional_hook_remote_url(remote_url: Option) -> Result RuntimeCommand { parse_runtime_command( @@ -476,6 +548,14 @@ mod tests { .expect("command should parse") } + fn help_payload(command: RuntimeCommand) -> String { + match command { + RuntimeCommand::Help(_) => services::help::help_text(), + RuntimeCommand::HelpText(command) => command.text, + _ => panic!("expected a help command"), + } + } + #[test] fn sync_parses_to_sync_request_with_default_text_format() { let command = parse(&["sce", "sync"]); @@ -519,17 +599,64 @@ mod tests { } #[test] - fn removed_trace_command_is_rejected() { - let result = parse_runtime_command( - ["sce", "trace", "sync"].into_iter().map(String::from), + fn unknown_top_level_command_routes_to_top_level_help() { + let fallback = help_payload(parse(&["sce", "some_random_non_existing_command"])); + let expected = help_payload(parse(&["sce", "--help"])); + + assert_eq!(fallback, expected); + } + + #[test] + fn unknown_auth_subcommand_routes_to_auth_help() { + let fallback = help_payload(parse(&["sce", "auth", "some_random_non_existing_command"])); + let expected = cli_schema::auth_help_text(); + + assert_eq!(fallback, expected); + } + + #[test] + fn unknown_non_auth_subcommand_routes_to_closest_parent_help() { + let fallback = help_payload(parse(&["sce", "hooks", "some_random_non_existing_command"])); + let expected = help_payload(parse(&["sce", "hooks", "--help"])); + + assert_eq!(fallback, expected); + } + + #[test] + fn unknown_command_help_uses_the_success_rendering_path() { + let payload = help_payload(parse(&["sce", "some_random_non_existing_command"])); + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + + let exit_code = services::app_support::render_run_outcome( + services::app_support::RunOutcome { + result: Ok(payload.clone()), + logger: None::, + startup_diagnostic: None, + }, + &mut stdout, + &mut stderr, + ); + + assert_eq!(exit_code, ExitCode::SUCCESS); + assert_eq!(stdout, format!("{payload}\n").as_bytes()); + assert!(stderr.is_empty()); + } + + #[test] + fn valid_command_and_unknown_option_keep_existing_routing() { + assert!(matches!(parse(&["sce", "sync"]), RuntimeCommand::Sync(_))); + + let Err(error) = parse_runtime_command( + ["sce", "--not-an-option"].into_iter().map(String::from), &CommandRegistry::default(), None, - ); + ) else { + panic!("unknown option should remain an error"); + }; - match result { - Ok(_) => panic!("trace should be unavailable"), - Err(error) => assert!(error.to_string().contains("Unknown command 'trace'")), - } + assert_eq!(error.class(), FailureClass::Parse); + assert!(error.to_string().contains("Unknown option")); } #[test] @@ -547,16 +674,10 @@ mod tests { } #[test] - fn auth_renew_is_rejected_as_parse_error() { - let Err(error) = parse_runtime_command( - ["sce", "auth", "renew"].into_iter().map(String::from), - &CommandRegistry::default(), - None, - ) else { - panic!("removed auth renew command should not parse") - }; - - assert_eq!(error.class(), FailureClass::Parse); - assert!(error.to_string().contains("Unknown command 'renew'")); + fn removed_auth_renew_command_routes_to_auth_help() { + assert_eq!( + help_payload(parse(&["sce", "auth", "renew"])), + cli_schema::auth_help_text() + ); } } diff --git a/context/architecture.md b/context/architecture.md index feda173d..81d50b21 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -105,10 +105,10 @@ The repository includes a new placeholder Rust binary crate at `cli/`. - `cli/src/main.rs` is the executable entrypoint (`sce`) and delegates to `app::run`. - `cli/src/cli_schema.rs` defines the clap-based CLI schema using derive macros for all top-level commands and subcommands, including the top-level `sync` command, and renders command-local help text for the `auth` command tree (`auth`, `auth login`, `auth logout`, `auth whoami`). -- `cli/src/app.rs` provides the clap-based argument dispatch loop with deterministic help/setup execution, bare-command help routing for `sce auth` and `sce config`, centralized stream routing (`stdout` success payloads, `stderr` redacted diagnostics), stable class-based exit-code mapping (`2` parse, `3` validation, `4` runtime, `5` dependency), and stable class-based stderr diagnostic codes (`SCE-ERR-PARSE`, `SCE-ERR-VALIDATION`, `SCE-ERR-RUNTIME`, `SCE-ERR-DEPENDENCY`) with default `Try:` remediation injection when missing. +- `cli/src/app.rs` provides the clap-based argument dispatch loop with deterministic help/setup execution, bare-command help routing for `sce auth` and `sce config`, successful unknown-command fallback to top-level or closest-parent help, centralized stream routing (`stdout` success payloads, `stderr` redacted diagnostics), stable class-based exit-code mapping (`2` parse, `3` validation, `4` runtime, `5` dependency), and stable class-based stderr diagnostic codes (`SCE-ERR-PARSE`, `SCE-ERR-VALIDATION`, `SCE-ERR-RUNTIME`, `SCE-ERR-DEPENDENCY`) with default `Try:` remediation injection when missing. - The app runtime now moves through explicit startup phases in `cli/src/app.rs`: dependency bootstrapping (`perform_dependency_check`), startup context construction (`build_startup_context`), runtime initialization (`initialize_runtime`), command parse/execute inside telemetry subscriber context (`run_command_lifecycle`, `parse_command_phase` plus `services::app_support::execute_command_phase`), and final output rendering through `services::app_support::render_run_outcome`. `AppRuntime` owns the concrete production logger, no-op telemetry runtime, filesystem ops, git ops, static `CommandRegistry`, and startup-diagnostic state across those phases; `RunOutcome` carries final render data with an optional generic logger implementing `services::observability::traits::Logger`, so render support can log classified errors without production-logger type coupling. If a telemetry implementation attempts to invoke the command action more than once, dispatch returns a runtime-classified error instead of panicking or reusing consumed arguments. - `AppContext` is the CLI's borrowed dependency view in `cli/src/app.rs`: it is generic over logger, telemetry, filesystem, and git capability implementations and stores references plus an optional `repo_root: Option` instead of owning `Arc` trait objects. Because it borrows from `AppRuntime`, `AppContext` is a lightweight, short-lived view and must not be stored long-term (e.g., in structs or across await points). Startup creates a context view over `AppRuntime`'s concrete production dependencies with `repo_root` set to `None`; command paths can derive repo-root-scoped context views through the `ContextWithRepoRoot` accessor trait / `AppContext::with_repo_root(...)`, which reuses the same borrowed dependencies while attaching the resolved root. Narrow accessor traits expose associated concrete capability types for logger, telemetry, fs, and git (`&Self::...`) plus repo-root access, so call sites can express capability requirements without erasing the borrowed dependencies back to trait objects; lifecycle providers consume the repo-root accessor rather than the full context type. -- Command parse-time conversion and run-time handling are separated by an internal static `RuntimeCommand` seam. `cli/src/services/command_registry.rs` defines the `RuntimeCommand` enum with variants for help/help-text, version, completion, auth, config, setup, doctor, hooks, policy, and sync, plus a deterministic `CommandRegistry` name catalog populated by `build_default_registry()`. `parse_command_phase` in `cli/src/app.rs` delegates clap-output conversion to `cli/src/services/parse/command_runtime.rs`, which owns clap error classification, help rendering bridges, and parsed-request-to-enum conversion while returning concrete enum values. Service-owned `command.rs` modules define command payload structs and generic execution methods with narrow context requirements: context-free commands accept any context, hooks requires logger access, setup/doctor require repo-root scoping, and central dispatch requires the union of logger plus repo-root-scoping capabilities. `services::app_support::execute_command_phase` emits lifecycle logs around `RuntimeCommand::execute_with_stderr(...)`; the enum performs the only central dispatch match and delegates business behavior to the service-owned command structs, with sync receiving the app-owned stderr writer for format-gated progress. +- Command parse-time conversion and run-time handling are separated by an internal static `RuntimeCommand` seam. `cli/src/services/command_registry.rs` defines the `RuntimeCommand` enum with variants for help/help-text, version, completion, auth, config, setup, doctor, hooks, policy, and sync, plus a deterministic `CommandRegistry` name catalog populated by `build_default_registry()`. `parse_command_phase` in `cli/src/app.rs` delegates clap-output conversion to `cli/src/services/parse/command_runtime.rs`, which owns clap error classification, help rendering bridges, longest-valid-parent traversal for unknown command paths, and parsed-request-to-enum conversion while returning concrete enum values. Service-owned `command.rs` modules define command payload structs and generic execution methods with narrow context requirements: context-free commands accept any context, hooks requires logger access, setup/doctor require repo-root scoping, and central dispatch requires the union of logger plus repo-root-scoping capabilities. `services::app_support::execute_command_phase` emits lifecycle logs around `RuntimeCommand::execute_with_stderr(...)`; the enum performs the only central dispatch match and delegates business behavior to the service-owned command structs, with sync receiving the app-owned stderr writer for format-gated progress. - Startup observability bootstrapping in `cli/src/app.rs` still tolerates invalid default-discovered config files by continuing with degraded defaults plus `sce.config.invalid_config` warn-level logs, but the warning/logging work is now isolated behind the startup-context and runtime-initialization phases rather than one inline startup function. - `cli/src/services/observability.rs` provides deterministic runtime observability controls and rendering for app lifecycle logs, including shared config-resolved threshold/format, explicit config-file/default `log_to_file`, and `log_dir` inputs with precedence `env > config file > defaults` for non-flag observability keys, stable event identifiers, severity filtering, the forced-emission warning path used for invalid discovered config startup diagnostics, error-specific stderr suppression when file logging is enabled while non-error records and file-write diagnostics remain on stderr, redaction-safe emission through the shared security helper, and log-directory writes with bounded retention. Config resolution also carries a positive config-file/default-only `log_file_retention_limit` (`10` by default) into startup observability config and `sce config show`; the concrete logger stores that resolved value and threads it through primary and v2 cleanup. When `log_dir` resolves from `SCE_LOG_DIR`, config, or the `/sce/logs` default, each enabled or forced log operation selects `/sce-.log` or `/sce--.log` using the machine-local date and optional logger session context, with deterministic percent-encoding for unsafe session filename bytes; after successfully writing a newly created selected file, retention keeps the configured number of newest direct regular `*.log` files by mtime plus path/name tie-break and fails open on cleanup errors. Its `observability::traits` submodule exposes the current `Logger` API with `Option<&str>` session context plus object-safe `Telemetry` trait boundaries and `NoopLogger`; the concrete observability logger and telemetry runtime still own behavior and implement those traits. `services::app_support::render_run_outcome` consumes the logger through that trait boundary when logging classified errors and stdout-write failures. - `cli/src/services/observability.rs` no longer owns duplicate log enums or parsing helpers; it consumes the canonical primitive seam from `cli/src/services/config/mod.rs` and stays focused on logger and telemetry runtime behavior. diff --git a/context/cli/cli-command-surface.md b/context/cli/cli-command-surface.md index cd560099..a842eae3 100644 --- a/context/cli/cli-command-surface.md +++ b/context/cli/cli-command-surface.md @@ -74,9 +74,9 @@ An interactive `setup` run instead resolves the selection through an `inquire` m - `parse_command_phase` delegates clap output conversion to `cli/src/services/parse/command_runtime.rs`, which returns concrete `RuntimeCommand` enum variants; `services::app_support::execute_command_phase` emits lifecycle logs around `RuntimeCommand::execute(...)`, and the enum delegates behavior to service-owned command payload structs. - Top-level failures are classified into stable exit-code classes owned by `cli/src/app.rs`: `2` parse, `3` validation, `4` runtime, and `5` dependency. - User-facing diagnostics are rendered on `stderr` as `Error [SCE-ERR-]: ...` with class-default `Try:` remediation appended only when missing; when stderr color is enabled the heading, error code, and diagnostic body all render through shared stderr styling helpers. -- Unknown commands/options and extra positional arguments return deterministic, actionable guidance to run `sce --help`. +- Unknown commands and subcommands are successful help requests: an unknown top-level token returns the same stdout payload as `sce --help`, while an unknown token below a valid command returns the closest valid parent's existing help payload (including the custom `auth` help). Unknown options, extra positional arguments, and other parse/validation failures retain deterministic actionable diagnostics and their existing failure classes. - `sce setup --help` returns setup-specific usage output with target-flag contract details and deterministic examples, including one-run non-interactive setup+hooks and composable follow-up validation/repair-intent flows (`sce doctor --format json`, `sce doctor --fix`). -- `sce auth` and `sce auth --help` return auth-specific usage output with available subcommands and deterministic examples, while `sce auth --help` stays scoped to the selected auth subcommand. The removed `sce auth renew` and `sce auth status` routes are rejected as invalid commands. +- `sce auth` and `sce auth --help` return auth-specific usage output with available subcommands and deterministic examples, while `sce auth --help` stays scoped to the selected auth subcommand. Unknown `sce auth ` paths, including removed `renew` and `status` routes, return the same auth help successfully; valid auth routes remain unchanged. - `sce doctor --help` and `sce hooks --help` return command-local usage output and deterministic copy-ready examples. - Interactive `sce setup` prompt cancellation/interrupt exits cleanly with: `Setup cancelled. No files were changed.` - Command handlers return deterministic status messaging: diff --git a/context/glossary.md b/context/glossary.md index 33ab4cce..e0827df9 100644 --- a/context/glossary.md +++ b/context/glossary.md @@ -70,7 +70,7 @@ - `auth login stored-credential renewal`: The `sce auth login` behavior that first validates every stored credential through the existing non-forced token path, preserves valid credentials, refreshes expired credentials, and falls back to device authorization when credentials are absent or renewal fails. Renewal reports retain `login` labels in text and JSON; credential renewal is not exposed as a public subcommand. - `command surface contract`: The current top-level command/help catalog split where `cli/src/cli_schema.rs` owns the real clap-backed command metadata (top-level purpose text plus help visibility for `auth`, `config`, `setup`, `doctor`, `hooks`, `policy`, `sync`, `version`, and `completion`) and `cli/src/command_surface.rs` consumes that catalog for the custom banner/help surface plus known-command classification, while still adding the synthetic `help` row. - `top-level help visibility metadata`: Per-command `show_in_top_level_help` metadata in `cli/src/cli_schema.rs` that controls whether a known command appears in `sce`, `sce help`, and `sce --help` without affecting direct invocation; the current hidden top-level commands are `hooks` and `policy`, while `auth` is visible, and `cli/src/command_surface.rs` renders the curated top-level help list from that shared metadata. -- `command loop`: The `clap` derive-based parser + dispatcher in `cli/src/cli_schema.rs`, `cli/src/services/parse/command_runtime.rs`, and `cli/src/app.rs` that routes `help`, `config`, `setup`, `doctor`, `auth`, `hooks`, `policy`, `sync`, `version`, and `completion`, executes implemented command flows, emits command-local help payloads for supported subcommand trees, and returns deterministic actionable errors for invalid invocation. +- `command loop`: The `clap` derive-based parser + dispatcher in `cli/src/cli_schema.rs`, `cli/src/services/parse/command_runtime.rs`, and `cli/src/app.rs` that routes `help`, `config`, `setup`, `doctor`, `auth`, `hooks`, `policy`, `sync`, `version`, and `completion`, executes implemented command flows, emits command-local help payloads for supported subcommand trees, resolves unknown command paths to the longest valid parent's help surface, and returns deterministic actionable errors for unknown options and other invalid invocation. - `RuntimeCommand seam`: Internal static command-execution abstraction in `cli/src/services/command_registry.rs` where clap-parsed commands are represented as a `RuntimeCommand` enum with variants for `Help`, `HelpText`, `Version`, `Completion`, `Auth`, `Config`, `Setup`, `Doctor`, `Hooks`, `Policy`, and `Sync`. The enum owns `name()` and `execute(...)` dispatch, delegating behavior to service-owned command payload structs while avoiding boxed `dyn RuntimeCommand` handles. Parsed request construction lives in `cli/src/services/parse/command_runtime.rs` when user-provided options or subcommands are required. - `sce dependency baseline`: Current crate dependency set declared in `cli/Cargo.toml` (`anyhow`, `chrono`, `clap`, `clap_complete`, `dirs`, `hmac`, `indicatif`, `inquire`, `jsonschema`, `keyring-core`, `murmur3`, `owo-colors`, `rand`, `reqwest`, `serde`, `serde_json`, `sha2`, `tokio`, `tracing`, `turso`, `uuid`, plus target-specific keyring backends). No CLI dev-dependencies are currently declared, and the baseline is validated through normal compile/test coverage. - `progress reporter contract`: Library-independent, consumer-typed sync seam in `cli/src/services/sync/progress.rs` where `ProgressReporter` carries an arbitrary consumer event type, supports explicit successful finalization and closure-based collectors, and provides `NoopProgressReporter` for callers without human progress output. The sync-owned `SyncProgressEvent` in `cli/src/services/sync/sync.rs` carries lifecycle, accepted-batch, and stream-completion payloads for the four concurrent Agent Trace streams; `services::sync::progress` also owns the fixed `indicatif` terminal adapter and focused contract tests, while `sync/command.rs` selects text versus JSON behavior. No top-level `services::progress` module exists. diff --git a/context/overview.md b/context/overview.md index 34fae06a..f7ba0b26 100644 --- a/context/overview.md +++ b/context/overview.md @@ -17,7 +17,7 @@ The generated `/next-task` workflow persists task-level context-synchronization - **Install channels:** repo-flake Nix, Cargo, npm, and source-built Flatpak (`dev.crocoder.sce`); Homebrew deferred (see `context/sce/cli-first-install-channels-contract.md`). The CLI crate currently depends on `anyhow`, `chrono`, `clap`, `clap_complete`, `dirs`, `hmac`, `indicatif`, `inquire`, `jsonschema`, `keyring-core`, `murmur3`, `owo-colors`, `rand`, `reqwest`, `serde`, `serde_json`, `sha2`, `tokio`, `tracing`, `turso`, and `uuid`, with target-specific keyring backend dependencies for Linux/FreeBSD, macOS, and Windows. No CLI dev-dependencies are currently declared. -Its command loop is implemented with `clap` derive-based argument parsing and `anyhow` error handling. Top-level help displays an ASCII art "SCE" banner with a per-column right-to-left color gradient (cyan to magenta when color is enabled, plain ASCII when disabled) above a slim command list without implemented/placeholder labels; `auth` is visible while `hooks` and `policy` remain directly invocable but hidden. The real top-level command catalog/help-visibility contract is centralized in `cli/src/cli_schema.rs` and consumed by `cli/src/command_surface.rs` for custom banner/help rendering plus known-command classification. The runtime includes implemented auth flows (`auth login|logout|whoami`), with authenticated whoami reading the Control Plane `GET /me` profile and rendering flat email/name/role/permissions/organization labels, optional names and missing values handled deterministically, and exact login guidance returned when logged out, alongside config inspection/validation, setup orchestration, doctor diagnosis/repair, attribution-only hooks, shell completion, and the top-level `sync` command. Parse-time command conversion plus run-time command handling flow through the internal `RuntimeCommand` seam in `cli/src/app.rs`. +Its command loop is implemented with `clap` derive-based argument parsing and `anyhow` error handling. Top-level help displays an ASCII art "SCE" banner with a per-column right-to-left color gradient (cyan to magenta when color is enabled, plain ASCII when disabled) above a slim command list without implemented/placeholder labels; `auth` is visible while `hooks` and `policy` remain directly invocable but hidden. The real top-level command catalog/help-visibility contract is centralized in `cli/src/cli_schema.rs` and consumed by `cli/src/command_surface.rs` for custom banner/help rendering plus known-command classification. The runtime includes implemented auth flows (`auth login|logout|whoami`), with authenticated whoami reading the Control Plane `GET /me` profile and rendering flat email/name/role/permissions/organization labels, optional names and missing values handled deterministically, and exact login guidance returned when logged out, alongside config inspection/validation, setup orchestration, doctor diagnosis/repair, attribution-only hooks, shell completion, and the top-level `sync` command. Unknown command paths are successful help requests: top-level unknown tokens use the normal top-level help payload and nested unknown tokens use the closest valid parent's help surface, while unknown options and other parse/validation failures retain their existing errors. Parse-time command conversion plus run-time command handling flow through the internal `RuntimeCommand` seam in `cli/src/app.rs`. The current doctor presentation contract supersedes the earlier output-shape scaffolding wording above: human text uses the compact Environment/Repository/Integrations hierarchy with healthy rows collapsed and unhealthy branches expanded, while JSON retains complete path, identity, problem, and fix-result detail. See `context/sce/doctor-human-text-contract.md`. The command loop now enforces a stable exit-code contract in `cli/src/app.rs`: `2` parse failures, `3` invocation validation failures, `4` runtime failures, and `5` dependency startup failures. The same runtime also emits stable user-facing stderr error classes (`SCE-ERR-PARSE`, `SCE-ERR-VALIDATION`, `SCE-ERR-RUNTIME`, `SCE-ERR-DEPENDENCY`) using deterministic `Error []: ...` diagnostics with class-default `Try:` remediation appended when missing. The command boundary's former flat, string-only `ClassifiedError` has been replaced by typed `CliError` in `cli/src/services/error.rs`: `CliError::User` carries a closed `UserError` catalog (`NotAuthenticated`, `NotGitRepository`, and payload-bearing `NotGitRemote { remote_name }`) for expected, deliberately-explained failures rendered without a `Try:` suffix, while `CliError::Internal` carries a live `anyhow::Error` source rendered as the real error chain with class-default remediation; `app_support` is the sole owner of turning either into the final styled stderr diagnostic, and `sce sync` is the first command to classify a failure (authentication) into `CliError::User`. Setup preflight errors preserve technical sources for observability, identify the configured missing remote by name, and keep raw remote URLs out of user-facing diagnostics. See `context/sce/cli-error-code-taxonomy.md` for the full contract. diff --git a/context/sce/cli-exit-code-contract.md b/context/sce/cli-exit-code-contract.md index 32662e42..839113cd 100644 --- a/context/sce/cli-exit-code-contract.md +++ b/context/sce/cli-exit-code-contract.md @@ -8,7 +8,7 @@ The contract is intentionally class-based so automation can branch on failure ca ## Exit-code classes - `0` (`success`): command completed successfully. -- `2` (`parse_failure`): top-level CLI parsing failed (for example unknown top-level command/option or malformed command token). +- `2` (`parse_failure`): CLI parsing failed for an invocation that is not an unknown command/subcommand help request (for example an unknown option or malformed command token). Unknown top-level commands and unknown subcommands instead resolve to successful help with exit code `0`. - `3` (`validation_failure`): command/subcommand arguments parsed but failed invocation validation (for example incompatible or missing command-local arguments). - `4` (`runtime_failure`): command invocation was valid but runtime execution failed (filesystem/process/environment/runtime operation errors). - `5` (`dependency_failure`): startup dependency checks failed before command parsing/dispatch.