Skip to content

Commit c1adc68

Browse files
Sync README with post-merge state: Ctx, safe_json_string, full example layout
- Update Example section: list all files including config.rs, config commands, contract.rs, tests/ directory, Ctx struct, --quiet flag - Update Reusable Modules: replace Format-only API with Ctx-based API matching actual example code, replace unsafe string interpolation fallback with safe_json_string pattern using serde_json::json! - Update Entry Point Runner: use Ctx::new instead of Format::detect - Fix Philosophy #8: match const pattern used in example - All 40 integration tests pass
1 parent 601fff5 commit c1adc68

1 file changed

Lines changed: 61 additions & 30 deletions

File tree

README.md

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ If `inbox list` works, `account list` works. If `--json` forces JSON in one CLI,
8282

8383
### 8. Self-contained and portable
8484

85-
The binary carries its own skill file (`include_str!`). `skill install` deploys it. `update` replaces the binary from GitHub Releases. One artifact. The self-update mechanism is opt-in -- CLIs distributed via package managers or in managed environments should disable it.
85+
The binary carries its own skill file as an embedded constant (via `const` or `include_str!`). `skill install` deploys it. `update` replaces the binary from GitHub Releases. One artifact. The self-update mechanism is opt-in -- CLIs distributed via package managers or in managed environments should disable it.
8686

8787
### 9. Speed is a feature
8888

@@ -236,9 +236,9 @@ Self-update should be disableable via config (`update.enabled = false`) for mana
236236

237237
These are battle-tested patterns extracted from production CLIs. Each module is self-contained -- copy the pattern into your CLI and adapt.
238238

239-
### Output Format Detection
239+
### Output Format Detection and Context
240240

241-
Every CLI needs this. Detect whether to output JSON or human-readable, based on `--json` flag or pipe detection.
241+
Detect whether to output JSON or human-readable, based on `--json` flag or pipe detection. Bundle format + quiet into a `Ctx` that gets passed to all commands.
242242

243243
```rust
244244
#[derive(Clone, Copy)]
@@ -255,39 +255,62 @@ impl Format {
255255
Format::Human
256256
}
257257
}
258+
}
258259

259-
pub fn is_json(self) -> bool {
260-
matches!(self, Format::Json)
260+
/// Output context: bundles format + quiet so commands take one parameter.
261+
#[derive(Clone, Copy)]
262+
pub struct Ctx {
263+
pub format: Format,
264+
pub quiet: bool,
265+
}
266+
267+
impl Ctx {
268+
pub fn new(json_flag: bool, quiet: bool) -> Self {
269+
Self { format: Format::detect(json_flag), quiet }
261270
}
262271
}
263272
```
264273

265274
### JSON Envelope Helpers
266275

267-
`print_success_or` is the workhorse -- it handles JSON automatically and lets you provide a closure for human output. `print_error` sends errors to stderr in both formats.
276+
`print_success_or` is the workhorse -- it handles JSON automatically and lets you provide a closure for human output. `--quiet` suppresses human output; JSON always emits. `print_error` sends errors to stderr in both formats (never suppressed by `--quiet`).
268277

