Metadata-Version: 2.4
Name: pysh-shell
Version: 0.4.0
Summary: PySH — fast, Python-first universal interactive shell for Debian and Unix-like systems.
Project-URL: Homepage, https://github.com/SSobol77/pysh
Project-URL: Repository, https://github.com/SSobol77/pysh
Project-URL: Issues, https://github.com/SSobol77/pysh/issues
Author: Siergej Sobolewski
License-Expression: GPL-3.0-or-later
License-File: LICENSE
Keywords: cli,debian,python,shell,terminal
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: System :: Shells
Classifier: Topic :: Terminals
Requires-Python: >=3.13
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Description-Content-Type: text/markdown

<!--
SPDX-License-Identifier: GPL-3.0-or-later

Project: PySH - fast, Python-first universal interactive shell for Debian and Unix-like systems
File: README.md
Repository: https://github.com/SSobol77/pysh
PyPI: https://pypi.org/project/pysh-shell

Copyright (c) 2026 Siergej Sobolewski
Licensed under the GNU General Public License v3.0 or later.
See the LICENSE file in the project root for full license text.
-->

<p align="center">
  <img src="https://raw.githubusercontent.com/SSobol77/pysh/main/docs/img/pysh-icon-512.png" alt="PySH logo" width="160"/>
</p>

<h1 align="center">PySH</h1>

<p align="center">
  <strong>PySH — fast, Python-first universal interactive shell for Debian and Unix-like systems.</strong>
</p>

<p align="center">
  <a href="https://github.com/SSobol77/pysh/actions/workflows/ci.yml">
    <img src="https://github.com/SSobol77/pysh/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI status"/>
  </a>
  <a href="https://pypi.org/project/pysh-shell/">
    <img src="https://img.shields.io/pypi/v/pysh-shell?label=PyPI&logo=pypi&logoColor=white" alt="PyPI version"/>
  </a>
  <a href="https://pypi.org/project/pysh-shell/">
    <img src="https://img.shields.io/pypi/pyversions/pysh-shell?label=Python&logo=python&logoColor=white" alt="Supported Python versions"/>
  </a>
  <a href="https://github.com/SSobol77/pysh/blob/main/LICENSE">
    <img src="https://img.shields.io/github/license/SSobol77/pysh?label=License" alt="License"/>
  </a>
  <a href="https://github.com/SSobol77/pysh/releases">
    <img src="https://img.shields.io/github/v/release/SSobol77/pysh?label=Release&logo=github" alt="GitHub release"/>
  </a>
  <a href="https://github.com/SSobol77/pysh">
    <img src="https://img.shields.io/badge/platform-Debian%20%7C%20Unix--like-2f4f6f?logo=debian&logoColor=white" alt="Debian and Unix-like systems"/>
  </a>
  <a href="https://github.com/SSobol77/pysh">
    <img src="https://img.shields.io/badge/shell-Python--first-3776AB?logo=python&logoColor=white" alt="Python-first shell"/>
  </a>
</p>

---

**PySH — fast, Python-first universal interactive shell for Debian and Unix-like systems.**

PySH is a small, dependency-free interactive shell written in pure Python.
It is packaged as a regular PyPI distribution (`pysh-shell`), installs a
single console command (`pysh`), and is designed to feel familiar to anyone
used to a Bourne-style shell while remaining hackable from Python.

Current development version: **0.4.0**. PySH targets **Python 3.13+** and is
validated primarily on **Debian 13** and Unix-like systems.

---

## Features

- Interactive REPL with persistent history (`~/.pysh_history`).
- Bash-like reverse incremental search bound to **Ctrl+R** when GNU readline
  is available; degrades silently on other backends.
- Welcome banner and configurable prompt with user and CWD (`~` collapsed).
- Robust quote-aware parser:
  - Splits chains on `;`, `&&`, `||` only outside of quotes.
  - Splits pipelines on `|` only outside of quotes.
- Pipelines with correctly managed file-descriptor handover.
- Redirection: `<`, `>`, `>>`, `2>`, `2>>`, `&>`, `&>>`.
- **Command substitution**: `$(command)` and `` `command` ``. Quote-aware:
  evaluated inside double quotes, suppressed inside single quotes.
  Bounded by a 5-second timeout by default.
