Skip to content

feat(cli): non-interactive foundation + ssh-keys add#476

Merged
itzlambda merged 20 commits into
mainfrom
feat/non-interactive-cli-foundation
May 18, 2026
Merged

feat(cli): non-interactive foundation + ssh-keys add#476
itzlambda merged 20 commits into
mainfrom
feat/non-interactive-cli-foundation

Conversation

@itzlambda
Copy link
Copy Markdown
Contributor

@itzlambda itzlambda commented May 15, 2026

Summary

Lands the foundation for non-interactive CLI execution — interactivity gate, error variants, JSON error renderer, spinner suppression — and migrates ssh-keys add end-to-end so the plumbing has a real consumer.

Non-interactive mode is detected via !stdin.is_terminal() || $BASILICA_NON_INTERACTIVE. In that mode, every routed prompt returns a structured MissingInput { field, hint, choices } (would have prompted) or MissingPrerequisite { field, hint } (would have set something up) instead of hanging on stdin. Errors render to stderr as a single JSON object per error when --json is set (schema_version: 1).

Changes

  • error.rsCliError::MissingInput and CliError::MissingPrerequisite variants.
  • interactive/gate.rs (new) — Interactivity::{Interactive, NonInteractive}, current(), ask_text/ask_select/ask_confirm helpers, SelectItem builder, Choices/Choice types.
  • output/error_render.rs (new) — render_error(err, mode, w) with RenderMode::{Human, Json}. Wired into main.rs so the top-level error path honors the global --json flag.
  • progress/mod.rscreate_spinner and ProgressManager::start_spinner return ProgressBar::hidden() in non-interactive mode (decorative spinners corrupt stderr capture for agents).
  • cli/handlers/ssh_keys.rs — every .interact*() site routed through the gate; added --force flag to skip the replace-existing confirmation; delete confirm also gated so -y is the only way through non-interactively; refuses to silently generate a new SSH key in non-interactive mode.
  • cli/commands.rsSshKeyAction::Add gains --force.

Tests

  • 4 unit tests on the gate (serial_test-gated)
  • 3 unit tests on the renderer
  • Integration tests asserting BASILICA_NON_INTERACTIVE=1 basilica --json ssh-keys add emits the right JSON shape
  • Hang-smoke harness asserting ssh-keys add, ssh-keys list, balance exit in ≤5s with stdin closed (will grow with follow-up PRs)

Dev-dep additions: serial_test = "3", assert_cmd = "2".

Out of scope (deferred to follow-up PRs)

basilica up non-interactive flags (--offering-id, --ssh-key-id, --max-hourly-rate guardrail, JSON response), rental selector sweep (down/ssh/exec/cp/logs), volumes/tokens/deploy/fund sweeps, docs updates.

Summary by CodeRabbit

  • New Features

    • Interactivity gate that disables prompts when stdin is not a TTY or BASILICA_NON_INTERACTIVE is set
    • Prompt helpers (text/select/confirm) that return structured errors in non-interactive mode
    • Error rendering now supports Human and structured JSON output
  • Improvements

    • Safer SSH key add/delete flow with duplicate detection and clearer non-interactive guidance
    • Auth retry skips interactive login in non-interactive mode and returns structured prerequisite hints
  • Tests

    • Unit and integration tests covering non-interactive behavior and JSON stderr rendering

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 93c63a13-b38a-47cb-b5ad-07f2b1fb08f6

📥 Commits

Reviewing files that changed from the base of the PR and between dacbb0c and 950704d.

📒 Files selected for processing (3)
  • crates/basilica-cli/CHANGELOG.md
  • crates/basilica-cli/src/cli/handlers/ssh_keys.rs
  • crates/basilica-cli/src/interactive/gate.rs
✅ Files skipped from review due to trivial changes (1)
  • crates/basilica-cli/CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/basilica-cli/src/interactive/gate.rs

Walkthrough

Adds an interactivity gate and prompt helpers that return structured non-interactive errors; introduces two CliError variants; adds JSON/Human error rendering and re-exports; refactors SSH add/delete to use gate prompts and explicit non-interactive errors; tweaks progress/main error plumbing; adds key-compare helper and non-interactive tests.

