📦 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

@@ -17,24 +17,74 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
import { copyFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
import { dirname, join, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
import { glob } from 'glob';
import fs from 'node:fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
const bundleDir = join(root, 'bundle');
const distDir = join(root, 'dist');
const coreVendorDir = join(root, 'packages', 'core', 'vendor');
// Create the bundle directory if it doesn't exist
if (!existsSync(bundleDir)) {
mkdirSync(bundleDir);
// Create the dist directory if it doesn't exist
if (!existsSync(distDir)) {
mkdirSync(distDir);
}
// Find and copy all .sb files from packages to the root of the bundle directory
// Find and copy all .sb files from packages to the root of the dist directory
const sbFiles = glob.sync('packages/**/*.sb', { cwd: root });
for (const file of sbFiles) {
copyFileSync(join(root, file), join(bundleDir, basename(file)));
copyFileSync(join(root, file), join(distDir, basename(file)));
}
console.log('Assets copied to bundle/');
console.log('Copied sandbox profiles to dist/');
// Copy vendor directory (contains ripgrep binaries)
console.log('Copying vendor directory...');
if (existsSync(coreVendorDir)) {
const destVendorDir = join(distDir, 'vendor');
copyRecursiveSync(coreVendorDir, destVendorDir);
console.log('Copied vendor directory to dist/');
} else {
console.warn(`Warning: Vendor directory not found at ${coreVendorDir}`);
}
console.log('\n✅ All bundle assets copied to dist/');
/**
* Recursively copy directory
*/
function copyRecursiveSync(src, dest) {
if (!existsSync(src)) {
return;
}
const stats = statSync(src);
if (stats.isDirectory()) {
if (!existsSync(dest)) {
mkdirSync(dest, { recursive: true });
}
const entries = fs.readdirSync(src);
for (const entry of entries) {
// Skip .DS_Store files
if (entry === '.DS_Store') {
continue;
}
const srcPath = join(src, entry);
const destPath = join(dest, entry);
copyRecursiveSync(srcPath, destPath);
}
} else {
copyFileSync(src, dest);
// Preserve execute permissions for binaries
const srcStats = statSync(src);
if (srcStats.mode & 0o111) {
fs.chmodSync(dest, srcStats.mode);
}
}
}

29
scripts/esbuild-shims.js Normal file
View File

@@ -0,0 +1,29 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Shims for esbuild ESM bundles to support require() calls
* This file is injected into the bundle via esbuild's inject option
*/
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
// Create require function for the current module and make it global
const _require = createRequire(import.meta.url);
// Make require available globally for dynamic requires
if (typeof globalThis.require === 'undefined') {
globalThis.require = _require;
}
// Export for esbuild injection
export const require = _require;
// Setup __filename and __dirname for compatibility
export const __filename = fileURLToPath(import.meta.url);
export const __dirname = dirname(__filename);

View File

@@ -1,213 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { execSync } from 'child_process';
/**
* Determines the correct previous tag for release notes generation.
* This function handles the complexity of mixed tag types (regular releases vs nightly releases).
*
* @param {string} currentTag - The current release tag (e.g., "v0.1.23")
* @returns {string|null} - The previous tag to compare against, or null if no suitable tag found
*/
export function getPreviousTag(currentTag) {
try {
// Parse the current tag to understand its type
const currentTagInfo = parseTag(currentTag);
if (!currentTagInfo) {
console.error(`Invalid current tag format: ${currentTag}`);
return null;
}
// Find the appropriate previous tag based on the current tag type
let previousTag = null;
if (currentTagInfo.isNightly) {
// For nightly releases, find the last stable release
previousTag = findLastStableTag(currentTagInfo);
} else {
// For stable releases, find the previous stable release
previousTag = findPreviousStableTag(currentTagInfo);
}
return previousTag;
} catch (error) {
console.error('Error getting previous tag:', error.message);
return null;
}
}
/**
* Parses a tag string to extract version information and type
*/
function parseTag(tag) {
// Remove 'v' prefix if present
const cleanTag = tag.startsWith('v') ? tag.substring(1) : tag;
// Match pattern: X.Y.Z or X.Y.Z-prerelease
const match = cleanTag.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
if (!match) {
return null;
}
const [, major, minor, patch, prerelease] = match;
return {
original: tag,
major: parseInt(major),
minor: parseInt(minor),
patch: parseInt(patch),
prerelease: prerelease || null,
isNightly: prerelease && prerelease.startsWith('nightly'),
isPreview: prerelease && prerelease.startsWith('preview'),
version: `${major}.${minor}.${patch}`,
};
}
/**
* Finds the last stable tag for a nightly release
* Assumes version numbers are incremental and checks backwards from current version
*/
function findLastStableTag(currentTagInfo) {
// For nightly releases, find the stable version of the same version number first
const baseVersion = `v${currentTagInfo.version}`;
// Check if the stable version of the current version exists
if (tagExists(baseVersion)) {
return baseVersion;
}
// If not, look for the previous stable versions by decrementing version numbers
let { major, minor, patch } = currentTagInfo;
// Try decrementing patch version first
while (patch > 0) {
patch--;
const candidateTag = `v${major}.${minor}.${patch}`;
if (tagExists(candidateTag)) {
return candidateTag;
}
}
// Try decrementing minor version
while (minor > 0) {
minor--;
patch = 999; // Start from a high patch number and work backwards
while (patch >= 0) {
const candidateTag = `v${major}.${minor}.${patch}`;
if (tagExists(candidateTag)) {
return candidateTag;
}
patch--;
// Don't check too many patch versions to avoid infinite loops
if (patch < 0) break;
}
}
// Try decrementing major version
while (major > 0) {
major--;
minor = 999; // Start from a high minor number and work backwards
while (minor >= 0) {
patch = 999;
while (patch >= 0) {
const candidateTag = `v${major}.${minor}.${patch}`;
if (tagExists(candidateTag)) {
return candidateTag;
}
patch--;
if (patch < 0) break;
}
minor--;
if (minor < 0) break;
}
}
return null;
}
/**
* Finds the previous stable tag for a stable release
* Assumes version numbers are incremental and checks backwards from current version
*/
function findPreviousStableTag(currentTagInfo) {
let { major, minor, patch } = currentTagInfo;
// Try decrementing patch version first
while (patch > 0) {
patch--;
const candidateTag = `v${major}.${minor}.${patch}`;
if (tagExists(candidateTag)) {
return candidateTag;
}
}
// Try decrementing minor version
while (minor > 0) {
minor--;
patch = 999; // Start from a high patch number and work backwards
while (patch >= 0) {
const candidateTag = `v${major}.${minor}.${patch}`;
if (tagExists(candidateTag)) {
return candidateTag;
}
patch--;
// Don't check too many patch versions to avoid infinite loops
if (patch < 0) break;
}
}
// Try decrementing major version
while (major > 0) {
major--;
minor = 999; // Start from a high minor number and work backwards
while (minor >= 0) {
patch = 999;
while (patch >= 0) {
const candidateTag = `v${major}.${minor}.${patch}`;
if (tagExists(candidateTag)) {
return candidateTag;
}
patch--;
if (patch < 0) break;
}
minor--;
if (minor < 0) break;
}
}
return null;
}
/**
* Checks if a git tag exists
*/
function tagExists(tag) {
try {
execSync(`git rev-parse --verify ${tag}`, { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
// CLI usage
if (process.argv[1] === new URL(import.meta.url).pathname) {
const currentTag = process.argv[2];
if (!currentTag) {
console.error('Usage: node get-previous-tag.js <current-tag>');
process.exit(1);
}
const previousTag = getPreviousTag(currentTag);
if (previousTag) {
console.log(previousTag);
} else {
console.error('No suitable previous tag found');
process.exit(1);
}
}

View File

@@ -26,36 +26,8 @@ function getArgs() {
return args;
}
function getLatestTag(pattern) {
const command = `git tag -l '${pattern}'`;
try {
const tags = execSync(command)
.toString()
.trim()
.split('\n')
.filter(Boolean);
if (tags.length === 0) return '';
// Convert tags to versions (remove 'v' prefix) and sort by semver
const versions = tags
.map((tag) => tag.replace(/^v/, ''))
.filter((version) => semver.valid(version))
.sort((a, b) => semver.rcompare(a, b)); // rcompare for descending order
if (versions.length === 0) return '';
// Return the latest version with 'v' prefix restored
return `v${versions[0]}`;
} catch (error) {
console.error(
`Failed to get latest git tag for pattern "${pattern}": ${error.message}`,
);
return '';
}
}
function getVersionFromNPM(distTag) {
const command = `npm view @google/gemini-cli version --tag=${distTag}`;
const command = `npm view @qwen-code/qwen-code version --tag=${distTag}`;
try {
return execSync(command).toString().trim();
} catch (error) {
@@ -67,7 +39,7 @@ function getVersionFromNPM(distTag) {
}
function getAllVersionsFromNPM() {
const command = `npm view @google/gemini-cli versions --json`;
const command = `npm view @qwen-code/qwen-code versions --json`;
try {
const versionsJson = execSync(command).toString().trim();
return JSON.parse(versionsJson);
@@ -78,7 +50,7 @@ function getAllVersionsFromNPM() {
}
function isVersionDeprecated(version) {
const command = `npm view @google/gemini-cli@${version} deprecated`;
const command = `npm view @qwen-code/qwen-code@${version} deprecated`;
try {
const output = execSync(command).toString().trim();
return output.length > 0;
@@ -159,7 +131,7 @@ function detectRollbackAndGetBaseline(npmDistTag) {
function doesVersionExist(version) {
// Check NPM
try {
const command = `npm view @google/gemini-cli@${version} version 2>/dev/null`;
const command = `npm view @qwen-code/qwen-code@${version} version 2>/dev/null`;
const output = execSync(command).toString().trim();
if (output === version) {
console.error(`Version ${version} already exists on NPM.`);
@@ -229,11 +201,20 @@ function getAndVerifyTags(npmDistTag, _gitTagPattern) {
};
}
function getLatestStableReleaseTag() {
try {
const { latestTag } = getAndVerifyTags('latest', 'v[0-9].[0-9].[0-9]');
return latestTag;
} catch (error) {
console.error(
`Failed to determine latest stable release tag: ${error.message}`,
);
return '';
}
}
function promoteNightlyVersion() {
const { latestVersion, latestTag } = getAndVerifyTags(
'nightly',
'v*-nightly*',
);
const { latestVersion } = getAndVerifyTags('nightly', 'v*-nightly*');
const baseVersion = latestVersion.split('-')[0];
const versionParts = baseVersion.split('.');
const major = versionParts[0];
@@ -244,7 +225,6 @@ function promoteNightlyVersion() {
return {
releaseVersion: `${major}.${nextMinor}.0-nightly.${date}.${gitShortHash}`,
npmTag: 'nightly',
previousReleaseTag: latestTag,
};
}
@@ -254,12 +234,9 @@ function getNightlyVersion() {
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
const gitShortHash = execSync('git rev-parse --short HEAD').toString().trim();
const releaseVersion = `${baseVersion}-nightly.${date}.${gitShortHash}`;
const previousReleaseTag = getLatestTag('v*-nightly*');
return {
releaseVersion,
npmTag: 'nightly',
previousReleaseTag,
};
}
@@ -290,15 +267,9 @@ function getStableVersion(args) {
releaseVersion = latestPreviewVersion.replace(/-preview.*/, '');
}
const { latestTag: previousStableTag } = getAndVerifyTags(
'latest',
'v[0-9].[0-9].[0-9]',
);
return {
releaseVersion,
npmTag: 'latest',
previousReleaseTag: previousStableTag,
};
}
@@ -321,15 +292,9 @@ function getPreviewVersion(args) {
latestNightlyVersion.replace(/-nightly.*/, '') + '-preview.0';
}
const { latestTag: previousPreviewTag } = getAndVerifyTags(
'preview',
'v*-preview*',
);
return {
releaseVersion,
npmTag: 'preview',
previousReleaseTag: previousPreviewTag,
};
}
@@ -341,7 +306,7 @@ function getPatchVersion(patchFrom) {
}
const distTag = patchFrom === 'stable' ? 'latest' : 'preview';
const pattern = distTag === 'latest' ? 'v[0-9].[0-9].[0-9]' : 'v*-preview*';
const { latestVersion, latestTag } = getAndVerifyTags(distTag, pattern);
const { latestVersion } = getAndVerifyTags(distTag, pattern);
if (patchFrom === 'stable') {
// For stable versions, increment the patch number: 0.5.4 -> 0.5.5
@@ -353,7 +318,6 @@ function getPatchVersion(patchFrom) {
return {
releaseVersion,
npmTag: distTag,
previousReleaseTag: latestTag,
};
} else {
// For preview versions, increment the preview number: 0.6.0-preview.2 -> 0.6.0-preview.3
@@ -373,7 +337,6 @@ function getPatchVersion(patchFrom) {
return {
releaseVersion,
npmTag: distTag,
previousReleaseTag: latestTag,
};
}
}
@@ -438,6 +401,8 @@ export function getVersion(options = {}) {
...versionData,
};
result.previousReleaseTag = getLatestStableReleaseTag();
return result;
}

View File

@@ -1,51 +1,110 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Prepares the bundled CLI package for npm publishing
* This script adds publishing metadata (package.json, README, LICENSE) to dist/
* All runtime assets (cli.js, vendor/, *.sb) are already in dist/ from the bundle step
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { execSync } from 'node:child_process';
// ES module equivalent of __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootDir = path.resolve(__dirname, '..');
function copyFiles(packageName, filesToCopy) {
const packageDir = path.resolve(rootDir, 'packages', packageName);
if (!fs.existsSync(packageDir)) {
console.error(`Error: Package directory not found at ${packageDir}`);
process.exit(1);
}
const distDir = path.join(rootDir, 'dist');
const cliBundlePath = path.join(distDir, 'cli.js');
const vendorDir = path.join(distDir, 'vendor');
console.log(`Preparing package: ${packageName}`);
for (const [source, dest] of Object.entries(filesToCopy)) {
const sourcePath = path.resolve(rootDir, source);
const destPath = path.resolve(packageDir, dest);
try {
fs.copyFileSync(sourcePath, destPath);
console.log(`Copied ${source} to packages/${packageName}/`);
} catch (err) {
console.error(`Error copying ${source}:`, err);
process.exit(1);
}
// Verify dist directory and bundle exist
if (!fs.existsSync(distDir)) {
console.error('Error: dist/ directory not found');
console.error('Please run "npm run bundle" first');
process.exit(1);
}
if (!fs.existsSync(cliBundlePath)) {
console.error(`Error: Bundle not found at ${cliBundlePath}`);
console.error('Please run "npm run bundle" first');
process.exit(1);
}
if (!fs.existsSync(vendorDir)) {
console.error(`Error: Vendor directory not found at ${vendorDir}`);
console.error('Please run "npm run bundle" first');
process.exit(1);
}
// Copy README and LICENSE
console.log('Copying documentation files...');
const filesToCopy = ['README.md', 'LICENSE'];
for (const file of filesToCopy) {
const sourcePath = path.join(rootDir, file);
const destPath = path.join(distDir, file);
if (fs.existsSync(sourcePath)) {
fs.copyFileSync(sourcePath, destPath);
console.log(`Copied ${file}`);
} else {
console.warn(`Warning: ${file} not found at ${sourcePath}`);
}
}
// Prepare 'core' package
copyFiles('core', {
'README.md': 'README.md',
LICENSE: 'LICENSE',
'.npmrc': '.npmrc',
});
// Copy package.json from root and modify it for publishing
console.log('Creating package.json for distribution...');
const rootPackageJson = JSON.parse(
fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8'),
);
const corePackageJson = JSON.parse(
fs.readFileSync(
path.join(rootDir, 'packages', 'core', 'package.json'),
'utf-8',
),
);
// Prepare 'cli' package
copyFiles('cli', {
'README.md': 'README.md',
LICENSE: 'LICENSE',
});
const runtimeDependencies = {};
if (corePackageJson.dependencies?.tiktoken) {
runtimeDependencies.tiktoken = corePackageJson.dependencies.tiktoken;
}
console.log('Successfully prepared all packages.');
// Create a clean package.json for the published package
const distPackageJson = {
name: rootPackageJson.name,
version: rootPackageJson.version,
description:
rootPackageJson.description || 'Qwen Code - AI-powered coding assistant',
repository: rootPackageJson.repository,
type: 'module',
main: 'cli.js',
bin: {
qwen: 'cli.js',
},
files: ['cli.js', 'vendor', 'README.md', 'LICENSE'],
config: rootPackageJson.config,
dependencies: runtimeDependencies,
optionalDependencies: {
'@lydell/node-pty': '1.1.0',
'@lydell/node-pty-darwin-arm64': '1.1.0',
'@lydell/node-pty-darwin-x64': '1.1.0',
'@lydell/node-pty-linux-x64': '1.1.0',
'@lydell/node-pty-win32-arm64': '1.1.0',
'@lydell/node-pty-win32-x64': '1.1.0',
'node-pty': '^1.0.0',
},
engines: rootPackageJson.engines,
};
fs.writeFileSync(
path.join(distDir, 'package.json'),
JSON.stringify(distPackageJson, null, 2) + '\n',
);
console.log('\n✅ Package prepared for publishing at dist/');
console.log('\nPackage structure:');
execSync('ls -lh dist/', { stdio: 'inherit', cwd: rootDir });

View File

@@ -57,7 +57,7 @@ describe('getVersion', () => {
// For doesVersionExist checks - default to not found
if (
command.includes('npm view') &&
command.includes('@google/gemini-cli@')
command.includes('@qwen-code/qwen-code@')
) {
throw new Error('NPM version not found');
}
@@ -83,7 +83,7 @@ describe('getVersion', () => {
const result = getVersion({ type: 'preview' });
expect(result.releaseVersion).toBe('0.8.0-preview.0');
expect(result.npmTag).toBe('preview');
expect(result.previousReleaseTag).toBe('v0.7.0-preview.1');
expect(result.previousReleaseTag).toBe('v0.6.1');
});
it('should calculate the next nightly version from package.json', () => {
@@ -92,7 +92,7 @@ describe('getVersion', () => {
// Note: The base version now comes from package.json, not the previous nightly tag.
expect(result.releaseVersion).toBe('0.8.0-nightly.20250917.d3bf8a3d');
expect(result.npmTag).toBe('nightly');
expect(result.previousReleaseTag).toBe('v0.8.0-nightly.20250916.abcdef');
expect(result.previousReleaseTag).toBe('v0.6.1');
});
it('should calculate the next patch version for a stable release', () => {
@@ -108,7 +108,7 @@ describe('getVersion', () => {
const result = getVersion({ type: 'patch', 'patch-from': 'preview' });
expect(result.releaseVersion).toBe('0.7.0-preview.2');
expect(result.npmTag).toBe('preview');
expect(result.previousReleaseTag).toBe('v0.7.0-preview.1');
expect(result.previousReleaseTag).toBe('v0.6.1');
});
});
@@ -124,7 +124,7 @@ describe('getVersion', () => {
// Mock the deprecation check
if (
command.includes(
'npm view @google/gemini-cli@0.9.0-nightly.20250917.deprecated deprecated',
'npm view @qwen-code/qwen-code@0.9.0-nightly.20250917.deprecated deprecated',
)
)
return 'This version is deprecated';
@@ -162,14 +162,14 @@ describe('getVersion', () => {
// The calculated preview 0.8.0-preview.0 already exists on NPM
if (
command.includes(
'npm view @google/gemini-cli@0.8.0-preview.0 version',
'npm view @qwen-code/qwen-code@0.8.0-preview.0 version',
)
)
return '0.8.0-preview.0';
// The next one is available
if (
command.includes(
'npm view @google/gemini-cli@0.8.0-preview.1 version',
'npm view @qwen-code/qwen-code@0.8.0-preview.1 version',
)
)
throw new Error('Not found');