Skip to content

Commit dc753d0

Browse files
max-sixtyclaude
andauthored
fix(shell): auto-configure PowerShell on Windows when SHELL is not set (#898)
* Refactor PowerShell environment detection for auto-config Introduce is_powershell_environment() function to detect PowerShell sessions via PSModulePath env var. Use this detection to auto-configure PowerShell when running `wt config shell install` from a PowerShell session, even if the profile doesn't exist yet (issue #885). On non-Windows, add PowerShell to the default shells list when detected. Rename explicit_shell parameter to allow_create for clarity, representing whether config files can be created for a shell. * fix(shell): restrict PowerShell auto-detect to non-Windows On Windows, PSModulePath is set system-wide (even in Git Bash, cmd), making it unreliable for detecting PowerShell sessions. This caused false positives where `wt config shell install` would create PowerShell profiles when run from Git Bash. Changes: - Restrict is_powershell_environment() to non-Windows only - On Windows, require explicit `install powershell` to create profile - Add Windows-specific hint when PowerShell is skipped - Use var_os() instead of var() for robust env var checking On macOS/Linux, PowerShell Core must be explicitly installed, so PSModulePath being set is a reliable signal for auto-detection. Co-Authored-By: Claude <noreply@anthropic.com> * fix(shell): auto-detect PowerShell on Windows via SHELL absence On Windows, detect Windows-native shells (cmd/PowerShell) by checking if the SHELL env var is NOT set. Git Bash, MSYS2, and Cygwin set SHELL, but cmd.exe and PowerShell don't. When detected, create both PowerShell profile files: - Documents/PowerShell/Microsoft.PowerShell_profile.ps1 (pwsh 7+) - Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1 (5.1) This solves issue #885 where PowerShell users couldn't auto-configure shell integration because PSModulePath detection had false positives. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(shell): rename is_powershell_environment to should_auto_configure_powershell The new name better reflects the function's purpose: it determines whether to auto-configure PowerShell profiles, not whether we're "in" PowerShell. Also adds an integration test for the WORKTRUNK_TEST_POWERSHELL_ENV override that verifies PowerShell profile creation works correctly. Co-Authored-By: Claude <noreply@anthropic.com> * fix(shell): show "shell extension" not "& completions" for PowerShell preview The preview output for `--dry-run` and `?` help was incorrectly showing "shell extension & completions" for PowerShell. Only Bash/Zsh have inline completions; Fish has separate files and PowerShell has no tab completion. This makes the preview consistent with the actual install output. Co-Authored-By: Claude <noreply@anthropic.com> * fix(tests): isolate PowerShell detection in test environment On CI environments: - Linux: PowerShell Core is installed, setting PSModulePath - Windows: Removing SHELL triggers the "SHELL not set" detection Both cause should_auto_configure_powershell() to return true in tests, adding PowerShell to the shell list and triggering the config hint. Fix by: 1. Removing PSModulePath to prevent Linux false positive 2. Setting WORKTRUNK_TEST_POWERSHELL_ENV=0 to disable detection entirely 3. Skip the PowerShell detection test on Windows (Documents folder can't be easily overridden in tests) Tests that need PowerShell detection should set the env var to "1". Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6c3dade commit dc753d0

File tree

6 files changed

+169
-24
lines changed

6 files changed

+169
-24
lines changed

.claude-plugin/skills/worktrunk/reference/faq.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ Created by `wt config shell install`:
9090
- **Bash**: adds line to `~/.bashrc`
9191
- **Zsh**: adds line to `~/.zshrc` (or `$ZDOTDIR/.zshrc`)
9292
- **Fish**: creates `~/.config/fish/functions/wt.fish` and `~/.config/fish/completions/wt.fish`
93+
- **PowerShell** (Windows): creates both profile files if they don't exist:
94+
- `Documents/PowerShell/Microsoft.PowerShell_profile.ps1` (PowerShell 7+)
95+
- `Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1` (Windows PowerShell 5.1)
96+
97+
**PowerShell detection on Windows:** When running from cmd.exe or PowerShell, both PowerShell profile files are created automatically. When running from Git Bash or MSYS2, PowerShell is skipped (use `wt config shell install powershell` to create the profiles explicitly).
9398

9499
**To remove:** `wt config shell uninstall`.
95100

.claude-plugin/skills/worktrunk/reference/troubleshooting.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,27 @@ post-create = "npm run build"
7373
post-create = "npm install"
7474
post-start = "npm run build"
7575
```
76+
77+
## PowerShell on Windows
78+
79+
### PowerShell profiles not created
80+
81+
On Windows, `wt config shell install` creates PowerShell profiles automatically when running from cmd.exe or PowerShell. It creates both:
82+
- `Documents/PowerShell/Microsoft.PowerShell_profile.ps1` (PowerShell 7+/pwsh)
83+
- `Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1` (Windows PowerShell 5.1)
84+
85+
**If running from Git Bash or MSYS2**, PowerShell is skipped because the `SHELL` environment variable is set. To create PowerShell profiles explicitly:
86+
87+
```bash
88+
wt config shell install powershell
89+
```
90+
91+
### Wrong PowerShell variant configured
92+
93+
Both profile files are created when installing from a Windows-native shell. This ensures shell integration works regardless of which PowerShell variant the user opens later. The profile files are small and harmless if unused.
94+
95+
### Detection logic
96+
97+
Worktrunk detects Windows-native shells (cmd/PowerShell) by checking if the `SHELL` environment variable is **not** set:
98+
- `SHELL` not set → Windows-native shell → create both PowerShell profiles
99+
- `SHELL` set (e.g., `/usr/bin/bash`) → Git Bash/MSYS2 → skip PowerShell

docs/content/faq.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,11 @@ Created by `wt config shell install`:
9696
- **Bash**: adds line to `~/.bashrc`
9797
- **Zsh**: adds line to `~/.zshrc` (or `$ZDOTDIR/.zshrc`)
9898
- **Fish**: creates `~/.config/fish/functions/wt.fish` and `~/.config/fish/completions/wt.fish`
99+
- **PowerShell** (Windows): creates both profile files if they don't exist:
100+
- `Documents/PowerShell/Microsoft.PowerShell_profile.ps1` (PowerShell 7+)
101+
- `Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1` (Windows PowerShell 5.1)
102+
103+
**PowerShell detection on Windows:** When running from cmd.exe or PowerShell, both PowerShell profile files are created automatically. When running from Git Bash or MSYS2, PowerShell is skipped (use `wt config shell install powershell` to create the profiles explicitly).
99104

100105
**To remove:** `wt config shell uninstall`.
101106

src/commands/configure_shell.rs

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -292,15 +292,53 @@ pub fn handle_configure_shell(
292292
})
293293
}
294294

295+
/// Check if we should auto-configure PowerShell profiles.
296+
///
297+
/// **Non-Windows:** PowerShell Core sets PSModulePath, which we use to detect
298+
/// PowerShell sessions. This is reliable because PowerShell must be explicitly
299+
/// installed on these platforms.
300+
///
301+
/// **Windows:** We check that `SHELL` is NOT set. The `SHELL` env var is set by
302+
/// Git Bash, MSYS2, and Cygwin, but NOT by cmd.exe or PowerShell. When `SHELL`
303+
/// is absent on Windows, the user is likely in a Windows-native shell (cmd or
304+
/// PowerShell), so we auto-configure both PowerShell profiles. This avoids the
305+
/// PSModulePath false-positive issue (issue #885) while still supporting
306+
/// PowerShell users who haven't created a profile yet.
307+
fn should_auto_configure_powershell() -> bool {
308+
// Allow tests to override detection (set via Command::env() in integration tests)
309+
if let Ok(val) = std::env::var("WORKTRUNK_TEST_POWERSHELL_ENV") {
310+
return val == "1";
311+
}
312+
313+
#[cfg(windows)]
314+
{
315+
// On Windows, SHELL is set by Git Bash/MSYS2/Cygwin but not by cmd/PowerShell.
316+
// If SHELL is absent, we're likely in a Windows-native shell.
317+
std::env::var_os("SHELL").is_none()
318+
}
319+
320+
#[cfg(not(windows))]
321+
{
322+
// On non-Windows, PSModulePath reliably indicates PowerShell Core
323+
std::env::var_os("PSModulePath").is_some()
324+
}
325+
}
326+
295327
pub fn scan_shell_configs(
296328
shell_filter: Option<Shell>,
297329
dry_run: bool,
298330
cmd: &str,
299331
) -> Result<ScanResult, String> {
300-
#[cfg(windows)]
301-
let default_shells = vec![Shell::Bash, Shell::Zsh, Shell::Fish, Shell::PowerShell];
302-
#[cfg(not(windows))]
303-
let default_shells = vec![Shell::Bash, Shell::Zsh, Shell::Fish];
332+
// Base shells to check
333+
let mut default_shells = vec![Shell::Bash, Shell::Zsh, Shell::Fish];
334+
335+
// Add PowerShell if we detect we're in a PowerShell-compatible environment.
336+
// - Non-Windows: PSModulePath reliably indicates PowerShell Core
337+
// - Windows: SHELL not set indicates Windows-native shell (cmd or PowerShell)
338+
let in_powershell_env = should_auto_configure_powershell();
339+
if in_powershell_env {
340+
default_shells.push(Shell::PowerShell);
341+
}
304342

305343
let shells = shell_filter.map_or(default_shells, |shell| vec![shell]);
306344

@@ -328,13 +366,26 @@ pub fn scan_shell_configs(
328366
target_path.is_some()
329367
};
330368

369+
// Auto-configure PowerShell when user is in a PowerShell-compatible environment,
370+
// even if the profile doesn't exist yet (issue #885). PowerShell doesn't create
371+
// a profile by default, so most users won't have one until they create it.
372+
// Detection:
373+
// - Non-Windows: PSModulePath indicates PowerShell Core
374+
// - Windows: SHELL not set indicates Windows-native shell (not Git Bash/MSYS2)
375+
let in_detected_shell = matches!(shell, Shell::PowerShell) && in_powershell_env;
376+
331377
// Only configure if explicitly targeting this shell OR if config file/location exists
332-
let should_configure = shell_filter.is_some() || has_config_location;
378+
// OR if we detected we're running in this shell's environment
379+
let should_configure = shell_filter.is_some() || has_config_location || in_detected_shell;
380+
381+
// Allow creating the config file if explicitly targeting this shell,
382+
// or if we detected we're in this shell's environment
383+
let allow_create = shell_filter.is_some() || in_detected_shell;
333384

334385
if should_configure {
335386
let path = target_path.or_else(|| paths.first());
336387
if let Some(path) = path {
337-
match configure_shell_file(shell, path, dry_run, shell_filter.is_some(), cmd) {
388+
match configure_shell_file(shell, path, dry_run, allow_create, cmd) {
338389
Ok(Some(result)) => results.push(result),
339390
Ok(None) => {} // No action needed
340391
Err(e) => {
@@ -379,7 +430,7 @@ fn configure_shell_file(
379430
shell: Shell,
380431
path: &Path,
381432
dry_run: bool,
382-
explicit_shell: bool,
433+
allow_create: bool,
383434
cmd: &str,
384435
) -> Result<Option<ConfigureResult>, String> {
385436
// The line we write to the config file (also used for display)
@@ -398,7 +449,7 @@ fn configure_shell_file(
398449
path,
399450
&fish_wrapper,
400451
dry_run,
401-
explicit_shell,
452+
allow_create,
402453
&config_line,
403454
);
404455
}
@@ -468,8 +519,8 @@ fn configure_shell_file(
468519
}))
469520
} else {
470521
// File doesn't exist
471-
// Only create if explicitly targeting this shell
472-
if explicit_shell {
522+
// Only create if allowed (explicitly targeting this shell or detected environment)
523+
if allow_create {
473524
if dry_run {
474525
return Ok(Some(ConfigureResult {
475526
shell,
@@ -517,7 +568,7 @@ fn configure_fish_file(
517568
path: &Path,
518569
content: &str,
519570
dry_run: bool,
520-
explicit_shell: bool,
571+
allow_create: bool,
521572
config_line: &str,
522573
) -> Result<Option<ConfigureResult>, String> {
523574
// For Fish, we write a minimal wrapper to functions/{cmd}.fish that sources
@@ -544,10 +595,10 @@ fn configure_fish_file(
544595
}
545596

546597
// File doesn't exist or doesn't have our integration
547-
// For Fish, create if parent directory exists or if explicitly targeting this shell
598+
// For Fish, create if parent directory exists or if explicitly allowed
548599
// This is different from other shells because Fish uses functions/ which may exist
549600
// even if the specific wt.fish file doesn't
550-
if !explicit_shell && !path.exists() {
601+
if !allow_create && !path.exists() {
551602
// Check if parent directory exists
552603
if !path.parent().is_some_and(|p| p.exists()) {
553604
return Ok(None);
@@ -614,11 +665,11 @@ pub fn show_install_preview(
614665

615666
let shell = result.shell;
616667
let path = format_path_for_display(&result.path);
617-
// Bash/Zsh: inline completions; Fish: separate completion file
618-
let what = if matches!(shell, Shell::Fish) {
619-
"shell extension"
620-
} else {
668+
// Bash/Zsh: inline completions; Fish/PowerShell: separate or no completions
669+
let what = if matches!(shell, Shell::Bash | Shell::Zsh) {
621670
"shell extension & completions"
671+
} else {
672+
"shell extension"
622673
};
623674

624675
eprintln!(
@@ -882,10 +933,8 @@ fn scan_for_uninstall(
882933
dry_run: bool,
883934
cmd: &str,
884935
) -> Result<UninstallScanResult, String> {
885-
#[cfg(windows)]
936+
// For uninstall, always include PowerShell to clean up any existing profiles
886937
let default_shells = vec![Shell::Bash, Shell::Zsh, Shell::Fish, Shell::PowerShell];
887-
#[cfg(not(windows))]
888-
let default_shells = vec![Shell::Bash, Shell::Zsh, Shell::Fish];
889938

890939
let shells = shell_filter.map_or(default_shells, |shell| vec![shell]);
891940

@@ -1122,11 +1171,11 @@ fn prompt_for_uninstall_confirmation(
11221171
let bold = Style::new().bold();
11231172
let shell = result.shell;
11241173
let path = format_path_for_display(&result.path);
1125-
// Bash/Zsh: inline completions; Fish: separate completion file
1126-
let what = if matches!(shell, Shell::Fish) {
1127-
"shell extension"
1128-
} else {
1174+
// Bash/Zsh: inline completions; Fish/PowerShell: separate or no completions
1175+
let what = if matches!(shell, Shell::Bash | Shell::Zsh) {
11291176
"shell extension & completions"
1177+
} else {
1178+
"shell extension"
11301179
};
11311180

11321181
eprintln!(
@@ -1316,4 +1365,7 @@ mod tests {
13161365
fn test_fish_completion_content_custom_cmd() {
13171366
insta::assert_snapshot!(fish_completion_content("myapp"));
13181367
}
1368+
1369+
// Note: should_auto_configure_powershell() is tested via WORKTRUNK_TEST_POWERSHELL_ENV
1370+
// override in tests/integration_tests/configure_shell.rs.
13191371
}

tests/common/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,11 @@ pub fn configure_cli_command(cmd: &mut Command) {
690690
// Remove $SHELL to avoid platform-dependent diagnostic output (macOS has /bin/zsh,
691691
// Linux has /bin/bash). Tests that need SHELL should set it explicitly.
692692
cmd.env_remove("SHELL");
693+
// Remove PSModulePath to prevent false PowerShell detection on CI environments
694+
// where PowerShell Core is installed but not being used.
695+
cmd.env_remove("PSModulePath");
696+
// Disable auto PowerShell detection (tests that need it should set to "1")
697+
cmd.env("WORKTRUNK_TEST_POWERSHELL_ENV", "0");
693698
cmd.env("WT_TEST_EPOCH", TEST_EPOCH.to_string());
694699
// Enable warn-level logging so diagnostics show up in test failures
695700
cmd.env("RUST_LOG", "warn");

tests/integration_tests/configure_shell.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,3 +1626,57 @@ mod pty_tests {
16261626
);
16271627
}
16281628
}
1629+
1630+
/// Test that WORKTRUNK_TEST_POWERSHELL_ENV=1 triggers PowerShell auto-detection.
1631+
/// This simulates the Windows behavior where we detect PowerShell when SHELL is not set.
1632+
#[rstest]
1633+
#[cfg_attr(
1634+
windows,
1635+
ignore = "Windows uses Documents folder which can't be easily overridden"
1636+
)]
1637+
fn test_powershell_env_detection(repo: TestRepo, temp_home: TempDir) {
1638+
// Create the PowerShell config directory (Unix: ~/.config/powershell)
1639+
// Note: On Windows, PowerShell uses Documents/ which dirs::document_dir() returns.
1640+
// This test only runs on Unix where we can control the path via HOME.
1641+
let powershell_dir = temp_home.path().join(".config/powershell");
1642+
fs::create_dir_all(&powershell_dir).unwrap();
1643+
1644+
let mut cmd = wt_command();
1645+
repo.configure_wt_cmd(&mut cmd);
1646+
set_temp_home_env(&mut cmd, temp_home.path());
1647+
// Force PowerShell detection via test env var
1648+
cmd.env("WORKTRUNK_TEST_POWERSHELL_ENV", "1");
1649+
// Set SHELL to something non-PowerShell to ensure we're testing the override
1650+
cmd.env("SHELL", "/bin/bash");
1651+
cmd.arg("config")
1652+
.arg("shell")
1653+
.arg("install")
1654+
.arg("--yes")
1655+
.current_dir(repo.root_path());
1656+
1657+
let output = cmd.output().expect("Failed to execute command");
1658+
assert!(output.status.success(), "Command should succeed");
1659+
1660+
let stderr = String::from_utf8_lossy(&output.stderr);
1661+
// Check that PowerShell was configured (not skipped)
1662+
assert!(
1663+
stderr.contains("Created shell extension for") && stderr.contains("powershell"),
1664+
"Output should show PowerShell was created:\n{}",
1665+
stderr
1666+
);
1667+
1668+
// Verify the PowerShell profile was created
1669+
let profile_path = powershell_dir.join("Microsoft.PowerShell_profile.ps1");
1670+
assert!(
1671+
profile_path.exists(),
1672+
"PowerShell profile should be created at {:?}",
1673+
profile_path
1674+
);
1675+
1676+
let content = fs::read_to_string(&profile_path).unwrap();
1677+
assert!(
1678+
content.contains("wt config shell init powershell"),
1679+
"Profile should contain shell init: {}",
1680+
content
1681+
);
1682+
}

0 commit comments

Comments
 (0)