269278
```rust
270279
use serde::Serialize;
271280

272-
fn to_json_pretty<T: Serialize>(value: &T) -> String {
273-
serde_json::to_string_pretty(value).unwrap_or_else(|e| {
274-
format!(
275-
r#"{{"version":"1","status":"error","error":{{"code":"serialize","message":"{e}"}}}}"#
276-
)
277-
})
281+
/// Safe serialization: never panics, never produces invalid JSON.
282+
fn safe_json_string<T: Serialize>(value: &T) -> String {
283+
match serde_json::to_string_pretty(value) {
284+
Ok(s) => s,
285+
Err(e) => {
286+
let fallback = serde_json::json!({
287+
"version": "1",
288+
"status": "error",
289+
"error": {
290+
"code": "serialize",
291+
"message": e.to_string(),
292+
"suggestion": "Retry the command",
293+
},
294+
});
295+
serde_json::to_string_pretty(&fallback).unwrap_or_else(|_| {
296+
r#"{"version":"1","status":"error","error":{"code":"serialize","message":"serialization failed","suggestion":"Retry the command"}}"#.to_string()
297+
})
298+
}
299+
}
278300
}
279301

280-
pub fn print_success_or<T: Serialize, F: FnOnce(&T)>(format: Format, data: &T, human: F) {
281-
match format {
302+
pub fn print_success_or<T: Serialize, F: FnOnce(&T)>(ctx: Ctx, data: &T, human: F) {
303+
match ctx.format {
282304
Format::Json => {
283305
let envelope = serde_json::json!({
284306
"version": "1",
285307
"status": "success",
286308
"data": data,
287309
});
288-
println!("{}", to_json_pretty(&envelope));
310+
println!("{}", safe_json_string(&envelope));
289311
}
290-
Format::Human => human(data),
312+
Format::Human if !ctx.quiet => human(data),
313+
Format::Human => {} // quiet: suppress human output
291314
}
292315
}
293316

@@ -302,7 +325,7 @@ pub fn print_error(format: Format, err: &AppError) {
302325
},
303326
});
304327
match format {
305-
Format::Json => eprintln!("{}", to_json_pretty(&envelope)),
328+
Format::Json => eprintln!("{}", safe_json_string(&envelope)),
306329
Format::Human => {
307330
eprintln!("error: {err}");
308331
eprintln!(" {}", err.suggestion());
@@ -415,10 +438,10 @@ fn main() {
415438
}
416439
};
417440

418-
let format = Format::detect(cli.json);
441+
let ctx = Ctx::new(cli.json, cli.quiet);
419442

420-
if let Err(e) = run(cli, format) {
421-
print_error(format, &e);
443+
if let Err(e) = run(cli, ctx) {
444+
print_error(ctx.format, &e);
422445
std::process::exit(e.exit_code());
423446
}
424447
}
@@ -684,21 +707,29 @@ The framework conventions (`env!("CARGO_PKG_NAME")`, config loading, skill insta
684707

685708
## Example
686709

687-
The `example/` directory contains a modular `greeter` CLI demonstrating the five core patterns (agent-info, JSON envelope, semantic exit codes, skill self-install, self-update) and the reusable entry point, error type, and output helpers. Config loading, secret handling, XDG paths, and HTTP retry are documented as code patterns in the Reusable Modules section above -- copy them into your CLI when you need them.
710+
The `example/` directory contains a modular `greeter` CLI demonstrating all core patterns: agent-info with argument schemas, JSON envelope, semantic exit codes (0-4), `--json` pre-scan, `--quiet` flag, config loading via Figment, skill self-install, and self-update. It includes 40 integration tests that verify every contract.
688711

689712
```
690713
example/
691714
src/
692-
main.rs # Entry point -- parse, detect format, dispatch, exit
693-
cli.rs # Clap definitions: Cli struct + Commands enum
694-
error.rs # AppError enum with exit_code(), error_code(), suggestion()
695-
output.rs # Format detection + envelope helpers
715+
main.rs # Entry point -- pre-scan --json, parse, dispatch, exit
716+
cli.rs # Clap definitions: Cli, Commands, Style (ValueEnum)
717+
config.rs # 3-tier config loading (defaults -> TOML -> env vars)
718+
error.rs # AppError with exit_code(), error_code(), suggestion()
719+
output.rs # Format detection, Ctx struct, envelope helpers
696720
commands/
697-
mod.rs # Command router
698-
hello.rs # Domain command (the actual feature)
699-
agent_info.rs # Capability manifest
700-
skill.rs # Skill install + status
701-
update.rs # Self-update
721+
mod.rs # Command router
722+
hello.rs # Domain command (the actual feature)
723+
agent_info.rs # Enriched capability manifest with arg schemas
724+
config.rs # config show / config path
725+
skill.rs # Skill install + status
726+
update.rs # Self-update
727+
contract.rs # Hidden: deterministic exit-code trigger for tests
728+
tests/
729+
exit_code_contracts.rs # All 5 exit codes verified
730+
output_contracts.rs # JSON envelope shape, quiet flag, help wrapping
731+
agent_info_contract.rs # Manifest fields, routable commands, arg schemas
732+
robustness.rs # Malformed config resilience, edge cases
702733
Cargo.toml
703734
```
704735

0 commit comments

Comments
 (0)