mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Merge pull request #1265 from QwenLM/mingholy/chore/bundle-cli-into-sdk
Bundle CLI into SDK package and separate CLI & SDK E2E tests
This commit is contained in:
12
.github/workflows/release-sdk.yml
vendored
12
.github/workflows/release-sdk.yml
vendored
@@ -121,6 +121,11 @@ jobs:
|
||||
IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}'
|
||||
MANUAL_VERSION: '${{ inputs.version }}'
|
||||
|
||||
- name: 'Build CLI Bundle'
|
||||
run: |
|
||||
npm run build
|
||||
npm run bundle
|
||||
|
||||
- name: 'Run Tests'
|
||||
if: |-
|
||||
${{ github.event.inputs.force_skip_tests != 'true' }}
|
||||
@@ -132,13 +137,6 @@ jobs:
|
||||
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
|
||||
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
|
||||
|
||||
- name: 'Build CLI for Integration Tests'
|
||||
if: |-
|
||||
${{ github.event.inputs.force_skip_tests != 'true' }}
|
||||
run: |
|
||||
npm run build
|
||||
npm run bundle
|
||||
|
||||
- name: 'Run SDK Integration Tests'
|
||||
if: |-
|
||||
${{ github.event.inputs.force_skip_tests != 'true' }}
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -133,8 +133,8 @@ jobs:
|
||||
${{ github.event.inputs.force_skip_tests != 'true' }}
|
||||
run: |
|
||||
npm run preflight
|
||||
npm run test:integration:sandbox:none
|
||||
npm run test:integration:sandbox:docker
|
||||
npm run test:integration:cli:sandbox:none
|
||||
npm run test:integration:cli:sandbox:docker
|
||||
env:
|
||||
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
|
||||
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
|
||||
|
||||
@@ -5,6 +5,7 @@ Welcome to the Qwen Code documentation. Qwen Code is an agentic coding tool that
|
||||
## Documentation Sections
|
||||
|
||||
### [User Guide](./users/overview)
|
||||
|
||||
Learn how to use Qwen Code as an end user. This section covers:
|
||||
|
||||
- Basic installation and setup
|
||||
|
||||
@@ -358,7 +358,7 @@ Arguments passed directly when running the CLI can override other configurations
|
||||
### Command-Line Arguments Table
|
||||
|
||||
| Argument | Alias | Description | Possible Values | Notes |
|
||||
| ---------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ---------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--model` | `-m` | Specifies the Qwen model to use for this session. | Model name | Example: `npm start -- --model qwen3-coder-plus` |
|
||||
| `--prompt` | `-p` | Used to pass a prompt directly to the command. This invokes Qwen Code in a non-interactive mode. | Your prompt text | For scripting examples, use the `--output-format json` flag to get structured output. |
|
||||
| `--prompt-interactive` | `-i` | Starts an interactive session with the provided prompt as the initial input. | Your prompt text | The prompt is processed within the interactive session, not before it. Cannot be used when piping input from stdin. Example: `qwen -i "explain this code"` |
|
||||
|
||||
@@ -13,9 +13,8 @@ npm install @qwen-code/sdk
|
||||
## Requirements
|
||||
|
||||
- Node.js >= 20.0.0
|
||||
- [Qwen Code](https://github.com/QwenLM/qwen-code) >= 0.4.0 (stable) installed and accessible in PATH
|
||||
|
||||
> **Note for nvm users**: If you use nvm to manage Node.js versions, the SDK may not be able to auto-detect the Qwen Code executable. You should explicitly set the `pathToQwenExecutable` option to the full path of the `qwen` binary.
|
||||
> From v0.1.1, the CLI is bundled with the SDK. So no standalone CLI installation is needed.
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -372,6 +371,23 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
## FAQ / Troubleshooting
|
||||
|
||||
### Version 0.1.0 Requirements
|
||||
|
||||
If you're using SDK version **0.1.0**, please note the following requirements:
|
||||
|
||||
#### Qwen Code Installation Required
|
||||
|
||||
Version 0.1.0 requires [Qwen Code](https://github.com/QwenLM/qwen-code) **>= 0.4.0** to be installed separately and accessible in your PATH.
|
||||
|
||||
```bash
|
||||
# Install Qwen Code globally
|
||||
npm install -g qwen-code@^0.4.0
|
||||
```
|
||||
|
||||
**Note**: From version **0.1.1** onwards, the CLI is bundled with the SDK, so no separate Qwen Code installation is needed.
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0 - see [LICENSE](./LICENSE) for details.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/sdk",
|
||||
"version": "0.5.1",
|
||||
"version": "0.1.1",
|
||||
"description": "TypeScript SDK for programmatic access to qwen-code CLI",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -45,7 +45,8 @@
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4"
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"tiktoken": "^1.0.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.0",
|
||||
|
||||
@@ -91,3 +91,35 @@ if (existsSync(licenseSource)) {
|
||||
console.warn('Could not copy LICENSE:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Bundling CLI into SDK package...');
|
||||
const repoRoot = join(rootDir, '..', '..');
|
||||
const rootDistDir = join(repoRoot, 'dist');
|
||||
|
||||
if (!existsSync(rootDistDir) || !existsSync(join(rootDistDir, 'cli.js'))) {
|
||||
console.log('Building CLI bundle...');
|
||||
try {
|
||||
execSync('npm run bundle', { stdio: 'inherit', cwd: repoRoot });
|
||||
} catch (error) {
|
||||
console.error('Failed to build CLI bundle:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const cliDistDir = join(rootDir, 'dist', 'cli');
|
||||
mkdirSync(cliDistDir, { recursive: true });
|
||||
|
||||
console.log('Copying CLI bundle...');
|
||||
cpSync(join(rootDistDir, 'cli.js'), join(cliDistDir, 'cli.js'));
|
||||
|
||||
const vendorSource = join(rootDistDir, 'vendor');
|
||||
if (existsSync(vendorSource)) {
|
||||
cpSync(vendorSource, join(cliDistDir, 'vendor'), { recursive: true });
|
||||
}
|
||||
|
||||
const localesSource = join(rootDistDir, 'locales');
|
||||
if (existsSync(localesSource)) {
|
||||
cpSync(localesSource, join(cliDistDir, 'locales'), { recursive: true });
|
||||
}
|
||||
|
||||
console.log('CLI bundle copied successfully to SDK package');
|
||||
|
||||
@@ -2,24 +2,16 @@
|
||||
* CLI path auto-detection and subprocess spawning utilities
|
||||
*
|
||||
* Supports multiple execution modes:
|
||||
* 1. Native binary: 'qwen' (production)
|
||||
* 2. Node.js bundle: 'node /path/to/cli.js' (production validation)
|
||||
* 1. Bundled CLI: Node.js bundle included in the SDK package (default)
|
||||
* 2. Node.js bundle: 'node /path/to/cli.js' (custom path)
|
||||
* 3. Bun bundle: 'bun /path/to/cli.js' (alternative runtime)
|
||||
* 4. TypeScript source: 'tsx /path/to/index.ts' (development)
|
||||
*
|
||||
* Auto-detection locations for native binary:
|
||||
* 1. QWEN_CODE_CLI_PATH environment variable
|
||||
* 2. ~/.volta/bin/qwen
|
||||
* 3. ~/.npm-global/bin/qwen
|
||||
* 4. /usr/local/bin/qwen
|
||||
* 5. ~/.local/bin/qwen
|
||||
* 6. ~/node_modules/.bin/qwen
|
||||
* 7. ~/.yarn/bin/qwen
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
/**
|
||||
* Executable types supported by the SDK
|
||||
@@ -40,49 +32,38 @@ export type SpawnInfo = {
|
||||
originalInput: string;
|
||||
};
|
||||
|
||||
function getBundledCliPath(): string | null {
|
||||
try {
|
||||
const currentFile =
|
||||
typeof __filename !== 'undefined'
|
||||
? __filename
|
||||
: fileURLToPath(import.meta.url);
|
||||
|
||||
const currentDir = path.dirname(currentFile);
|
||||
|
||||
const bundledCliPath = path.join(currentDir, 'cli', 'cli.js');
|
||||
|
||||
if (fs.existsSync(bundledCliPath)) {
|
||||
return bundledCliPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function findNativeCliPath(): string {
|
||||
const homeDir = process.env['HOME'] || process.env['USERPROFILE'] || '';
|
||||
|
||||
const candidates: Array<string | undefined> = [
|
||||
// 1. Environment variable (highest priority)
|
||||
process.env['QWEN_CODE_CLI_PATH'],
|
||||
|
||||
// 2. Volta bin
|
||||
path.join(homeDir, '.volta', 'bin', 'qwen'),
|
||||
|
||||
// 3. Global npm installations
|
||||
path.join(homeDir, '.npm-global', 'bin', 'qwen'),
|
||||
|
||||
// 4. Common Unix binary locations
|
||||
'/usr/local/bin/qwen',
|
||||
|
||||
// 5. User local bin
|
||||
path.join(homeDir, '.local', 'bin', 'qwen'),
|
||||
|
||||
// 6. Node modules bin in home directory
|
||||
path.join(homeDir, 'node_modules', '.bin', 'qwen'),
|
||||
|
||||
// 7. Yarn global bin
|
||||
path.join(homeDir, '.yarn', 'bin', 'qwen'),
|
||||
];
|
||||
|
||||
// Find first existing candidate
|
||||
for (const candidate of candidates) {
|
||||
if (candidate && fs.existsSync(candidate)) {
|
||||
return path.resolve(candidate);
|
||||
}
|
||||
const bundledCli = getBundledCliPath();
|
||||
if (bundledCli) {
|
||||
return bundledCli;
|
||||
}
|
||||
|
||||
// Not found - throw helpful error
|
||||
throw new Error(
|
||||
'qwen CLI not found. Please:\n' +
|
||||
' 1. Install qwen globally: npm install -g qwen\n' +
|
||||
' 2. Or provide explicit executable: query({ pathToQwenExecutable: "/path/to/qwen" })\n' +
|
||||
' 3. Or set environment variable: QWEN_CODE_CLI_PATH="/path/to/qwen"\n' +
|
||||
'\n' +
|
||||
'For development/testing, you can also use:\n' +
|
||||
'Bundled qwen CLI not found. The CLI should be included in the SDK package.\n' +
|
||||
'If you need to use a custom CLI, provide explicit executable:\n' +
|
||||
' • query({ pathToQwenExecutable: "/path/to/cli.js" })\n' +
|
||||
' • TypeScript source: query({ pathToQwenExecutable: "/path/to/index.ts" })\n' +
|
||||
' • Node.js bundle: query({ pathToQwenExecutable: "/path/to/cli.js" })\n' +
|
||||
' • Force specific runtime: query({ pathToQwenExecutable: "bun:/path/to/cli.js" })',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ describe('CLI Path Utilities', () => {
|
||||
mockFs.statSync.mockReturnValue({
|
||||
isFile: () => true,
|
||||
} as ReturnType<typeof import('fs').statSync>);
|
||||
// Default: return true for existsSync (can be overridden in specific tests)
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -50,28 +52,26 @@ describe('CLI Path Utilities', () => {
|
||||
|
||||
describe('parseExecutableSpec', () => {
|
||||
describe('auto-detection (no spec provided)', () => {
|
||||
it('should auto-detect native CLI when no spec provided', () => {
|
||||
// Mock environment variable
|
||||
const originalEnv = process.env['QWEN_CODE_CLI_PATH'];
|
||||
process.env['QWEN_CODE_CLI_PATH'] = '/usr/local/bin/qwen';
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
it('should auto-detect bundled CLI when no spec provided', () => {
|
||||
// Mock existsSync to return true for bundled CLI
|
||||
mockFs.existsSync.mockImplementation((p) => {
|
||||
const pathStr = p.toString();
|
||||
return (
|
||||
pathStr.includes('cli/cli.js') || pathStr.includes('cli\\cli.js')
|
||||
);
|
||||
});
|
||||
|
||||
const result = parseExecutableSpec();
|
||||
|
||||
expect(result).toEqual({
|
||||
executablePath: path.resolve('/usr/local/bin/qwen'),
|
||||
isExplicitRuntime: false,
|
||||
expect(result.executablePath).toContain('cli.js');
|
||||
expect(result.isExplicitRuntime).toBe(false);
|
||||
});
|
||||
|
||||
// Restore env
|
||||
process.env['QWEN_CODE_CLI_PATH'] = originalEnv;
|
||||
});
|
||||
|
||||
it('should throw when auto-detection fails', () => {
|
||||
it('should throw when bundled CLI not found', () => {
|
||||
mockFs.existsSync.mockReturnValue(false);
|
||||
|
||||
expect(() => parseExecutableSpec()).toThrow(
|
||||
'qwen CLI not found. Please:',
|
||||
'Bundled qwen CLI not found',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -361,65 +361,44 @@ describe('CLI Path Utilities', () => {
|
||||
});
|
||||
|
||||
describe('auto-detection fallback', () => {
|
||||
it('should auto-detect when no spec provided', () => {
|
||||
// Mock environment variable
|
||||
const originalEnv = process.env['QWEN_CODE_CLI_PATH'];
|
||||
process.env['QWEN_CODE_CLI_PATH'] = '/usr/local/bin/qwen';
|
||||
it('should auto-detect bundled CLI when no spec provided', () => {
|
||||
// Mock existsSync to return true for bundled CLI
|
||||
mockFs.existsSync.mockImplementation((p) => {
|
||||
const pathStr = p.toString();
|
||||
return (
|
||||
pathStr.includes('cli/cli.js') || pathStr.includes('cli\\cli.js')
|
||||
);
|
||||
});
|
||||
|
||||
const result = prepareSpawnInfo();
|
||||
|
||||
expect(result).toEqual({
|
||||
command: path.resolve('/usr/local/bin/qwen'),
|
||||
args: [],
|
||||
type: 'native',
|
||||
originalInput: '',
|
||||
});
|
||||
|
||||
// Restore env
|
||||
process.env['QWEN_CODE_CLI_PATH'] = originalEnv;
|
||||
expect(result.command).toBe(process.execPath);
|
||||
expect(result.args[0]).toContain('cli.js');
|
||||
expect(result.type).toBe('node');
|
||||
expect(result.originalInput).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findNativeCliPath', () => {
|
||||
it('should find CLI from environment variable', () => {
|
||||
const originalEnv = process.env['QWEN_CODE_CLI_PATH'];
|
||||
process.env['QWEN_CODE_CLI_PATH'] = '/custom/path/to/qwen';
|
||||
mockFs.existsSync.mockReturnValue(true);
|
||||
|
||||
const result = findNativeCliPath();
|
||||
|
||||
expect(result).toBe(path.resolve('/custom/path/to/qwen'));
|
||||
|
||||
process.env['QWEN_CODE_CLI_PATH'] = originalEnv;
|
||||
});
|
||||
|
||||
it('should search common installation locations', () => {
|
||||
const originalEnv = process.env['QWEN_CODE_CLI_PATH'];
|
||||
delete process.env['QWEN_CODE_CLI_PATH'];
|
||||
|
||||
// Mock fs.existsSync to return true for volta bin
|
||||
// Use path.join to match platform-specific path separators
|
||||
const voltaBinPath = path.join('.volta', 'bin', 'qwen');
|
||||
it('should find bundled CLI', () => {
|
||||
// Mock existsSync to return true for bundled CLI
|
||||
mockFs.existsSync.mockImplementation((p) => {
|
||||
return p.toString().includes(voltaBinPath);
|
||||
const pathStr = p.toString();
|
||||
return (
|
||||
pathStr.includes('cli/cli.js') || pathStr.includes('cli\\cli.js')
|
||||
);
|
||||
});
|
||||
|
||||
const result = findNativeCliPath();
|
||||
|
||||
expect(result).toContain(voltaBinPath);
|
||||
|
||||
process.env['QWEN_CODE_CLI_PATH'] = originalEnv;
|
||||
expect(result).toContain('cli.js');
|
||||
});
|
||||
|
||||
it('should throw descriptive error when CLI not found', () => {
|
||||
const originalEnv = process.env['QWEN_CODE_CLI_PATH'];
|
||||
delete process.env['QWEN_CODE_CLI_PATH'];
|
||||
it('should throw descriptive error when bundled CLI not found', () => {
|
||||
mockFs.existsSync.mockReturnValue(false);
|
||||
|
||||
expect(() => findNativeCliPath()).toThrow('qwen CLI not found. Please:');
|
||||
|
||||
process.env['QWEN_CODE_CLI_PATH'] = originalEnv;
|
||||
expect(() => findNativeCliPath()).toThrow('Bundled qwen CLI not found');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -634,13 +613,10 @@ describe('CLI Path Utilities', () => {
|
||||
mockFs.existsSync.mockReturnValue(false);
|
||||
|
||||
expect(() => parseExecutableSpec('/missing/file')).toThrow(
|
||||
'Set QWEN_CODE_CLI_PATH environment variable',
|
||||
'Executable file not found at',
|
||||
);
|
||||
expect(() => parseExecutableSpec('/missing/file')).toThrow(
|
||||
'Install qwen globally: npm install -g qwen',
|
||||
);
|
||||
expect(() => parseExecutableSpec('/missing/file')).toThrow(
|
||||
'Force specific runtime: bun:/path/to/cli.js or tsx:/path/to/index.ts',
|
||||
'Please check the file path and ensure the file exists',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,5 +48,5 @@
|
||||
}
|
||||
|
||||
.assistant-message-container.assistant-message-loading::after {
|
||||
display: none
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -172,7 +172,8 @@
|
||||
|
||||
/* Loading animation for toolcall header */
|
||||
@keyframes toolcallHeaderPulse {
|
||||
0%, 100% {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
.composer-form:focus-within {
|
||||
/* match existing highlight behavior */
|
||||
border-color: var(--app-input-highlight);
|
||||
box-shadow: 0 1px 2px color-mix(in srgb, var(--app-input-highlight), transparent 80%);
|
||||
box-shadow: 0 1px 2px
|
||||
color-mix(in srgb, var(--app-input-highlight), transparent 80%);
|
||||
}
|
||||
|
||||
/* Composer: input editable area */
|
||||
@@ -66,7 +67,7 @@
|
||||
The data attribute is needed because some browsers insert a <br> in
|
||||
contentEditable, which breaks :empty matching. */
|
||||
.composer-input:empty:before,
|
||||
.composer-input[data-empty="true"]::before {
|
||||
.composer-input[data-empty='true']::before {
|
||||
content: attr(data-placeholder);
|
||||
color: var(--app-input-placeholder-foreground);
|
||||
pointer-events: none;
|
||||
@@ -80,7 +81,7 @@
|
||||
outline: none;
|
||||
}
|
||||
.composer-input:disabled,
|
||||
.composer-input[contenteditable="false"] {
|
||||
.composer-input[contenteditable='false'] {
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user