Skip to content

feat: return to first class clap handling of sub commands#213

Merged
socksy merged 1 commit intodevelopfrom
feature/improve-run-params-errors
Feb 27, 2026
Merged

feat: return to first class clap handling of sub commands#213
socksy merged 1 commit intodevelopfrom
feature/improve-run-params-errors

Conversation

@socksy
Copy link
Contributor

@socksy socksy commented Feb 26, 2026

Building on the work from #206, improve the handling of sub commands. Specifically allowing --help after tower run foo-app, giving better error messages when doing things like tower run --param foo=bar (where --param should either be -p or --parameter), and make sub command handling and documentation consistent.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved error messages with helpful guidance when attempting to run or manage non-existent applications.
  • Documentation

    • Enhanced help text clarifying that run parameters are provided as JSON objects rather than CLI flags.
    • Refined argument descriptions across schedule and team management commands for better clarity.

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/improve-run-params-errors

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
crates/tower-cmd/src/schedules.rs (1)

73-80: Consider aligning delete arg naming with update (id_or_name).

update accepts id_or_name, while delete still uses schedule_id. Unifying names would make the CLI surface more consistent.

♻️ Suggested consistency refactor
- Arg::new("schedule_id")
+ Arg::new("id_or_name")
     .value_parser(value_parser!(String))
     .index(1)
     .required(true)
-    .help("The schedule ID to delete"),
+    .help("ID or name of the schedule to delete"),

- let schedule_id = args.get_one::<String>("schedule_id").expect("schedule_id is required");
+ let id_or_name = args.get_one::<String>("id_or_name").expect("id_or_name is required");

- api::delete_schedule(&config, schedule_id),
+ api::delete_schedule(&config, id_or_name),

- output::success(&format!("Schedule {} deleted", schedule_id));
+ output::success(&format!("Schedule {} deleted", id_or_name));

