From eb2a9a8beff27991b3718f9d879e6471977eb7de Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Thu, 30 Apr 2026 22:04:01 +0800 Subject: [PATCH] feat(installer): add standalone archive installation --- .github/workflows/release.yml | 48 + .gitignore | 6 +- .../2026-04-30-standalone-installer-design.md | 141 +++ .../2026-04-30-standalone-installer-plan.md | 124 ++ ...26-04-30-standalone-installer-test-plan.md | 87 ++ README.md | 15 +- docs/users/overview.md | 12 +- docs/users/quickstart.md | 14 +- package.json | 1 + scripts/create-standalone-package.js | 376 ++++++ scripts/installation/INSTALLATION_GUIDE.md | 331 +++-- .../installation/install-qwen-with-source.bat | 776 ++++++++---- .../installation/install-qwen-with-source.sh | 1122 ++++++++++------- scripts/tests/install-script.test.js | 218 ++++ 14 files changed, 2387 insertions(+), 884 deletions(-) create mode 100644 .qwen/design/2026-04-30-standalone-installer-design.md create mode 100644 .qwen/design/2026-04-30-standalone-installer-plan.md create mode 100644 .qwen/e2e-tests/2026-04-30-standalone-installer-test-plan.md create mode 100644 scripts/create-standalone-package.js create mode 100644 scripts/tests/install-script.test.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 367a3834f..bb7f897dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -379,6 +379,52 @@ jobs: npm run bundle npm run prepare:package + - name: 'Build Standalone Archives' + env: + RELEASE_VERSION: '${{ needs.prepare.outputs.release_version }}' + run: |- + set -euo pipefail + + NODE_VERSION="$(node -p "process.versions.node")" + RUNTIME_DIR="${RUNNER_TEMP}/qwen-node-runtime" + NODE_DIST_URL="https://nodejs.org/dist/v${NODE_VERSION}" + mkdir -p "${RUNTIME_DIR}" + curl -fsSL "${NODE_DIST_URL}/SHASUMS256.txt" -o "${RUNTIME_DIR}/SHASUMS256.txt" + + download_node() { + local qwen_target="$1" + local node_target="$2" + local extension="$3" + local archive="${RUNTIME_DIR}/node-v${NODE_VERSION}-${node_target}.${extension}" + local archive_name + archive_name="$(basename "${archive}")" + local checksum_line + local url="${NODE_DIST_URL}/${archive_name}" + + echo "Downloading ${url}" + curl -fsSL "${url}" -o "${archive}" + checksum_line="$(awk -v name="${archive_name}" '$2 == name { print }' "${RUNTIME_DIR}/SHASUMS256.txt")" + if [[ -z "${checksum_line}" ]]; then + echo "::error::Node.js SHASUMS256.txt does not list ${archive_name}" + exit 1 + fi + printf '%s\n' "${checksum_line}" | (cd "${RUNTIME_DIR}" && sha256sum -c -) + npm run package:standalone -- \ + --target "${qwen_target}" \ + --node-archive "${archive}" \ + --out-dir dist/standalone \ + --version "${RELEASE_VERSION}" + } + + download_node darwin-arm64 darwin-arm64 tar.gz + download_node darwin-x64 darwin-x64 tar.gz + download_node linux-arm64 linux-arm64 tar.xz + download_node linux-x64 linux-x64 tar.xz + download_node win-x64 win-x64 zip + + ls -la dist/standalone + cat dist/standalone/SHA256SUMS + - name: 'Publish @qwen-code/qwen-code' working-directory: 'dist' run: |- @@ -411,6 +457,8 @@ jobs: gh release create "${RELEASE_TAG}" \ dist/cli.js \ + dist/standalone/qwen-code-* \ + dist/standalone/SHA256SUMS \ --target "${RELEASE_BRANCH}" \ --title "Release ${RELEASE_TAG}" \ --notes-start-tag "${PREVIOUS_RELEASE_TAG}" \ diff --git a/.gitignore b/.gitignore index 2dae5710a..0a0b02ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,10 @@ CLAUDE.md !.qwen/skills/** !.qwen/agents/ !.qwen/agents/** +!.qwen/design/ +!.qwen/design/** +!.qwen/e2e-tests/ +!.qwen/e2e-tests/** # OS metadata .DS_Store @@ -89,4 +93,4 @@ storybook-static # Dev symlink: qc-helper bundled skill docs (created by scripts/dev.js) packages/core/src/skills/bundled/qc-helper/docs -tmp/ \ No newline at end of file +tmp/ diff --git a/.qwen/design/2026-04-30-standalone-installer-design.md b/.qwen/design/2026-04-30-standalone-installer-design.md new file mode 100644 index 000000000..ba49364fe --- /dev/null +++ b/.qwen/design/2026-04-30-standalone-installer-design.md @@ -0,0 +1,141 @@ +# Standalone Installer Design + +## Problem + +The current one-line installer installs Qwen Code through npm. That keeps the +script small, but it still requires users to bring a working Node.js and npm +environment. This is fragile for less technical users, and it does not support +offline or controlled enterprise installs well. + +Qwen Code already publishes a bundled `dist/cli.js` to GitHub Releases, but the +asset still needs a local Node.js runtime. To remove that dependency, releases +need standalone archives that bundle the Qwen CLI with a private Node.js +runtime and a small launcher. + +## Goals + +- Prefer standalone release archives when they are available. +- Fall back to npm when no standalone asset exists for the requested platform. +- Keep npm installation available explicitly with `--method npm`. +- Support fully offline installs with `--archive /path/to/archive`. +- Support GitHub Releases and an Aliyun OSS/CDN mirror with the same artifact + names and checksums. +- Avoid modifying npm config, shell profiles, or user PATH permanently. +- Never start `qwen` automatically from the installer. + +## Non-Goals + +- Build a single native executable in this change. +- Add geolocation-based mirror selection. +- Install Node.js, NVM, or system packages on behalf of the user. +- Solve code signing or notarization in the first implementation. +- Guarantee parity for optional native modules such as `node-pty` and clipboard + packages. The CLI already degrades when these optional modules are absent; + a later release job can add target-specific `node_modules` if that parity is + required. + +## Artifact Format + +Each release can publish these assets: + +- `qwen-code-darwin-arm64.tar.gz` +- `qwen-code-darwin-x64.tar.gz` +- `qwen-code-linux-arm64.tar.gz` +- `qwen-code-linux-x64.tar.gz` +- `qwen-code-win-x64.zip` +- `SHA256SUMS` + +The asset names intentionally do not include the version. This allows the +installer to use GitHub's `releases/latest/download/` URL without an API +call. Versioned installation is still supported by switching the base URL to +`releases/download/vX.Y.Z`. + +Archive layout: + +```text +qwen-code/ + bin/qwen + bin/qwen.cmd + lib/cli.js + node/... + package.json + README.md + LICENSE + manifest.json +``` + +The Unix launcher executes `node/bin/node ../lib/cli.js`. The Windows launcher +executes `node/node.exe ..\lib\cli.js`. Bundling the full Node distribution is +larger than a single executable, but it is predictable and works with the +existing ESM bundle without requiring a user-managed Node.js installation. + +## Installer Behavior + +`--method detect` is the default: + +1. If `--archive` is provided, install that local archive. +2. Detect OS and architecture. +3. Build an archive URL from the selected mirror/base URL. +4. If the archive exists, download it, verify `SHA256SUMS`, extract it into the + user install directory, and expose `qwen`. +5. If the archive does not exist, fall back to npm. + +`--method standalone` follows the same standalone path, but a missing or failed +standalone asset is fatal. + +`--method npm` skips standalone logic and runs npm installation after checking +that Node.js 20+ and npm are available. + +## Install Locations + +Unix: + +- Runtime: `$HOME/.local/lib/qwen-code` +- Command shim: `$HOME/.local/bin/qwen` + +Windows: + +- Runtime: `%LOCALAPPDATA%\qwen-code\qwen-code` +- Command shim: `%LOCALAPPDATA%\qwen-code\bin\qwen.cmd` + +The installer may add the command directory to the current process PATH for +verification, but it does not write shell profiles or persistent environment +variables. If the command directory is not on PATH, the installer prints the +exact directory to add. + +## Distribution Sources + +GitHub is the canonical source: + +```text +https://github.com/QwenLM/qwen-code/releases/latest/download +https://github.com/QwenLM/qwen-code/releases/download/vX.Y.Z +``` + +Aliyun OSS/CDN is a mirror: + +```text +https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/releases/qwen-code/latest +https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/releases/qwen-code/vX.Y.Z +``` + +All mirrors must serve byte-identical artifacts and the same `SHA256SUMS`. + +## Safety + +- Remote standalone installs require checksum verification. +- Local archive installs do not require network access. +- The installer only deletes temporary extraction directories and the previous + managed standalone install directory. +- npm fallback does not change npm prefix, npmrc, or PATH. + +## Verification Strategy + +- Static tests ensure the installer keeps the expected methods and does not + reintroduce Node/NVM installation or automatic `qwen` startup. +- Packaging tests can run against a fake target and fake Node distribution. +- Shell smoke tests run installer branches with fake `curl`, `tar`, `npm`, + `node`, and `qwen`. +- GitHub Actions should later run Linux, macOS, and Windows installer smoke + tests with locally generated archives before enabling standalone as the + public default. diff --git a/.qwen/design/2026-04-30-standalone-installer-plan.md b/.qwen/design/2026-04-30-standalone-installer-plan.md new file mode 100644 index 000000000..78c4648a4 --- /dev/null +++ b/.qwen/design/2026-04-30-standalone-installer-plan.md @@ -0,0 +1,124 @@ +# Standalone Installer Implementation Plan + +**Goal:** Add code-server-style standalone archive distribution with npm fallback. + +**Architecture:** Release builds produce per-platform archives that bundle +`dist/cli.js`, required runtime assets, and a private Node.js runtime. The +installer defaults to `detect`, installs a standalone archive when available, +and falls back to npm otherwise. + +**Tech Stack:** Bash, Windows batch, Node.js release scripting, GitHub Actions, +Vitest static/smoke tests. + +## Task 1: Installer Contract Tests + +**Files:** + +- Modify: `scripts/tests/install-script.test.js` + +**Steps:** + +1. Add tests asserting the Unix installer exposes `--method`, `--mirror`, + `--base-url`, `--archive`, standalone install functions, checksum + verification, and npm fallback. +2. Add tests asserting the Windows installer exposes the same options and uses + PowerShell/CertUtil for archive install and checksum verification. +3. Run `npm run test:scripts`. +4. Confirm the new tests fail before implementation. + +## Task 2: Standalone Package Script + +**Files:** + +- Create: `scripts/create-standalone-package.js` +- Modify: `package.json` + +**Steps:** + +1. Add a Node.js script that accepts `--target`, `--node-archive`, + `--out-dir`, and optional `--version`. +2. Require `dist/cli.js`, `dist/vendor`, `README.md`, and `LICENSE`. +3. Extract a Node.js distribution archive into a staging directory. +4. Create `qwen-code/bin/qwen`, `qwen-code/bin/qwen.cmd`, + `qwen-code/lib/cli.js`, copied runtime assets, and `manifest.json`. +5. Emit `qwen-code-.tar.gz` for Unix targets and + `qwen-code-.zip` for Windows targets. +6. Write/update `SHA256SUMS`. +7. Add `npm run package:standalone`. +8. Add focused script tests where practical. + +## Task 3: Unix Installer Standalone Flow + +**Files:** + +- Modify: `scripts/installation/install-qwen-with-source.sh` + +**Steps:** + +1. Add argument parsing for `--method`, `--mirror`, `--base-url`, `--archive`, + and `--version`. +2. Add target detection for supported OS/arch combinations. +3. Add URL construction for GitHub and Aliyun mirrors. +4. Add archive availability check for detect mode. +5. Add download, checksum verification, extraction, and shim creation. +6. Keep npm installation as fallback and as explicit `--method npm`. +7. Keep source tracking and final instructions. + +## Task 4: Windows Installer Standalone Flow + +**Files:** + +- Modify: `scripts/installation/install-qwen-with-source.bat` + +**Steps:** + +1. Add argument parsing for `--method`, `--mirror`, `--base-url`, `--archive`, + and `--version`. +2. Add target detection for `win-x64`. +3. Add archive download with PowerShell. +4. Add checksum verification with `certutil`. +5. Add archive extraction with PowerShell `Expand-Archive`. +6. Install to `%LOCALAPPDATA%\qwen-code\qwen-code` and expose + `%LOCALAPPDATA%\qwen-code\bin\qwen.cmd`. +7. Keep npm fallback and source tracking. + +## Task 5: Release Workflow + +**Files:** + +- Modify: `.github/workflows/release.yml` + +**Steps:** + +1. After `npm run prepare:package`, download supported Node.js runtime + archives. +2. Run `npm run package:standalone -- --target ...` for each supported target. +3. Upload `dist/standalone/qwen-code-*` and `dist/standalone/SHA256SUMS` to the + GitHub Release alongside `dist/cli.js`. + +## Task 6: Documentation + +**Files:** + +- Modify: `README.md` +- Modify: `docs/users/overview.md` +- Modify: `docs/users/quickstart.md` +- Modify: `scripts/installation/INSTALLATION_GUIDE.md` +- Create: `.qwen/e2e-tests/2026-04-30-standalone-installer-test-plan.md` + +**Steps:** + +1. Document install methods and mirror choices. +2. Document offline archive installation. +3. Document release artifact names. +4. Document platform verification plan. + +## Task 7: Verification + +**Commands:** + +- `npm run test:scripts` +- `npx prettier --check README.md docs/users/quickstart.md docs/users/overview.md scripts/installation/INSTALLATION_GUIDE.md .qwen/design/2026-04-30-standalone-installer-design.md .qwen/design/2026-04-30-standalone-installer-plan.md .qwen/e2e-tests/2026-04-30-standalone-installer-test-plan.md scripts/tests/install-script.test.js` +- `bash -n scripts/installation/install-qwen-with-source.sh` +- `git diff --check` +- Local fake-runtime installer smoke for npm and standalone paths. diff --git a/.qwen/e2e-tests/2026-04-30-standalone-installer-test-plan.md b/.qwen/e2e-tests/2026-04-30-standalone-installer-test-plan.md new file mode 100644 index 000000000..edcf7ec80 --- /dev/null +++ b/.qwen/e2e-tests/2026-04-30-standalone-installer-test-plan.md @@ -0,0 +1,87 @@ +# Standalone Installer Test Plan + +## Scope + +This plan verifies the one-line installer after standalone archive support is +added. It covers installer behavior, artifact packaging, and fallback behavior +without requiring real global npm writes. + +## Local Smoke Matrix + +Run from the repository root after `npm run bundle && npm run prepare:package`. + +### Unix Standalone Archive + +1. Build a standalone archive for the current target with a local Node.js + archive. +2. Create a temporary `HOME`. +3. Run: + + ```bash + HOME="$tmp_home" bash scripts/installation/install-qwen-with-source.sh \ + --method standalone \ + --archive dist/standalone/qwen-code-.tar.gz \ + --source github + ``` + +4. Expected: + - `$tmp_home/.local/lib/qwen-code` exists. + - `$tmp_home/.local/bin/qwen` exists and is executable. + - `$tmp_home/.qwen/source.json` contains `{"source":"github"}`. + - Installer does not write `.bashrc`, `.zshrc`, `.npmrc`, or npm prefix. + +### Unix npm Fallback + +1. Put fake `node`, `npm`, and `qwen` commands in a temporary PATH. +2. Run detect mode with a fake base URL whose archive does not exist. +3. Expected: + - npm is invoked with `install -g @qwen-code/qwen-code@latest`. + - `qwen` is not executed interactively. + +### Unix Standalone Failure + +1. Run `--method standalone` with a fake base URL whose archive does not exist. +2. Expected: + - installer exits non-zero. + - npm is not invoked. + +### Windows Standalone Archive + +Run on `windows-latest` or a Windows VM: + +```cmd +set USERPROFILE=%TEMP%\qwen-user +set LOCALAPPDATA=%TEMP%\qwen-local +scripts\installation\install-qwen-with-source.bat --method standalone --archive dist\standalone\qwen-code-win-x64.zip --source github +``` + +Expected: + +- `%LOCALAPPDATA%\qwen-code\qwen-code` exists. +- `%LOCALAPPDATA%\qwen-code\bin\qwen.cmd` exists. +- `%USERPROFILE%\.qwen\source.json` exists. +- The script does not require Administrator. + +## CI Matrix + +- `ubuntu-latest`: package + install Linux x64 archive. +- `macos-latest`: package + install Darwin arm64/x64 depending on runner. +- `windows-latest`: package + install Windows x64 archive. + +## Manual Release Verification + +For a release candidate: + +1. Download `SHA256SUMS` and all archives from GitHub Release. +2. Verify checksums locally. +3. Sync the same files to OSS/CDN. +4. Download one archive from GitHub and one from OSS/CDN. +5. Confirm byte-identical checksums. +6. Run installer with: + + ```bash + --mirror github --method standalone + --mirror aliyun --method standalone + --method npm + --archive /path/to/archive + ``` diff --git a/README.md b/README.md index 5358f9d0e..edae546ef 100644 --- a/README.md +++ b/README.md @@ -43,13 +43,18 @@ Qwen Code is an open-source AI agent for the terminal, optimized for Qwen series ### Quick Install (Recommended) +The installer uses a standalone Qwen Code archive when one is available for +your platform, so the default path does not require a preinstalled Node.js +runtime. If a standalone archive is not available, it falls back to npm and then +requires Node.js 20 or later with npm on PATH. + #### Linux / macOS ```bash bash -c "$(curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.sh)" ``` -#### Windows (Run as Administrator) +#### Windows Works in both Command Prompt and PowerShell: @@ -57,13 +62,17 @@ Works in both Command Prompt and PowerShell: powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path $env:TEMP 'install-qwen.bat')" ``` -> **Note**: It's recommended to restart your terminal after installation to ensure environment variables take effect. +> **Note**: It's recommended to restart your terminal after installation if +> `qwen` is not immediately available on PATH. For offline installation, download +> a release archive such as `qwen-code-linux-x64.tar.gz` or +> `qwen-code-win-x64.zip`, then run the installer with `--archive PATH`. ### Manual Installation #### Prerequisites -Make sure you have Node.js 20 or later installed. Download it from [nodejs.org](https://nodejs.org/en/download). +Manual npm installation requires Node.js 20 or later. Download it from +[nodejs.org](https://nodejs.org/en/download). #### NPM diff --git a/docs/users/overview.md b/docs/users/overview.md index 55506fefa..2bfee0b7a 100644 --- a/docs/users/overview.md +++ b/docs/users/overview.md @@ -9,13 +9,17 @@ ### Install Qwen Code: +The recommended installer uses a standalone archive when one is available for +your platform. If it falls back to npm, Node.js 20 or later with npm must be +available on PATH. + **Linux / macOS** ```sh curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.sh | bash ``` -**Windows (Run as Administrator)** +**Windows** ```cmd powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path $env:TEMP 'install-qwen.bat')" @@ -23,7 +27,11 @@ powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou > [!note] > -> It's recommended to restart your terminal after installation to ensure environment variables take effect. If the installation fails, please refer to [Manual Installation](./quickstart#manual-installation) in the Quickstart guide. +> It's recommended to restart your terminal after installation if `qwen` is not +> immediately available on PATH. If the installation fails, please refer to +> [Manual Installation](./quickstart#manual-installation) in the Quickstart +> guide. For offline installation, download a release archive and run the +> installer with `--archive PATH`. ### Start using Qwen Code: diff --git a/docs/users/quickstart.md b/docs/users/quickstart.md index ee3ff6081..10840cbd4 100644 --- a/docs/users/quickstart.md +++ b/docs/users/quickstart.md @@ -12,6 +12,10 @@ Make sure you have: - A code project to work with - An API key from Alibaba Cloud Model Studio ([Beijing](https://bailian.console.aliyun.com/) / [intl](https://modelstudio.console.alibabacloud.com/)), or an Alibaba Cloud Coding Plan ([Beijing](https://bailian.console.aliyun.com/cn-beijing/?tab=coding-plan#/efm/coding-plan-index) / [intl](https://modelstudio.console.alibabacloud.com/?tab=coding-plan#/efm/coding-plan-index)) subscription +The recommended installer uses a standalone archive when one is available for +your platform. If it falls back to npm, you will need Node.js 20 or later with +npm available on PATH. + ## Step 1: Install Qwen Code To install Qwen Code, use one of the following methods: @@ -24,7 +28,7 @@ To install Qwen Code, use one of the following methods: curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.sh | bash ``` -**Windows (Run as Administrator)** +**Windows** ```cmd powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path $env:TEMP 'install-qwen.bat')" @@ -32,13 +36,17 @@ powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou > [!note] > -> It's recommended to restart your terminal after installation to ensure environment variables take effect. +> It's recommended to restart your terminal after installation if `qwen` is not +> immediately available on PATH. For offline installation, download a release +> archive such as `qwen-code-linux-x64.tar.gz` or `qwen-code-win-x64.zip`, then +> run the installer with `--archive PATH`. ### Manual Installation **Prerequisites** -Make sure you have Node.js 20 or later installed. Download it from [nodejs.org](https://nodejs.org/en/download). +Manual npm installation requires Node.js 20 or later. Download it from +[nodejs.org](https://nodejs.org/en/download). **NPM** diff --git a/package.json b/package.json index 8ab445e35..984d96e72 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "preflight": "npm run clean && npm ci && npm run format && npm run lint:ci && npm run build && npm run typecheck && npm run test:ci", "prepare": "husky && npm run build && npm run bundle", "prepare:package": "node scripts/prepare-package.js", + "package:standalone": "node scripts/create-standalone-package.js", "release:version": "node scripts/version.js", "telemetry": "node scripts/telemetry.js", "check:lockfile": "node scripts/check-lockfile.js", diff --git a/scripts/create-standalone-package.js b/scripts/create-standalone-package.js new file mode 100644 index 000000000..6da9ef6db --- /dev/null +++ b/scripts/create-standalone-package.js @@ -0,0 +1,376 @@ +#!/usr/bin/env node + +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import { execFileSync } from 'node:child_process'; +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const rootDir = path.resolve(__dirname, '..'); +const distDir = path.join(rootDir, 'dist'); + +const TARGETS = new Map([ + [ + 'darwin-arm64', + { outputExtension: 'tar.gz', nodeExecutable: ['bin', 'node'] }, + ], + [ + 'darwin-x64', + { outputExtension: 'tar.gz', nodeExecutable: ['bin', 'node'] }, + ], + [ + 'linux-arm64', + { outputExtension: 'tar.gz', nodeExecutable: ['bin', 'node'] }, + ], + ['linux-x64', { outputExtension: 'tar.gz', nodeExecutable: ['bin', 'node'] }], + ['win-x64', { outputExtension: 'zip', nodeExecutable: ['node.exe'] }], +]); + +const DIST_REQUIRED_PATHS = ['cli.js', 'vendor', 'bundled/qc-helper/docs']; +const ROOT_REQUIRED_PATHS = ['README.md', 'LICENSE']; + +main(); + +function main() { + const args = parseArgs(process.argv.slice(2)); + + if (args.help) { + printUsage(); + return; + } + + const target = args.target; + if (!target || !TARGETS.has(target)) { + fail(`--target must be one of: ${Array.from(TARGETS.keys()).join(', ')}`); + } + + if (!args.nodeArchive) { + fail('--node-archive is required'); + } + + const nodeArchive = path.resolve(args.nodeArchive); + if (!fs.existsSync(nodeArchive)) { + fail(`Node.js archive not found: ${nodeArchive}`); + } + + assertRequiredInputs(); + + const version = args.version || readPackageVersion(); + const outDir = path.resolve(args.outDir || path.join(distDir, 'standalone')); + fs.mkdirSync(outDir, { recursive: true }); + + const targetConfig = TARGETS.get(target); + const outputName = `qwen-code-${target}.${targetConfig.outputExtension}`; + const outputPath = path.join(outDir, outputName); + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'qwen-standalone-')); + + try { + const packageRoot = path.join(tempRoot, 'qwen-code'); + const runtimeExtractDir = path.join(tempRoot, 'runtime'); + fs.mkdirSync(packageRoot, { recursive: true }); + fs.mkdirSync(runtimeExtractDir, { recursive: true }); + + copyRuntimeAssets(packageRoot); + extractNodeArchive(nodeArchive, runtimeExtractDir); + const nodeDir = path.join(packageRoot, 'node'); + copyExtractedNode(runtimeExtractDir, nodeDir); + validateNodeRuntime(target, nodeDir); + writeShims(packageRoot); + writeManifest(packageRoot, { + version, + target, + nodeArchive: path.basename(nodeArchive), + }); + + if (fs.existsSync(outputPath)) { + fs.rmSync(outputPath, { force: true }); + } + createArchive(targetConfig.outputExtension, outputPath, tempRoot); + writeSha256Sums(outDir); + + console.log(`Created ${path.relative(rootDir, outputPath)}`); + console.log( + `Updated ${path.relative(rootDir, path.join(outDir, 'SHA256SUMS'))}`, + ); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } +} + +function parseArgs(argv) { + const args = { + help: false, + outDir: undefined, + nodeArchive: undefined, + target: undefined, + version: undefined, + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + switch (arg) { + case '--help': + case '-h': + args.help = true; + break; + case '--target': + args.target = readOptionValue(argv, index, arg); + index += 1; + break; + case '--node-archive': + args.nodeArchive = readOptionValue(argv, index, arg); + index += 1; + break; + case '--out-dir': + args.outDir = readOptionValue(argv, index, arg); + index += 1; + break; + case '--version': + args.version = readOptionValue(argv, index, arg); + index += 1; + break; + default: + fail(`Unknown option: ${arg}`); + } + } + + return args; +} + +function readOptionValue(argv, index, optionName) { + const value = argv[index + 1]; + if (!value || value.startsWith('-')) { + fail(`${optionName} requires a value`); + } + return value; +} + +function printUsage() { + console.log(`Qwen Code standalone package builder + +Usage: + npm run package:standalone -- --target TARGET --node-archive PATH [OPTIONS] + +Options: + --target TARGET One of: ${Array.from(TARGETS.keys()).join(', ')} + --node-archive PATH Downloaded Node.js runtime archive. + --out-dir DIR Output directory. Defaults to dist/standalone. + --version VERSION Qwen Code version. Defaults to package.json version. + -h, --help Show this help message.`); +} + +function assertRequiredInputs() { + if (!fs.existsSync(distDir)) { + fail('dist/ directory not found. Run "npm run bundle" first.'); + } + + for (const relativePath of DIST_REQUIRED_PATHS) { + const fullPath = path.join(distDir, relativePath); + if (!fs.existsSync(fullPath)) { + fail(`Required dist asset missing: ${fullPath}`); + } + } + + for (const relativePath of ROOT_REQUIRED_PATHS) { + const fullPath = path.join(rootDir, relativePath); + if (!fs.existsSync(fullPath)) { + fail(`Required repository file missing: ${fullPath}`); + } + } +} + +function readPackageVersion() { + const packageJsonPath = path.join(rootDir, 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + return packageJson.version; +} + +function copyRuntimeAssets(packageRoot) { + const libDir = path.join(packageRoot, 'lib'); + fs.mkdirSync(libDir, { recursive: true }); + + for (const entry of fs.readdirSync(distDir)) { + if (entry === 'standalone') { + continue; + } + fs.cpSync(path.join(distDir, entry), path.join(libDir, entry), { + recursive: true, + verbatimSymlinks: true, + }); + } + + for (const fileName of ROOT_REQUIRED_PATHS) { + fs.copyFileSync( + path.join(rootDir, fileName), + path.join(packageRoot, fileName), + ); + } + + const distPackageJson = path.join(distDir, 'package.json'); + if (fs.existsSync(distPackageJson)) { + fs.copyFileSync(distPackageJson, path.join(packageRoot, 'package.json')); + } else { + fs.copyFileSync( + path.join(rootDir, 'package.json'), + path.join(packageRoot, 'package.json'), + ); + } +} + +function extractNodeArchive(nodeArchive, extractDir) { + if (nodeArchive.endsWith('.zip')) { + run('unzip', ['-q', nodeArchive, '-d', extractDir]); + return; + } + + if ( + nodeArchive.endsWith('.tar.gz') || + nodeArchive.endsWith('.tgz') || + nodeArchive.endsWith('.tar.xz') + ) { + run('tar', ['-xf', nodeArchive, '-C', extractDir]); + return; + } + + fail( + `Unsupported Node.js archive format: ${nodeArchive}. Expected .zip, .tar.gz, .tgz, or .tar.xz.`, + ); +} + +function copyExtractedNode(extractDir, nodeDir) { + const entries = fs + .readdirSync(extractDir) + .filter((entry) => entry !== '.DS_Store'); + if (entries.length === 0) { + fail('Node.js archive did not contain any files.'); + } + + const sourceRoot = + entries.length === 1 && + fs.statSync(path.join(extractDir, entries[0])).isDirectory() + ? path.join(extractDir, entries[0]) + : extractDir; + + fs.cpSync(sourceRoot, nodeDir, { + recursive: true, + verbatimSymlinks: true, + }); +} + +function validateNodeRuntime(target, nodeDir) { + const targetConfig = TARGETS.get(target); + const executablePath = path.join(nodeDir, ...targetConfig.nodeExecutable); + const displayPath = targetConfig.nodeExecutable.join('/'); + + if (!fs.existsSync(executablePath)) { + fail(`Node.js runtime for ${target} must contain ${displayPath}.`); + } + + if (target !== 'win-x64') { + const mode = fs.statSync(executablePath).mode; + if ((mode & 0o111) === 0) { + fail( + `Node.js runtime for ${target} must provide executable ${displayPath}.`, + ); + } + } +} + +function writeShims(packageRoot) { + const binDir = path.join(packageRoot, 'bin'); + fs.mkdirSync(binDir, { recursive: true }); + + const unixShim = `#!/usr/bin/env sh +set -e +ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +exec "$ROOT/node/bin/node" "$ROOT/lib/cli.js" "$@" +`; + const unixShimPath = path.join(binDir, 'qwen'); + fs.writeFileSync(unixShimPath, unixShim); + fs.chmodSync(unixShimPath, 0o755); + + const windowsShim = `@echo off +setlocal +set "ROOT=%~dp0.." +"%ROOT%\\node\\node.exe" "%ROOT%\\lib\\cli.js" %* +`; + fs.writeFileSync(path.join(binDir, 'qwen.cmd'), windowsShim); +} + +function writeManifest(packageRoot, manifest) { + const manifestPath = path.join(packageRoot, 'manifest.json'); + fs.writeFileSync( + manifestPath, + JSON.stringify( + { + name: '@qwen-code/qwen-code', + version: manifest.version, + target: manifest.target, + nodeArchive: manifest.nodeArchive, + createdAt: new Date().toISOString(), + }, + null, + 2, + ) + '\n', + ); +} + +function createArchive(outputExtension, outputPath, cwd) { + if (outputExtension === 'zip') { + run('zip', ['-qr', outputPath, 'qwen-code'], { cwd }); + return; + } + + run('tar', ['-czf', outputPath, '-C', cwd, 'qwen-code']); +} + +function writeSha256Sums(outDir) { + const entries = fs + .readdirSync(outDir) + .filter( + (entry) => + entry.startsWith('qwen-code-') && + (entry.endsWith('.tar.gz') || entry.endsWith('.zip')), + ) + .sort(); + + const lines = entries.map((entry) => { + const filePath = path.join(outDir, entry); + const hash = crypto + .createHash('sha256') + .update(fs.readFileSync(filePath)) + .digest('hex'); + return `${hash} ${entry}`; + }); + + fs.writeFileSync(path.join(outDir, 'SHA256SUMS'), `${lines.join('\n')}\n`); +} + +function run(command, args, options = {}) { + try { + execFileSync(command, args, { + stdio: 'inherit', + ...options, + }); + } catch (error) { + const detail = + error && typeof error === 'object' && 'message' in error + ? `: ${error.message}` + : ''; + fail(`Command failed: ${command} ${args.join(' ')}${detail}`); + } +} + +function fail(message) { + console.error(`Error: ${message}`); + process.exit(1); +} diff --git a/scripts/installation/INSTALLATION_GUIDE.md b/scripts/installation/INSTALLATION_GUIDE.md index 8a41bc47a..2df19c22b 100644 --- a/scripts/installation/INSTALLATION_GUIDE.md +++ b/scripts/installation/INSTALLATION_GUIDE.md @@ -1,148 +1,177 @@ # Installation Guide for Qwen Code with Source Tracking -This guide describes how to install Node.js and Qwen Code with source information tracking. +This guide describes the source-tracking installation scripts for Qwen Code. +The scripts prefer standalone release archives and can fall back to npm when a +standalone archive is not available. ## Overview -The installation scripts automate the process of installing Node.js (if not present or below version 20) and Qwen Code, while capturing and storing the installation source information for analytics and tracking purposes. +The installers are intentionally lightweight: + +- They try a standalone archive first by default. +- They do not install Node.js, NVM, or any other Node version manager. +- They do not edit npm config or shell profiles. +- They do not start `qwen` automatically after installation. +- They store source information in `~/.qwen/source.json` or + `%USERPROFILE%\.qwen\source.json` when `--source` is provided. + +Standalone archives include a private Node.js runtime, so users do not need a +local Node.js installation on the standalone path. Node.js 20 or newer and npm +are only required when the installer falls back to npm or when +`--method npm` is used. ## Installation Scripts -We provide platform-specific installation scripts: +- Linux/macOS: `install-qwen-with-source.sh` +- Windows: `install-qwen-with-source.bat` -- **Linux/macOS**: `install-qwen-with-source.sh` -- **Windows**: `install-qwen-with-source.bat` +## Release Artifacts -## Linux/macOS Installation +GitHub releases publish these standalone archives: -### Script: install-qwen-with-source.sh +- `qwen-code-darwin-arm64.tar.gz` +- `qwen-code-darwin-x64.tar.gz` +- `qwen-code-linux-arm64.tar.gz` +- `qwen-code-linux-x64.tar.gz` +- `qwen-code-win-x64.zip` +- `SHA256SUMS` -#### Features: +Archive layout: -- Checks for existing Node.js installation and version -- Installs Node.js 20+ if needed using NVM -- Installs Qwen Code globally with source information -- Stores the source information in `~/.qwen/source.json` +```text +qwen-code/ + bin/qwen + bin/qwen.cmd + lib/cli.js + node/ + package.json + README.md + LICENSE + manifest.json +``` -#### Usage: +## Install Methods + +The default method is `detect`: + +1. Detect the current platform. +2. Try to download and install the matching standalone archive. +3. Verify the archive with `SHA256SUMS` when available. +4. Fall back to npm if the standalone archive is not available. + +You can force a method: ```bash -# Install with a specific source -sh install-qwen-with-source.sh --source github - -# Install with internal source -sh install-qwen-with-source.sh -s internal - -# Show help -sh install-qwen-with-source.sh --help +bash install-qwen-with-source.sh --method standalone +bash install-qwen-with-source.sh --method npm ``` -#### Supported Source Values: +```bat +install-qwen-with-source.bat --method standalone +install-qwen-with-source.bat --method npm +``` -- `github` - Installed from GitHub repository -- `npm` - Installed from npm registry -- `internal` - Internal installation -- `local-build` - Local build installation +## Optional Native Modules -#### How it Works: +The standalone archives bundle Qwen Code and a private Node.js runtime. They do +not currently install npm optional native modules such as `node-pty` and +`@teddyzhu/clipboard`. Qwen Code is designed to degrade when these optional +modules are absent, but terminal pty behavior and clipboard image support may +not be identical to an npm installation. -1. The script accepts a `--source` parameter to specify where Qwen Code is being installed from -2. It installs Node.js if needed -3. It installs Qwen Code globally -4. It creates `~/.qwen/source.json` with the specified source information +Use `--method npm` if you specifically need npm to resolve optional native +modules for the current machine. -#### Important Notes: - -⚠️ **After installation, you need to restart your terminal or run:** +## Linux/macOS Usage ```bash -source ~/.bashrc # For bash users -# or -source ~/.zshrc # For zsh users +# Default: standalone archive with npm fallback +bash install-qwen-with-source.sh + +# Record a source value +bash install-qwen-with-source.sh --source github + +# Use npm explicitly +bash install-qwen-with-source.sh --method npm --registry https://registry.npmjs.org + +# Use the Aliyun standalone mirror +bash install-qwen-with-source.sh --mirror aliyun + +# Install an offline archive +bash install-qwen-with-source.sh --archive ./qwen-code-linux-x64.tar.gz ``` -This is required to load the newly installed Node.js and Qwen Code into your PATH. +Standalone installs to: -#### Prerequisites: +- Runtime: `~/.local/lib/qwen-code` +- Shim: `~/.local/bin/qwen` -- curl (for NVM installation and script download) -- bash-compatible shell +Override with `QWEN_INSTALL_ROOT`, `QWEN_INSTALL_LIB_PARENT`, +`QWEN_INSTALL_LIB_DIR`, or `QWEN_INSTALL_BIN_DIR` when needed. -## Windows Installation +## Windows Usage -### Script: install-qwen-with-source.bat +```bat +REM Default: standalone archive with npm fallback +install-qwen-with-source.bat -#### Features: +REM Record a source value +install-qwen-with-source.bat --source github -- Checks for existing Node.js installation and version (requires version 18+) -- Automatically downloads and installs Node.js 24 LTS if not present or version is too low -- Installs Qwen Code globally with source information -- Stores the source information in `%USERPROFILE%\.qwen\source.json` +REM Use npm explicitly +install-qwen-with-source.bat --method npm --registry https://registry.npmjs.org -#### Prerequisites: +REM Use the Aliyun standalone mirror +install-qwen-with-source.bat --mirror aliyun -- **PowerShell (Administrator)**: The script must be run in PowerShell with Administrator privileges -- Internet connection for downloading Node.js and Qwen Code - -#### Usage: - -> ⚠️ **Important**: You must run PowerShell as Administrator to install Node.js and global npm packages. - -**Step 1**: Open PowerShell as Administrator - -- Right-click on PowerShell and select "Run as Administrator" -- Or press `Win + X` and select "Windows PowerShell (Admin)" - -**Step 2**: Navigate to the script directory and run: - -```powershell -# Install with a specific source using --source parameter -./install-qwen-with-source.bat --source github - -# Install with short parameter -./install-qwen-with-source.bat -s internal - -# Use default source (unknown) -./install-qwen-with-source.bat +REM Install an offline archive +install-qwen-with-source.bat --archive qwen-code-win-x64.zip ``` -#### Supported Source Values: +Standalone installs to: -- `github` - Installed from GitHub repository -- `npm` - Installed from npm registry -- `internal` - Internal installation -- `local-build` - Local build installation +- Runtime: `%LOCALAPPDATA%\qwen-code\qwen-code` +- Shim: `%LOCALAPPDATA%\qwen-code\bin\qwen.cmd` -#### How it Works: +Restart the terminal if `qwen` is not immediately available on PATH. -1. The script accepts a `--source` or `-s` parameter to specify where Qwen Code is being installed from -2. It checks if Node.js is already installed and if the version is 18 or higher -3. If Node.js is not installed or version is too low, it automatically downloads and installs Node.js 24 LTS -4. It installs Qwen Code globally using npm -5. It creates `%USERPROFILE%\.qwen\source.json` with the specified source information +## Mirrors and Overrides -#### Why Administrator Privileges are Required: +Options: -- Installing Node.js requires writing to `C:\Program Files\nodejs` -- Installing global npm packages requires elevated permissions -- Modifying system PATH environment variables requires Administrator access +- `--method detect|standalone|npm` +- `--mirror github|aliyun` +- `--base-url URL` +- `--archive PATH` +- `--version VERSION` +- `--registry REGISTRY` +- `--source SOURCE` -## Installation Source Feature +Environment variables: -### Overview +- `QWEN_INSTALL_METHOD` +- `QWEN_INSTALL_MIRROR` +- `QWEN_INSTALL_BASE_URL` +- `QWEN_INSTALL_ARCHIVE` +- `QWEN_INSTALL_VERSION` +- `QWEN_NPM_REGISTRY` -This feature implements the ability to capture and store the installation source of the Qwen Code package. The source information is used for analytics and tracking purposes. +Use `--base-url` for private mirrors. The URL must contain +`qwen-code-` archives and `SHA256SUMS` in the same directory. -### Storage Location +## Supported Source Values -The installation source is stored in a separate file at: +The source value may only contain letters, numbers, dot, underscore, and dash. +Common values are: -- **Unix/Linux/macOS**: `~/.qwen/source.json` -- **Windows**: `%USERPROFILE%\.qwen\source.json` (equivalent to `C:\Users\{username}\.qwen\source.json`) +- `github` +- `npm` +- `internal` +- `local-build` -### File Format +## Source Tracking -The `source.json` file contains: +When `--source` or `-s` is provided, the installer writes: ```json { @@ -150,53 +179,23 @@ The `source.json` file contains: } ``` -### How the Source Information is Used +Locations: -1. **Telemetry Tracking**: The source information is included in RUM (Real User Monitoring) telemetry logs -2. **Analytics**: Helps understand how users are discovering and installing Qwen Code -3. **Distribution Analysis**: Tracks which distribution channels are most popular +- Linux/macOS: `~/.qwen/source.json` +- Windows: `%USERPROFILE%\.qwen\source.json` -### Technical Implementation +The telemetry logger reads this file when available. Missing, invalid, or +unreadable source files are ignored. -- The source information is stored as a separate JSON file -- The `QwenLogger` class reads this file during telemetry initialization -- The source is included in the `app.channel` field of the RUM payload -- The implementation gracefully handles missing files, unknown values, and parsing errors +## Manual Installation -### Verification - -After installation and restarting your terminal (or sourcing your shell configuration), you can verify the source information: - -**Linux/macOS:** - -```bash -cat ~/.qwen/source.json -``` - -**Windows:** - -```cmd -type %USERPROFILE%\.qwen\source.json -``` - -## Manual Installation (Without Source Tracking) - -If you prefer not to use the installation scripts or don't want source tracking: - -### Prerequisites - -```bash -# Node.js 20+ -curl -qL https://www.npmjs.com/install.sh | sh -``` - -### NPM Installation +If source tracking is not needed and Node.js 20 or newer is already available: ```bash npm install -g @qwen-code/qwen-code@latest ``` -### Homebrew (macOS, Linux) +Homebrew users can also install Qwen Code with: ```bash brew install qwen-code @@ -204,47 +203,47 @@ brew install qwen-code ## Troubleshooting -### Script Execution Issues +### Standalone Archive Missing -**Linux/macOS:** +In `detect` mode, the installer falls back to npm. In `standalone` mode, install +fails so that automation can detect the missing artifact. + +### Node.js Missing or Too Old + +This only blocks npm installation. Install or activate Node.js 20 or newer, then +rerun the installer with `--method npm` or let `detect` fall back again. + +### npm Missing + +Install a Node.js distribution that includes npm, then rerun the installer. + +### Permission Errors During npm Install + +The installers do not rewrite npm prefix settings. If global npm installation +fails with a permission error, fix the npm global install location or use a +user-owned Node.js installation, then rerun: ```bash -# Run with sh -sh install-qwen-with-source.sh --source github +npm install -g @qwen-code/qwen-code@latest --registry https://registry.npmmirror.com ``` -**Windows (PowerShell as Administrator):** +### qwen Is Not on PATH After Installation -```powershell -# Run the script with --source parameter -./install-qwen-with-source.bat --source github +Restart the terminal first. For standalone installs, add the shim directory: -# Or with short parameter -./install-qwen-with-source.bat -s github +```bash +export PATH="$HOME/.local/bin:$PATH" ``` -### Node.js Installation Issues +For npm installs, add npm's global binary directory. On Linux/macOS this is +usually: -**Linux/macOS:** +```bash +export PATH="$(npm prefix -g)/bin:$PATH" +``` -- Ensure NVM is installed: `curl -o- https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install_nvm.sh | bash` -- Restart your terminal or run: `source ~/.bashrc` +On Windows standalone installs, add this directory to PATH: -**Windows:** - -- Install NVM for Windows from: https://github.com/coreybutler/nvm-windows/releases -- After installation, run the script again - -### Permission Issues - -You may need administrative privileges for global npm installation: - -- **Linux/macOS**: Use `sudo` with npm -- **Windows**: Run PowerShell as Administrator (required for Node.js installation and global npm packages) - -## Notes - -- The scripts require internet access to download Node.js and Qwen Code -- Administrative privileges may be required for global npm installation -- The installation source is stored locally and used for tracking purposes only -- If the source file is missing or invalid, the application continues to work normally +```bat +%LOCALAPPDATA%\qwen-code\bin +``` diff --git a/scripts/installation/install-qwen-with-source.bat b/scripts/installation/install-qwen-with-source.bat index fe5263e0e..def861137 100644 --- a/scripts/installation/install-qwen-with-source.bat +++ b/scripts/installation/install-qwen-with-source.bat @@ -1,304 +1,596 @@ @echo off -REM Script to install Node.js and Qwen Code with source information -REM This script handles the installation process and sets the installation source -REM -REM Usage: install-qwen-with-source.bat --source -REM install-qwen-with-source.bat -s -REM +REM Qwen Code Installation Script +REM Installs Qwen Code from a standalone archive when available, with npm fallback. +REM This script intentionally does not install Node.js or change npm config. setlocal enabledelayedexpansion set "SOURCE=unknown" +set "METHOD=%QWEN_INSTALL_METHOD%" +set "MIRROR=github" +if not "%QWEN_INSTALL_MIRROR%"=="" set "MIRROR=%QWEN_INSTALL_MIRROR%" +set "BASE_URL=%QWEN_INSTALL_BASE_URL%" +set "ARCHIVE_PATH=%QWEN_INSTALL_ARCHIVE%" +set "VERSION=latest" +if not "%QWEN_INSTALL_VERSION%"=="" set "VERSION=%QWEN_INSTALL_VERSION%" +set "NPM_REGISTRY=https://registry.npmmirror.com" +if not "%QWEN_NPM_REGISTRY%"=="" set "NPM_REGISTRY=%QWEN_NPM_REGISTRY%" +set "INSTALL_BASE=%LOCALAPPDATA%\qwen-code" +set "INSTALL_DIR=%INSTALL_BASE%\qwen-code" +set "INSTALL_BIN_DIR=%INSTALL_BASE%\bin" -REM Parse command line arguments :parse_args if "%~1"=="" goto end_parse if /i "%~1"=="--source" ( - if not "%~2"=="" ( - set "SOURCE=%~2" - shift - shift - goto parse_args + if "%~2"=="" ( + echo ERROR: --source requires a value + exit /b 1 ) + set "SOURCE=%~2" + shift + shift + goto parse_args ) if /i "%~1"=="-s" ( - if not "%~2"=="" ( - set "SOURCE=%~2" - shift - shift - goto parse_args + if "%~2"=="" ( + echo ERROR: -s requires a value + exit /b 1 ) + set "SOURCE=%~2" + shift + shift + goto parse_args ) -shift -goto parse_args +if /i "%~1"=="--method" ( + if "%~2"=="" ( + echo ERROR: --method requires a value + exit /b 1 + ) + set "METHOD=%~2" + shift + shift + goto parse_args +) +if /i "%~1"=="--mirror" ( + if "%~2"=="" ( + echo ERROR: --mirror requires a value + exit /b 1 + ) + set "MIRROR=%~2" + shift + shift + goto parse_args +) +if /i "%~1"=="--base-url" ( + if "%~2"=="" ( + echo ERROR: --base-url requires a value + exit /b 1 + ) + set "BASE_URL=%~2" + shift + shift + goto parse_args +) +if /i "%~1"=="--archive" ( + if "%~2"=="" ( + echo ERROR: --archive requires a value + exit /b 1 + ) + set "ARCHIVE_PATH=%~2" + shift + shift + goto parse_args +) +if /i "%~1"=="--version" ( + if "%~2"=="" ( + echo ERROR: --version requires a value + exit /b 1 + ) + set "VERSION=%~2" + shift + shift + goto parse_args +) +if /i "%~1"=="--registry" ( + if "%~2"=="" ( + echo ERROR: --registry requires a value + exit /b 1 + ) + set "NPM_REGISTRY=%~2" + shift + shift + goto parse_args +) +if /i "%~1"=="-h" goto usage +if /i "%~1"=="--help" goto usage + +echo ERROR: Unknown option: %~1 +echo. +goto usage_error :end_parse -echo =========================================== -echo Qwen Code Installation Script with Source Tracking -echo =========================================== -echo. -echo INFO: Installation source: %SOURCE% -echo. +call :ValidateOptions +if %ERRORLEVEL% NEQ 0 exit /b 1 -REM Check if Node.js is already installed -call :CheckCommandExists node -if !ERRORLEVEL! EQU 0 ( - for /f "delims=" %%i in ('node --version') do set "NODE_VERSION=%%i" - echo INFO: Node.js is already installed: !NODE_VERSION! - - REM Extract major version number - set "MAJOR_VERSION=!NODE_VERSION:v=!" - for /f "tokens=1 delims=." %%a in ("!MAJOR_VERSION!") do ( - set "MAJOR_VERSION=%%a" - ) - - if !MAJOR_VERSION! GEQ 20 ( - echo INFO: Node.js version !NODE_VERSION! is sufficient. Skipping Node.js installation. - goto :InstallQwenCode +echo =========================================== +echo Qwen Code Installation Script +echo =========================================== +echo. +echo INFO: Install method: !METHOD! +if /i not "!METHOD!"=="npm" ( + echo INFO: Standalone mirror: !MIRROR! + if not "!BASE_URL!"=="" echo INFO: Standalone base URL: !BASE_URL! + if not "!ARCHIVE_PATH!"=="" ( + echo INFO: Standalone archive: !ARCHIVE_PATH! ) else ( - echo INFO: Node.js version !NODE_VERSION! is too low. Need version 20 or higher. - echo INFO: Installing Node.js 20+ - call :InstallNodeJSDirectly - if !ERRORLEVEL! NEQ 0 ( - echo ERROR: Failed to install Node.js. Cannot continue with Qwen Code installation. - exit /b 1 - ) - ) -) else ( - echo INFO: Node.js not found. Installing Node.js 20+ - call :InstallNodeJSDirectly - if !ERRORLEVEL! NEQ 0 ( - echo ERROR: Failed to install Node.js. Cannot continue with Qwen Code installation. - exit /b 1 + echo INFO: Standalone version: !VERSION! ) ) +if /i not "!METHOD!"=="standalone" echo INFO: npm registry: !NPM_REGISTRY! +if not "!SOURCE!"=="unknown" echo INFO: Installation source: !SOURCE! +echo. -:InstallQwenCode - -REM Verify npm is available before installing Qwen Code -REM Always use full path to npm to avoid local node_modules conflicts -set "NODEJS_PATH=C:\Program Files\nodejs" -set "NODEJS_PATH_X86=C:\Program Files (x86)\nodejs" - -if exist "!NODEJS_PATH!\npm.cmd" ( - echo INFO: Using npm from !NODEJS_PATH! - set "NPM_CMD=!NODEJS_PATH!\npm.cmd" -) else if exist "!NODEJS_PATH_X86!\npm.cmd" ( - echo INFO: Using npm from !NODEJS_PATH_X86! - set "NPM_CMD=!NODEJS_PATH_X86!\npm.cmd" -) else ( - call :CheckCommandExists npm - if !ERRORLEVEL! NEQ 0 ( - echo ERROR: npm command not found. Node.js installation may have failed. - echo INFO: Please restart your command prompt and try again. - echo INFO: If the problem persists, manually install Node.js from: https://nodejs.org/ - exit /b 1 - ) - set "NPM_CMD=npm" +if /i "!METHOD!"=="standalone" ( + call :InstallStandalone + if !ERRORLEVEL! NEQ 0 exit /b !ERRORLEVEL! + call :PrintFinalInstructions "!INSTALL_BIN_DIR!" + endlocal + exit /b 0 ) -REM Install Qwen Code with source information -echo INFO: Installing Qwen Code with source: %SOURCE% -echo INFO: Running: %NPM_CMD% install -g @qwen-code/qwen-code@latest --registry https://registry.npmmirror.com -call "%NPM_CMD%" install -g @qwen-code/qwen-code@latest --registry https://registry.npmmirror.com - -if %ERRORLEVEL% EQU 0 ( - echo SUCCESS: Qwen Code installed successfully! -) else ( - echo ERROR: Failed to install Qwen Code. - exit /b 1 +if /i "!METHOD!"=="npm" ( + call :InstallNpm + if !ERRORLEVEL! NEQ 0 exit /b !ERRORLEVEL! + call :PrintFinalInstructions "" + endlocal + exit /b 0 ) -REM Create source.json only if --source or -s was explicitly provided -if not "!SOURCE!"=="unknown" ( - echo INFO: Creating source.json in %USERPROFILE%\.qwen... - - set "QWEN_DIR=%USERPROFILE%\.qwen" - if not exist "!QWEN_DIR!" ( - mkdir "!QWEN_DIR!" - ) - - REM Create the source.json file with the installation source - ( - echo { - echo "source": "!SOURCE!" - echo } - ) > "!QWEN_DIR!\source.json" - - echo SUCCESS: Installation source saved to %USERPROFILE%\.qwen\source.json +call :InstallStandalone +set "STANDALONE_STATUS=!ERRORLEVEL!" +if !STANDALONE_STATUS! EQU 0 ( + call :PrintFinalInstructions "!INSTALL_BIN_DIR!" + endlocal + exit /b 0 ) -REM Verify installation -call :CheckCommandExists qwen -if %ERRORLEVEL% EQU 0 ( - echo SUCCESS: Qwen Code is available as 'qwen' command. - call qwen --version - echo. - echo INFO: Starting Qwen Code... - echo. - call qwen -) else ( - echo WARNING: Qwen Code may not be in PATH. Please check your npm global bin directory. - echo. - echo =========================================== - echo SUCCESS: Installation completed! - echo The source information is stored in %USERPROFILE%\.qwen\source.json - echo. - echo =========================================== +if !STANDALONE_STATUS! EQU 2 ( + echo WARNING: Falling back to npm installation. + call :InstallNpm + if !ERRORLEVEL! NEQ 0 exit /b !ERRORLEVEL! + call :PrintFinalInstructions "" + endlocal + exit /b 0 ) -endlocal +exit /b !STANDALONE_STATUS! + +:usage +echo Qwen Code Installer +echo. +echo Usage: install-qwen-with-source.bat [OPTIONS] +echo. +echo Options: +echo -s, --source SOURCE Record the installation source. +echo Only letters, numbers, dot, underscore, and dash are allowed. +echo --method METHOD Install method: detect, standalone, or npm. +echo --mirror MIRROR Standalone archive mirror: github or aliyun. +echo --base-url URL Override standalone archive base URL. +echo --archive PATH Install from a local standalone archive. +echo --version VERSION Standalone release version. Defaults to latest. +echo --registry REGISTRY npm registry to use. +echo Defaults to QWEN_NPM_REGISTRY or https://registry.npmmirror.com +echo -h, --help Show this help message. exit /b 0 -REM ============================================================ -REM Function: CheckCommandExists -REM Description: Check if a command exists in the system -REM ============================================================ -:CheckCommandExists -where %~1 >nul 2>&1 +:usage_error +echo Qwen Code Installer +echo. +echo Usage: install-qwen-with-source.bat [OPTIONS] +echo. +echo Options: +echo -s, --source SOURCE Record the installation source. +echo --method METHOD Install method: detect, standalone, or npm. +echo --mirror MIRROR Standalone archive mirror: github or aliyun. +echo --base-url URL Override standalone archive base URL. +echo --archive PATH Install from a local standalone archive. +echo --version VERSION Standalone release version. Defaults to latest. +echo --registry REGISTRY npm registry to use. +echo -h, --help Show this help message. +exit /b 1 + +:ValidateOptions +if "!METHOD!"=="" set "METHOD=detect" + +if /i "!METHOD!"=="detect" goto validate_method_ok +if /i "!METHOD!"=="standalone" goto validate_method_ok +if /i "!METHOD!"=="npm" goto validate_method_ok +echo ERROR: --method must be detect, standalone, or npm. +exit /b 1 + +:validate_method_ok +if /i "!MIRROR!"=="github" goto validate_mirror_ok +if /i "!MIRROR!"=="aliyun" goto validate_mirror_ok +echo ERROR: --mirror must be github or aliyun. +exit /b 1 + +:validate_mirror_ok +call :ValidateSource exit /b %ERRORLEVEL% -REM ============================================================ -REM Function: InstallNodeJSDirectly -REM Description: Download and install Node.js directly from official website -REM ============================================================ -:InstallNodeJSDirectly -echo INFO: Downloading Node.js LTS (20.x) from official website +:ValidateSource +if "!SOURCE!"=="unknown" exit /b 0 +echo(!SOURCE!| findstr /R /C:"^[A-Za-z0-9._-][A-Za-z0-9._-]*$" >nul +if %ERRORLEVEL% EQU 0 exit /b 0 -REM Create temp directory for download -set "TEMP_DIR=%TEMP%\qwen-nodejs-install" -if not exist "%TEMP_DIR%" mkdir "%TEMP_DIR%" +echo ERROR: --source may only contain letters, numbers, dot, underscore, or dash. +exit /b 1 -REM Determine architecture -set "ARCH=x64" -if "%PROCESSOR_ARCHITECTURE%"=="x86" set "ARCH=x86" -if "%PROCESSOR_ARCHITECTURE%"=="AMD64" set "ARCH=x64" -if defined PROCESSOR_ARCHITEW6432 set "ARCH=x64" - -REM Set Node.js download URL (LTS version 20.x) -set "NODE_VERSION=20.18.1" -set "NODE_URL=https://nodejs.org/dist/v!NODE_VERSION!/node-v!NODE_VERSION!-!ARCH!.msi" -set "NODE_INSTALLER=%TEMP_DIR%\nodejs-installer.msi" - -echo INFO: Downloading from: !NODE_URL! -echo INFO: Architecture: !ARCH! - -REM Download Node.js installer using PowerShell -powershell -Command "try { Invoke-WebRequest -Uri '!NODE_URL!' -OutFile '!NODE_INSTALLER!' -UseBasicParsing; Write-Host 'Download completed successfully.' } catch { Write-Host 'Download failed:' $_.Exception.Message; exit 1 }" - -if !ERRORLEVEL! NEQ 0 ( - echo ERROR: Failed to download Node.js installer from official source. - echo INFO: Please manually download and install Node.js from: https://nodejs.org/ - echo INFO: After manual installation, restart your command prompt and run this script again. +:DetectTarget +set "TARGET=" +if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" set "TARGET=win-x64" +if /i "%PROCESSOR_ARCHITEW6432%"=="AMD64" set "TARGET=win-x64" +if "!TARGET!"=="" ( + echo WARNING: Standalone archive is not available for this Windows architecture. exit /b 1 ) +exit /b 0 -if not exist "!NODE_INSTALLER!" ( - echo ERROR: Node.js installer not found after download. - exit /b 1 +:ReleaseVersionPath +if /i "!VERSION!"=="latest" ( + set "VERSION_PATH=latest" + exit /b 0 ) +set "VERSION_PATH=!VERSION!" +if /i "!VERSION_PATH:~0,1!"=="v" exit /b 0 +set "VERSION_PATH=v!VERSION_PATH!" +exit /b 0 -echo INFO: Installing Node.js silently -REM Install Node.js silently -msiexec /i "!NODE_INSTALLER!" /quiet /norestart ADDLOCAL=ALL - -if !ERRORLEVEL! NEQ 0 ( - echo ERROR: Failed to install Node.js. - echo INFO: You may need to run this script as Administrator. - echo INFO: Or manually install Node.js from: https://nodejs.org/ - exit /b 1 -) - -echo INFO: Node.js installation completed. - -REM Clean up installer -del "!NODE_INSTALLER!" 2>nul -rmdir "!TEMP_DIR!" 2>nul - -REM Refresh environment variables -echo INFO: Refreshing environment variables -call :RefreshEnvVars - -REM Verify installation and return success -set "NODEJS_INSTALL_PATH=C:\Program Files\nodejs" -if exist "!NODEJS_INSTALL_PATH!\node.exe" ( - for /f "delims=" %%i in ('"!NODEJS_INSTALL_PATH!\node.exe" --version') do set "NODE_VERSION=%%i" - echo SUCCESS: Node.js !NODE_VERSION! installed successfully! +:StandaloneBaseUrl +if not "!BASE_URL!"=="" ( + set "STANDALONE_BASE_URL=!BASE_URL!" exit /b 0 ) -set "NODEJS_INSTALL_PATH_X86=C:\Program Files (x86)\nodejs" -if exist "!NODEJS_INSTALL_PATH_X86!\node.exe" ( - for /f "delims=" %%i in ('"!NODEJS_INSTALL_PATH_X86!\node.exe" --version') do set "NODE_VERSION=%%i" - echo SUCCESS: Node.js !NODE_VERSION! installed successfully! +call :ReleaseVersionPath +if /i "!MIRROR!"=="aliyun" ( + set "STANDALONE_BASE_URL=https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/releases/qwen-code/!VERSION_PATH!" exit /b 0 ) -call :CheckCommandExists node -if !ERRORLEVEL! EQU 0 ( - for /f "delims=" %%i in ('node --version') do set "NODE_VERSION=%%i" - echo SUCCESS: Node.js !NODE_VERSION! installed successfully! +if /i "!VERSION_PATH!"=="latest" ( + set "STANDALONE_BASE_URL=https://github.com/QwenLM/qwen-code/releases/latest/download" exit /b 0 +) + +set "STANDALONE_BASE_URL=https://github.com/QwenLM/qwen-code/releases/download/!VERSION_PATH!" +exit /b 0 + +:UrlExists +set "CHECK_URL=%~1" +powershell -NoProfile -ExecutionPolicy Bypass -Command "$request = [Net.WebRequest]::Create('%CHECK_URL%'); $request.Method = 'HEAD'; try { $response = $request.GetResponse(); $response.Close(); exit 0 } catch { exit 1 }" >nul 2>&1 +exit /b %ERRORLEVEL% + +:DownloadFile +set "DOWNLOAD_URL=%~1" +set "DOWNLOAD_DEST=%~2" +powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%DOWNLOAD_DEST%')" +exit /b %ERRORLEVEL% + +:VerifyChecksum +set "ARCHIVE_FILE=%~1" +set "CHECKSUM_SOURCE=%~2" +set "ARCHIVE_NAME=%~3" +set "CHECKSUM_FILE=!CHECKSUM_SOURCE!" +set "TEMP_CHECKSUM=" +set "REQUIRE_CHECKSUM=0" + +if "!CHECKSUM_FILE!"=="" ( + for %%I in ("!ARCHIVE_FILE!") do set "CHECKSUM_FILE=%%~dpISHA256SUMS" ) else ( - echo WARNING: Node.js installed but not found in PATH. - echo INFO: Trying to use Node.js from default installation path - - REM Try to use Node.js directly from installation path - set "NODE_PATH=C:\Program Files\nodejs" - if exist "%NODE_PATH%\node.exe" ( - echo INFO: Found Node.js at %NODE_PATH% - REM Update PATH for current session - set "PATH=%PATH%;%NODE_PATH%" - - REM Test if node works now - "%NODE_PATH%\node.exe" --version >nul 2>&1 - if !ERRORLEVEL! EQU 0 ( - for /f "delims=" %%i in ('"%NODE_PATH%\node.exe" --version') do set "NODE_VERSION=%%i" - echo SUCCESS: Node.js %NODE_VERSION% is working from %NODE_PATH% - exit /b 0 + echo !CHECKSUM_FILE!| findstr /R /C:"^https*://" >nul + if !ERRORLEVEL! EQU 0 ( + set "REQUIRE_CHECKSUM=1" + set "TEMP_CHECKSUM=%TEMP%\qwen-code-checksums-%RANDOM%%RANDOM%.txt" + call :DownloadFile "!CHECKSUM_FILE!" "!TEMP_CHECKSUM!" + if !ERRORLEVEL! NEQ 0 ( + if exist "!TEMP_CHECKSUM!" del /F /Q "!TEMP_CHECKSUM!" >nul 2>&1 + echo ERROR: Could not download SHA256SUMS for checksum verification. + exit /b 1 ) + set "CHECKSUM_FILE=!TEMP_CHECKSUM!" ) - - REM Try x86 path - set "NODE_PATH_X86=C:\Program Files (x86)\nodejs" - if exist "%NODE_PATH_X86%\node.exe" ( - echo INFO: Found Node.js at %NODE_PATH_X86% - REM Update PATH for current session - set "PATH=%PATH%;%NODE_PATH_X86%" - - REM Test if node works now - "%NODE_PATH_X86%\node.exe" --version >nul 2>&1 - if !ERRORLEVEL! EQU 0 ( - for /f "delims=" %%i in ('"%NODE_PATH_X86%\node.exe" --version') do set "NODE_VERSION=%%i" - echo SUCCESS: Node.js %NODE_VERSION% is working from %NODE_PATH_X86% - exit /b 0 - ) +) + +if not exist "!CHECKSUM_FILE!" ( + if "!REQUIRE_CHECKSUM!"=="1" ( + echo ERROR: SHA256SUMS not found; cannot verify remote archive. + exit /b 1 ) - - echo ERROR: Node.js installation completed but cannot be executed + echo WARNING: SHA256SUMS not found; skipping checksum verification. + exit /b 0 +) + +set "EXPECTED_HASH=" +for /f "tokens=1" %%H in ('findstr /C:"!ARCHIVE_NAME!" "!CHECKSUM_FILE!"') do ( + if "!EXPECTED_HASH!"=="" set "EXPECTED_HASH=%%H" +) + +if "!EXPECTED_HASH!"=="" ( + if not "!TEMP_CHECKSUM!"=="" del /F /Q "!TEMP_CHECKSUM!" >nul 2>&1 + if "!REQUIRE_CHECKSUM!"=="1" ( + echo ERROR: Checksum entry for !ARCHIVE_NAME! not found. + exit /b 1 + ) + echo WARNING: Checksum entry for !ARCHIVE_NAME! not found; skipping checksum verification. + exit /b 0 +) + +set "ACTUAL_HASH=" +for /f "tokens=1" %%H in ('certutil -hashfile "!ARCHIVE_FILE!" SHA256 ^| findstr /R /C:"^[0-9A-Fa-f][0-9A-Fa-f]"') do ( + if "!ACTUAL_HASH!"=="" set "ACTUAL_HASH=%%H" +) + +if not "!TEMP_CHECKSUM!"=="" del /F /Q "!TEMP_CHECKSUM!" >nul 2>&1 + +if "!ACTUAL_HASH!"=="" ( + if "!REQUIRE_CHECKSUM!"=="1" ( + echo ERROR: Could not calculate SHA-256 checksum for remote archive. + exit /b 1 + ) + echo WARNING: Could not calculate SHA-256 checksum; skipping checksum verification. + exit /b 0 +) + +if /i not "!EXPECTED_HASH!"=="!ACTUAL_HASH!" ( + echo ERROR: Checksum verification failed for !ARCHIVE_NAME!. exit /b 1 ) +echo SUCCESS: Checksum verified for !ARCHIVE_NAME!. exit /b 0 -REM ============================================================ -REM Function: RefreshEnvVars -REM Description: Refresh environment variables without restarting -REM ============================================================ -:RefreshEnvVars -REM Add Node.js to PATH if not already there -set "NODEJS_DIR=C:\Program Files\nodejs" -if exist "!NODEJS_DIR!\node.exe" ( - echo INFO: Found Node.js at !NODEJS_DIR! - set "PATH=!PATH!;!NODEJS_DIR!" +:InstallStandalone +set "TEMP_DIR=" +set "CHECKSUM_SOURCE=" + +if not "!ARCHIVE_PATH!"=="" ( + set "ARCHIVE_FILE=!ARCHIVE_PATH!" + for %%I in ("!ARCHIVE_FILE!") do set "ARCHIVE_NAME=%%~nxI" + if not exist "!ARCHIVE_FILE!" ( + echo ERROR: Standalone archive not found: !ARCHIVE_FILE! + exit /b 1 + ) +) else ( + call :DetectTarget + if !ERRORLEVEL! NEQ 0 exit /b 2 + + set "ARCHIVE_NAME=qwen-code-win-x64.zip" + call :StandaloneBaseUrl + set "ARCHIVE_URL=!STANDALONE_BASE_URL!/!ARCHIVE_NAME!" + set "CHECKSUM_SOURCE=!STANDALONE_BASE_URL!/SHA256SUMS" + + if /i "!METHOD!"=="detect" ( + call :UrlExists "!ARCHIVE_URL!" + if !ERRORLEVEL! NEQ 0 ( + echo WARNING: Standalone archive not found: !ARCHIVE_NAME! + exit /b 2 + ) + ) + + set "TEMP_DIR=%TEMP%\qwen-code-install-%RANDOM%%RANDOM%" + mkdir "!TEMP_DIR!" >nul 2>&1 + set "ARCHIVE_FILE=!TEMP_DIR!\!ARCHIVE_NAME!" + + echo INFO: Downloading !ARCHIVE_URL! + call :DownloadFile "!ARCHIVE_URL!" "!ARCHIVE_FILE!" + if !ERRORLEVEL! NEQ 0 ( + if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1 + echo WARNING: Failed to download standalone archive. + exit /b 2 + ) ) -REM Try alternative path for x86 systems -set "NODEJS_DIR_X86=C:\Program Files (x86)\nodejs" -if exist "!NODEJS_DIR_X86!\node.exe" ( - echo INFO: Found Node.js at !NODEJS_DIR_X86! - set "PATH=!PATH!;!NODEJS_DIR_X86!" +if "!TEMP_DIR!"=="" ( + set "TEMP_DIR=%TEMP%\qwen-code-install-%RANDOM%%RANDOM%" + mkdir "!TEMP_DIR!" >nul 2>&1 ) +call :VerifyChecksum "!ARCHIVE_FILE!" "!CHECKSUM_SOURCE!" "!ARCHIVE_NAME!" +if !ERRORLEVEL! NEQ 0 ( + if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1 + exit /b 1 +) + +set "EXTRACT_DIR=!TEMP_DIR!\extract" +mkdir "!EXTRACT_DIR!" >nul 2>&1 +powershell -NoProfile -ExecutionPolicy Bypass -Command "Expand-Archive -LiteralPath '%ARCHIVE_FILE%' -DestinationPath '%EXTRACT_DIR%' -Force" +if !ERRORLEVEL! NEQ 0 ( + if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1 + echo ERROR: Failed to extract standalone archive. + exit /b 1 +) + +if not exist "!EXTRACT_DIR!\qwen-code\bin\qwen.cmd" ( + if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1 + echo ERROR: Archive does not contain qwen-code\bin\qwen.cmd. + exit /b 1 +) + +if not exist "!EXTRACT_DIR!\qwen-code\node\node.exe" ( + if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1 + echo ERROR: Archive does not contain qwen-code\node\node.exe. + exit /b 1 +) + +if not exist "!INSTALL_BASE!" mkdir "!INSTALL_BASE!" +if not exist "!INSTALL_BIN_DIR!" mkdir "!INSTALL_BIN_DIR!" + +set "NEW_INSTALL_DIR=!INSTALL_DIR!.new" +set "OLD_INSTALL_DIR=!INSTALL_DIR!.old" +if exist "!NEW_INSTALL_DIR!" rmdir /S /Q "!NEW_INSTALL_DIR!" >nul 2>&1 +if exist "!OLD_INSTALL_DIR!" rmdir /S /Q "!OLD_INSTALL_DIR!" >nul 2>&1 +move /Y "!EXTRACT_DIR!\qwen-code" "!NEW_INSTALL_DIR!" >nul +if !ERRORLEVEL! NEQ 0 ( + if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1 + echo ERROR: Failed to stage standalone archive. + exit /b 1 +) + +if exist "!INSTALL_DIR!" move /Y "!INSTALL_DIR!" "!OLD_INSTALL_DIR!" >nul +move /Y "!NEW_INSTALL_DIR!" "!INSTALL_DIR!" >nul +if !ERRORLEVEL! NEQ 0 ( + if exist "!OLD_INSTALL_DIR!" move /Y "!OLD_INSTALL_DIR!" "!INSTALL_DIR!" >nul + if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1 + echo ERROR: Failed to install standalone archive to !INSTALL_DIR!. + exit /b 1 +) + +if exist "!OLD_INSTALL_DIR!" rmdir /S /Q "!OLD_INSTALL_DIR!" >nul 2>&1 + +( +echo @echo off +echo call "!INSTALL_DIR!\bin\qwen.cmd" %%* +) > "!INSTALL_BIN_DIR!\qwen.cmd" + +set "PATH=!INSTALL_BIN_DIR!;!PATH!" +call :CreateSourceJson +if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1 + +echo SUCCESS: Qwen Code standalone archive installed successfully. +echo INFO: Installed to !INSTALL_DIR! +exit /b 0 + +:RequireNode +where node >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo ERROR: Node.js was not found. + echo. + echo Node.js 20 or newer is required before installing Qwen Code with npm. + echo Please install Node.js from https://nodejs.org/ and rerun this installer. + exit /b 1 +) + +for /f "delims=" %%i in ('node -p "process.versions.node" 2^>nul') do set "NODE_VERSION=%%i" +if "%NODE_VERSION%"=="" ( + echo ERROR: Unable to determine Node.js version. + echo Node.js 20 or newer is required before installing Qwen Code with npm. + exit /b 1 +) + +for /f "tokens=1 delims=." %%a in ("%NODE_VERSION%") do set "MAJOR_VERSION=%%a" +set /a NODE_MAJOR_NUM=%MAJOR_VERSION% >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo ERROR: Unable to determine Node.js version. + echo Node.js 20 or newer is required before installing Qwen Code with npm. + exit /b 1 +) + +if %NODE_MAJOR_NUM% LSS 20 ( + echo ERROR: Node.js %NODE_VERSION% is installed, but Node.js 20 or newer is required. + echo Please install Node.js from https://nodejs.org/ and rerun this installer. + exit /b 1 +) + +echo SUCCESS: Node.js %NODE_VERSION% detected. +exit /b 0 + +:RequireNpm +where npm >nul 2>&1 +if %ERRORLEVEL% NEQ 0 ( + echo ERROR: npm was not found. + echo Please install Node.js with npm included, then rerun this installer. + exit /b 1 +) + +for /f "delims=" %%i in ('npm -v 2^>nul') do set "NPM_VERSION=%%i" +echo SUCCESS: npm %NPM_VERSION% detected. +exit /b 0 + +:InstallNpm +call :RequireNode +if %ERRORLEVEL% NEQ 0 exit /b 1 + +call :RequireNpm +if %ERRORLEVEL% NEQ 0 exit /b 1 + +where qwen >nul 2>&1 +if %ERRORLEVEL% EQU 0 ( + for /f "delims=" %%i in ('qwen --version 2^>nul') do set "QWEN_VERSION=%%i" + echo INFO: Existing Qwen Code detected: !QWEN_VERSION! + echo INFO: Upgrading to the latest version. +) + +echo INFO: Running: npm install -g @qwen-code/qwen-code@latest --registry !NPM_REGISTRY! +call npm install -g @qwen-code/qwen-code@latest --registry "!NPM_REGISTRY!" +if %ERRORLEVEL% NEQ 0 ( + echo ERROR: Failed to install Qwen Code. + echo. + echo This installer does not change your npm prefix or PATH. + echo If the failure is a permission error, fix your npm global package directory, then run: + echo npm install -g @qwen-code/qwen-code@latest --registry !NPM_REGISTRY! + exit /b 1 +) + +echo SUCCESS: Qwen Code installed successfully. +call :CreateSourceJson +exit /b 0 + +:CreateSourceJson +if "!SOURCE!"=="unknown" exit /b 0 + +set "QWEN_DIR=%USERPROFILE%\.qwen" +if not exist "%QWEN_DIR%" mkdir "%QWEN_DIR%" + +( +echo { +echo "source": "!SOURCE!" +echo } +) > "%QWEN_DIR%\source.json" + +echo SUCCESS: Installation source saved to %USERPROFILE%\.qwen\source.json +exit /b 0 + +:PrintFinalInstructions +set "EXTRA_BIN=%~1" +if not "!EXTRA_BIN!"=="" set "PATH=!EXTRA_BIN!;!PATH!" + +echo. +echo =========================================== +echo Installation completed! +echo =========================================== +echo. + +where qwen >nul 2>&1 +if %ERRORLEVEL% EQU 0 ( + for /f "delims=" %%i in ('qwen --version 2^>nul') do set "QWEN_VERSION=%%i" + echo SUCCESS: Qwen Code is ready to use: !QWEN_VERSION! + echo. + echo You can now run: qwen + echo. + echo INFO: Run qwen in your project directory to start an interactive session. + exit /b 0 +) + +echo WARNING: Qwen Code was installed, but qwen is not on PATH in this prompt. +echo. +echo Restart your command prompt, then run: qwen +if not "!EXTRA_BIN!"=="" ( + echo. + echo Or add this directory to PATH: + echo !EXTRA_BIN! + echo Then run: + echo qwen + exit /b 0 +) + +for /f "delims=" %%i in ('npm prefix -g 2^>nul') do set "NPM_PREFIX=%%i" +if not "!NPM_PREFIX!"=="" ( + echo. + echo Or add this npm global directory to PATH: + echo !NPM_PREFIX! + echo Then run: + echo qwen +) exit /b 0 diff --git a/scripts/installation/install-qwen-with-source.sh b/scripts/installation/install-qwen-with-source.sh index ce6d46c26..6e60c6b5d 100755 --- a/scripts/installation/install-qwen-with-source.sh +++ b/scripts/installation/install-qwen-with-source.sh @@ -1,574 +1,762 @@ -#!/bin/bash +#!/usr/bin/env bash # Qwen Code Installation Script -# This script installs Node.js (via NVM) and Qwen Code CLI -# Supports Linux and macOS +# Installs Qwen Code from a standalone archive when available, with npm fallback. +# This script intentionally does not install Node.js or change npm config. # -# Usage: install-qwen-with-source.sh --source [github|npm|internal|local-build] -# install-qwen-with-source.sh -s [github|npm|internal|local-build] +# Usage: +# install-qwen-with-source.sh --source [github|npm|internal|local-build] +# install-qwen-with-source.sh --method [detect|standalone|npm] -# Re-execute with bash if running with sh or other shells -# This block must use POSIX-compliant syntax ([ not [[) since it runs before we know bash is available if [ -z "${BASH_VERSION}" ] && [ -z "${__QWEN_INSTALL_REEXEC:-}" ]; then - # Check if we're in a git hook environment - case "${0}" in - *.git/hooks/*) export __QWEN_IN_GIT_HOOK=1 ;; - esac - if [ -n "${GIT_DIR:-}" ]; then - export __QWEN_IN_GIT_HOOK=1 - fi - - # Try to find bash if command -v bash >/dev/null 2>&1; then - export __QWEN_INSTALL_REEXEC=1 - # Re-exec with bash, preserving all arguments - exec bash -- "${0}" "$@" - else - echo "Error: This script requires bash. Please install bash first." + if [ -f "${0}" ]; then + export __QWEN_INSTALL_REEXEC=1 + exec bash -- "${0}" "$@" + fi + + echo "Error: This script requires bash. Run the installer with: curl ... | bash" exit 1 fi + + echo "Error: This script requires bash. Please install bash first." + exit 1 fi -# Enable strict mode (bash-specific options) -# pipefail requires bash 3+; check before setting -if [ -n "${BASH_VERSION:-}" ]; then - # shellcheck disable=SC3040 - set -eo pipefail -else - set -e -fi +set -eo pipefail -# ============================================ -# Color definitions -# ============================================ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' -NC='\033[0m' # No Color +NC='\033[0m' -# ============================================ -# Log functions -# ============================================ log_info() { - echo -e "${BLUE}ℹ️ $1${NC}" + echo -e "${BLUE}INFO:${NC} $1" } log_success() { - echo -e "${GREEN}✅ $1${NC}" + echo -e "${GREEN}SUCCESS:${NC} $1" } log_warning() { - echo -e "${YELLOW}⚠️ $1${NC}" + echo -e "${YELLOW}WARNING:${NC} $1" } log_error() { - echo -e "${RED}❌ $1${NC}" + echo -e "${RED}ERROR:${NC} $1" } -# ============================================ -# Utility functions -# ============================================ command_exists() { command -v "$1" >/dev/null 2>&1 } -get_shell_profile() { - local current_shell - current_shell=$(basename "${SHELL}") - case "${current_shell}" in - bash) - echo "${HOME}/.bashrc" - ;; - zsh) - echo "${HOME}/.zshrc" - ;; - fish) - # Fish uses its own syntax; bash/zsh export statements are not compatible. - # Return empty string to signal callers to skip automatic profile writes. - echo "" - ;; - *) - echo "${HOME}/.profile" - ;; - esac +print_usage() { + cat </dev/null; then - log_info "Cleaning npmrc conflicts..." - # Backup original npmrc before modifying - cp -f "${npmrc}" "${npmrc}.bak" - log_info "Backed up original .npmrc to ${npmrc}.bak" - grep -Ev '^(prefix|globalconfig) *= *' "${npmrc}.bak" > "${npmrc}.tmp" || true - mv -f "${npmrc}.tmp" "${npmrc}" || true - log_success "Removed conflicting prefix/globalconfig entries from .npmrc" +print_header() { + echo "==========================================" + echo " Qwen Code Installation Script" + echo "==========================================" + echo "" + log_info "System: $(uname -s 2>/dev/null || echo unknown) $(uname -r 2>/dev/null || true)" + log_info "Install method: ${METHOD}" + if [[ "${METHOD}" != "npm" ]]; then + log_info "Standalone mirror: ${MIRROR}" + if [[ -n "${BASE_URL}" ]]; then + log_info "Standalone base URL: ${BASE_URL}" + fi + if [[ -n "${ARCHIVE_PATH}" ]]; then + log_info "Standalone archive: ${ARCHIVE_PATH}" + else + log_info "Standalone version: ${VERSION}" fi fi + if [[ "${METHOD}" != "standalone" ]]; then + log_info "npm registry: ${NPM_REGISTRY}" + fi + if [[ "${SOURCE}" != "unknown" ]]; then + log_info "Installation source: ${SOURCE}" + fi + echo "" } -# ============================================ -# Install NVM -# ============================================ -install_nvm() { - local NVM_DIR="${NVM_DIR:-${HOME}/.nvm}" - local NVM_VERSION="${NVM_VERSION:-v0.40.3}" +print_node_help() { + echo "" + echo "Node.js 20 or newer is required before installing Qwen Code with npm." + echo "" + echo "Install Node.js, then rerun this installer:" + case "$(uname -s 2>/dev/null || echo unknown)" in + Darwin) + echo " brew install node" + echo " # or download from https://nodejs.org/" + ;; + Linux) + echo " # Use your distribution package manager or:" + echo " https://nodejs.org/en/download/package-manager" + ;; + *) + echo " https://nodejs.org/" + ;; + esac + echo "" + echo "If you already use a Node version manager, activate Node.js 20+" + echo "in this shell before rerunning the installer." +} - if [[ -s "${NVM_DIR}/nvm.sh" ]]; then - log_info "NVM is already installed at ${NVM_DIR}" - return 0 - fi - - log_info "Installing NVM ${NVM_VERSION}..." - - # Download and install NVM from Aliyun OSS - # Use temporary file instead of pipe to avoid potential subshell issues - local NVM_INSTALL_TEMP - NVM_INSTALL_TEMP=$(mktemp) - if "${DOWNLOAD_CMD}" "${DOWNLOAD_ARGS}" "https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install_nvm.sh" > "${NVM_INSTALL_TEMP}"; then - # Run the script in current shell environment - # shellcheck source=/dev/null - . "${NVM_INSTALL_TEMP}" - rm -f "${NVM_INSTALL_TEMP}" - log_success "NVM installed successfully" - else - rm -f "${NVM_INSTALL_TEMP}" - log_error "Failed to install NVM" - log_info "Please install NVM manually: https://github.com/nvm-sh/nvm#install--update-script" +require_node() { + if ! command_exists node; then + log_error "Node.js was not found." + print_node_help exit 1 fi - # Configure shell profile - local PROFILE_FILE - PROFILE_FILE=$(get_shell_profile) + local node_version + node_version=$(node -p "process.versions.node" 2>/dev/null || true) + local node_major + node_major=$(node -p "Number(process.versions.node.split('.')[0])" 2>/dev/null || true) - # Fish shell returns empty string from get_shell_profile because export/source - # syntax is incompatible with fish. Skip automatic profile writes for fish users. - if [[ -z "${PROFILE_FILE}" ]]; then - log_warning "Fish shell detected: automatic shell profile configuration is not supported." - log_info "Please add NVM configuration manually. See: https://github.com/nvm-sh/nvm#fish" - # Check if profile file is writable - elif [[ -f "${PROFILE_FILE}" ]] && [[ ! -w "${PROFILE_FILE}" ]]; then - log_warning "Cannot write to ${PROFILE_FILE} (permission denied)" - log_info "Skipping shell profile configuration" - log_info "You may need to manually add NVM configuration to your shell profile" - elif ! grep -q 'NVM_DIR' "${PROFILE_FILE}" 2>/dev/null; then - # shellcheck disable=SC2016 - # The following echo statements intentionally use single quotes to write literal strings - { - echo "" - echo "# NVM configuration (added by Qwen Code installer)" - echo "export NVM_DIR=\"\$HOME/.nvm\"" - echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' - echo '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"' - } >> "${PROFILE_FILE}" 2>/dev/null || { - log_warning "Failed to write to ${PROFILE_FILE}" - log_info "Skipping shell profile configuration" - return 0 - } - log_info "Added NVM config to ${PROFILE_FILE}" + if [[ -z "${node_major}" ]] || ! [[ "${node_major}" =~ ^[0-9]+$ ]]; then + log_error "Unable to determine Node.js version." + print_node_help + exit 1 fi - # Load NVM for current session - export NVM_DIR="${NVM_DIR}" - # shellcheck source=/dev/null - [[ -s "${NVM_DIR}/nvm.sh" ]] && \. "${NVM_DIR}/nvm.sh" + if [[ "${node_major}" -lt 20 ]]; then + log_error "Node.js ${node_version:-unknown} is installed, but Node.js 20 or newer is required." + print_node_help + exit 1 + fi - log_success "NVM configured successfully" - return 0 + log_success "Node.js ${node_version} detected." } -# ============================================ -# Install Node.js via NVM -# ============================================ -install_nodejs_with_nvm() { - local NODE_VERSION="${NODE_VERSION:-20}" - local NVM_DIR="${NVM_DIR:-${HOME}/.nvm}" - - # Ensure NVM is loaded - export NVM_DIR="${NVM_DIR}" - # shellcheck source=/dev/null - [[ -s "${NVM_DIR}/nvm.sh" ]] && \. "${NVM_DIR}/nvm.sh" - - if ! command_exists nvm; then - log_error "NVM not loaded properly" - return 1 - fi - - # Set Node.js mirror source for faster downloads in China - export NVM_NODEJS_ORG_MIRROR="https://npmmirror.com/mirrors/node" - - # Install Node.js - log_info "Installing Node.js v${NODE_VERSION}..." - if nvm install "${NODE_VERSION}"; then - nvm alias default "${NODE_VERSION}" || true - nvm use default || true - log_success "Node.js v${NODE_VERSION} installed successfully" - - # Verify installation - log_info "Node.js version: $(node -v)" || true - log_info "npm version: $(npm -v)" || true - +require_npm() { + if command_exists npm; then + log_success "npm $(npm -v 2>/dev/null || echo unknown) detected." return 0 - else - log_error "Failed to install Node.js" - return 1 fi + + log_error "npm was not found." + echo "" + echo "Please install Node.js with npm included, then rerun this installer." + echo "Download Node.js from https://nodejs.org/ if your package manager" + echo "installed Node without npm." + exit 1 } -# ============================================ -# Check Node.js version -# ============================================ -check_node_version() { - if ! command_exists node; then - return 1 - fi +get_npm_global_bin() { + local prefix + prefix=$(npm prefix -g 2>/dev/null || true) - local current_version - current_version=$(node -v | sed 's/v//') - local major_version - major_version=$(echo "${current_version}" | cut -d. -f1 | sed 's/[^0-9]//g') - - # Handle cases where major_version is empty or non-numeric - if [[ -z "${major_version}" ]]; then - log_warning "Unable to determine Node.js version from: $(node -v)" - return 1 - fi - - if [[ "${major_version}" -ge 20 ]]; then - log_success "Node.js v${current_version} is already installed (>= 20)" + if [[ -z "${prefix}" ]]; then return 0 - else - log_warning "Node.js v${current_version} is installed but version < 20" - return 1 fi -} -# ============================================ -# Install Node.js -# ============================================ -install_nodejs() { - local platform - platform=$(uname -s) - - case "${platform}" in - Linux|Darwin) - log_info "Installing Node.js on ${platform}..." - - # Install NVM - if ! install_nvm; then - log_error "Failed to install NVM" - return 1 - fi - - # Load NVM - export NVM_DIR="${HOME}/.nvm" - # shellcheck source=/dev/null - [[ -s "${NVM_DIR}/nvm.sh" ]] && \. "${NVM_DIR}/nvm.sh" - - # Install Node.js - if ! install_nodejs_with_nvm; then - log_error "Failed to install Node.js" - return 1 - fi - ;; - MINGW*|CYGWIN*|MSYS*) - log_error "Windows platform detected. Please use Windows installer or WSL." - log_info "Visit: https://nodejs.org/en/download/" - exit 1 + case "$(uname -s 2>/dev/null || echo unknown)" in + MINGW*|MSYS*|CYGWIN*) + echo "${prefix}" ;; *) - log_error "Unsupported platform: ${platform}" - exit 1 + echo "${prefix}/bin" ;; esac } -# ============================================ -# Check and install Node.js -# ============================================ -check_and_install_nodejs() { - if check_node_version; then - log_info "Using existing Node.js installation" - clean_npmrc_conflict - else - log_warning "Installing or upgrading Node.js..." - install_nodejs - fi -} - -# ============================================ -# Fix npm permissions (without using sudo) -# ============================================ -fix_npm_permissions() { - log_info "Checking npm permissions..." - - local NPM_GLOBAL_DIR - NPM_GLOBAL_DIR=$(npm config get prefix 2>/dev/null) || true - - # Determine whether we need to fall back to ~/.npm-global: - # 1. prefix is empty or contains an error string - # 2. prefix is a system directory (would break sudo setuid binaries) - # 3. prefix directory is not writable - local use_user_dir=false - - if [[ -z "${NPM_GLOBAL_DIR}" ]] || [[ "${NPM_GLOBAL_DIR}" == *"error"* ]]; then - log_info "npm prefix is unset or invalid, switching to user directory" - use_user_dir=true - else - # SAFETY CHECK: Never use system directories - case "${NPM_GLOBAL_DIR}" in - /|/usr|/usr/local|/bin|/sbin|/lib|/lib64|/opt|/snap|/var|/etc) - log_warning "npm prefix is a system directory (${NPM_GLOBAL_DIR}), switching to user directory to avoid breaking system binaries." - use_user_dir=true - ;; - esac - fi - - if [[ "${use_user_dir}" == false ]] && [[ ! -w "${NPM_GLOBAL_DIR}" ]]; then - log_warning "npm global directory is not writable: ${NPM_GLOBAL_DIR}, switching to user directory." - use_user_dir=true - fi - - if [[ "${use_user_dir}" == true ]]; then - NPM_GLOBAL_DIR="${HOME}/.npm-global" - # Create the directory before setting prefix so npm config set succeeds - mkdir -p "${NPM_GLOBAL_DIR}" - npm config set prefix "${NPM_GLOBAL_DIR}" - log_success "npm prefix set to: ${NPM_GLOBAL_DIR}" - - # Only add ~/.npm-global/bin to PATH when we actually use it - local PROFILE_FILE - PROFILE_FILE=$(get_shell_profile) - if [[ -n "${PROFILE_FILE}" ]] && ! grep -q '.npm-global/bin' "${PROFILE_FILE}" 2>/dev/null; then - { - echo "" - echo "# NPM global bin (added by Qwen Code installer)" - echo "export PATH=\"\$HOME/.npm-global/bin:\$PATH\"" - } >> "${PROFILE_FILE}" 2>/dev/null || log_warning "Failed to write PATH update to ${PROFILE_FILE}" - log_info "Added npm global bin to PATH in ${PROFILE_FILE}" - fi - else - log_info "npm global directory is writable: ${NPM_GLOBAL_DIR}" - fi - - return 0 -} - -# ============================================ -# Install Qwen Code -# ============================================ -install_qwen_code() { - # Ensure NVM node is in PATH - export NVM_DIR="${HOME}/.nvm" - # shellcheck source=/dev/null - [[ -s "${NVM_DIR}/nvm.sh" ]] && \. "${NVM_DIR}/nvm.sh" 2>/dev/null || true - - # Add npm global bin to PATH - local NPM_GLOBAL_BIN - NPM_GLOBAL_BIN=$(npm config get prefix 2>/dev/null)/bin - if [[ -n "${NPM_GLOBAL_BIN}" ]]; then - export PATH="${NPM_GLOBAL_BIN}:${PATH}" - fi - - if command_exists qwen; then - local QWEN_VERSION - QWEN_VERSION=$(qwen --version 2>/dev/null || echo "unknown") - log_success "Qwen Code is already installed: ${QWEN_VERSION}" - log_info "Upgrading to the latest version..." - fi - - # Clean npmrc conflicts - clean_npmrc_conflict - - # Fix npm permissions if needed - fix_npm_permissions - - # Install Qwen Code - log_info "Installing Qwen Code..." - if npm install -g @qwen-code/qwen-code@latest --registry https://registry.npmmirror.com; then - log_success "Qwen Code installed successfully!" - - # Verify installation - if command_exists qwen; then - local qwen_version - qwen_version=$(qwen --version 2>/dev/null) || qwen_version="unknown" - log_info "Qwen Code version: ${qwen_version}" - fi - else - log_error "Failed to install Qwen Code!" - log_info "Please check your internet connection and try again" - exit 1 - fi - - # Create source.json if source parameter was provided - if [[ "${SOURCE}" != "unknown" ]]; then - create_source_json - fi -} - -# ============================================ -# Create source.json -# ============================================ create_source_json() { - local QWEN_DIR="${HOME}/.qwen" + if [[ "${SOURCE}" == "unknown" ]]; then + return 0 + fi - mkdir -p "${QWEN_DIR}" + local qwen_dir="${HOME}/.qwen" + mkdir -p "${qwen_dir}" - # Escape special characters in SOURCE for JSON - local ESCAPED_SOURCE - ESCAPED_SOURCE=$(printf '%s' "${SOURCE}" | sed 's/\\/\\\\/g; s/"/\\"/g') + local escaped_source + escaped_source=$(printf '%s' "${SOURCE}" | sed 's/\\/\\\\/g; s/"/\\"/g') - cat > "${QWEN_DIR}/source.json" < "${qwen_dir}/source.json" </dev/null || echo unknown) + local arch + arch=$(uname -m 2>/dev/null || echo unknown) + + case "${os}" in + Darwin) + os="darwin" + ;; + Linux) + os="linux" + ;; + *) + return 1 + ;; + esac + + case "${arch}" in + x86_64|amd64) + arch="x64" + ;; + arm64|aarch64) + arch="arm64" + ;; + *) + return 1 + ;; + esac + + echo "${os}-${arch}" +} + +archive_extension_for_target() { + case "$1" in + darwin-*|linux-*) + echo "tar.gz" + ;; + *) + return 1 + ;; + esac +} + +release_version_path() { + if [[ "${VERSION}" == "latest" ]]; then + echo "latest" + return 0 + fi + + case "${VERSION}" in + v*) + echo "${VERSION}" + ;; + *) + echo "v${VERSION}" + ;; + esac +} + +standalone_base_url() { + if [[ -n "${BASE_URL}" ]]; then + echo "${BASE_URL%/}" + return 0 + fi + + local version_path + version_path=$(release_version_path) + + if [[ "${MIRROR}" == "aliyun" ]]; then + echo "https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/releases/qwen-code/${version_path}" + return 0 + fi + + if [[ "${version_path}" == "latest" ]]; then + echo "https://github.com/QwenLM/qwen-code/releases/latest/download" + return 0 + fi + + echo "https://github.com/QwenLM/qwen-code/releases/download/${version_path}" +} + +download_file() { + local url="$1" + local destination="$2" + + if command_exists curl; then + curl -fsSL --retry 2 "${url}" -o "${destination}" + return $? + fi + + if command_exists wget; then + wget -q "${url}" -O "${destination}" + return $? + fi + + log_error "curl or wget is required to download the standalone archive." + return 1 +} + +url_exists() { + local url="$1" + + if command_exists curl; then + curl -fsIL --retry 1 "${url}" >/dev/null 2>&1 + return $? + fi + + if command_exists wget; then + wget -q --spider "${url}" >/dev/null 2>&1 + return $? + fi + + return 1 +} + +sha256_file() { + local file_path="$1" + + if command_exists sha256sum; then + sha256sum "${file_path}" | awk '{print $1}' + return 0 + fi + + if command_exists shasum; then + shasum -a 256 "${file_path}" | awk '{print $1}' + return 0 + fi + + return 1 +} + +verify_checksum() { + local archive_path="$1" + local checksum_source="$2" + local archive_name="$3" + local checksum_file="${checksum_source}" + local temp_checksum="" + local checksum_required="false" + + if [[ -z "${checksum_file}" ]]; then + checksum_file="$(dirname "${archive_path}")/SHA256SUMS" + elif [[ "${checksum_file}" == http://* || "${checksum_file}" == https://* ]]; then + checksum_required="true" + temp_checksum="$(mktemp)" + if ! download_file "${checksum_file}" "${temp_checksum}"; then + rm -f "${temp_checksum}" + log_error "Could not download SHA256SUMS for checksum verification." + return 1 fi - log_info "Using HOME=${HOME}" + checksum_file="${temp_checksum}" fi - # Ensure download tool is available - ensure_download_tool - - # Check and install Node.js - check_and_install_nodejs - echo "" - - # Install Qwen Code - install_qwen_code - echo "" - - # ============================================ - # Final instructions - # ============================================ - echo "==========================================" - echo "✅ Installation completed!" - echo "==========================================" - echo "" - - # Ensure NVM and npm global bin are in PATH - export NVM_DIR="${HOME}/.nvm" - # shellcheck source=/dev/null - [[ -s "${NVM_DIR}/nvm.sh" ]] && \. "${NVM_DIR}/nvm.sh" 2>/dev/null || true - local NPM_GLOBAL_BIN - NPM_GLOBAL_BIN=$(npm config get prefix 2>/dev/null)/bin - if [[ -n "${NPM_GLOBAL_BIN}" ]]; then - export PATH="${NPM_GLOBAL_BIN}:${PATH}" + if [[ ! -f "${checksum_file}" ]]; then + if [[ "${checksum_required}" == "true" ]]; then + log_error "SHA256SUMS not found; cannot verify remote archive." + return 1 + fi + log_warning "SHA256SUMS not found; skipping checksum verification." + return 0 fi - # Check if qwen is immediately available + local expected + expected=$(grep -E "(^|[[:space:]])[*]?${archive_name}$" "${checksum_file}" | awk '{print $1}' | head -n 1) + if [[ -z "${expected}" ]]; then + rm -f "${temp_checksum}" + if [[ "${checksum_required}" == "true" ]]; then + log_error "Checksum entry for ${archive_name} not found." + return 1 + fi + log_warning "Checksum entry for ${archive_name} not found; skipping checksum verification." + return 0 + fi + + local actual + if ! actual=$(sha256_file "${archive_path}"); then + rm -f "${temp_checksum}" + if [[ "${checksum_required}" == "true" ]]; then + log_error "No SHA-256 utility found; cannot verify remote archive." + return 1 + fi + log_warning "No SHA-256 utility found; skipping checksum verification." + return 0 + fi + + rm -f "${temp_checksum}" + + if [[ "${expected}" != "${actual}" ]]; then + log_error "Checksum verification failed for ${archive_name}." + return 1 + fi + + log_success "Checksum verified for ${archive_name}." +} + +extract_archive() { + local archive_path="$1" + local destination="$2" + + mkdir -p "${destination}" + + case "${archive_path}" in + *.zip) + if ! command_exists unzip; then + log_error "unzip is required to extract ${archive_path}." + return 1 + fi + unzip -q "${archive_path}" -d "${destination}" + ;; + *.tar.gz|*.tgz) + tar -xzf "${archive_path}" -C "${destination}" + ;; + *) + log_error "Unsupported archive format: ${archive_path}" + return 1 + ;; + esac +} + +install_standalone() { + local target="" + local archive_name="" + local archive_path="" + local checksum_source="" + local temp_dir="" + + if [[ -n "${ARCHIVE_PATH}" ]]; then + archive_path="${ARCHIVE_PATH}" + archive_name="$(basename "${archive_path}")" + if [[ ! -f "${archive_path}" ]]; then + log_error "Standalone archive not found: ${archive_path}" + return 1 + fi + else + if ! target=$(detect_target); then + log_warning "Standalone archive is not available for this platform." + return 2 + fi + + local archive_extension + archive_extension=$(archive_extension_for_target "${target}") + archive_name="qwen-code-${target}.${archive_extension}" + + local base_url + base_url=$(standalone_base_url) + local archive_url="${base_url}/${archive_name}" + checksum_source="${base_url}/SHA256SUMS" + + if [[ "${METHOD}" == "detect" ]] && ! url_exists "${archive_url}"; then + log_warning "Standalone archive not found: ${archive_name}" + return 2 + fi + + temp_dir=$(mktemp -d) + archive_path="${temp_dir}/${archive_name}" + + log_info "Downloading ${archive_url}" + if ! download_file "${archive_url}" "${archive_path}"; then + rm -rf "${temp_dir}" + log_warning "Failed to download standalone archive." + return 2 + fi + fi + + if [[ -z "${temp_dir}" ]]; then + temp_dir=$(mktemp -d) + fi + + if ! verify_checksum "${archive_path}" "${checksum_source}" "${archive_name}"; then + rm -rf "${temp_dir}" + return 1 + fi + + local extract_dir="${temp_dir}/extract" + if ! extract_archive "${archive_path}" "${extract_dir}"; then + rm -rf "${temp_dir}" + return 1 + fi + + if [[ ! -x "${extract_dir}/qwen-code/bin/qwen" ]]; then + log_error "Archive does not contain qwen-code/bin/qwen." + rm -rf "${temp_dir}" + return 1 + fi + + if [[ ! -x "${extract_dir}/qwen-code/node/bin/node" ]]; then + log_error "Archive does not contain executable qwen-code/node/bin/node." + rm -rf "${temp_dir}" + return 1 + fi + + mkdir -p "${INSTALL_LIB_PARENT}" "${INSTALL_BIN_DIR}" + + local new_install_dir="${INSTALL_LIB_DIR}.new" + local old_install_dir="${INSTALL_LIB_DIR}.old" + rm -rf "${new_install_dir}" "${old_install_dir}" + mv "${extract_dir}/qwen-code" "${new_install_dir}" + + if [[ -e "${INSTALL_LIB_DIR}" ]]; then + mv "${INSTALL_LIB_DIR}" "${old_install_dir}" + fi + + if ! mv "${new_install_dir}" "${INSTALL_LIB_DIR}"; then + if [[ -e "${old_install_dir}" ]]; then + mv "${old_install_dir}" "${INSTALL_LIB_DIR}" + fi + rm -rf "${temp_dir}" + log_error "Failed to install standalone archive to ${INSTALL_LIB_DIR}." + return 1 + fi + + rm -rf "${old_install_dir}" + cat > "${INSTALL_BIN_DIR}/qwen" </dev/null || echo "unknown") + log_info "Existing Qwen Code detected: ${qwen_version}" + log_info "Upgrading to the latest version." + fi + + local install_cmd=( + npm + install + -g + @qwen-code/qwen-code@latest + --registry + "${NPM_REGISTRY}" + ) + + log_info "Running: npm install -g @qwen-code/qwen-code@latest --registry ${NPM_REGISTRY}" + if "${install_cmd[@]}"; then + log_success "Qwen Code installed successfully." + create_source_json + return 0 + fi + + log_error "Failed to install Qwen Code." + echo "" + echo "This installer does not change your npm prefix or shell profile." + echo "If the failure is a permission error, install Node.js with a user-owned" + echo "Node version manager or fix your npm global package directory, then run:" + echo " npm install -g @qwen-code/qwen-code@latest --registry ${NPM_REGISTRY}" + exit 1 +} + +print_final_instructions() { + local install_bin_dir="${1:-}" + if [[ -n "${install_bin_dir}" ]]; then + export PATH="${install_bin_dir}:${PATH}" + fi + + echo "" + echo "==========================================" + echo "Installation completed!" + echo "==========================================" + echo "" + + if command_exists qwen; then + local qwen_version + qwen_version=$(qwen --version 2>/dev/null || echo "unknown") + log_success "Qwen Code is ready to use: ${qwen_version}" echo "" echo "You can now run: qwen" echo "" - # Auto-start qwen - log_info "Starting Qwen Code..." + log_info "Run qwen in your project directory to start an interactive session." + return 0 + fi + + log_warning "Qwen Code was installed, but qwen is not on PATH in this shell." + echo "" + echo "Restart your terminal, then run: qwen" + if [[ -n "${install_bin_dir}" ]]; then echo "" - exec qwen - else - log_warning "Qwen Code command not found in current session" - echo "" - echo "To use Qwen Code immediately without restarting your terminal," - echo "run the following command in your current shell:" - echo " eval \$(${0} --print-env)" - echo "" - log_info "Or simply restart your terminal, then run: qwen" + echo "Or run this in the current shell:" + echo " export PATH=\"${install_bin_dir}:\$PATH\"" + echo " qwen" fi } -# Run main function +main() { + if [[ -z "${HOME:-}" ]]; then + log_error "HOME is not set; cannot determine where to install Qwen Code." + exit 1 + fi + + print_header + + case "${METHOD}" in + standalone) + install_standalone + print_final_instructions "${INSTALL_BIN_DIR}" + ;; + npm) + install_npm + print_final_instructions "$(get_npm_global_bin)" + ;; + detect) + if install_standalone; then + print_final_instructions "${INSTALL_BIN_DIR}" + else + standalone_status=$? + if [[ "${standalone_status}" -eq 2 ]]; then + log_warning "Falling back to npm installation." + install_npm + print_final_instructions "$(get_npm_global_bin)" + else + exit "${standalone_status}" + fi + fi + ;; + esac +} + main "$@" diff --git a/scripts/tests/install-script.test.js b/scripts/tests/install-script.test.js new file mode 100644 index 000000000..4fe034850 --- /dev/null +++ b/scripts/tests/install-script.test.js @@ -0,0 +1,218 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it, vi } from 'vitest'; + +const { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} = await vi.importActual('node:fs'); +const { execFileSync } = await vi.importActual('node:child_process'); +const { tmpdir } = await vi.importActual('node:os'); +const path = await vi.importActual('node:path'); +const readScript = (path) => readFileSync(path, 'utf8'); + +describe('installation scripts', () => { + it('keeps the Linux/macOS installer lightweight', () => { + const script = readScript( + 'scripts/installation/install-qwen-with-source.sh', + ); + + expect(script).not.toContain('install_nvm'); + expect(script).not.toContain('install_nvm.sh'); + expect(script).not.toContain('nvm install'); + expect(script).not.toContain('NVM_NODEJS_ORG_MIRROR'); + expect(script).not.toContain('npm config set prefix'); + expect(script).not.toContain('clean_npmrc_conflict'); + expect(script).not.toContain('.npmrc'); + expect(script).not.toContain('.npm-global'); + expect(script).not.toMatch(/^\s*exec\s+qwen\s*$/m); + expect(script).not.toContain('--print-env'); + expect(script).not.toContain('brew install node@20'); + expect(script).toContain('brew install node'); + expect(script).toContain( + '--source may only contain letters, numbers, dot, underscore, or dash', + ); + expect(script).toContain('Node.js 20 or newer is required'); + expect(script).toContain( + 'npm install -g @qwen-code/qwen-code@latest --registry', + ); + expect(script).toContain('You can now run: qwen'); + }); + + it('supports code-server-style standalone install on Linux/macOS', () => { + const script = readScript( + 'scripts/installation/install-qwen-with-source.sh', + ); + + expect(script).toContain('--method METHOD'); + expect(script).toContain('--mirror MIRROR'); + expect(script).toContain('--base-url URL'); + expect(script).toContain('--archive PATH'); + expect(script).toContain('install_standalone()'); + expect(script).toContain('install_npm()'); + expect(script).toContain('detect_target()'); + expect(script).toContain('verify_checksum()'); + expect(script).toContain( + 'SHA256SUMS not found; cannot verify remote archive', + ); + expect(script).toContain('qwen-code-${target}'); + expect(script).toContain('METHOD="${METHOD:-detect}"'); + expect(script).toContain('Falling back to npm installation'); + expect(script).toContain('standalone_status=$?'); + expect(script).toContain('[[ "${standalone_status}" -eq 2 ]]'); + expect(script).not.toContain('ln -sf "${INSTALL_LIB_DIR}/bin/qwen"'); + expect(script).toContain('exec "${INSTALL_LIB_DIR}/bin/qwen"'); + expect(script).toContain('qwen-code/node/bin/node'); + }); + + it('keeps the Windows installer lightweight', () => { + const script = readScript( + 'scripts/installation/install-qwen-with-source.bat', + ); + + expect(script).not.toContain('InstallNodeJSDirectly'); + expect(script).not.toContain('node-v!NODE_VERSION!'); + expect(script).not.toContain('msiexec'); + expect(script).not.toContain('Invoke-WebRequest'); + expect(script).not.toContain('PowerShell (Administrator)'); + expect(script).not.toContain('echo INFO: Installation source: %SOURCE%'); + expect(script).not.toMatch(/^\s*call\s+qwen\s*$/m); + expect(script).toContain(':ValidateSource'); + expect(script).toContain('findstr /R'); + expect(script).toContain( + '--source may only contain letters, numbers, dot, underscore, or dash', + ); + expect(script).toContain('Node.js 20 or newer is required'); + expect(script).toContain('Please install Node.js'); + expect(script).toContain( + 'npm install -g @qwen-code/qwen-code@latest --registry', + ); + expect(script).toContain('You can now run: qwen'); + }); + + it('supports code-server-style standalone install on Windows', () => { + const script = readScript( + 'scripts/installation/install-qwen-with-source.bat', + ); + + expect(script).toContain('--method METHOD'); + expect(script).toContain('--mirror MIRROR'); + expect(script).toContain('--base-url URL'); + expect(script).toContain('--archive PATH'); + expect(script).toContain(':InstallStandalone'); + expect(script).toContain(':InstallNpm'); + expect(script).toContain(':VerifyChecksum'); + expect(script).toContain( + 'SHA256SUMS not found; cannot verify remote archive', + ); + expect(script).toContain('qwen-code-win-x64.zip'); + expect(script).toContain('Expand-Archive'); + expect(script).toContain('Falling back to npm installation'); + expect(script).toContain('set "STANDALONE_STATUS=!ERRORLEVEL!"'); + expect(script).toContain('if !STANDALONE_STATUS! EQU 2'); + expect(script).toContain('qwen-code\\node\\node.exe'); + }); +}); + +describe('standalone release packaging', () => { + it('defines a standalone packaging script', () => { + const packageJson = JSON.parse(readScript('package.json')); + + expect(packageJson.scripts['package:standalone']).toBe( + 'node scripts/create-standalone-package.js', + ); + expect(existsSync('scripts/create-standalone-package.js')).toBe(true); + + const packageScript = readScript('scripts/create-standalone-package.js'); + expect(packageScript).toContain("'bundled/qc-helper/docs'"); + expect(packageScript).toContain("path.join(packageRoot, 'package.json')"); + expect(packageScript).toContain('validateNodeRuntime'); + }); + + it('rejects a runtime archive without a Node executable', () => { + const createdDist = ensureMinimalDist(); + const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-package-test-')); + + try { + const fakeRuntimeDir = path.join(tmpDir, 'not-node'); + mkdirSync(fakeRuntimeDir, { recursive: true }); + writeFileSync(path.join(fakeRuntimeDir, 'README.txt'), 'not node\n'); + const fakeRuntimeArchive = path.join(tmpDir, 'bad-runtime.tar.gz'); + execFileSync( + 'tar', + ['-czf', fakeRuntimeArchive, '-C', tmpDir, 'not-node'], + { + env: { ...process.env, LC_ALL: 'C' }, + stdio: 'ignore', + }, + ); + + expect(() => + execFileSync( + 'node', + [ + 'scripts/create-standalone-package.js', + '--target', + 'linux-x64', + '--node-archive', + fakeRuntimeArchive, + '--out-dir', + path.join(tmpDir, 'out'), + '--version', + '0.0.0-test', + ], + { stdio: 'pipe' }, + ), + ).toThrow(/Node.js runtime for linux-x64 must contain/); + } finally { + rmSync(tmpDir, { recursive: true, force: true }); + if (createdDist) { + rmSync('dist', { recursive: true, force: true }); + } + } + }); + + it('uploads standalone archives during release', () => { + const workflow = readScript('.github/workflows/release.yml'); + + expect(workflow).toContain('set -euo pipefail'); + expect(workflow).toContain('SHASUMS256.txt'); + expect(workflow).toContain('$2 == name'); + expect(workflow).toContain('does not list ${archive_name}'); + expect(workflow).toContain('sha256sum -c -'); + expect(workflow).toContain('npm run package:standalone'); + expect(workflow).toContain('dist/standalone/qwen-code-*'); + expect(workflow).toContain('dist/standalone/SHA256SUMS'); + }); + + it('documents optional native module parity for standalone installs', () => { + const guide = readScript('scripts/installation/INSTALLATION_GUIDE.md'); + + expect(guide).toContain('Optional Native Modules'); + expect(guide).toContain('node-pty'); + expect(guide).toContain('clipboard'); + }); +}); + +function ensureMinimalDist() { + if (existsSync('dist')) { + return false; + } + + mkdirSync('dist/vendor', { recursive: true }); + mkdirSync('dist/bundled/qc-helper/docs', { recursive: true }); + writeFileSync('dist/cli.js', 'console.log("qwen");\n'); + writeFileSync( + 'dist/package.json', + JSON.stringify({ name: '@qwen-code/qwen-code', version: '0.0.0' }), + ); + return true; +}