Compare commits

...

14 Commits

Author SHA1 Message Date
github-actions[bot]
42dcc79877 chore(release): v0.8.0-preview.2 2026-01-23 01:56:40 +00:00
tanzhenxin
8d0f785c28 Merge pull request #1572 from weiyuanke/patch-1
Update command usage in add.ts to reflect new name
2026-01-23 09:33:01 +08:00
tanzhenxin
6be47fe008 Merge pull request #1542 from QwenLM/vscode-ide-companion-github-action-publish
Add VSCode IDE Companion Release Workflow
2026-01-23 09:32:39 +08:00
tanzhenxin
29e71a5d7d Merge pull request #1553 from QwenLM/feature/add-trendshift-badge
docs: add Trendshift badge to README
2026-01-23 09:15:12 +08:00
yiliang114
bfe451bb4a ci(vscode-ide-companion): improve release workflow and fix yaml lint errors
- Fix yaml lint errors by properly quoting conditional expressions
- Update package version step to use correct working directory
- Modify test execution to run in the correct directory (packages/vscode-ide-companion)
- Enhance version retrieval logic to use actual package version for preview releases
- Add working directory to all relevant steps for consistency
- Simplify package version update command by removing redundant workspace flag

These changes ensure the release workflow runs correctly and follows
consistent directory structure practices.
2026-01-22 21:40:09 +08:00
yiliang114
c143c68656 Merge branch 'main' of https://github.com/QwenLM/qwen-code into vscode-ide-companion-github-action-publish 2026-01-22 21:19:35 +08:00
顾盼
011f3d2320 Merge pull request #1580 from QwenLM/feat/extension-improvements
feat(extensions): add detail command and improve extension validation
2026-01-22 20:00:55 +08:00
LaZzyMan
674bb6386e feat(extensions): add detail command and improve extension validation
- Add /extensions detail command to show extension details
- Allow underscores and dots in extension names
- Fix contextFileName empty array handling to use default QWEN.md
- Fix marketplace extension clone to use correct source URL
- Add inline parameter to extensionToOutputString
- Add comprehensive tests for all changes
2026-01-22 19:37:01 +08:00
tanzhenxin
2aa681f610 Merge pull request #1578 from QwenLM/fix/pkg-dependence
fix prompts denpendence
2026-01-22 16:04:59 +08:00
tanzhenxin
a7e55ccf43 Merge pull request #1576 from QwenLM/fix/pkg-dependence
fix github pkg dependence
2026-01-22 15:30:29 +08:00
tanzhenxin
64eea4889d Merge pull request #1574 from QwenLM/fix/pkg-dependence
fix dependences of core pkg
2026-01-22 14:26:02 +08:00
yuanke wei
27df0486a3 Update command usage in add.ts to reflect new name 2026-01-22 09:56:59 +08:00
pomelo-nwu
47ee9b5db8 docs: add Trendshift badge to README
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-01-20 16:17:49 +08:00
yiliang114
605e8709fb build(vscode): Add VSCode IDE Companion Publish Workflow 2026-01-19 15:04:05 +08:00
17 changed files with 523 additions and 27 deletions

View File