- Local variables (`NAME=value`) and exported environment variables
  (`export NAME=value`) with `$NAME` / `${NAME}` expansion.
- Aliases with sane defaults; `alias` and **`unalias`** builtins.
- **Migration layer for zsh/bash/sh**: `source_zsh <file>` preserves the
  existing alias importer, `source_zsh_profile <file>` and
  `source_sh_aliases <file>` statically import simple aliases, exports and
  assignments without executing profile code, `run_script <file> [args...]`
  delegates shebang scripts to their real interpreter, and `compat_check
  <file>` reports migration risk before execution.
- **Zsh Transition Layer** for explicit delegation: `zsh <command>` delegates
  to real zsh when installed, and `zsh_fallback` can be enabled for
  controlled fallback experiments.
- **Python-native runtime bridge**: `py <code>` executes one-line Python code
  in a persistent per-session runtime context.
- **Python automation blocks**: `py { ... }` runs a multiline Python block in
  the same persistent runtime context as the one-line `py` form.
- **Debian/system profile helpers**: `sys_info`, `env_audit`, `path_audit`,
  `which_all`, `apt_check`, `apt_search` — non-mutating, never call `sudo`.
- **Command planning**: `plan <command...>` previews how PySH would classify
  and execute a command without running it.
- Startup file `~/.pyshrc` plus a **plugin directory** at `~/.pyshrc.d/`
  whose `*.pysh` files load in deterministic lexicographic order.
- **Mini rc-interpreter** for control flow inside `~/.pyshrc` and plugins:
  `if`/`else`/`fi`, `for`/`do`/`done`, `while`/`do`/`done` (with a hard
  iteration safety limit).
- **Directory stack**: `pushd`, `popd`, `dirs`.
- **`svc` builtin** for PyInit-style service control by PID file:
  `svc list`, `svc status`, `svc stop`, `svc restart`, `svc start`.
- **PyInit service metadata** parser with dependency tracking
  (`depends: [network]`).
- Safe ANSI color helpers that respect `NO_COLOR` and `TERM=dumb`, used for
  the banner and diagnostics. The input line itself is left untouched so
  editing remains stable.
- Basic tab completion for aliases, builtins, files and directories.
- Clean Ctrl+C (cancels current line, keeps the shell alive) and Ctrl+D
  (exits the shell).

---

## Installation

### From PyPI

```bash
pip install pysh-shell
```

Then start the shell with:

```bash
pysh
```

### Quick start

```sh
pysh
pysh --version
pysh -c "echo hi"
alias ll='ls -lah'
source_zsh_profile ~/.zshrc
source_sh_aliases ~/.bash_aliases
compat_check ~/scripts/maintenance.sh
run_script ~/scripts/maintenance.sh --dry-run
py import platform; print(platform.platform())
```

### Development install

```bash
python3.13 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
```

### Running

```bash
pysh           # console entry point installed by the wheel
python -m pysh # equivalent module entry point
pysh --version # print version and exit
pysh -c "echo hi; echo there"  # run one command line and exit
```

---

## Documentation

