eosh 0.2.0__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.
Files changed (112) hide show
  1. eosh-0.2.0/LICENSE +21 -0
  2. eosh-0.2.0/PKG-INFO +637 -0
  3. eosh-0.2.0/README.md +606 -0
  4. eosh-0.2.0/README.pypi.md +603 -0
  5. eosh-0.2.0/pyproject.toml +67 -0
  6. eosh-0.2.0/setup.cfg +4 -0
  7. eosh-0.2.0/src/eosh/__init__.py +34 -0
  8. eosh-0.2.0/src/eosh/__main__.py +23 -0
  9. eosh-0.2.0/src/eosh/_config.py +367 -0
  10. eosh-0.2.0/src/eosh/colors.py +63 -0
  11. eosh-0.2.0/src/eosh/commands.py +785 -0
  12. eosh-0.2.0/src/eosh/completion.py +952 -0
  13. eosh-0.2.0/src/eosh/completion_cache.py +77 -0
  14. eosh-0.2.0/src/eosh/context.py +189 -0
  15. eosh-0.2.0/src/eosh/decorators/__init__.py +263 -0
  16. eosh-0.2.0/src/eosh/decorators/bg.py +39 -0
  17. eosh-0.2.0/src/eosh/decorators/quiet.py +39 -0
  18. eosh-0.2.0/src/eosh/decorators/retry.py +46 -0
  19. eosh-0.2.0/src/eosh/decorators/time.py +44 -0
  20. eosh-0.2.0/src/eosh/decorators/watch.py +660 -0
  21. eosh-0.2.0/src/eosh/history.py +36 -0
  22. eosh-0.2.0/src/eosh/lineedit.py +1442 -0
  23. eosh-0.2.0/src/eosh/notify.py +404 -0
  24. eosh-0.2.0/src/eosh/parsing.py +226 -0
  25. eosh-0.2.0/src/eosh/paths.py +18 -0
  26. eosh-0.2.0/src/eosh/pipeline.py +668 -0
  27. eosh-0.2.0/src/eosh/process.py +465 -0
  28. eosh-0.2.0/src/eosh/prompt.py +77 -0
  29. eosh-0.2.0/src/eosh/recipes/__init__.py +208 -0
  30. eosh-0.2.0/src/eosh/recipes/_awsut_agentcore/__init__.py +46 -0
  31. eosh-0.2.0/src/eosh/recipes/_awsut_agentcore/harness.py +721 -0
  32. eosh-0.2.0/src/eosh/recipes/_awsut_agentcore/memory.py +1209 -0
  33. eosh-0.2.0/src/eosh/recipes/_awsut_agentcore/render.py +337 -0
  34. eosh-0.2.0/src/eosh/recipes/_awsut_common.py +533 -0
  35. eosh-0.2.0/src/eosh/recipes/_awsut_sagemaker/__init__.py +36 -0
  36. eosh-0.2.0/src/eosh/recipes/_awsut_sagemaker/hub.py +866 -0
  37. eosh-0.2.0/src/eosh/recipes/_awsut_sagemaker/hyperpod.py +1948 -0
  38. eosh-0.2.0/src/eosh/recipes/_awsut_sagemaker/jobs.py +1041 -0
  39. eosh-0.2.0/src/eosh/recipes/_awsut_sagemaker/render.py +300 -0
  40. eosh-0.2.0/src/eosh/recipes/_awsut_sagemaker/studio.py +1584 -0
  41. eosh-0.2.0/src/eosh/recipes/_missing.py +168 -0
  42. eosh-0.2.0/src/eosh/recipes/aws.py +239 -0
  43. eosh-0.2.0/src/eosh/recipes/awsut.py +1649 -0
  44. eosh-0.2.0/src/eosh/recipes/chmod.py +58 -0
  45. eosh-0.2.0/src/eosh/recipes/chown.py +117 -0
  46. eosh-0.2.0/src/eosh/recipes/cp.py +80 -0
  47. eosh-0.2.0/src/eosh/recipes/curl.py +108 -0
  48. eosh-0.2.0/src/eosh/recipes/df.py +33 -0
  49. eosh-0.2.0/src/eosh/recipes/du.py +82 -0
  50. eosh-0.2.0/src/eosh/recipes/find.py +60 -0
  51. eosh-0.2.0/src/eosh/recipes/git.py +295 -0
  52. eosh-0.2.0/src/eosh/recipes/grep.py +54 -0
  53. eosh-0.2.0/src/eosh/recipes/kill.py +92 -0
  54. eosh-0.2.0/src/eosh/recipes/ls.py +44 -0
  55. eosh-0.2.0/src/eosh/recipes/lsof.py +56 -0
  56. eosh-0.2.0/src/eosh/recipes/make.py +219 -0
  57. eosh-0.2.0/src/eosh/recipes/mv.py +52 -0
  58. eosh-0.2.0/src/eosh/recipes/ps.py +73 -0
  59. eosh-0.2.0/src/eosh/recipes/rm.py +55 -0
  60. eosh-0.2.0/src/eosh/recipes/rsync.py +143 -0
  61. eosh-0.2.0/src/eosh/recipes/scp.py +97 -0
  62. eosh-0.2.0/src/eosh/recipes/ssh.py +92 -0
  63. eosh-0.2.0/src/eosh/recipes/tail.py +23 -0
  64. eosh-0.2.0/src/eosh/recipes/tar.py +170 -0
  65. eosh-0.2.0/src/eosh/recipes/terraform.py +374 -0
  66. eosh-0.2.0/src/eosh/recipes/top.py +84 -0
  67. eosh-0.2.0/src/eosh/recipes/unzip.py +96 -0
  68. eosh-0.2.0/src/eosh/recipes/zip.py +53 -0
  69. eosh-0.2.0/src/eosh/scrollbar.py +122 -0
  70. eosh-0.2.0/src/eosh/shell.py +4253 -0
  71. eosh-0.2.0/src/eosh/terminal.py +446 -0
  72. eosh-0.2.0/src/eosh/tui.py +1082 -0
  73. eosh-0.2.0/src/eosh/variables.py +253 -0
  74. eosh-0.2.0/src/eosh.egg-info/PKG-INFO +637 -0
  75. eosh-0.2.0/src/eosh.egg-info/SOURCES.txt +110 -0
  76. eosh-0.2.0/src/eosh.egg-info/dependency_links.txt +1 -0
  77. eosh-0.2.0/src/eosh.egg-info/entry_points.txt +2 -0
  78. eosh-0.2.0/src/eosh.egg-info/requires.txt +8 -0
  79. eosh-0.2.0/src/eosh.egg-info/top_level.txt +1 -0
  80. eosh-0.2.0/tests/test_alias_expansion.py +143 -0
  81. eosh-0.2.0/tests/test_argcomplete_fallback.py +268 -0
  82. eosh-0.2.0/tests/test_aws_recipe.py +218 -0
  83. eosh-0.2.0/tests/test_awsut_credentials.py +171 -0
  84. eosh-0.2.0/tests/test_awsut_whoami.py +317 -0
  85. eosh-0.2.0/tests/test_cobra_completion.py +293 -0
  86. eosh-0.2.0/tests/test_commands.py +440 -0
  87. eosh-0.2.0/tests/test_completion.py +230 -0
  88. eosh-0.2.0/tests/test_completion_cache.py +92 -0
  89. eosh-0.2.0/tests/test_completion_picker.py +365 -0
  90. eosh-0.2.0/tests/test_context.py +291 -0
  91. eosh-0.2.0/tests/test_decorators.py +1060 -0
  92. eosh-0.2.0/tests/test_env_prefix.py +98 -0
  93. eosh-0.2.0/tests/test_history_completion.py +634 -0
  94. eosh-0.2.0/tests/test_lineedit_history.py +184 -0
  95. eosh-0.2.0/tests/test_make_recipe.py +233 -0
  96. eosh-0.2.0/tests/test_notify.py +500 -0
  97. eosh-0.2.0/tests/test_parsing.py +166 -0
  98. eosh-0.2.0/tests/test_passthrough_input.py +135 -0
  99. eosh-0.2.0/tests/test_piped_python_commands.py +251 -0
  100. eosh-0.2.0/tests/test_pipeline.py +200 -0
  101. eosh-0.2.0/tests/test_process.py +116 -0
  102. eosh-0.2.0/tests/test_recipe_missing.py +144 -0
  103. eosh-0.2.0/tests/test_recipes.py +246 -0
  104. eosh-0.2.0/tests/test_redirects.py +49 -0
  105. eosh-0.2.0/tests/test_sagemaker_recipe.py +2412 -0
  106. eosh-0.2.0/tests/test_scrollbar.py +128 -0
  107. eosh-0.2.0/tests/test_shell_continuation.py +74 -0
  108. eosh-0.2.0/tests/test_source_bash.py +230 -0
  109. eosh-0.2.0/tests/test_subcommands.py +270 -0
  110. eosh-0.2.0/tests/test_tar_recipe.py +142 -0
  111. eosh-0.2.0/tests/test_user_config.py +27 -0
  112. eosh-0.2.0/tests/test_variables.py +273 -0