@@ -0,0 +1,207 @@
name: 'Release VSCode IDE Companion'
on:
workflow_dispatch:
inputs:
version:
description: 'The version to release (e.g., v0.1.11). Required for manual patch releases.'
required: false
type: 'string'
ref:
description: 'The branch or ref (full git sha) to release from.'
required: true
type: 'string'
default: 'main'
dry_run:
description: 'Run a dry-run of the release process; no branches, vsix packages or GitHub releases will be created.'
required: true
type: 'boolean'
default: true
create_preview_release:
description: 'Auto apply the preview release tag, input version is ignored.'
required: false
type: 'boolean'
default: false
force_skip_tests:
description: 'Select to skip the "Run Tests" step in testing. Prod releases should run tests'
required: false
type: 'boolean'
default: false
concurrency:
group: '${{ github.workflow }}'
cancel-in-progress: false
jobs:
release-vscode-companion:
runs-on: 'ubuntu-latest'
environment:
name: 'production-release'
url: '${{ github.server_url }}/${{ github.repository }}/releases/tag/vscode-companion-${{ steps.version.outputs.RELEASE_TAG }}'
if: |-
${{ github.repository == 'QwenLM/qwen-code' }}
permissions:
contents: 'read'
issues: 'write'
steps:
- name: 'Checkout'
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
with:
ref: '${{ github.event.inputs.ref || github.sha }}'
fetch-depth: 0
- name: 'Set booleans for simplified logic'
env:
CREATE_PREVIEW_RELEASE: '${{ github.event.inputs.create_preview_release }}'
DRY_RUN_INPUT: '${{ github.event.inputs.dry_run }}'
id: 'vars'
run: |-
is_preview="false"
if [[ "${CREATE_PREVIEW_RELEASE}" == "true" ]]; then
is_preview="true"
fi
echo "is_preview=${is_preview}" >> "${GITHUB_OUTPUT}"
is_dry_run="false"
if [[ "${DRY_RUN_INPUT}" == "true" ]]; then
is_dry_run="true"
fi
echo "is_dry_run=${is_dry_run}" >> "${GITHUB_OUTPUT}"
- name: 'Setup Node.js'
uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: 'package-lock.json'
- name: 'Install Dependencies'
env:
NPM_CONFIG_PREFER_OFFLINE: 'true'
run: |-
npm ci
- name: 'Install VSCE and OVSX'
run: |-
npm install -g @vscode/vsce
npm install -g ovsx
- name: 'Get the version'
id: 'version'
working-directory: 'packages/vscode-ide-companion'
run: |
# Get the base version from package.json regardless of scenario
BASE_VERSION=$(node -p "require('./package.json').version")
if [[ "${IS_PREVIEW}" == "true" ]]; then
# Generate preview version with timestamp based on actual package version
TIMESTAMP=$(date +%Y%m%d%H%M%S)
PREVIEW_VERSION="${BASE_VERSION}-preview.${TIMESTAMP}"
RELEASE_TAG="preview.${TIMESTAMP}"
echo "RELEASE_TAG=${RELEASE_TAG}" >> "$GITHUB_OUTPUT"
echo "RELEASE_VERSION=${PREVIEW_VERSION}" >> "$GITHUB_OUTPUT"
echo "VSCODE_TAG=preview" >> "$GITHUB_OUTPUT"
else
# Use specified version or get from package.json
if [[ -n "${MANUAL_VERSION}" ]]; then
RELEASE_VERSION="${MANUAL_VERSION#v}" # Remove 'v' prefix if present
RELEASE_TAG="${MANUAL_VERSION#v}" # Remove 'v' prefix if present
else
RELEASE_VERSION="${BASE_VERSION}"
RELEASE_TAG="${BASE_VERSION}"
fi
echo "RELEASE_TAG=${RELEASE_TAG}" >> "$GITHUB_OUTPUT"
echo "RELEASE_VERSION=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT"
echo "VSCODE_TAG=latest" >> "$GITHUB_OUTPUT"
fi
env:
IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}'
MANUAL_VERSION: '${{ inputs.version }}'
- name: 'Update package version (for preview releases)'
if: '${{ steps.vars.outputs.is_preview == ''true'' }}'
working-directory: 'packages/vscode-ide-companion'
env:
RELEASE_VERSION: '${{ steps.version.outputs.RELEASE_VERSION }}'
run: |-
# Update package.json with preview version
npm version "${RELEASE_VERSION}" --no-git-tag-version --allow-same-version
- name: 'Run Tests'
if: |-
${{ github.event.inputs.force_skip_tests != 'true' }}
working-directory: 'packages/vscode-ide-companion'
run: |
npm run test:ci
env:
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
- name: 'Prepare VSCode Extension'
run: |
# Build and stage the extension + bundled CLI once.
npm --workspace=qwen-code-vscode-ide-companion run prepackage
- name: 'Package VSIX (dry run)'
if: '${{ steps.vars.outputs.is_dry_run == ''true'' }}'
working-directory: 'packages/vscode-ide-companion'
run: |-
if [[ "${{ steps.vars.outputs.is_preview }}" == "true" ]]; then
vsce package --pre-release --out ../qwen-code-vscode-companion-${{ steps.version.outputs.RELEASE_VERSION }}.vsix
else
vsce package --out ../qwen-code-vscode-companion-${{ steps.version.outputs.RELEASE_VERSION }}.vsix
fi
- name: 'Upload VSIX Artifact (dry run)'
if: '${{ steps.vars.outputs.is_dry_run == ''true'' }}'
uses: 'actions/upload-artifact@v4'
with:
name: 'qwen-code-vscode-companion-${{ steps.version.outputs.RELEASE_VERSION }}.vsix'
path: 'packages/qwen-code-vscode-companion-${{ steps.version.outputs.RELEASE_VERSION }}.vsix'
if-no-files-found: 'error'
- name: 'Publish to Microsoft Marketplace'
if: '${{ steps.vars.outputs.is_dry_run == ''false'' }}'
working-directory: 'packages/vscode-ide-companion'
env:
VSCE_PAT: '${{ secrets.VSCE_PAT }}'
VSCODE_TAG: '${{ steps.version.outputs.VSCODE_TAG }}'
run: |-
if [[ "${{ steps.vars.outputs.is_preview }}" == "true" ]]; then
echo "Skipping Microsoft Marketplace for preview release"
else
vsce publish --pat "${VSCE_PAT}" --tag "${VSCODE_TAG}"
fi
- name: 'Publish to OpenVSX'
if: '${{ steps.vars.outputs.is_dry_run == ''false'' }}'
working-directory: 'packages/vscode-ide-companion'
env:
OVSX_TOKEN: '${{ secrets.OVSX_TOKEN }}'
VSCODE_TAG: '${{ steps.version.outputs.VSCODE_TAG }}'
run: |-
if [[ "${{ steps.vars.outputs.is_preview }}" == "true" ]]; then
# For preview releases, publish with preview tag
# First package the extension for preview
vsce package --pre-release --out ../qwen-code-vscode-companion-${{ steps.version.outputs.RELEASE_VERSION }}.vsix
ovsx publish ../qwen-code-vscode-companion-${{ steps.version.outputs.RELEASE_VERSION }}.vsix --pat "${OVSX_TOKEN}" --pre-release
else
# Package and publish normally
vsce package --out ../qwen-code-vscode-companion-${{ steps.version.outputs.RELEASE_VERSION }}.vsix
ovsx publish ../qwen-code-vscode-companion-${{ steps.version.outputs.RELEASE_VERSION }}.vsix --pat "${OVSX_TOKEN}" --tag "${VSCODE_TAG}"
fi
- name: 'Create Issue on Failure'
if: |-
${{ failure() }}
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
DETAILS_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
run: |-
gh issue create \
--title "VSCode IDE Companion Release Failed for ${{ steps.version.outputs.RELEASE_VERSION }} on $(date +'%Y-%m-%d')" \
--body "The VSCode IDE Companion release workflow failed. See the full run for details: ${DETAILS_URL}"

