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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ms>` and `--max-retries <n>` 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.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ Translate text, files, or directories.

```bash
deepl translate [OPTIONS] [TEXT|FILE|DIRECTORY]
deepl t [OPTIONS] [TEXT|FILE|DIRECTORY] # alias
```

#### Description
Expand Down Expand Up @@ -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
Expand Down
12 changes: 8 additions & 4 deletions src/cli/commands/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 [];
}
Expand Down Expand Up @@ -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}'`);
}
Expand Down Expand Up @@ -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, "\\'")}'`);
}
Expand Down
1 change: 1 addition & 0 deletions src/cli/commands/register-translate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:')
Expand Down
1 change: 1 addition & 0 deletions src/cli/commands/register-write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:')
Expand Down
87 changes: 87 additions & 0 deletions tests/e2e/cli-aliases.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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 <language>');
});

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 <style>');
});

it('should dispatch to write option validation', () => {
const result = runCLIExpectError('w "Hello" --format bogus');
expect(result.status).toBeGreaterThan(0);
expect(result.output).toContain("Allowed choices are text, json");
});

it('should fail identically to write on an unknown option', () => {
const viaAlias = runCLIExpectError('w --no-such-flag');
const viaFull = runCLIExpectError('write --no-such-flag');
expect(viaAlias.status).toBe(viaFull.status);
expect(viaAlias.status).toBeGreaterThan(0);
expect(viaAlias.output).toContain('unknown option');
});
});

describe('help output', () => {
it('should list both aliases in top-level help', () => {
const output = runCLI('--help');
expect(output).toContain('translate|t');
expect(output).toContain('write|w');
});
});

describe('shell completion', () => {
it('should offer t and w in bash completions', () => {
const output = runCLI('completion bash');
expect(output).toMatch(/compgen -W "[^"]*\btranslate t\b/);
expect(output).toMatch(/compgen -W "[^"]*\bwrite w\b/);
});

it('should offer t and w in zsh completions', () => {
const output = runCLI('completion zsh');
expect(output).toMatch(/'t:.*[Tt]ranslate/);
expect(output).toMatch(/'w:.*[Ww]rite/);
});

it('should offer t and w in fish completions', () => {
const output = runCLI('completion fish');
expect(output).toContain("-a 't'");
expect(output).toContain("-a 'w'");
});
});
});
2 changes: 1 addition & 1 deletion tests/integration/cli-translate.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ describe('Translate CLI Integration', () => {

// Text should be optional (can use stdin)
// Shown in usage line as [text]
expect(output).toContain('translate [options] [text]');
expect(output).toContain('translate|t [options] [text]');
expect(output).toContain('Arguments:');
});

Expand Down
18 changes: 18 additions & 0 deletions tests/unit/completion-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('CompletionCommand', () => {

program
.command('translate')
.alias('t')
.description('Translate text or files')
.option('-t, --to <lang>', 'Target language')
.option('-f, --from <lang>', 'Source language');
Expand Down Expand Up @@ -275,4 +276,21 @@ describe('CompletionCommand', () => {
expect(zsh).toContain('_deepl_style_rules()');
});
});

describe('command aliases', () => {
it('bash: offers aliases as top-level completions', () => {
const script = completionCommand.generate('bash');
expect(script).toContain('compgen -W "translate t auth');
});

it('zsh: offers aliases with the aliased command description', () => {
const script = completionCommand.generate('zsh');
expect(script).toContain("'t:Translate text or files'");
});

it('fish: offers aliases with the aliased command description', () => {
const script = completionCommand.generate('fish');
expect(script).toContain("-a 't' -d 'Translate text or files'");
});
});
});
15 changes: 15 additions & 0 deletions tests/unit/register-translate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,21 @@ describe('registerTranslate', () => {
({ program } = setupProgram());
});

describe('alias', () => {
it('registers t as an alias of translate', () => {
const translateCmd = program.commands.find(c => c.name() === 'translate')!;
expect(translateCmd.aliases()).toContain('t');
});

it('shows the alias in help output', () => {
const translateCmd = program.commands.find(c => c.name() === 'translate')!;
let helpOutput = '';
translateCmd.configureOutput({ writeOut: (str: string) => { helpOutput += str; } });
translateCmd.outputHelp();
expect(helpOutput).toContain('translate|t');
});
});

describe('--output-format option', () => {
it('should only accept docx as a valid choice', () => {
const translateCmd = program.commands.find(c => c.name() === 'translate')!;
Expand Down
14 changes: 14 additions & 0 deletions tests/unit/register-write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ describe('registerWrite', () => {
process.exitCode = undefined;
});

describe('alias', () => {
it('registers w as an alias of write', () => {
const writeCmd = program.commands.find(c => c.name() === 'write')!;
expect(writeCmd.aliases()).toContain('w');
});

it('dispatches w to the write action', async () => {
mockWriteCommand.improve.mockResolvedValue('Improved text');
await program.parseAsync(['node', 'test', 'w', 'Hello world']);
expect(mockWriteCommand.improve).toHaveBeenCalledWith('Hello world', expect.any(Object));
expect(Logger.output).toHaveBeenCalledWith('Improved text');
});
});

describe('basic improve (text)', () => {
it('should improve text and output result', async () => {
mockWriteCommand.improve.mockResolvedValue('Improved text');
Expand Down