mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
Merge tag 'v0.1.15' into feature/yiheng/sync-gemini-cli-0.1.15
This commit is contained in:
@@ -50,7 +50,7 @@ function renderHastNode(
|
||||
}
|
||||
|
||||
// Determine the color to pass down: Use this element's specific color
|
||||
// if found, otherwise, continue passing down the already inherited color.
|
||||
// if found; otherwise, continue passing down the already inherited color.
|
||||
const colorToPassDown = elementColor || inheritedColor;
|
||||
|
||||
// Recursively render children, passing the determined color down
|
||||
@@ -68,9 +68,9 @@ function renderHastNode(
|
||||
return <React.Fragment>{children}</React.Fragment>;
|
||||
}
|
||||
|
||||
// Handle Root Node: Start recursion with initial inherited color
|
||||
// Handle Root Node: Start recursion with initially inherited color
|
||||
if (node.type === 'root') {
|
||||
// Check if children array is empty - this happens when lowlight can't detect language – fallback to plain text
|
||||
// Check if children array is empty - this happens when lowlight can't detect language – fall back to plain text
|
||||
if (!node.children || node.children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -88,6 +88,34 @@ function renderHastNode(
|
||||
return null;
|
||||
}
|
||||
|
||||
function highlightAndRenderLine(
|
||||
line: string,
|
||||
language: string | null,
|
||||
theme: Theme,
|
||||
): React.ReactNode {
|
||||
try {
|
||||
const getHighlightedLine = () =>
|
||||
!language || !lowlight.registered(language)
|
||||
? lowlight.highlightAuto(line)
|
||||
: lowlight.highlight(language, line);
|
||||
|
||||
const renderedNode = renderHastNode(getHighlightedLine(), theme, undefined);
|
||||
|
||||
return renderedNode !== null ? renderedNode : line;
|
||||
} catch (_error) {
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
export function colorizeLine(
|
||||
line: string,
|
||||
language: string | null,
|
||||
theme?: Theme,
|
||||
): React.ReactNode {
|
||||
const activeTheme = theme || themeManager.getActiveTheme();
|
||||
return highlightAndRenderLine(line, language, activeTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders syntax-highlighted code for Ink applications using a selected theme.
|
||||
*
|
||||
@@ -100,9 +128,10 @@ export function colorizeCode(
|
||||
language: string | null,
|
||||
availableHeight?: number,
|
||||
maxWidth?: number,
|
||||
theme?: Theme,
|
||||
): React.ReactNode {
|
||||
const codeToHighlight = code.replace(/\n$/, '');
|
||||
const activeTheme = themeManager.getActiveTheme();
|
||||
const activeTheme = theme || themeManager.getActiveTheme();
|
||||
|
||||
try {
|
||||
// Render the HAST tree using the adapted theme
|
||||
@@ -122,11 +151,6 @@ export function colorizeCode(
|
||||
}
|
||||
}
|
||||
|
||||
const getHighlightedLines = (line: string) =>
|
||||
!language || !lowlight.registered(language)
|
||||
? lowlight.highlightAuto(line)
|
||||
: lowlight.highlight(language, line);
|
||||
|
||||
return (
|
||||
<MaxSizedBox
|
||||
maxHeight={availableHeight}
|
||||
@@ -135,17 +159,19 @@ export function colorizeCode(
|
||||
overflowDirection="top"
|
||||
>
|
||||
{lines.map((line, index) => {
|
||||
const renderedNode = renderHastNode(
|
||||
getHighlightedLines(line),
|
||||
const contentToRender = highlightAndRenderLine(
|
||||
line,
|
||||
language,
|
||||
activeTheme,
|
||||
undefined,
|
||||
);
|
||||
|
||||
const contentToRender = renderedNode !== null ? renderedNode : line;
|
||||
return (
|
||||
<Box key={index}>
|
||||
<Text color={activeTheme.colors.Gray}>
|
||||
{`${String(index + 1 + hiddenLinesCount).padStart(padWidth, ' ')} `}
|
||||
{`${String(index + 1 + hiddenLinesCount).padStart(
|
||||
padWidth,
|
||||
' ',
|
||||
)} `}
|
||||
</Text>
|
||||
<Text color={activeTheme.defaultColor} wrap="wrap">
|
||||
{contentToRender}
|
||||
@@ -160,7 +186,7 @@ export function colorizeCode(
|
||||
`[colorizeCode] Error highlighting code for language "${language}":`,
|
||||
error,
|
||||
);
|
||||
// Fallback to plain text with default color on error
|
||||
// Fall back to plain text with default color on error
|
||||
// Also display line numbers in fallback
|
||||
const lines = codeToHighlight.split('\n');
|
||||
const padWidth = String(lines.length).length; // Calculate padding width based on number of lines
|
||||
|
||||
345
packages/cli/src/ui/utils/commandUtils.test.ts
Normal file
345
packages/cli/src/ui/utils/commandUtils.test.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, Mock } from 'vitest';
|
||||
import { spawn } from 'child_process';
|
||||
import { EventEmitter } from 'events';
|
||||
import {
|
||||
isAtCommand,
|
||||
isSlashCommand,
|
||||
copyToClipboard,
|
||||
} from './commandUtils.js';
|
||||
|
||||
// Mock child_process
|
||||
vi.mock('child_process');
|
||||
|
||||
// Mock process.platform for platform-specific tests
|
||||
const mockProcess = vi.hoisted(() => ({
|
||||
platform: 'darwin',
|
||||
}));
|
||||
|
||||
vi.stubGlobal('process', {
|
||||
...process,
|
||||
get platform() {
|
||||
return mockProcess.platform;
|
||||
},
|
||||
});
|
||||
|
||||
interface MockChildProcess extends EventEmitter {
|
||||
stdin: EventEmitter & {
|
||||
write: Mock;
|
||||
end: Mock;
|
||||
};
|
||||
stderr: EventEmitter;
|
||||
}
|
||||
|
||||
describe('commandUtils', () => {
|
||||
let mockSpawn: Mock;
|
||||
let mockChild: MockChildProcess;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
// Dynamically import and set up spawn mock
|
||||
const { spawn } = await import('child_process');
|
||||
mockSpawn = spawn as Mock;
|
||||
|
||||
// Create mock child process with stdout/stderr emitters
|
||||
mockChild = Object.assign(new EventEmitter(), {
|
||||
stdin: Object.assign(new EventEmitter(), {
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
}),
|
||||
stderr: new EventEmitter(),
|
||||
}) as MockChildProcess;
|
||||
|
||||
mockSpawn.mockReturnValue(mockChild as unknown as ReturnType<typeof spawn>);
|
||||
});
|
||||
|
||||
describe('isAtCommand', () => {
|
||||
it('should return true when query starts with @', () => {
|
||||
expect(isAtCommand('@file')).toBe(true);
|
||||
expect(isAtCommand('@path/to/file')).toBe(true);
|
||||
expect(isAtCommand('@')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when query contains @ preceded by whitespace', () => {
|
||||
expect(isAtCommand('hello @file')).toBe(true);
|
||||
expect(isAtCommand('some text @path/to/file')).toBe(true);
|
||||
expect(isAtCommand(' @file')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when query does not start with @ and has no spaced @', () => {
|
||||
expect(isAtCommand('file')).toBe(false);
|
||||
expect(isAtCommand('hello')).toBe(false);
|
||||
expect(isAtCommand('')).toBe(false);
|
||||
expect(isAtCommand('email@domain.com')).toBe(false);
|
||||
expect(isAtCommand('user@host')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when @ is not preceded by whitespace', () => {
|
||||
expect(isAtCommand('hello@file')).toBe(false);
|
||||
expect(isAtCommand('text@path')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSlashCommand', () => {
|
||||
it('should return true when query starts with /', () => {
|
||||
expect(isSlashCommand('/help')).toBe(true);
|
||||
expect(isSlashCommand('/memory show')).toBe(true);
|
||||
expect(isSlashCommand('/clear')).toBe(true);
|
||||
expect(isSlashCommand('/')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when query does not start with /', () => {
|
||||
expect(isSlashCommand('help')).toBe(false);
|
||||
expect(isSlashCommand('memory show')).toBe(false);
|
||||
expect(isSlashCommand('')).toBe(false);
|
||||
expect(isSlashCommand('path/to/file')).toBe(false);
|
||||
expect(isSlashCommand(' /help')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyToClipboard', () => {
|
||||
describe('on macOS (darwin)', () => {
|
||||
beforeEach(() => {
|
||||
mockProcess.platform = 'darwin';
|
||||
});
|
||||
|
||||
it('should successfully copy text to clipboard using pbcopy', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
|
||||
// Simulate successful execution
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 0);
|
||||
}, 0);
|
||||
|
||||
await copyToClipboard(testText);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledWith('pbcopy', []);
|
||||
expect(mockChild.stdin.write).toHaveBeenCalledWith(testText);
|
||||
expect(mockChild.stdin.end).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle pbcopy command failure', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
|
||||
// Simulate command failure
|
||||
setTimeout(() => {
|
||||
mockChild.stderr.emit('data', 'Command not found');
|
||||
mockChild.emit('close', 1);
|
||||
}, 0);
|
||||
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow(
|
||||
"'pbcopy' exited with code 1: Command not found",
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle spawn error', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.emit('error', new Error('spawn error'));
|
||||
}, 0);
|
||||
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow('spawn error');
|
||||
});
|
||||
|
||||
it('should handle stdin write error', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.stdin.emit('error', new Error('stdin error'));
|
||||
}, 0);
|
||||
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow('stdin error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('on Windows (win32)', () => {
|
||||
beforeEach(() => {
|
||||
mockProcess.platform = 'win32';
|
||||
});
|
||||
|
||||
it('should successfully copy text to clipboard using clip', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 0);
|
||||
}, 0);
|
||||
|
||||
await copyToClipboard(testText);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledWith('clip', []);
|
||||
expect(mockChild.stdin.write).toHaveBeenCalledWith(testText);
|
||||
expect(mockChild.stdin.end).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('on Linux', () => {
|
||||
beforeEach(() => {
|
||||
mockProcess.platform = 'linux';
|
||||
});
|
||||
|
||||
it('should successfully copy text to clipboard using xclip', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 0);
|
||||
}, 0);
|
||||
|
||||
await copyToClipboard(testText);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledWith('xclip', [
|
||||
'-selection',
|
||||
'clipboard',
|
||||
]);
|
||||
expect(mockChild.stdin.write).toHaveBeenCalledWith(testText);
|
||||
expect(mockChild.stdin.end).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fall back to xsel when xclip fails', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
let callCount = 0;
|
||||
|
||||
mockSpawn.mockImplementation(() => {
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
stdin: Object.assign(new EventEmitter(), {
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
}),
|
||||
stderr: new EventEmitter(),
|
||||
}) as MockChildProcess;
|
||||
|
||||
setTimeout(() => {
|
||||
if (callCount === 0) {
|
||||
// First call (xclip) fails
|
||||
child.stderr.emit('data', 'xclip not found');
|
||||
child.emit('close', 1);
|
||||
callCount++;
|
||||
} else {
|
||||
// Second call (xsel) succeeds
|
||||
child.emit('close', 0);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return child as unknown as ReturnType<typeof spawn>;
|
||||
});
|
||||
|
||||
await copyToClipboard(testText);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledTimes(2);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(1, 'xclip', [
|
||||
'-selection',
|
||||
'clipboard',
|
||||
]);
|
||||
expect(mockSpawn).toHaveBeenNthCalledWith(2, 'xsel', [
|
||||
'--clipboard',
|
||||
'--input',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw error when both xclip and xsel fail', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
let callCount = 0;
|
||||
|
||||
mockSpawn.mockImplementation(() => {
|
||||
const child = Object.assign(new EventEmitter(), {
|
||||
stdin: Object.assign(new EventEmitter(), {
|
||||
write: vi.fn(),
|
||||
end: vi.fn(),
|
||||
}),
|
||||
stderr: new EventEmitter(),
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
if (callCount === 0) {
|
||||
// First call (xclip) fails
|
||||
child.stderr.emit('data', 'xclip command not found');
|
||||
child.emit('close', 1);
|
||||
callCount++;
|
||||
} else {
|
||||
// Second call (xsel) fails
|
||||
child.stderr.emit('data', 'xsel command not found');
|
||||
child.emit('close', 1);
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return child as unknown as ReturnType<typeof spawn>;
|
||||
});
|
||||
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow(
|
||||
/All copy commands failed/,
|
||||
);
|
||||
|
||||
expect(mockSpawn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('on unsupported platform', () => {
|
||||
beforeEach(() => {
|
||||
mockProcess.platform = 'unsupported';
|
||||
});
|
||||
|
||||
it('should throw error for unsupported platform', async () => {
|
||||
await expect(copyToClipboard('test')).rejects.toThrow(
|
||||
'Unsupported platform: unsupported',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
beforeEach(() => {
|
||||
mockProcess.platform = 'darwin';
|
||||
});
|
||||
|
||||
it('should handle command exit without stderr', async () => {
|
||||
const testText = 'Hello, world!';
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 1);
|
||||
}, 0);
|
||||
|
||||
await expect(copyToClipboard(testText)).rejects.toThrow(
|
||||
"'pbcopy' exited with code 1",
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty text', async () => {
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 0);
|
||||
}, 0);
|
||||
|
||||
await copyToClipboard('');
|
||||
|
||||
expect(mockChild.stdin.write).toHaveBeenCalledWith('');
|
||||
});
|
||||
|
||||
it('should handle multiline text', async () => {
|
||||
const multilineText = 'Line 1\nLine 2\nLine 3';
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 0);
|
||||
}, 0);
|
||||
|
||||
await copyToClipboard(multilineText);
|
||||
|
||||
expect(mockChild.stdin.write).toHaveBeenCalledWith(multilineText);
|
||||
});
|
||||
|
||||
it('should handle special characters', async () => {
|
||||
const specialText = 'Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||
|
||||
setTimeout(() => {
|
||||
mockChild.emit('close', 0);
|
||||
}, 0);
|
||||
|
||||
await copyToClipboard(specialText);
|
||||
|
||||
expect(mockChild.stdin.write).toHaveBeenCalledWith(specialText);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,8 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
/**
|
||||
* Checks if a query string potentially represents an '@' command.
|
||||
* It triggers if the query starts with '@' or contains '@' preceded by whitespace
|
||||
@@ -24,3 +26,57 @@ export const isAtCommand = (query: string): boolean =>
|
||||
* @returns True if the query looks like an '/' command, false otherwise.
|
||||
*/
|
||||
export const isSlashCommand = (query: string): boolean => query.startsWith('/');
|
||||
|
||||
//Copies a string snippet to the clipboard for different platforms
|
||||
export const copyToClipboard = async (text: string): Promise<void> => {
|
||||
const run = (cmd: string, args: string[]) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const child = spawn(cmd, args);
|
||||
let stderr = '';
|
||||
child.stderr.on('data', (chunk) => (stderr += chunk.toString()));
|
||||
child.on('error', reject);
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) return resolve();
|
||||
const errorMsg = stderr.trim();
|
||||
reject(
|
||||
new Error(
|
||||
`'${cmd}' exited with code ${code}${errorMsg ? `: ${errorMsg}` : ''}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
child.stdin.on('error', reject);
|
||||
child.stdin.write(text);
|
||||
child.stdin.end();
|
||||
});
|
||||
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
return run('clip', []);
|
||||
case 'darwin':
|
||||
return run('pbcopy', []);
|
||||
case 'linux':
|
||||
try {
|
||||
await run('xclip', ['-selection', 'clipboard']);
|
||||
} catch (primaryError) {
|
||||
try {
|
||||
// If xclip fails for any reason, try xsel as a fallback.
|
||||
await run('xsel', ['--clipboard', '--input']);
|
||||
} catch (fallbackError) {
|
||||
const primaryMsg =
|
||||
primaryError instanceof Error
|
||||
? primaryError.message
|
||||
: String(primaryError);
|
||||
const fallbackMsg =
|
||||
fallbackError instanceof Error
|
||||
? fallbackError.message
|
||||
: String(fallbackError);
|
||||
throw new Error(
|
||||
`All copy commands failed. xclip: "${primaryMsg}", xsel: "${fallbackMsg}". Please ensure xclip or xsel is installed and configured.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${process.platform}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
isApiError,
|
||||
isStructuredError,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
|
||||
// Free Tier message functions
|
||||
const getRateLimitErrorMessageGoogleFree = (
|
||||
fallbackModel: string = DEFAULT_GEMINI_FLASH_MODEL,
|
||||
|
||||
@@ -29,7 +29,7 @@ This function aims to find an *intelligent* or "safe" index within the provided
|
||||
* **Single Line Breaks:** If no double newline is found in a suitable range, it will look for a single newline (`\n`).
|
||||
* Any newline chosen as a split point must also not be inside a code block.
|
||||
|
||||
4. **Fallback to `idealMaxLength`:**
|
||||
4. **Fall back to `idealMaxLength`:**
|
||||
* If no "safer" split point (respecting code blocks or finding suitable newlines) is identified before or at `idealMaxLength`, and `idealMaxLength` itself is not determined to be an unsafe split point (e.g., inside a code block), the function may return a length larger than `idealMaxLength`, again it CANNOT break markdown formatting. This could happen with very long lines of text without Markdown block structures or newlines.
|
||||
|
||||
**In essence, `findSafeSplitPoint` tries to be a good Markdown citizen when forced to divide content, preferring structural boundaries over arbitrary character limits, with a strong emphasis on not corrupting code blocks.**
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { isBinary } from './textUtils';
|
||||
|
||||
describe('textUtils', () => {
|
||||
describe('isBinary', () => {
|
||||
it('should return true for a buffer containing a null byte', () => {
|
||||
const buffer = Buffer.from([
|
||||
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x1a, 0x0a, 0x00,
|
||||
]);
|
||||
expect(isBinary(buffer)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for a buffer containing only text', () => {
|
||||
const buffer = Buffer.from('This is a test string.');
|
||||
expect(isBinary(buffer)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for an empty buffer', () => {
|
||||
const buffer = Buffer.from([]);
|
||||
expect(isBinary(buffer)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a null or undefined buffer', () => {
|
||||
expect(isBinary(null)).toBe(false);
|
||||
expect(isBinary(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should only check the sample size', () => {
|
||||
const longBufferWithNullByteAtEnd = Buffer.concat([
|
||||
Buffer.from('a'.repeat(1024)),
|
||||
Buffer.from([0x00]),
|
||||
]);
|
||||
expect(isBinary(longBufferWithNullByteAtEnd, 512)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -17,35 +17,6 @@ export const getAsciiArtWidth = (asciiArt: string): number => {
|
||||
return Math.max(...lines.map((line) => line.length));
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a Buffer is likely binary by testing for the presence of a NULL byte.
|
||||
* The presence of a NULL byte is a strong indicator that the data is not plain text.
|
||||
* @param data The Buffer to check.
|
||||
* @param sampleSize The number of bytes from the start of the buffer to test.
|
||||
* @returns True if a NULL byte is found, false otherwise.
|
||||
*/
|
||||
export function isBinary(
|
||||
data: Buffer | null | undefined,
|
||||
sampleSize = 512,
|
||||
): boolean {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const sample = data.length > sampleSize ? data.subarray(0, sampleSize) : data;
|
||||
|
||||
for (const byte of sample) {
|
||||
// The presence of a NULL byte (0x00) is one of the most reliable
|
||||
// indicators of a binary file. Text files should not contain them.
|
||||
if (byte === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If no NULL bytes were found in the sample, we assume it's text.
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* -------------------------------------------------------------------------
|
||||
* Unicode‑aware helpers (work at the code‑point level rather than UTF‑16
|
||||
|
||||
@@ -20,6 +20,23 @@ vi.mock('update-notifier', () => ({
|
||||
describe('checkForUpdates', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
// Clear DEV environment variable before each test
|
||||
delete process.env.DEV;
|
||||
});
|
||||
|
||||
it('should return null when running from source (DEV=true)', async () => {
|
||||
process.env.DEV = 'true';
|
||||
getPackageJson.mockResolvedValue({
|
||||
name: 'test-package',
|
||||
version: '1.0.0',
|
||||
});
|
||||
updateNotifier.mockReturnValue({
|
||||
update: { current: '1.0.0', latest: '1.1.0' },
|
||||
});
|
||||
const result = await checkForUpdates();
|
||||
expect(result).toBeNull();
|
||||
expect(getPackageJson).not.toHaveBeenCalled();
|
||||
expect(updateNotifier).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return null if package.json is missing', async () => {
|
||||
|
||||
@@ -10,6 +10,11 @@ import { getPackageJson } from '../../utils/package.js';
|
||||
|
||||
export async function checkForUpdates(): Promise<string | null> {
|
||||
try {
|
||||
// Skip update check when running from source (development mode)
|
||||
if (process.env.DEV === 'true') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const packageJson = await getPackageJson();
|
||||
if (!packageJson || !packageJson.name || !packageJson.version) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user