Also applies to: 191-199

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/tower-cmd/src/schedules.rs` around lines 73 - 80, Rename the delete
command's argument from "schedule_id" to match update's "id_or_name" to keep CLI
consistent: update the Arg::new("schedule_id") in the Command::new("delete")
block to Arg::new("id_or_name"), adjust its value_parser/index/required/help
text accordingly (e.g., "The schedule ID or name to delete"), and make the same
change for the other delete occurrence referenced around the 191-199 region so
both delete argument definitions mirror the update argument name "id_or_name".
crates/tower-cmd/src/run.rs (1)

83-99: Simplify ApiRunError handling by binding source once.

Line 96 re-matches the same variant already matched at Line 83, and Line 99 adds unnecessary control-flow noise. You can keep behavior identical with a single binding.

♻️ Proposed simplification
-            Error::ApiRunError { ref source } => {
+            Error::ApiRunError { source } => {
                 let is_not_found = matches!(
-                    source,
+                    &source,
                     tower_api::apis::Error::ResponseError(resp) if resp.status == reqwest::StatusCode::NOT_FOUND
                 );
                 if is_not_found {
                     output::error("App not found. It may not exist or hasn't been deployed yet.");
                     output::write("\nTo fix this:\n");
                     output::write("  1. Check your app exists:  tower apps list\n");
                     output::write("  2. Deploy your app:        tower deploy\n");
                     output::write("  3. Then run it:            tower run\n");
                     std::process::exit(1);
                 }
-                if let Error::ApiRunError { source } = e {
-                    output::tower_error_and_die(source, "Scheduling run failed");
-                }
-                unreachable!();
+                output::tower_error_and_die(source, "Scheduling run failed");
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/tower-cmd/src/run.rs` around lines 83 - 99, The Error::ApiRunError arm
should bind source once and reuse it instead of re-matching; change the pattern
to Error::ApiRunError { source } => { let is_not_found = matches!(&source,
tower_api::apis::Error::ResponseError(resp) if resp.status ==
reqwest::StatusCode::NOT_FOUND); if is_not_found { ... std::process::exit(1); }
output::tower_error_and_die(source, "Scheduling run failed"); } so you compute
the NOT_FOUND check by matching on &source and then call
output::tower_error_and_die with the moved source, removing the redundant if let
and unreachable!.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/tower-cmd/src/apps.rs`:
- Around line 83-92: In the let (name, seq) = ... block, guard against providing
both app#run and the positional run_number by detecting when
app_name_raw.split_once('#') yields Some(...) and
cmd.get_one::<i64>("run_number").is_some(); in that case call output::die with a
clear error like "Ambiguous run number: specify either app#run or the positional
run_number, not both" so the code fails fast; otherwise preserve the existing
behavior that uses the embedded # value or falls back to
cmd.get_one::<i64>("run_number")/latest_run_number(&config, app_name_raw).await
as currently implemented.

In `@crates/tower-cmd/src/secrets.rs`:
- Around line 191-196: The current parsing of secret_name_arg using
split_once('/') accepts empty parts (e.g., "/secret" or "env/") and forwards
invalid environment/name values; update the logic around
secret_name_arg.split_once('/') to validate that both env and name are non-empty
strings before using them (reference secret_name_arg, environment, name, and
args.get_one), and if either part is empty return or surface a clear error (or
fall back to the existing args.get_one path) so you don't call the API with
invalid values.

In `@tests/integration/features/steps/cli_steps.py`:
- Around line 146-155: Formatting check is failing for the test function
step_output_should_show_app_not_found_with_guidance; run the Black formatter on
the file that contains this function (or run black .) to apply consistent
formatting, fix any spacing/trailing-whitespace issues around the function and
its docstring, then add and commit the reformatted file so CI's Black check
passes.

---

Nitpick comments:
In `@crates/tower-cmd/src/run.rs`:
- Around line 83-99: The Error::ApiRunError arm should bind source once and
reuse it instead of re-matching; change the pattern to Error::ApiRunError {
source } => { let is_not_found = matches!(&source,
tower_api::apis::Error::ResponseError(resp) if resp.status ==
reqwest::StatusCode::NOT_FOUND); if is_not_found { ... std::process::exit(1); }
output::tower_error_and_die(source, "Scheduling run failed"); } so you compute
the NOT_FOUND check by matching on &source and then call
output::tower_error_and_die with the moved source, removing the redundant if let
and unreachable!.

In `@crates/tower-cmd/src/schedules.rs`:
- Around line 73-80: Rename the delete command's argument from "schedule_id" to
match update's "id_or_name" to keep CLI consistent: update the
Arg::new("schedule_id") in the Command::new("delete") block to
Arg::new("id_or_name"), adjust its value_parser/index/required/help text
accordingly (e.g., "The schedule ID or name to delete"), and make the same
change for the other delete occurrence referenced around the 191-199 region so
both delete argument definitions mirror the update argument name "id_or_name".

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 198783e and 2194e34.

📒 Files selected for processing (9)
  • crates/tower-cmd/src/apps.rs
  • crates/tower-cmd/src/lib.rs
  • crates/tower-cmd/src/mcp.rs
  • crates/tower-cmd/src/run.rs
  • crates/tower-cmd/src/schedules.rs
  • crates/tower-cmd/src/secrets.rs
  • crates/tower-cmd/src/teams.rs
  • tests/integration/features/cli_runs.feature
  • tests/integration/features/steps/cli_steps.py

Comment on lines +83 to +92
let (name, seq) = if let Some((name, num_str)) = app_name_raw.split_once('#') {
let num = num_str.parse::<i64>().unwrap_or_else(|_| output::die("Run number must be a number"));
(name.to_string(), num)
} else {
let num = match cmd.get_one::<i64>("run_number").copied() {
Some(n) => n,
None => latest_run_number(&config, app_name_raw).await,
};
(app_name_raw.clone(), num)
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reject ambiguous run-number input forms.

If users pass both app#run and positional run_number, the positional value is silently ignored. It’s better to fail fast with a clear message.

💡 Suggested guard
 let (name, seq) = if let Some((name, num_str)) = app_name_raw.split_once('#') {
+    if cmd.get_one::<i64>("run_number").is_some() {
+        output::die(
+            "Provide run number either as 'app_name#run_number' or as a separate positional run_number, not both.",
+        );
+    }
     let num = num_str.parse::<i64>().unwrap_or_else(|_| output::die("Run number must be a number"));
     (name.to_string(), num)
 } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/tower-cmd/src/apps.rs` around lines 83 - 92, In the let (name, seq) =