View File

@@ -5,6 +5,8 @@
[![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen.svg)](https://nodejs.org/)
[![Downloads](https://img.shields.io/npm/dm/@qwen-code/qwen-code.svg)](https://www.npmjs.com/package/@qwen-code/qwen-code)
<a href="https://trendshift.io/repositories/15287" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15287" alt="QwenLM%2Fqwen-code | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
**An open-source AI agent that lives in your terminal.**
<a href="https://qwenlm.github.io/qwen-code-docs/zh/users/overview">中文</a> |

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"workspaces": [
"packages/*"
],
@@ -17343,7 +17343,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"dependencies": {
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
@@ -17977,7 +17977,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"hasInstallScript": true,
"dependencies": {
"@anthropic-ai/sdk": "^0.36.1",
@@ -21442,7 +21442,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -21454,7 +21454,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"engines": {
"node": ">=20.0.0"
},
@@ -13,7 +13,7 @@
"url": "git+https://github.com/QwenLM/qwen-code.git"
},
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.8.0"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.8.0-preview.2"
},
"scripts": {
"start": "cross-env node scripts/start.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"description": "Qwen Code",
"repository": {
"type": "git",
@@ -33,7 +33,7 @@
"dist"
],
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.8.0"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.8.0-preview.2"
},
"dependencies": {
"@google/genai": "1.30.0",

View File

@@ -5,7 +5,8 @@
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getExtensionManager } from './utils.js';
import { getExtensionManager, extensionToOutputString } from './utils.js';
import type { Extension, ExtensionManager } from '@qwen-code/qwen-code-core';
const mockRefreshCache = vi.fn();
const mockExtensionManagerInstance = {
@@ -64,3 +65,70 @@ describe('getExtensionManager', () => {
);
});
});
describe('extensionToOutputString', () => {
const mockIsEnabled = vi.fn();
const mockExtensionManager = {
isEnabled: mockIsEnabled,
} as unknown as ExtensionManager;
const createMockExtension = (overrides = {}): Extension => ({
id: 'test-ext-id',
name: 'test-extension',
version: '1.0.0',
isActive: true,
path: '/path/to/extension',
contextFiles: [],
config: { name: 'test-extension', version: '1.0.0' },
...overrides,
});
beforeEach(() => {
vi.clearAllMocks();
mockIsEnabled.mockReturnValue(true);
});
it('should include status icon when inline is false', () => {
const extension = createMockExtension();
const result = extensionToOutputString(
extension,
mockExtensionManager,
'/workspace',
false,
);
// Should contain either ✓ or ✗ (with ANSI color codes)
expect(result).toMatch(/test-extension/);
expect(result).toContain('(1.0.0)');
});
it('should exclude status icon when inline is true', () => {
const extension = createMockExtension();
const result = extensionToOutputString(
extension,
mockExtensionManager,
'/workspace',
true,
);
// Should start with extension name (after stripping potential whitespace)
expect(result.trim()).toMatch(/^test-extension/);
});
it('should default inline to false', () => {
const extension = createMockExtension();
const resultWithoutInline = extensionToOutputString(
extension,
mockExtensionManager,
'/workspace',
);
const resultWithInlineFalse = extensionToOutputString(
extension,
mockExtensionManager,
'/workspace',
false,
);
expect(resultWithoutInline).toEqual(resultWithInlineFalse);
});
});

View File

@@ -32,6 +32,7 @@ export function extensionToOutputString(
extension: Extension,
extensionManager: ExtensionManager,
workspaceDir: string,
inline = false,
): string {
const cwd = workspaceDir;
const userEnabled = extensionManager.isEnabled(
@@ -44,7 +45,7 @@ export function extensionToOutputString(
);
const status = workspaceEnabled ? chalk.green('✓') : chalk.red('✗');
let output = `${status} ${extension.config.name} (${extension.config.version})`;
let output = `${inline ? '' : status} ${extension.config.name} (${extension.config.version})`;
output += `\n Path: ${extension.path}`;
if (extension.installMetadata) {
output += `\n Source: ${extension.installMetadata.source} (Type: ${extension.installMetadata.type})`;

View File

@@ -139,7 +139,7 @@ export const addCommand: CommandModule = {
describe: 'Add a server',
builder: (yargs) =>
yargs
.usage('Usage: gemini mcp add [options] <name> <commandOrUrl> [args...]')
.usage('Usage: qwen mcp add [options] <name> <commandOrUrl> [args...]')
.parserConfiguration({
'unknown-options-as-args': true, // Pass unknown options as server args
'populate--': true, // Populate server args after -- separator

View File

@@ -777,4 +777,87 @@ describe('extensionsCommand', () => {
);
});
});
describe('detail', () => {
const detailAction = extensionsCommand.subCommands?.find(
(cmd) => cmd.name === 'detail',
)?.action;
if (!detailAction) {
throw new Error('Detail action not found');
}
let realMockExtensionManager: ExtensionManager;
beforeEach(() => {
vi.resetAllMocks();
realMockExtensionManager = Object.create(ExtensionManager.prototype);
realMockExtensionManager.getLoadedExtensions = mockGetLoadedExtensions;
mockContext = createMockCommandContext({
invocation: {
raw: '/extensions detail',
name: 'detail',
args: '',
},
services: {
config: {
getExtensions: mockGetExtensions,
getWorkingDir: () => '/test/dir',
getExtensionManager: () => realMockExtensionManager,
},
},
ui: {
dispatchExtensionStateUpdate: vi.fn(),
},
});
});
it('should show usage if no name is provided', async () => {
await detailAction(mockContext, '');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: 'Usage: /extensions detail <extension-name>',
},
expect.any(Number),
);
});
it('should show error if extension not found', async () => {
mockGetExtensions.mockReturnValue([]);
await detailAction(mockContext, 'nonexistent-extension');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.ERROR,
text: 'Extension "nonexistent-extension" not found.',
},
expect.any(Number),
);
});
it('should show extension details when found', async () => {
const extension: Extension = {
id: 'test-ext',
name: 'test-ext',
version: '1.0.0',
isActive: true,
path: '/test/dir/test-ext',
contextFiles: [],
config: { name: 'test-ext', version: '1.0.0' },
};
mockGetExtensions.mockReturnValue([extension]);
realMockExtensionManager.isEnabled = vi.fn().mockReturnValue(true);
await detailAction(mockContext, 'test-ext');
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text: expect.stringContaining('test-ext'),
},
expect.any(Number),
);
});
});
});

