mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: release qwen-code CLI packages as a standalone bundled js, with necessary vendors
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
MCPServerStatus,
|
||||
getErrorMessage,
|
||||
MCPOAuthTokenStorage,
|
||||
MCPOAuthProvider,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { appEvents, AppEvent } from '../../utils/events.js';
|
||||
import { MessageType, type HistoryItemMcpStatus } from '../types.js';
|
||||
@@ -93,9 +94,6 @@ const authCommand: SlashCommand = {
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
// Import dynamically to avoid circular dependencies
|
||||
const { MCPOAuthProvider } = await import('@qwen-code/qwen-code-core');
|
||||
|
||||
let oauthConfig = server.oauth;
|
||||
if (!oauthConfig) {
|
||||
oauthConfig = { enabled: false };
|
||||
|
||||
@@ -14,14 +14,16 @@
|
||||
"format": "prettier --write .",
|
||||
"test": "vitest run",
|
||||
"test:ci": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"postinstall": "node scripts/postinstall.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
"dist",
|
||||
"vendor",
|
||||
"scripts/postinstall.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"@google/genai": "1.16.0",
|
||||
"@joshua.litt/get-ripgrep": "^0.0.2",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-logs-otlp-grpc": "^0.203.0",
|
||||
|
||||
85
packages/core/scripts/postinstall.js
Normal file
85
packages/core/scripts/postinstall.js
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Get the package root directory
|
||||
const packageRoot = path.join(__dirname, '..');
|
||||
const vendorDir = path.join(packageRoot, 'vendor', 'ripgrep');
|
||||
|
||||
/**
|
||||
* Remove quarantine attribute and set executable permissions on macOS/Linux
|
||||
*/
|
||||
function setupRipgrepBinaries() {
|
||||
if (!fs.existsSync(vendorDir)) {
|
||||
console.log('Vendor directory not found, skipping ripgrep setup');
|
||||
return;
|
||||
}
|
||||
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
|
||||
// Determine the binary directory based on platform and architecture
|
||||
let binaryDir;
|
||||
if (platform === 'darwin' || platform === 'linux') {
|
||||
const archStr = arch === 'x64' || arch === 'arm64' ? arch : null;
|
||||
if (archStr) {
|
||||
binaryDir = path.join(vendorDir, `${archStr}-${platform}`);
|
||||
}
|
||||
} else if (platform === 'win32') {
|
||||
// Windows doesn't need these fixes
|
||||
return;
|
||||
}
|
||||
|
||||
if (!binaryDir || !fs.existsSync(binaryDir)) {
|
||||
console.log(
|
||||
`Binary directory not found for ${platform}-${arch}, skipping ripgrep setup`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const rgBinary = path.join(binaryDir, 'rg');
|
||||
|
||||
if (!fs.existsSync(rgBinary)) {
|
||||
console.log(`Ripgrep binary not found at ${rgBinary}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Set executable permissions
|
||||
fs.chmodSync(rgBinary, 0o755);
|
||||
console.log(`✓ Set executable permissions on ${rgBinary}`);
|
||||
|
||||
// On macOS, remove quarantine attribute
|
||||
if (platform === 'darwin') {
|
||||
try {
|
||||
execSync(`xattr -d com.apple.quarantine "${rgBinary}"`, {
|
||||
stdio: 'pipe',
|
||||
});
|
||||
console.log(`✓ Removed quarantine attribute from ${rgBinary}`);
|
||||
} catch (error) {
|
||||
// Quarantine attribute might not exist, which is fine
|
||||
if (error.message && !error.message.includes('No such xattr')) {
|
||||
console.warn(
|
||||
`Warning: Could not remove quarantine attribute: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error setting up ripgrep binary: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
setupRipgrepBinaries();
|
||||
@@ -38,7 +38,8 @@ vi.mock('fs', async (importOriginal) => {
|
||||
import { ShellTool } from '../tools/shell.js';
|
||||
import { ReadFileTool } from '../tools/read-file.js';
|
||||
import { GrepTool } from '../tools/grep.js';
|
||||
import { RipGrepTool, canUseRipgrep } from '../tools/ripGrep.js';
|
||||
import { canUseRipgrep } from '../utils/ripgrepUtils.js';
|
||||
import { RipGrepTool } from '../tools/ripGrep.js';
|
||||
import { logRipgrepFallback } from '../telemetry/loggers.js';
|
||||
import { RipgrepFallbackEvent } from '../telemetry/types.js';
|
||||
import { ToolRegistry } from '../tools/tool-registry.js';
|
||||
@@ -75,9 +76,11 @@ vi.mock('../tools/ls');
|
||||
vi.mock('../tools/read-file');
|
||||
vi.mock('../tools/grep.js');
|
||||
vi.mock('../tools/ripGrep.js', () => ({
|
||||
canUseRipgrep: vi.fn(),
|
||||
RipGrepTool: class MockRipGrepTool {},
|
||||
}));
|
||||
vi.mock('../utils/ripgrepUtils.js', () => ({
|
||||
canUseRipgrep: vi.fn(),
|
||||
}));
|
||||
vi.mock('../tools/glob');
|
||||
vi.mock('../tools/edit');
|
||||
vi.mock('../tools/shell');
|
||||
|
||||
@@ -49,7 +49,8 @@ import { LSTool } from '../tools/ls.js';
|
||||
import { MemoryTool, setGeminiMdFilename } from '../tools/memoryTool.js';
|
||||
import { ReadFileTool } from '../tools/read-file.js';
|
||||
import { ReadManyFilesTool } from '../tools/read-many-files.js';
|
||||
import { canUseRipgrep, RipGrepTool } from '../tools/ripGrep.js';
|
||||
import { canUseRipgrep } from '../utils/ripgrepUtils.js';
|
||||
import { RipGrepTool } from '../tools/ripGrep.js';
|
||||
import { ShellTool } from '../tools/shell.js';
|
||||
import { SmartEditTool } from '../tools/smart-edit.js';
|
||||
import { TaskTool } from '../tools/task.js';
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
type Mock,
|
||||
} from 'vitest';
|
||||
import type { RipGrepToolParams } from './ripGrep.js';
|
||||
import { canUseRipgrep, RipGrepTool, ensureRgPath } from './ripGrep.js';
|
||||
import { RipGrepTool } from './ripGrep.js';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import os, { EOL } from 'node:os';
|
||||
@@ -22,24 +22,11 @@ import type { Config } from '../config/config.js';
|
||||
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { downloadRipGrep } from '@joshua.litt/get-ripgrep';
|
||||
import { fileExists } from '../utils/fileUtils.js';
|
||||
import { ensureRipgrepPath } from '../utils/ripgrepUtils.js';
|
||||
|
||||
// Mock dependencies for canUseRipgrep
|
||||
vi.mock('@joshua.litt/get-ripgrep', () => ({
|
||||
downloadRipGrep: vi.fn(),
|
||||
}));
|
||||
vi.mock('../utils/fileUtils.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../utils/fileUtils.js')>();
|
||||
return {
|
||||
...actual,
|
||||
fileExists: vi.fn(),
|
||||
};
|
||||
});
|
||||
vi.mock('../config/storage.js', () => ({
|
||||
Storage: {
|
||||
getGlobalBinDir: vi.fn().mockReturnValue('/mock/bin/dir'),
|
||||
},
|
||||
// Mock ripgrepUtils
|
||||
vi.mock('../utils/ripgrepUtils.js', () => ({
|
||||
ensureRipgrepPath: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock child_process for ripgrep calls
|
||||
@@ -49,97 +36,6 @@ vi.mock('child_process', () => ({
|
||||
|
||||
const mockSpawn = vi.mocked(spawn);
|
||||
|
||||
describe('canUseRipgrep', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return true if ripgrep already exists', async () => {
|
||||
(fileExists as Mock).mockResolvedValue(true);
|
||||
const result = await canUseRipgrep();
|
||||
expect(result).toBe(true);
|
||||
expect(fileExists).toHaveBeenCalledWith(path.join('/mock/bin/dir', 'rg'));
|
||||
expect(downloadRipGrep).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should download ripgrep and return true if it does not exist initially', async () => {
|
||||
(fileExists as Mock)
|
||||
.mockResolvedValueOnce(false)
|
||||
.mockResolvedValueOnce(true);
|
||||
(downloadRipGrep as Mock).mockResolvedValue(undefined);
|
||||
|
||||
const result = await canUseRipgrep();
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(fileExists).toHaveBeenCalledTimes(2);
|
||||
expect(downloadRipGrep).toHaveBeenCalledWith('/mock/bin/dir');
|
||||
});
|
||||
|
||||
it('should return false if download fails and file does not exist', async () => {
|
||||
(fileExists as Mock).mockResolvedValue(false);
|
||||
(downloadRipGrep as Mock).mockResolvedValue(undefined);
|
||||
|
||||
const result = await canUseRipgrep();
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(fileExists).toHaveBeenCalledTimes(2);
|
||||
expect(downloadRipGrep).toHaveBeenCalledWith('/mock/bin/dir');
|
||||
});
|
||||
|
||||
it('should propagate errors from downloadRipGrep', async () => {
|
||||
const error = new Error('Download failed');
|
||||
(fileExists as Mock).mockResolvedValue(false);
|
||||
(downloadRipGrep as Mock).mockRejectedValue(error);
|
||||
|
||||
await expect(canUseRipgrep()).rejects.toThrow(error);
|
||||
expect(fileExists).toHaveBeenCalledTimes(1);
|
||||
expect(downloadRipGrep).toHaveBeenCalledWith('/mock/bin/dir');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ensureRgPath', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return rg path if ripgrep already exists', async () => {
|
||||
(fileExists as Mock).mockResolvedValue(true);
|
||||
const rgPath = await ensureRgPath();
|
||||
expect(rgPath).toBe(path.join('/mock/bin/dir', 'rg'));
|
||||
expect(fileExists).toHaveBeenCalledOnce();
|
||||
expect(downloadRipGrep).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return rg path if ripgrep is downloaded successfully', async () => {
|
||||
(fileExists as Mock)
|
||||
.mockResolvedValueOnce(false)
|
||||
.mockResolvedValueOnce(true);
|
||||
(downloadRipGrep as Mock).mockResolvedValue(undefined);
|
||||
const rgPath = await ensureRgPath();
|
||||
expect(rgPath).toBe(path.join('/mock/bin/dir', 'rg'));
|
||||
expect(downloadRipGrep).toHaveBeenCalledOnce();
|
||||
expect(fileExists).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should throw an error if ripgrep cannot be used after download attempt', async () => {
|
||||
(fileExists as Mock).mockResolvedValue(false);
|
||||
(downloadRipGrep as Mock).mockResolvedValue(undefined);
|
||||
await expect(ensureRgPath()).rejects.toThrow('Cannot use ripgrep.');
|
||||
expect(downloadRipGrep).toHaveBeenCalledOnce();
|
||||
expect(fileExists).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should propagate errors from downloadRipGrep', async () => {
|
||||
const error = new Error('Download failed');
|
||||
(fileExists as Mock).mockResolvedValue(false);
|
||||
(downloadRipGrep as Mock).mockRejectedValue(error);
|
||||
|
||||
await expect(ensureRgPath()).rejects.toThrow(error);
|
||||
expect(fileExists).toHaveBeenCalledTimes(1);
|
||||
expect(downloadRipGrep).toHaveBeenCalledWith('/mock/bin/dir');
|
||||
});
|
||||
});
|
||||
|
||||
// Helper function to create mock spawn implementations
|
||||
function createMockSpawn(
|
||||
options: {
|
||||
@@ -201,8 +97,7 @@ describe('RipGrepTool', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
(downloadRipGrep as Mock).mockResolvedValue(undefined);
|
||||
(fileExists as Mock).mockResolvedValue(true);
|
||||
(ensureRipgrepPath as Mock).mockResolvedValue('/mock/path/to/rg');
|
||||
mockSpawn.mockClear();
|
||||
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-root-'));
|
||||
grepTool = new RipGrepTool(mockConfig);
|
||||
@@ -551,16 +446,18 @@ describe('RipGrepTool', () => {
|
||||
});
|
||||
|
||||
it('should throw an error if ripgrep is not available', async () => {
|
||||
// Make ensureRgPath throw
|
||||
(fileExists as Mock).mockResolvedValue(false);
|
||||
(downloadRipGrep as Mock).mockResolvedValue(undefined);
|
||||
// Make ensureRipgrepBinary throw
|
||||
(ensureRipgrepPath as Mock).mockRejectedValue(
|
||||
new Error('Ripgrep binary not found'),
|
||||
);
|
||||
|
||||
const params: RipGrepToolParams = { pattern: 'world' };
|
||||
const invocation = grepTool.build(params);
|
||||
|
||||
expect(await invocation.execute(abortSignal)).toStrictEqual({
|
||||
llmContent: 'Error during grep search operation: Cannot use ripgrep.',
|
||||
returnDisplay: 'Error: Cannot use ripgrep.',
|
||||
llmContent:
|
||||
'Error during grep search operation: Ripgrep binary not found',
|
||||
returnDisplay: 'Error: Ripgrep binary not found',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,44 +8,16 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { EOL } from 'node:os';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { downloadRipGrep } from '@joshua.litt/get-ripgrep';
|
||||
import type { ToolInvocation, ToolResult } from './tools.js';
|
||||
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
|
||||
import { SchemaValidator } from '../utils/schemaValidator.js';
|
||||
import { makeRelative, shortenPath } from '../utils/paths.js';
|
||||
import { getErrorMessage, isNodeError } from '../utils/errors.js';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { fileExists } from '../utils/fileUtils.js';
|
||||
import { Storage } from '../config/storage.js';
|
||||
import { ensureRipgrepPath } from '../utils/ripgrepUtils.js';
|
||||
|
||||
const DEFAULT_TOTAL_MAX_MATCHES = 20000;
|
||||
|
||||
function getRgPath(): string {
|
||||
return path.join(Storage.getGlobalBinDir(), 'rg');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if `rg` exists, if not then attempt to download it.
|
||||
*/
|
||||
export async function canUseRipgrep(): Promise<boolean> {
|
||||
if (await fileExists(getRgPath())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await downloadRipGrep(Storage.getGlobalBinDir());
|
||||
return await fileExists(getRgPath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures `rg` is downloaded, or throws.
|
||||
*/
|
||||
export async function ensureRgPath(): Promise<string> {
|
||||
if (await canUseRipgrep()) {
|
||||
return getRgPath();
|
||||
}
|
||||
throw new Error('Cannot use ripgrep.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for the GrepTool
|
||||
*/
|
||||
@@ -320,7 +292,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
rgArgs.push(absolutePath);
|
||||
|
||||
try {
|
||||
const rgPath = await ensureRgPath();
|
||||
const rgPath = await ensureRipgrepPath();
|
||||
const output = await new Promise<string>((resolve, reject) => {
|
||||
const child = spawn(rgPath, rgArgs, {
|
||||
windowsHide: true,
|
||||
@@ -342,11 +314,7 @@ class GrepToolInvocation extends BaseToolInvocation<
|
||||
|
||||
child.on('error', (err) => {
|
||||
options.signal.removeEventListener('abort', cleanup);
|
||||
reject(
|
||||
new Error(
|
||||
`Failed to start ripgrep: ${err.message}. Please ensure @lvce-editor/ripgrep is properly installed.`,
|
||||
),
|
||||
);
|
||||
reject(new Error(`Failed to start ripgrep: ${err.message}.`));
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
|
||||
254
packages/core/src/utils/ripgrepUtils.test.ts
Normal file
254
packages/core/src/utils/ripgrepUtils.test.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* @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;
|
||||
expected: string;
|
||||
}> = [
|
||||
{ platform: 'darwin', arch: 'x64', expected: 'x64-darwin/rg' },
|
||||
{ platform: 'darwin', arch: 'arm64', expected: 'arm64-darwin/rg' },
|
||||
{ platform: 'linux', arch: 'x64', expected: 'x64-linux/rg' },
|
||||
{ platform: 'linux', arch: 'arm64', expected: 'arm64-linux/rg' },
|
||||
{ platform: 'win32', arch: 'x64', expected: 'x64-win32/rg.exe' },
|
||||
];
|
||||
|
||||
combinations.forEach(({ platform, arch, expected }) => {
|
||||
Object.defineProperty(process, 'platform', { value: platform });
|
||||
Object.defineProperty(process, 'arch', { value: arch });
|
||||
|
||||
const rgPath = getRipgrepPath();
|
||||
expect(rgPath).toContain(expected);
|
||||
});
|
||||
|
||||
// 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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
114
packages/core/src/utils/ripgrepUtils.ts
Normal file
114
packages/core/src/utils/ripgrepUtils.ts
Normal 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;
|
||||
}
|
||||
3
packages/core/vendor/ripgrep/COPYING
vendored
Normal file
3
packages/core/vendor/ripgrep/COPYING
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
This project is dual-licensed under the Unlicense and MIT licenses.
|
||||
|
||||
You may use this code under the terms of either license.
|
||||
BIN
packages/core/vendor/ripgrep/arm64-darwin/rg
vendored
Executable file
BIN
packages/core/vendor/ripgrep/arm64-darwin/rg
vendored
Executable file
Binary file not shown.
BIN
packages/core/vendor/ripgrep/arm64-linux/rg
vendored
Executable file
BIN
packages/core/vendor/ripgrep/arm64-linux/rg
vendored
Executable file
Binary file not shown.
BIN
packages/core/vendor/ripgrep/x64-darwin/rg
vendored
Executable file
BIN
packages/core/vendor/ripgrep/x64-darwin/rg
vendored
Executable file
Binary file not shown.
BIN
packages/core/vendor/ripgrep/x64-linux/rg
vendored
Executable file
BIN
packages/core/vendor/ripgrep/x64-linux/rg
vendored
Executable file
Binary file not shown.
BIN
packages/core/vendor/ripgrep/x64-win32/rg.exe
vendored
Normal file
BIN
packages/core/vendor/ripgrep/x64-win32/rg.exe
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user