Metadata-Version: 2.1
Name: legesher
Version: 0.24.0
Summary: Command-line tools to write, translate, and run Python in your own language
Keywords: translation,programming,multilingual,i18n,cli,developer-tools
Author-Email: "Madison (Pfaff) Edgar" <7844510+madiedgar@users.noreply.github.com>
License: Apache-2.0
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Code Generators
Classifier: Topic :: Software Development :: Internationalization
Classifier: Topic :: Software Development :: Localization
Classifier: Operating System :: OS Independent
Project-URL: Homepage, https://legesher.io
Project-URL: Documentation, https://docs.legesher.io
Project-URL: Repository, https://github.com/legesher/legesher
Project-URL: Changelog, https://github.com/legesher/legesher/blob/main/libs/legesher/CHANGELOG.md
Project-URL: Issues, https://github.com/legesher/legesher/issues
Requires-Python: <4.0,>=3.10
Requires-Dist: click>=8.0.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: questionary>=2.0.0
Requires-Dist: tomli>=2.0.0; python_version < "3.11"
Requires-Dist: legesher-core[treesitter]<1.0.0,>=0.22.0
Requires-Dist: legesher-i18n<1.0.0,>=0.23.0
Requires-Dist: legesher-runtime<1.0.0,>=0.1.0
Description-Content-Type: text/markdown

# legesher

> Command-line interface for coding in any language

**Status**: Pre-Beta | **Languages**: 56 | **Version**: see `pyproject.toml`

The legesher CLI is your gateway to programming in your native language. Translate Python code between languages,
manage git integration for collaborative workflows, and configure your multilingual development environment.

## About

The `legesher` package provides command-line tools for developers who want to write code in their native language
instead of English. It handles:

- **Project Setup**: `legesher init` scaffolds configuration, git integration, and AI context
- **Code Translation**: Convert Python files between languages using language packs
- **Execution**: `legesher run` executes a native-language file directly
- **Git Integration**: a clean filter that keeps each file in the language its author wrote it in
- **Configuration**: Manage project and user settings for language preferences
- **Metadata**: Inspect and validate the translation history recorded for your files

This is the main user-facing package in the Legesher ecosystem, built on top of `legesher-core` and `legesher-i18n`.

## Installation

```bash
pip install legesher
```

Or install from source in the monorepo:

```bash
cd libs/legesher
pip install -e .
```

## Quick Start

```bash
# Initialize a new project with Spanish language pack
legesher init --language es

# Translate a Python file to Spanish
legesher translate mi_codigo.py --to es

# Set up git integration for collaborative development
legesher git setup

# View available language packs
legesher-i18n registry list
```

## Commands

### `legesher init`

Scaffold Legesher in a Python project: write `.legesher/config.yml`, optionally wire the git filters, and
optionally generate AI context files.

```bash
# Interactive setup
legesher init

# Pre-select languages and skip the prompts
legesher init --language es --language fr --git --no-ai-context
```

### `legesher translate`

Translate Python source files between languages.

```bash
# Translate a single file to Spanish
legesher translate app.py --to es

# Translate to English, the run form Python executes. --from defaults to en, so name the source
legesher translate mi_app.py --from es --to en

# Between two non-English languages, go through English: direct pairs are not supported yet
legesher translate code.py --from es --to en --output code_en.py
legesher translate code_en.py --to fr --output code_fr.py

# Translate and write to different file
legesher translate input.py --to es --output salida.py

# Translate an entire directory
legesher translate src/ --to es --output traducido/
```

**Full LLM Translation** (translates identifiers, comments, strings, docstrings):

```bash
# Full translation with Anthropic Claude
legesher translate src/ --to es --full --llm-backend anthropic

# Full translation with local model (vLLM, Ollama, llama.cpp)
legesher translate src/ --to ur --full --llm-backend openai-compat \
    --llm-base-url http://localhost:8000

# Batch with cross-file consistency and resume
legesher translate src/ --to zh --full \
    --identifier-map .legesher/identifier_map.json \
    --checkpoint .legesher/checkpoint.txt
```

**Options:**

- `--to LANG`: Target language code (required)
- `--from LANG`: Source language code (default: `en`)
- `--output PATH`: Output file or directory. Omitted, a single file goes to stdout and a directory goes to a
  sibling `<dir>_translated/`. Your input file is never overwritten in place
