Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ can't hang forever, and output is dense and machine-readable.
`--statement-timeout`, `--lock-timeout`). A bad query fails fast instead of
pinning a backend.
- **Structured output.** Add `--json` to query/diagnostic commands when the
result feeds further work. Human tables otherwise.
result feeds further work. Human tables otherwise — and those auto-densify
when output is piped or captured (no box-drawing or padding, same info, fewer
tokens); pass `--pretty` to force the decorated form.
- **Semantic exit codes** — branch on them, don't parse text:
- `0` healthy / success
- `1` warning (non-critical finding)
Expand Down
45 changes: 43 additions & 2 deletions src/commands/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -689,8 +689,49 @@ fn check_fix_terminate_capability(has_pg_terminate: bool, read_only: bool) -> Ca
}
}

/// Print capabilities in human-readable format
pub fn print_human(result: &CapabilitiesResult) {
/// Print capabilities in human-readable format.
///
/// `Pretty` pads the capability id to a fixed column and uses glyph status
/// markers. `Dense` drops the padding and glyphs (status as a word) — same
/// per-capability id, status, and notes, fewer tokens.
pub fn print_human(result: &CapabilitiesResult, density: crate::output::Density) {
if density.is_dense() {
print_dense(result);
} else {
print_pretty(result);
}
}

/// Plain status word, shared so both forms agree on the vocabulary.
fn status_word(status: CapabilityStatus) -> &'static str {
match status {
CapabilityStatus::Available => "available",
CapabilityStatus::Degraded => "degraded",
CapabilityStatus::Unavailable => "unavailable",
CapabilityStatus::Unknown => "unknown",
}
}

fn print_dense(result: &CapabilitiesResult) {
println!("CAPABILITIES:");
for cap in &result.capabilities {
println!("{} {}", cap.id, status_word(cap.status));
for lim in &cap.limitations {
println!(" - {}", lim);
}
if cap.status != CapabilityStatus::Available {
for reason in &cap.reasons {
println!(" - {}", reason.message);
}
}
}
println!(
"SUMMARY: {} available, {} degraded, {} unavailable",
result.summary.available, result.summary.degraded, result.summary.unavailable
);
}

fn print_pretty(result: &CapabilitiesResult) {
println!("CAPABILITIES:");
println!();

Expand Down
86 changes: 84 additions & 2 deletions src/commands/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,90 @@ pub async fn run_context(
})
}

/// Print context in human-readable format
pub fn print_human(result: &ContextResult) {
/// Print context in human-readable format.
///
/// `Pretty` keeps the padded, blank-line-separated layout a person reads.
/// `Dense` strips the blank lines and padding and folds extensions/roles onto
/// single lines — same fields, far fewer tokens for an agent capturing it.
pub fn print_human(result: &ContextResult, density: crate::output::Density) {
if density.is_dense() {
print_dense(result);
} else {
print_pretty(result);
}
}

fn yes_no(b: bool) -> &'static str {
if b {
"yes"
} else {
"no"
}
}

fn print_dense(result: &ContextResult) {
let ctx = &result.context;

println!(
"CONNECTION: {}@{}:{}/{} ({})",
ctx.target.user,
ctx.target.host,
ctx.target.port,
ctx.target.database,
if ctx.target.readonly {
"read-only"
} else {
"read-write"
}
);

let recovery = if ctx.server.in_recovery {
"replica"
} else {
"primary"
};
match ctx.server.data_directory {
Some(ref dir) => println!(
"SERVER: pg {} ({}) {} data_dir={}",
ctx.server.version_major, ctx.server.version_num, recovery, dir
),
None => println!(
"SERVER: pg {} ({}) {}",
ctx.server.version_major, ctx.server.version_num, recovery
),
}

let mut exts: Vec<_> = ctx.extensions.iter().collect();
exts.sort_by_key(|(name, _)| name.as_str());
let ext_list: Vec<String> = exts
.iter()
.map(|(name, version)| format!("{}={}", name, version))
.collect();
if ext_list.is_empty() {
println!("EXTENSIONS (0):");
} else {
println!(
"EXTENSIONS ({}): {}",
ctx.extensions.len(),
ext_list.join(" ")
);
}

println!(
"PRIVILEGES: superuser={} pg_stat_activity={} pg_cancel_backend={} pg_terminate={} pg_stat_statements={}",
yes_no(ctx.privileges.is_superuser),
yes_no(ctx.privileges.pg_stat_activity_select),
yes_no(ctx.privileges.pg_cancel_backend_execute),
yes_no(ctx.privileges.pg_terminate_backend_execute),
yes_no(ctx.privileges.pg_stat_statements_select),
);

if !ctx.privileges.roles.is_empty() {
println!("ROLES: {}", ctx.privileges.roles.join(" "));
}
}

