Skip to content

Conversation

@Girishbari
Copy link

@Girishbari Girishbari commented Jan 23, 2026

Summary

Add man cli wrapper for internalization of every command #1761

  • 🌐 Translate any man page or --help output to your preferred language
  • ⚑ Smart caching for instant repeat lookups
  • πŸ“„ Supports both man pages and --help/-h flags
  • 🎨 Preserves terminal formatting where possible
image image

Changes

  • added community project using lingo.dev js sdk

Checklist

  • Changeset added (if version bump needed)
  • Tests cover business logic (not just happy path)
  • No breaking changes (or documented below)

Closes #[1761]

Summary by CodeRabbit

  • New Features

    • CLI tool to translate man pages and command help into many languages
    • Streaming review mode with chunked display and progress prompts
    • Local caching to skip redundant translations
    • Formatting preservation so terminal styling remains intact
  • Documentation

    • Full README with installation, API key setup, usage examples, flags, and license
  • Chores

    • Project scaffolding and package configuration added; common files ignored (build/env artifacts)

✏️ Tip: You can customize this high-level summary in your review settings.

@sumitsaurabh927
Copy link
Contributor

hi @Girishbari your PR is missing changeset. Please add the changeset by following instructions here. Make sure you've joined our discord to see this message. If you haven't you need to:

  1. Join our discord here
  2. Go through the instructions to add changeset and add it to your PR

Please also sign your commits

@Girishbari
Copy link
Author

@sumitsaurabh927 check now

@Girishbari Girishbari changed the title Add man cli wrapper for internalization of every command PR #1761 feat: add mani-man cli wrapper to convert manuals in various lang #1761 Jan 23, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 24, 2026

πŸ“ Walkthrough

Walkthrough

Introduces a new CLI "mani" that translates man pages and help output via Lingo.dev; includes multiple help-execution strategies, paragraph-chunked streaming translation, ANSI formatting preservation, file-based caching, and TypeScript build/configuration.

Changes

Cohort / File(s) Summary
Project config & metadata
.\changeset\jolly-wasps-shop.md, community/mani-man-cli-wrapper/.gitignore, community/mani-man-cli-wrapper/package.json, community/mani-man-cli-wrapper/tsconfig.json
Changeset entry, ignore rules, npm package with CLI bin dist/cli.js, scripts and dependencies, and TypeScript compiler config.
Documentation
community/mani-man-cli-wrapper/README.md
New README covering features, install, usage examples, flags, workflow, supported languages, cache details, and license.
CLI entrypoint
community/mani-man-cli-wrapper/src/cli.ts
Commander-based CLI that executes target commands, manages flags (--lang, --force, --no-cache, --clear-cache, --progress), integrates cache, and triggers streaming translation.
Help execution
community/mani-man-cli-wrapper/src/executor.ts
Attempts to obtain help via man-page names or running the command with --help/-h; returns structured ExecutionResult with output and executed command.
Translation wiring
community/mani-man-cli-wrapper/src/translator.ts
Lingo.dev singleton initialization (env var), translateText and translateLargeText with paragraph chunking and optional progress callbacks; error wrapping.
ANSI formatting preservation
community/mani-man-cli-wrapper/src/formatter.ts
Utilities to strip ANSI, extract/reapply ANSI markers proportionally, and translate while preserving formatting: stripAnsi, translateWithFormatting, translateLinesWithFormatting.
Streaming UI & flow
community/mani-man-cli-wrapper/src/streamer.ts
Paragraph-splitting stream state, chunked translation (CHUNK_SIZE=5), prefetching, interactive prompts (ENTER/SPACE to continue, q/Ctrl+C to quit), and progress display.
Caching layer
community/mani-man-cli-wrapper/src/cache.ts
File-based SHA-256 keyed cache under ~/.lingo-man/cache/ with CacheEntry and APIs: getFromCache, saveToCache, clearCache; validates original content hash.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as "mani CLI"
    participant Executor as "Help Executor"
    participant Cache as "Cache Manager"
    participant Translator as "Lingo Translator"
    participant Streamer as "Streamer"
    participant Display as "Terminal"

    User ->> CLI: mani <command> --lang=es
    CLI ->> Executor: executeHelpCommand(args)
    Executor -->> CLI: ExecutionResult(output, command)

    CLI ->> Cache: getFromCache(command, "es", output)
    alt cache hit
        Cache -->> CLI: cached translation
        CLI ->> Display: print cached translation
    else cache miss
        CLI ->> Streamer: runStreamingTranslation(output, "es")
        loop for each chunk
            Streamer ->> Translator: translateText(chunk, "es")
            Translator -->> Streamer: translated chunk
            Streamer ->> Display: print chunk
            Display -->> Streamer: user input (continue/quit)
        end
        Streamer ->> Cache: saveToCache(command, "es", output, fullTranslation)
        Streamer -->> CLI: translation complete
    end
    CLI -->> User: exit
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🐰
I sniffed the manpages, found words to share,
Sprinkled in Lingo, handled colors with care.
Chunks hop along, you press Enter to seeβ€”
Cached in my burrow for next time, whee! ✨