Changes

Non-interactive Flow Gating and Error Handling

Layer / File(s) Summary
Error variants for non-interactive flows
crates/basilica-cli/src/error.rs
CliError gains MissingInput and MissingPrerequisite variants, each with field and hint strings.
Interactivity detection and prompt routing
crates/basilica-cli/src/interactive/gate.rs, crates/basilica-cli/src/interactive/mod.rs
New Interactivity detection (TTY + BASILICA_NON_INTERACTIVE) and current(); ask_text, ask_select, ask_confirm return MissingInput in non-interactive mode or invoke dialoguer in interactive mode; includes serial unit tests and pub mod gate.
Error rendering with JSON and Human modes
crates/basilica-cli/src/output/error_render.rs, crates/basilica-cli/src/output/mod.rs
RenderMode and render_error dispatcher serialize MissingInput/MissingPrerequisite distinctly for JSON and format human output appropriately; unit tests added and re-exported from output module.
SSH key handler gate-based prompt refactoring
crates/basilica-cli/src/cli/handlers/ssh_keys.rs
Handlers now use gate::ask_* APIs. No local keys -> non-interactive returns MissingInput, interactive generates a key. Selection via ask_select, name via ask_text. Existing-key replacement short-circuits on identical public keys, refuses replacement in non-interactive mode with MissingPrerequisite, otherwise asks ask_confirm. Deletion confirmation uses ask_confirm.
Progress spinners and main error rendering
crates/basilica-cli/src/progress/mod.rs, crates/basilica-cli/src/main.rs
Spinner steady-tick enabled before message set; main captures json flag before running args and uses render_error with RenderMode::Json or Human for error output.
SSH key comparison utility
crates/basilica-cli/src/ssh/key_matcher.rs, crates/basilica-cli/src/ssh/mod.rs
Adds same_public_key() that parses and compares SSH public key type+data; re-exported from ssh module.
Non-interactive integration tests & dev deps
crates/basilica-cli/tests/non_interactive.rs, crates/basilica-cli/Cargo.toml
Adds parse_stderr_json and a non-interactive integration test verifying structured JSON stderr response; adds dev-deps serial_test and assert_cmd.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • epappas

Poem

🐰 I checked the gates before I knocked,

If TTY's gone, I won't get stuck.
Errors tidy, JSON neat,
Spinners hush their steady beat,
Keys compare and tests stay pluck.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(cli): non-interactive foundation + ssh-keys add' clearly and concisely summarizes the main changes—introducing non-interactive mode support and updating ssh-keys add to use it.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/non-interactive-cli-foundation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@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: 1

🧹 Nitpick comments (1)
crates/basilica-cli/src/interactive/gate.rs (1)

118-134: 💤 Low value

Document why default is ignored in non-interactive mode.

