📦 Release qwen-code CLI as a Standalone Bundled Package (#866)

This commit is contained in:
tanzhenxin
2025-10-24 17:08:59 +08:00
committed by GitHub
parent 5cf609c367
commit be633a80cc
36 changed files with 802 additions and 1077 deletions

View File

@@ -0,0 +1,258 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest';
import {
canUseRipgrep,
ensureRipgrepPath,
getRipgrepPath,
} from './ripgrepUtils.js';
import { fileExists } from './fileUtils.js';
import path from 'node:path';
// Mock fileUtils
vi.mock('./fileUtils.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('./fileUtils.js')>();
return {
...actual,
fileExists: vi.fn(),
};
});
describe('ripgrepUtils', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('getRipgrepPath', () => {
it('should return path with .exe extension on Windows', () => {
const originalPlatform = process.platform;
const originalArch = process.arch;
// Mock Windows x64
Object.defineProperty(process, 'platform', { value: 'win32' });
Object.defineProperty(process, 'arch', { value: 'x64' });
const rgPath = getRipgrepPath();
expect(rgPath).toContain('x64-win32');
expect(rgPath).toContain('rg.exe');
expect(rgPath).toContain(path.join('vendor', 'ripgrep'));
// Restore original values
Object.defineProperty(process, 'platform', { value: originalPlatform });
Object.defineProperty(process, 'arch', { value: originalArch });
});
it('should return path without .exe extension on macOS', () => {
const originalPlatform = process.platform;
const originalArch = process.arch;
// Mock macOS arm64
Object.defineProperty(process, 'platform', { value: 'darwin' });
Object.defineProperty(process, 'arch', { value: 'arm64' });
const rgPath = getRipgrepPath();
expect(rgPath).toContain('arm64-darwin');
expect(rgPath).toContain('rg');
expect(rgPath).not.toContain('.exe');
expect(rgPath).toContain(path.join('vendor', 'ripgrep'));
// Restore original values
Object.defineProperty(process, 'platform', { value: originalPlatform });
Object.defineProperty(process, 'arch', { value: originalArch });
});
it('should return path without .exe extension on Linux', () => {
const originalPlatform = process.platform;
const originalArch = process.arch;
// Mock Linux x64
Object.defineProperty(process, 'platform', { value: 'linux' });
Object.defineProperty(process, 'arch', { value: 'x64' });
const rgPath = getRipgrepPath();
expect(rgPath).toContain('x64-linux');
expect(rgPath).toContain('rg');
expect(rgPath).not.toContain('.exe');
expect(rgPath).toContain(path.join('vendor', 'ripgrep'));
// Restore original values
Object.defineProperty(process, 'platform', { value: originalPlatform });
Object.defineProperty(process, 'arch', { value: originalArch });
});
it('should throw error for unsupported platform', () => {
const originalPlatform = process.platform;
const originalArch = process.arch;
// Mock unsupported platform
Object.defineProperty(process, 'platform', { value: 'freebsd' });
Object.defineProperty(process, 'arch', { value: 'x64' });
expect(() => getRipgrepPath()).toThrow('Unsupported platform: freebsd');
// Restore original values
Object.defineProperty(process, 'platform', { value: originalPlatform });
Object.defineProperty(process, 'arch', { value: originalArch });
});
it('should throw error for unsupported architecture', () => {
const originalPlatform = process.platform;
const originalArch = process.arch;
// Mock unsupported architecture
Object.defineProperty(process, 'platform', { value: 'darwin' });
Object.defineProperty(process, 'arch', { value: 'ia32' });
expect(() => getRipgrepPath()).toThrow('Unsupported architecture: ia32');
// Restore original values
Object.defineProperty(process, 'platform', { value: originalPlatform });
Object.defineProperty(process, 'arch', { value: originalArch });
});
it('should handle all supported platform/arch combinations', () => {
const originalPlatform = process.platform;
const originalArch = process.arch;
const combinations: Array<{
platform: string;
arch: string;
}> = [
{ platform: 'darwin', arch: 'x64' },
{ platform: 'darwin', arch: 'arm64' },
{ platform: 'linux', arch: 'x64' },
{ platform: 'linux', arch: 'arm64' },
{ platform: 'win32', arch: 'x64' },
];
combinations.forEach(({ platform, arch }) => {
Object.defineProperty(process, 'platform', { value: platform });
Object.defineProperty(process, 'arch', { value: arch });
const rgPath = getRipgrepPath();
const binaryName = platform === 'win32' ? 'rg.exe' : 'rg';
const expectedPathSegment = path.join(
`${arch}-${platform}`,
binaryName,
);
expect(rgPath).toContain(expectedPathSegment);
});
// Restore original values
Object.defineProperty(process, 'platform', { value: originalPlatform });
Object.defineProperty(process, 'arch', { value: originalArch });
});
});
describe('canUseRipgrep', () => {
it('should return true if ripgrep binary exists', async () => {
(fileExists as Mock).mockResolvedValue(true);
const result = await canUseRipgrep();
expect(result).toBe(true);
expect(fileExists).toHaveBeenCalledOnce();
});
it('should return false if ripgrep binary does not exist', async () => {
(fileExists as Mock).mockResolvedValue(false);
const result = await canUseRipgrep();
expect(result).toBe(false);
expect(fileExists).toHaveBeenCalledOnce();
});
it('should return false if platform is unsupported', async () => {
const originalPlatform = process.platform;
// Mock unsupported platform
Object.defineProperty(process, 'platform', { value: 'aix' });
const result = await canUseRipgrep();
expect(result).toBe(false);
expect(fileExists).not.toHaveBeenCalled();
// Restore original value
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('should return false if architecture is unsupported', async () => {
const originalArch = process.arch;
// Mock unsupported architecture
Object.defineProperty(process, 'arch', { value: 's390x' });
const result = await canUseRipgrep();
expect(result).toBe(false);
expect(fileExists).not.toHaveBeenCalled();
// Restore original value
Object.defineProperty(process, 'arch', { value: originalArch });
});
});
describe('ensureRipgrepBinary', () => {
it('should return ripgrep path if binary exists', async () => {
(fileExists as Mock).mockResolvedValue(true);
const rgPath = await ensureRipgrepPath();
expect(rgPath).toBeDefined();
expect(rgPath).toContain('rg');
expect(fileExists).toHaveBeenCalledOnce();
expect(fileExists).toHaveBeenCalledWith(rgPath);
});
it('should throw error if binary does not exist', async () => {
(fileExists as Mock).mockResolvedValue(false);
await expect(ensureRipgrepPath()).rejects.toThrow(
/Ripgrep binary not found/,
);
await expect(ensureRipgrepPath()).rejects.toThrow(/Platform:/);
await expect(ensureRipgrepPath()).rejects.toThrow(/Architecture:/);
expect(fileExists).toHaveBeenCalled();
});
it('should throw error with correct path information', async () => {
(fileExists as Mock).mockResolvedValue(false);
try {
await ensureRipgrepPath();
// Should not reach here
expect(true).toBe(false);
} catch (error) {
expect(error).toBeInstanceOf(Error);
const errorMessage = (error as Error).message;
expect(errorMessage).toContain('Ripgrep binary not found at');
expect(errorMessage).toContain(process.platform);
expect(errorMessage).toContain(process.arch);
}
});
it('should throw error if platform is unsupported', async () => {
const originalPlatform = process.platform;
// Mock unsupported platform
Object.defineProperty(process, 'platform', { value: 'openbsd' });
await expect(ensureRipgrepPath()).rejects.toThrow(
'Unsupported platform: openbsd',
);
// Restore original value
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
});
});

View File

@@ -0,0 +1,114 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { fileExists } from './fileUtils.js';
// Get the directory of the current module
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
type Platform = 'darwin' | 'linux' | 'win32';
type Architecture = 'x64' | 'arm64';
/**
* Maps process.platform values to vendor directory names
*/
function getPlatformString(platform: string): Platform {
switch (platform) {
case 'darwin':
case 'linux':
case 'win32':
return platform;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
/**
* Maps process.arch values to vendor directory names
*/
function getArchitectureString(arch: string): Architecture {
switch (arch) {
case 'x64':
case 'arm64':
return arch;
default:
throw new Error(`Unsupported architecture: ${arch}`);
}
}
/**
* Returns the path to the bundled ripgrep binary for the current platform
*/
export function getRipgrepPath(): string {
const platform = getPlatformString(process.platform);
const arch = getArchitectureString(process.arch);
// Binary name includes .exe on Windows
const binaryName = platform === 'win32' ? 'rg.exe' : 'rg';
// Path resolution:
// When running from transpiled code: dist/src/utils/ripgrepUtils.js -> ../../../vendor/ripgrep/
// When running from bundle: dist/index.js -> vendor/ripgrep/
// Detect if we're running from a bundle (single file)
// In bundle, __filename will be something like /path/to/dist/index.js
// In transpiled code, __filename will be /path/to/dist/src/utils/ripgrepUtils.js
const isBundled = !__filename.includes(path.join('src', 'utils'));
const vendorPath = isBundled
? path.join(
__dirname,
'vendor',
'ripgrep',
`${arch}-${platform}`,
binaryName,
)
: path.join(
__dirname,
'..',
'..',
'..',
'vendor',
'ripgrep',
`${arch}-${platform}`,
binaryName,
);
return vendorPath;
}
/**
* Checks if ripgrep binary is available
*/
export async function canUseRipgrep(): Promise<boolean> {
try {
const rgPath = getRipgrepPath();
return await fileExists(rgPath);
} catch (_error) {
// Unsupported platform/arch
return false;
}
}
/**
* Ensures ripgrep binary exists and returns its path
* @throws Error if ripgrep binary is not available
*/
export async function ensureRipgrepPath(): Promise<string> {
const rgPath = getRipgrepPath();
if (!(await fileExists(rgPath))) {
throw new Error(
`Ripgrep binary not found at ${rgPath}. ` +
`Platform: ${process.platform}, Architecture: ${process.arch}`,
);
}
return rgPath;
}