Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Removed

- **BREAKING — auth**: `deepl auth set-key <KEY>` no longer accepts the key as an argument and exits **6** naming the supported paths. The positional form warned as deprecated on every use since 1.0.0: a command line is readable by other users through process listings — `/proc/<pid>/cmdline` is world-readable on Linux unless `/proc` is mounted `hidepid`, and `ps` discloses other users' argv on macOS as well — and it is recorded in shell history, where it outlives the process indefinitely. Pipe the key instead (`echo "$KEY" | deepl auth set-key --from-stdin`), or redirect a file into it (`deepl auth set-key --from-stdin < keyfile`), which also keeps it out of history, or run `deepl init`, which prompts with masked input. Redirected stdin without the flag is unchanged, so `deepl auth set-key < keyfile` still works, and `--from-stdin` is now the explicit spelling of the only source rather than a switch between two. The argument is still declared to the parser so the value can be refused by name: commander's own excess-argument error quotes the offending token, which would have written the key to stderr — the stream CI captures and retains — turning a transient `ps` exposure into a durable one.
- **BREAKING — sync**: `tms.api_key` and `tms.token` are gone from the `.deepl-sync.yaml` schema, leaving `TMS_API_KEY` and `TMS_TOKEN` as the only credential source. Both were accepted with a stderr warning through 1.x and now fail config load with a `ConfigError` (exit 7) that names the environment variable to use instead — `tms.api_key is no longer read from .deepl-sync.yaml` — and never quotes the value. This file is committed, which is its purpose, so a credential written into it was a secret in version control: present in every clone and fork, and surviving its own deletion from the file, so **rotate any credential that has ever been pushed**. Nothing working depended on the file being read, since the environment variable already won wherever both were set. Two consequences reach past the schema. The TMS destination-trust gate loses its bypass: it applied only to environment-supplied credentials, on the reasoning that a credential inlined in the same file that chose the destination leaked nothing of the operator's, and now that every credential comes from the environment, every destination is checked. And `SyncTmsConfig` drops both fields, along with the `'config'` member of the internal credential-provenance type.
- **BREAKING — sync**: `tms.auto_push`, `tms.auto_pull` and `tms.require_review` are gone from the config schema. All three were on the `tms:` allowlist and in `docs/SYNC.md`, and **no code read any of them**, so a review gate configured through `require_review` was doing nothing. Each now fails config load with a `ConfigError` (exit 7) naming it — `tms.require_review was never implemented and has been removed` — rather than as a generic unknown field, which would read as a typo. `require_review` is not implementable from this side, since the documented export contract is a flat `{ key: value }` map with no per-entry review flag; use `deepl sync pull --dry-run` to preview a pull instead, and run `deepl sync push` / `deepl sync pull` explicitly in place of the auto flags.
- **BREAKING — cli**: The `--enable-beta-languages` flag on `translate` is gone. The API deprecated the underlying parameter as having no effect — beta languages are part of the regular language set — so the flag had become a silent no-op. Scripts passing it exit 6 with an unknown-option error; remove the flag.
- **BREAKING — sync**: `deepl sync init --source-lang` and `--target-langs`, the deprecated aliases introduced in 1.x, are removed and fail with `error: unknown option` (exit 6). Use `--source-locale` and `--target-locales`. `deepl translate --target-lang` is unaffected — it is the API's wire name, not a deprecated alias.
- **BREAKING — sync**: `deepl sync init --source-lang` and `--target-langs`, the deprecated aliases introduced in 1.x, are removed and fail with `error: unknown option` (exit 6). Use `--source-locale` and `--target-locales`. `deepl translate` is unaffected — it selects its target with `-t` / `--to`, which was never a locale alias.
- **usage**: The dedicated "Speech-to-Text Usage" section (text output), the "Speech-to-text" row (table output) and the `speechToTextMilliseconds*` fields they read are gone, following the API's deprecation of `speech_to_text_milliseconds_count`/`_limit` ("Always returns 0"). Voice usage remains visible in the Product Breakdown, which reads live per-product minutes; the Admin API's per-key `speech_to_text_milliseconds` usage limit is a different, still-current field and is unaffected.
- **deps**: `better-sqlite3` and `@types/better-sqlite3`. The production dependency tree no longer contains any native addon, removing the whole class of ABI-mismatch failures (`ERR_DLOPEN_FAILED` / `NODE_MODULE_VERSION` after a Node major upgrade), a 1.9 MB platform-specific binary, and the C++ compilation-toolchain requirement for installs from source. The cacheless-degradation safety net remains: a runtime whose `node:sqlite` is unusable warns once and runs uncached rather than crashing, and never touches the cache database.
- **deps**: `inquirer`, which no source file imported.
Expand All @@ -84,6 +86,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **config**: An empty `~/.deepl-cli/` directory no longer captures path resolution. The legacy layout was selected on the bare existence of that directory, so one left behind after `rm ~/.deepl-cli/config.json` or `rm ~/.deepl-cli/cache.db` — both of which `docs/TROUBLESHOOTING.md` tells you to run — silently won over an explicitly set `XDG_CONFIG_HOME` **and** `XDG_CACHE_HOME`, resolving config and cache into a directory holding neither and reporting nothing, so a user who set an XDG variable and found it ignored had no signal pointing at the leftover. The legacy layout now applies only when `config.json` or `cache.db` is actually present, so every real 1.x installation resolves exactly as before while an empty directory falls through to XDG. The precedence order is unchanged: `DEEPL_CONFIG_DIR` still wins outright, and a populated `~/.deepl-cli/` still beats XDG.