Full documentation lives under the repository [`docs/`](https://github.com/SSobol77/pysh/tree/main/docs):

- [Installation](https://github.com/SSobol77/pysh/blob/main/docs/installation.md) — installing from PyPI and dev install.
- [Usage](https://github.com/SSobol77/pysh/blob/main/docs/usage.md) — invocation, operators, pipelines, redirection, command substitution, variables, builtins.
- [Builtins](https://github.com/SSobol77/pysh/blob/main/docs/builtins.md) — syntax, examples, return behavior and limitations for every builtin.
- [Operators](https://github.com/SSobol77/pysh/blob/main/docs/operators.md) — chains, pipelines, redirection, command substitution, quoting and parser limitations.
- [Configuration](https://github.com/SSobol77/pysh/blob/main/docs/configuration.md) — `~/.pyshrc`, plugins under `~/.pyshrc.d/`, aliases, exports, prompt behavior.
- [Migration](https://github.com/SSobol77/pysh/blob/main/docs/migration.md) — static profile import, script transition runner, and compatibility reporting.
- [Zsh compatibility](https://github.com/SSobol77/pysh/blob/main/docs/zsh-compatibility.md) — transition bridge, safe profile import, explicit zsh delegation, optional fallback mode.
- [Python runtime](https://github.com/SSobol77/pysh/blob/main/docs/python-runtime.md) — persistent Python-native `py` execution context.
- [Development](https://github.com/SSobol77/pysh/blob/main/docs/development.md) — running the test suite, linting, building artifacts, repository layout.
- [Release process](https://github.com/SSobol77/pysh/blob/main/docs/release.md) — how PySH ships via GitHub Actions and PyPI Trusted Publishing.
- [System profile](https://github.com/SSobol77/pysh/blob/main/docs/system-profile.md) — `sys_info`, `env_audit`, `path_audit`, `which_all`, `apt_check`, `apt_search`.
- [Command planning](https://github.com/SSobol77/pysh/blob/main/docs/command-planning.md) — `plan <command...>`, the advisory classifier.
- [Packaging](https://github.com/SSobol77/pysh/blob/main/docs/packaging.md) — PyPI / `.deb` / `.rpm` artifact naming contract and build scripts.
- [Limitations](https://github.com/SSobol77/pysh/blob/main/docs/limitations.md) — explicit non-goals and compatibility boundaries.
- [Documentation policy](https://github.com/SSobol77/pysh/blob/main/docs/documentation-policy.md) — required coverage for new commands, parser behavior, migration helpers and limitations.

---

## Builtins

Available shell builtins. Most run inside the shell process; transition
builtins such as `zsh` and `run_script` may delegate explicitly as
documented.

| Builtin    | Description                                              |
| ---------- | -------------------------------------------------------- |
| `cd`       | Change the current working directory.                    |
| `pwd`      | Print the current working directory.                     |
| `alias`    | Define or display aliases.                               |
| `unalias`  | Remove one or more aliases.                              |
| `export`   | Define or display exported environment vars.             |
| `source`   | Execute commands from a file (also `.`).                 |
| `source_zsh` | Safely import simple aliases from a zsh-compatible file. |
| `source_zsh_profile` | Statically import simple zsh profile entries. |
| `source_sh_aliases` | Statically import simple sh/bash aliases and vars. |
| `run_script` | Run a script through a shebang interpreter or native PySH lines. |
| `compat_check` | Produce a static migration report for a shell file. |
| `zsh`      | Execute one command through real `zsh -lc`.              |
| `zsh_fallback` | Enable or disable explicit zsh fallback mode.       |
| `py`       | Execute Python code in the persistent PySH runtime.      |
| `sys_info` | Print platform / Python / user / shell / PATH summary.   |
| `env_audit` | Print a redacted environment audit summary.             |
| `path_audit` | Report missing / duplicate / non-directory PATH entries. |
| `which_all` | Print every executable match for a command in PATH.     |
| `apt_check` | Run `apt list --upgradable` (Debian helper, no sudo).   |
| `apt_search` | Run `apt search <query>` (Debian helper, no sudo).     |
| `plan`     | Preview classification/execution of a command, advisory. |
| `pushd`    | Push CWD onto the directory stack and `cd` to a path.    |
| `popd`     | Pop the directory stack and `cd` to the popped entry.    |
| `dirs`     | Print the current directory followed by the stack.       |
| `svc`      | Query / signal PyInit services. See [svc / PyInit](#svc--pyinit). |
| `exit`     | Exit the shell with an optional status code.             |
| `quit`     | Same as `exit`.                                          |

---

## Operators

| Operator         | Meaning                                                |
| ---------------- | ------------------------------------------------------ |
| `cmd1 ; cmd2`    | Run `cmd1`, then unconditionally run `cmd2`.           |
| `cmd1 && cmd2`   | Run `cmd2` only if `cmd1` exits with status 0.         |
| `cmd1 \|\| cmd2` | Run `cmd2` only if `cmd1` exits with non-zero status.  |
| `cmd1 \| cmd2`   | Pipe `cmd1`'s stdout into `cmd2`'s stdin.              |

Operators inside single or double quotes are treated as literal text.

```sh
echo "🐍 PySH v0.4.0 | Python 3.13.5"
echo "Test | pipe & semicolon; && ok"
python3.13 -c "import subprocess; print('ok')"
```

---

## Redirection

| Syntax       | Effect                                  |
| ------------ | --------------------------------------- |
| `< file`     | Read stdin from `file`.                 |
| `> file`     | Write stdout to `file` (truncate).      |
| `>> file`    | Write stdout to `file` (append).        |
| `2> file`    | Write stderr to `file` (truncate).      |
| `2>> file`   | Write stderr to `file` (append).        |
| `&> file`    | Write stdout + stderr to `file`.        |
| `&>> file`   | Append stdout + stderr to `file`.       |

```sh
ls -la 2>/dev/null | head -3
python3.13 -c "import sys; print('err', file=sys.stderr)" 2> err.log
echo "hello" > out.txt
echo "again" >> out.txt
```

Redirection operators inside quotes are kept as literal characters.

---

## Pipelines

PySH connects each stage with a real OS pipe and closes the parent's
duplicate handles after the child is spawned, so neither side deadlocks.

```sh
ls -la | head -3
apt list --upgradable 2>/dev/null | grep -c "/"
```

---

## Command substitution

```sh
echo "Kernel: `uname -r`"
echo "Date: $(date '+%Y-%m-%d')"
echo 'No substitution: $(date)'
```

- `$(...)` and `` `...` `` are both supported.
- Quotes are honoured: substitutions inside single quotes are kept
  literally; substitutions inside double quotes are evaluated.
- Each substitution runs with a 5-second timeout by default. On timeout or
  failure the substitution expands to an empty string and the shell
  remains usable.

---

## Variables

```sh
NAME=world           # local shell variable
export GREETING=hi   # exported environment variable
echo "$GREETING, $NAME"
```

Local variables shadow environment variables when expanded. Single quotes
suppress expansion; double quotes do not.

---

## Zsh Transition Layer

PySH is Python-first, not a full zsh clone. The zsh compatibility bridge is
for transition: it lets users move aliases and selected legacy commands into
PySH without pretending that every zsh grammar feature is native.

```sh
source_zsh ~/.zsh_aliases
source_zsh_profile ~/.zshrc
source_sh_aliases ~/.bash_aliases
compat_check ~/scripts/maintenance.sh
run_script ~/scripts/maintenance.sh --dry-run
zsh 'source ~/.zshrc; my_old_alias'
zsh 'print -r -- hello'
```

`source_zsh <file>` statically imports supported simple alias definitions
such as `alias ll='ls -lah'`, ignores comments and unsupported constructs,
and never executes the file as code. It prints `imported=N skipped=M
file=<path>` and reports malformed alias lines deterministically on stderr.

`source_zsh_profile <file>` and `source_sh_aliases <file>` use the 0.3.0
static profile importer. They read files such as `~/.zshrc`, `.profile` and
`.bash_aliases` without executing them, import supported simple aliases,
exports and local assignments, and print `aliases=N exports=N vars=N
skipped=M file=<path>`.

`compat_check <file>` produces a static report with `supported`,
`delegated`, `skipped` and `risky` counts. Risky constructs such as `eval`,
command substitution, `source` and shell functions cause exit status 2.

`run_script <file> [args...]` is an explicit transition runner. Scripts with
`zsh`, `bash` or `sh` shebangs are delegated to the real interpreter through
an argv list; no-shebang scripts are executed line-by-line by PySH's native
engine where possible.

`zsh <command>` delegates explicitly to real `zsh -lc <command>`. If zsh is
not installed, it returns 127 with a deterministic error.

Fallback mode is off by default. It can be enabled only explicitly:

```sh
zsh_fallback on
zsh_fallback off
PYSH_ZSH_FALLBACK=1
```

When fallback is on, PySH may delegate commands it cannot parse or execute
natively to zsh. Builtins already handled by PySH stay native, and native
command failures are not hidden.

---

## Python Runtime

The `py` builtin executes one-line Python code in a persistent runtime context
owned by the current PySH session:

```sh
py print("hello from python")
py import platform; print(platform.platform())
py from pathlib import Path; print(Path(".").resolve())
```

Variables and imports persist across `py` invocations:

```sh
py x = 10
py print(x)
py import pathlib
py print(pathlib.Path(".").exists())
```

Exceptions are printed to stderr and return non-zero without terminating the
shell.

### Multiline Python automation blocks

```sh
py {
    import os
    targets = [p for p in os.environ.get("PATH", "").split(":") if p]
    print(f"PATH entries: {len(targets)}")
}
```

The opener line is exactly `py {`, the closer is a line that contains only
`}`. Block bodies execute in the same persistent runtime context as one-line
`py` invocations, so variables and imports flow in both directions.
Unterminated blocks return non-zero in script/source mode; nested
`py { ... }` blocks are rejected deterministically.

---

## System profile helpers

PySH 0.3.0 adds a small, non-mutating Debian/system profile layer. None of
these helpers call `sudo` or modify system state.

```sh
sys_info                # platform, Python, user, shell, PATH count
env_audit               # safe env audit with secret redaction
path_audit              # report missing / duplicate / non-directory entries
which_all python3       # all executables for "python3" along PATH
apt_check               # apt list --upgradable
apt_search vim          # apt search vim
```

Variables whose name contains `KEY`, `TOKEN`, `SECRET`, `PASSWORD`, `PASS`,
`CREDENTIAL`, or `AUTH` are replaced with `<redacted>` in `env_audit`. See
[docs/system-profile.md](https://github.com/SSobol77/pysh/blob/main/docs/system-profile.md).

---

## Command planning

`plan <command...>` previews how PySH would classify and execute a command
without running it. It is advisory only — there is no policy enforcement.

```sh
plan ls -la
plan echo a && echo b
plan sudo apt update
plan py print("x")
```

The output prints `original=`, `kind=`, `execution=`, `risk=` and `reason=`
fields. See
[docs/command-planning.md](https://github.com/SSobol77/pysh/blob/main/docs/command-planning.md).

---

## `~/.pyshrc` and `~/.pyshrc.d/`

At startup PySH first executes `~/.pyshrc` (if present), then every file in
`~/.pyshrc.d/` whose name ends with `.pysh`. Plugins are loaded in
deterministic lexicographic order so prefix numbering (`10-…`, `20-…`)
gives predictable layering.

You can also re-source any file at any time:

```sh
source ~/.pyshrc
```

A failing line is reported on stderr and the next line is still executed.
A broken plugin does not prevent later plugins from loading.

### Example `~/.pyshrc`

```sh
export EDITOR="nano"
export PAGER="less"
export LANG="pl_PL.UTF-8"
export PYTHONDONTWRITEBYTECODE="1"

alias rm="rm -i"
alias cp="cp -i"
alias mv="mv -i"
alias python="python3.13"
alias pip="pip3.13"

if [ -d /opt/local/bin ]; then
    export PATH="/opt/local/bin:$PATH"
fi

for dir in ~/bin ~/.local/bin; do
    if [ -d "$dir" ]; then
        export PATH="$dir:$PATH"
    fi
done

echo "🐍 PySH 0.4.0 | Python 3.13+"
echo "💡 Operators: && || ; | > >> < 2> 2>> &> &>>  + \$() and backticks"
```

### Example plugin `~/.pyshrc.d/10-aliases.pysh`

```sh
# Loaded after ~/.pyshrc, in lexicographic order.
alias gs="git status -sb"
alias gd="git diff"
alias gl="git log --oneline --decorate"

if [ -f ~/.work_aliases ]; then
    source ~/.work_aliases
fi
```

### Mini rc-interpreter cheat sheet

| Construct                                  | Notes                                |
| ------------------------------------------ | ------------------------------------ |
| `if [ <cond> ]; then ... fi`               | `else` block optional                |
| `for VAR in a b c; do ... done`            | Iterates literal words               |
| `while [ <cond> ]; do ... done`            | Bounded by a safety iteration limit  |

The canonical else keyword is `else`. The form `else:` is accepted as a
compatibility alias.

Supported test operators:

| Test                  | Meaning                              |
| --------------------- | ------------------------------------ |
| `[ -f path ]`         | path exists and is a regular file    |
| `[ -d path ]`         | path exists and is a directory       |
| `[ -e path ]`         | path exists                          |
| `[ -z "$VAR" ]`       | `$VAR` is empty                      |
| `[ -n "$VAR" ]`       | `$VAR` is non-empty                  |
| `[ "$A" = "$B" ]`     | string equality                      |
| `[ "$A" == "$B" ]`    | string equality (alias)              |
| `[ "$A" != "$B" ]`    | string inequality                    |
| `[ ! -f path ]` etc.  | negate any of the above              |

---

## Directory stack

```sh
pushd /tmp
pushd /var/log
dirs
popd
popd
```

`pushd path` pushes the current directory onto the stack and changes to
`path`. `popd` returns to the most-recently-pushed directory. `dirs` prints
the current directory followed by the stack contents. Popping an empty
stack produces a deterministic error and a non-zero exit status.

---

## svc / PyInit

PySH ships a small service client used by the `svc` builtin. It is
deliberately PID-file based so it can be useful on systems that do not run
a full supervisor, but it is also designed to plug into PyInit when a
control interface is present.

```
svc list
svc status <name>
svc start <name>
svc stop <name>
svc restart <name>
```

- `svc list` walks `/run/pyinit/*.pid` and prints each service as
  `name\tactive|dead\tpid=N`.
- `svc status <name>` checks the PID file at
  `/run/pyinit/<name>.pid`.
- `svc stop <name>` reads the PID file and sends `SIGTERM`.
- `svc restart <name>` sends `SIGTERM`. Without a registered PyInit control
  interface it then reports that restart requires supervision.
- `svc start <name>` requires a PyInit control interface. Without one it
  fails deterministically rather than pretending to work.

PySH never calls `sudo`. To control system-wide PyInit services, run PySH
under an account that already has permission.

### PyInit service metadata

PyInit-style service files live in `~/.config/pyinit/services/` (or a path
chosen by your integration). PySH ships a strict metadata parser:

```
# ~/.config/pyinit/services/example.service
name: example
command: python3.13 -m http.server 8080
depends: [network]
```

Recognised fields:

- `name`     — required, identifier
- `command`  — required, the launch command line
- `depends`  — optional list of dependency names; accepts `[a, b]` or
  comma-separated form

Invalid metadata (unknown syntax, malformed dependencies, missing fields)
produces a deterministic `ServiceMetadataError` rather than silently
dropping content. This is metadata support for PyInit integration, **not**
a complete replacement for systemd.

---

## Tab completion

`Tab` completes aliases and builtins for the first word, and filesystem
paths for any word. Inaccessible directories are silently skipped.

---

## Limitations

- No job control (`&`, `bg`, `fg`, `jobs`, `Ctrl+Z` job suspension).
- No full POSIX shell grammar — only the constructs documented above.
- No glob expansion is performed by PySH itself; external commands still
  receive globs through their own expansion logic when run via a system
  shell, but plain pipelines do not expand `*` / `?` in arguments.
- No full zsh compatibility. The zsh compatibility bridge is a transition
  layer with safe static alias import and explicit delegation to real zsh.
- `svc start` and `svc restart` to actually re-launch a process require a
  PyInit control interface; without one they return a deterministic error.
- Multiline `py { ... }` blocks do not support nested blocks; the opener
  must be exactly `py {` and the closer must be a line containing only `}`.
- Debian helpers (`apt_check`, `apt_search`) require `apt` to exist; they
  return 127 deterministically when it does not.
- `plan` is advisory only. Policy enforcement is intentionally out of scope
  in 0.4.0.

---

## Testing and quality gates

```bash
pytest -q
ruff check src tests
python -m build
twine check dist/*
```

The project ships unit tests for the parser, the redirection module, the
rc loader and mini-interpreter, command substitution, the history manager,
the highlighting helpers, the plugin loader, directory stack, `unalias`,
the `svc` builtin, the PyInit metadata parser, the zsh transition layer and
the Python runtime bridge.

---

## Publishing

This repository is configured for **PyPI Trusted Publishing** via GitHub
Actions. See [`.github/workflows/publish.yml`](https://github.com/SSobol77/pysh/blob/main/.github/workflows/publish.yml) — it uses `pypa/gh-action-pypi-publish@release/v1` with `id-token: write` and the `pypi` environment. Tagging a release on GitHub publishes the build.

Do **not** publish from a developer machine; let the workflow do it.

---

## Target platform

- Primary target: **Debian 13** with **Python 3.13+**.
- Should work on any POSIX system with a working `subprocess` and
  `readline`, but only Debian 13 is regularly validated.

---

## License

PySH is distributed under the **GNU General Public License v3.0 or later**
(`GPL-3.0-or-later`). See [`LICENSE`](https://github.com/SSobol77/pysh/blob/main/LICENSE) for the full text.
