envon 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
envon-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 USER1995
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
envon-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: envon
3
+ Version: 0.0.1
4
+ Summary: Emit the activation command for the nearest Python virtual environment and install shell bootstrap wrappers.
5
+ Project-URL: Homepage, https://github.com/userfrom1995/envon
6
+ Project-URL: Repository, https://github.com/userfrom1995/envon
7
+ Author-email: User1995 <userfrom1995@gmail.com>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2025 USER1995
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Keywords: activation,bootstrap,shell,venv,virtualenv
31
+ Classifier: Environment :: Console
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Operating System :: OS Independent
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3 :: Only
36
+ Classifier: Topic :: Software Development :: Build Tools
37
+ Classifier: Topic :: System :: Shells
38
+ Requires-Python: >=3.8
39
+ Requires-Dist: virtualenv>=20
40
+ Description-Content-Type: text/markdown
41
+
42
+ # envon
43
+
44
+ Emit the activation command for the nearest or specified Python virtual environment, and install shell bootstrap wrappers for seamless activation in your favorite shell.
45
+
46
+ ## Features
47
+ - Auto-detects and activates Python virtual environments in your project.
48
+ - Supports multiple shells: bash, zsh, sh, fish, powershell, pwsh, nushell, cmd, csh/tcsh/cshell.
49
+ - Installs a shell bootstrap function for one-command activation.
50
+ - Flexible CLI flags for advanced usage.
51
+
52
+ ## Supported Shells
53
+ - **bash** (full auto-activation)
54
+ - **zsh** (full auto-activation)
55
+ - **sh** (full auto-activation)
56
+ - **fish** (full auto-activation)
57
+ - **powershell**, **pwsh** (full auto-activation)
58
+ - **cmd**, **batch**, **bat** (prints command for manual activation)
59
+ - **nushell**, **nu** (prints command for manual activation)
60
+ - **csh**, **tcsh**, **cshell** (prints command for manual activation)
61
+
62
+ For detailed shell support and limitations, see [docs/user_guide.md](https://github.com/userfrom1995/envon/blob/main/docs/user_guide.md).
63
+
64
+ ## Installation
65
+ **Recommended:** Install with pipx for isolated environments:
66
+ ```bash
67
+ pipx install envon
68
+ ```
69
+
70
+ **Alternative:** Install with pip (may fail on some distros like Ubuntu or Windows due to PEP 668):
71
+ ```bash
72
+ python3 -m pip install envon
73
+ ```
74
+
75
+ After installation, run:
76
+ ```bash
77
+ envon --install
78
+ ```
79
+ This detects your shell and sets up the bootstrap for auto-activation.
80
+
81
+ For more detailed installation instructions, see [docs/installation.md](https://github.com/userfrom1995/envon/blob/main/docs/installation.md).
82
+
83
+ ## Usage
84
+ After installation and bootstrap setup, run:
85
+ ```bash
86
+ envon
87
+ ```
88
+ This will activate the nearest virtual environment in your project.
89
+
90
+ Supported flags: `--emit [SHELL]`, `--print-path`, `--install [SHELL]`.
91
+
92
+ For advanced usage, examples, and all flags, see [docs/user_guide.md](https://github.com/userfrom1995/envon/blob/main/docs/user_guide.md).
93
+
94
+ ## Development
95
+ For development setup, building, and project structure, see [docs/development.md](https://github.com/userfrom1995/envon/blob/main/docs/development.md).
96
+
97
+ ## Contributor Note
98
+
99
+ **envon is in its early phase. Basic functionality is solid, but we welcome help!**
100
+ - TCSH/cshell and Nushell support need improvement (auto-activation, overlays).
101
+ - If you find issues, please [raise an issue](https://github.com/userfrom1995/envon/issues).
102
+ - If you'd like to contribute, fork and submit a PR—contributions are very welcome!
103
+
104
+ Let's make envon the best Python venv activator for every shell!
105
+
106
+ ## Release Notes
107
+
108
+ **Version 0.1.0 (First Release)**
109
+ This is the initial release of envon. A lot of work is still ongoing, especially in the testing, CI, and adding support for missing shells (e.g., full auto-activation for Nushell and csh/tcsh).
110
+
111
+ If you see any issues, feel free to [open an issue](https://github.com/userfrom1995/envon/issues). If you're interested in contributing, feel free to submit a PR. If you have ideas or anything regarding the project, feel free to open a discussion or feature request in an issue.
112
+
113
+ Check out the project on [PyPI](https://pypi.org/project/envon/).
envon-0.0.1/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # envon
2
+
3
+ Emit the activation command for the nearest or specified Python virtual environment, and install shell bootstrap wrappers for seamless activation in your favorite shell.
4
+
5
+ ## Features
6
+ - Auto-detects and activates Python virtual environments in your project.
7
+ - Supports multiple shells: bash, zsh, sh, fish, powershell, pwsh, nushell, cmd, csh/tcsh/cshell.
8
+ - Installs a shell bootstrap function for one-command activation.
9
+ - Flexible CLI flags for advanced usage.
10
+
11
+ ## Supported Shells
12
+ - **bash** (full auto-activation)
13
+ - **zsh** (full auto-activation)
14
+ - **sh** (full auto-activation)
15
+ - **fish** (full auto-activation)
16
+ - **powershell**, **pwsh** (full auto-activation)
17
+ - **cmd**, **batch**, **bat** (prints command for manual activation)
18
+ - **nushell**, **nu** (prints command for manual activation)
19
+ - **csh**, **tcsh**, **cshell** (prints command for manual activation)
20
+
21
+ For detailed shell support and limitations, see [docs/user_guide.md](https://github.com/userfrom1995/envon/blob/main/docs/user_guide.md).
22
+
23
+ ## Installation
24
+ **Recommended:** Install with pipx for isolated environments:
25
+ ```bash
26
+ pipx install envon
27
+ ```
28
+
29
+ **Alternative:** Install with pip (may fail on some distros like Ubuntu or Windows due to PEP 668):
30
+ ```bash
31
+ python3 -m pip install envon
32
+ ```
33
+
34
+ After installation, run:
35
+ ```bash
36
+ envon --install
37
+ ```
38
+ This detects your shell and sets up the bootstrap for auto-activation.
39
+
40
+ For more detailed installation instructions, see [docs/installation.md](https://github.com/userfrom1995/envon/blob/main/docs/installation.md).
41
+
42
+ ## Usage
43
+ After installation and bootstrap setup, run:
44
+ ```bash
45
+ envon
46
+ ```
47
+ This will activate the nearest virtual environment in your project.
48
+
49
+ Supported flags: `--emit [SHELL]`, `--print-path`, `--install [SHELL]`.
50
+
51
+ For advanced usage, examples, and all flags, see [docs/user_guide.md](https://github.com/userfrom1995/envon/blob/main/docs/user_guide.md).
52
+
53
+ ## Development
54
+ For development setup, building, and project structure, see [docs/development.md](https://github.com/userfrom1995/envon/blob/main/docs/development.md).
55
+
56
+ ## Contributor Note
57
+
58
+ **envon is in its early phase. Basic functionality is solid, but we welcome help!**
59
+ - TCSH/cshell and Nushell support need improvement (auto-activation, overlays).
60
+ - If you find issues, please [raise an issue](https://github.com/userfrom1995/envon/issues).
61
+ - If you'd like to contribute, fork and submit a PR—contributions are very welcome!
62
+
63
+ Let's make envon the best Python venv activator for every shell!
64
+
65
+ ## Release Notes
66
+
67
+ **Version 0.1.0 (First Release)**
68
+ This is the initial release of envon. A lot of work is still ongoing, especially in the testing, CI, and adding support for missing shells (e.g., full auto-activation for Nushell and csh/tcsh).
69
+
70
+ If you see any issues, feel free to [open an issue](https://github.com/userfrom1995/envon/issues). If you're interested in contributing, feel free to submit a PR. If you have ideas or anything regarding the project, feel free to open a discussion or feature request in an issue.
71
+
72
+ Check out the project on [PyPI](https://pypi.org/project/envon/).
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "envon"
7
+ version = "0.0.1"
8
+ description = "Emit the activation command for the nearest Python virtual environment and install shell bootstrap wrappers."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { file = "LICENSE" }
12
+ keywords = ["virtualenv", "venv", "activation", "shell", "bootstrap"]
13
+ authors = [{ name = "User1995", email = "userfrom1995@gmail.com" }]
14
+ urls = { "Homepage" = "https://github.com/userfrom1995/envon", "Repository" = "https://github.com/userfrom1995/envon" }
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Environment :: Console",
21
+ "Topic :: System :: Shells",
22
+ "Topic :: Software Development :: Build Tools",
23
+ ]
24
+
25
+ # Require virtualenv so activator plugins are always available
26
+ dependencies = [
27
+ "virtualenv>=20"
28
+ ]
29
+
30
+ [project.scripts]
31
+ envon = "envon.envon:main"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/envon"]
35
+
36
+ # Ensure non-Python bootstrap files are included
37
+ [tool.hatch.build]
38
+ include = [
39
+ "src/envon/bootstrap_*.sh",
40
+ "src/envon/bootstrap_*.fish",
41
+ "src/envon/bootstrap_*.ps1",
42
+ "src/envon/bootstrap_csh.csh",
43
+ "src/envon/bootstrap_*.nu",
44
+ "src/envon/*.py",
45
+ "README.md",
46
+ "LICENSE",
47
+ ]
48
+
49
+ [tool.hatch.build.targets.sdist]
50
+ include = [
51
+ "src/envon/**",
52
+ "README.md",
53
+ "LICENSE",
54
+ ]
@@ -0,0 +1,3 @@
1
+ __all__ = ["main"]
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from .envon import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,12 @@
1
+ envon() {
2
+ if [ "$#" -gt 0 ]; then
3
+ case "$1" in
4
+ help|-h|--help|--install) command envon "$@"; return $? ;;
5
+ -*) command envon "$@"; return $? ;;
6
+ esac
7
+ fi
8
+ local cmd ec
9
+ cmd="$(command envon "$@")"; ec=$?
10
+ if [ $ec -ne 0 ]; then printf %s\n "$cmd" >&2; return $ec; fi
11
+ eval "$cmd"
12
+ }
@@ -0,0 +1,102 @@
1
+ #!usr/bin/tcsh -f
2
+ #! /bin/tcsh -f
3
+ # envon wrapper script for tcsh compatibility
4
+
5
+ # Check if this is a help/install command
6
+ # alias envon 'if ( $#argv >= 1 ) then
7
+ # if ( "$argv[1]" == "help" || "$argv[1]" == "-h" || "$argv[1]" == "--help" || "$argv[1]" == "--install" ) then
8
+ # exec /usr/bin/envon $argv:q
9
+ # endif
10
+ # endif
11
+
12
+ # # For environment activation
13
+ # set _ev=`~/.local/bin/envon $argv:q`
14
+ # if ( $status == 0 && "$_ev" != "" ) then
15
+ # eval "$_ev"
16
+ # endif'
17
+
18
+ # alias envon `if ( $#argv >= 1 ) then
19
+ # if ( "$argv[1]" == "help" || "$argv[1]" == "-h" || "$argv[1]" == "--help" || "$argv[1]" == "--install" ) then
20
+ # exec /usr/bin/envon $argv:q
21
+ # endif
22
+ # endif
23
+ # set _ev=`~/.local/bin/envon $argv:q`
24
+ # if ( $status == 0 && "$_ev" != "" ) then
25
+ # eval "$_ev"
26
+ # endif`
27
+
28
+ # alias envon 'if ( $#argv >= 1 ) then \
29
+ # if ( "$argv[1]" == "help" || "$argv[1]" == "-h" || "$argv[1]" == "--help" || "$argv[1]" == "--install" ) then \
30
+ # ~/.local/bin/envon \!* \
31
+ # else \
32
+ # set _ev=`~/.local/bin/envon \!*` \
33
+ # if ( $status == 0 && "$_ev" != "" ) then \
34
+ # eval "$_ev" \
35
+ # endif \
36
+ # if ( $?_ev ) unset _ev \
37
+ # endif \
38
+ # else \
39
+ # set _ev=`~/.local/bin/envon` \
40
+ # if ( $status == 0 && "$_ev" != "" ) then \
41
+ # eval "$_ev" \
42
+ # endif \
43
+ # if ( $?_ev ) unset _ev \
44
+ # endif'
45
+ # For tcsh, we need to avoid complex control structures in aliases
46
+ # Instead, we'll use a very simple approach
47
+
48
+
49
+ # alias envon 'set _cmd="\!*"; if ( "$_cmd" == "--help" || "$_cmd" == "-h" || "$_cmd" == "help" || "$_cmd" == "--install" ) ~/.local/bin/envon \!*; if ( "$_cmd" != "--help" && "$_cmd" != "-h" && "$_cmd" != "help" && "$_cmd" != "--install" ) set _result="`~/.local/bin/envon \!*`" && if ( $status == 0 ) eval "$_result"; unset _cmd; if ( $?_result ) unset _result'
50
+
51
+ # alias envon 'if ( $#argv >= 1 ) then \
52
+ # if ( "$argv[1]" == "help" || "$argv[1]" == "-h" || "$argv[1]" == "--help" || "$argv[1]" == "--install" ) then \
53
+ # exec /.local/bin/envon $argv:q \
54
+ # endif \
55
+ # endif \
56
+ # set _ev=`~/.local/bin/envon $argv:q` \
57
+ # if ( $status == 0 && "$_ev" != "" ) then \
58
+ # eval "$_ev" \
59
+ # endif'
60
+
61
+ # alias envon `if ( $#argv >= 1 ) then
62
+ # if ( "$argv[1]" == "help" || "$argv[1]" == "-h" || "$argv[1]" == "--help" || "$argv[1]" == "--install" ) then
63
+ # exec /usr/bin/envon $argv:q
64
+ # endif
65
+ # endif
66
+ # set _ev=`~/.local/bin/envon $argv:q`
67
+ # if ( $status == 0 && "$_ev" != "" ) then
68
+ # eval "$_ev"
69
+ # endif`
70
+
71
+ # envon managed bootstrap - minimal fixes applied
72
+ # Define a shell function for envon
73
+ # alias envon 'envon_func \!*'
74
+
75
+ # envon_func:
76
+ # if ( $#argv >= 1 ) then
77
+ # if ("$argv[1]" == "help" || "$argv[1]" == "-h" || "$argv[1]" == "--help" || "$argv[1]" == "--install") then
78
+ # exec ~/.local/bin/envon $argv:q
79
+ # endif
80
+ # endif
81
+ # set _ev=`~/.local/bin/envon $argv:q`
82
+ # if ( $status == 0 && "$_ev" != "" ) then
83
+ # eval "$_ev"
84
+ # endif
85
+ # return
86
+
87
+ #!/bin/tcsh -f
88
+ # envon wrapper script for tcsh
89
+
90
+ # Check if this is a help/install command
91
+ # if ( $#argv >= 1 ) then
92
+ # if ( "$argv[1]" == "help" || "$argv[1]" == "-h" || "$argv[1]" == "--help" || "$argv[1]" == "--install" ) then
93
+ # exec ~/.local/bin/envon $argv:q
94
+ # endif
95
+ # endif
96
+
97
+ # # For environment activation
98
+ # set _ev=`~/.local/bin/envon $argv:q`
99
+ # if ( $status == 0 && "$_ev" != "" ) then
100
+ # eval "$_ev"
101
+ # endif
102
+ alias envon '~/.local/bin/envon \!*'
@@ -0,0 +1,2 @@
1
+ alias envon 'if ( \$#argv >= 0 ) then \\
2
+ switch (" \\)
@@ -0,0 +1,17 @@
1
+ function envon
2
+ if test (count $argv) -gt 0
3
+ set first $argv[1]
4
+ if test "$first" = "--"
5
+ set -e argv[1]
6
+ else if string match -rq '^(help|-h|--help|--install|-).*' -- $first
7
+ command envon $argv
8
+ return $status
9
+ end
10
+ end
11
+ set cmd (command envon $argv)
12
+ if test $status -ne 0
13
+ echo $cmd >&2
14
+ return 1
15
+ end
16
+ eval $cmd
17
+ end
@@ -0,0 +1,19 @@
1
+ def --env envon [...args] {
2
+ if ($args | is-empty) == false {
3
+ let first = ($args | first)
4
+ if $first == '--' { let args = ($args | skip 1); ^envon ...$args; return }
5
+ if ($first == 'help') or ($first == '-h') or ($first == '--help') or ($first == '--install') or (($first | str starts-with '-') == true) {
6
+ ^envon ...$args; return
7
+ }
8
+ }
9
+ let venv = (^envon --print-path ...$args | str trim)
10
+ if ($venv | is-empty) { return }
11
+ let act = ($venv | path join 'bin' 'activate.nu')
12
+ if ($act | path exists) {
13
+ echo $"overlay use '($act | path expand)'"
14
+ echo 'Run the printed command in your interactive shell to activate the virtual environment.'
15
+ return
16
+ }
17
+ echo 'Nushell activation script (activate.nu) not found for this virtual environment.'
18
+ echo 'Create or upgrade the environment with a tool that generates Nushell activation scripts.'
19
+ }
@@ -0,0 +1,14 @@
1
+ function envon {
2
+ param([Parameter(ValueFromRemainingArguments=$true)][string[]]$Args)
3
+ $envonExe = Get-Command envon -CommandType Application -ErrorAction SilentlyContinue
4
+ if (-not $envonExe) { Write-Error 'envon console script not found on PATH'; return }
5
+ if ($Args.Count -gt 0) {
6
+ if ($Args[0] -eq '--') { $Args = $Args[1..($Args.Count-1)] }
7
+ elseif ($Args[0] -eq 'help' -or $Args[0] -eq '--help' -or $Args[0] -eq '--install' -or $Args[0].StartsWith('-')) {
8
+ & $envonExe.Source @Args; return
9
+ }
10
+ }
11
+ $cmd = & $envonExe.Source @Args
12
+ if ($LASTEXITCODE -ne 0) { Write-Error $cmd; return }
13
+ Invoke-Expression $cmd
14
+ }
@@ -0,0 +1,11 @@
1
+ envon() {
2
+ if [ "$#" -gt 0 ]; then
3
+ case "$1" in
4
+ help|-h|--help|--install) command envon "$@"; return $? ;;
5
+ -*) command envon "$@"; return $? ;;
6
+ esac
7
+ fi
8
+ cmd=$(command envon "$@"); ec=$?
9
+ if [ $ec -ne 0 ]; then printf %s\n "$cmd" >&2; return $ec; fi
10
+ eval "$cmd"
11
+ }
@@ -0,0 +1,843 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import platform
6
+ import shutil
7
+ import sys
8
+ from pathlib import Path
9
+ import stat
10
+
11
+ try:
12
+ from virtualenv.run.plugin.base import PluginLoader
13
+ except ImportError:
14
+ # Fallback if plugin system not available
15
+ PluginLoader = None
16
+
17
+ try: # version info for managed bootstrap tagging
18
+ from virtualenv.version import __version__ as VENV_VERSION
19
+ except Exception: # pragma: no cover - defensive fallback
20
+ VENV_VERSION = "unknown"
21
+
22
+ PREFERRED_NAMES = (".venv", "venv", "env", ".env")
23
+
24
+
25
+ class EnvonError(Exception):
26
+ pass
27
+
28
+
29
+ def is_venv_dir(path: Path) -> bool:
30
+ """Return True if the given path looks like a Python virtual environment directory."""
31
+ if not path or not path.is_dir():
32
+ return False
33
+
34
+ # Check for pyvenv.cfg file - this is the most reliable indicator
35
+ if (path / "pyvenv.cfg").exists():
36
+ return True
37
+
38
+ # Try to use virtualenv's activation system to detect available scripts
39
+ if PluginLoader:
40
+ try:
41
+ activators = PluginLoader.entry_points_for("virtualenv.activate")
42
+ # Check if any activation scripts exist
43
+ for activator_name in ["bash", "batch", "powershell", "fish", "cshell", "nushell"]:
44
+ if activator_name in activators:
45
+ # Check common script locations based on platform
46
+ if activator_name == "bash" and (path / "bin" / "activate").exists():
47
+ return True
48
+ if activator_name == "batch" and (path / "Scripts" / "activate.bat").exists():
49
+ return True
50
+ if activator_name == "powershell" and (path / "Scripts" / "Activate.ps1").exists():
51
+ return True
52
+ if activator_name == "fish" and (path / "bin" / "activate.fish").exists():
53
+ return True
54
+ if activator_name == "cshell" and (path / "bin" / "activate.csh").exists():
55
+ return True
56
+ if activator_name == "nushell" and (path / "bin" / "activate.nu").exists():
57
+ return True
58
+ except Exception:
59
+ # Fall back to hardcoded detection
60
+ pass
61
+
62
+ # Fallback: hardcoded detection for compatibility
63
+ # Windows layout
64
+ if (path / "Scripts" / "activate.bat").exists() or (path / "Scripts" / "Activate.ps1").exists():
65
+ return True
66
+ # POSIX layout
67
+ if (path / "bin" / "activate").exists():
68
+ return True
69
+ # Other shells
70
+ if (path / "bin" / "activate.fish").exists() or (path / "bin" / "activate.csh").exists() or (
71
+ path / "bin" / "activate.nu").exists():
72
+ return True
73
+ return False
74
+
75
+
76
+ def find_nearest_venv(start: Path) -> Path | None:
77
+ """Walk upwards from start to root and try common names; return the first venv path found."""
78
+ cur = start
79
+ tried: list[Path] = []
80
+ while True:
81
+ for name in PREFERRED_NAMES:
82
+ cand = cur / name
83
+ tried.append(cand)
84
+ if is_venv_dir(cand):
85
+ return cand
86
+ parent = cur.parent
87
+ if parent == cur:
88
+ break
89
+ cur = parent
90
+ return None
91
+
92
+
93
+ def _list_venvs_in_dir(root: Path) -> list[Path]:
94
+ """Return all virtualenv directories directly under root.
95
+
96
+ Preference order: common names first (PREFERRED_NAMES) in that order, then any other subdirectory
97
+ that looks like a venv in alphabetical order.
98
+ """
99
+ found: list[Path] = []
100
+ seen: set[Path] = set()
101
+ for name in PREFERRED_NAMES:
102
+ cand = root / name
103
+ if is_venv_dir(cand):
104
+ found.append(cand)
105
+ seen.add(cand)
106
+ # Scan all subdirectories
107
+ try:
108
+ for child in sorted([p for p in root.iterdir() if p.is_dir()]):
109
+ if child in seen:
110
+ continue
111
+ if is_venv_dir(child):
112
+ found.append(child)
113
+ except FileNotFoundError:
114
+ pass
115
+ return found
116
+
117
+
118
+ def _choose_interactively(candidates: list[Path], context: str) -> Path:
119
+ """Prompt the user to choose a venv when multiple are found.
120
+
121
+ If stdin is not a TTY, print options and raise EnvonError.
122
+ """
123
+ if not sys.stdin.isatty():
124
+ lines = "\n".join(f" {i + 1}) {p}" for i, p in enumerate(candidates))
125
+ raise EnvonError(
126
+ f"Multiple virtual environments found in {context}. Choose one by passing a path or name:\n{lines}"
127
+ )
128
+ print(f"Multiple virtual environments found in {context}:", file=sys.stderr)
129
+ for i, p in enumerate(candidates, 1):
130
+ print(f" {i}) {p}", file=sys.stderr)
131
+ while True:
132
+ # Print prompt to stderr so command substitution doesn't capture it
133
+ sys.stderr.write("Select [1-{}]: ".format(len(candidates)))
134
+ sys.stderr.flush()
135
+ try:
136
+ sel = sys.stdin.readline()
137
+ except Exception:
138
+ raise EnvonError("Aborted.")
139
+ if not sel:
140
+ raise EnvonError("Aborted.")
141
+ sel = sel.strip()
142
+ if not sel:
143
+ continue
144
+ if sel.isdigit():
145
+ idx = int(sel)
146
+ if 1 <= idx <= len(candidates):
147
+ return candidates[idx - 1]
148
+ print("Invalid selection.", file=sys.stderr)
149
+
150
+
151
+ def resolve_target(target: str | None) -> Path:
152
+ if not target:
153
+ # First, prefer venvs directly in the current directory; if multiple, ask.
154
+ cwd = Path.cwd()
155
+ in_here = _list_venvs_in_dir(cwd)
156
+ if len(in_here) == 1:
157
+ return in_here[0]
158
+ if len(in_here) > 1:
159
+ return _choose_interactively(in_here, str(cwd))
160
+ # Fallback to walking upwards to find a named venv (e.g., project/.venv)
161
+ venv = find_nearest_venv(cwd)
162
+ if not venv:
163
+ # Fallback: if a virtual environment is already active, respect it
164
+ ve = os.environ.get("VIRTUAL_ENV")
165
+ if ve and is_venv_dir(Path(ve)):
166
+ return Path(ve)
167
+ raise EnvonError("No virtual environment found here. Create one (e.g., '.venv') or pass a path.")
168
+ return venv
169
+
170
+ p = Path(target)
171
+ if p.exists():
172
+ if p.is_dir() and is_venv_dir(p):
173
+ return p
174
+ # Allow passing project root; try common children
175
+ multiple = _list_venvs_in_dir(p)
176
+ if len(multiple) == 1:
177
+ return multiple[0]
178
+ if len(multiple) > 1:
179
+ return _choose_interactively(multiple, str(p))
180
+ raise EnvonError(f"Path does not appear to contain a virtual environment: {p}")
181
+
182
+ # Fallback: WORKON_HOME name
183
+ workon = os.environ.get("WORKON_HOME")
184
+ if workon:
185
+ cand = Path(workon) / target
186
+ if is_venv_dir(cand):
187
+ return cand
188
+ raise EnvonError(f"Cannot resolve virtual environment from argument: {target}")
189
+
190
+
191
+ def detect_shell(explicit: str | None) -> str:
192
+ if explicit:
193
+ return explicit.lower()
194
+
195
+ # Heuristics by platform/env
196
+ if os.name == "nt":
197
+ # Prefer PowerShell if available, else default to cmd
198
+ if "PSModulePath" in os.environ:
199
+ return "powershell"
200
+ return "cmd"
201
+ # POSIX
202
+ # 1) Environment variables set by the shell itself (most reliable when present)
203
+ if os.environ.get("ZSH_VERSION"):
204
+ return "zsh"
205
+ if os.environ.get("BASH_VERSION"):
206
+ return "bash"
207
+ if os.environ.get("FISH_VERSION"):
208
+ return "fish"
209
+ # nushell does not (always) export a dedicated var; try a common one if present
210
+ if os.environ.get("NU_VERSION"):
211
+ return "nushell"
212
+
213
+ # 2) Inspect parent process (the shell) via /proc when available (Linux/WSL)
214
+ try:
215
+ ppid = os.getppid()
216
+ proc_comm = Path("/proc") / str(ppid) / "comm"
217
+ name = ""
218
+ if proc_comm.exists():
219
+ try:
220
+ name = proc_comm.read_text(encoding="utf-8").strip().lower()
221
+ except Exception:
222
+ name = ""
223
+ if not name:
224
+ proc_exe = Path("/proc") / str(ppid) / "exe"
225
+ if proc_exe.exists():
226
+ try:
227
+ name = os.path.basename(os.readlink(proc_exe)).lower()
228
+ except Exception:
229
+ name = ""
230
+ if name:
231
+ # Normalize common names
232
+ if "zsh" in name:
233
+ return "zsh"
234
+ if name in {"bash", "sh"} or "bash" in name:
235
+ return "bash" if "bash" in name else "sh"
236
+ if "fish" in name:
237
+ return "fish"
238
+ if name in {"csh", "tcsh"} or "csh" in name or "tcsh" in name:
239
+ return "cshell"
240
+ if "nu" in name:
241
+ return "nushell"
242
+ if name in {"pwsh", "powershell"}:
243
+ return "powershell"
244
+ if name in {"cmd", "cmd.exe"}:
245
+ return "cmd"
246
+ except Exception:
247
+ pass
248
+
249
+ # 3) Fallback to $SHELL login shell
250
+ shell = os.environ.get("SHELL", "").lower()
251
+ if "zsh" in shell:
252
+ return "zsh"
253
+ if "fish" in shell:
254
+ return "fish"
255
+ if "csh" in shell or "tcsh" in shell:
256
+ return "cshell"
257
+ if "nu" in shell or "nushell" in shell:
258
+ return "nushell"
259
+ if shell.endswith("sh") and "bash" not in shell:
260
+ return "sh"
261
+ return "bash"
262
+
263
+
264
+ def emit_activation(venv: Path, shell: str) -> str:
265
+ """Generate activation command using virtualenv's activation plugin system."""
266
+ shell = shell.lower()
267
+
268
+ # Nushell is not supported on Windows — refuse to emit activation for it
269
+ if os.name == "nt" and shell in {"nu", "nushell"}:
270
+ raise EnvonError(
271
+ "Nushell activation is not supported on Windows. Use PowerShell (powershell/pwsh) or cmd."
272
+ )
273
+
274
+ # Map shell names to activator entry point names
275
+ shell_to_activator = {
276
+ "bash": "bash",
277
+ "zsh": "bash", # zsh uses bash activator
278
+ "sh": "bash", # sh uses bash activator
279
+ "fish": "fish",
280
+ "csh": "cshell",
281
+ "tcsh": "cshell",
282
+ "cshell": "cshell",
283
+ "nu": "nushell", # Map nushell to its activator
284
+ "nushell": "nushell", # Map nushell to its activator
285
+ "powershell": "powershell",
286
+ "pwsh": "powershell",
287
+ "cmd": "batch",
288
+ "batch": "batch",
289
+ "bat": "batch",
290
+ }
291
+
292
+ activator_name = shell_to_activator.get(shell)
293
+ if not activator_name:
294
+ supported = ", ".join(sorted(shell_to_activator.keys()))
295
+ raise EnvonError(
296
+ f"Unsupported shell: {shell}. Supported shells: {supported}. "
297
+ f"Specify --emit <shell> explicitly or omit --emit to auto-detect."
298
+ )
299
+
300
+ # Try to use the plugin system to get proper script names
301
+ if PluginLoader:
302
+ try:
303
+ activators = PluginLoader.entry_points_for("virtualenv.activate")
304
+ if activator_name in activators:
305
+ activator_class = activators[activator_name]
306
+
307
+ # Create a minimal mock creator to get script names
308
+ class MockCreator:
309
+ def __init__(self, venv_path):
310
+ self.dest = venv_path
311
+ if (venv_path / "Scripts").exists(): # Windows
312
+ self.bin_dir = venv_path / "Scripts"
313
+ else: # POSIX
314
+ self.bin_dir = venv_path / "bin"
315
+
316
+ mock_creator = MockCreator(venv)
317
+
318
+ # Try to determine activation script name from the activator
319
+ try:
320
+ # Create a temporary activator instance with minimal options
321
+ class MockOptions:
322
+ prompt = None
323
+
324
+ activator = activator_class(MockOptions())
325
+
326
+ # Get the templates to determine script names
327
+ if hasattr(activator, 'templates'):
328
+ for template in activator.templates():
329
+ if hasattr(activator, 'as_name'):
330
+ script_name = activator.as_name(template)
331
+ else:
332
+ script_name = template
333
+
334
+ script_path = mock_creator.bin_dir / script_name
335
+ if script_path.exists():
336
+ return _generate_activation_command(script_path, shell)
337
+ except Exception:
338
+ # Fall back to hardcoded approach if activator instantiation fails
339
+ pass
340
+ except Exception:
341
+ # Fall back to hardcoded paths if plugin system fails
342
+ pass
343
+
344
+ # Fallback: Use hardcoded script detection
345
+ return _emit_activation_fallback(venv, shell)
346
+
347
+
348
+ def _generate_activation_command(script_path: Path, shell: str) -> str:
349
+ """Generate the appropriate activation command for the given script and shell."""
350
+ shell = shell.lower()
351
+
352
+ if shell in {"bash", "zsh", "sh"}:
353
+ return f". '{script_path.as_posix()}'"
354
+ elif shell == "fish":
355
+ return f"source '{script_path.as_posix()}'"
356
+ if shell in {"csh", "tcsh", "cshell"}:
357
+ return f"source {script_path.as_posix()}"
358
+ elif shell in {"nu", "nushell"}:
359
+ # For Nushell we only print the overlay use on the activation script path.
360
+ return f"overlay use \"{script_path.as_posix()}\""
361
+ elif shell in {"powershell", "pwsh"}:
362
+ return f". '{script_path.as_posix()}'"
363
+ elif shell in {"cmd", "batch", "bat"}:
364
+ return f"call \"{script_path}\""
365
+
366
+ raise EnvonError(f"Unknown shell command format for: {shell}")
367
+
368
+
369
+ def _emit_activation_fallback(venv: Path, shell: str) -> str:
370
+ """Fallback activation detection using hardcoded paths."""
371
+ shell = shell.lower()
372
+
373
+ if shell in {"bash", "zsh", "sh"}:
374
+ act = venv / "bin" / "activate"
375
+ if act.exists():
376
+ return f". '{act.as_posix()}'"
377
+ elif shell == "fish":
378
+ act = venv / "bin" / "activate.fish"
379
+ if act.exists():
380
+ return f"source '{act.as_posix()}'"
381
+ elif shell in {"csh", "tcsh", "cshell"}:
382
+ act = venv / "bin" / "activate.csh"
383
+ if act.exists():
384
+ return f"source {act.as_posix()}"
385
+ elif shell in {"nu", "nushell"}:
386
+ # Check for activate.nu in both Windows and POSIX locations and print overlay use on it.
387
+ act_posix = venv / "bin" / "activate.nu"
388
+ act_windows = venv / "Scripts" / "activate.nu"
389
+ act = act_posix if act_posix.exists() else act_windows if act_windows.exists() else None
390
+ if act and act.exists():
391
+ return f"overlay use \"{act.as_posix()}\""
392
+ raise EnvonError(
393
+ f"Virtual environment '{venv}' does not support Nushell activation: 'activate.nu' is missing. "
394
+ "Create or upgrade the environment with a tool that generates Nushell activation scripts, "
395
+ "or use a different shell (bash/zsh/fish)."
396
+ )
397
+ elif shell in {"powershell", "pwsh"}:
398
+ act = venv / "Scripts" / "Activate.ps1"
399
+ if act.exists():
400
+ return f". '{act.as_posix()}'"
401
+ elif shell in {"cmd", "batch", "bat"}:
402
+ act = venv / "Scripts" / "activate.bat"
403
+ if act.exists():
404
+ return f"call \"{act}\""
405
+
406
+ raise EnvonError(
407
+ f"No activation script found for shell '{shell}' in '{venv}'. "
408
+ "Try specifying --emit explicitly, or ensure the virtualenv's activation scripts exist."
409
+ )
410
+
411
+
412
+ ## Nushell: uses overlay use on activate.nu directly
413
+
414
+
415
+ def parse_args(argv: list[str]) -> argparse.Namespace:
416
+ p = argparse.ArgumentParser(
417
+ prog="envon",
418
+ description="Emit the activation command for the nearest or specified virtual environment.",
419
+ )
420
+ p.add_argument("target", nargs="?", help="Path, project root, or name (searched in WORKON_HOME)")
421
+ p.add_argument(
422
+ "--emit",
423
+ nargs="?",
424
+ const="",
425
+ metavar="SHELL",
426
+ help=(
427
+ "Emit activation command. If SHELL is provided, use it (bash, zsh, sh, fish, cshell, nushell, powershell, pwsh, cmd); "
428
+ "if omitted, auto-detect the current shell."
429
+ ),
430
+ )
431
+ p.add_argument(
432
+ "--print-path",
433
+ action="store_true",
434
+ help="Print only the resolved virtual environment path and exit.",
435
+ )
436
+ p.add_argument(
437
+ "--install",
438
+ nargs="?",
439
+ const="",
440
+ metavar="SHELL",
441
+ help=(
442
+ "Install envon bootstrap function directly to shell configuration file. "
443
+ "If SHELL is omitted, auto-detect."
444
+ ),
445
+ )
446
+ return p.parse_args(argv)
447
+
448
+
449
+ def emit_bootstrap(shell: str) -> str:
450
+ """Generate the bootstrap function for the given shell by reading from dedicated files."""
451
+ shell = shell.lower()
452
+ bootstrap_dir = Path(__file__).parent # Directory of envon.py
453
+
454
+ file_map = {
455
+ "bash": "bootstrap_bash.sh",
456
+ "zsh": "bootstrap_bash.sh", # zsh reuses bash
457
+ "sh": "bootstrap_sh.sh",
458
+ "fish": "bootstrap_fish.fish",
459
+ "nushell": "bootstrap_nushell.nu",
460
+ "nu": "bootstrap_nushell.nu",
461
+ "powershell": "bootstrap_powershell.ps1",
462
+ "pwsh": "bootstrap_powershell.ps1",
463
+ "csh": "bootstrap_csh.csh",
464
+ "tcsh": "bootstrap_csh.csh",
465
+ "cshell": "bootstrap_csh.csh",
466
+ }
467
+
468
+ if shell not in file_map:
469
+ raise EnvonError(f"Unsupported shell: {shell}")
470
+
471
+ bootstrap_file = bootstrap_dir / file_map[shell]
472
+ if not bootstrap_file.exists():
473
+ raise EnvonError(f"Bootstrap file missing: {bootstrap_file}")
474
+
475
+ text = bootstrap_file.read_text(encoding="utf-8")
476
+ if text.startswith("\ufeff"): # strip BOM
477
+ text = text.lstrip("\ufeff")
478
+ return text
479
+
480
+
481
+ def get_shell_config_path(shell: str) -> Path:
482
+ """Get the configuration file path for a given shell."""
483
+ shell = shell.lower()
484
+ home = Path.home()
485
+
486
+ if shell == "bash":
487
+ # Try .bashrc first, fall back to .bash_profile
488
+ bashrc = home / ".bashrc"
489
+ if bashrc.exists():
490
+ return bashrc
491
+ return home / ".bash_profile"
492
+ if shell == "sh":
493
+ # POSIX sh typically sources ~/.profile (login shells); there is no standard per-shell rc
494
+ # We choose ~/.profile as the install target.
495
+ return home / ".profile"
496
+ elif shell == "zsh":
497
+ return home / ".zshrc"
498
+ elif shell == "fish":
499
+ config_dir = home / ".config" / "fish"
500
+ return config_dir / "config.fish"
501
+ elif shell in {"nushell", "nu"}:
502
+ if os.name == "nt": # Windows
503
+ config_dir = Path(os.environ.get("APPDATA", home)) / "nushell"
504
+ else: # POSIX
505
+ config_dir = home / ".config" / "nushell"
506
+ return config_dir / "config.nu"
507
+ elif shell in {"powershell", "pwsh"}:
508
+ if os.name == "nt": # Windows
509
+ documents = Path.home() / "Documents"
510
+ # Check for both possible profile file names
511
+ if shell == "pwsh":
512
+ core_profile = documents / "PowerShell" / "Microsoft.PowerShell_profile.ps1"
513
+ alt_core_profile = documents / "PowerShell" / "profile.ps1"
514
+ if core_profile.exists():
515
+ return core_profile
516
+ if alt_core_profile.exists():
517
+ return alt_core_profile
518
+ # Default to core_profile if neither exists
519
+ return core_profile
520
+ else:
521
+ win_profile = documents / "WindowsPowerShell" / "Microsoft.PowerShell_profile.ps1"
522
+ alt_win_profile = documents / "WindowsPowerShell" / "profile.ps1"
523
+ if win_profile.exists():
524
+ return win_profile
525
+ if alt_win_profile.exists():
526
+ return alt_win_profile
527
+ # Default to win_profile if neither exists
528
+ return win_profile
529
+ else: # POSIX PowerShell Core
530
+ return home / ".config" / "powershell" / "Microsoft.PowerShell_profile.ps1"
531
+ elif shell in {"csh", "tcsh", "cshell"}:
532
+ if shell == "tcsh":
533
+ return home / ".tcshrc"
534
+ return home / ".cshrc"
535
+
536
+ raise EnvonError(f"Unknown shell configuration path for: {shell}")
537
+
538
+
539
+ def install_bootstrap(shell: str | None) -> str:
540
+ """Install envon bootstrap function to shell configuration file."""
541
+ shell = detect_shell(shell) # auto-detect when None or empty string
542
+ shell = shell.lower()
543
+ # Windows-specific policy: do not modify any profile files automatically
544
+ # - Nushell is not supported on Windows for installation
545
+ # - For PowerShell, write the managed file and instruct the user to update their profile manually
546
+ if os.name == "nt":
547
+ if shell in {"nushell", "nu"}:
548
+ raise EnvonError(
549
+ "Nushell is not supported on Windows. Please use PowerShell (powershell/pwsh)."
550
+ )
551
+
552
+ # Default to PowerShell on Windows installs
553
+ ps_shell = "powershell" if shell == "powershell" else ("pwsh" if shell == "pwsh" else "powershell")
554
+ managed_file = get_managed_bootstrap_path(ps_shell)
555
+ managed_file.parent.mkdir(parents=True, exist_ok=True)
556
+
557
+ # Generate and write the managed content (PowerShell)
558
+ content = _managed_content_for_shell("powershell" if ps_shell == "powershell" else "powershell")
559
+ _write_managed_if_changed(managed_file, content)
560
+
561
+ # Compute the recommended profile path to show the user
562
+ profile_path = get_shell_config_path(ps_shell)
563
+
564
+ manual_block = (
565
+ f"$envonPath = '{managed_file}'\nif (Test-Path $envonPath) {{ . $envonPath }}"
566
+ )
567
+
568
+ return (
569
+ "envon bootstrap prepared for Windows (no profile auto-edit performed).\n"
570
+ f"- managed file: {managed_file}\n"
571
+ f"- PowerShell profile: {profile_path}\n\n"
572
+ "Add the following lines to your PowerShell profile manually, then restart the shell:\n\n"
573
+ f"{manual_block}"
574
+ )
575
+
576
+ # Non-Windows: proceed with automated RC update
577
+ config_path = get_shell_config_path(shell)
578
+
579
+ # Ensure parent directory exists and target is a file path
580
+ config_path.parent.mkdir(parents=True, exist_ok=True)
581
+ # Guard against a directory accidentally existing at the profile path
582
+ if config_path.exists() and config_path.is_dir():
583
+ raise EnvonError(
584
+ f"Profile path points to a directory, not a file: {config_path}. "
585
+ "Please remove/rename this directory or set the correct profile file."
586
+ )
587
+
588
+ # Managed bootstrap: write function to a stable file and source it from RC with markers
589
+ managed_file = get_managed_bootstrap_path(shell)
590
+ managed_file.parent.mkdir(parents=True, exist_ok=True)
591
+
592
+ # Generate function content for the managed file
593
+ target_shell = (
594
+ "bash" if shell == "bash" else
595
+ "zsh" if shell == "zsh" else
596
+ "sh" if shell == "sh" else
597
+ "fish" if shell == "fish" else
598
+ "nushell" if shell in {"nushell", "nu"} else
599
+ "powershell" if shell in {"powershell", "pwsh"} else
600
+ "csh" if shell in {"csh", "tcsh", "cshell"} else None
601
+ )
602
+ if target_shell is None:
603
+ supported = "bash, zsh, sh, fish, nushell, nu, powershell, pwsh, csh, tcsh, cshell"
604
+ raise EnvonError(
605
+ f"Unsupported shell for installation: {shell}. Supported: {supported}. "
606
+ f"Specify '--install <shell>' explicitly or run 'envon --bootstrap <shell>' and source it manually."
607
+ )
608
+ content = _managed_content_for_shell(target_shell)
609
+ _write_managed_if_changed(managed_file, content)
610
+
611
+ # Ensure RC contains a single, marked source block
612
+ _ensure_rc_sources_managed(config_path, managed_file, shell)
613
+ # Pick correct source command for user hint
614
+ source_cmd = (
615
+ "." if shell in {"sh", "powershell", "pwsh"} else "source"
616
+ )
617
+ return (
618
+ f"envon bootstrap installed:\n- managed: {managed_file}\n- rc: {config_path}\n"
619
+ f"Restart your shell or run: {source_cmd} {config_path}"
620
+ )
621
+
622
+
623
+ MARK_START = "# >>> envon bootstrap >>>"
624
+ MARK_END = "# <<< envon bootstrap <<<"
625
+
626
+
627
+ def get_managed_bootstrap_path(shell: str) -> Path:
628
+ """Return the managed bootstrap file path for a shell."""
629
+ shell = shell.lower()
630
+ # Determine config base dir
631
+ if os.name == "nt":
632
+ base = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
633
+ else:
634
+ base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
635
+ envon_dir = base / "envon"
636
+
637
+ name = (
638
+ "envon.bash" if shell == "bash" else
639
+ "envon.zsh" if shell == "zsh" else
640
+ "envon.sh" if shell == "sh" else
641
+ "envon.fish" if shell == "fish" else
642
+ "envon.nu" if shell in {"nushell", "nu"} else
643
+ "envon.ps1" if shell in {"powershell", "pwsh"} else
644
+ "envon.csh" if shell in {"csh", "tcsh", "cshell"} else None
645
+ )
646
+ if name is None:
647
+ raise EnvonError(f"Unsupported shell: {shell}")
648
+ return envon_dir / name
649
+
650
+
651
+ # def get_nushell_venv_path() -> Path:
652
+ # """Return the path to the managed Nushell venv.nu file."""
653
+ # if os.name == "nt":
654
+ # base = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
655
+ # else:
656
+ # base = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
657
+ # envon_dir = base / "envon"
658
+ # return envon_dir / "venv.nu"
659
+
660
+
661
+ # def _ensure_nushell_venv_file() -> None:
662
+ # """Ensure the managed Nushell venv.nu file and parent dir exist."""
663
+ # venv_path = get_nushell_venv_path()
664
+ # venv_path.parent.mkdir(parents=True, exist_ok=True)
665
+ # if not venv_path.exists():
666
+ # # create a placeholder file that will be overwritten on activation
667
+ # venv_path.write_text("# envon venv.nu - will be overwritten when activating environments\n", encoding="utf-8")
668
+
669
+
670
+ def _write_managed_if_changed(path: Path, content: str) -> None:
671
+ """Write content to path if missing or different."""
672
+ try:
673
+ if path.exists() and path.read_text() == content:
674
+ return
675
+ except Exception:
676
+ # If read fails, attempt to overwrite
677
+ pass
678
+ tmp = path.with_suffix(path.suffix + ".tmp")
679
+ tmp.write_text(content, encoding="utf-8")
680
+ tmp.replace(path)
681
+
682
+
683
+ def _ensure_rc_sources_managed(config_path: Path, managed_file: Path, shell: str) -> None:
684
+ """Ensure the user's RC/profile sources the managed file, using idempotent markers."""
685
+ rc_exists = config_path.exists() and config_path.is_file()
686
+ rc_text = config_path.read_text(encoding="utf-8") if rc_exists else ""
687
+
688
+ # If already installed with markers, do nothing
689
+ if MARK_START in rc_text and MARK_END in rc_text:
690
+ return
691
+
692
+ mf = managed_file.as_posix()
693
+ if shell in {"bash", "zsh", "sh"}:
694
+ block = f"\n{MARK_START}\n[ -f {mf} ] && . {mf}\n{MARK_END}\n"
695
+ elif shell == "fish":
696
+ block = f"\n{MARK_START}\nif test -f {mf}\n source {mf}\nend\n{MARK_END}\n"
697
+ elif shell in {"nushell", "nu"}:
698
+ # Always quote the managed file path to avoid extra positional argument errors
699
+ block = (
700
+ f"\n{MARK_START}\n"
701
+ f"if (ls '{mf}' | is-empty) == false {{\n source '{mf}'\n}}\n"
702
+ f"{MARK_END}\n"
703
+ )
704
+ elif shell in {"powershell", "pwsh"}:
705
+ block = (
706
+ f"\n{MARK_START}\n"
707
+ f"$envonPath = '{managed_file}'\nif (Test-Path $envonPath) {{ . $envonPath }}\n"
708
+ f"{MARK_END}\n"
709
+ )
710
+ elif shell in {"csh", "tcsh", "cshell"}:
711
+ block = f"\n{MARK_START}\nif ( -f {mf} ) source {mf}\n{MARK_END}\n"
712
+ else:
713
+ raise EnvonError(f"Unsupported shell: {shell}")
714
+
715
+ # Write or append the block with a robust fallback for Windows I/O quirks
716
+ try:
717
+ # Try to clear read-only flag if present
718
+ if rc_exists:
719
+ try:
720
+ os.chmod(config_path, stat.S_IWRITE | stat.S_IREAD)
721
+ except Exception:
722
+ pass
723
+ if not rc_exists:
724
+ # Create new profile file with our block
725
+ config_path.write_text(block, encoding="utf-8")
726
+ else:
727
+ with config_path.open("a", encoding="utf-8") as f:
728
+ f.write(block)
729
+ except OSError as e:
730
+ # Fallback: write the full combined content (existing + block)
731
+ combined = rc_text + block
732
+ try:
733
+ config_path.write_text(combined, encoding="utf-8")
734
+ except Exception as e2:
735
+ # On PowerShell, also try the alternate profile file name
736
+ if shell in {"powershell", "pwsh"}:
737
+ try:
738
+ alt_name = (
739
+ "Microsoft.PowerShell_profile.ps1"
740
+ if config_path.name.lower() == "profile.ps1"
741
+ else "profile.ps1"
742
+ )
743
+ alt_path = config_path.with_name(alt_name)
744
+ alt_path.parent.mkdir(parents=True, exist_ok=True)
745
+ try:
746
+ # Clear read-only if exists
747
+ if alt_path.exists():
748
+ try:
749
+ os.chmod(alt_path, stat.S_IWRITE | stat.S_IREAD)
750
+ except Exception:
751
+ pass
752
+ # If alternate exists and already contains our block, we're done
753
+ try:
754
+ alt_text = alt_path.read_text(encoding="utf-8")
755
+ except Exception:
756
+ alt_text = ""
757
+ if MARK_START in alt_text and MARK_END in alt_text:
758
+ return
759
+ # Otherwise write combined content to alternate profile
760
+ alt_combined = alt_text + block if alt_text else block
761
+ alt_path.write_text(alt_combined, encoding="utf-8")
762
+ return
763
+ except Exception as e3:
764
+ raise EnvonError(
765
+ "Failed to update PowerShell profile. Tried both: "
766
+ f"{config_path} (error: {e2}) and {alt_path} (error: {e3}). "
767
+ "Close any editor locking the file, ensure it's not a directory or read-only, "
768
+ "or create the file manually and re-run."
769
+ ) from e3
770
+ except Exception:
771
+ # If building alt path failed, fall through to generic error
772
+ pass
773
+ # Generic failure if all fallbacks failed
774
+ raise EnvonError(
775
+ f"Failed to update shell profile at {config_path}: {e2}"
776
+ ) from e
777
+
778
+
779
+ def _managed_content_for_shell(shell: str) -> str:
780
+ """Build the content stored in the managed file, tagged with the package version.
781
+
782
+ Including the version allows us to detect when an upgrade may require refreshing
783
+ the managed file, while avoiding unnecessary rewrites.
784
+ """
785
+ body = emit_bootstrap(shell)
786
+ header = f"# envon managed bootstrap - version: {VENV_VERSION}\n"
787
+ return header + body
788
+
789
+
790
+ def _maybe_update_managed_current_shell(explicit_shell: str | None) -> None:
791
+ """If a managed bootstrap file exists for the current/detected shell, refresh it when outdated.
792
+
793
+ This runs silently on each invocation and only writes when the content differs,
794
+ so normal runs stay fast and side-effect free for already up-to-date installs.
795
+ """
796
+ try:
797
+ shell = detect_shell(explicit_shell)
798
+ managed = get_managed_bootstrap_path(shell)
799
+ if managed.exists():
800
+ desired = _managed_content_for_shell(
801
+ "bash" if shell == "bash" else
802
+ "zsh" if shell == "zsh" else
803
+ "sh" if shell == "sh" else
804
+ "fish" if shell == "fish" else
805
+ "nushell" if shell in {"nushell", "nu"} else
806
+ "powershell" if shell in {"powershell", "pwsh"} else
807
+ "csh" if shell in {"csh", "tcsh", "cshell"} else shell
808
+ )
809
+ try:
810
+ current = managed.read_text(encoding="utf-8")
811
+ except Exception:
812
+ current = ""
813
+ if current != desired:
814
+ _write_managed_if_changed(managed, desired)
815
+ except Exception:
816
+ # Never fail the main command due to a managed-file refresh issue
817
+ pass
818
+
819
+
820
+ def main(argv: list[str] | None = None) -> int:
821
+ ns = parse_args(argv or sys.argv[1:])
822
+ try:
823
+ # Opportunistic refresh of managed bootstrap (no-op if not installed)
824
+ _maybe_update_managed_current_shell(None)
825
+ if ns.install is not None:
826
+ result = install_bootstrap(ns.install)
827
+ print(result)
828
+ return 0
829
+ venv = resolve_target(ns.target)
830
+ if ns.print_path:
831
+ print(str(venv))
832
+ return 0
833
+ shell = detect_shell(ns.emit)
834
+ cmd = emit_activation(venv, shell)
835
+ print(cmd)
836
+ return 0
837
+ except EnvonError as e:
838
+ print(str(e), file=sys.stderr)
839
+ return 2
840
+
841
+
842
+ if __name__ == "__main__": # pragma: no cover
843
+ raise SystemExit(main())