- **sync**: Two concurrent `deepl sync` runs in one directory can no longer both believe they hold the process lock. Reclaiming a pidfile already proven stale renames it aside and then confirms it is the same file before deleting it, but that confirmation compared inode and device only — and an inode number is reused once its file is unlinked, which is exactly what a sync winning the race does when it replaces the pidfile. On ext4 the freed inode comes straight back, so a winner's live pidfile compared equal to the stale one it replaced and was deleted, leaving both runs writing the same target files and the same lockfile. Identity now also requires the recorded `pid` and `startedAt` to match, which a different holder cannot satisfy. The behaviour was filesystem-dependent: APFS never reuses inodes, so this reproduced on Linux only.
- **cli**: `deepl completion fish` no longer emits a broken line for a command or option whose description contains a backslash. Descriptions were escaped for the surrounding fish single quotes by replacing `'` only, so a trailing backslash escaped the closing quote and ran the rest of the generated line into the description. Backslashes are now escaped first, then quotes. The bash and zsh generators use the POSIX `'\''` form and were unaffected.
- **sync**: `translation.locale_overrides.<locale>.model_type` is now applied. The key was on the config allowlist and validated per locale against `translation_memory`, but the translator only ever read the top-level `translation.model_type`, so the per-locale value was silently dropped — and the validator's own error message named that inert scope as the remedy. A locale that configures translation memory per locale therefore got requests the TM could not be applied to, silently, and was billed for them. The override now resolves exactly like its siblings (`formality`, `translation_memory_threshold`, `custom_instructions`, `style_id`): the per-locale value wins, otherwise the top-level one applies. `SyncLocaleOverrides` also declares the field, and — as with those siblings — a per-locale override now takes precedence over `--model-type`.
Expand Down
9 changes: 2 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,8 @@ Or set your API key directly:
# Piping from stdin keeps the key out of process listings and shell history
echo "YOUR_API_KEY" | deepl auth set-key --from-stdin