ask_confirm accepts a default parameter but doesn't use it when current() returns NonInteractive—it always returns MissingInput instead. This differs from ask_text (lines 63-64), which returns the default value in non-interactive mode when one is provided. The asymmetry is likely intentional (confirmations often guard destructive operations), but the behavior should be documented in a doc comment to avoid confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/basilica-cli/src/interactive/gate.rs` around lines 118 - 134, Document
that ask_confirm intentionally ignores the default in NonInteractive mode: add a
doc comment on the ask_confirm function explaining that when current() returns
Interactivity::NonInteractive the function returns Err(CliError::MissingInput)
rather than using the default parameter (unlike ask_text), and state the
rationale (safety for destructive/confirmation prompts) so callers understand
the asymmetry with ask_text and why default is not applied in non-interactive
runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/basilica-cli/src/output/error_render.rs`:
- Around line 50-64: The JSON output currently inserts "id" and "label" into the
object then merges c.meta which can overwrite them; in the choices_to_json
function change the insertion order so you first insert all entries from c.meta
into the serde_json::Map and then insert "id" and "label" (using c.id and
c.label) last, ensuring the authoritative Choice fields (c.id, c.label) cannot
be overwritten by keys in c.meta.

---

Nitpick comments:
In `@crates/basilica-cli/src/interactive/gate.rs`:
- Around line 118-134: Document that ask_confirm intentionally ignores the
default in NonInteractive mode: add a doc comment on the ask_confirm function
explaining that when current() returns Interactivity::NonInteractive the
function returns Err(CliError::MissingInput) rather than using the default
parameter (unlike ask_text), and state the rationale (safety for
destructive/confirmation prompts) so callers understand the asymmetry with
ask_text and why default is not applied in non-interactive runs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1202dc38-4f2e-4e19-87b5-e74ed0150e0f

📥 Commits

Reviewing files that changed from the base of the PR and between caac5ee and e51e945.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • crates/basilica-cli/Cargo.toml
  • crates/basilica-cli/src/cli/args.rs
  • crates/basilica-cli/src/cli/commands.rs
  • crates/basilica-cli/src/cli/handlers/ssh_keys.rs
  • crates/basilica-cli/src/error.rs
  • crates/basilica-cli/src/interactive/gate.rs
  • crates/basilica-cli/src/interactive/mod.rs
  • crates/basilica-cli/src/main.rs
  • crates/basilica-cli/src/output/error_render.rs
  • crates/basilica-cli/src/output/mod.rs
  • crates/basilica-cli/src/progress/mod.rs
  • crates/basilica-cli/tests/no_hang.rs
  • crates/basilica-cli/tests/non_interactive.rs

Comment thread crates/basilica-cli/src/output/error_render.rs Outdated
itzlambda added 2 commits May 15, 2026 15:09
Drops --force from `ssh-keys add`. Only one SSH key is allowed per user,
so replacement was collapsing two destructive cloud-mutating actions
(delete + register) into one silent flag, leaving no forensic trail in
CI output. The caller now has to run `basilica ssh-keys delete -y`
explicitly before re-registering.

- Same public key already registered: idempotent no-op success
  (compared via parsed key data, ignoring trailing comment/whitespace).
- Different key registered, non-interactive: emit MissingPrerequisite
  with the cleanup command in the hint, no auto-delete.
- Different key registered, interactive: keep the original confirm +
  delete + register flow.

Adds `ssh::same_public_key` (reusing the existing key-matcher parser)
so equality holds across `user@host` comment differences.
- Use basilica_common::rental::generate_random_rental_name() as the
  default ssh key label instead of the literal "default" string, so
  non-interactive `ssh-keys add` without --name matches the rental
  flow's naming pattern.
- Drop `schema_version` from the JSON error payload. Versioning with
  no documented acceptance contract and no second consumer was
  premature; can be reintroduced when a real breaking shape change
  arrives.
- Remove timing-based `no_hang.rs` test. A 5s wall-clock threshold
  conflates slow network with stdin hangs; `non_interactive.rs`
  already covers gate behavior with positive JSON assertions.
@itzlambda itzlambda marked this pull request as draft May 18, 2026 07:08
itzlambda added 2 commits May 18, 2026 15:34
The Choices/Choice/SelectItem types let caller-supplied `meta` keys
collide with the renderer's authoritative `id` and `label` fields,
silently shadowing the agent's stable handle. Rather than defend
reserved keys, drop the surface: `MissingInput { field, hint }` is
enough for agents to retry. `ask_select` now takes `&[&str]` of
labels.
@itzlambda itzlambda marked this pull request as ready for review May 18, 2026 10:07
Copy link
Copy Markdown
Contributor

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/basilica-cli/src/cli/handlers/ssh_keys.rs (1)

324-336: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Apply the same max-length validation to prompted key names.

--name input enforces <= 100 chars, but the interactive ask_text path does not. This creates inconsistent behavior and pushes validation failures downstream to the API.

Suggested patch
         None => {
             let default_name = basilica_common::rental::generate_random_rental_name();
             let input = ask_text(
                 "name",
                 Some(&default_name),
                 "Pass --name <label> to label the registered key",
             )?;
             if input.trim().is_empty() {
                 return Err(CliError::Internal(color_eyre::eyre::eyre!(
                     "SSH key name cannot be empty"
                 )));
             }
+            if input.len() > 100 {
+                return Err(CliError::Internal(color_eyre::eyre::eyre!(
+                    "SSH key name must be 100 characters or less"
+                )));
+            }
             input
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/basilica-cli/src/cli/handlers/ssh_keys.rs` around lines 324 - 336, The
interactive prompt path that uses ask_text to read the SSH key "name" should
enforce the same max-length check as the --name flag; after obtaining and
trimming the input produced by ask_text (the variable named input) validate
input.len() <= 100 and return a CliError::Internal (using
color_eyre::eyre::eyre! like the existing branch) when it exceeds 100 chars,
preserving the current "SSH key name cannot be empty" check and error flow;
update the block around generate_random_rental_name / ask_text / input to
perform the non-empty and max-length validation before returning input so
behavior matches the flag path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/basilica-cli/src/cli/handlers/ssh_keys.rs`:
- Around line 455-459: The confirmation prompt passed to ask_confirm currently
reads like usage guidance instead of a direct yes/no question; update the
message argument in the ask_confirm call (where confirmed is set) to a concise
confirmation question such as "Are you sure you want to delete the registered
SSH key?" (you can append "(y/N)" or mention "-y/--yes to skip" if desired) so
interactive users see a clear yes/no prompt; preserve the key name
"confirm_delete_ssh_key" and the call to ask_confirm(...) and keep handling the
returned confirmed value as before.

---

Outside diff comments:
In `@crates/basilica-cli/src/cli/handlers/ssh_keys.rs`:
- Around line 324-336: The interactive prompt path that uses ask_text to read
the SSH key "name" should enforce the same max-length check as the --name flag;
after obtaining and trimming the input produced by ask_text (the variable named
input) validate input.len() <= 100 and return a CliError::Internal (using
color_eyre::eyre::eyre! like the existing branch) when it exceeds 100 chars,
preserving the current "SSH key name cannot be empty" check and error flow;
update the block around generate_random_rental_name / ask_text / input to
perform the non-empty and max-length validation before returning input so
behavior matches the flag path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c47131bc-8b25-4b83-81cd-fb1eeb74ffc2

📥 Commits

Reviewing files that changed from the base of the PR and between e51e945 and f4b154c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • crates/basilica-cli/src/cli/handlers/ssh_keys.rs
  • crates/basilica-cli/src/error.rs
  • crates/basilica-cli/src/interactive/gate.rs
  • crates/basilica-cli/src/output/error_render.rs
  • crates/basilica-cli/src/ssh/key_matcher.rs
  • crates/basilica-cli/src/ssh/mod.rs
  • crates/basilica-cli/tests/non_interactive.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/basilica-cli/tests/non_interactive.rs

Comment thread crates/basilica-cli/src/cli/handlers/ssh_keys.rs
itzlambda added 4 commits May 18, 2026 16:29
… guards

`indicatif`'s default draw target already hides spinners and progress bars
on non-TTY stderr, so the manual `match current()` guards in
`start_spinner` and `create_spinner` were duplicating built-in behavior.
Remove them so spinner construction matches the existing progress-bar
paths.

Also collapse `gate.rs`'s two-tier `OVERRIDE` + `DETECTED` `OnceLock`s
into a single `CURRENT`. The override tier was only consumed by
`set_for_test`, which a single `OnceLock::set` handles equivalently.

No behavior change. Gate tests still pass.
The TTY + env check is cheap enough to run on every prompt, so the static cache
just added complexity. Tests now toggle the env var via an RAII guard instead
of seeding the OnceLock.
Separate the human-readable question shown via dialoguer from the stable
machine field identifier used in non-interactive MissingInput errors.
`execute_with_auth_retry` caught `CliError::Auth` and unconditionally
started the OAuth callback server, which blocks 300s waiting for the
user. In CI / scripted use with `BASILICA_NON_INTERACTIVE=1` this hung
every auth-requiring command for the full timeout before failing.

In non-interactive mode, return `MissingPrerequisite { field: "authentication" }`
immediately instead. Drop the brittle ssh-keys integration test that
assumed the runner was logged in.
Copy link
Copy Markdown
Contributor

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/basilica-cli/src/cli/handlers/ssh_keys.rs (2)

232-236: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

--force is documented in hints but not actually applied in the replacement path.

Line 372 tells users to pass --force, but this handler has no force input and always prompts (interactive) or errors (non-interactive) when a key already exists.

Suggested patch
 pub async fn handle_add_ssh_key(
     client: &BasilicaClient,
     name: Option<String>,
     file: Option<PathBuf>,
+    force: bool,
 ) -> Result<(), CliError> {
@@
-        if matches!(gate::current(), Interactivity::NonInteractive) {
+        if matches!(gate::current(), Interactivity::NonInteractive) && !force {
             return Err(CliError::MissingPrerequisite {
                 field: "ssh_key_already_registered".into(),
                 hint: format!(
@@
-        let confirmed = ask_confirm(
-            "replace_existing",
-            "Do you want to replace it with the new key?",
-            false,
-            "Pass --force to replace the existing SSH key without prompting.",
-        )?;
-        if !confirmed {
-            println!("Operation cancelled.");
-            return Ok(());
+        if !force {
+            let confirmed = ask_confirm(
+                "replace_existing",
+                "Do you want to replace it with the new key?",
+                false,
+                "Pass --force to replace the existing SSH key without prompting.",
+            )?;
+            if !confirmed {
+                println!("Operation cancelled.");
+                return Ok(());
+            }
         }

Also applies to: 351-373

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/basilica-cli/src/cli/handlers/ssh_keys.rs` around lines 232 - 236, The
handler handle_add_ssh_key currently lacks a force flag so the documented
--force hint is not honored; update the function signature to accept a force:
bool (and propagate from callers), then in the duplicate-key branch (the code
that currently prompts or returns an error when a key already exists) use that
force boolean to skip prompting and perform replacement immediately; also ensure
non-interactive flows check force to avoid returning an error and that any
prompt code path is only used when force is false.

