-
Notifications
You must be signed in to change notification settings - Fork 825
feat: add mani-man cli wrapper to convert manuals in various lang #1761 #1855
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
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:
Please also sign your commits |
|
@sumitsaurabh927 check now |
π WalkthroughWalkthroughIntroduces 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
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
Estimated code review effortπ― 4 (Complex) | β±οΈ ~60 minutes
π₯ Pre-merge checks | β 2 | β 1β Failed checks (1 warning)
β Passed checks (2 passed)
βοΈ Tip: You can configure your own custom pre-merge checks in the settings. β¨ Finishing touches
Comment |
There was a problem hiding this 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.yamlcan 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 addingtype,main, andenginesfields.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 versioncommunity/mani-man-cli-wrapper/src/translator.ts (1)
62-92: Sequential API calls may be inefficient for large texts.
translateLargeTextmakes 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 vialocalizeObjectwith 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(""); }
|
|
||
| man cli wrapper which uses lingo to convert english manual in various lang |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
|
|
||
| ``` | ||
|
|
||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| ``` | ||
| ~/.lingo-man/cache/ | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| ``` | |
| ~/.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.
| // 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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 }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π§© Analysis chain
π Script executed:
find . -name "executor.ts" | head -20Repository: lingodotdev/lingo.dev
Length of output: 113
π Script executed:
cat -n ./community/mani-man-cli-wrapper/src/executor.tsRepository: 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π§© Analysis chain
π Script executed:
find . -name "formatter.ts" -type f | head -20Repository: lingodotdev/lingo.dev
Length of output: 114
π Script executed:
cat -n ./community/mani-man-cli-wrapper/src/formatter.tsRepository: lingodotdev/lingo.dev
Length of output: 5501
π Script executed:
find . -name "biome.json" -o -name "biome.jsonc" -o -name ".biomerc*" | head -10Repository: lingodotdev/lingo.dev
Length of output: 77
π Script executed:
cat ./biome.jsonRepository: 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 theuflag (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.jsonRepository: 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 jsonRepository: 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 -10Repository: 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.tsRepository: 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}")
EOFRepository: 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/noControlCharactersInRegexabove 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).
| // 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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π§© Analysis chain
π Script executed:
find . -type f -name "formatter.ts" | head -20Repository: lingodotdev/lingo.dev
Length of output: 114
π Script executed:
cat -n community/mani-man-cli-wrapper/src/formatter.ts | head -200Repository: 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.
| // 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.
| 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")); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| // 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); | ||
| } | ||
| } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Summary
Add man cli wrapper for internalization of every command #1761
--helpoutput to your preferred languagemanpages and--help/-hflagsChanges
Checklist
Closes #[1761]
Summary by CodeRabbit
New Features
Documentation
Chores
βοΈ Tip: You can customize this high-level summary in your review settings.