πŸš₯ Pre-merge checks | βœ… 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides a summary and key features with visual demos, includes a Changes section, and has a partial checklist. However, the Testing section is entirely missing with no tests mentioned or validated. Add a Testing section that documents business logic tests added, specifies what behaviors/edge cases are validated, and confirms all tests pass locally. This is required by the template.
βœ… Passed checks (2 passed)
Check name Status Explanation
Title check βœ… Passed The title clearly describes the main change: adding a CLI wrapper for man page translation into various languages, which aligns with the primary objective of the PR.
Docstring Coverage βœ… Passed Docstring coverage is 90.48% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • πŸ“ Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

πŸ€– Fix all issues with AI agents
In @.changeset/jolly-wasps-shop.md:
- Around line 1-2: The changeset "jolly-wasps-shop" is missing the required YAML
frontmatter; add frontmatter at the top of the file that declares the target
package(s) and the version bump type (patch/minor/major) so the changesets CLI
can parse it (e.g., a frontmatter block with package names and their bump type
or packages/type keys), then keep the existing human-readable description below
that block; ensure the frontmatter is wrapped with --- delimiters and uses valid
YAML keys for package names and bump type.

In `@community/mani-man-cli-wrapper/README.md`:
- Around line 108-110: Update the README code fence for the cache path to
include a language specifier (e.g., change the triple backticks surrounding
"~/.lingo-man/cache/" to "```text") so the snippet is marked as plain text;
locate the code block containing "~/.lingo-man/cache/" and replace the opening
"```" with "```text" to satisfy the static analysis hint.
- Line 38: The README contains a fenced code block with no language specifier
around the "# Run locally" snippet; update that fence (the triple backticks
surrounding the line "# Run locally") to include an appropriate language tag
(e.g., bash) so the block becomes ```bash ... ``` to satisfy static analysis and
enable proper syntax highlighting.
- Around line 20-36: Close the unclosed code block in README.md by adding a
closing ``` after the "npm install" line, then open a new fenced code block
(e.g., ```bash) before the environment variable lines and close it after the env
vars so the "Setup" section renders normally; locate the block around the npm
install and the LINGODOTDEV_API_KEY / LINGO_TARGET_LANG lines to make the fix.

In `@community/mani-man-cli-wrapper/src/cli.ts`:
- Around line 61-67: The code currently calls stripAnsi(result.output) which
permanently removes ANSI formatting before streaming; instead preserve
formatting by either (A) not stripping ANSI and using a new helper
translateLinesWithFormatting that splits result.output into lines/chunks,
translates the text content while retaining or reapplying ANSI sequences per
line, and streams those formatted chunks, or (B) keep a plainText for section
counting (getSectionCount) but pass the original result.output (with ANSI) into
runStreamingTranslation (or update runStreamingTranslation to accept both plain
and formatted inputs) so translation re-applies formatting markers per chunk;
update calls around stripAnsi, runStreamingTranslation, and getSectionCount
(referencing stripAnsi, runStreamingTranslation, getSectionCount, and the new
translateLinesWithFormatting) accordingly.
- Around line 64-80: The early unconditional return after
runStreamingTranslation makes the cache/save block unreachable and leaves
translatedText uninitialized; to fix, change control flow so streaming and
non-streaming paths are separated (e.g., if (streaming) {
console.log(...getSectionCount...); await runStreamingTranslation(plainText,
lang); return; } else { const translatedText = result?.output ?? await
translate(...); if (cache) saveToCache(commandKey, lang, result.output ??
translatedText, translatedText); console.log(`βœ“ Translation cached`); ... }),
ensuring translatedText is assigned before calling saveToCache and that
saveToCache and console output use the correct variable names
(runStreamingTranslation, saveToCache, translatedText, result.output,
commandKey, lang).

In `@community/mani-man-cli-wrapper/src/executor.ts`:
- Around line 1-119: The code is vulnerable to shell injection because
tryManPage and tryHelpFlag use execSync with interpolated command strings;
replace execSync with execFileSync and pass the executable and its arguments as
an array (use the built command name for man and for help use args plus flag as
an array) while preserving options (encoding, env, maxBuffer, timeout). In
tryManPage(), build the man binary name (manCommand) and call
execFileSync("man", [manCommand], options) and fall back to execFileSync("man",
[command], options) for the subcommand branch; in tryHelpFlag(), call
execFileSync(args[0], args.slice(1).concat(flag), options) instead of joining
into a single string and keep the existing error handling by reading
stdout/stderr from the caught error object. Ensure returned
ExecutionResult.command reflects the executed binary and argument list (e.g.,
"man <name>" or "<cmd> <args> <flag>").

