Skip to content

feat(infra): T-226 shell detector for $SHELL / pwsh / cmd.exe (closes #121)#133

Open
mpiton wants to merge 1 commit into
mainfrom
feat/T-226-shell-detector
Open

feat(infra): T-226 shell detector for $SHELL / pwsh / cmd.exe (closes #121)#133
mpiton wants to merge 1 commit into
mainfrom
feat/T-226-shell-detector

Conversation

@mpiton
Copy link
Copy Markdown
Owner

@mpiton mpiton commented May 15, 2026

Summary

Implements infrastructure/pty/shell_detector.rs per ARCHI §11.2 so T-227 (PtyPool) has a ready-made ShellSpec { program, args } to feed to portable_pty::CommandBuilder. No agent code, no Tauri code — pure platform detection.

Closes #121.

Why

Forgent never spawns claude directly. It always spawns the user's system shell (so .bashrc / .zshrc, aliases, and PATH are honoured) and then injects the Claude CLI command via stdin (ADR-014). The shell detector is the first brick of the infrastructure/pty/ module, dependency-free of any agent / Tauri code, runnable in parallel with T-225.

Changes

File Change
src-tauri/src/infrastructure/pty/shell_detector.rs NEWShellSpec value object + detect() per platform + pure resolve_windows_shell helper
src-tauri/src/infrastructure/pty/mod.rs Register pub mod shell_detector;
src-tauri/Cargo.toml cargo add which --target 'cfg(windows)'which = "8.0.2" under [target.'cfg(windows)'.dependencies]
src/shared/types/ShellSpec.ts Auto-generated by ts-rs
CHANGELOG.md [Unreleased] → Added entry

Platform logic

  • Unix: std::env::var("SHELL").ok().filter(non-empty).unwrap_or("/bin/bash")ShellSpec { program, args: [] }.
  • Windows: which("pwsh") first (→ -NoLogo), then which("powershell") (→ -NoLogo), then %COMSPEC% (filtered for empty), finally cmd.exe.

Testability

The Windows priority chain is extracted into a pure resolve_windows_shell(pwsh_on_path, powershell_on_path, comspec) helper gated with #[cfg(any(test, windows))]. Five branch tests run on every CI platform (Linux included), so the pwsh → powershell → COMSPEC → cmd.exe ladder cannot regress silently. Real which::which calls only happen inside the platform-#[cfg(windows)] detect() wrapper.

Acceptance criteria

  • pub fn detect() -> ShellSpec returns the right program per platform
  • Unix: reads std::env::var("SHELL"), falls back to /bin/bash when unset or empty
  • Windows: which("pwsh")which("powershell")$COMSPEC (or empty) → cmd.exe
  • Login-shell flag follows ARCHI §11.2 sample: -NoLogo on pwsh / powershell, none elsewhere
  • ShellSpec derives Serialize + Deserialize + TS, exported to src/shared/types/ShellSpec.ts
  • Unit tests cover SHELL set, SHELL empty, SHELL unset (via temp_env::with_var)
  • Windows tests gated behind #[cfg(windows)], file still compiles on Linux CI

Testing

Command Result
cargo fmt --check clean
cargo clippy --all-targets -- -D warnings 0 errors (10 pre-existing ts-rs warnings on serde(transparent) newtypes — unrelated)
cargo test --workspace 283 passed (+5 vs main from new resolve_windows_shell cases)
cargo deny check advisories / bans / licenses / sources OK
pnpm exec tsc -b no errors (ShellSpec.ts integrates cleanly)

Tests added

  • windows_resolver::resolve_prefers_pwsh_when_available
  • windows_resolver::resolve_falls_back_to_powershell_when_pwsh_missing
  • windows_resolver::resolve_uses_comspec_when_no_powershell_variants
  • windows_resolver::resolve_falls_back_to_cmd_when_comspec_unset
  • windows_resolver::resolve_falls_back_to_cmd_when_comspec_empty
  • unix::detect_uses_shell_env_var_when_set
  • unix::detect_falls_back_to_bin_bash_when_shell_unset
  • unix::detect_falls_back_to_bin_bash_when_shell_empty
  • windows::detect_returns_non_empty_program_on_windows_runner (integration smoke, runs only on Windows CI)

Adversarial review notes

Three parallel reviewers (security / logic / clean-code) ran on the diff. Resolved:

  • HIGH (clean code): generic /// Detect the appropriate shell docstring on both platform branches → replaced with platform-specific contracts.
  • MEDIUM (logic): Windows fallback chain was untestable on Linux CI → extracted pure resolve_windows_shell helper with full branch coverage on every runner.
  • MEDIUM (clean code): .to_string() everywhere → .into() for &'static str → String conversions, matching error.rs.
  • MEDIUM (clean code): temp_env::with_var(..., detect) function-pointer form → closure form || { ... }, matching config.rs / sentry_init.rs.
  • LOW (clean code): missing Deserialize derive vs the canonical domain/tasks/model.rs pattern → added.

Accepted as out of scope (consistent with ADR-014 trust model):

  • PATH-hijack via which("pwsh") resolving a user-writable directory (LOW security) — Forgent explicitly inherits the user's $PATH so .bashrc / aliases work; same risk surface as the user typing pwsh in their own terminal.
  • which() reporting a binary that fails to execute (LOW logic) — out of scope for T-226; spawn-time error handling lives in T-227 PtyPool.

Dependencies

None — runs in parallel with T-225 per the original ticket.


Generated by APEX workflow

Summary by CodeRabbit

New Features

  • Added platform-aware shell detection that automatically identifies the appropriate system shell on both Unix and Windows platforms, enabling the application to seamlessly integrate with the user's configured shell environment.

Review Change Stack

Implement `infrastructure/pty/shell_detector.rs` per ARCHI §11.2.
`detect()` returns a `ShellSpec { program, args }` that `PtyPool::spawn`
will hand to `portable_pty::CommandBuilder`:

- Unix: honours `$SHELL`, falls back to `/bin/bash` when unset or empty.
- Windows: prefers `pwsh` (PowerShell 7) over `powershell.exe` over
  `%COMSPEC%` / `cmd.exe`. `-NoLogo` on PowerShell variants.

`ShellSpec` derives `Serialize + Deserialize + TS` (exported to
`src/shared/types/ShellSpec.ts`) so it can flow through debug IPC later.
The Windows priority chain lives in a pure `resolve_windows_shell`
helper that is unit-tested on every platform; Linux CI runners exercise
the full pwsh > powershell > COMSPEC > cmd.exe fallback ladder without
needing real PowerShell binaries. Three `temp_env::with_var` tests cover
the Unix env-var fallback (set, unset, empty).

`which = "8.0.2"` added via `cargo add --target 'cfg(windows)'` so the
Unix build keeps the dependency surface lean.

Closes #121
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

📝 Walkthrough

Walkthrough

This PR implements platform-aware shell detection for PTY spawning. It introduces ShellSpec (program + args), Unix detect() using $SHELL with /bin/bash fallback, Windows detect() with pwshpowershellcmd.exe priority, comprehensive unit tests, and TypeScript type bindings via ts-rs.

Changes

Shell Detection Infrastructure

Layer / File(s) Summary
ShellSpec type contract and module wiring
src-tauri/src/infrastructure/pty/shell_detector.rs, src-tauri/src/infrastructure/pty/mod.rs, src-tauri/Cargo.toml, src/shared/types/ShellSpec.ts
ShellSpec { program: String, args: Vec<String> } is derived with serde::Serialize, ts-rs::TS for cross-language use; module is publicly exported and Windows which crate dependency (v8.0.2) is added to support platform detection.
Unix shell detection with $SHELL fallback
src-tauri/src/infrastructure/pty/shell_detector.rs
Platform-specific detect() for Unix reads $SHELL environment variable, treating unset/empty as invalid and falling back to /bin/bash; unit tests cover both configured and missing $SHELL scenarios.
Windows shell detection with priority resolution
src-tauri/src/infrastructure/pty/shell_detector.rs
Platform-specific detect() for Windows uses which crate to check for pwsh first, then powershell, then reads COMSPEC or defaults to cmd.exe; resolve_windows_shell helper centralizes priority logic for testability; tests validate all fallback paths and -NoLogo flag handling.
Changelog documentation
CHANGELOG.md
Unreleased entry documents T-226 implementation including platform-specific detection rules, ShellSpec export, Windows dependency, and test/lint validation.

Possibly related issues

  • #123: The shell detection infrastructure (ShellSpec type and detect()) directly supports PTY pool spawn behavior that the pty_pool integration tests will exercise.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A rabbit hops through shells with care,
Unix respects the $SHELL there,
Windows checks its PowerShell queue—
pwsh first, then cmd.exe to pursue! 🐇✨

🚥 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 accurately describes the primary change: implementing platform-aware shell detection for Unix ($SHELL), Windows (pwsh/cmd.exe), addressing T-226 and closing issue #121.
Linked Issues check ✅ Passed The PR fully implements all acceptance criteria from #121: ShellSpec struct with program/args, platform-specific detect() functions with correct shell priority chains, serde/TS derives for IPC, comprehensive unit tests for Unix and Windows with proper cfg gating, and Cargo dependency management.
Out of Scope Changes check ✅ Passed All changes are directly scoped to #121 requirements: shell_detector.rs implementation, mod.rs wiring, Cargo dependency for which crate, TypeScript type generation, CHANGELOG documentation, and supporting tests—no unrelated modifications present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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/T-226-shell-detector

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

Copy link
Copy Markdown

@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

🤖 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 `@src-tauri/src/infrastructure/pty/shell_detector.rs`:
- Around line 24-27: The env var handling for program resolution (e.g., the
std::env::var("SHELL") block that assigns program) should treat whitespace-only
values as empty; change the chain to trim the retrieved string before checking
emptiness (for example, map or filter using value.trim()) so SHELL="   " won't
be accepted and the fallback ("/bin/bash") will be used; apply the same
trim-before-empty-check fix to the COMSPEC handling at the other occurrence
(lines referenced around the COMSPEC resolution) so both std::env::var("SHELL")
and std::env::var("COMSPEC") treat whitespace-only values as empty.
🪄 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: 4517983d-7209-4ddb-bbf4-b0bef15e1ea8

📥 Commits

Reviewing files that changed from the base of the PR and between c4125ee and 06c65e1.

⛔ Files ignored due to path filters (1)
  • src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • CHANGELOG.md
  • src-tauri/Cargo.toml
  • src-tauri/src/infrastructure/pty/mod.rs
  • src-tauri/src/infrastructure/pty/shell_detector.rs
  • src/shared/types/ShellSpec.ts

Comment on lines +24 to +27
let program = std::env::var("SHELL")
.ok()
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "/bin/bash".into());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Treat whitespace-only env vars as empty before fallback resolution.

SHELL=" " or COMSPEC=" " currently bypasses the empty check and can return an unusable program value. Trim first, then apply the emptiness fallback.

💡 Suggested patch
 pub fn detect() -> ShellSpec {
     let program = std::env::var("SHELL")
         .ok()
-        .filter(|value| !value.is_empty())
+        .map(|value| value.trim().to_string())
+        .filter(|value| !value.is_empty())
         .unwrap_or_else(|| "/bin/bash".into());
     ShellSpec {
         program,
         args: Vec::new(),
@@
     let program = comspec
-        .filter(|value| !value.is_empty())
+        .map(str::trim)
+        .filter(|value| !value.is_empty())
         .map(String::from)
         .unwrap_or_else(|| "cmd.exe".into());

Also applies to: 67-69

🤖 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 `@src-tauri/src/infrastructure/pty/shell_detector.rs` around lines 24 - 27, The
env var handling for program resolution (e.g., the std::env::var("SHELL") block
that assigns program) should treat whitespace-only values as empty; change the
chain to trim the retrieved string before checking emptiness (for example, map
or filter using value.trim()) so SHELL="   " won't be accepted and the fallback
("/bin/bash") will be used; apply the same trim-before-empty-check fix to the
COMSPEC handling at the other occurrence (lines referenced around the COMSPEC
resolution) so both std::env::var("SHELL") and std::env::var("COMSPEC") treat
whitespace-only values as empty.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 06c65e161f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* system shell (so `.bashrc`, `.zshrc`, aliases and `PATH` are honoured) and
* then injects the Claude CLI command through stdin.
*/
export type ShellSpec = { program: string, args: Array<string>, };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Regenerate shared type barrel after adding ShellSpec

Adding ShellSpec.ts without updating src/shared/types/index.ts introduces deterministic codegen drift: scripts/codegen-types.ts rebuilds the barrel from all *.ts files in that directory, so it will add export type * from "./ShellSpec"; on the next pnpm run codegen. The CI codegen-diff gate in .github/workflows/ci.yml checks for exactly this drift and will fail even if runtime code is unchanged.

Useful? React with 👍 / 👎.

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented May 15, 2026

Merging this PR will not alter performance

✅ 7 untouched benchmarks


Comparing feat/T-226-shell-detector (06c65e1) with main (c4125ee)

Open in CodSpeed

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.

T-226: infrastructure/pty/shell_detector.rs ($SHELL on Unix, pwsh/cmd on Windows)

1 participant