# Passing it as an argument also works, but is deprecated and warns:
# other users can read it via `ps`
deepl auth set-key YOUR_API_KEY
# Or from a file, keeping the key out of shell history
deepl auth set-key --from-stdin < ~/.deepl-api-key
```

Or use an environment variable:
Expand Down Expand Up @@ -1007,10 +1006,6 @@ echo "YOUR_API_KEY" | deepl auth set-key --from-stdin
echo "YOUR_API_KEY" | deepl auth set-key --from-stdin --no-verify
# ✓ API key saved without validation

# Passing the key as an argument still works, but is deprecated and warns —
# other users can read it via `ps`
deepl auth set-key YOUR_API_KEY

# Show the stored key, masked (does not contact the API)
deepl auth show
# API Key: abc1...2def
Expand Down
39 changes: 18 additions & 21 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ deepl --config /path/to/test-config.json usage
- **Environment separation**: Separate configs for dev/staging/production
- **Testing**: Use test configurations without affecting default settings

**Precedence:** `--config` replaces the config _file_ only, overriding `DEEPL_CONFIG_DIR` for configuration. The cache location is unaffected — it still follows `DEEPL_CONFIG_DIR` > legacy `~/.deepl-cli/` > XDG resolution.
**Precedence:** `--config` replaces the config _file_ only, overriding `DEEPL_CONFIG_DIR` for configuration. The cache location is unaffected — it still follows `DEEPL_CONFIG_DIR` > legacy `~/.deepl-cli/` (when it holds `config.json` or `cache.db`) > XDG resolution.

**Command Suggestions:**

Expand Down Expand Up @@ -1374,7 +1374,7 @@ Interactive setup wizard that creates `.deepl-sync.yaml` by scanning the project
- `--format FORMAT` - Output format: `text` (default), `json`. Under `json`, success emits the envelope described below and failure emits the shared error envelope, both on stdout
- `--sync-config PATH` - Path to `.deepl-sync.yaml`

`--source-lang` and `--target-langs` were accepted as deprecated aliases during `1.x` and were removed in `2.0.0`; use `--source-locale` / `--target-locales`. `deepl translate --target-lang` is unchanged — it operates on strings and stays aligned with the DeepL API's wire name.
`--source-lang` and `--target-langs` were accepted as deprecated aliases during `1.x` and were removed in `2.0.0`; use `--source-locale` / `--target-locales`. `deepl translate` is unaffected — it selects its target with `-t` / `--to` and operates on strings rather than locale files.

**Examples:**

Expand Down Expand Up @@ -2798,40 +2798,34 @@ deepl auth <SUBCOMMAND>

#### Subcommands

##### `set-key [api-key]`
##### `set-key`

Set your DeepL API key and validate it with the DeepL API.

**Arguments:**

- `api-key` (optional) - Your DeepL API authentication key. If omitted, reads from stdin.
Set your DeepL API key and validate it with the DeepL API. The key is read from
stdin; there is no argument form.

**Options:**

- `--from-stdin` - Read API key from stdin
- `--from-stdin` - Read API key from stdin. Stdin is the only source, so this flag is explicit rather than required.
- `--no-verify` - Store the key without validating it against the API. Use on offline or proxied networks, where validation cannot reach the API and the key would otherwise be discarded. Exports of `DEEPL_API_KEY` also bypass validation entirely.

**Examples:**

```bash
# Pipe key from stdin (recommended - avoids exposing key in process listings)
echo "YOUR-API-KEY" | deepl auth set-key --from-stdin
# ✓ API key saved and validated successfully

# Read from file
# Read from file, keeping the key out of shell history
deepl auth set-key --from-stdin < ~/.deepl-api-key

# Provide key as argument
deepl auth set-key YOUR-API-KEY-HERE
# ✓ API key saved and validated successfully