325-338: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Apply the same max-length check to prompted key names.

The flag path enforces <= 100 chars, but the prompted path only checks empty input.

Suggested patch
             let input = ask_text(
                 "name",
                 "Enter a name for this SSH key",
                 Some(&default_name),
                 "Pass --name <label> to label the registered key",
             )?;
             if input.trim().is_empty() {
                 return Err(CliError::Internal(color_eyre::eyre::eyre!(
                     "SSH key name cannot be empty"
                 )));
             }
+            if input.len() > 100 {
+                return Err(CliError::Internal(color_eyre::eyre::eyre!(
+                    "SSH key name must be 100 characters or less"
+                )));
+            }
             input
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/basilica-cli/src/cli/handlers/ssh_keys.rs` around lines 325 - 338, The
prompted SSH key name path (inside the block using
basilica_common::rental::generate_random_rental_name() and ask_text) only checks
for empty input but must enforce the same max-length <= 100 as the flag path;
after obtaining and trimming input (the variable input), add a length check and
return the same CliError::Internal (color_eyre::eyre::eyre!("SSH key name cannot
be empty" style) but with a clear message for length, e.g. "SSH key name must be
at most 100 characters") when input.len() > 100 so both code paths share the
same validation for the SSH key name.
🧹 Nitpick comments (1)
crates/basilica-cli/tests/non_interactive.rs (1)

39-43: ⚡ Quick win

Strengthen this assertion to validate the JSON error contract, not just “some string error.”

Right now this passes for many unrelated failures. Assert schema_version and presence/shape of error so regressions in JSON rendering are caught.

Suggested patch
-    assert!(v["error"].is_string());
+    assert_eq!(v["schema_version"], serde_json::Value::from(1));
+    assert!(
+        v.get("error").is_some(),
+        "missing `error` field in stderr json: {v}"
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/basilica-cli/tests/non_interactive.rs` around lines 39 - 43, The
current test only asserts v["error"].is_string(), which is too weak; update the
assertions in crates/basilica-cli/tests/non_interactive.rs to validate the JSON
error contract more strictly: assert that v contains a "schema_version" (e.g.
assert!(v.get("schema_version").and_then(|s| s.as_str()).is_some() or equals the
expected version), and assert that v["error"] is either a string or an object
with a "message" field (e.g. check v["error"].is_string() ||
(v["error"].is_object() && v["error"].get("message").and_then(|m|
m.as_str()).is_some())). Keep the existing assert that error is present but
strengthen it by verifying "schema_version" and the shape of "error" using the
variable v.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/basilica-cli/src/interactive/gate.rs`:
- Around line 104-116: The RAII guard currently always removes
BASILICA_NON_INTERACTIVE in NonInteractiveEnv::drop which clobbers any
pre-existing value; change NonInteractiveEnv to store the previous value (use
std::env::var_os to capture Option<OsString>) in the struct, have
NonInteractiveEnv::set() save that previous value before calling
std::env::set_var, and update impl Drop for NonInteractiveEnv to restore the
previous value: if the saved Option is Some(v) call std::env::set_var with that
value, otherwise call std::env::remove_var, ensuring the original environment is
preserved.

---

Outside diff comments:
In `@crates/basilica-cli/src/cli/handlers/ssh_keys.rs`:
- Around line 232-236: The handler handle_add_ssh_key currently lacks a force
flag so the documented --force hint is not honored; update the function
signature to accept a force: bool (and propagate from callers), then in the
duplicate-key branch (the code that currently prompts or returns an error when a
key already exists) use that force boolean to skip prompting and perform
replacement immediately; also ensure non-interactive flows check force to avoid
returning an error and that any prompt code path is only used when force is
false.
- Around line 325-338: The prompted SSH key name path (inside the block using
basilica_common::rental::generate_random_rental_name() and ask_text) only checks
for empty input but must enforce the same max-length <= 100 as the flag path;
after obtaining and trimming input (the variable input), add a length check and
return the same CliError::Internal (color_eyre::eyre::eyre!("SSH key name cannot
be empty" style) but with a clear message for length, e.g. "SSH key name must be
at most 100 characters") when input.len() > 100 so both code paths share the
same validation for the SSH key name.

---

Nitpick comments:
In `@crates/basilica-cli/tests/non_interactive.rs`:
- Around line 39-43: The current test only asserts v["error"].is_string(), which
is too weak; update the assertions in
crates/basilica-cli/tests/non_interactive.rs to validate the JSON error contract
more strictly: assert that v contains a "schema_version" (e.g.
assert!(v.get("schema_version").and_then(|s| s.as_str()).is_some() or equals the
expected version), and assert that v["error"] is either a string or an object
with a "message" field (e.g. check v["error"].is_string() ||
(v["error"].is_object() && v["error"].get("message").and_then(|m|
m.as_str()).is_some())). Keep the existing assert that error is present but
strengthen it by verifying "schema_version" and the shape of "error" using the
variable v.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 68104801-da34-4dad-aad4-3b5583492aba

📥 Commits

Reviewing files that changed from the base of the PR and between 38c2381 and dacbb0c.

📒 Files selected for processing (5)
  • crates/basilica-cli/src/cli/args.rs
  • crates/basilica-cli/src/cli/handlers/ssh_keys.rs
  • crates/basilica-cli/src/interactive/gate.rs
  • crates/basilica-cli/src/output/error_render.rs
  • crates/basilica-cli/tests/non_interactive.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/basilica-cli/src/output/error_render.rs

Comment thread crates/basilica-cli/src/interactive/gate.rs Outdated
itzlambda added 3 commits May 18, 2026 17:59
Capture the previous env var on set and restore it on drop so nested or
neighboring tests that rely on the variable aren't clobbered.
…sh-keys add hint

- `gate::current()` no longer flips to NonInteractive when
  `BASILICA_NON_INTERACTIVE=""`; only non-empty values force the mode.
- The replace-existing prompt in `ssh-keys add` referenced a `--force` flag
  that does not exist on the subcommand. Replace it with the same
  `basilica ssh-keys delete -y` guidance the non-interactive prerequisite
  branch already uses.
@itzlambda itzlambda merged commit 778d1ab into main May 18, 2026
14 checks passed
@itzlambda itzlambda deleted the feat/non-interactive-cli-foundation branch May 18, 2026 19:30
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.

1 participant