View File

@@ -20,6 +20,7 @@ import {
} from '@qwen-code/qwen-code-core';
import { SettingScope } from '../../config/settings.js';
import open from 'open';
import { extensionToOutputString } from '../../commands/extensions/utils.js';
const EXTENSION_EXPLORE_URL = {
Gemini: 'https://geminicli.com/extensions/',
@@ -475,6 +476,53 @@ async function enableAction(context: CommandContext, args: string) {
}
}
async function detailAction(context: CommandContext, args: string) {
const extensionManager = context.services.config?.getExtensionManager();
if (!(extensionManager instanceof ExtensionManager)) {
console.error(
`Cannot ${context.invocation?.name} extensions in this environment`,
);
return;
}
const name = args.trim();
if (!name) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: t('Usage: /extensions detail <extension-name>'),
},
Date.now(),
);
return;
}
const extensions = context.services.config!.getExtensions();
const extension = extensions.find((extension) => extension.name === name);
if (!extension) {
context.ui.addItem(
{
type: MessageType.ERROR,
text: t('Extension "{{name}}" not found.', { name }),
},
Date.now(),
);
return;
}
context.ui.addItem(
{
type: MessageType.INFO,
text: extensionToOutputString(
extension,
extensionManager,
process.cwd(),
true,
),
},
Date.now(),
);
}
export async function completeExtensions(
context: CommandContext,
partialArg: string,
@@ -495,7 +543,10 @@ export async function completeExtensions(
name.startsWith(partialArg),
);
if (context.invocation?.name !== 'uninstall') {
if (
context.invocation?.name !== 'uninstall' &&
context.invocation?.name !== 'detail'
) {
if ('--all'.startsWith(partialArg) || 'all'.startsWith(partialArg)) {
suggestions.unshift('--all');
}
@@ -594,6 +645,16 @@ const uninstallCommand: SlashCommand = {
completion: completeExtensions,
};
const detailCommand: SlashCommand = {
name: 'detail',
get description() {
return t('Get detail of an extension');
},
kind: CommandKind.BUILT_IN,
action: detailAction,
completion: completeExtensions,
};
export const extensionsCommand: SlashCommand = {
name: 'extensions',
get description() {
@@ -608,6 +669,7 @@ export const extensionsCommand: SlashCommand = {
installCommand,
uninstallCommand,
exploreExtensionsCommand,
detailCommand,
],
action: (context, args) =>
// Default to list if no subcommand is provided

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-core",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"description": "Qwen Code Core",
"repository": {
"type": "git",

View File

@@ -218,6 +218,30 @@ describe('extension tests', () => {
]);
});
it('should use default QWEN.md when contextFileName is empty array', async () => {
const extDir = path.join(userExtensionsDir, 'ext-empty-context');
fs.mkdirSync(extDir, { recursive: true });
fs.writeFileSync(
path.join(extDir, EXTENSIONS_CONFIG_FILENAME),
JSON.stringify({
name: 'ext-empty-context',
version: '1.0.0',
contextFileName: [],
}),
);
fs.writeFileSync(path.join(extDir, 'QWEN.md'), 'context content');
const manager = createExtensionManager();
await manager.refreshCache();
const extensions = manager.getLoadedExtensions();
expect(extensions).toHaveLength(1);
const ext = extensions.find((e) => e.config.name === 'ext-empty-context');
expect(ext?.contextFiles).toEqual([
path.join(userExtensionsDir, 'ext-empty-context', 'QWEN.md'),
]);
});
it('should skip extensions with invalid JSON and log a warning', async () => {
const consoleSpy = vi
.spyOn(console, 'error')
@@ -694,13 +718,14 @@ describe('extension tests', () => {
expect(() => validateName('UPPERCASE')).not.toThrow();
});
it('should accept names with underscores and dots', () => {
expect(() => validateName('my_extension')).not.toThrow();
expect(() => validateName('my.extension')).not.toThrow();
expect(() => validateName('my_ext.v1')).not.toThrow();
expect(() => validateName('ext_1.2.3')).not.toThrow();
});
it('should reject names with invalid characters', () => {
expect(() => validateName('my_extension')).toThrow(
'Invalid extension name',
);
expect(() => validateName('my.extension')).toThrow(
'Invalid extension name',
);
expect(() => validateName('my extension')).toThrow(
'Invalid extension name',
);

View File

@@ -190,7 +190,7 @@ function filterMcpConfig(original: MCPServerConfig): MCPServerConfig {
}
function getContextFileNames(config: ExtensionConfig): string[] {
if (!config.contextFileName) {
if (!config.contextFileName || config.contextFileName.length === 0) {
return ['QWEN.md'];
} else if (!Array.isArray(config.contextFileName)) {
return [config.contextFileName];
@@ -1244,9 +1244,9 @@ export function hashValue(value: string): string {
}
export function validateName(name: string) {
if (!/^[a-zA-Z0-9-]+$/.test(name)) {
if (!/^[a-zA-Z0-9-_.]+$/.test(name)) {
throw new Error(
`Invalid extension name: "${name}". Only letters (a-z, A-Z), numbers (0-9), and dashes (-) are allowed.`,
`Invalid extension name: "${name}". Only letters (a-z, A-Z), numbers (0-9), underscores (_), dots (.), and dashes (-) are allowed.`,
);
}
}

View File

@@ -117,6 +117,51 @@ describe('git extension helpers', () => {
'Failed to clone Git repository from http://my-repo.com',
);
});
it('should use marketplace source for marketplace type extensions', async () => {
const installMetadata = {
source: 'marketplace:my-plugin',
type: 'marketplace' as const,
marketplace: {
pluginName: 'my-plugin',
marketplaceSource: 'https://github.com/marketplace/my-plugin',
},
};
const destination = '/dest';
mockGit.getRemotes.mockResolvedValue([
{
name: 'origin',
refs: { fetch: 'https://github.com/marketplace/my-plugin' },
},
]);
await cloneFromGit(installMetadata, destination);
expect(mockGit.clone).toHaveBeenCalledWith(
'https://github.com/marketplace/my-plugin',
'./',
['--depth', '1'],
);
});
it('should use source for marketplace type without marketplace metadata', async () => {
const installMetadata = {
source: 'http://fallback-repo.com',
type: 'marketplace' as const,
};
const destination = '/dest';
mockGit.getRemotes.mockResolvedValue([
{ name: 'origin', refs: { fetch: 'http://fallback-repo.com' } },
]);
await cloneFromGit(installMetadata, destination);
expect(mockGit.clone).toHaveBeenCalledWith(
'http://fallback-repo.com',
'./',
['--depth', '1'],
);
});
});
describe('checkForExtensionUpdate', () => {

View File

@@ -53,7 +53,10 @@ export async function cloneFromGit(
): Promise<void> {
try {
const git = simpleGit(destination);
let sourceUrl = installMetadata.source;
let sourceUrl =
installMetadata.type === 'marketplace' && installMetadata.marketplace
? installMetadata.marketplace.marketplaceSource
: installMetadata.source;
const token = getGitHubToken();
if (token) {
try {

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",

View File

@@ -2,7 +2,7 @@
"name": "qwen-code-vscode-ide-companion",
"displayName": "Qwen Code Companion",
"description": "Enable Qwen Code with direct access to your VS Code workspace.",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"publisher": "qwenlm",
"icon": "assets/icon.png",
"repository": {