In `@community/mani-man-cli-wrapper/src/formatter.ts`:
- Around line 143-151: ANSI_PATTERN is a global regex so calling
ANSI_PATTERN.test(line) can advance its lastIndex and produce intermittent false
negatives; before testing each line (in the block that decides to call
translateWithFormatting vs translateFn) reset ANSI_PATTERN.lastIndex = 0 or use
a non-global test (e.g., String.prototype.match) so the formatting check is
reliable; update the code around the hasFormatting check (where
ANSI_PATTERN.test(line) is used) to reset lastIndex to 0 before the test so
translateWithFormatting(line, translateFn) is invoked whenever ANSI sequences
are present.
- Line 5: The ANSI regex literal contains a raw control character (ANSI_PATTERN)
which trips Biome's noControlCharactersInRegex ruleβ€”either add a lint
suppression comment directly above the ANSI_PATTERN declaration (`//
biome-ignore suspicious/noControlCharactersInRegex`) or refactor to build the
RegExp without a literal control char (e.g., construct via new
RegExp(String.fromCharCode(27) + '\\[[0-9;]*m','g')); additionally, make the
regex non-stateful by resetting ANSI_PATTERN.lastIndex = 0 before each use where
you call ANSI_PATTERN.test(line) (same pattern used in extractFormatMarkers
should already reset; apply the same reset at the spot around the test near the
later usage).

In `@community/mani-man-cli-wrapper/src/streamer.ts`:
- Around line 64-151: The prompt's remaining/progress math uses
state.currentIndex which is advanced by translateNextChunk during background
prefetch, causing the UI to report sections the user hasn't seen; introduce a
separate displayedCount (or displayedIndex) variable tracked in the streamer
loop (initialize to 0), use displayedCount in showPrompt's remaining/progress
calculation instead of state.currentIndex, and only increment displayedCount
when content is actually printed (i.e., after awaiting prefetchPromise or
translateNextChunk and console.log of content); leave
translateNextChunk/state.currentIndex behavior unchanged so prefetch can advance
the internal index but the prompt reflects only displayed sections.
- Around line 114-163: The data handler can have multiple concurrent consumers
awaiting the same prefetchPromise and producing duplicate output; introduce a
short-lived input-guard and consume the shared promise atomically: add a boolean
(e.g., handlingInput) checked at the top of the process.stdin "data" handler to
ignore re-entrant presses while handlingInput is true, and when using
prefetchPromise capture it into a local const (e.g., const currentPrefetch =
prefetchPromise), immediately set prefetchPromise = null before awaiting so the
shared promise is consumed only once, and clear handlingInput after display;
update references to prefetchPromise, translateNextChunk, startPrefetch and
state.isTranslating accordingly to ensure only one waiter prints each chunk.
🧹 Nitpick comments (3)
community/mani-man-cli-wrapper/.gitignore (1)

3-3: Consider committing the lock file for reproducible builds.

Ignoring pnpm-lock.yaml can lead to inconsistent dependency resolution across different environments. Typically, lock files should be committed to ensure reproducible builds.

community/mani-man-cli-wrapper/package.json (1)

1-31: Consider adding type, main, and engines fields.

For better compatibility and clarity:

  • Add "type": "module" or ensure CommonJS is intended
  • Add "main": "./dist/cli.js" for programmatic usage
  • Add "engines" to specify minimum Node.js version
community/mani-man-cli-wrapper/src/translator.ts (1)

62-92: Sequential API calls may be inefficient for large texts.

translateLargeText makes a separate API call per paragraph, which can be slow for large man pages with many paragraphs. Consider batching multiple paragraphs per request or using the SDK's built-in chunking via localizeObject with a mapped payload.

Additionally, rejoining with "\n\n" may not preserve the original whitespace patterns if the source had varying amounts of whitespace between paragraphs.

Potential optimization using batched translation
export async function translateLargeText(
  text: string,
  targetLocale: string,
  onProgress?: (percent: number) => void
): Promise<string> {
  const lingoDotDev = getEngine();
  const paragraphs = text.split(/(\n\s*\n)/); // Capture separators
  const contentParagraphs: { index: number; text: string }[] = [];
  
  // Identify non-empty content paragraphs
  paragraphs.forEach((p, i) => {
    if (p.trim() && !/^\s+$/.test(p)) {
      contentParagraphs.push({ index: i, text: p });
    }
  });

  // Batch translate using SDK's built-in chunking
  const payload: Record<string, string> = {};
  contentParagraphs.forEach((p, i) => {
    payload[`p${i}`] = p.text;
  });

  const translated = await lingoDotDev.localizeObject(payload, {
    sourceLocale: "en",
    targetLocale,
  }, (progress) => onProgress?.(progress));

  // Reconstruct with original separators
  contentParagraphs.forEach((p, i) => {
    paragraphs[p.index] = translated[`p${i}`] || p.text;
  });

  return paragraphs.join("");
}