fn print_pretty(result: &ContextResult) {
let ctx = &result.context;

println!("CONNECTION:");
Expand Down
47 changes: 33 additions & 14 deletions src/commands/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -694,35 +694,54 @@ pub async fn describe(
return Ok(());
}

// Human mode: formatted output
// Human mode: formatted output. Dense drops the decorative rule and the
// blank lines framing each section; pretty keeps them.
let density = output.density();
let dense = density.is_dense();
let mut result = String::new();
result.push('\n');
if !dense {
result.push('\n');
}
result.push_str(&format!(
"Table: {}.{}\n",
quote_ident(&resolved.schema),
quote_ident(&resolved.name)
));
result.push_str(&"─".repeat(64));
result.push('\n');
result.push('\n');
result.push_str(&table_info.format(verbose));
if !dense {
result.push_str(&"─".repeat(64));
result.push('\n');
result.push('\n');
}
result.push_str(&table_info.format(verbose, density));

// Append dependents/dependencies section if requested
if let Some(ref deps) = deps_data {
result.push('\n');
result.push('\n');
if dense {
result.push('\n');
} else {
result.push('\n');
result.push('\n');
}
result.push_str("Direct Dependents:");
result.push('\n');
result.push('\n');
result.push_str(&deps.format(&resolved.schema, &resolved.name));
if !dense {
result.push('\n');
}
result.push_str(&deps.format(&resolved.schema, &resolved.name, density));
}
if let Some(ref deps) = dependencies_data {
result.push('\n');
result.push('\n');
if dense {
result.push('\n');
} else {
result.push('\n');
result.push('\n');
}
result.push_str("Direct Dependencies:");
result.push('\n');
result.push('\n');
result.push_str(&deps.format(&resolved.schema, &resolved.name));
if !dense {
result.push('\n');
}
result.push_str(&deps.format(&resolved.schema, &resolved.name, density));
}

output.data(&result);
Expand Down
30 changes: 24 additions & 6 deletions src/commands/sql_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pub struct SqlOptions {
pub timeouts: TimeoutConfig,
pub quiet: bool,
pub json: bool,
/// Readable-output density for result tables (ignored in JSON/quiet mode).
pub density: crate::output::Density,
}

#[derive(Serialize)]
Expand Down Expand Up @@ -219,7 +221,7 @@ async fn run_read(client: &Client, sql: &str, opts: &SqlOptions) -> Result<()> {
if opts.quiet {
return Ok(());
}
print_results(&results);
print_results(&results, opts.density);
Ok(())
}

Expand Down Expand Up @@ -420,7 +422,7 @@ fn finish_write(
}

// Print any sample rows first, then the summary banner.
print_results(&results);
print_results(&results, opts.density);

if committed {
println!("\nCOMMITTED — {rows_affected} row(s) affected.");
Expand Down Expand Up @@ -499,22 +501,22 @@ fn emit_json(opts: &SqlOptions, write: Option<WriteOutcome>, results: Vec<SqlRes
let _ = opts;
}

fn print_results(results: &[SqlResult]) {
fn print_results(results: &[SqlResult], density: crate::output::Density) {
for result in results {
match result {
SqlResult::Query {
columns,
rows,
truncated,
} => {
print_table(columns, rows);
print_table(columns, rows, density);
if *truncated > 0 {
println!("… +{truncated} more row(s) — use --limit N (or --limit 0 to uncap)");
}
}
SqlResult::Sample { columns, rows } => {
println!("Sample of affected rows:");
print_table(columns, rows);
print_table(columns, rows, density);
}
SqlResult::CommandComplete { rows } => {
println!("OK ({rows} rows)");
Expand Down Expand Up @@ -656,11 +658,27 @@ fn classify(sql: &str) -> Result<ParsedSql> {
})
}

fn print_table(columns: &[String], rows: &[Vec<Option<String>>]) {
fn print_table(columns: &[String], rows: &[Vec<Option<String>>], density: crate::output::Density) {
if columns.is_empty() {
return;
}

if density.is_dense() {
// Dense: header row plus unpadded ` | `-joined cells, no width padding
// and no separator rule. Columns are still labelled and aligned by the
// pipe delimiter, so the result stays unambiguous.
println!("{}", columns.join(" | "));
for row in rows {
let line: Vec<&str> = columns
.iter()
.enumerate()
.map(|(i, _)| row.get(i).and_then(|v| v.as_deref()).unwrap_or("NULL"))
.collect();
println!("{}", line.join(" | "));
}
return;
}

let mut widths: Vec<usize> = columns.iter().map(|c| c.len()).collect();
for row in rows {
for (i, cell) in row.iter().enumerate() {
Expand Down
Loading
Loading