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: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,4 @@ jobs:
- run: npm run lint
- run: npm run type-check
- run: npm run build
- name: Link CLI for E2E tests
run: npm link
- run: npm test
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **docs**: All install instructions now target `@deepl/cli` (10 occurrences across `docs/SYNC.md`, four example scripts, `examples/README.md`, and the git-hook template in `src/services/git-hooks.ts`). The README installation section documents three install paths — Homebrew (`brew install deepl/tap/deepl`, which manages Node itself), npm (`npm install -g @deepl/cli`), and from source — with an explicit Node.js 24 prerequisite, replacing the `better-sqlite3` native-compilation caveat (Xcode CLT / python3-make-gcc), which no longer applies. The `TROUBLESHOOTING.md` `NODE_MODULE_VERSION` / `npm rebuild better-sqlite3` entry is replaced by accurate guidance for the one remaining cache-degradation cause (running on Node < 24). Six stale `DeepLcom` GitHub URLs now point at the `DeepL` org directly. `CONTRIBUTING.md` states the Node 24 development prerequisite, and `SECURITY.md`'s supported-versions table reflects that only 2.x is a published, supported line.
- **tests**: Removed the global manual mocks for `p-limit` and `fast-glob` (`tests/__mocks__/`). Because a manual mock for a node module is auto-applied to every suite, and `resetMocks: true` strips its implementation, `pLimit(n)` resolved to `undefined` and `fg(...)` resolved to `undefined` in all 236 suites — so no test exercised a real concurrency limit or a real glob walk, and two concurrency defects reached a release candidate undetected. The suites that need these mocked declare them explicitly with their own implementations, so nothing depended on the global versions. Added `tests/unit/concurrency-limiting.test.ts`, which asserts that `p-limit` rejects a non-positive concurrency, that peak overlap never exceeds the limit, and that `fast-glob` returns real paths — it fails if either global mock is reintroduced.
- **tests**: Suites that shell out to the bare `deepl` command now run this tree's built CLI via a PATH shim installed in jest `globalSetup`, instead of whatever `deepl` happens to be globally installed. 23 suites (392 tests) previously required a global install — absent one they all failed with `command not found`, and present one they silently tested the installed version rather than the working tree. The shim lives in the real environment before jest workers spawn (a `setupFilesAfterEnv` hook cannot do this: test code sees a copied `process.env` that child processes never inherit). CI's now-redundant `npm link` step is removed, and a new unit suite pins that `deepl` resolves to the shim and reports the tree's version.
- **tests**: `npm test` now fails fast with an actionable message when `dist/cli/index.js` is missing. `dist/` is gitignored and `npm test` does not build, yet all three test tiers execute the built CLI, so a fresh checkout previously failed hundreds of tests with errors that never mentioned the missing build.
- **tests**: Jest's haste map no longer indexes `.claude/` (via `modulePathIgnorePatterns`). A leftover agent worktree under `.claude/worktrees/` duplicated the manual mocks in `tests/__mocks__/` and made every jest run emit a `jest-haste-map: duplicate manual mock found` warning; test files in worktrees were already excluded, but module indexing was not. Verified warning-free with a worktree present.
- **BREAKING — package**: The package is now published as the scoped **`@deepl/cli`** (previously the unpublished working name `deepl-cli`). Scoped packages default to restricted visibility, so `publishConfig.access: "public"` is set explicitly — without it the publish fails. The `bin` name is unchanged: the command is still `deepl`, and scoping changes only the install string (`npm install -g @deepl/cli`). Repository, bugs, and homepage metadata now point at `github.com/DeepL/deepl-cli` directly instead of relying on the redirect from the legacy `DeepLcom` org name.
Expand Down
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export default {
],

// Setup files
globalSetup: '<rootDir>/tests/global-setup.ts',
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],

// Test timeout
Expand Down
12 changes: 12 additions & 0 deletions tests/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import installHermeticDeepl from './hermetic-deepl';

/**
* Runs in the main jest process before workers spawn, so the shim's PATH entry
* lands in the real environment every worker (and every CLI subprocess the
* tests spawn) inherits. A setupFilesAfterEnv hook cannot do this: test code
* sees a copied process.env, and mutations there never reach child processes
* spawned without an explicit env.
*/
export default function globalSetup(): void {
installHermeticDeepl();
}
27 changes: 27 additions & 0 deletions tests/hermetic-deepl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Puts a `deepl` shim on PATH that execs this repo's built CLI, so tests that
* shell out to the bare `deepl` command always run dist/cli/index.js — never a
* globally installed copy, which may be absent (failing every such test with
* "command not found") or a different version than the tree under test.
*
* The shim directory is created once per jest worker process and advertised
* through DEEPL_CLI_TEST_SHIM so subsequent suites in the same worker reuse it.
*/

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';

const CLI_ENTRY = path.join(process.cwd(), 'dist', 'cli', 'index.js');

export default function installHermeticDeepl(): void {
const existing = process.env['DEEPL_CLI_TEST_SHIM'];
if (existing && fs.existsSync(path.join(existing, 'deepl'))) return;

const shimDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deepl-cli-shim-'));
fs.writeFileSync(path.join(shimDir, 'deepl'), `#!/bin/sh\nexec node "${CLI_ENTRY}" "$@"\n`, {
mode: 0o755,
});
process.env['DEEPL_CLI_TEST_SHIM'] = shimDir;
process.env['PATH'] = `${shimDir}${path.delimiter}${process.env['PATH'] ?? ''}`;
}
29 changes: 29 additions & 0 deletions tests/unit/hermetic-deepl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Tests that the bare `deepl` command resolves to the test shim and executes
* this tree's built CLI, so suites that shell out to `deepl` are independent
* of any globally installed copy.
*/

import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';

describe('hermetic deepl shim', () => {
it('resolves `deepl` to the shim directory, ahead of any global install', () => {
const resolved = execSync('command -v deepl', {
encoding: 'utf-8',
shell: '/bin/sh',
}).trim();

expect(resolved).toBe(path.join(process.env['DEEPL_CLI_TEST_SHIM']!, 'deepl'));
});

it('executes the built CLI from this tree', () => {
const version = execSync('deepl --version', { encoding: 'utf-8' }).trim();
const pkg = JSON.parse(
fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'),
) as { version: string };

expect(version).toBe(pkg.version);
});
});