# Store without a network round-trip (offline, or behind an unconfigured proxy)
echo "YOUR-API-KEY" | deepl auth set-key --from-stdin --no-verify
# ✓ API key saved without validation
```

**Security Note:** Prefer `--from-stdin` over passing the key as a command argument. Command arguments are visible to other users via process listings (`ps aux`).
**Security Note:** The key is never taken from the command line. Command arguments are visible to other users via process listings (`ps aux`) and are recorded in shell history, so `set-key` reads stdin only. Run `deepl init` to be prompted for the key with masked input instead.

> **Deprecation:** Passing the API key as a positional argument is deprecated and will emit a warning. Use `--from-stdin` instead for secure key input.
> **Removed in 2.0.0:** Passing the API key as a positional argument (`deepl auth set-key YOUR_KEY`) warned as deprecated in 1.x and now exits 6. Use `--from-stdin`, or `deepl init`.

##### `show`

Expand Down Expand Up @@ -3276,11 +3270,14 @@ The CLI resolves configuration and cache paths using the following priority orde
| Priority | Condition | Config path | Cache path |
| -------- | ---------------------- | ---------------------------------------- | ------------------------------------ |
| 1 | `DEEPL_CONFIG_DIR` set | `$DEEPL_CONFIG_DIR/config.json` | `$DEEPL_CONFIG_DIR/cache.db` |
| 2 | `~/.deepl-cli/` exists | `~/.deepl-cli/config.json` | `~/.deepl-cli/cache.db` |
| 2 | `~/.deepl-cli/config.json` or `~/.deepl-cli/cache.db` exists | `~/.deepl-cli/config.json` | `~/.deepl-cli/cache.db` |
| 3 | XDG env vars set | `$XDG_CONFIG_HOME/deepl-cli/config.json` | `$XDG_CACHE_HOME/deepl-cli/cache.db` |
| 4 | Default | `~/.config/deepl-cli/config.json` | `~/.cache/deepl-cli/cache.db` |

Existing `~/.deepl-cli/` installations continue to work with no changes needed.
Existing `~/.deepl-cli/` installations continue to work with no changes needed. An
empty `~/.deepl-cli/` directory is ignored — the legacy layout applies only when it
holds `config.json` or `cache.db`, so a directory left behind after deleting those
files does not shadow an explicitly set `XDG_CONFIG_HOME` or `XDG_CACHE_HOME`.

### Configuration Schema

Expand Down Expand Up @@ -3366,15 +3363,15 @@ export DEEPL_CONFIG_DIR="/custom/path"

### `XDG_CONFIG_HOME`

Override XDG config base directory (default: `~/.config`). Config is stored at `$XDG_CONFIG_HOME/deepl-cli/config.json`. Only used when `DEEPL_CONFIG_DIR` is unset and legacy `~/.deepl-cli/` does not exist.
Override XDG config base directory (default: `~/.config`). Config is stored at `$XDG_CONFIG_HOME/deepl-cli/config.json`. Only used when `DEEPL_CONFIG_DIR` is unset and legacy `~/.deepl-cli/` holds neither `config.json` nor `cache.db`.

```bash
export XDG_CONFIG_HOME="$HOME/.config"
```

### `XDG_CACHE_HOME`

Override XDG cache base directory (default: `~/.cache`). Cache is stored at `$XDG_CACHE_HOME/deepl-cli/cache.db`. Only used when `DEEPL_CONFIG_DIR` is unset and legacy `~/.deepl-cli/` does not exist.
Override XDG cache base directory (default: `~/.cache`). Cache is stored at `$XDG_CACHE_HOME/deepl-cli/cache.db`. Only used when `DEEPL_CONFIG_DIR` is unset and legacy `~/.deepl-cli/` holds neither `config.json` nor `cache.db`.

```bash
export XDG_CACHE_HOME="$HOME/.cache"
Expand Down Expand Up @@ -3496,7 +3493,7 @@ Authentication failed or no API key is available. Emitted by:
- Every command that touches the API (`translate`, `write`, `voice`, `glossary`, `usage`, `sync`, `tm list`, `admin`, etc.) when `DEEPL_API_KEY` is unset and no key is in the config file
- HTTP 401/403 responses from the DeepL API

Remediation: run `deepl init` or `deepl auth set-key <your-api-key>`, or export `DEEPL_API_KEY`.
Remediation: run `deepl init` or `deepl auth set-key --from-stdin < keyfile`, or export `DEEPL_API_KEY`.

#### 3 — RateLimitError

Expand Down
Loading
Loading