Comment on lines +1 to +2

man cli wrapper which uses lingo to convert english manual in various lang
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Invalid changeset format β€” missing required frontmatter.

Changesets require YAML frontmatter specifying the package name and version bump type. The current format will not be recognized by the changesets CLI.

Proposed fix
-
-man cli wrapper which uses lingo to convert english manual in various lang
+---
+"lingo-man": patch
+---
+
+feat: add man CLI wrapper which uses Lingo to translate English manuals into various languages
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
man cli wrapper which uses lingo to convert english manual in various lang
---
"lingo-man": patch
---
feat: add man CLI wrapper which uses Lingo to translate English manuals into various languages
πŸ€– Prompt for AI Agents
In @.changeset/jolly-wasps-shop.md around lines 1 - 2, The changeset
"jolly-wasps-shop" is missing the required YAML frontmatter; add frontmatter at
the top of the file that declares the target package(s) and the version bump
type (patch/minor/major) so the changesets CLI can parse it (e.g., a frontmatter
block with package names and their bump type or packages/type keys), then keep
the existing human-readable description below that block; ensure the frontmatter
is wrapped with --- delimiters and uses valid YAML keys for package names and
bump type.


```

```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Add language specifier to code block.

Per static analysis hint, fenced code blocks should have a language specified for proper syntax highlighting.

Proposed fix
-```
+```bash
 # Run locally
🧰 Tools
πŸͺ› markdownlint-cli2 (0.18.1)

38-38: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

πŸ€– Prompt for AI Agents
In `@community/mani-man-cli-wrapper/README.md` at line 38, The README contains a
fenced code block with no language specifier around the "# Run locally" snippet;
update that fence (the triple backticks surrounding the line "# Run locally") to
include an appropriate language tag (e.g., bash) so the block becomes ```bash
... ``` to satisfy static analysis and enable proper syntax highlighting.

Comment on lines +108 to +110
```
~/.lingo-man/cache/
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Add language specifier to code block.

Per static analysis hint, add a language specifier for the cache path code block.

Proposed fix
-```
+```text
 ~/.lingo-man/cache/
 ```
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
~/.lingo-man/cache/
```
🧰 Tools
πŸͺ› markdownlint-cli2 (0.18.1)

108-108: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

πŸ€– Prompt for AI Agents
In `@community/mani-man-cli-wrapper/README.md` around lines 108 - 110, Update the
README code fence for the cache path to include a language specifier (e.g.,
change the triple backticks surrounding "~/.lingo-man/cache/" to "```text") so
the snippet is marked as plain text; locate the code block containing
"~/.lingo-man/cache/" and replace the opening "```" with "```text" to satisfy
the static analysis hint.

Comment on lines +61 to +67
// Step 3: Translate the content
const plainText = stripAnsi(result.output);

// Streaming mode - translate and display in chunks

console.log(`πŸ“Š Total sections: ${getSectionCount(plainText)}`);
await runStreamingTranslation(plainText, lang);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Streaming path drops ANSI formatting permanently.
stripAnsi removes formatting but nothing re-applies it, so β€œpreserve formatting where possible” isn’t met. Consider translating with formatting per line (e.g., translateLinesWithFormatting) or reapplying markers per chunk.

πŸ€– Prompt for AI Agents
In `@community/mani-man-cli-wrapper/src/cli.ts` around lines 61 - 67, The code
currently calls stripAnsi(result.output) which permanently removes ANSI
formatting before streaming; instead preserve formatting by either (A) not
stripping ANSI and using a new helper translateLinesWithFormatting that splits
result.output into lines/chunks, translates the text content while retaining or
reapplying ANSI sequences per line, and streams those formatted chunks, or (B)
keep a plainText for section counting (getSectionCount) but pass the original
result.output (with ANSI) into runStreamingTranslation (or update
runStreamingTranslation to accept both plain and formatted inputs) so
translation re-applies formatting markers per chunk; update calls around
stripAnsi, runStreamingTranslation, and getSectionCount (referencing stripAnsi,
runStreamingTranslation, getSectionCount, and the new
translateLinesWithFormatting) accordingly.

Comment on lines +1 to +119
import { execSync } from "child_process";

export interface ExecutionResult {
success: boolean;
output: string;
command: string;
}

/**
* Executes a help command and captures its output.
* Tries multiple strategies:
* 1. man <command>
* 2. <command> --help
* 3. <command> -h
*/
export function executeHelpCommand(args: string[]): ExecutionResult {
const command = args[0];
const subcommand = args.slice(1).join(" ");

// Strategy 1: Try man page
const manResult = tryManPage(command, subcommand);
if (manResult.success) {
return manResult;
}

// Strategy 2: Try --help flag
const helpResult = tryHelpFlag(args, "--help");
if (helpResult.success) {
return helpResult;
}

// Strategy 3: Try -h flag
const shortHelpResult = tryHelpFlag(args, "-h");
if (shortHelpResult.success) {
return shortHelpResult;
}

// Return the man page error as fallback
return {
success: false,
output: `Could not get help for command: ${args.join(" ")}\nTried:\n - man ${command} ${subcommand}\n - ${args.join(" ")} --help\n - ${args.join(" ")} -h`,
command: args.join(" "),
};
}