... block, guard against providing both app#run and the positional run_number by
detecting when app_name_raw.split_once('#') yields Some(...) and
cmd.get_one::<i64>("run_number").is_some(); in that case call output::die with a
clear error like "Ambiguous run number: specify either app#run or the positional
run_number, not both" so the code fails fast; otherwise preserve the existing
behavior that uses the embedded # value or falls back to
cmd.get_one::<i64>("run_number")/latest_run_number(&config, app_name_raw).await
as currently implemented.

Comment on lines +191 to +196
let (environment, name) = if let Some((env, name)) = secret_name_arg.split_once('/') {
(env.to_string(), name.to_string())
} else {
let env = args.get_one::<String>("environment").expect("environment has default");
(env.clone(), secret_name_arg.clone())
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate environment/name shorthand before calling the API.

split_once('/') currently allows empty parts (e.g., /secret or env/). That sends invalid values downstream and returns less helpful errors.

💡 Suggested fix
 let secret_name_arg = args.get_one::<String>("secret_name").expect("secret_name is required");
 let (environment, name) = if let Some((env, name)) = secret_name_arg.split_once('/') {
+    if env.is_empty() || name.is_empty() {
+        output::die("Invalid secret identifier. Use 'secret_name' or 'environment/secret_name'.");
+    }
     (env.to_string(), name.to_string())
 } else {
     let env = args.get_one::<String>("environment").expect("environment has default");
     (env.clone(), secret_name_arg.clone())
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let (environment, name) = if let Some((env, name)) = secret_name_arg.split_once('/') {
(env.to_string(), name.to_string())
} else {
let env = args.get_one::<String>("environment").expect("environment has default");
(env.clone(), secret_name_arg.clone())
};
let (environment, name) = if let Some((env, name)) = secret_name_arg.split_once('/') {
if env.is_empty() || name.is_empty() {
output::die("Invalid secret identifier. Use 'secret_name' or 'environment/secret_name'.");
}
(env.to_string(), name.to_string())
} else {
let env = args.get_one::<String>("environment").expect("environment has default");
(env.clone(), secret_name_arg.clone())
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/tower-cmd/src/secrets.rs` around lines 191 - 196, The current parsing
of secret_name_arg using split_once('/') accepts empty parts (e.g., "/secret" or
"env/") and forwards invalid environment/name values; update the logic around
secret_name_arg.split_once('/') to validate that both env and name are non-empty
strings before using them (reference secret_name_arg, environment, name, and
args.get_one), and if either part is empty return or surface a clear error (or
fall back to the existing args.get_one path) so you don't call the API with
invalid values.

Comment on lines +146 to +155
@step('the output should show "App not found" with guidance')
def step_output_should_show_app_not_found_with_guidance(context):
"""Verify 404 error shows helpful guidance instead of a bare error"""
output = context.cli_output
assert (
red_color_code in output
), f"Expected red color codes in output, got: {output}"
assert "App not found" in output, f"Expected 'App not found' in output, got: {output}"
assert "tower deploy" in output, f"Expected 'tower deploy' guidance in output, got: {output}"

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Black formatting check is failing and blocks CI.

The pipeline reports one Python file needs reformatting. Please run Black on this file/update before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integration/features/steps/cli_steps.py` around lines 146 - 155,
Formatting check is failing for the test function
step_output_should_show_app_not_found_with_guidance; run the Black formatter on
the file that contains this function (or run black .) to apply consistent
formatting, fix any spacing/trailing-whitespace issues around the function and
its docstring, then add and commit the reformatted file so CI's Black check
passes.

Specifically to improve error messages for run params, and to give
guidance for what to do when something goes wrong. Additionally,
moving back to clap means we get stuff like  still working
for free (this was confusing claude, and also users).
@socksy socksy force-pushed the feature/improve-run-params-errors branch from 2194e34 to c0f642b Compare February 26, 2026 19:32
Copy link
Contributor

@bradhe bradhe left a comment

Choose a reason for hiding this comment

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

LGTM

@socksy socksy merged commit 3c58d47 into develop Feb 27, 2026
30 checks passed
@socksy socksy deleted the feature/improve-run-params-errors branch February 27, 2026 11:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants