Skip to content

Commit 8506920

Browse files
cli: Add shared styling service with slim help surface and trimmed validate output
Add comprehensive styling infrastructure with stderr, help-text, and prompt support through the shared style service Co-authored-by: SCE <sce@crocoder.dev>
1 parent cc7a267 commit 8506920

22 files changed

Lines changed: 922 additions & 220 deletions

.opencode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"dependencies": {
3-
"@opencode-ai/plugin": "1.3.9"
3+
"@opencode-ai/plugin": "1.3.10"
44
}
55
}

cli/src/app.rs

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,11 @@ where
127127
};
128128

129129
let styled_code = services::style::error_code(error.code());
130+
let styled_heading = services::style::heading_stderr("Error");
131+
let styled_message =
132+
services::style::error_text(&services::security::redact_sensitive_text(&rendered));
130133

131-
let _ = writeln!(
132-
writer,
133-
"Error [{}]: {}",
134-
styled_code,
135-
services::security::redact_sensitive_text(&rendered)
136-
);
134+
let _ = writeln!(writer, "{styled_heading} [{styled_code}]: {styled_message}");
137135
}
138136

139137
#[allow(clippy::too_many_lines)]
@@ -310,11 +308,8 @@ where
310308
return Ok(Command::Help);
311309
}
312310
if error.kind() == clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand {
313-
if args_vec.get(1).map(String::as_str) == Some(services::auth_command::NAME) {
314-
return Ok(Command::HelpText {
315-
name: services::auth_command::NAME.to_string(),
316-
text: cli_schema::auth_help_text(),
317-
});
311+
if let Some(help_text) = render_missing_subcommand_help(&args_vec) {
312+
return Ok(help_text);
318313
}
319314

320315
return Err(ClassifiedError::parse(
@@ -377,6 +372,22 @@ fn render_subcommand_help_from_args(args: &[String]) -> Option<(String, String)>
377372
cli_schema::render_help_for_path(&command_path).map(|text| (command_name, text))
378373
}
379374

375+
fn render_missing_subcommand_help(args: &[String]) -> Option<Command> {
376+
let command_name = args.get(1)?.as_str();
377+
378+
match command_name {
379+
services::auth_command::NAME => Some(Command::HelpText {
380+
name: services::auth_command::NAME.to_string(),
381+
text: cli_schema::auth_help_text(),
382+
}),
383+
services::config::NAME => Some(Command::HelpText {
384+
name: services::config::NAME.to_string(),
385+
text: cli_schema::render_help_for_path(&[services::config::NAME])?,
386+
}),
387+
_ => None,
388+
}
389+
}
390+
380391
fn clean_clap_error_message(message: &str, kind: clap::error::ErrorKind) -> String {
381392
use clap::error::ErrorKind;
382393

@@ -730,12 +741,13 @@ fn dispatch(
730741
mod tests {
731742
use std::process::ExitCode;
732743

744+
use crate::services::error::ClassifiedError;
733745
use crate::services::setup::{SetupMode, SetupRequest, SetupTarget};
734746

735747
use super::{
736748
parse_command, run, run_with_dependency_check, run_with_dependency_check_and_streams,
737-
Command, EXIT_CODE_DEPENDENCY_FAILURE, EXIT_CODE_PARSE_FAILURE, EXIT_CODE_RUNTIME_FAILURE,
738-
EXIT_CODE_VALIDATION_FAILURE,
749+
write_error_diagnostic, Command, EXIT_CODE_DEPENDENCY_FAILURE, EXIT_CODE_PARSE_FAILURE,
750+
EXIT_CODE_RUNTIME_FAILURE, EXIT_CODE_VALIDATION_FAILURE,
739751
};
740752

741753
#[test]
@@ -836,6 +848,21 @@ mod tests {
836848
assert!(stderr.contains("Try:"));
837849
}
838850

851+
#[test]
852+
fn write_error_diagnostic_preserves_plain_text_contract_without_color() {
853+
let mut stderr = Vec::new();
854+
855+
write_error_diagnostic(
856+
&mut stderr,
857+
&ClassifiedError::parse("Unknown command 'nope'. Try: run 'sce --help'."),
858+
);
859+
860+
assert_eq!(
861+
String::from_utf8(stderr).expect("stderr should be utf-8"),
862+
"Error [SCE-ERR-PARSE]: Unknown command 'nope'. Try: run 'sce --help'.\n"
863+
);
864+
}
865+
839866
#[test]
840867
fn empty_payload_does_not_write_stdout() {
841868
let mut stdout = Vec::new();
@@ -1543,6 +1570,38 @@ mod tests {
15431570
);
15441571
}
15451572

1573+
#[test]
1574+
fn parser_routes_bare_config_to_help_text() {
1575+
let command = parse_command(vec!["sce".to_string(), "config".to_string()], None)
1576+
.expect("bare config should route to help");
1577+
1578+
assert_eq!(
1579+
command,
1580+
Command::HelpText {
1581+
name: crate::services::config::NAME.to_string(),
1582+
text: crate::cli_schema::render_help_for_path(&[crate::services::config::NAME])
1583+
.expect("config help should render"),
1584+
}
1585+
);
1586+
}
1587+
1588+
#[test]
1589+
fn parser_routes_bare_config_to_same_help_as_config_help_flag() {
1590+
let bare_config = parse_command(vec!["sce".to_string(), "config".to_string()], None)
1591+
.expect("bare config should route to help");
1592+
let config_help = parse_command(
1593+
vec![
1594+
"sce".to_string(),
1595+
"config".to_string(),
1596+
"--help".to_string(),
1597+
],
1598+
None,
1599+
)
1600+
.expect("config --help should route to help");
1601+
1602+
assert_eq!(bare_config, config_help);
1603+
}
1604+
15461605
#[test]
15471606
fn parser_routes_completion_bash_shell() {
15481607
let command = parse_command(

cli/src/cli_schema.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use clap::{CommandFactory, Parser, Subcommand, ValueEnum};
22
use std::path::PathBuf;
33

4+
use crate::services::style;
5+
46
#[derive(Parser, Debug)]
57
#[command(name = "sce", version, about, long_about = None)]
68
pub struct Cli {
@@ -39,7 +41,9 @@ pub fn render_help_for_path(path: &[&str]) -> Option<String> {
3941
.write_long_help(&mut buffer)
4042
.expect("help rendering should write to memory");
4143

42-
Some(String::from_utf8(buffer).expect("help output should be valid UTF-8"))
44+
let help = String::from_utf8(buffer).expect("help output should be valid UTF-8");
45+
46+
Some(style::clap_help(&help))
4347
}
4448

4549
pub fn auth_help_text() -> String {
@@ -183,7 +187,7 @@ pub enum ConfigSubcommand {
183187
timeout_ms: Option<u64>,
184188
},
185189

186-
#[command(about = "Validate config files and report resolved observability values")]
190+
#[command(about = "Validate config files and report pass/fail with errors or warnings")]
187191
Validate {
188192
#[arg(long, value_enum, default_value_t = OutputFormat::Text)]
189193
format: OutputFormat,
@@ -504,11 +508,11 @@ mod tests {
504508
}
505509

506510
#[test]
507-
fn render_help_for_config_validate_mentions_observability() {
511+
fn render_help_for_config_validate_mentions_pass_fail_output() {
508512
let help = render_help_for_path(&["config", "validate"])
509513
.expect("config validate help should render");
510514

511-
assert!(help.contains("Validate config files and report resolved observability values"));
515+
assert!(help.contains("Validate config files and report pass/fail with errors or warnings"));
512516
assert!(help.contains("--config <CONFIG>"));
513517
assert!(help.contains("--format <FORMAT>"));
514518
}

cli/src/command_surface.rs

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::fmt::Write;
22

33
use crate::services;
4-
use services::style::{command_name, heading, status_implemented, status_placeholder};
4+
use services::style::{command_name, example_command, heading};
55

66
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
77
pub enum ImplementationStatus {
@@ -14,58 +14,69 @@ pub struct CommandContract {
1414
pub name: &'static str,
1515
pub status: ImplementationStatus,
1616
pub purpose: &'static str,
17+
pub show_in_top_level_help: bool,
1718
}
1819

1920
pub const COMMANDS: &[CommandContract] = &[
2021
CommandContract {
2122
name: "help",
2223
status: ImplementationStatus::Implemented,
23-
purpose: "Print the current placeholder command surface",
24+
purpose: "Show help for the current CLI surface",
25+
show_in_top_level_help: true,
2426
},
2527
CommandContract {
2628
name: services::config::NAME,
2729
status: ImplementationStatus::Implemented,
2830
purpose: "Inspect and validate resolved CLI configuration",
31+
show_in_top_level_help: true,
2932
},
3033
CommandContract {
3134
name: services::setup::NAME,
3235
status: ImplementationStatus::Implemented,
3336
purpose: "Prepare local repository/workspace prerequisites",
37+
show_in_top_level_help: true,
3438
},
3539
CommandContract {
3640
name: services::doctor::NAME,
3741
status: ImplementationStatus::Implemented,
3842
purpose: "Inspect SCE operator health and explicit repair readiness",
43+
show_in_top_level_help: true,
3944
},
4045
CommandContract {
4146
name: services::auth_command::NAME,
4247
status: ImplementationStatus::Implemented,
4348
purpose: "Authenticate with WorkOS and inspect local auth state",
49+
show_in_top_level_help: false,
4450
},
4551
CommandContract {
4652
name: services::hooks::NAME,
4753
status: ImplementationStatus::Implemented,
4854
purpose: "Run git-hook runtime entrypoints for local Agent Trace flows",
55+
show_in_top_level_help: false,
4956
},
5057
CommandContract {
5158
name: services::trace::NAME,
5259
status: ImplementationStatus::Implemented,
5360
purpose: "Inspect persisted Agent Trace records and captured prompts",
61+
show_in_top_level_help: false,
5462
},
5563
CommandContract {
5664
name: services::sync::NAME,
5765
status: ImplementationStatus::Placeholder,
5866
purpose: "Coordinate future cloud sync workflows",
67+
show_in_top_level_help: false,
5968
},
6069
CommandContract {
6170
name: services::version::NAME,
6271
status: ImplementationStatus::Implemented,
6372
purpose: "Print deterministic runtime version metadata",
73+
show_in_top_level_help: true,
6474
},
6575
CommandContract {
6676
name: services::completion::NAME,
6777
status: ImplementationStatus::Implemented,
6878
purpose: "Generate deterministic shell completion scripts",
79+
show_in_top_level_help: true,
6980
},
7081
];
7182

@@ -76,20 +87,14 @@ pub fn is_known_command(name: &str) -> bool {
7687
pub fn help_text() -> String {
7788
let mut command_rows = String::new();
7889
for command in COMMANDS {
79-
let status_text = match command.status {
80-
ImplementationStatus::Implemented => "implemented",
81-
ImplementationStatus::Placeholder => "placeholder",
82-
};
83-
let styled_status = match command.status {
84-
ImplementationStatus::Implemented => status_implemented(status_text),
85-
ImplementationStatus::Placeholder => status_placeholder(status_text),
86-
};
90+
if !command.show_in_top_level_help {
91+
continue;
92+
}
8793

8894
writeln!(
8995
command_rows,
90-
" {:<10} {:<12} {}",
96+
" {:<10} {}",
9197
command_name(command.name),
92-
styled_status,
9398
command.purpose
9499
)
95100
.unwrap();
@@ -101,31 +106,29 @@ pub fn help_text() -> String {
101106
{}:\n {} <show|validate> [--format <text|json>] [options]\n\n\
102107
{}:\n {} [--opencode|--claude|--both] [--non-interactive] [--hooks] [--repo <path>]\n\n\
103108
{}:\n {} [--fix] [--all-databases] [--format <text|json>]\n\n\
104-
{}:\n {} <login|logout|status> [--format <text|json>]\n\n\
105109
{}:\n {} --shell <bash|zsh|fish>\n\n\
106-
{}:\n {} prompts <commit-sha> [--format <text|json>|--json]\n\n\
107110
{}:\n Supported commands accept --format <text|json>\n\n\
108-
{}:\n sce setup\n sce setup --opencode --non-interactive --hooks\n sce setup --hooks --repo ../demo-repo\n sce auth status\n sce auth login --format json\n sce trace prompts abc1234\n sce trace prompts abc1234 --json\n sce doctor --format json\n sce doctor --all-databases --format json\n sce doctor --fix\n sce version --format json\n\n\
109-
{}:\n{command_rows}\n\
110-
Setup defaults to interactive target selection when no setup target flag is passed, and installs hooks in the same run.\n\
111-
Use '--hooks' to install required git hooks for the current repository or '--repo <path>' for a specific repository.\n\
112-
`setup`, `doctor`, `auth`, `hooks`, `trace`, `version`, and `completion` are implemented; `sync` remains placeholder-oriented.\n",
113-
heading("sce - Shared Context Engineering CLI (placeholder foundation)"),
111+
{}:\n {}\n {}\n {}\n {}\n {}\n {}\n {}\n\n\
112+
{}:\n{command_rows}",
113+
heading("sce - Shared Context Engineering CLI"),
114114
heading("Usage"),
115115
heading("Config usage"),
116116
command_name("sce config"),
117117
heading("Setup usage"),
118118
command_name("sce setup"),
119119
heading("Doctor usage"),
120120
command_name("sce doctor"),
121-
heading("Auth usage"),
122-
command_name("sce auth"),
123121
heading("Completion usage"),
124122
command_name("sce completion"),
125-
heading("Trace usage"),
126-
command_name("sce trace"),
127123
heading("Output format contract"),
128124
heading("Examples"),
125+
example_command("sce setup"),
126+
example_command("sce setup --opencode --non-interactive --hooks"),
127+
example_command("sce setup --hooks --repo ../demo-repo"),
128+
example_command("sce doctor --format json"),
129+
example_command("sce doctor --all-databases --format json"),
130+
example_command("sce doctor --fix"),
131+
example_command("sce version --format json"),
129132
heading("Commands"),
130133
)
131134
}
@@ -151,7 +154,6 @@ mod tests {
151154
assert!(help.contains(
152155
"sce setup [--opencode|--claude|--both] [--non-interactive] [--hooks] [--repo <path>]"
153156
));
154-
assert!(help.contains("installs hooks in the same run"));
155157
assert!(help.contains("sce setup --opencode --non-interactive --hooks"));
156158
}
157159

@@ -173,11 +175,25 @@ mod tests {
173175
}
174176

175177
#[test]
176-
fn help_text_mentions_auth_usage_examples() {
178+
fn hidden_commands_remain_known_even_when_not_shown_in_top_level_help() {
179+
for hidden_command in [
180+
crate::services::auth_command::NAME,
181+
crate::services::hooks::NAME,
182+
crate::services::trace::NAME,
183+
crate::services::sync::NAME,
184+
] {
185+
assert!(crate::command_surface::is_known_command(hidden_command));
186+
}
187+
}
188+
189+
#[test]
190+
fn help_text_hides_selected_commands_from_top_level_help() {
177191
let help = help_text();
178-
assert!(help.contains("sce auth <login|logout|status> [--format <text|json>]"));
179-
assert!(help.contains("sce auth status"));
180-
assert!(help.contains("sce auth login --format json"));
192+
193+
assert!(!help.contains("auth"));
194+
assert!(!help.contains("hooks"));
195+
assert!(!help.contains("trace"));
196+
assert!(!help.contains("sync"));
181197
}
182198

183199
#[test]
@@ -188,11 +204,14 @@ mod tests {
188204
}
189205

190206
#[test]
191-
fn help_text_mentions_trace_command() {
207+
fn help_text_drops_placeholder_and_status_copy() {
192208
let help = help_text();
193-
assert!(help.contains("trace"));
194-
assert!(help.contains("sce trace prompts <commit-sha>"));
195-
assert!(help.contains("sce trace prompts abc1234 --json"));
209+
210+
assert!(!help.contains("placeholder foundation"));
211+
assert!(!help.contains("implemented"));
212+
assert!(!help.contains("placeholder-oriented"));
213+
assert!(!help.contains("Setup defaults to interactive target selection"));
214+
assert!(!help.contains("Use '--hooks' to install required git hooks"));
196215
}
197216

198217
#[test]

0 commit comments

Comments
 (0)