From 805f686026f156fc2b0e5a7b389c974375ca6b9b Mon Sep 17 00:00:00 2001 From: Steven Syrek Date: Wed, 29 Jul 2026 17:42:58 -0400 Subject: [PATCH] feat(cli): add t and w aliases for translate and write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Community request DeepL/deepl-cli#12. The aliases show up in --help and in bash/zsh/fish completion output; the completion generator now walks command aliases and resolves alias names back to the aliased command for descriptions. w is deliberately assigned to write over watch β€” write is a primary API feature, watch a workflow helper β€” and v/s aliases were considered and deferred. πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CHANGELOG.md | 2 + README.md | 3 + docs/API.md | 2 + src/cli/commands/completion.ts | 12 ++- src/cli/commands/register-translate.ts | 1 + src/cli/commands/register-write.ts | 1 + tests/e2e/cli-aliases.e2e.test.ts | 87 +++++++++++++++++++ .../cli-translate.integration.test.ts | 2 +- tests/unit/completion-command.test.ts | 18 ++++ tests/unit/register-translate.test.ts | 15 ++++ tests/unit/register-write.test.ts | 14 +++ 11 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 tests/e2e/cli-aliases.e2e.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8611d977..9339f3cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **cli**: `t` and `w` command aliases for `translate` and `write` ([#12](https://github.com/DeepL/deepl-cli/issues/12)). The aliases appear in `--help` output and in bash/zsh/fish shell completions. `w` is deliberately assigned to `write` rather than `watch` β€” write is a primary API feature, watch a workflow helper. + - **ci**: Pushing a `v*` tag now runs the full release pipeline. The npm publish job is enabled (it was hard-disabled with `if: false` since its introduction, which is why the repo has 17 tags and zero GitHub Releases); it authenticates with a granular `NPM_TOKEN` for the first publish and keeps `--provenance` attestation. A new, deliberately independent `release` job creates a GitHub Release from the tag, with notes extracted from the version's CHANGELOG section (falling back to generated notes if the section is missing) β€” a publish failure cannot suppress the Release, and vice versa. A `homebrew` formula-bump job (version + sha256 PR against `DeepL/homebrew-tap`) ships gated with `if: false` until the tap repo and its cross-repo token exist. - **cli**: Global `--timeout ` and `--max-retries ` options override the HTTP transport defaults (30000 ms, 3 retries) for a single invocation. Neither was previously configurable from the CLI. - **translate**: `--format json` output now includes the documented `cached` boolean, so scripts can distinguish cache hits from fresh API calls. diff --git a/README.md b/README.md index 8947b0d0..d1e50b79 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,9 @@ export DEEPL_API_KEY=YOUR_API_KEY deepl translate "Hello, world!" --to es # Output: # Β‘Hola, mundo! + +# Short alias: t (and w for write) +deepl t "Hello, world!" --to es ``` ## πŸ”§ Global Options diff --git a/docs/API.md b/docs/API.md index 409d579f..8db5692a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -202,6 +202,7 @@ Translate text, files, or directories. ```bash deepl translate [OPTIONS] [TEXT|FILE|DIRECTORY] +deepl t [OPTIONS] [TEXT|FILE|DIRECTORY] # alias ``` #### Description @@ -670,6 +671,7 @@ Improve text with DeepL Write API (grammar, style, tone enhancement). ```bash deepl write [OPTIONS] TEXT +deepl w [OPTIONS] TEXT # alias ``` #### Description diff --git a/src/cli/commands/completion.ts b/src/cli/commands/completion.ts index b9bc0e8e..15a27b8e 100644 --- a/src/cli/commands/completion.ts +++ b/src/cli/commands/completion.ts @@ -25,7 +25,7 @@ export class CompletionCommand { const topLevel: string[] = []; for (const cmd of this.program.commands) { - topLevel.push(cmd.name()); + topLevel.push(cmd.name(), ...cmd.aliases()); const subcommands = cmd.commands.map((sub) => sub.name()); if (subcommands.length > 0) { tree.set(cmd.name(), subcommands); @@ -36,8 +36,12 @@ export class CompletionCommand { return tree; } + private findCommand(name: string): Command | undefined { + return this.program.commands.find((c) => c.name() === name || c.aliases().includes(name)); + } + private getCommandOptions(cmdName: string): string[] { - const cmd = this.program.commands.find((c) => c.name() === cmdName); + const cmd = this.findCommand(cmdName); if (!cmd) { return []; } @@ -144,7 +148,7 @@ complete -F _deepl_completions deepl const topLevelDescriptions: string[] = []; for (const cmdName of topLevel) { - const cmd = this.program.commands.find((c) => c.name() === cmdName); + const cmd = this.findCommand(cmdName); const desc = cmd ? cmd.description().replace(/'/g, "'\\''") : ''; topLevelDescriptions.push(`'${cmdName}:${desc}'`); } @@ -218,7 +222,7 @@ _deepl "$@" const noSubcmdCondition = '__fish_use_subcommand'; for (const cmdName of topLevel) { - const cmd = this.program.commands.find((c) => c.name() === cmdName); + const cmd = this.findCommand(cmdName); const desc = cmd ? cmd.description() : ''; lines.push(`complete -c deepl -n '${noSubcmdCondition}' -a '${cmdName}' -d '${desc.replace(/'/g, "\\'")}'`); } diff --git a/src/cli/commands/register-translate.ts b/src/cli/commands/register-translate.ts index 6025ff5d..9e0c56f4 100644 --- a/src/cli/commands/register-translate.ts +++ b/src/cli/commands/register-translate.ts @@ -13,6 +13,7 @@ export function registerTranslate( program .command('translate') + .alias('t') .description('Translate text, files, or directories using DeepL API') .argument('[text]', 'Text, file path, or directory to translate (or read from stdin)') .optionsGroup('Core Options:') diff --git a/src/cli/commands/register-write.ts b/src/cli/commands/register-write.ts index 84b9de2b..0507322b 100644 --- a/src/cli/commands/register-write.ts +++ b/src/cli/commands/register-write.ts @@ -21,6 +21,7 @@ export function registerWrite( program .command('write') + .alias('w') .description('Improve text using DeepL Write API (grammar, style, tone)') .argument('[text]', 'Text to improve, file path, or read from stdin') .optionsGroup('Core Options:') diff --git a/tests/e2e/cli-aliases.e2e.test.ts b/tests/e2e/cli-aliases.e2e.test.ts new file mode 100644 index 00000000..acd9fc0a --- /dev/null +++ b/tests/e2e/cli-aliases.e2e.test.ts @@ -0,0 +1,87 @@ +/** + * E2E Tests for Command Aliases + * Tests that `deepl t` and `deepl w` dispatch to translate and write + */ + +import { createTestConfigDir, makeNodeRunCLI } from '../helpers'; + +describe('Command Aliases E2E', () => { + const testConfig = createTestConfigDir('e2e-aliases'); + const { runCLI, runCLIExpectError } = makeNodeRunCLI(testConfig.path); + + afterAll(() => { + testConfig.cleanup(); + }); + + describe('deepl t', () => { + it('should show translate help under the alias', () => { + const output = runCLI('t --help'); + expect(output).toContain('translate|t'); + expect(output).toContain('--to '); + }); + + it('should dispatch to translate validation logic', () => { + const result = runCLIExpectError('t "Hello" --to es --tm-threshold abc'); + expect(result.status).toBeGreaterThan(0); + expect(result.output).toContain('--tm-threshold must be an integer'); + }); + + it('should fail identically to translate on an unknown option', () => { + const viaAlias = runCLIExpectError('t --no-such-flag'); + const viaFull = runCLIExpectError('translate --no-such-flag'); + expect(viaAlias.status).toBe(viaFull.status); + expect(viaAlias.status).toBeGreaterThan(0); + expect(viaAlias.output).toContain('unknown option'); + }); + }); + + describe('deepl w', () => { + it('should show write help under the alias', () => { + const output = runCLI('w --help'); + expect(output).toContain('write|w'); + expect(output).toContain('--style