- `--full`: Enable full LLM translation (identifiers, comments, strings, docstrings)
- `--llm-backend`: LLM backend: `anthropic`, `cohere-aya`, or `openai-compat` (default: anthropic)
- `--llm-base-url`: Base URL for openai-compat backend (e.g., `http://localhost:8000`)
- `--llm-model`: Model name for any backend (default: the backend's own default)
- `--identifier-map PATH`: Load/save identifier glossary for cross-file consistency
  (default: `.legesher/identifier_map.json`); `--no-identifier-map` disables it
- `--checkpoint PATH`: Resume interrupted batch jobs

**Example Output:**

```bash
$ legesher translate hello.py --to es
definir greet():
    imprimir("hi")

$ legesher translate hello.py --to es --output hola.py
✓ Translated hello.py → hola.py
```

### `legesher run`

Execute a native-language Python file.

```bash
legesher run programa.py --lang es

# Pass arguments through to the script
legesher run programa.py --lang es --args "--verbose input.txt"
```

`--lang` wins. Without it the language comes from the `language:` key in `.legesher/config.yml`,
falling back to `en` when that key is absent. A config written by `legesher init` does not have that
key (see [Configuration](#configuration)), so dropping `--lang` there runs the file untranslated and
Python raises `SyntaxError`. Pass `--lang`, or write the config with `legesher git setup`.

### `legesher git`

Git stores each file in its author's language. `legesher git setup` configures the clean filter, as one
long-running process per git command, and the guard hook for a repository.

```bash
# Set up for the current repository
legesher git setup

# Set up with a specific language
legesher git setup --language es

# Opt in to the keyword-only smudge (terminal editors)
legesher git setup --language es --keyword-smudge

# Check the configuration
legesher git status
```

**Subcommands:**

- **`setup`**: Configure the clean filter, which only normalizes: a stray keyword, such as an English `if` typed
  into a Spanish file, is stored in its block's language. It never refuses a file, because git stores a file
  unchanged when its filter fails, so setup also installs a pre-commit hook that runs `legesher guard`. An
  existing pre-commit hook is never overwritten; setup prints the line to add to it instead. `--no-hook` skips
  the hook, and removes it if setup installed it.
- **`status`**: Display the filter, smudge and hook configuration and the language settings

Smudge is off by default: a reader's rendering is never written into a file, so views render in the editor and
on GitHub. `--keyword-smudge` is the opt-in for terminal editors. It renders an English file's keywords in your
language on checkout, from the open language pack only (never builtins or library names), and only when the
clean filter can store the result back exactly. Running `setup` again without the flag turns it off in that
scope, and setup says so when a smudge is still configured in another, such as the global git config.

Until in-file region markers land, a file is one block. Its language is read from its committed version or, for a
new file, from the file itself, among English and your configured language. A reading counts only when the file
parses under it, and the language with the most keywords wins, so one slip never converts a file; with no reading
that parses, or a tie, the file is stored as written. A file in a third language is left as written unless every
keyword it uses is spelled the same in your language, which closely related languages can do.

**One process per git command.** Setup configures git's long-running filter process
(`filter.legesher.process`), so a git command starts the filter once instead of once per file; the single-file
`clean` command stays configured for git older than 2.11. The process runs through a small launcher,
`.git/legesher/filter-process` (`~/.legesher/filter-process` with `--global`), which names the `legesher-filter`
that ran setup and falls back to `PATH`. Git fails a whole command when a filter process ends before its handshake,
so the launcher answers the handshake itself and only then starts the filter: a filter that is missing, broken or
too old costs only that file's filtering, and git stores it as written, as it does for a missing single-file
filter. A single-file `smudge` command in any git config is ignored once a process is configured; the process
offers the smudge only when setup ran with `--keyword-smudge`. To take Legesher's filter out of a repository, run
`git config --remove-section filter.legesher`.

**Example Output:**

```bash
$ legesher git setup --language es
✓ Found git repository at: /path/to/repo

Validating language pack 'es'...
✓ Loaded language pack: es (39 keywords, 143 builtins)

Creating configuration...
✓ Saved project config to: /path/to/repo/.legesher/config.yml

Configuring git filters...
✓ Filter process: sh '/path/to/repo/.git/legesher/filter-process'
✓ Clean filter (git before 2.11): legesher-filter --clean --path %f

Configuring .gitattributes...
✓ Updated .gitattributes

Initializing metadata system...
✓ Created metadata directory: /path/to/repo/.legesher/metadata
✓ Metadata will be tracked in git
✓ Metadata system initialized successfully

Configuring the guard hook...
✓ Installed the guard hook (pre-commit: legesher guard --staged)

Verifying setup...
✓ Git filter setup verified

============================================================
✓ Git filter setup complete!
============================================================

Your repository is now configured for multilingual code.
Language: es
Storage: each file in its author's language
```

The trailing "Next steps" list is omitted above. The `143 builtins` in that output is not the pack's
built-in count: the translator merges the pack's 70 exception names into its built-in map before
reporting, so `143` is the pack's 73 built-ins plus its 70 exceptions. The per-category counts are in
[legesher-i18n](../i18n/README.md#supported-languages).

### `legesher guard`

Refuses what must not be committed. The pre-commit hook runs `legesher guard --staged`; without paths it checks the
staged files, and with paths it checks those files on disk. `--diff BASE` checks only what changed from `BASE` to
`HEAD`, read from `HEAD` and judged against each file's version at `BASE`: the command a pull-request check runs. It
compares the two trees, so with a pull request's merge commit checked out it covers exactly the pull request, and a
shallow clone only needs the base commit itself. `--format github` prints each finding as a GitHub Actions annotation.
A file is checked if `.gitattributes` gives it the `legesher` filter (staged or in the working tree for `--staged`; at
the base or the head for `--diff`); files under `.legesher/` are always checked.

```bash
$ legesher guard --staged
src/app.py:2:11: unicode: U+202E RIGHT-TO-LEFT OVERRIDE: Trojan Source bidi reordering control (CVE-2021-42574)
src/app.py:3:5: keyword: `if` in a block stored in es; its stored form is `si`

legesher guard: 2 finding(s) in 1 file(s).
```

- **`encoding`**: content that is not UTF-8, or a coding declaration other than UTF-8. Python honors the
  declaration, so a pure-ASCII file declared `utf-7` can run code that reads as a string.
- **`unicode`**: bidi reordering controls, invisible characters outside string and comment prose, Arabic tatweel,
  and identifier characters outside every supported script. There is no override.
- **`keyword`**: a keyword not in its block's stored form. The clean filter normalizes these, so a finding means
  the file skipped it; run `legesher git setup`, then `git add --renormalize <path>`.
- **`artifact`**: a symlink marked for Legesher (the guard cannot check what it points to; replace it with the
  file), or a file under `.legesher/` that is a symlink or a submodule, that does not match its format
  (`config.yml`, `metadata/*.yml`, `translation_memory.json`, `identifier_map.json`), or that records a path
  outside the repository; `metadata.directory` must stay under `.legesher/`, with no `..`. The directory's name is
  matched in any letter case. Other files there are never loaded.

Exit status is 0 with no findings, 1 with findings, and 2 when the guard cannot run (outside a repository, or an
unknown language).

The hook is a client-side check: `git commit --no-verify`, merges and rebases skip it, and it lets a commit through
with a notice when it cannot find `legesher`. A pull-request check running `legesher guard --diff` sees changes
made anywhere, edits on github.com included, but it checks a pull request's net change against its base: the
commits inside it land as they are under a merge commit or a rebase merge, and a push that skips pull requests is
not checked unless branch protection requires one.

In diff mode a file counts as Legesher's when the base's or the head's `.gitattributes` marks it, and the language
comes from the base's `.legesher/config.yml` (English when the base names none; the pull request's own configuration
is never read), so nothing a pull request changes in its tree relaxes what it is judged by. Diff mode also refuses
every marked symlink in the tree, and every symlink or submodule under `.legesher/`, not only the changed ones, since
a change elsewhere can use a link already there. A repository that already holds a marked symlink fails every pull
request until the link is replaced with the file it points to.

The check's own workflow file is outside what the guard reads: on `pull_request`, GitHub runs the workflow as the pull
request left it, so a pull request could change the step's inputs or remove it while the check keeps its name.
Protect `.github/workflows/` as well, for example with a code owner and required code-owner review.

### `legesher config`

Manage project and global configuration.

Project configuration is created by `legesher init` or `legesher git setup`; `config` reads and writes it
afterwards. `get` and `list` report the keys that are actually in the files, so what they show depends on
which of those two commands wrote the config (see [Configuration](#configuration)).

```bash
# Set default language for project
legesher config set language es

# Get the key back
legesher config get language

# List all configuration settings (user-level merged with project)
legesher config list

# Set user-level configuration
legesher config set language es --global
```

`config get` exits 1 with `Key '<key>' not found.` when the key is absent from both files: it does not
fall back to the built-in defaults.

**Configuration Locations:**

- **Project**: `.legesher/config.yml` (current directory)
- **User**: `~/.legesher/config.yml` (user home directory)

**Available Settings** (the keys the runtime reads, and the ones `legesher git setup` writes):

- `language`: Default language code (e.g., `es`, `fr`, `ar`)
- `python_version`: Target Python version (e.g., `3.12`, `3.11`), or `null` to auto-detect
- `git.canonical_language`: written as `en` by `legesher init` and `legesher git setup` for compatibility; the
  filter does not read it, because each file is stored in its author's language
- `git.filter_enabled`: Enable git filter integration (boolean)
- `runtime.cache_size`: Translation cache size. Honoured when present; neither command writes it out
- `metadata.enabled`, `metadata.directory`, `metadata.track_in_git`,
  `metadata.verbose_history`, `metadata.max_history_entries`: Metadata tracking

`legesher init` writes a different set of keys (`languages`, `git.enabled`, `ai_context.*`,
`translation.*`, `editor.*`). `config get` and `config list` will report those verbatim, but nothing in
the runtime consults them.

### `legesher metadata`

Inspect the translation metadata Legesher records for the files in your repository. Every subcommand except
`list` takes a file path, not a language code.

```bash
# Show metadata for one file
legesher metadata show src/calculator.py

# Show the clean/smudge history for a file
legesher metadata history src/calculator.py --limit 20

# List every file with recorded metadata
legesher metadata list

# Show the raw YAML
legesher metadata raw src/calculator.py

# Check a metadata file is present, valid YAML, and complete
legesher metadata validate src/calculator.py
```

Every subcommand takes `--repo PATH` to point at a repository other than the current directory.

To inspect a *language pack* rather than a file's history, use the `legesher-i18n` CLI:

```bash
legesher-i18n registry list
legesher-i18n validate libs/i18n/language-packs/python/es/legesher_i18n_python_es/packs/3.13.yml
```

## Configuration

The legesher CLI uses YAML configuration files for project and user settings: `.legesher/config.yml`
in a project, `~/.legesher/config.yml` for user-wide defaults.

> **Known issue: `legesher init` and `legesher git setup` write two different, incompatible shapes.**
> `git setup` writes the shape the runtime reads. `init` writes its own shape, which the runtime does
> not understand, so after `legesher init --language es` the working language silently falls back to
> `en`: `legesher git status` reports `Language: en`, `legesher run` without `--lang` fails to compile
> the file, `config get language` exits 1, and the git clean filter cannot recognize your language, so it
> leaves stray keywords as typed. Reconciling the two is tracked as its own issue. Until it lands, run
> `legesher git setup --language <code>` after `legesher init` (it rewrites `config.yml` in the shape
> everything else reads, and leaves the generated AI context files alone).

### Project configuration written by `legesher git setup`

This is the shape `LegesherConfig` loads, and the keys `legesher config` documents above:

```yaml
language: es
python_version: null      # null means auto-detect
git:
  canonical_language: en
  filter_enabled: true
metadata:
  enabled: true
  directory: .legesher/metadata
  track_in_git: true
  verbose_history: false
  max_history_entries: 100
```

### Project configuration written by `legesher init`

Same path, different keys. Nothing in the runtime reads the keys unique to
this shape; `python_version` and `git.canonical_language` are shared with the
shape above and are read:

```yaml
# Languages enabled in this project
languages:                # a list, not the single `language:` string above
  - es

python_version: "3.13"

git:
  enabled: true           # not git.filter_enabled
  canonical_language: "en"

ai_context:
  enabled: true
  context_files:
    - AI-CONTEXT-es.md

translation:
  default_source: "en"
  default_target: "es"
  preserve_comments: true
  preserve_strings: true

editor:
  lsp_hover: true
  inline_completion: true
  bilingual_tooltips: true
```

### User Configuration

`~/.legesher/config.yml` holds user-wide defaults and uses the same keys as the `git setup` shape.

### Configuration Precedence

Settings are resolved in this order (highest to lowest priority):

1. Command-line flags (e.g., `--language es`)
2. Project configuration (`.legesher/config.yml`)
3. User configuration (`~/.legesher/config.yml`)
4. Built-in defaults

## Examples

### Example 1: Starting a New Spanish Project

```bash
# Create project directory
mkdir mi_proyecto
cd mi_proyecto
git init

# Initialize legesher configuration (writes config.yml, git filters, AI context)
legesher init --language es --git --python-version 3.13

# Rewrite config.yml in the shape the runtime reads. Without this the filters are
# installed but inert, and your code is stored in Spanish. See Configuration above
legesher git setup --language es --python-version 3.13

# Create a Python file in Spanish. Keywords and builtins are Spanish;
# your own names, and everything inside strings, stay as you write them
cat > hola.py << 'EOF'
definir saludar(nombre):
    """Saluda a una persona por su nombre."""
    imprimir(f"¡Hola, {nombre}!")

saludar("Mundo")
EOF

git add .
git commit -m "Initial commit"

# The file is stored in Spanish, as written; a stray English keyword would be normalized to Spanish
```

### Example 2: Working in an English Project

```bash
# Clone an English Python project
git clone https://github.com/example/python-project.git
cd python-project

# Set up legesher to work in Spanish
legesher git setup --language es

# Translate a throwaway copy into Spanish for reading
legesher translate src/ --to es --output src_es/

# The project's files stay in English, the language their authors wrote them in.
# A Spanish keyword typed into one of them is normalized back to English at git add;
# a new file you write in Spanish is stored in Spanish.
```

### Example 3: Switching Between Languages

```bash
# Currently working in Spanish (config written by `legesher git setup --language es`)
legesher config get language
# Output: es

# Translate a copy of the codebase to French, through English (direct pairs are not supported yet)
legesher translate src_es/ --from es --to en --output src_en/
legesher translate src_en/ --to fr --output src_fr/

# Update configuration
legesher config set language fr

# Now working in French!
```

### Example 4: Checking a File's Translation Record

```bash
# Show what Legesher has recorded for a file
legesher metadata show src/calculator.py

# Check that record is present and well-formed
legesher metadata validate src/calculator.py
```

## Development

### Running Tests

```bash
# Run all tests
pytest

# Run with coverage
pytest --cov=legesher --cov-report=html

# Run specific test file
pytest tests/test_cli.py
```

### Project Structure

```text
libs/legesher/
├── legesher/
│   ├── __init__.py
│   ├── config.py           # Configuration loading and precedence
│   ├── cli/
│   │   ├── main.py         # Main CLI entry point
│   │   ├── init.py         # init command
│   │   ├── translate.py    # translate command
│   │   ├── run.py          # run command
│   │   ├── git.py          # git command group
│   │   ├── config_cmd.py   # config command group
│   │   └── metadata.py     # metadata command group
│   └── runtime/
│       ├── import_hook.py  # Import hook for `legesher run`
│       ├── source_mapper.py
│       └── error_handler.py
├── tests/
├── pyproject.toml
└── README.md
```

## Contributing

We welcome contributions! Please see:

- [CONTRIBUTING.md](../../CONTRIBUTING.md) in the monorepo root
- [Development Guide](../../DEVELOPMENT.md)
- [Open Issues](https://github.com/legesher/legesher/issues)

## License

Apache License 2.0 - see [LICENSE](./LICENSE) for details.

## Links

- **Monorepo**: [legesher/legesher](https://github.com/legesher/legesher)
- **Documentation**: [docs.legesher.io](https://docs.legesher.io)
- **Language Packs**: [legesher-i18n packages](https://pypi.org/search/?q=legesher-i18n)
- **Changelog**: [CHANGELOG.md](./CHANGELOG.md)
- **Issues**: [GitHub Issues](https://github.com/legesher/legesher/issues)
- **Discussions**: [GitHub Discussions](https://github.com/legesher/legesher/discussions)

## Related Packages

- **[legesher-core](../core/README.md)**: Core translation engine and AST utilities
- **[legesher-i18n](../i18n/README.md)**: Language pack registry and translation API
- **[legesher-i18n-python-es](../i18n/language-packs/python/es/README.md)**: Spanish language pack for Python
- **[legesher-language-server](../language-server/README.md)**: LSP implementation for IDE integration
- **[legesher-runtime](../runtime/README.md)**: Localized tracebacks, used by `legesher run`

---

**Part of the Legesher Project** - Empowering developers to code in their native language.
