qwen-code/packages/cli/package.json
易良 9e4bea606a
feat(cli): add standalone auto-update support (#4629)
* feat(cli): add standalone auto-update support

Standalone installs (via install-qwen-standalone.sh) previously fell
through to npm global detection, causing auto-update to run
`npm install -g` which doesn't update the standalone binary.

Now the CLI detects standalone installs by looking for manifest.json
in ancestor directories, then performs a self-update: download archive
from OSS/GitHub, verify SHA256, extract, and atomically replace the
installation directory.

Includes: input validation (semver + target allowlist), lockfile for
concurrency, Windows deferred replacement via helper script, and tar
path traversal protection.

Closes #4627

* feat(cli): harden standalone update — smoke test, rollback, signature

- Smoke test: spawn new binary with --version after extraction,
  before replacing the installation. Rejects broken/wrong-arch packages.
- Rollback: keep .old directory after updates. Export
  rollbackStandaloneUpdate() for future /doctor rollback integration.
- Signature: Ed25519 verification of SHA256SUMS via node:crypto.
  Embeds public key; CI signs with scripts/sign-release.sh.
  Graceful degradation when .sig is not yet published (transition period).
  Set QWEN_REQUIRE_SIGNATURE=1 to enforce strict mode.

* fix(cli): address CI + review findings on standalone update

- Fix TS4111: use bracket notation for process.env index signature
- Fix CodeQL: validate paths for cmd.exe metacharacters before bat script
- Fix Windows lock race: keep lock alive during deferred swap, release
  in bat script after move completes

* fix(cli): use event payload in update UI + add rollback tests

- handleAutoUpdate: handleUpdateFailed/handleUpdateSuccess now read
  the message from the event payload instead of hardcoding text.
  Windows deferred users now see the correct "applied after exit" message.
- Add standalone-update.test.ts with 4 tests covering rollback scenarios.

* fix(cli): address review findings — 5 bugs in standalone-update

1. SHA256SUMS: handle GNU coreutils binary-mode `*` prefix in filename
2. acquireLock: treat NaN (empty/corrupt lock file) as stale, not held
3. tryFetch: consume response body on non-OK status to release sockets
4. Windows bat: add 30s timeout to PID wait loop preventing infinite hang
5. Finally block: clean orphaned .new directory on Windows error path

* fix(cli): update handleAutoUpdate tests for payload-driven messages

Tests previously asserted hardcoded handler text. After making handlers
read messages from event payload, the assertions need to reflect the
emitted payload values instead.

* feat(cli): auto-migrate npm users to standalone when prefix is not writable

When a global npm install requires sudo (prefix not writable), the
auto-update now falls back to the standalone path instead of spawning
a doomed `npm install -g` that always EACCES.

- installationInfo.ts: detect writable prefix with fs.accessSync;
  return isStandalone=true with fallback dir when not writable
- standalone-update.ts: support first-time migration (no existing
  manifest), detect platform target, create bin wrapper, refuse to
  overwrite unmanaged directories

Closes #4643

* fix(cli): remove unused _mockedAccessSync (TS6133)

* fix(cli): address wenshao review — Windows deferred, PATH, rollback, headers

- Fix Windows deferred update: move pendingDir cleanup to catch block
  so it only runs on error, not on successful deferred path
- Add ensurePathInShellRc: auto-append ~/.local/bin to shell rc on
  npm→standalone migration so the wrapper is actually discoverable
- Wire rollbackStandaloneUpdate into /doctor rollback subcommand
- Add license headers to all 4 new source files
- Fix spawnAndCapture double-settle with settled flag
- Add tests for ensureBinWrapper and ensurePathInShellRc
- Clarify signature verification comments (test key, not production)

* fix(cli): add i18n translations for /doctor rollback command

The mustTranslateKeys test requires zh-CN and zh-TW translations for
all built-in command descriptions. Add translations for the new
rollback subcommand and its user-facing messages.

* fix(cli): skip Unix-only wrapper tests on Windows

The ensureBinWrapper Unix-wrapper test asserts on POSIX file mode bits
(mode & 0o111) which Windows NTFS does not implement. Skip Unix-specific
tests when running on Windows runners.

* fix(cli): address CodeQL + test coverage on standalone-update

- Add assertPathWithin() and resolve install paths before spawning the
  smoke-test process. Closes the CodeQL "shell command built from
  environment values" finding by guaranteeing nodeBin/cliBin stay
  inside the freshly extracted install directory.
- Add performStandaloneUpdate test coverage for the four pre-flight
  failure cases (bad version, missing manifest, unknown target,
  concurrent lock) so the core update path is no longer untested.

* fix(cli): address wenshao review round 2 — bat error handling, rollback safety, shell injection, lock race, test coverage

Five issues raised in PR #4629 review (2026-06-01):

1. Windows bat: add errorlevel checks after each move, rollback on
   second-move failure, timestamped log to qwen-update.log
2. Unix rollback: inner try/catch on recovery rename; compound error
   with manual mv instructions if both renames fail
3. Shell injection: assertSafeForShellEmbed() validates paths before
   embedding in /bin/sh wrappers and shell rc (platform-aware: allows
   backslash on Windows)
4. Lock-file race: bat writes .swap sentinel before rename; acquireLock
   refuses to reclaim stale-PID lock while sentinel exists
5. Test coverage: 4 new handleAutoUpdate standalone-path tests (done,
   deferred, error, npm-not-entered) + 3 shell-safety rejection tests

* fix(cli): prevent symlink attack on staging directory via mkdtempSync

Replace fixed-name .qwen-code-update-staging with mkdtempSync random
suffix — an attacker who can write to parentDir can no longer pre-create
a symlink to hijack recursive deletion.

Also adds lstat guard before cleanup rmSync (defense-in-depth) and fixes
a potential tempDir leak if mkdtempSync for extractDir fails.

* fix(cli): harden standalone-update — download size guard, shell-meta coverage, cleanup logging

- Add MAX_DOWNLOAD_BYTES (512 MB) streaming size guard in downloadToFile
  to prevent resource exhaustion from oversized responses
- Extend UNSAFE_SHELL_META_UNIX to also reject semicolon and single-quote
- Add debugLogger.warn when extractDir cleanup is skipped (unexpected type)
- Add Windows backslash acceptance test for assertSafeForShellEmbed

* fix(cli): harden standalone-update — tar preservePaths, timeout signal, shell-rc validation

Defense-in-depth improvements from independent code review:
- Explicitly set preservePaths:false on tar extract to prevent symlink path traversal
- Check err.signal in spawnAndCapture for cross-platform timeout detection
- Validate binDir in ensurePathInShellRc independently of calling context

* fix(cli): address review round 3 — archive timeout, stderr capture, rollback result type

1. Separate ARCHIVE_TIMEOUT_MS (5 min) from FETCH_TIMEOUT_MS (30s) so
   large archive downloads on slow networks don't time out prematurely
2. Capture stderr from smoke-test execFile and include it in the error
   message so users see the actual crash reason (missing lib, etc.)
3. Change rollbackStandaloneUpdate return from boolean to discriminated
   result type (ok/reason/detail) so doctorCommand shows accurate
   messages for each failure mode instead of a generic "not found"

* fix(test): drop pre-mkdir in shell-safety double-quote test

The 'rejects standaloneDir with embedded double-quote' test called
fs.mkdirSync on a path containing `"`, which ENOENTs on Windows since
NTFS forbids `"` in path components. The validator
assertSafeForShellEmbed runs synchronously before any FS access in
ensureBinWrapper, so the pre-mkdir was vestigial — removing it makes
the test pass on Windows without weakening the assertion.

* refactor(standalone-update): remove over-engineered defenses

Remove theoretical security mechanisms that solve near-impossible
attack scenarios but introduce real complexity and new bug surface:

- sentinel mechanism (solves a <2s race window requiring 3 independent
  low-probability events to align)
- assertSafeForShellEmbed + UNSAFE_SHELL_META regexes (default paths
  are ~/.local/lib/qwen-code, never contain metacharacters)
- assertPathWithin (execFile doesn't invoke shell; CodeQL false positive)
- lstatSync symlink guard (mkdtempSync random naming already prevents
  predictable symlink pre-creation)

Retains unsafeCmdChars in atomicReplace (bat script interpolation is a
real context), bat error handling, archive timeout, and stderr capture.

* fix(cli): harden standalone-update error paths and lock release

* fix(cli): address multi-model review findings on standalone-update

Security:
- Preserve per-mirror error details in downloadWithFallback diagnostics
- Deduplicate SHA256SUMS.sig download via downloadWithFallback
- Upgrade signature-skip log from info to warn with enforcement hint
- Add post-extraction path traversal validation for Windows ZIP
- Add symlink target validation in tar extraction filter
- Add shell metacharacter validation (assertSafeForShellEmbed) for
  binDir in ensurePathInShellRc and standaloneDir in ensureBinWrapper
- Validate cmd.exe metacharacters BEFORE filesystem mutation in
  atomicReplace Windows path

Correctness:
- Fix spawnAndCapture treating string error codes as exit 0
- Release lock when mkdtempSync for extractDir fails
- Add concurrent lock protection to rollbackStandaloneUpdate
- Fix misleading manual-recovery message after auto-recovery succeeds
- Fix t() dynamic i18n key — split into translated prefix + raw detail
- Reject unsupported architectures in detectTarget instead of silent
  fallback to linux-x64

Performance:
- Compute SHA256 hash during download stream (single-pass), eliminating
  the second full read of the 50-150 MB archive

Quality:
- Fix macOS bash shell rc priority (.bash_profile over .bashrc)
- Rename npmPrefixDir to npmPackageDir for accuracy
- Extract magic number 3 to MAX_MANIFEST_SEARCH_DEPTH constant

Tests:
- Add rollback concurrent lock protection tests (live + dead PID)
- Add fish shell ensurePathInShellRc test
- Add shell metacharacter rejection test for ensurePathInShellRc

* fix(cli): move shell validation after platform branch in ensureBinWrapper

assertSafeForShellEmbed rejects backslashes, which are normal in Windows
paths. Move the call into the Unix-only else branch so Windows wrapper
creation uses only the cmd.exe metacharacter check.

* fix(cli): add post-extraction path validation for Unix tar

Matches the Windows zip path's validateExtractedPaths call.
The tar filter's string-level path.resolve check cannot detect chained
symlink attacks (symlink A → ".", then A/payload → "../../etc").
The post-extraction realpath walk catches these.

* fix(cli): fix standalone auto-update reachability and rebase conflicts

- Add missing isStandalone/standaloneDir fields to getStandaloneInstallInfo
  so handleAutoUpdate and /doctor rollback can detect standalone installs
- Remove redundant findStandaloneDir (conflicted with stricter detection)
- Fix race condition: move mkdirSync after lock acquisition
- Ensure parent directory exists before lock file creation
- Clean up empty .old directory after first-time npm→standalone migration
- Align bash RC file selection with install script (Linux → .bashrc)
- Use ?? instead of || for manifest.target fallback
- Upgrade ensureBinWrapper failure logging from debug to warn
- Add debug logging for SHA256SUMS.sig download failures
2026-06-04 22:53:12 +08:00

124 lines
3.6 KiB
JSON

{
"name": "@qwen-code/qwen-code",
"version": "0.17.1",
"description": "Qwen Code",
"repository": {
"type": "git",
"url": "git+https://github.com/QwenLM/qwen-code.git"
},
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"qwen": "dist/index.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./export": {
"types": "./dist/src/export/index.d.ts",
"import": "./dist/src/export/index.js"
}
},
"scripts": {
"build": "node ../../scripts/build_package.js",
"start": "node dist/index.js",
"debug": "node --inspect-brk dist/index.js",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write .",
"test": "vitest run",
"test:ci": "vitest run",
"typecheck": "tsc --noEmit",
"check-i18n": "tsx ../../scripts/check-i18n.ts"
},
"files": [
"dist"
],
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.17.1"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.14.1",
"@google/genai": "2.6.0",
"@iarna/toml": "^2.2.5",
"@modelcontextprotocol/sdk": "^1.25.2",
"@qwen-code/acp-bridge": "file:../acp-bridge",
"@qwen-code/channel-base": "file:../channels/base",
"@qwen-code/channel-dingtalk": "file:../channels/dingtalk",
"@qwen-code/channel-feishu": "file:../channels/feishu",
"@qwen-code/channel-telegram": "file:../channels/telegram",
"@qwen-code/channel-weixin": "file:../channels/weixin",
"@qwen-code/qwen-code-core": "file:../core",
"@qwen-code/web-templates": "file:../web-templates",
"@types/update-notifier": "^6.0.8",
"ansi-regex": "^6.2.2",
"command-exists": "^1.2.9",
"comment-json": "^4.2.5",
"diff": "^7.0.0",
"dotenv": "^17.1.0",
"express": "^5.2.1",
"fzf": "^0.5.2",
"glob": "^10.5.0",
"highlight.js": "^11.11.1",
"ink": "7.0.3",
"ink-gradient": "^3.0.0",
"ink-link": "^4.1.0",
"ink-spinner": "^5.0.0",
"lowlight": "^3.3.0",
"open": "^10.1.2",
"p-limit": "^7.3.0",
"prompts": "^2.4.2",
"react": "^19.2.4",
"read-package-up": "^11.0.0",
"shell-quote": "^1.8.3",
"simple-git": "^3.28.0",
"string-width": "^7.1.0",
"strip-ansi": "^7.1.0",
"strip-json-comments": "^3.1.1",
"tar": "^7.5.2",
"undici": "^6.22.0",
"update-notifier": "^7.3.1",
"wrap-ansi": "^10.0.0",
"yargs": "^17.7.2",
"zod": "^3.23.8"
},
"devDependencies": {
"@babel/runtime": "^7.27.6",
"@testing-library/react": "^16.3.0",
"@types/archiver": "^6.0.3",
"@types/command-exists": "^1.2.3",
"@types/diff": "^7.0.2",
"@types/dotenv": "^6.1.1",
"@types/express": "^5.0.3",
"@types/node": "^22.0.0",
"@types/prompts": "^2.4.9",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/semver": "^7.7.0",
"@types/shell-quote": "^1.7.5",
"@types/supertest": "^6.0.3",
"@types/yargs": "^17.0.32",
"archiver": "^7.0.1",
"ink-testing-library": "^4.0.0",
"jsdom": "^26.1.0",
"pretty-format": "^30.0.2",
"react-dom": "^19.1.0",
"supertest": "^7.2.2",
"typescript": "^5.3.3",
"vitest": "^3.1.1"
},
"optionalDependencies": {
"@teddyzhu/clipboard": "^0.0.5",
"@teddyzhu/clipboard-darwin-arm64": "0.0.5",
"@teddyzhu/clipboard-darwin-x64": "0.0.5",
"@teddyzhu/clipboard-linux-arm64-gnu": "0.0.5",
"@teddyzhu/clipboard-linux-x64-gnu": "0.0.5",
"@teddyzhu/clipboard-win32-arm64-msvc": "0.0.5",
"@teddyzhu/clipboard-win32-x64-msvc": "0.0.5"
},
"engines": {
"node": ">=22"
}
}