function tryManPage(command: string, subcommand: string): ExecutionResult {
try {
// For subcommands like "git commit", try "man git-commit" first
const manCommand = subcommand
? `${command}-${subcommand.replace(/\s+/g, "-")}`
: command;

const output = execSync(`man ${manCommand}`, {
encoding: "utf-8",
env: { ...process.env, PAGER: "cat", MANPAGER: "cat" },
maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large man pages
timeout: 30000,
});

return {
success: true,
output: output.trim(),
command: `man ${manCommand}`,
};
} catch {
// If subcommand format failed, try just the main command
if (subcommand) {
try {
const output = execSync(`man ${command}`, {
encoding: "utf-8",
env: { ...process.env, PAGER: "cat", MANPAGER: "cat" },
maxBuffer: 10 * 1024 * 1024,
timeout: 30000,
});

return {
success: true,
output: output.trim(),
command: `man ${command}`,
};
} catch {
return { success: false, output: "", command: `man ${command}` };
}
}

return { success: false, output: "", command: `man ${command}` };
}
}

function tryHelpFlag(args: string[], flag: string): ExecutionResult {
const fullCommand = [...args, flag].join(" ");

try {
const output = execSync(fullCommand, {
encoding: "utf-8",
maxBuffer: 10 * 1024 * 1024,
timeout: 30000,
});

return {
success: true,
output: output.trim(),
command: fullCommand,
};
} catch (error: unknown) {
// Some commands print help to stderr and exit with non-zero
if (error && typeof error === "object" && "stdout" in error) {
const execError = error as { stdout?: string; stderr?: string };
const combinedOutput = (execError.stdout || "") + (execError.stderr || "");
if (combinedOutput.trim().length > 50) {
return {
success: true,
output: combinedOutput.trim(),
command: fullCommand,
};
}
}

return { success: false, output: "", command: fullCommand };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "executor.ts" | head -20

Repository: lingodotdev/lingo.dev

Length of output: 113


🏁 Script executed:

cat -n ./community/mani-man-cli-wrapper/src/executor.ts

Repository: lingodotdev/lingo.dev

Length of output: 4338


Switch to execFileSync to prevent shell injection.

The code uses execSync with string interpolation (lines 53, 69, 94), which interprets shell metacharacters in arguments. An argument containing backticks, semicolons, or pipes would execute as shell commands. Using execFileSync with an array of arguments bypasses shell interpretation and treats all values as literal arguments to the executable.

Proposed fix
-import { execSync } from "child_process";
+import { execFileSync } from "child_process";

In tryManPage:

-    const output = execSync(`man ${manCommand}`, {
+    const output = execFileSync("man", [manCommand], {
-        const output = execSync(`man ${command}`, {
+        const output = execFileSync("man", [command], {

In tryHelpFlag:

-    const output = execSync(fullCommand, {
+    const output = execFileSync(args[0], [...args.slice(1), flag], {
πŸ€– Prompt for AI Agents
In `@community/mani-man-cli-wrapper/src/executor.ts` around lines 1 - 119, The
code is vulnerable to shell injection because tryManPage and tryHelpFlag use
execSync with interpolated command strings; replace execSync with execFileSync
and pass the executable and its arguments as an array (use the built command
name for man and for help use args plus flag as an array) while preserving
options (encoding, env, maxBuffer, timeout). In tryManPage(), build the man
binary name (manCommand) and call execFileSync("man", [manCommand], options) and
fall back to execFileSync("man", [command], options) for the subcommand branch;
in tryHelpFlag(), call execFileSync(args[0], args.slice(1).concat(flag),
options) instead of joining into a single string and keep the existing error
handling by reading stdout/stderr from the caught error object. Ensure returned
ExecutionResult.command reflects the executed binary and argument list (e.g.,
"man <name>" or "<cmd> <args> <flag>").

* ANSI escape sequence pattern for terminal formatting.
* Matches sequences like \x1b[1m (bold), \x1b[0m (reset), etc.
*/
const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "formatter.ts" -type f | head -20

Repository: lingodotdev/lingo.dev

Length of output: 114


🏁 Script executed:

cat -n ./community/mani-man-cli-wrapper/src/formatter.ts

Repository: lingodotdev/lingo.dev

Length of output: 5501


🏁 Script executed:

find . -name "biome.json" -o -name "biome.jsonc" -o -name ".biomerc*" | head -10

Repository: lingodotdev/lingo.dev

Length of output: 77


🏁 Script executed:

cat ./biome.json

Repository: lingodotdev/lingo.dev

Length of output: 1839


🌐 Web query:

Biome noControlCharactersInRegex lint rule regex escape sequences

πŸ’‘ Result:

Biome’s lint/suspicious/noControlCharactersInRegex flags ASCII control characters (U+0000–U+001F) inside regex patternsβ€”either written directly or via escape forms that evaluate to those characters. It’s recommended (on by default) and has no auto-fix. [1]

What it considers invalid (typical β€œregex escape sequences” that trigger it)

  • Hex escapes: \x00 … \x1F (e.g. /\x0C/) [1]
  • Unicode escapes: \u0000 … \u001F (e.g. /\u000C/) [1]
  • Code point escapes: \u{0} … \u{1F} with the u flag (e.g. /\u{C}/u) [1]
  • Raw control characters U+0000…U+001F embedded in the pattern [1]

Control whitespace escapes like \t and \n are explicitly allowed. [1]

Important gotcha: RegExp("...") strings

If you pass "\\x0C" to new RegExp(...), the regex engine receives \x0C (a control-char escape), so it’s still flagged. [1]

If you meant a literal backslash + text (not a control character)

Write the regex so it matches the characters \x0C literally, e.g.:

  • Regex literal: /\\x0C/ (matches the four characters \, x, 0, C)

Configuration

{
  "linter": {
    "rules": {
      "suspicious": {
        "noControlCharactersInRegex": "error"
      }
    }
  }
}
``` [1]

**Sources:** Biome rule docs. [1]

---

🏁 Script executed:

```shell
rg "noControlCharactersInRegex" ./biome.json

Repository: lingodotdev/lingo.dev

Length of output: 47


🏁 Script executed:

cat -n ./biome.json | grep -A 20 "suspicious"

Repository: lingodotdev/lingo.dev

Length of output: 622


🏁 Script executed:

rg "noControlCharactersInRegex" . --type json

Repository: lingodotdev/lingo.dev

Length of output: 47


🏁 Script executed:

cd community/mani-man-cli-wrapper && find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "ANSI_PATTERN" 2>/dev/null | head -10

Repository: lingodotdev/lingo.dev

Length of output: 83


🏁 Script executed:

# Check if there's a lint suppression or any comments near line 5
sed -n '1,10p' ./community/mani-man-cli-wrapper/src/formatter.ts

Repository: lingodotdev/lingo.dev

Length of output: 372


🏁 Script executed:

python3 << 'EOF'
# Test what happens with global regex and .test()
import re

# Simulate the behavior
pattern_str = r"\x1b\[[0-9;]*m"
lines = ["line1 \x1b[1m formatted", "line2 \x1b[0m reset", "line3 plain"]

# Global regex behavior in JS: lastIndex persists
class GlobalRegex:
    def __init__(self, pattern):
        self.pattern = re.compile(pattern)
        self.lastIndex = 0
    
    def test(self, text):
        # In JS, with global flag, test() uses and updates lastIndex
        match = self.pattern.search(text, self.lastIndex)
        if match:
            self.lastIndex = match.end()
            return True
        else:
            self.lastIndex = 0  # Reset on no match
            return False

regex = GlobalRegex(pattern_str)
print("Testing global regex behavior with .test():")
for line in lines:
    result = regex.test(line)
    print(f"  Line '{line[:20]}...': {result}, lastIndex={regex.lastIndex}")
EOF

Repository: lingodotdev/lingo.dev

Length of output: 151


Use a lint suppression comment or refactor to avoid control characters in the regex literal.

Biome's noControlCharactersInRegex rule flags the control character U+001B (\x1b) in the regex pattern and is enabled by default. The proposed fix using new RegExp("\\u001B...") will not resolve the issue because the escape sequence still evaluates to a control character and triggers the same rule.

Use either:

  • A lint suppression: add // biome-ignore suspicious/noControlCharactersInRegex above line 5, or
  • Construct the pattern dynamically to avoid literal control-character escapes in source code

Additionally, line 144 should reset ANSI_PATTERN.lastIndex = 0; before the .test(line) call to avoid stateful behavior across loop iterations, similar to line 32 in extractFormatMarkers.

🧰 Tools
πŸͺ› Biome (2.1.2)

[error] 5-5: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)

πŸ€– Prompt for AI Agents
In `@community/mani-man-cli-wrapper/src/formatter.ts` at line 5, The ANSI regex
literal contains a raw control character (ANSI_PATTERN) which trips Biome's
noControlCharactersInRegex ruleβ€”either add a lint suppression comment directly
above the ANSI_PATTERN declaration (`// biome-ignore
suspicious/noControlCharactersInRegex`) or refactor to build the RegExp without
a literal control char (e.g., construct via new RegExp(String.fromCharCode(27) +
'\\[[0-9;]*m','g')); additionally, make the regex non-stateful by resetting
ANSI_PATTERN.lastIndex = 0 before each use where you call
ANSI_PATTERN.test(line) (same pattern used in extractFormatMarkers should
already reset; apply the same reset at the spot around the test near the later
usage).

Comment on lines +143 to +151
// Check if line has formatting
const hasFormatting = ANSI_PATTERN.test(line);

if (hasFormatting) {
const translated = await translateWithFormatting(line, translateFn);
translatedLines.push(translated);
} else {
const translated = await translateFn(line);
translatedLines.push(translated);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "formatter.ts" | head -20

Repository: lingodotdev/lingo.dev

Length of output: 114


🏁 Script executed:

cat -n community/mani-man-cli-wrapper/src/formatter.ts | head -200

Repository: lingodotdev/lingo.dev

Length of output: 5501


Reset the global regex before .test() to avoid false negatives.
Because ANSI_PATTERN is global, .test() advances lastIndex and can alternate between true and false on subsequent lines, dropping formatting checks.

🧩 Suggested fix
     // Check if line has formatting
+    ANSI_PATTERN.lastIndex = 0;
     const hasFormatting = ANSI_PATTERN.test(line);
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check if line has formatting
const hasFormatting = ANSI_PATTERN.test(line);
if (hasFormatting) {
const translated = await translateWithFormatting(line, translateFn);
translatedLines.push(translated);
} else {
const translated = await translateFn(line);
translatedLines.push(translated);
// Check if line has formatting
ANSI_PATTERN.lastIndex = 0;
const hasFormatting = ANSI_PATTERN.test(line);
if (hasFormatting) {
const translated = await translateWithFormatting(line, translateFn);
translatedLines.push(translated);
} else {
const translated = await translateFn(line);
translatedLines.push(translated);
πŸ€– Prompt for AI Agents
In `@community/mani-man-cli-wrapper/src/formatter.ts` around lines 143 - 151,
ANSI_PATTERN is a global regex so calling ANSI_PATTERN.test(line) can advance
its lastIndex and produce intermittent false negatives; before testing each line
(in the block that decides to call translateWithFormatting vs translateFn) reset
ANSI_PATTERN.lastIndex = 0 or use a non-global test (e.g.,
String.prototype.match) so the formatting check is reliable; update the code
around the hasFormatting check (where ANSI_PATTERN.test(line) is used) to reset
lastIndex to 0 before the test so translateWithFormatting(line, translateFn) is
invoked whenever ANSI sequences are present.

Comment on lines +64 to +151
const state = createStreamState(text);

// Setup readline for capturing user input
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

// Enable raw mode to capture keystrokes immediately
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}

console.log(`\n${"─".repeat(60)}`);
console.log(`πŸ“– Streaming translation mode (${state.paragraphs.length} sections)`);
console.log(` Press ENTER/SPACE for more, 'q' or Ctrl+C to quit`);
console.log(`${"─".repeat(60)}\n`);

// Translate and display first chunk immediately
state.isTranslating = true;
const firstChunk = await translateNextChunk(state, targetLocale);
state.translatedBuffer.push(...firstChunk);
console.log(firstChunk.join("\n\n"));
state.isTranslating = false;

if (state.isDone) {
console.log(`\n${"─".repeat(60)}`);
console.log("βœ“ End of translation");
rl.close();
process.exit(0);
}

// Pre-fetch next chunk in background
let prefetchPromise: Promise<string[]> | null = null;
const startPrefetch = () => {
if (!state.isDone && !prefetchPromise) {
prefetchPromise = translateNextChunk(state, targetLocale);
}
};
startPrefetch();

// Show continuation prompt
const showPrompt = () => {
const remaining = state.paragraphs.length - state.currentIndex + (prefetchPromise ? CHUNK_SIZE : 0);
const progress = Math.round(((state.paragraphs.length - remaining) / state.paragraphs.length) * 100);
process.stdout.write(`\n[${progress}% - ${remaining} sections remaining] Press ENTER for more, 'q' to quit: `);
};

showPrompt();

// Handle input
process.stdin.on("data", async (key: Buffer) => {
const char = key.toString();

// Handle quit commands
if (char === "q" || char === "Q" || char === "\u0003" || char === "\u001A") {
// q, Q, Ctrl+C, Ctrl+Z
console.log("\n\nπŸ‘‹ Translation stopped by user");
rl.close();
process.exit(0);
}

// Handle continue commands (Enter, Space, arrow down)
if (char === "\r" || char === "\n" || char === " " || char === "\u001B[B") {
if (state.isDone && !prefetchPromise) {
console.log(`\n\n${"─".repeat(60)}`);
console.log("βœ“ End of translation");
rl.close();
process.exit(0);
}

// Clear the prompt line
process.stdout.write("\r" + " ".repeat(80) + "\r");

// Wait for prefetched content or fetch new
let content: string[];
if (prefetchPromise) {
content = await prefetchPromise;
prefetchPromise = null;
} else {
state.isTranslating = true;
content = await translateNextChunk(state, targetLocale);
state.isTranslating = false;
}

// Display content
console.log("\n" + content.join("\n\n"));

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Progress/remaining counts drift when prefetch advances the index.
Because translateNextChunk updates currentIndex during prefetch, the prompt can report remaining sections that the user hasn’t seen yet.

πŸ› οΈ Suggested fix (track displayed count)
-  const state = createStreamState(text);
+  const state = createStreamState(text);
+  let displayedIndex = 0;
@@
-  console.log(firstChunk.join("\n\n"));
+  console.log(firstChunk.join("\n\n"));
+  displayedIndex += firstChunk.length;
@@
-  const showPrompt = () => {
-    const remaining = state.paragraphs.length - state.currentIndex + (prefetchPromise ? CHUNK_SIZE : 0);
-    const progress = Math.round(((state.paragraphs.length - remaining) / state.paragraphs.length) * 100);
+  const showPrompt = () => {
+    const remaining = state.paragraphs.length - displayedIndex;
+    const progress = Math.round((displayedIndex / state.paragraphs.length) * 100);
     process.stdout.write(`\n[${progress}% - ${remaining} sections remaining] Press ENTER for more, 'q' to quit: `);
   };
@@
-      console.log("\n" + content.join("\n\n"));
+      console.log("\n" + content.join("\n\n"));
+      displayedIndex += content.length;
πŸ€– Prompt for AI Agents
In `@community/mani-man-cli-wrapper/src/streamer.ts` around lines 64 - 151, The
prompt's remaining/progress math uses state.currentIndex which is advanced by
translateNextChunk during background prefetch, causing the UI to report sections
the user hasn't seen; introduce a separate displayedCount (or displayedIndex)
variable tracked in the streamer loop (initialize to 0), use displayedCount in
showPrompt's remaining/progress calculation instead of state.currentIndex, and
only increment displayedCount when content is actually printed (i.e., after
awaiting prefetchPromise or translateNextChunk and console.log of content);
leave translateNextChunk/state.currentIndex behavior unchanged so prefetch can
advance the internal index but the prompt reflects only displayed sections.

Comment on lines +114 to +163
// Handle input
process.stdin.on("data", async (key: Buffer) => {
const char = key.toString();

// Handle quit commands
if (char === "q" || char === "Q" || char === "\u0003" || char === "\u001A") {
// q, Q, Ctrl+C, Ctrl+Z
console.log("\n\nπŸ‘‹ Translation stopped by user");
rl.close();
process.exit(0);
}

// Handle continue commands (Enter, Space, arrow down)
if (char === "\r" || char === "\n" || char === " " || char === "\u001B[B") {
if (state.isDone && !prefetchPromise) {
console.log(`\n\n${"─".repeat(60)}`);
console.log("βœ“ End of translation");
rl.close();
process.exit(0);
}

// Clear the prompt line
process.stdout.write("\r" + " ".repeat(80) + "\r");

// Wait for prefetched content or fetch new
let content: string[];
if (prefetchPromise) {
content = await prefetchPromise;
prefetchPromise = null;
} else {
state.isTranslating = true;
content = await translateNextChunk(state, targetLocale);
state.isTranslating = false;
}

// Display content
console.log("\n" + content.join("\n\n"));

// Start prefetching next chunk
if (!state.isDone) {
startPrefetch();
showPrompt();
} else {
console.log(`\n${"─".repeat(60)}`);
console.log("βœ“ End of translation");
rl.close();
process.exit(0);
}
}
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Guard against rapid keypresses reusing the same prefetch.
Multiple data events can concurrently await the same prefetchPromise, causing duplicate output.

🧩 Suggested guard
-  process.stdin.on("data", async (key: Buffer) => {
-    const char = key.toString();
+  let handlingInput = false;
+  process.stdin.on("data", async (key: Buffer) => {
+    if (handlingInput) return;
+    handlingInput = true;
+    try {
+      const char = key.toString();
@@
-    }
-  });
+    } finally {
+      handlingInput = false;
+    }
+  });
πŸ€– Prompt for AI Agents
In `@community/mani-man-cli-wrapper/src/streamer.ts` around lines 114 - 163, The
data handler can have multiple concurrent consumers awaiting the same
prefetchPromise and producing duplicate output; introduce a short-lived
input-guard and consume the shared promise atomically: add a boolean (e.g.,
handlingInput) checked at the top of the process.stdin "data" handler to ignore
re-entrant presses while handlingInput is true, and when using prefetchPromise
capture it into a local const (e.g., const currentPrefetch = prefetchPromise),
immediately set prefetchPromise = null before awaiting so the shared promise is
consumed only once, and clear handlingInput after display; update references to
prefetchPromise, translateNextChunk, startPrefetch and state.isTranslating
accordingly to ensure only one waiter prints each chunk.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants