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
503 changes: 503 additions & 0 deletions src/cargo_cmd.rs

Large diffs are not rendered by default.

338 changes: 300 additions & 38 deletions src/gh_cmd.rs

Large diffs are not rendered by default.

434 changes: 415 additions & 19 deletions src/git.rs

Large diffs are not rendered by default.

26 changes: 19 additions & 7 deletions src/json_cmd.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::tracking;
use anyhow::{Context, Result};
use serde_json::Value;
use std::fs;
use std::path::Path;
use crate::tracking;

/// Show JSON structure without values
pub fn run(file: &Path, max_depth: usize, verbose: u8) -> Result<()> {
Expand All @@ -13,15 +13,24 @@ pub fn run(file: &Path, max_depth: usize, verbose: u8) -> Result<()> {
let content = fs::read_to_string(file)
.with_context(|| format!("Failed to read file: {}", file.display()))?;

let value: Value = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse JSON: {}", file.display()))?;

let schema = extract_schema(&value, 0, max_depth);
let schema = filter_json_string(&content, max_depth)?;
println!("{}", schema);
tracking::track(&format!("cat {}", file.display()), "rtk json", &content, &schema);
tracking::track(
&format!("cat {}", file.display()),
"rtk json",
&content,
&schema,
);
Ok(())
}

/// Parse a JSON string and return its schema representation.
/// Useful for piping JSON from other commands (e.g., `gh api`, `curl`).
pub fn filter_json_string(json_str: &str, max_depth: usize) -> Result<String> {
let value: Value = serde_json::from_str(json_str).context("Failed to parse JSON")?;
Ok(extract_schema(&value, 0, max_depth))
Comment on lines +29 to +31
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filter_json_string now returns a generic parse error context ("Failed to parse JSON"). When rtk json <file> fails, the error no longer includes the filename (previously it did), which makes debugging harder. Consider adding file context at the call site (e.g., with_context(|| format!("Failed to parse JSON: {}", file.display()))) or accepting an optional label/path for richer context.

Copilot uses AI. Check for mistakes.
}

fn extract_schema(value: &Value, depth: usize, max_depth: usize) -> String {
let indent = " ".repeat(depth);

Expand Down Expand Up @@ -82,7 +91,10 @@ fn extract_schema(value: &Value, depth: usize, max_depth: usize) -> String {
let val_trimmed = val_schema.trim();

// Inline simple types
let is_simple = matches!(val, Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_));
let is_simple = matches!(
val,
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_)
);

if is_simple {
if i < keys.len() - 1 {
Expand Down
193 changes: 192 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod cargo_cmd;
mod cc_economics;
mod ccusage;
mod config;
Expand All @@ -19,6 +20,7 @@ mod local_llm;
mod log_cmd;
mod ls;
mod next_cmd;
mod npm_cmd;
mod playwright_cmd;
mod pnpm_cmd;
mod prettier_cmd;
Expand All @@ -32,7 +34,7 @@ mod utils;
mod vitest_cmd;
mod wget_cmd;

use anyhow::Result;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::path::PathBuf;

Expand All @@ -54,6 +56,10 @@ struct Cli {
/// Ultra-compact mode: ASCII icons, inline format (Level 2 optimizations)
#[arg(short = 'u', long, global = true)]
ultra_compact: bool,

/// Set SKIP_ENV_VALIDATION=1 for child processes (Next.js, tsc, lint, prisma)
#[arg(long = "skip-env", global = true)]
skip_env: bool,
Comment on lines +60 to +62
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--skip-env is documented as applying to Next.js/tsc/lint/prisma child processes, but the env var is only set in npm_cmd::run. As a result, rtk tsc, rtk next, rtk lint, rtk prisma, and the npx routed variants won’t inherit it. A simple fix is to call std::env::set_var("SKIP_ENV_VALIDATION", "1") once in main() when the flag is set so all spawned Commands inherit it.

Copilot uses AI. Check for mistakes.
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -357,6 +363,26 @@ enum Commands {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},

/// Cargo commands with compact output
Cargo {
#[command(subcommand)]
command: CargoCommands,
},
Comment on lines +367 to +371
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description and “Files changed” list focus on npm/npx/pnpm + --skip-env, but this diff also adds substantial new functionality (e.g. rtk cargo ..., multiple new rtk git ... subcommands, plus gh/json/utils changes). Please update the PR description (or split into separate PRs) so reviewers can validate scope and test plan for these additional features.

Copilot uses AI. Check for mistakes.

/// npm run with filtered output (strip boilerplate)
Npm {
/// npm run arguments (script name + options)
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},

/// npx with intelligent routing (tsc, eslint, prisma -> specialized filters)
Npx {
/// npx arguments (command + options)
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -391,6 +417,32 @@ enum GitCommands {
Push,
/// Pull → "ok ✓ <stats>"
Pull,
/// Compact branch listing (current/local/remote)
Branch {
/// Git branch arguments (supports -d, -D, -m, etc.)
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Fetch → "ok fetched (N new refs)"
Fetch {
/// Git fetch arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Stash management (list, show, pop, apply, drop)
Stash {
/// Subcommand: list, show, pop, apply, drop, push
subcommand: Option<String>,
/// Additional arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Compact worktree listing
Worktree {
/// Git worktree arguments (add, remove, prune, or empty for list)
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -418,6 +470,18 @@ enum PnpmCommands {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Build (delegates to next build filter)
Build {
/// Additional build arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Typecheck (delegates to tsc filter)
Typecheck {
/// Additional typecheck arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -512,6 +576,28 @@ enum PrismaMigrateCommands {
},
}

#[derive(Subcommand)]
enum CargoCommands {
/// Build with compact output (strip Compiling lines, keep errors)
Build {
/// Additional cargo build arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Test with failures-only output
Test {
/// Additional cargo test arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Clippy with warnings grouped by lint rule
Clippy {
/// Additional cargo clippy arguments
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
}

fn main() -> Result<()> {
let cli = Cli::parse();

Expand Down Expand Up @@ -564,6 +650,23 @@ fn main() -> Result<()> {
GitCommands::Pull => {
git::run(git::GitCommand::Pull, &[], None, cli.verbose)?;
}
GitCommands::Branch { args } => {
git::run(git::GitCommand::Branch, &args, None, cli.verbose)?;
}
GitCommands::Fetch { args } => {
git::run(git::GitCommand::Fetch, &args, None, cli.verbose)?;
}
GitCommands::Stash { subcommand, args } => {
git::run(
git::GitCommand::Stash { subcommand },
&args,
None,
cli.verbose,
)?;
}
GitCommands::Worktree { args } => {
git::run(git::GitCommand::Worktree, &args, None, cli.verbose)?;
}
},

Commands::Gh { subcommand, args } => {
Expand All @@ -584,6 +687,12 @@ fn main() -> Result<()> {
cli.verbose,
)?;
}
PnpmCommands::Build { args } => {
next_cmd::run(&args, cli.verbose)?;
}
PnpmCommands::Typecheck { args } => {
tsc_cmd::run(&args, cli.verbose)?;
}
},

Commands::Err { command } => {
Expand Down Expand Up @@ -823,6 +932,88 @@ fn main() -> Result<()> {
Commands::Playwright { args } => {
playwright_cmd::run(&args, cli.verbose)?;
}

Commands::Cargo { command } => match command {
CargoCommands::Build { args } => {
cargo_cmd::run(cargo_cmd::CargoCommand::Build, &args, cli.verbose)?;
}
CargoCommands::Test { args } => {
cargo_cmd::run(cargo_cmd::CargoCommand::Test, &args, cli.verbose)?;
}
CargoCommands::Clippy { args } => {
cargo_cmd::run(cargo_cmd::CargoCommand::Clippy, &args, cli.verbose)?;
}
},

Commands::Npm { args } => {
npm_cmd::run(&args, cli.verbose, cli.skip_env)?;
}

Commands::Npx { args } => {
if args.is_empty() {
anyhow::bail!("npx requires a command argument");
}

// Intelligent routing: delegate to specialized filters
match args[0].as_str() {
"tsc" | "typescript" => {
tsc_cmd::run(&args[1..], cli.verbose)?;
}
"eslint" => {
lint_cmd::run(&args[1..], cli.verbose)?;
}
"prisma" => {
// Route to prisma_cmd based on subcommand
if args.len() > 1 {
let prisma_args: Vec<String> = args[2..].to_vec();
match args[1].as_str() {
"generate" => {
prisma_cmd::run(
prisma_cmd::PrismaCommand::Generate,
&prisma_args,
cli.verbose,
)?;
}
"db" if args.len() > 2 && args[2] == "push" => {
prisma_cmd::run(
prisma_cmd::PrismaCommand::DbPush,
&args[3..],
cli.verbose,
)?;
}
_ => {
// Passthrough other prisma subcommands
let mut cmd = std::process::Command::new("npx");
for arg in &args {
cmd.arg(arg);
}
let status = cmd.status().context("Failed to run npx prisma")?;
std::process::exit(status.code().unwrap_or(1));
}
}
} else {
let status = std::process::Command::new("npx")
.arg("prisma")
.status()
.context("Failed to run npx prisma")?;
std::process::exit(status.code().unwrap_or(1));
}
}
"next" => {
next_cmd::run(&args[1..], cli.verbose)?;
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The npx next routing delegates to next_cmd::run, but next_cmd::run always injects build. With rtk npx next build ..., this becomes next build build .... Routing should detect/strip the explicit build subcommand (or only route when the subcommand is build) and passthrough other next subcommands.

Suggested change
next_cmd::run(&args[1..], cli.verbose)?;
// Route `next` intelligently:
// - If no subcommand: let `next_cmd` decide (typically `build`)
// - If subcommand is `build`: strip it so `next_cmd` does not inject `build` twice
// - Otherwise: passthrough to npm_cmd for other `next` subcommands (e.g. `dev`, `lint`)
if args.len() == 1 {
// `rtk npx next` -> next_cmd default behavior (usually `next build`)
next_cmd::run(&[], cli.verbose)?;
} else if args[1] == "build" {
// `rtk npx next build ...` -> avoid `next build build ...`
next_cmd::run(&args[2..], cli.verbose)?;
} else {
// Passthrough other `next` subcommands unchanged
npm_cmd::run(&args, cli.verbose, cli.skip_env)?;
}

Copilot uses AI. Check for mistakes.
}
"prettier" => {
prettier_cmd::run(&args[1..], cli.verbose)?;
}
"playwright" => {
playwright_cmd::run(&args[1..], cli.verbose)?;
}
_ => {
// Generic passthrough with npm boilerplate filter
npm_cmd::run(&args, cli.verbose, cli.skip_env)?;
Comment on lines +1012 to +1013
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rtk npx fallback currently calls npm_cmd::run, which executes npm run ... rather than npx .... This will run the wrong tool for any non-script npx usage. Consider introducing a real npx runner (Command::new("npx") with the provided args) and apply the npm-style filtering to its output, or rename this command to reflect that it’s actually npm run.

Suggested change
// Generic passthrough with npm boilerplate filter
npm_cmd::run(&args, cli.verbose, cli.skip_env)?;
// Generic passthrough: invoke npx directly
let mut cmd = std::process::Command::new("npx");
for arg in &args {
cmd.arg(arg);
}
let status = cmd.status().context("Failed to run npx command")?;
std::process::exit(status.code().unwrap_or(1));

Copilot uses AI. Check for mistakes.
}
}
}
}

Ok(())
Expand Down
Loading
Loading