Publish v1.2.0: Write API extension + Style Rules CRUD + table-format parity#45
Open
sjsyrek wants to merge 24 commits intorelease/v1.1.0-publishfrom
Open
Publish v1.2.0: Write API extension + Style Rules CRUD + table-format parity#45sjsyrek wants to merge 24 commits intorelease/v1.1.0-publishfrom
sjsyrek wants to merge 24 commits intorelease/v1.1.0-publishfrom
Conversation
Extract validLanguages/validStyles/validTones into module-scope WRITE_LANGUAGES/WRITE_STYLES/WRITE_TONES as const arrays, consumed by both the commander option descriptions (interpolated) and the validator branches. Prevents help-text/validator drift as the accepted language and style sets grow. No behavior change.
Extend the DeepL Write API surface to match the March 2026 language additions: Japanese (ja), Korean (ko), and Simplified Chinese (zh, zh-Hans) are now valid --lang values. - WriteLanguage union (src/types/api.ts) grows four literals, preserving alphabetical ordering. - WRITE_LANGUAGES runtime const (register-write.ts) grows four codes; validator and --help interpolation pick them up for free. Tests added: - unit: parametric accept-case per new code + existing reject-path unchanged - integration: nock body-shape assertion for each new target_lang joining the existing "Different Languages" matrix - e2e: validation-exit-code check per new code (exit != 6)
DeepL's March 2026 Write API extension added server-side support
for --tone and --style on Spanish (es), Italian (it), French (fr),
and Portuguese (pt, pt-BR, pt-PT) target languages. The CLI was
already passing these combinations through unchanged; this commit:
- Integration tests lock in the combined (lang, style) and
(lang, tone) happy paths so a future server regression would
be caught by our nock body-shape assertions.
- WriteClient enhances error translation: when a 4xx arrives and
--style or --tone was sent, the ValidationError is re-thrown
with a recovery pointer ("See docs/API.md for supported target
language / style / tone combinations."). Matches the CLI's
"name the thing and the next command" error convention.
- Unit tests cover three cases: 4xx + style (hint appended),
4xx + tone (hint appended), 4xx without style/tone (no hint).
Extend StyleRulesClient with the five new endpoints that shipped with the DeepL API's March 2026 Style Rules additions. Purely the API layer; no CLI surface in this commit. Added methods: createStyleRule(opts) -> POST /v3/style_rules getStyleRule(id, detailed?) -> GET /v3/style_rules/:id updateStyleRule(id, opts) -> PATCH /v3/style_rules/:id deleteStyleRule(id) -> DELETE /v3/style_rules/:id replaceConfiguredRules(id, []) -> PUT /v3/style_rules/:id/configured_rules - Existing getStyleRules (list) is preserved, not renamed; internal snake<->camel mapping hoisted to two small helpers (mapStyleRule, mapStyleRuleDetailed) so all 6 methods share one serialization boundary. - New types CreateStyleRuleOptions and UpdateStyleRuleOptions in src/types/api.ts; all request bodies use snake_case on the wire per DeepL convention, responses are camelCased DTOs to match the rest of the client surface. - Style IDs are URL-encoded in path construction (defensive against any server id format we haven't seen). Tests (14 new, 23 total in file): - Per method: happy path with request-shape assertion (method+url+body), plus one 4xx propagation case. - getStyleRule: additionally covers detailed=true path and URL-encoding of style IDs containing spaces. - createStyleRule: additionally asserts snake_case serialization of customInstructions sourceLanguage field. - updateStyleRule: empty-options body produces empty object, not undefined.
Wire the five new StyleRulesClient methods into the CLI as four glossary-mirrored subcommands under `deepl style-rules`. The PUT /configured_rules endpoint folds into `update --rules`; no separate top-level verb (matches the council's 5-verb top-level surface). Subcommands added: create --name <name> --language <lang> [--rules <csv|json>] [--format text|json] show <id> [--detailed] [--format text|json] update <id> [--name <new>] [--rules <csv|json>] [--format text|json] delete <id> [-y|--yes] [--dry-run] - `--rules` accepts either a comma-separated list or a JSON array, parsed via parseRulesArg() with explicit ValidationError on malformed input. No file-reading mode this commit — a user who needs that can shell-interpolate `$(cat rules.json)`. - `update` requires at least one of --name or --rules, else exit 6. When both are passed, PATCH runs first (rename), then PUT (rules replacement); final state is the detailed rule echoed to the user. - `delete` inherits glossary's interaction model exactly: TTY confirm by default, `--yes` to skip, `--dry-run` to preview. Confirms via utils/confirm.js (same module the glossary path uses). - Text-format rendering of stored user strings (rule name, custom instruction label and prompt) passes through sanitizeForTerminal per the security-seat consensus; JSON format path preserves raw bytes as JSON-escaped (JSON.stringify handles control bytes). - DeepLClient, StyleRulesService, StyleRulesCommand all grow matching proxy methods so the facade chain stays uniform with getStyleRules. Tests (59 new across 5 files): - Unit (StyleRulesCommand): proxy-to-service for all 5 methods; formatStyleRule / formatStyleRuleJson for basic and detailed rules, plus ANSI-escape sanitization cases for both text and JSON paths. - Unit (StyleRulesService): proxy-to-client for all 5 methods. - Integration: CRUD invariants 1-3 per the council plan (create->show round-trip, update->show matches, delete->show 404), replaceConfiguredRules PUT shape assertion, plus CLI flag surface tests for the 4 new subcommands (required-args, dry-run preview, missing-options exit code). - E2E: --help output for each new subcommand. Mock factories extended for the 5 new DeepLClient and StyleRulesService methods so downstream command-facade unit tests compile.
Extend StyleRulesClient with the four Custom Instructions endpoints that ship under each Style Rule in the DeepL API's March 2026 additions. No list endpoint exists server-side — the CLI list verb will synthesize from getStyleRule().customInstructions. Added methods: createCustomInstruction(styleId, opts) getCustomInstruction(styleId, label) updateCustomInstruction(styleId, label, opts) deleteCustomInstruction(styleId, label) - CustomInstructionWireShape + mapCustomInstruction hoisted alongside existing StyleRule mappers; all snake<->camel conversion in one place. - New types CreateCustomInstructionOptions (label+prompt+sourceLanguage?) and UpdateCustomInstructionOptions (prompt?+sourceLanguage?) in src/types/api.ts. Update intentionally cannot change the label — label is the URL-path identifier. - styleId and label path segments are both URL-encoded. Tests (11 new, 34 total in file): - Per method: happy path with request-shape assertion (method+url+body) plus one 4xx propagation case. - createCustomInstruction: snake_case serialization of sourceLanguage. - getCustomInstruction: URL-encoding verified for both path segments. - updateCustomInstruction: empty options produces empty body.
Wire the four Custom Instructions client methods into the CLI as four glossary-mirrored subcommands. Surface mirrors glossary's dual pattern exactly — read-noun + flat sibling mutations: instructions <style-id> # list (synthesized from getStyleRule) add-instruction <style-id> <label> <prompt> [--source-language <lang>] update-instruction <style-id> <label> <prompt> [--source-language <lang>] remove-instruction <style-id> <label> [-y|--yes] [--dry-run] - listInstructions reads the nested customInstructions[] from a detailed getStyleRule response — no separate LIST endpoint exists server-side. This is a deliberate read-verb-as-noun, not a shortcut. - remove-instruction inherits the destructive-op UX per the council consensus (user-authored text warrants confirmation): TTY confirm by default, --yes to skip, --dry-run to preview. - update-instruction cannot change the label — label is the URL-path identifier; callers rename by remove + add. - Text-format output of CustomInstruction.label and .prompt passes through sanitizeForTerminal; JSON path unchanged (JSON.stringify escapes control bytes at the encoding layer). - DeepLClient, StyleRulesService, StyleRulesCommand all grow matching proxy methods so the facade chain stays uniform with Style Rules. Tests (35 new across 5 files): - Unit (StyleRulesCommand): listInstructions synthesis from detailed getStyleRule (both with-array and without-array response branches); proxy-to-service for add/update/remove; formatCustomInstruction for basic + sourceLanguage variant; ANSI sanitization in text, JSON preservation of raw strings. - Unit (StyleRulesService): proxy-to-client for all 4 methods. - Integration: Custom Instructions create->get round-trip, list-from- detailed-getStyleRule synthesis, update->delete sequence; CLI flag surface tests for the 4 new subcommands (required-args, --dry-run preview, --source-language flag acceptance). - E2E: --help output for each new subcommand. Mock factories extended for the 4 new client+service methods.
Close out the API-parity bundle with the user-facing documentation that tells someone reading a release note exactly what changed and lets them copy-paste a working invocation. CHANGELOG.md [Unreleased]: - Six Added bullets: Write CJK langs; Write tone/style on Romance variants; Write 4xx docs-hint; Style Rules CRUD; custom instructions management; two new examples. - All bullets describe user-visible behavior, not internal mechanics. - No bead ids, council jargon, or ADR refs — public-remote clean. docs/API.md: - Write section: --lang list updated to 14 codes; "Supports 14 target languages" synopsis; new target-language / style-and-tone compat table. The 4xx-hint behavior is documented alongside the table so users who hit the unsupported-combination error know where to look. - style-rules section: full rewrite. The stale "created via web UI, not through the API" note is gone; list is preserved verbatim and eight new subcommands (create / show / update / delete / instructions / add-instruction / update-instruction / remove-instruction) get synopsis + args + options + examples each. The text-output sanitization contract is called out in Notes. examples/35-style-rules-crud.sh: - End-to-end lifecycle script: create rule (JSON output to capture the id), show detailed, replace configured rules, add/list/update/ remove custom instruction, rename, delete. Cleanup trap removes the style rule on mid-script failure. Free-tier API keys short-circuit gracefully — the Pro-only error is expected and the script exits 0. examples/36-write-extended-languages.sh: - Ten-step demo: JA/KO/ZH/zh-Hans target-language invocations plus tone/style on ES/IT/FR/PT-BR/PT-PT, concluding with an auto-detect rephrase of a Korean input to verify the round-trip. examples/run-all.sh + examples/README.md: - Both scripts registered in the full and fast EXAMPLES arrays and in the README Write / Configuration sections.
Seven integration tests added in the Style Rules CRUD and custom-instructions commits asserted error output would match /API key|auth/i, which is only true when the test harness runs without a DEEPL_API_KEY set. With a real key the subcommands reach the network, the server rejects the fake "sr-1" style id with a 400, and the assertion fails. The existing `list`-shape tests in the same describe block do not fail the same way because list returns an empty result on a real free-tier key — the catch block is never entered and the assertion is never evaluated. Commands that take a positional id do not have that accidental escape. Fix: pass `excludeApiKey: true` to runCLI on the seven affected tests, matching the pattern already used by the "without API key" describe block above. Also applied to the `create --name Foo --language en` test, which would otherwise hit the network on a real key and create an actual style rule — a side-effect worse than a flaky failure. Tests still pass against both keyless and keyed test environments; no source code change.
Coverage threshold dipped to 93.5% (below the 94% floor) after the
Style Rules CRUD and custom-instructions commits added 8 new
subcommand action handlers. The integration tests that exercise
them via runCLI run in a subprocess, so Jest's parent-process
coverage instrumentation doesn't see them.
Add in-process unit tests that drive each handler directly via
commander's parseAsync, with createStyleRulesCommand and Logger
mocked. Covers:
- list: text + JSON output paths, error handling
- create: required flags, --rules parsing (CSV + JSON array +
malformed + non-string-array rejection), JSON output
- show: basic + --detailed + JSON + error propagation
- update: PATCH-only, PUT-only, both-at-once, missing-both
ValidationError, JSON output
- delete: --yes fast-path, --dry-run preview, confirmed prompt,
declined-prompt abort
- instructions: text + JSON, error path
- add-instruction: required positionals, --source-language,
JSON output
- update-instruction: same three
- remove-instruction: --yes, --dry-run, declined prompt
register-style-rules.ts function coverage: 12.5% -> 100%.
Global function coverage: 93.5% -> 94.57%.
…t table The DeepL API models configured_rules as a two-level dictionary (category → setting → value), not a flat list of rule IDs. The implementation typed it as string[], which caused PUT requests to fail with a server-side type-conversion error and silently elided the Rules: line in text output (length on a runtime object is undefined). Fixes: - Add ConfiguredRules type = Record<string, Record<string, string>> - Wire shape, mapper, formatters (text + new table), service signatures all carry the dict shape. - PUT /configured_rules sends the rules dict as the body directly; the configured_rules outer wrapper is only used on POST/PATCH where the body has multiple top-level fields. - --rules now requires JSON object input; rejects arrays and validates the two-level shape with clear error messages. Also adds --format table to style-rules list and instructions (addressed by the prior council's UX review), with a non-TTY fallback to plain text matching the translate --format table pattern. Tests: integration assertions now verify the unwrapped PUT body and the nested rule shape; unit tests cover the new parser branches, the new table formatters (including ANSI sanitization on user-authored keys and values), and the TTY-vs-non-TTY handler paths. Example 35 updated: switches to fr (the punctuation/quotation_mark rule applies only to French) and replaces the now-invalid CSV --rules form with a JSON object. Style-id capture switches from grep+sed to jq, which avoids tripping on colored grep output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…igned id
The DeepL API's per-instruction endpoints
(GET/PUT/DELETE /v3/style_rules/{style_id}/custom_instructions/{instruction_id})
take a server-assigned UUID as the URL path segment, not the
user-facing label. The CLI was sending label in the URL, producing
"Invalid or missing instruction id" 400s on every update/delete/get.
The CLI's user-facing positional argument is the label (per the
2026-04-23 council UX decision), so the client now does a label→id
lookup via a detailed getStyleRule before each per-instruction
operation. This costs one extra GET per mutation, acceptable for a CLI.
Also fixes:
- The PUT body for update-instruction must include label, even though
instruction_id is in the URL (server requires it).
- CustomInstruction now carries the optional server-assigned id field;
the wire mapper preserves it through both the dedicated mapper and
the rule-detail nested mapping path.
- ValidationError raised with a clear "no custom instruction with
label X" message when the lookup misses.
Tests: client unit tests now queue a lookup response before the
operation response and assert the URL contains the resolved UUID.
Integration test asserts the full lookup-then-act flow with two nock
scopes per operation. Existing test for missing-label was rewritten to
exercise the lookup path rather than rely on a 404 from the server.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prior runs that aborted before the end-of-script cleanup left glossaries with the demo names on the server. Subsequent runs hit "Entry already exists" when re-adding the test database entry, and "Multiple glossaries share the name" when resolving names that had collected duplicates across many aborted runs. Adds an upfront step 0 that lists glossaries as JSON, finds every glossary id matching one of the five demo names the script uses (tech-terms-demo / tech-terms-renamed / tech-final / business-terms-demo / multi-demo), and deletes each by UUID. UUID-based deletion sidesteps the multi-match disambiguation error. Falls back to a noop with a warning when jq is not installed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…acOS symlink mismatch The script cd's into /tmp/deepl-sync-demo and then ran `deepl sync export --output "$PROJECT_DIR/handoff.xlf"`. On macOS /tmp is a symlink to /private/tmp, so process.cwd() in node returns the resolved /private/tmp/... path while $PROJECT_DIR retains the unresolved /tmp/... form. loadSyncConfig captures the resolved /private/tmp form for projectRoot; --output stays as /tmp/...; and assertPathWithinRoot uses path.resolve (which does not follow symlinks) so the two never match and the export aborts with "Target path escapes project root". The script already cd's into the project dir, so a relative path works correctly (path.resolve(projectRoot, 'handoff.xlf') stays inside the resolved root). The underlying overly-strict path-equality check is a separate concern affecting any path containing symlinks; it should be addressed in a dedicated fix to assertPathWithinRoot, not bundled into an example fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ket config Three independent bugs in the live-validation script were combining to exit silently after Phase 1, hiding failures from later phases: 1. ((PASS++)) under set -e exits with status 1 when PASS is 0 (post-increment evaluates to the OLD value; arithmetic context treats 0 as failure). The trap cleanup masked the resulting silent exit. Replaced with PASS=$((PASS + 1)) plain assignment, which always returns 0. 2. `assert "desc" echo "$X" | python3 -c "..."` parses the pipe at the assert call level — assert validates `echo` (always 0) and the pipe carries assert's own "✓ desc\n" stdout into python instead of the captured JSON. Wrapped each pipeline in a helper function (_check_status_json_valid / _check_status_source_en) so assert can run them atomically. 3. Phase 7 created the source PO at locales/po/messages.po with no source-locale segment in the path, so resolveTargetPath could not compute target paths. Moved the source to locales/en/po/messages.po so the engine substitutes en → de etc. for targets, and updated the leftover "alt path" assertion that had been masking the resulting FAIL with `|| true`. 4. Phase 8 used bucket key `xml` — the registered key for Android XML is `android_xml`. Switched and added target_path_pattern to handle Android's locale-via-directory-suffix layout (values/ → values-de/). Final result: 30 passed / 0 failed across all nine phases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
\`deepl sync --force\` triggers an interactive y/n confirmation prompt
("Retranslate all keys and bypass cost cap?") via confirm() when
stdin.isTTY is true and --yes is absent (register-sync-root.ts:127-143).
The check is on the --force flag itself, regardless of --dry-run.
Phase 6 had \`deepl sync --dry-run --force\` (no --yes), which is fine
under the Bash tool (stdin is a pipe, the TTY guard short-circuits) but
hangs forever in a user's interactive terminal — confirm() reads from
stdin and there is no input.
Adds --yes to both the run and the assert_exit_code wrapper for that
line. Confirmed end-to-end: 30 passed / 0 failed unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…heck \`path.resolve\` performs lexical normalization only; it does not follow symlinks. So a project rooted under a symlinked directory — the macOS \`/tmp\` → \`/private/tmp\` case is the canonical example — produced false-positive "Target path escapes project root" rejections whenever the user typed one form and the sync engine had captured the other. Concrete failure that surfaced this: example 30 cd's into \`/tmp/deepl-sync-demo\`, then \`loadSyncConfig\` resolves the project root via \`process.cwd()\` to \`/private/tmp/deepl-sync-demo\` (Node follows the symlink). When the script passed \`--output /tmp/deepl-sync-demo/handoff.xlf\` (the unresolved typed form), \`/tmp/...\`.startsWith(\`/private/tmp/.../\`) was false → rejected. Fix: introduce \`realpathOrAncestor\` that calls \`fs.realpathSync\` on each side of the comparison. \`realpathSync\` only works on existing paths, but the output side is typically a path that doesn't exist yet (it's about to be created). \`realpathOrAncestor\` handles that by walking up to the closest existing ancestor, resolving that, and re-appending the unresolved tail so the result is the symlink-resolved form of the would-be path. Defense-in-depth bonus: symlink-based escapes (a symlink inside the project pointing outside, e.g. \`<root>/escape -> /etc\`) are now also rejected, where previously they slipped through the lexical check. Tests: four new cases under \`assertPathWithinRoot\` exercise (1) symlink project-root with realpath output, (2) realpath project-root with symlink output (the example-30 shape), (3) symlink-escape rejection, (4) non-existent output paths under existing roots. The fixture creates a real tmpdir + symlink portably under \`os.tmpdir()\`. Also reverts the workaround in example 30 (commit 75479b9): the absolute \`$PROJECT_DIR/handoff.xlf\` path is now valid again. Closes sync-kxri. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-visible: anyone with a project under a symlinked path (the macOS /tmp → /private/tmp case is canonical, but also home directories that mount through a symlink, /var → /private/var, custom dev environments) will see different behavior on commands that pass through assertPathWithinRoot. Worth surfacing in the release notes alongside the Added items so the change isn't a surprise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ats, usage The three commands had advertised \`table\` in their \`--format\` choices list since 1.x, but the action handlers only branched on \`'json'\` before falling through to the text path. \`--format table\` therefore silently produced text output with no warning, no error. Adds dedicated table formatters to each command class: - \`LanguagesCommand.formatLanguagesTable\` and \`formatAllLanguagesTable\` render Code/Name/Category columns (plus Formality for target lists whose rows report it). Empty lists render an empty-state line. - \`CacheCommand.formatStatsTable\` renders a five-row Metric/Value table (Status, Entries, Used, Limit, Usage). - \`UsageCommand.formatUsageTable\` renders a Resource/Used/Limit/Usage table with rows for each present quota dimension (characters, account units, API key units or characters, speech-to-text). When the response includes a product breakdown, an additional table follows with one row per product type. The handler-side wiring in each register-* file mirrors the established pattern from \`translate\` and \`style-rules\`: branch on \`'table'\`, render via the new formatter, and fall back to the existing text formatter with a \`Logger.warn\` when stdout is not a TTY (cli-table3's Unicode box-drawing characters are noise in pipes and CI logs). Tests: nine new formatter unit tests across the three command classes covering happy path, edge cases (empty lists, zero limits), and the optional sections (formality column, product breakdown). Twelve new handler-wiring tests under register-* for the TTY/non-TTY branching; created \`tests/unit/register-usage.test.ts\` for the previously-untested register-usage module. Closes sync-xfow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Investigated sync-uit7 with `--runInBand --detectOpenHandles` and pinned the cause: nock v14's @mswjs/interceptors backend constructs a synthetic Node IncomingMessage on every \`replyWithError(...)\` call and never drains it, leaving an HTTPINCOMINGMESSAGE handle pinned in the test worker. Six call sites (three in tests/unit/deepl-client.test.ts, three in integration tests) account for all seven leaked handles. Verified that none of the available jest knobs help: - \`forceExit: true\` does NOT suppress the worker-side warning (it fires from the worker before the main process forceExit kicks in) and adds its own "Force exiting Jest" noise on top. - \`--runInBand\` does eliminate the warning (no workers means no worker-exit warning) but slows the full suite from 38s to 195s — 5× cost. Fixing the leak upstream (nock or @mswjs/interceptors) is out of scope for this project. Replacing replyWithError with a hand-rolled mock across six tests would also work but is disproportionate effort for benign noise. Recording the finding in jest.config.js so future maintainers don't chase this same dead end, and adding \`npm run test:debug\` (--runInBand --detectOpenHandles) so any NEW handle leak that isn't one of these six known sites surfaces clearly during audit. Closes sync-uit7 as won't-fix (upstream limitation). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…erpolation
The DeepL HTTP client interpolates the server's JSON \`responseData.message\`
field into two user-facing error strings:
src/api/http-client.ts:393 Server error (5xx): \${message}
src/api/http-client.ts:399 API error: \${message}
Without sanitization, a buggy or malicious server returning a body
containing ANSI escape codes or control characters could scribble on
the user's terminal — move the cursor, change colors, clear the screen,
overwrite previous output. The TMS client was already hardened against
this (sync-pagq.7); the main client wasn't, and the 2026-04-23 council
flagged the gap as a nice-to-have deferred post-1.2.0.
Sanitizing at the source — once, on the \`message\` local — covers both
downstream interpolation sites plus any other branch that uses it.
\`?? ''\` coalesce because some axios error shapes have no \`.message\`
field and \`sanitizeForTerminal\` expects a string.
Tests: three new cases under the existing error-handling describe in
deepl-client.test.ts — mirrors the TMS-client hardening test pattern at
tests/unit/sync/tms-client.test.ts:253. Covers 4xx, 5xx, and bidi
override codepoints.
Closes sync-knl5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…note
Tech-writer's pre-ship review flagged two minor nits:
1. The "languages/cache stats/usage --format table" entry described a
bug fix (flag was advertised in --help but the action handler only
branched on 'json', so --format table silently produced text) yet
was filed under ### Added. Per Keep-a-Changelog conventions, "flag
never worked, now does" belongs under ### Fixed. Moved.
2. The custom-instructions Added entry called the positional arguments
abstractly ("the style-rule id and the instruction label as
positional arguments") which was incomplete: update-instruction also
takes a third <prompt> positional. Spelled out the full signature
for each subcommand.
No code changes — just CHANGELOG hygiene before the 1.2.0 cut.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump VERSION and package.json to 1.2.0; promote CHANGELOG [Unreleased] to [1.2.0] - 2026-04-25 with a fresh empty Unreleased stub above; update docs/API.md header (Version + Last Updated). Release scope: - Write API extension: JA/KO/ZH/zh-Hans target languages and tone/style support for ES/IT/FR/PT variants - Style Rules: full CRUD + Custom Instructions CRUD with configured-rules dict shape (category → setting → value), label → server-id lookup for per-instruction operations - Table format parity: --format table now actually renders on languages/cache stats/usage (was advertised but unwired) - Sync hardening: assertPathWithinRoot follows symlinks; the macOS /tmp → /private/tmp false-positive that broke example 30 is fixed - Security: server-returned error messages sanitized through sanitizeForTerminal before terminal interpolation (defense-in-depth parity with TMS client) - Five fresh example scripts validated end-to-end against the live DeepL API: 15-glossaries, 30-sync-basic, 32-sync-live-validation, 35-style-rules-crud, 36-write-extended-languages Tests: 5490 passing (up from 5448 at branch start). Lint + typecheck clean. Two design-council reviews on file (2026-04-24 ship review, 2026-04-25 final pre-ship review) with verdicts SHIP and SHIP. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Release v1.2.0: API parity for Write extension and Style Rules CRUD, plus sync symlink hardening, table-format parity for languages/cache/ usage, and http-client error-message sanitization. See merge request hack-projects/deepl-cli!4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on top of #44 (
release/v1.1.0-publish). Once #44 merges, GitHub will rebase this PR's base ontomainand the diff will narrow to just the v1.2.0 work.Summary
ja), Korean (ko), and Simplified Chinese (zh,zh-Hans) target languages;--toneand--styleextended to Spanish, Italian, French, and Portuguese variants; 4xx responses with--style/--toneset carry an actionable recovery hint pointing atdocs/API.md.deepl style-rules create|show|update|deletealongside the existinglist.--rulestakes a JSON object of category → settings (e.g.'{"punctuation":{"quotation_mark":"use_guillemets"}}') matching the DeepL API's two-level rule shape.instructions <id>(list) plusadd-instruction <style-id> <label> <prompt>,update-instruction <style-id> <label> <prompt>,remove-instruction <style-id> <label>. Per-instruction operations resolve label → server-assigned UUID before hitting the URL path.--format tablenow actually renders acli-table3table onlanguages,cache stats,usage,style-rules list, andstyle-rules instructions. Previously the flag was advertised in--helponlanguages/cache/usagebut the action handlers only branched on'json'. All five commands fall back to plain text with aWARNline on stderr in non-TTY contexts.Changes Made
Added
deepl writeacceptsja,ko,zh,zh-Hanstarget languages--toneand--styleextended toes,it,fr,pt,pt-BR,pt-PTdeepl style-rulesfull CRUD subcommands (create,show,update,delete)deepl style-rulescustom instructions management (instructions,add-instruction,update-instruction,remove-instruction)--format tableonstyle-rules list/instructions(with non-TTY fallback)examples/35-style-rules-crud.shandexamples/36-write-extended-languages.sh— end-to-end workflow demosFixed
--format tablenow actually renders onlanguages,cache stats, andusage(was previously a silent no-op)deepl sync export --output <path>no longer rejects valid output paths under symlinked project roots (the macOS/tmp→/private/tmpcase).assertPathWithinRootnow resolves both sides throughfs.realpathbefore comparing. Symlink-based escape attempts are now also rejected as a defense-in-depth bonus.Security
sanitizeForTerminalbefore interpolation into user-facingAPI error: …andServer error (5xx): …strings. Defense-in-depth against a malicious or buggy server scribbling ANSI escape codes / control characters on the user's terminal via the error path. Mirrors the existing TMS-client hardening.Backward Compatibility
✅ 100% backward-compatible. No existing flag, method, or endpoint renamed or removed. All new subcommands are additive. The
WriteLanguagetype union extends rather than replacing prior values. TheCustomInstruction.idfield is optional and only present on responses (never sent on create).❌ Breaking changes: None.
Test Coverage
tsc --noEmitclean15-glossaries.sh— 32s30-sync-basic.sh— 1s32-sync-live-validation.sh— 6s, 30/30 phase assertions pass35-style-rules-crud.sh— 6s, 9-phase round-trip36-write-extended-languages.sh— 7s, JA/KO/ZH + ES/IT/FR/PT round-tripTechnical Details
The most consequential change is the
configured_rulesdata shape. The DeepL API models this as a two-level dictionary:```json
{
"punctuation": {
"quotation_mark": "use_guillemets",
"spacing_and_punctuation": "do_not_use_space"
},
"spelling_and_grammar": {
"accents_and_cedillas": "use_even_on_capital_letters"
}
}
```
Empty rules are `{}`. The PUT endpoint at
/v3/style_rules/{id}/configured_rulestakes the rules dict as the entire body (noconfigured_rulesouter wrapper); POST and PATCH on the rule itself wrap it underconfigured_rulesbecause they have multiple top-level fields.Custom instructions are addressed in URLs by a server-assigned UUID, not by user-facing label. The CLI's positional argument is the label (per the
instructions <style-id>+ flat sibling pattern matching the glossary precedent), so per-instruction operations do agetStyleRule(detailed=true)lookup to resolve label → id before hitting the URL path. One extra GET per mutation; acceptable for a CLI.Benefits
translate --format table(cli-table3+isColorEnabled()forNO_COLOR+ non-TTY fallback). Future commands gain--format tableconsistently.Size: Medium ✓
23 source/test commits + 1 release-cut commit. 45 files changed, +4135 / −135. Two design-council reviews on file (2026-04-24 ship review, 2026-04-25 final pre-ship review); both verdict SHIP.
🤖 Generated with Claude Code