shell-next 0.1.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.
- shell_next-0.1.0/.gitattributes +7 -0
- shell_next-0.1.0/.github/workflows/ci.yml +86 -0
- shell_next-0.1.0/.github/workflows/publish.yml +42 -0
- shell_next-0.1.0/.gitignore +18 -0
- shell_next-0.1.0/CHANGELOG.md +18 -0
- shell_next-0.1.0/LICENSE +21 -0
- shell_next-0.1.0/PKG-INFO +124 -0
- shell_next-0.1.0/README.md +98 -0
- shell_next-0.1.0/docs/development/architecture.md +60 -0
- shell_next-0.1.0/docs/development/release.md +32 -0
- shell_next-0.1.0/docs/development/specification.md +498 -0
- shell_next-0.1.0/docs/usage.md +148 -0
- shell_next-0.1.0/pyproject.toml +61 -0
- shell_next-0.1.0/scripts/__init__.py +0 -0
- shell_next-0.1.0/scripts/quality/__init__.py +0 -0
- shell_next-0.1.0/scripts/quality/check_source.py +66 -0
- shell_next-0.1.0/scripts/quality/rhel8.sh +14 -0
- shell_next-0.1.0/scripts/release/__init__.py +0 -0
- shell_next-0.1.0/scripts/release/check_wheel.py +40 -0
- shell_next-0.1.0/scripts/release/client.py +60 -0
- shell_next-0.1.0/scripts/release/github.py +76 -0
- shell_next-0.1.0/scripts/release/publish.py +105 -0
- shell_next-0.1.0/scripts/release/verify.py +35 -0
- shell_next-0.1.0/scripts/release/wheel_probe.py +32 -0
- shell_next-0.1.0/shell-next Development Specification.md +498 -0
- shell_next-0.1.0/shell-next-logo.png +0 -0
- shell_next-0.1.0/src/shell_next/__init__.py +71 -0
- shell_next-0.1.0/src/shell_next/backends/__init__.py +1 -0
- shell_next-0.1.0/src/shell_next/backends/bash/__init__.py +1 -0
- shell_next-0.1.0/src/shell_next/backends/bash/authentication.py +64 -0
- shell_next-0.1.0/src/shell_next/backends/bash/containment.py +34 -0
- shell_next-0.1.0/src/shell_next/backends/bash/password_channel.py +43 -0
- shell_next-0.1.0/src/shell_next/backends/bash/syntax.py +30 -0
- shell_next-0.1.0/src/shell_next/backends/cmd/__init__.py +1 -0
- shell_next-0.1.0/src/shell_next/backends/cmd/syntax.py +32 -0
- shell_next-0.1.0/src/shell_next/backends/mock/__init__.py +1 -0
- shell_next-0.1.0/src/shell_next/backends/mock/driver.py +198 -0
- shell_next-0.1.0/src/shell_next/backends/mock/scenario.py +131 -0
- shell_next-0.1.0/src/shell_next/backends/mock/session.py +151 -0
- shell_next-0.1.0/src/shell_next/backends/mock/state.py +25 -0
- shell_next-0.1.0/src/shell_next/backends/native/__init__.py +1 -0
- shell_next-0.1.0/src/shell_next/backends/native/bridge.py +65 -0
- shell_next-0.1.0/src/shell_next/backends/native/channels.py +116 -0
- shell_next-0.1.0/src/shell_next/backends/native/containment.py +17 -0
- shell_next-0.1.0/src/shell_next/backends/native/driver.py +215 -0
- shell_next-0.1.0/src/shell_next/backends/native/preparation.py +85 -0
- shell_next-0.1.0/src/shell_next/backends/native/process.py +184 -0
- shell_next-0.1.0/src/shell_next/backends/native/syntax.py +66 -0
- shell_next-0.1.0/src/shell_next/backends/native/termination.py +42 -0
- shell_next-0.1.0/src/shell_next/backends/powershell/__init__.py +1 -0
- shell_next-0.1.0/src/shell_next/backends/powershell/driver.ps1 +22 -0
- shell_next-0.1.0/src/shell_next/backends/powershell/syntax.py +86 -0
- shell_next-0.1.0/src/shell_next/backends/protocol.py +69 -0
- shell_next-0.1.0/src/shell_next/backends/windows/__init__.py +1 -0
- shell_next-0.1.0/src/shell_next/backends/windows/containment.py +83 -0
- shell_next-0.1.0/src/shell_next/backends/windows/limits.py +41 -0
- shell_next-0.1.0/src/shell_next/errors.py +142 -0
- shell_next-0.1.0/src/shell_next/frontend/__init__.py +1 -0
- shell_next-0.1.0/src/shell_next/frontend/capture.py +115 -0
- shell_next-0.1.0/src/shell_next/frontend/execution.py +199 -0
- shell_next-0.1.0/src/shell_next/frontend/finalization.py +51 -0
- shell_next-0.1.0/src/shell_next/frontend/handle.py +228 -0
- shell_next-0.1.0/src/shell_next/frontend/lease.py +40 -0
- shell_next-0.1.0/src/shell_next/frontend/observation.py +35 -0
- shell_next-0.1.0/src/shell_next/frontend/operations.py +78 -0
- shell_next-0.1.0/src/shell_next/frontend/output.py +123 -0
- shell_next-0.1.0/src/shell_next/frontend/session.py +313 -0
- shell_next-0.1.0/src/shell_next/models/__init__.py +1 -0
- shell_next-0.1.0/src/shell_next/models/capabilities.py +74 -0
- shell_next-0.1.0/src/shell_next/models/commands.py +48 -0
- shell_next-0.1.0/src/shell_next/models/config.py +174 -0
- shell_next-0.1.0/src/shell_next/models/input.py +91 -0
- shell_next-0.1.0/src/shell_next/models/privilege.py +80 -0
- shell_next-0.1.0/src/shell_next/models/results.py +167 -0
- shell_next-0.1.0/src/shell_next/models/state.py +49 -0
- shell_next-0.1.0/src/shell_next/py.typed +0 -0
- shell_next-0.1.0/tests/__init__.py +0 -0
- shell_next-0.1.0/tests/backends/__init__.py +0 -0
- shell_next-0.1.0/tests/backends/bash/__init__.py +0 -0
- shell_next-0.1.0/tests/backends/bash/test_authentication.py +63 -0
- shell_next-0.1.0/tests/backends/bash/test_password_channel.py +78 -0
- shell_next-0.1.0/tests/backends/bash/test_sudo_integration.py +134 -0
- shell_next-0.1.0/tests/backends/mock/__init__.py +0 -0
- shell_next-0.1.0/tests/backends/mock/test_input_edges.py +53 -0
- shell_next-0.1.0/tests/backends/mock/test_privilege.py +83 -0
- shell_next-0.1.0/tests/backends/mock/test_session.py +175 -0
- shell_next-0.1.0/tests/backends/mock/test_state.py +52 -0
- shell_next-0.1.0/tests/backends/mock/test_virtual_observation.py +80 -0
- shell_next-0.1.0/tests/backends/native/__init__.py +0 -0
- shell_next-0.1.0/tests/backends/native/test_bridge.py +90 -0
- shell_next-0.1.0/tests/backends/native/test_channels.py +18 -0
- shell_next-0.1.0/tests/backends/native/test_interrupt_integration.py +51 -0
- shell_next-0.1.0/tests/backends/native/test_platform_containment.py +56 -0
- shell_next-0.1.0/tests/backends/native/test_process_failures.py +102 -0
- shell_next-0.1.0/tests/backends/native/test_resources_integration.py +80 -0
- shell_next-0.1.0/tests/backends/native/test_state_integration.py +68 -0
- shell_next-0.1.0/tests/backends/native/test_stdin_integration.py +25 -0
- shell_next-0.1.0/tests/backends/native/test_syntax.py +10 -0
- shell_next-0.1.0/tests/backends/native/test_termination.py +42 -0
- shell_next-0.1.0/tests/conftest.py +11 -0
- shell_next-0.1.0/tests/contracts/test_execution.py +147 -0
- shell_next-0.1.0/tests/contracts/test_lifecycle.py +103 -0
- shell_next-0.1.0/tests/contracts/test_queue_cancellation.py +47 -0
- shell_next-0.1.0/tests/frontend/__init__.py +0 -0
- shell_next-0.1.0/tests/frontend/test_capture.py +102 -0
- shell_next-0.1.0/tests/frontend/test_capture_failures.py +76 -0
- shell_next-0.1.0/tests/frontend/test_failures.py +192 -0
- shell_next-0.1.0/tests/frontend/test_lease_races.py +23 -0
- shell_next-0.1.0/tests/frontend/test_observation_edges.py +42 -0
- shell_next-0.1.0/tests/frontend/test_operations.py +39 -0
- shell_next-0.1.0/tests/models/__init__.py +0 -0
- shell_next-0.1.0/tests/models/test_values.py +120 -0
- shell_next-0.1.0/tests/support/__init__.py +0 -0
- shell_next-0.1.0/tests/support/native.py +80 -0
- shell_next-0.1.0/tests/support/sessions.py +29 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
name: quality
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
pull_request:
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
jobs:
|
|
9
|
+
contracts:
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
os: [ubuntu-latest, windows-latest]
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
timeout-minutes: 20
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: '3.14'
|
|
21
|
+
- run: python -m pip install -e ".[dev]"
|
|
22
|
+
- run: python -m ruff check .
|
|
23
|
+
- run: python -m ruff format --check .
|
|
24
|
+
- run: python -m mypy
|
|
25
|
+
- run: python scripts/quality/check_source.py
|
|
26
|
+
- name: Run platform contracts and collect partial coverage
|
|
27
|
+
run: python -m pytest --cov --cov-branch --cov-report=xml --cov-fail-under=0
|
|
28
|
+
- name: Verify installed wheel outside the source checkout
|
|
29
|
+
run: python -m scripts.release.check_wheel
|
|
30
|
+
- uses: actions/upload-artifact@v4
|
|
31
|
+
if: always()
|
|
32
|
+
with:
|
|
33
|
+
name: coverage-${{ matrix.os }}
|
|
34
|
+
path: .coverage
|
|
35
|
+
include-hidden-files: true
|
|
36
|
+
release-gates:
|
|
37
|
+
needs: [contracts, rhel8-reference]
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
- uses: actions/setup-python@v5
|
|
42
|
+
with:
|
|
43
|
+
python-version: '3.14'
|
|
44
|
+
- run: python -m pip install -e ".[dev]"
|
|
45
|
+
- uses: actions/download-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
pattern: coverage-*
|
|
48
|
+
path: reports/coverage
|
|
49
|
+
- run: python -m coverage combine reports/coverage/coverage-ubuntu-latest/.coverage reports/coverage/coverage-windows-latest/.coverage reports/coverage/coverage-rhel8/.coverage
|
|
50
|
+
- name: Enforce full combined branch coverage
|
|
51
|
+
run: python -m coverage report --fail-under=100
|
|
52
|
+
- run: python -m build
|
|
53
|
+
- run: python -m twine check dist/*
|
|
54
|
+
- uses: actions/upload-artifact@v4
|
|
55
|
+
with:
|
|
56
|
+
name: distributions
|
|
57
|
+
path: dist/*
|
|
58
|
+
rhel8-reference:
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
container: registry.access.redhat.com/ubi8/ubi:8.10
|
|
61
|
+
timeout-minutes: 40
|
|
62
|
+
steps:
|
|
63
|
+
- uses: actions/checkout@v4
|
|
64
|
+
- uses: actions/cache@v4
|
|
65
|
+
with:
|
|
66
|
+
path: /opt/python
|
|
67
|
+
key: rhel8-python-3.14.2-v1
|
|
68
|
+
- run: bash scripts/quality/rhel8.sh
|
|
69
|
+
- run: /opt/python/bin/python3.14 -m pip install -e ".[dev]"
|
|
70
|
+
- name: Configure disposable sudo contract account
|
|
71
|
+
run: |
|
|
72
|
+
useradd --create-home shellnext
|
|
73
|
+
echo 'shellnext:shell-next-disposable-ci-password' | chpasswd
|
|
74
|
+
printf 'Defaults:shellnext timestamp_type=global\nDefaults:shellnext passwd_tries=2\nshellnext ALL=(ALL) ALL\n' > /etc/sudoers.d/shell-next-contract
|
|
75
|
+
chmod 0440 /etc/sudoers.d/shell-next-contract
|
|
76
|
+
chown -R shellnext:shellnext "$GITHUB_WORKSPACE"
|
|
77
|
+
- name: Reference contracts and interactive sudo
|
|
78
|
+
env:
|
|
79
|
+
SHELL_NEXT_SUDO_PASSWORD: shell-next-disposable-ci-password
|
|
80
|
+
run: su -m -s /bin/bash shellnext -c '/opt/python/bin/python3.14 -m pytest --cov --cov-branch --cov-report=xml --cov-fail-under=0'
|
|
81
|
+
- uses: actions/upload-artifact@v4
|
|
82
|
+
if: always()
|
|
83
|
+
with:
|
|
84
|
+
name: coverage-rhel8
|
|
85
|
+
path: .coverage
|
|
86
|
+
include-hidden-files: true
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
on:
|
|
3
|
+
workflow_dispatch:
|
|
4
|
+
inputs:
|
|
5
|
+
version:
|
|
6
|
+
description: Exact package version to publish after all release gates pass
|
|
7
|
+
required: true
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
actions: read
|
|
11
|
+
jobs:
|
|
12
|
+
verify:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: '3.14'
|
|
19
|
+
- run: python -m pip install -e ".[dev]"
|
|
20
|
+
- name: Require completed release evidence for this commit
|
|
21
|
+
env:
|
|
22
|
+
GH_TOKEN: ${{ github.token }}
|
|
23
|
+
VERSION: ${{ inputs.version }}
|
|
24
|
+
run: python -m scripts.release.verify
|
|
25
|
+
- run: python -m build
|
|
26
|
+
- run: python -m twine check dist/*
|
|
27
|
+
- uses: actions/upload-artifact@v4
|
|
28
|
+
with:
|
|
29
|
+
name: verified-distributions
|
|
30
|
+
path: dist/*
|
|
31
|
+
pypi:
|
|
32
|
+
needs: verify
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
environment: pypi
|
|
35
|
+
permissions:
|
|
36
|
+
id-token: write
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/download-artifact@v4
|
|
39
|
+
with:
|
|
40
|
+
name: verified-distributions
|
|
41
|
+
path: dist
|
|
42
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Persistent Bash, PowerShell 7, and cmd sessions for Python 3.14+.
|
|
6
|
+
- Structural process arguments and native scripts with persistent shell state.
|
|
7
|
+
- Managed commands, scoped handles, FIFO queuing, independent observation deadlines,
|
|
8
|
+
and bounded interruption and cleanup.
|
|
9
|
+
- Manual and planned stdin, literal prompt matching, bounded byte capture,
|
|
10
|
+
optional durable files, and independent output subscribers.
|
|
11
|
+
- Interactive/noninteractive Bash sudo with separate authentication channels.
|
|
12
|
+
- Strict deterministic mock scenarios through `SessionConfig._session_cls`.
|
|
13
|
+
- RHEL 8, Linux, and Windows contracts, installed-wheel checks, static analysis,
|
|
14
|
+
and 100% combined branch coverage as publication gates.
|
|
15
|
+
|
|
16
|
+
Active Windows elevation, terminal emulation, and strict cleanup of privileged
|
|
17
|
+
descendants are unsupported. Forced interruption invalidates the persistent
|
|
18
|
+
session; callers create a new session explicitly.
|
shell_next-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 shell-next contributors
|
|
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.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: shell-next
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Persistent asynchronous shell sessions with deterministic test doubles
|
|
5
|
+
Project-URL: Repository, https://github.com/gokurakujoudo/shell-next
|
|
6
|
+
Project-URL: Issues, https://github.com/gokurakujoudo/shell-next/issues
|
|
7
|
+
Author: shell-next contributors
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.14
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: build>=1.4; extra == 'dev'
|
|
19
|
+
Requires-Dist: mypy>=1.19; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-asyncio>=1; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-cov>=7; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=9; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.15; extra == 'dev'
|
|
24
|
+
Requires-Dist: twine>=6; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# shell-next
|
|
28
|
+
|
|
29
|
+
Persistent asynchronous Bash, PowerShell, and cmd sessions for Python 3.14+.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from shell_next import Backend, ProcessCommand, SessionConfig, use_shell_session
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def inspect_repository(config: SessionConfig):
|
|
36
|
+
async with use_shell_session(config) as shell:
|
|
37
|
+
await shell.chdir("/srv/project")
|
|
38
|
+
result = await shell.run(ProcessCommand("git", ("status", "--short")), check=True)
|
|
39
|
+
return result.stdout.tail
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Use `ProcessCommand` for executable arguments that must remain structural. Use
|
|
43
|
+
`SessionScript` for backend-native scripts and persistent shell variables,
|
|
44
|
+
functions, and aliases. Shell syntax is never translated between languages.
|
|
45
|
+
|
|
46
|
+
Install with `python -m pip install shell-next`. Supply
|
|
47
|
+
`SessionConfig(backend=Backend.BASH)` on Linux, or select `Backend.POWERSHELL`
|
|
48
|
+
(PowerShell 7 installed) or `Backend.CMD` on Windows. Python 3.14+ is required.
|
|
49
|
+
|
|
50
|
+
## Interactive commands
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from shell_next import CommandOptions, StdinMode
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def answer_prompt(shell, program):
|
|
57
|
+
options = CommandOptions(stdin=StdinMode.MANUAL)
|
|
58
|
+
async with shell.command(program, options=options, timeout=30) as command:
|
|
59
|
+
await command.expect(b"Name: ")
|
|
60
|
+
await command.sendline(b"Ada")
|
|
61
|
+
await command.close_stdin()
|
|
62
|
+
return await command.wait()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`submit()` immediately registers a command and returns its handle. One session
|
|
66
|
+
executes one command at a time; the default policy rejects concurrent submissions.
|
|
67
|
+
`ConcurrencyPolicy.QUEUE` enables FIFO queuing. Different sessions run independently.
|
|
68
|
+
|
|
69
|
+
Cancelling `wait()` cancels observation only. Cancelling `run()` or escaping a
|
|
70
|
+
command context requests termination. A force-stopped shell becomes unusable;
|
|
71
|
+
create another session explicitly when required.
|
|
72
|
+
|
|
73
|
+
## Deterministic application tests
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from shell_next import Emit, MockExpectation, MockScenario, MockShellSession
|
|
77
|
+
|
|
78
|
+
scenario = MockScenario(
|
|
79
|
+
[
|
|
80
|
+
MockExpectation(ProcessCommand("git", ("status", "--short")), (Emit(b" M README.md\n"),)),
|
|
81
|
+
]
|
|
82
|
+
)
|
|
83
|
+
config = SessionConfig()
|
|
84
|
+
config._session_cls = MockShellSession.configured(scenario)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Pass this same configuration to application code using `use_shell_session`.
|
|
88
|
+
The mock shares production lifecycle code while keeping execution, state, input,
|
|
89
|
+
and output in memory. Unmatched commands and unconsumed strict expectations fail.
|
|
90
|
+
`Advance(60)` advances mock time without sleeping. Plain
|
|
91
|
+
`config._session_cls = MockShellSession` creates an empty strict scenario.
|
|
92
|
+
|
|
93
|
+
## Capture and capabilities
|
|
94
|
+
|
|
95
|
+
Output is bytes. Each stream retains a bounded tail (64 KiB by default), and
|
|
96
|
+
literal prompt matching uses a separate bounded window. Slow event subscribers
|
|
97
|
+
receive an overflow error without blocking primary capture. Opt into full capture
|
|
98
|
+
with `CaptureConfig(directory=existing_directory)`. Results distinguish received
|
|
99
|
+
bytes, durable file bytes, truncation, sealing, and possible unread output.
|
|
100
|
+
|
|
101
|
+
Inspect `shell.capabilities` before requesting backend-specific behavior.
|
|
102
|
+
Windows active elevation and terminal/PowerShell Host prompt automation are
|
|
103
|
+
unsupported. The library is a process-management API, **not a security sandbox**
|
|
104
|
+
for untrusted scripts.
|
|
105
|
+
|
|
106
|
+
See the [development specification](docs/development/specification.md),
|
|
107
|
+
[usage guide](docs/usage.md),
|
|
108
|
+
[architecture](docs/development/architecture.md), and
|
|
109
|
+
[release checklist](docs/development/release.md).
|
|
110
|
+
|
|
111
|
+
## Development
|
|
112
|
+
|
|
113
|
+
```console
|
|
114
|
+
python -m pip install -e ".[dev]"
|
|
115
|
+
python -m ruff check .
|
|
116
|
+
python -m mypy
|
|
117
|
+
python -m pytest --cov --cov-branch
|
|
118
|
+
python -m build
|
|
119
|
+
python -m twine check dist/*
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The package uses only the Python standard library at runtime. Tests that start
|
|
123
|
+
real shells are marked `integration`. Test files mirror production responsibilities;
|
|
124
|
+
reusable test infrastructure lives in `tests/support`.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# shell-next
|
|
2
|
+
|
|
3
|
+
Persistent asynchronous Bash, PowerShell, and cmd sessions for Python 3.14+.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from shell_next import Backend, ProcessCommand, SessionConfig, use_shell_session
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def inspect_repository(config: SessionConfig):
|
|
10
|
+
async with use_shell_session(config) as shell:
|
|
11
|
+
await shell.chdir("/srv/project")
|
|
12
|
+
result = await shell.run(ProcessCommand("git", ("status", "--short")), check=True)
|
|
13
|
+
return result.stdout.tail
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Use `ProcessCommand` for executable arguments that must remain structural. Use
|
|
17
|
+
`SessionScript` for backend-native scripts and persistent shell variables,
|
|
18
|
+
functions, and aliases. Shell syntax is never translated between languages.
|
|
19
|
+
|
|
20
|
+
Install with `python -m pip install shell-next`. Supply
|
|
21
|
+
`SessionConfig(backend=Backend.BASH)` on Linux, or select `Backend.POWERSHELL`
|
|
22
|
+
(PowerShell 7 installed) or `Backend.CMD` on Windows. Python 3.14+ is required.
|
|
23
|
+
|
|
24
|
+
## Interactive commands
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from shell_next import CommandOptions, StdinMode
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def answer_prompt(shell, program):
|
|
31
|
+
options = CommandOptions(stdin=StdinMode.MANUAL)
|
|
32
|
+
async with shell.command(program, options=options, timeout=30) as command:
|
|
33
|
+
await command.expect(b"Name: ")
|
|
34
|
+
await command.sendline(b"Ada")
|
|
35
|
+
await command.close_stdin()
|
|
36
|
+
return await command.wait()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`submit()` immediately registers a command and returns its handle. One session
|
|
40
|
+
executes one command at a time; the default policy rejects concurrent submissions.
|
|
41
|
+
`ConcurrencyPolicy.QUEUE` enables FIFO queuing. Different sessions run independently.
|
|
42
|
+
|
|
43
|
+
Cancelling `wait()` cancels observation only. Cancelling `run()` or escaping a
|
|
44
|
+
command context requests termination. A force-stopped shell becomes unusable;
|
|
45
|
+
create another session explicitly when required.
|
|
46
|
+
|
|
47
|
+
## Deterministic application tests
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from shell_next import Emit, MockExpectation, MockScenario, MockShellSession
|
|
51
|
+
|
|
52
|
+
scenario = MockScenario(
|
|
53
|
+
[
|
|
54
|
+
MockExpectation(ProcessCommand("git", ("status", "--short")), (Emit(b" M README.md\n"),)),
|
|
55
|
+
]
|
|
56
|
+
)
|
|
57
|
+
config = SessionConfig()
|
|
58
|
+
config._session_cls = MockShellSession.configured(scenario)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Pass this same configuration to application code using `use_shell_session`.
|
|
62
|
+
The mock shares production lifecycle code while keeping execution, state, input,
|
|
63
|
+
and output in memory. Unmatched commands and unconsumed strict expectations fail.
|
|
64
|
+
`Advance(60)` advances mock time without sleeping. Plain
|
|
65
|
+
`config._session_cls = MockShellSession` creates an empty strict scenario.
|
|
66
|
+
|
|
67
|
+
## Capture and capabilities
|
|
68
|
+
|
|
69
|
+
Output is bytes. Each stream retains a bounded tail (64 KiB by default), and
|
|
70
|
+
literal prompt matching uses a separate bounded window. Slow event subscribers
|
|
71
|
+
receive an overflow error without blocking primary capture. Opt into full capture
|
|
72
|
+
with `CaptureConfig(directory=existing_directory)`. Results distinguish received
|
|
73
|
+
bytes, durable file bytes, truncation, sealing, and possible unread output.
|
|
74
|
+
|
|
75
|
+
Inspect `shell.capabilities` before requesting backend-specific behavior.
|
|
76
|
+
Windows active elevation and terminal/PowerShell Host prompt automation are
|
|
77
|
+
unsupported. The library is a process-management API, **not a security sandbox**
|
|
78
|
+
for untrusted scripts.
|
|
79
|
+
|
|
80
|
+
See the [development specification](docs/development/specification.md),
|
|
81
|
+
[usage guide](docs/usage.md),
|
|
82
|
+
[architecture](docs/development/architecture.md), and
|
|
83
|
+
[release checklist](docs/development/release.md).
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
```console
|
|
88
|
+
python -m pip install -e ".[dev]"
|
|
89
|
+
python -m ruff check .
|
|
90
|
+
python -m mypy
|
|
91
|
+
python -m pytest --cov --cov-branch
|
|
92
|
+
python -m build
|
|
93
|
+
python -m twine check dist/*
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The package uses only the Python standard library at runtime. Tests that start
|
|
97
|
+
real shells are marked `integration`. Test files mirror production responsibilities;
|
|
98
|
+
reusable test infrastructure lives in `tests/support`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
The public entry point is `use_shell_session(SessionConfig(...))`. The factory
|
|
4
|
+
selects the implementation before entering the session, including the official
|
|
5
|
+
`config._session_cls` injection point. Application code should depend on this
|
|
6
|
+
interface instead of constructing native adapters.
|
|
7
|
+
|
|
8
|
+
## Dependency boundaries
|
|
9
|
+
|
|
10
|
+
| Responsibility | Modules | Tests |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| Value validation and immutable contracts | `models/{commands,config,capabilities,state,results,input,privilege}`; `errors` | `tests/models` |
|
|
13
|
+
| Session ownership and command lifecycle | `frontend/{session,execution,lease,handle,observation,operations}` | `tests/frontend`, `tests/contracts` |
|
|
14
|
+
| Bounded output and subscriptions | `frontend/{capture,output,finalization}` | `tests/frontend` |
|
|
15
|
+
| Native process and byte transports | `backends/native/{process,channels,containment,termination}` | `tests/backends/native` |
|
|
16
|
+
| Native command protocol | `backends/native/{driver,preparation,syntax,bridge}` | `tests/backends/native` |
|
|
17
|
+
| Bash syntax and sudo | `backends/bash/{syntax,containment,authentication,password_channel}` | `tests/backends/bash` |
|
|
18
|
+
| PowerShell and cmd syntax | `backends/powershell/{syntax,driver.ps1}`; `backends/cmd/syntax` | native and common contract tests |
|
|
19
|
+
| Windows process containment | `backends/windows/{containment,limits}` | `tests/backends/native/test_platform_containment.py` |
|
|
20
|
+
| Deterministic application double | `backends/mock/{session,driver,scenario}` | `tests/backends/mock`, `tests/contracts` |
|
|
21
|
+
|
|
22
|
+
Quality tools live in `scripts/quality`; packaging and release operations live in
|
|
23
|
+
`scripts/release`. Shared test fixtures belong in `tests/support`. Keep shell
|
|
24
|
+
language details in their backend instead of branching throughout the frontend.
|
|
25
|
+
|
|
26
|
+
Value models do not start processes or perform I/O. The common frontend owns
|
|
27
|
+
submission, queuing, cancellation, and final results. Drivers own transport
|
|
28
|
+
resources. Both drivers implement the same protocol and feed the same bounded
|
|
29
|
+
capture and input interfaces. Mock code cannot call native driver methods.
|
|
30
|
+
|
|
31
|
+
Each session owns one idle interpreter, one execution lease, and all its submitted
|
|
32
|
+
commands. Command-specific pipes isolate business input and output from shell
|
|
33
|
+
control messages. A bridge invokes structural processes with argv while inheriting
|
|
34
|
+
the shell's current directory and exported environment. Native scripts run in the
|
|
35
|
+
existing interpreter scope. Wrapper state uses the reserved `sn_`/`$sn_` namespace;
|
|
36
|
+
Bash reserves file descriptor 9 for status messages. Scripts that corrupt the
|
|
37
|
+
protocol invalidate the session rather than causing transparent restart.
|
|
38
|
+
|
|
39
|
+
Windows uses IOCP named pipes and a Job Object assigned before user commands.
|
|
40
|
+
POSIX uses private FIFOs and a new process group. Native shells are trusted:
|
|
41
|
+
programs that deliberately escape a process group are outside that containment
|
|
42
|
+
guarantee. Active Windows elevation is rejected.
|
|
43
|
+
|
|
44
|
+
Capture retains bounded tails and rolling match windows. Each file destination
|
|
45
|
+
has a dedicated single-worker executor; transport backpressure limits outstanding
|
|
46
|
+
writes. Subscriber queues are bounded and cannot backpressure primary capture.
|
|
47
|
+
File bytes are called durable only after flush and fsync complete.
|
|
48
|
+
|
|
49
|
+
Tests must mock external connectivity. Filesystem tests own a TemporaryDirectory.
|
|
50
|
+
Contract tests assert behavior through the public factory; unit tests exercise
|
|
51
|
+
failure boundaries with narrowly scoped fakes. Integration tests assert native
|
|
52
|
+
semantics and process cleanup instead of merely checking an echo command.
|
|
53
|
+
|
|
54
|
+
Production modules are limited to 200 code-bearing physical lines. Production
|
|
55
|
+
classes and functions use descriptive names without underscore prefixes, except
|
|
56
|
+
Python protocol dunders. Every function and value class uses English rST
|
|
57
|
+
documentation with parameter, return, and intentional-exception documentation.
|
|
58
|
+
Ruff, strict mypy, structural policy checks, and 100% combined branch coverage are
|
|
59
|
+
release gates. Platform-specific tests contribute coverage across the OS matrix;
|
|
60
|
+
coverage must not be lowered or platform implementations excluded to pass release.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# First release gates
|
|
2
|
+
|
|
3
|
+
No stable tag or PyPI publication is permitted until these gates are verified.
|
|
4
|
+
Candidate source metadata may carry the intended version while publication
|
|
5
|
+
remains blocked until all evidence passes for that exact commit.
|
|
6
|
+
|
|
7
|
+
- Common behavioral contracts pass for Mock, Bash, PowerShell 7, and cmd.
|
|
8
|
+
- Bash reference testing includes RHEL 8, persistent state, large simultaneous
|
|
9
|
+
streams, descendant cleanup, cancellation races, and ignored soft termination.
|
|
10
|
+
- Interactive sudo, cached credentials, wrong passwords, authentication deadlines,
|
|
11
|
+
separate business stdin, and strict privileged-cleanup rejection pass.
|
|
12
|
+
- PowerShell and cmd pass native script, structural process, persistent state,
|
|
13
|
+
timeout, cancellation, containment, and unsupported-elevation tests.
|
|
14
|
+
- Mock tests prohibit real subprocesses, filesystem writes, network access,
|
|
15
|
+
environment/directory mutation, and real sleeps.
|
|
16
|
+
- Ruff, strict mypy, production policy checks, and **100% branch coverage** pass.
|
|
17
|
+
- Repeated lifecycle stress tests detect no process, handle, task, or thread leaks.
|
|
18
|
+
- Wheel and sdist build, metadata checks, and installed-wheel smoke tests pass.
|
|
19
|
+
- GitHub release artifacts are built from the tested commit.
|
|
20
|
+
- PyPI publication uses a configured trusted publisher or the maintainer's existing
|
|
21
|
+
local PyPI credential configuration.
|
|
22
|
+
|
|
23
|
+
The optional GitHub publishing workflow uses an environment named `pypi` and
|
|
24
|
+
the PyPA trusted publisher action. Local publication uses Twine after the same
|
|
25
|
+
exact-commit verification. Credentials are never committed to the repository or
|
|
26
|
+
printed by release tooling. A draft release does not establish that gates passed.
|
|
27
|
+
|
|
28
|
+
For an authorized local release, start with a clean checkout of the tested commit
|
|
29
|
+
and run `python -m scripts.release.publish`. This verifies the complete successful
|
|
30
|
+
quality run, builds both distributions, checks metadata, stages GitHub assets,
|
|
31
|
+
publishes through the existing Twine configuration, and publishes the GitHub
|
|
32
|
+
release. An existing version or asset with different bytes is never overwritten.
|