eosh-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 craftware
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.
eosh-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,637 @@
1
+ Metadata-Version: 2.4
2
+ Name: eosh
3
+ Version: 0.2.0
4
+ Summary: A lightweight but powerful terminal shell with rich TAB completion and context switching
5
+ Author: craftware
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/crftwr/eosh
8
+ Project-URL: Repository, https://github.com/crftwr/eosh
9
+ Project-URL: Issues, https://github.com/crftwr/eosh/issues
10
+ Keywords: shell,terminal,completion,repl,aws
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX
17
+ Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: System :: Shells
23
+ Classifier: Topic :: Terminals
24
+ Requires-Python: >=3.12
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Provides-Extra: aws
28
+ Requires-Dist: boto3; extra == "aws"
29
+ Requires-Dist: pexpect; extra == "aws"
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8; extra == "dev"
32
+ Requires-Dist: eosh[aws]; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ <!-- Generated from README.md by scripts/gen_pypi_readme.py; relative
36
+ links rewritten against the v0.2.0 tag for PyPI. Do not edit
37
+ or commit. -->
38
+
39
+ # Eolith Shell
40
+
41
+ **Eolith Shell** (`eosh`) is a lightweight but powerful terminal shell environment with rich tab completion and context switching.
42
+
43
+
44
+ ![Eolith Shell demo: TAB completion with descriptions, the multi-select flag picker, and switching contexts while a build keeps running](https://raw.githubusercontent.com/crftwr/eosh/v0.2.0/doc/images/demo.gif)
45
+
46
+ ## Features
47
+
48
+ - **Rich tab completion** — per-argument completers with descriptions, inline picker UI
49
+ - **Multi-select flag picker** — TAB on flags opens a Space-to-toggle checkbox list
50
+ - **Context switching** — named environments with variables and working directories, with push/pop and `Ctrl+]` live switching
51
+ - **PTY process multiplexing** — run processes in contexts and switch between them without killing them
52
+ - **Pipelines and redirections** — `|`, `>`, `>>`, `<`, `2>`, `2>&1`, `;`, `&&`, `||`, globs (`*`, `?`, `**`), and `\`-line-continuation
53
+ - **Pipeline decorators** — wrap any pipeline with `@watch`, `@time`, `@retry`, `@quiet`, or `@bg`; authoring your own is a few lines of Python
54
+ - **Custom commands** — define Python functions as shell commands with full completion support
55
+ - **Python-backed variables** — `var aws_region=us-east-1` can drive multiple `os.environ` keys via a `Var` subclass; `$NAME` expansion is symmetric
56
+ - **Completion recipes** — opt-in TAB completion for `git`, `make`, `ssh`, `aws`, and more
57
+ - **Protocol fallbacks** — automatic completion for cobra-based tools (`docker`, `kubectl`, `helm`, `gh`, …) and argcomplete-based Python CLIs (`pipx`, `conda`, `pre-commit`, `tox`, …) — no recipe needed
58
+ - **System command fallback** — anything not a registered command runs through the system shell
59
+ - **Cross-platform** — interactive shell, completion, pipelines, redirects, contexts, and history all work on POSIX and Windows; PTY-backed multiplexing of running native processes is POSIX-only
60
+ - **History** — persistent history with up/down navigation, `Ctrl+R` search, and past command lines offered as multi-argument TAB candidates, scoped to the context and directory you're in
61
+ - **Desktop notifications** — an OS notification when a command that took more than 10 seconds finishes, including ones running in a backgrounded context; native backends only, no extra dependencies
62
+
63
+ ## Installation
64
+
65
+ Requires Python 3.12+. The core has no dependencies beyond the standard library.
66
+
67
+ ```bash
68
+ pip install eosh # or: pipx install eosh / uv tool install eosh
69
+ pip install "eosh[aws]" # + the `awsut` recipe (boto3, pexpect)
70
+ uv tool install "eosh[aws]" # same, as an isolated uv tool
71
+ ```
72
+
73
+ On first launch `eosh` writes a starter `~/.eosh/config.py` that enables
74
+ every built-in recipe. Without the `[aws]` extra, `awsut` is skipped and
75
+ everything else loads normally; running `awsut` then prints the install
76
+ command for the environment eosh lives in (`uv tool`, `pipx`, or pip).
77
+
78
+ `uv tool` and `pipx` install eosh into its own virtualenv, so a
79
+ `pip install boto3` in another shell does not reach it — add packages with
80
+ `uv tool install eosh --with <pkg>` or `pipx inject eosh <pkg>`. With
81
+ `uv tool`, `--with` *replaces* the previous list, so repeat any earlier
82
+ entries (`uv tool list --show-with` shows them).
83
+
84
+ ### From source
85
+
86
+ ```bash
87
+ git clone https://github.com/crftwr/eosh
88
+ cd eosh
89
+ make install # .venv/ with `pip install -e ".[dev]"`
90
+ make test
91
+ make run
92
+ ```
93
+
94
+ ## Usage
95
+
96
+ ```bash
97
+ eosh
98
+ ```
99
+
100
+ ### Built-in Commands
101
+
102
+ | Command | Description |
103
+ |---------|-------------|
104
+ | `cd [path]` | Change directory (default: home) |
105
+ | `help [command]` | Show help for a command or list all commands |
106
+ | `context` | Manage contexts (see below) |
107
+ | `var [NAME[=VALUE] ...]` | List variables, print one (`var NAME`), set (`var NAME=VALUE`), or unset (`var NAME=`) |
108
+ | `alias [NAME[=EXPANSION] ...]` | List aliases, show one, or define a shorthand for a command |
109
+ | `unalias NAME [NAME ...]` | Remove aliases |
110
+ | `source-bash [FILE [ARG ...]]` | Run a bash script (or a pasted block) and import its variables and cwd |
111
+ | `reload` | Reload `~/.eosh/config.py` without restarting |
112
+ | `exit` | Exit the shell |
113
+
114
+ Any command not listed above is passed through to the system shell (e.g., `ls`, `git`, `grep`).
115
+
116
+ ### Aliases
117
+
118
+ An alias replaces the first word of a command line. Quote the expansion when it contains spaces:
119
+
120
+ ```
121
+ eosh> alias hp='awsut sagemaker hyperpod'
122
+ eosh> hp list # runs: awsut sagemaker hyperpod list
123
+ eosh> alias # list all aliases
124
+ eosh> unalias hp
125
+ ```
126
+
127
+ ### Importing Variables from Bash
128
+
129
+ `source-bash` runs a script in a real `bash` — so `export`, `$(…)`, loops, heredocs and conditionals all work — and imports the environment and working directory it leaves behind, the way bash's own `source` would:
130
+
131
+ ```
132
+ eosh> source-bash # paste `export KEY=VALUE` lines, end with a blank line or Ctrl+D
133
+ eosh> source-bash setup.sh s3 # source a file with arguments
134
+ eosh> source-bash -c 'export A=$(date +%s)'
135
+ ```
136
+
137
+ `--no-cd` keeps the current directory and `-q` suppresses the summary of imported variable names. Shell functions, aliases and shell options can't be imported.
138
+
139
+ ### Contexts
140
+
141
+ Contexts let you define named environments with variables that are exported to `os.environ` and a remembered working directory.
142
+
143
+ ```
144
+ eosh> context push prod
145
+ Pushed context 'prod'
146
+ [prod] eosh> var ACCOUNT=123456 REGION=us-east-1
147
+ [prod] eosh> context push staging
148
+ Pushed context 'staging'
149
+ [staging] eosh> var ACCOUNT=789012 REGION=us-west-2
150
+ [staging] eosh> context pop
151
+ Popped 'staging', now in 'prod'
152
+ [prod] eosh> context list
153
+ * prod {'ACCOUNT': '123456', 'REGION': 'us-east-1'}
154
+ staging {'ACCOUNT': '789012', 'REGION': 'us-west-2'}
155
+ ```
156
+
157
+ Context subcommands:
158
+
159
+ | Subcommand | Description |
160
+ |------------|-------------|
161
+ | `context push <name>` | Create a new context (inheriting current vars) and switch to it |
162
+ | `context pop` | Return to the previous context and remove the current one |
163
+ | `context switch <name>` | Switch to an existing context without modifying the stack |
164
+ | `context list` | Show all contexts with their state and variables |
165
+ | `context kill <name>` | Send SIGTERM to the running process in a context |
166
+
167
+ ### Context Switching with Ctrl+]
168
+
169
+ Press `Ctrl+]` at the shell prompt (or while a process is running) to open a TUI context picker:
170
+
171
+ - Arrow keys or `Ctrl+P/N` to navigate
172
+ - **Enter** to switch to the selected context
173
+ - **Esc** / `Ctrl+C` to cancel
174
+ - Select `+ new context` to create a new one
175
+
176
+ If the target context has a running process, switching to it resumes that process immediately. The original process keeps running in the background — visible as `[bg:1]` in the prompt.
177
+
178
+ ### Tab Completion
179
+
180
+ Press TAB to complete:
181
+ - Command names (registered commands + system PATH executables)
182
+ - File/directory paths (default fallback)
183
+ - Custom per-argument completions defined by commands
184
+ - Past command lines from history (see below)
185
+
186
+ **History completion** — TAB also offers past command lines that start with
187
+ what you've typed so far. Only the part that would be *added* is listed, like
188
+ any other candidate, tagged `history` and shown first:
189
+
190
+ ```
191
+ eosh> git commit <TAB>
192
+ ┌────────────────────────────────────────────────┐
193
+ │ -m "fix typo" history │
194
+ │ --amend --no-edit history │
195
+ │ doc/ │
196
+ │ src/ │
197
+ └────────────────────────────────────────────────┘
198
+ ```
199
+
200
+ Accepting one inserts it at the cursor verbatim, so a single suggestion can fill
201
+ in several arguments at once. Matching is against the entire typed line —
202
+ including pipelines (`ls | grep fo<TAB>`) — and draws on the current context's
203
+ history, the same list `↑`/`↓` walks (`Ctrl+R` searches every context). A
204
+ history candidate is never inserted without being shown in the picker first, and
205
+ a unique ordinary completion still applies on the first TAB as before.
206
+
207
+ Candidates are also scoped to the **directory** you're in: eosh records where
208
+ each command was run (`~/.eosh/history.dirs`) and offers only the lines you ran
209
+ here, so another checkout's `make deploy` stays out of the way. Nothing matching
210
+ run here means no history rows — the picker just shows the ordinary candidates.
211
+ `↑`/`↓` and `Ctrl+R` are not directory-scoped, so lines from elsewhere are still
212
+ one key away.
213
+
214
+ **Flag completion** — when flags are available, TAB opens a multi-select checkbox picker:
215
+ - Navigate with arrows; **Space** toggles a flag; **Enter** confirms
216
+ - Type a letter to jump to the next flag starting with that letter
217
+ - Short boolean flags are combined: selecting `-a` and `-l` inserts `-al`
218
+ - Flags that take a value (e.g. `-d N`) insert `flag ` then open a value picker or show an inline hint
219
+
220
+ ### Pipelines, Redirects, and Sequencing
221
+
222
+ eosh supports the operators you'd expect from a POSIX shell:
223
+
224
+ | Operator | Meaning |
225
+ |----------|---------|
226
+ | `cmd1 \| cmd2` | Pipe stdout of `cmd1` into stdin of `cmd2` |
227
+ | `cmd > file` / `>> file` | Redirect stdout (truncate / append) |
228
+ | `cmd < file` | Redirect stdin from a file |
229
+ | `cmd 2> file` / `2>> file` | Redirect stderr |
230
+ | `cmd 2>&1` | Merge stderr into stdout |
231
+ | `cmd1 ; cmd2` | Sequence (run both regardless of exit code) |
232
+ | `cmd1 && cmd2` | Run `cmd2` only if `cmd1` succeeded |
233
+ | `cmd1 \|\| cmd2` | Run `cmd2` only if `cmd1` failed |
234
+ | `*`, `?`, `**` | Glob expansion (recursive `**` supported) |
235
+ | `\` at end of line | Continue command on the next line (one history entry) |
236
+
237
+ ```
238
+ eosh> ls *.py | grep test | wc -l
239
+ eosh> make 2>&1 | tee build.log
240
+ eosh> echo hello > out.txt && cat out.txt
241
+ ```
242
+
243
+ Both registered Python commands and external programs work seamlessly inside pipelines.
244
+
245
+ ### Pipeline Decorators
246
+
247
+ A **decorator** is a token of the form `@name [flags]` at the start of a line that wraps the rest of the line as a pipeline and modifies how it runs. The leading `@` keeps the syntax visually distinct so it never collides with a regular command name.
248
+
249
+ ```
250
+ @watch ls # bare single-command body
251
+ @watch -n 1 {df -h | grep abc} # braced body required when operators appear
252
+ @time {make && ./run-tests}
253
+ @retry -n 5 --delay 2 curl https://flaky.example.com/
254
+ @quiet pytest -q
255
+ @bg {tail -f /var/log/system.log} # run in a fresh background context
256
+ ```
257
+
258
+ **Scope rule.** If the wrapped pipeline contains `|`, `;`, `&&`, `||`, or a redirect, it must be enclosed in `{...}`. Single-command bodies don't need braces. This makes the decorator's scope visible at a glance and side-steps the `watch -n 5 ls | grep abc` ambiguity that POSIX `watch` is famous for.
259
+
260
+ **Built-in decorators:**
261
+
262
+ | Decorator | Description |
263
+ |-----------|-------------|
264
+ | `@watch [-n SEC] [--no-clear]` | Repeatedly run a pipeline until interrupted |
265
+ | `@time` | Print elapsed wall/user/sys time after the pipeline finishes |
266
+ | `@retry [-n N] [--delay SEC]` | Re-run the pipeline on non-zero exit, up to `N` attempts |
267
+ | `@quiet [--stderr]` | Discard stdout (and stderr with `--stderr`); still propagates the exit code |
268
+ | `@bg [name]` | Run the pipeline in a fresh background context (resumable via `Ctrl+]`) |
269
+
270
+ See the [Custom Decorators](#custom-decorators) section below for authoring your own.
271
+
272
+ ## Customization
273
+
274
+ Create `~/.eosh/config.py` to define custom commands and completers. This file is plain Python that imports from eosh. Use `reload` to apply changes without restarting.
275
+
276
+ ```python
277
+ # ~/.eosh/config.py
278
+ from eosh.commands import registry, arg
279
+ from eosh.completion import Completer, Completion, ChoiceCompleter
280
+ from eosh.recipes import enable
281
+
282
+ # Enable TAB completion for system commands
283
+ enable("make", "git", "ssh")
284
+
285
+ class InstanceCompleter(Completer):
286
+ def complete(self, ctx):
287
+ account = ctx.args[0] if ctx.args else ctx.shell_context.get_variable("ACCOUNT")
288
+ # fetch instances for account...
289
+ return [Completion(value="i-abc123", description="web-server-1")]
290
+
291
+ @registry.command(
292
+ name="connect",
293
+ help="SSH into an EC2 instance.",
294
+ params=[
295
+ arg("account", choices=["prod", "staging"]),
296
+ arg("region", choices=["us-east-1", "us-west-2"]),
297
+ arg("instance_id", completer=InstanceCompleter()),
298
+ ],
299
+ )
300
+ def connect(account, region, instance_id):
301
+ import os
302
+ os.system(f"ssh {instance_id}")
303
+ ```
304
+
305
+ ### Python-Backed Variables
306
+
307
+ A `Var` subclass mirrors the `CommandRegistry` pattern: subclass `Var`, register an instance with `var_registry`, and the built-in `var` command (and bare `NAME=VALUE` assignment) dispatches through your class. `$NAME` / `${NAME}` expansion uses the same lookup, so a Python-backed variable is read- and write-symmetric with `os.environ`.
308
+
309
+ ```python
310
+ # ~/.eosh/config.py
311
+ import os
312
+ from eosh import Var, EnvVar, var_registry
313
+ from eosh.completion import ChoiceCompleter, CallbackCompleter
314
+
315
+ class AwsRegionVar(Var):
316
+ name = "aws_region"
317
+ description = "AWS region — sets AWS_REGION + AWS_DEFAULT_REGION"
318
+
319
+ def get(self):
320
+ return os.environ.get("AWS_REGION")
321
+
322
+ def set(self, value):
323
+ os.environ["AWS_REGION"] = value
324
+ os.environ["AWS_DEFAULT_REGION"] = value
325
+
326
+ @property
327
+ def value_completer(self):
328
+ return ChoiceCompleter(["us-east-1", "us-west-2", "eu-west-1"])
329
+
330
+ var_registry.register(AwsRegionVar())
331
+ var_registry.register(
332
+ EnvVar("aws_profile", "AWS_PROFILE",
333
+ completer=CallbackCompleter(lambda: ["default", "prod", "staging"]))
334
+ )
335
+ ```
336
+
337
+ Use `EnvVar(name, env_var, completer=...)` for a single-key passthrough; subclass `Var` directly when one logical name needs to drive multiple `os.environ` keys (or any other side effect). With the variables above:
338
+
339
+ ```
340
+ eosh> var aws_region=us-west-2
341
+ eosh> echo $AWS_REGION
342
+ us-west-2
343
+ eosh> aws ec2 describe-instances --region $aws_region
344
+ ```
345
+
346
+ ### Custom Decorators
347
+
348
+ To author your own decorator, decorate a function with `decorator_registry.decorator(...)`. The function receives the wrapped `Pipeline` as its first positional argument and the parsed flag namespace as kwargs; call `pipeline.run()` to execute the body and return the exit code.
349
+
350
+ ```python
351
+ # ~/.eosh/config.py
352
+ import sys
353
+ import time
354
+ from eosh.commands import arg
355
+ from eosh.decorators import registry as decorator_registry
356
+ from eosh.pipeline import Pipeline
357
+
358
+ @decorator_registry.decorator(
359
+ name="repeat",
360
+ help="Run the pipeline N times, stopping early on the first failure.",
361
+ params=[
362
+ arg("-n", "--count", type=int, default=3, metavar="N",
363
+ help="number of iterations (default 3)"),
364
+ arg("--delay", type=float, default=0.0, metavar="SEC",
365
+ help="seconds to sleep between iterations"),
366
+ ],
367
+ )
368
+ def repeat(pipeline: Pipeline, *, count: int, delay: float) -> int:
369
+ last = 0
370
+ for i in range(1, count + 1):
371
+ sys.stderr.write(f"@repeat: iteration {i}/{count}\n")
372
+ last = pipeline.run()
373
+ if last != 0:
374
+ return last
375
+ if delay > 0 and i < count:
376
+ time.sleep(delay)
377
+ return last
378
+ ```
379
+
380
+ Usage:
381
+
382
+ ```
383
+ eosh> @repeat -n 5 --delay 1 ls
384
+ eosh> @repeat -n 3 {make && ./run-tests}
385
+ ```
386
+
387
+ To share decorators across machines or teammates, drop a module under `~/.eosh/decorators/<name>.py` that defines `register()` (same shape as the built-ins) and call `enable()` from `config.py`:
388
+
389
+ ```python
390
+ # ~/.eosh/config.py
391
+ from eosh.decorators import add_decorator_path, enable as enable_decorators
392
+
393
+ add_decorator_path("/team/shared/decorators") # optional extra directory
394
+ enable_decorators("repeat") # found in ~/.eosh/decorators/
395
+ # or /team/shared/decorators/
396
+ ```
397
+
398
+ ### Spawning Interactive Subprocesses
399
+
400
+ If a custom command needs to spawn a subprocess that reads from the user (SSH-like sessions, TUIs, MFA prompts, anything that calls `getpass`), wrap the call with `passthrough_run` — *not* `subprocess.run`:
401
+
402
+ ```python
403
+ from eosh import passthrough_run
404
+
405
+ @registry.command(name="my_ssm", ...)
406
+ def my_ssm():
407
+ passthrough_run(["aws", "ssm", "start-session", "--target", "i-abc123"])
408
+ ```
409
+
410
+ Plain `subprocess.run` would have the main shell thread and the subprocess both reading from the real terminal, splitting keystrokes between them; `passthrough_run` allocates a slot-owned PTY for the child so input flows cleanly.
411
+
412
+ For reading a single line of input back from the user, use `passthrough_input(prompt)` instead of plain `input()`:
413
+
414
+ ```python
415
+ from eosh import passthrough_input
416
+
417
+ answer = passthrough_input("Continue? [y/N] ")
418
+ ```
419
+
420
+ Outside a Python command thread, both helpers fall back to the obvious thing (`subprocess.run` and `input`), so the same code is safe in either context. Non-interactive subprocesses (`capture_output=True`, pipeline stages with explicit pipes, `pexpect.popen_spawn`, …) don't need wrapping.
421
+
422
+ ### Desktop Notifications
423
+
424
+ When a command runs for at least 10 seconds, eosh posts an OS notification as it finishes — by then you've probably switched to a browser:
425
+
426
+ ```
427
+ ✓ eosh — 1m 23s make -j8 release
428
+ ✗ eosh — exit 2 (20.0s) make
429
+ ✓ eosh — 1h 05m [bg-1] terraform apply
430
+ ```
431
+
432
+ Commands running in a context you backgrounded with `Ctrl+]` (or with `@bg`) notify too, tagged with the context name. Interactive programs — editors, pagers, `top`, `ssh`, `tmux`, sub-shells — are skipped, since sitting in them for an hour isn't work finishing.
433
+
434
+ Backends are whatever the platform already ships: `osascript` on macOS, `notify-send` on Linux, a PowerShell toast on Windows, and the terminal bell as a fallback. Nothing to install.
435
+
436
+ Control it at the prompt:
437
+
438
+ ```
439
+ eosh> var notify=off # disable for this session
440
+ eosh> var notify_threshold=30 # only notify for commands ≥ 30s
441
+ ```
442
+
443
+ Or from your config:
444
+
445
+ ```python
446
+ # ~/.eosh/config.py
447
+ from eosh import notify
448
+
449
+ notify.configure(threshold=30)
450
+ notify.SKIP_COMMANDS.add("psql")
451
+
452
+ # Or replace the backend entirely — Slack, ntfy.sh, tmux display-message, ...
453
+ notify.set_notifier(lambda title, message: post_to_slack(f"{title}\n{message}"))
454
+ ```
455
+
456
+ See [doc/notifications.md](https://github.com/crftwr/eosh/blob/v0.2.0/doc/notifications.md) for the design and the known limits.
457
+
458
+ ### Prompt Customization
459
+
460
+ The prompt is generated by a Python function you can override with `set_prompt()`. The default prompt shows the context name (if not `"default"`), current directory (up to 2 levels), a timestamp, and `[bg:N]` when N other contexts have running processes:
461
+
462
+ ```
463
+ [prod] projects/eosh 14:32:07>
464
+ ```
465
+
466
+ To customize, define a function that takes a `ContextManager` and returns a string:
467
+
468
+ ```python
469
+ # ~/.eosh/config.py
470
+ import os
471
+ from datetime import datetime
472
+ from eosh import set_prompt
473
+
474
+ def my_prompt(context_manager):
475
+ ctx = context_manager.current()
476
+ prefix = f"({ctx.name}) " if ctx else ""
477
+ cwd = os.path.basename(os.getcwd()) or "/"
478
+ time = datetime.now().strftime("%H:%M")
479
+ return f"{prefix}{cwd} [{time}]$ "
480
+
481
+ set_prompt(my_prompt)
482
+ ```
483
+
484
+ The function is called each time the prompt is displayed, so it reflects dynamic state like the current directory, time, or context variables.
485
+
486
+ ### Available Completers
487
+
488
+ | Completer | Description |
489
+ |-----------|-------------|
490
+ | `ChoiceCompleter(items)` | Complete from a static list |
491
+ | `CallbackCompleter(func)` | Complete from a function's return value |
492
+ | `FileCompleter()` | Complete filesystem paths (files and directories) |
493
+ | `DirCompleter()` | Complete directory paths only |
494
+ | `OptionsCompleter(options, args)` | Complete flags with multi-select TUI; `args` declares value-taking flags |
495
+ | `ConditionalCompleter(mapping)` | Pick a sub-completer based on preceding args |
496
+ | `HistoryCompleter(history_fn, limit, ran_here_fn)` | Continue the typed line from past command lines (may span several arguments), scoped to the ones run in the cwd |
497
+
498
+ ### Completion Recipes
499
+
500
+ Built-in recipes add TAB completion for common system commands. Enable them in `~/.eosh/config.py`:
501
+
502
+ ```python
503
+ from eosh.recipes import enable
504
+ enable("git", "make", "ssh", "kill", "ls", "grep", "find", "du", "df", "tail", "aws")
505
+ ```
506
+
507
+ Each recipe registers flag completion (via `OptionsCompleter`) and positional completions (subcommands, files, branches, etc.) for the named command.
508
+
509
+ Two protocol fallbacks activate automatically — no recipe needed:
510
+
511
+ - **Cobra-based tools** (`docker`, `kubectl`, `helm`, `gh`, `argocd`, …) — `CobraCompleter` drives their `__complete` subcommand, including live resource enumeration (running containers, k8s resources, GitHub issues, …). See [doc/cobra-fallback.md](https://github.com/crftwr/eosh/blob/v0.2.0/doc/cobra-fallback.md).
512
+ - **argcomplete-based Python CLIs** (`pipx`, `conda`, `pre-commit`, `tox`, `pdm`, `httpie`, …) — `ArgcompleteCompleter` detects the `# PYTHON_ARGCOMPLETE_OK` marker and drives the argcomplete protocol. See [doc/argcomplete-fallback.md](https://github.com/crftwr/eosh/blob/v0.2.0/doc/argcomplete-fallback.md).
513
+
514
+ #### User-Defined Recipes
515
+
516
+ You can write your own recipes and place them in `~/.eosh/recipes/` (or any directory you add to the search path). `enable()` checks the search path automatically after the built-ins, so the call site in `config.py` is identical:
517
+
518
+ ```python
519
+ from eosh.recipes import enable
520
+ enable("git") # built-in
521
+ enable("my_tool") # found in ~/.eosh/recipes/my_tool.py
522
+ ```
523
+
524
+ A recipe file must define a `register()` function:
525
+
526
+ ```python
527
+ # ~/.eosh/recipes/my_tool.py
528
+ from eosh.commands import arg, registry
529
+ from eosh.completion import CallbackCompleter, ChoiceCompleter
530
+
531
+ def register():
532
+ registry.command(
533
+ "my-tool",
534
+ help="my-tool — deploy/rollback/status helper",
535
+ params=[
536
+ arg("subcommand", choices=["deploy", "rollback", "status"]),
537
+ arg("target", help="deploy target", completer=CallbackCompleter(_list_targets)),
538
+ arg("-v", "--verbose", action="store_true", help="verbose"),
539
+ arg("--dry-run", action="store_true", help="don't apply changes"),
540
+ ],
541
+ )
542
+
543
+ def _list_targets():
544
+ # Return dynamic values (cached, fetched from an API, etc.)
545
+ return ["web", "worker", "scheduler"]
546
+ ```
547
+
548
+ #### Recipe Search Path
549
+
550
+ The default search path contains only `~/.eosh/recipes/`. Call `add_recipe_path()` to add more directories — useful for sharing recipes across a team:
551
+
552
+ ```python
553
+ from eosh.recipes import add_recipe_path, enable
554
+
555
+ add_recipe_path("/team/shared/recipes") # checked after ~/.eosh/recipes/
556
+ enable("my_tool") # found in whichever directory contains my_tool.py first
557
+ ```
558
+
559
+ Lookup order for every `enable()` call:
560
+
561
+ 1. Built-in package (`eosh.recipes.<name>`) — always highest priority
562
+ 2. `~/.eosh/recipes/<name>.py` — personal recipes
563
+ 3. Additional paths in the order they were added via `add_recipe_path()`
564
+
565
+ You can also read or modify `recipe_search_path` directly (it is a plain `list[Path]`).
566
+
567
+ A recipe may import third-party packages, but they must be installed in
568
+ eosh's own environment (see [Installation](#installation) for `uv tool` /
569
+ `pipx`). When one is missing, `enable("*")` skips that recipe instead of
570
+ failing the whole config and registers a placeholder command under the
571
+ recipe's name (unless that name is already a command or an executable on
572
+ `PATH`); running it says which module is missing and how to install it.
573
+ Naming the recipe explicitly — `enable("my_tool")` — still raises.
574
+
575
+ ### Writing a Custom Completer
576
+
577
+ Subclass `Completer` and implement `complete()`. The `CompletionContext` gives you:
578
+
579
+ - `command` — the command being completed for
580
+ - `args` — previously completed arguments
581
+ - `arg_index` — which argument position is being completed
582
+ - `prefix` — partial text typed so far
583
+ - `shell_context` — the active context (access variables with `.get_variable()`)
584
+
585
+ To add completion to a system command without wrapping it, register a handler-less command — execution falls through to the real binary:
586
+
587
+ ```python
588
+ from eosh.commands import arg, registry
589
+ from eosh.completion import FileCompleter
590
+
591
+ registry.command(
592
+ "mytools",
593
+ help="my custom tool",
594
+ params=[
595
+ arg("file", nargs="*", completer=FileCompleter()),
596
+ arg("-v", "--verbose", action="store_true", help="verbose"),
597
+ arg("--output", metavar="FILE", help="output file"),
598
+ ],
599
+ )
600
+ ```
601
+
602
+ ### Key Bindings
603
+
604
+ | Key | Action |
605
+ |-----|--------|
606
+ | `Tab` | Open completion picker |
607
+ | `Ctrl+R` | Search history |
608
+ | `Ctrl+]` | Open context switcher |
609
+ | `↑` / `↓` or `Ctrl+P/N` | Navigate history |
610
+ | `Ctrl+A` / `Ctrl+E` | Move to start / end of line |
611
+ | `Ctrl+B` / `Ctrl+F` | Move one character left / right |
612
+ | `Alt+B` / `Alt+F` | Move one word left / right |
613
+ | `Ctrl+W` | Delete word before cursor |
614
+ | `Ctrl+K` | Delete to end of line |
615
+ | `Ctrl+U` | Delete to beginning of line |
616
+ | `Ctrl+L` | Clear screen |
617
+ | `Ctrl+D` | Exit (on empty line) |
618
+
619
+ ## File Locations
620
+
621
+ | Path | Purpose |
622
+ |------|---------|
623
+ | `~/.eosh/config.py` | User configuration |
624
+ | `~/.eosh/history` | Command history |
625
+ | `~/.eosh/history.dirs` | Directories each history line was run in (scopes history TAB candidates) |
626
+ | `~/.eosh/recipes/<name>.py` | User-defined completion recipes (loaded by `enable("<name>")`) |
627
+ | `~/.eosh/decorators/<name>.py` | User-defined pipeline decorators (loaded by `enable("<name>")`) |
628
+
629
+ ## Platform Support
630
+
631
+ The interactive shell — line editing, completion, all TUI pickers, history, pipelines, redirects, built-ins, decorators, and `Ctrl+]` context switching at the prompt — runs natively on both POSIX and Windows. Path separators are normalized to `/` on every platform (Git-Bash style) so a path can never be mistaken for a `\`-line-continuation.
632
+
633
+ The one POSIX-only feature is **PTY-backed multiplexing of a live external process**: backgrounding a *running* native program with `Ctrl+]` and resuming it later. On Windows, external commands run on the real console with inherited stdio.
634
+
635
+ ## License
636
+
637
+ MIT — see [LICENSE](https://github.com/crftwr/eosh/blob/v0.2.0/LICENSE).