Commit graph

18 commits

Author SHA1 Message Date
易良
d59c9e7b77
feat(installer): add standalone hosted install and uninstall flow (#3828)
* feat(installer): add standalone archive installation

* fix(installer): harden standalone archive installs

* fix(installer): address standalone review findings

* chore(installer): clarify review followups

* fix(installer): stabilize standalone script checks

* chore(installer): remove internal planning docs

* chore(installer): simplify standalone release review fixes

* test(installer): add Windows batch install smoke

* test(installer): fix Windows batch smoke quoting

* test(installer): preserve Windows cmd quotes

* fix(installer): use robust Windows checksum hashing

* ci: narrow installer debug matrix

* fix(installer): address standalone review hardening

* fix(installer): avoid Windows validation parse errors

* fix(installer): simplify Windows option validation

* fix(installer): harden standalone review fixes

* feat(installer): publish release installer assets

* fix(installer): address release asset review feedback

* fix(installer): avoid prerelease installer asset links

* test(installer): isolate standalone dist fixture

* feat(installer): add hosted install release alias

* chore: no changes - code review requested

Agent-Logs-Url: https://github.com/QwenLM/qwen-code/sessions/38467aec-15b9-4b76-9139-0b2cfe40477a

* fix(installer): pin versioned installer assets

* fix: parallelize Node.js binary downloads in standalone release build

Use Promise.all instead of sequential for...of+await for
the 5 independent Node.js runtime downloads, reducing CI
release build time by ~4-5x.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(installer): address release asset review followups

* refactor(installer): share release CLI parsing

* fix(installer): address release asset review followups

- sh: reject CR/LF in archive entry names before the literal `..` glob so
  a `..\r` entry cannot bypass path validation.
- bat: prefer Tls12+Tls13 in PowerShell helpers, fall back to Tls12 alone
  on older .NET Framework where the Tls13 enum is missing.
- bat: document the implicit `:ValidateOptions` dependency next to the
  qwen.cmd wrapper writer so loosening the validator stays a conscious
  choice.
- build-standalone-release: surface the `xz-utils` host requirement for
  Linux Node downloads in `--help`.
- release-script-utils: support `--key=value` form in `parseCliArgs`.
- tests: cover the new CRLF message, TLS string, and `--key=value` parsing;
  register process-level signal/exit handlers in `ensureMinimalDist` so a
  crashed test still restores `dist/`.

* fix(installer): unblock Windows CI for standalone install path

Three CI failures and a few review followups in one pass.

- ensureMinimalDist places its dist/ backup beside dist/ instead of
  under os.tmpdir(). On Windows GitHub runners the workspace lives on
  D: while os.tmpdir() is on C:, so renameSync raised EXDEV for every
  test that needed to swap dist/ in.
- create-standalone-package.js and the matching test fixture build
  win-x64 zips with [IO.Compression.ZipFile]::CreateFromDirectory.
  Compress-Archive emits backslash entry names that the .bat
  installer's path-traversal guard then rejected, so every freshly
  built archive failed the standalone install path on Windows.
- :ValidateArchiveContents normalizes entry separators to '/' before
  checking for '..', absolute paths, and drive prefixes - archives
  from any Windows zip tool still install while real traversal
  entries remain rejected.
- createWindowsTraversalStandaloneArchive runs PowerShell via -File
  instead of a single -Command line; the joined-with-'; ' form had a
  function definition the runner's PowerShell refused to parse.

Drive-by review followups:

- replaceRequired uses replaceAll so a future duplicate placeholder
  cannot silently keep the trailing copy as 'latest'.
- :ValidateOptions runs the unsafe-character check on SOURCE
  alongside the other variables.
- build-installation-assets.js drops a dead INSTALLATION_ASSETS
  re-export; consumers already import from release-asset-config.js.
- .gitignore covers the new sibling .qwen-dist-backup-* directory.

* fix(installer): address release asset review findings

* fix(installer): keep installer entrypoint hosted

* fix(installer): reject stale hosted assets

* fix(installer): refine hosted asset staging

* fix(installer): tighten hosted default-version check, flag legacy URL

- Replace the loose `latest` fragment check with per-format regex patterns
  in HOSTED_INSTALLER_DEFAULT_VERSION_PATTERNS so an unrelated occurrence
  of `latest` (comment, help text) cannot satisfy the staging guard. The
  patterns still tolerate whitespace variation, only the default-version
  assignment itself must be intact.
- Add a "Hosted endpoint status" callout in INSTALLATION_GUIDE.md before
  the curl examples. The documented `--version` flow does not work against
  the OSS URL today because it currently serves the legacy NVM-based
  installer; the callout points users at a local checkout until the next
  release sync.
- Tests: drop `latest` from the fragments equality assertion, add positive
  and negative regex coverage, add a failure-path case for sources whose
  default version is not `latest`, and pin the new guide markers so the
  callout cannot silently disappear.

* feat(installer): verify installation release assets

Adds `npm run verify:installation-release` and wires it into the release
workflow after `Build Standalone Archives`, so a broken release directory
fails CI before publishing.

Local mode (`--dir PATH`) checks:
- All five `qwen-code-{platform}.{ext}` standalone archives exist.
- `SHA256SUMS` covers exactly those five — missing or unexpected entries fail.
- Each archive's actual SHA256 matches its `SHA256SUMS` entry.

Remote mode (`--base-url URL`) checks:
- `SHA256SUMS` is downloadable, parseable, and contains exactly the expected
  archive entries.
- Each archive URL is reachable via HEAD, with a 1-byte ranged GET fallback
  for hosts that disable HEAD.

Hosted installer scripts (`install-qwen.sh` / `install-qwen.bat`) are
intentionally out of scope here — they are served from the hosted endpoint
prepared by `package:hosted-installation` (PR #3853), not from the GitHub
Release surface this verifier targets.

* fix(installer): tighten verifier base-url + clarify test helper

Three small refinements from the second review pass:

- normalizeHttpsBaseUrl rejects everything except https, since real release
  URLs are always HTTPS. Accepting http previously would let an operator
  silently target a stale or attacker-controlled mirror.
- Drop EXPECTED_RELEASE_ASSET_NAMES from the public exports; it was only
  used internally for the verification log line.
- Rename the test helper standaloneChecksumContent to
  placeholderChecksumContent and document that the hashes in its output are
  placeholders — the remote verifier does not download archives or compare
  hashes, it only validates that SHA256SUMS lists the expected names and
  that each archive URL is reachable.

The non-https rejection test now also covers `http://` in addition to the
existing `file://` case.

* fix(installer): address standalone review follow-ups

* fix(installer): repair Windows installer tests

* fix(release): tighten standalone asset checks

* fix(installer): stabilize Windows managed install checks

* test(installer): relax Windows installer timeout

* fix(test): escape release asset regex

* test(cli): avoid POSIX node path in relaunch test

* fix(installer): align npm fallback node gate with engines

* test(installer): allow Windows archive validation more time

* fix(installer): remove stale node 20 installer references

* docs(installer): clarify hosted endpoint sync requirement

* refactor(installer): reuse standaloneArchiveName in release verifier

The verify-installation-release script was duplicating the archive name
derivation logic with a hardcoded ternary instead of reusing the
standaloneArchiveName helper from build-standalone-release. Export the
helper and import it so the extension mapping lives in one place.

* fix(scripts): address release verifier review feedback

* feat(installer): add standalone archive installer with multi-platform release workflow

- Add standalone archive installer (bat/sh) that downloads platform binaries
  from GitHub/Aliyun without requiring Node.js or npm on the target machine
- Add fork-friendly release-test workflow for manual GitHub Release creation
  covering all 5 platforms (darwin-arm64/x64, linux-arm64/x64, win-x64)
- Add OSS upload/mirror tools for staging and release distribution
- Update .gitignore to exclude generated build artifacts (release-staging/,
  hosted-staging/)
- Fix Windows PowerShell test command in copy-release-to-latest tool

* feat(installer): support QWEN_INSTALL_GITHUB_REPO env var for custom repo

* chore(installer): exclude local-only staging tools from PR

The tools/ directory contained personal staging-OSS upload helpers
(upload-staging, upload-release-mirror, copy-release-to-latest,
test-upload-one) that should not ship in the public PR. They reference
a personal staging bucket and only exist to validate the installer
end-to-end before production release.

Removes them from git tracking via `git rm --cached` (files stay on
disk for the author's local use) and adds /tools/ to root .gitignore
so they cannot be re-added accidentally.

No runtime / installer code change. Production CI on ubuntu-latest is
unaffected.

* fix(installer): enforce CRLF line endings for .bat files via gitattributes

cmd.exe requires CRLF in batch scripts; the global eol=lf was causing
every line to be misparsed on Windows, producing errors like
'QWEN_VALIDATE_METHOD=detect is not recognized as a command'.

* fix(installer): store .bat files with CRLF in git blob for raw GitHub downloads

GitHub raw file serving bypasses gitattributes eol conversion and serves
blob bytes directly, so eol=crlf alone was not enough. Use -text to disable
normalization and commit with actual CRLF so raw downloads work on Windows.

* fix(installer): follow HTTP redirects in UrlExists and RaceMirrorHead probes

GitHub release asset URLs return HTTP 302 to objects.githubusercontent.com.
[Net.WebRequest] with HEAD does not auto-redirect by default, so the
existence check and mirror-race probe both incorrectly reported the file
as missing. Set AllowAutoRedirect=true on HttpWebRequest instances.

* fix(installer): surface download errors and add MaximumRedirection 10

* feat(installer): add hosted install-qwen.ps1 shim for irm|iex one-liner

The previous Windows quick-install one-liner used `Invoke-WebRequest -OutFile
(Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path …)`. When pasted into a
narrow terminal, line wrap could land on `-OutFile`, orphaning the parameter
from its value and producing the "missing argument for OutFile" failure
followed by a "file not found" when the second `&` ran. PowerShell's line
continuation rules cannot resolve this for parameter-name-at-EOL.

Add `install-qwen.ps1` as a thin hosted entrypoint that downloads
`install-qwen.bat` into TEMP, runs it, and cleans up. Documented one-liner
becomes the standard pattern used by bun, uv, scoop, deno, pnpm:

    powershell -ExecutionPolicy Bypass -c "irm <url>/install-qwen.ps1 | iex"

The `.bat` remains the source of truth for installer behavior; `.ps1` is just
the modern hosted entrypoint. Version pinning via `$env:QWEN_INSTALL_VERSION`
flows through unchanged. Stored with `*.ps1 -text` so CRLF survives both
GitHub raw and OSS uploads, matching the existing `.bat` handling.

* fix(installer): stage direct hosted install scripts

* chore(installer): trim hosted release diff scope

* chore(installer): narrow hosted release diff

* feat(installer): restore hosted PowerShell entrypoint

* chore(installer): stage standalone hosted entrypoints

* fix(installer): address hosted installer review followups

* fix(installer): stabilize Windows installer tests

* fix(installer): make Windows option validation readable

* feat(installer): wire Aliyun OSS sync, address review followups

- Add Aliyun OSS sync steps to release workflow: package hosted assets,
  install pinned ossutil, configure credentials, upload versioned and
  latest paths, and verify upload via verify:installation-release plus
  curl probes against the hosted installer endpoint.
- Document required production-release environment secrets and bucket
  variables in INSTALLATION_GUIDE.md.
- Restructure hosted endpoint guidance to lead with the pre-sync
  warning, splitting "Run today" (local checkout) from "After the OSS
  sync" (hosted one-liners) so users no longer copy a one-liner that
  silently installs latest.
- Distinguish mirror auto-selection timeout from successful selection
  in install-qwen-standalone.sh and install-qwen-standalone.bat: emit
  a "timed out; defaulting to github" log instead of pretending the
  HEAD probe picked github.
- Support QWEN_INSTALLER_BAT_URL override (https only) in the
  PowerShell shim so staging mirrors can be exercised without forking
  the file.
- Strip a leading UTF-8 BOM in verify-installation-release.js
  parseSha256Sums so BOM-prefixed SHA256SUMS reports a useful
  "Missing checksum entry" error instead of "Malformed SHA256SUMS
  line 1".
- Add tests for verifier HEAD→Range fallback, partial-failure
  formatting, all-failure wording, and BOM tolerance.

* ci(installer): add temporary OSS smoke test

* fix(installer): make OSS release assets public-readable

* chore(installer): remove temporary OSS smoke workflow

* fix(installer): address hosted installer review gaps

* feat(installer): refactor argument parsing and utility functions for release scripts

* fix(installer): harden hosted release script checks

* fix(installer): suppress PowerShell progress bar in hosted entrypoint shim

Add $ProgressPreference = 'SilentlyContinue' to the .ps1 wrapper so
Invoke-WebRequest downloads don't render a progress bar when invoked
via the irm | iex one-liner.

* fix(installer): suppress PowerShell progress bar in bat installer downloads

Add $ProgressPreference = 'SilentlyContinue' to DownloadFile so the
full-screen progress UI does not appear during archive downloads in
interactive PowerShell sessions, consistent with the .ps1 shim.

* fix(installer): use curl.exe -# progress bar in Windows downloads

Prefer curl.exe with -# (hash-mark progress bar) for archive and installer
downloads on Windows 10+. Falls back to Invoke-WebRequest (which shows its
own progress bar) when curl.exe is unavailable. Matches the approach used
by code-server (curl -#fL) and bun.sh (curl.exe -#SfLo).

* fix(installer): suppress progress bars for small downloads and Expand-Archive

- .ps1: replace curl.exe -# with silent mode, suppress Invoke-WebRequest
  progress bar; save/restore $global:ProgressPreference
- .bat: add $ProgressPreference = 'SilentlyContinue' before Expand-Archive
  to prevent full-screen extraction progress UI
- .sh: remove --progress-bar / --show-progress from download_file, always
  use silent curl/wget

* fix(installer): auto-backup non-qwen directories and simplify output

- ensure_managed_install_dir / :EnsureManagedInstallDir now back up
  non-qwen directories instead of refusing to install, so users
  upgrading from npm or old installers don't hit a hard error
- Simplify header/footer output: remove banner bars, verbose INFO
  lines, and redundant "Installation completed!" message
- Match bun.sh / code-server style: minimal, to the point

* fix(installer): revert Expand-Archive progress suppression in bat

The inline $ProgressPreference = 'SilentlyContinue' caused a cmd.exe
parsing error ("此时不应有 >") on Chinese Windows. Revert to the
original Expand-Archive invocation.

* fix(installer): fix cmd.exe parsing error in backup fallback code

The %s in the for /f fallback command string was interpreted as a variable
reference by cmd.exe, causing "此时不应有 >" on Chinese Windows. Replace
with a safe fallback and re-enable Expand-Archive progress suppression.

* fix(installer): always persist install bin to user PATH

Previously MaybeUpdateUserPath was only called when shadow qwen
executables were detected. When no shadow was found, the PATH update
was skipped entirely, leaving the user without qwen on PATH after
restarting their terminal.

Now always persist the bin directory to PATH (unless --no-modify-path
is set), regardless of whether other qwen installations exist.

* fix(installer): persist PATH to current terminal session on Windows

Use the `endlocal & set` trick (same as bun/Rust installers) to export
the install bin directory from the setlocal scope to the current cmd
session. qwen is now usable immediately without restarting the terminal.

* docs(installer): document cmd.exe one-liner for immediate PATH availability

Add curl-based one-liner for cmd.exe users. Running the .bat directly
in the current cmd session makes `qwen` available immediately via the
`endlocal & set` trick. The `powershell -c "irm | iex"` path creates
a child process so PATH changes cannot propagate to the parent.

* feat(installer): make qwen usable immediately from PowerShell after install

- .ps1: detect parent process, update current session PATH, and for
  cmd.exe parents emit a `set PATH=...` command
- .bat: skip final instructions when called from PowerShell to avoid
  duplicate "Run: qwen" output

* fix(installer): remove non-functional doskey approach for cmd parent

doskey /exename from a child PowerShell process cannot modify the
parent cmd.exe session. Replace with a simple set PATH=... command
that the user can copy-paste.

* fix(installer): make Windows standalone shim available in cmd

* feat(installer): add standalone uninstall scripts

* fix(uninstall): match shell-quoted paths when removing the wrapper

The installer's write_unix_wrapper shell-quotes the binary path, so
paths containing single quotes (or other shell metacharacters) appear
as shell-quoted strings in the generated wrapper file. The uninstall
script's literal grep -qF missed these, leaving the wrapper orphaned.

Add shell_quote to the uninstall script and match against both the raw
and shell-quoted forms before removing the wrapper.

* fix(installer): update download commands to use progress indicators for curl and wget

* fix(installer): resolve Aliyun latest via version pointer

* fix(installer): cleanup mirror probe temp dirs

* fix(installer): harden standalone release fallback

* fix(installer): address standalone review feedback

* style(installer): align standalone install output

* fix(installer): print standalone uninstall commands

* fix(installer): address release review follow-ups

* fix(installer): harden Windows target detection

* test(installer): stabilize Windows fake tool path

* fix(installer): allow explicit Windows curl path

* test(installer): use cmd fake curl on Windows

* test(installer): cover Windows fake curl helper

* test(installer): inject Windows arch overrides in cmd

* test(cli): wait for prompt suggestion render

* test(cli): revert prompt suggestion wait tweak

* fix(installer): harden hosted release publishing

* fix(installer): harden Windows latest pointer parsing

* fix(installer): bound Windows download timeouts

* fix(installer): bound hosted installer probes

* fix(release): make ossutil download configurable

* fix(installer): address hosted release review feedback

* test(installer): keep dist backup on same filesystem

* fix(installer): address remaining review feedback on PR #3828

- Remove REQUIRE_CHECKSUM dead code, always hard-fail on checksum issues
- Add JSDoc to HOSTED_INSTALLER_BEHAVIOR_PATTERNS explaining its purpose
- Add credential cleanup trap for ossutilconfig in release workflow
- Add 3-attempt retry with exponential backoff for OSS uploads
- Tighten findstr SOURCE regex to require leading letter

* fix(release): correct OSS credentials lifetime and mirror probe fallback

- release.yml: remove `trap EXIT` inside the Configure step; it deleted
  ${RUNNER_TEMP}/.ossutilconfig as soon as the configure shell exited,
  so every subsequent step (publish/sync/verify) lost the credentials.
  Move credential cleanup to a final `if: always()` step at the job tail.
- install-qwen-standalone.sh: drop the predictable PID-based mktemp -d
  fallback in race_mirror_head; if mktemp fails, return "github" instead
  of using /tmp/qwen-mirror.$$ which a local attacker could pre-create
  to bias mirror selection.

* fix(installer): address review feedback round 2

Workflow:
- Move 'Publish Aliyun OSS Latest VERSION' to run after the hosted installer
  assets are uploaded and verified, so the latest/VERSION pointer only flips
  once every release artifact is in place. Previously a hosted-sync failure
  could leave the pointer ahead of the actual installer scripts.

upload-aliyun-oss-assets.js:
- Replace `spawnSync('sleep', ...)` retry backoff with an Atomics.wait-based
  cross-platform sleep so retries also work on Windows runners.

install-qwen-standalone.bat:
- :DetectTarget no longer emits TARGET=win-arm64 because RELEASE_TARGETS has
  no win-arm64 archive; ARM64 hosts now fall through to the unsupported-arch
  branch and (in detect mode) get the npm fallback instead of a 404.
- Add QWEN_INSTALL_CURL_EXE to :ValidateRawEnvironmentOptions so this curl
  override is checked for shell metacharacters like every other knob.
- Replace `call echo %%i>>...` with plain `echo %%i>>...` when capturing
  pre-install qwen.cmd paths; `call` triggered an extra parse pass that
  could interpret &/|/<,>/etc. inside a directory name as command separators.
- Add `--retry 2` to curl.exe downloads (`:DownloadFile` / `:DownloadFileQuiet`)
  to match the shell installer.
- Include expected vs actual hash in the checksum-mismatch error message.

install-qwen-standalone.ps1:
- Stage the downloaded installer at a cryptographically random temp path
  (`qwen-installer-<random>.bat`) so a same-user attacker cannot pre-stage a
  malicious .bat at a predictable path and race the verify/execute window.
- Atomically install the current-session cmd shim by writing to a sibling
  `.new` temp file then renaming, so a partial write cannot leave a
  half-written shim on PATH.
- Add `--retry 2` to the curl.exe download path.
- Include expected vs actual hash in the checksum-mismatch error message.

install-qwen-standalone.sh:
- Include expected vs actual hash in the checksum-mismatch error message.

uninstall-qwen-standalone.ps1:
- Accept `-Purge` and `-Help` parameters; previously every CLI flag was
  silently dropped, so users running with `-Purge` got no purge and no error.
  `-Purge` maps to `QWEN_UNINSTALL_PURGE=1`.

uninstall-qwen-standalone.sh:
- `remove_install_wrapper` additionally requires the wrapper file to start
  with a `#!` shebang before it deletes it; a user-authored script that just
  happens to mention the install path now stays untouched.

verify-installation-release.js, build-hosted-installation-assets.js:
- Include expected vs actual hash in the checksum-mismatch error messages.

scripts/tests/install-script.test.js:
- Update assertions for the new error wording, the curl `--retry 2` flag,
  the dropped ARM64 detection, and the new release-step ordering.

* fix(installer): address review feedback round 3

Workflow:
- Configure Aliyun OSS Credentials: write the ossutil config file directly
  with restricted umask instead of invoking `ossutil config -k <secret>`.
  Passing the access-key secret via argv made it visible in /proc/<pid>/cmdline
  for the lifetime of that step; writing the INI file in-process keeps the
  secret out of the process table.

upload-aliyun-oss-assets.js:
- Upload assets in parallel with `Promise.all` + async `spawn` instead of a
  sequential `spawnSync` loop. Each asset keeps its own retry budget; failures
  are aggregated so one flaky upload does not mask a separate failure.
- Replace the bespoke `Atomics.wait` retry sleep with `timers/promises#setTimeout`
  now that the loop is async.

INSTALLATION_GUIDE.md:
- Drop the misleading "instead of overwriting the global installation/
  entrypoint objects" sentence; the workflow has always also refreshed the
  global versionless objects so curl|bash links keep resolving without a
  version segment. Document the rollback story instead.

* test(installer): add parseUploadArgs unit tests and align verify derivation

- scripts/tests/upload-aliyun-oss-assets.test.js: cover --help short-circuit,
  required-option validation (--bucket/--config/--prefix/empty assets),
  unknown options, missing option values, and trailing-slash prefix
  normalization.
- scripts/verify-installation-release.js: switch the win-only zip branch
  from `startsWith('win-')` to the strict `=== 'win-x64'` check used by
  build-standalone-release.js, and add a comment recording that the two
  derivations must stay aligned. Without this the helpers would diverge
  the moment a non-x64 win target gets added.

* test(installer): add uploadAssets integration tests with fake ossutil

Add two integration tests that route a temp-directory ossutil shim onto
PATH so uploadAssets actually spawns the real binary with the real cp
argv:

- happy-path test asserts the destination URI, `-c <config>`, `--acl
  public-read`, and per-asset cp invocations land for both inputs.
- failure-path test asserts non-zero ossutil exits surface as an
  aggregate `asset uploads failed` error after the retry budget runs out.

* revert(installer): drop over-engineered ossutil/upload changes

Roll back two changes from a1ef8697b/0a5d308c9 that were not justified
by the actual threat model or release-pipeline needs:

- .github/workflows/release.yml: restore the supported `ossutil config -k`
  invocation. The earlier switch to writing the .ossutilconfig INI file
  in-process was meant to keep the access-key out of /proc/<pid>/cmdline,
  but GitHub-hosted runners are single-tenant ephemeral VMs where no other
  user can read that namespace. The benefit was theoretical; the cost was
  taking on a brittle dependency on ossutil's undocumented config format.

- scripts/upload-aliyun-oss-assets.js: revert the uploadAssets parallel
  rewrite (Promise.all + spawn + setTimeout) back to the original sync
  spawnSync loop with retry. Release-time uploads of ~6 small files do
  not need parallelism, and the async refactor changed the public
  contract (sync→async) for no real wall-clock win.

Kept from those commits:
- The cleanup `if: always()` step that removes RUNNER_TEMP/.ossutilconfig
  at the end of the publish job.
- The cross-platform sleepSync(ms) helper, since `spawnSync('sleep', ...)`
  still does not work on Windows runners.
- The INSTALLATION_GUIDE.md doc fix.
- All other round-2 fixes.

Test assertions updated for the restored sync uploadAssets contract.

* test(installer): cover Windows release script regressions

* test(release): avoid Windows shim lookup in oss upload tests

* test(installer): use stable fake Aliyun version on Windows

* fix(installer): parse Aliyun latest version in batch

* fix(installer): validate Aliyun latest version without findstr

* fix(installer): normalize Aliyun latest version via PowerShell

* fix(installer): avoid captured PowerShell output in batch latest parsing

* fix(installer): normalize Aliyun latest pointer from file

* test(installer): fix fake Windows curl output parsing

* fix(installer): print checksum path on miss, gate hardcoded version pin in ps1 [skip ci]

Address two narrow follow-ups from PR #3828 review:

- build-hosted-installation-assets.js: add a HOSTED_INSTALLER_FORBIDDEN_PATTERNS guard for install-qwen-standalone.ps1. The ps1 shim has no VERSION variable of its own (it forwards @args to the .bat), so the existing default-version positive-match patterns don't apply. The new guard fails the build if a $env:QWEN_INSTALL_VERSION assignment or a --version flag prepended to the forwarded argument list ever lands in the shim. Patterns are line-anchored with /m so the documented usage examples in the header docstring stay valid. Two vitest cases cover the reject and allow paths.

- install-qwen-standalone.sh / .bat: include the searched checksum-file path in the "SHA256SUMS not found" error. Operators triaging --archive failures could not tell from the prior message whether the fallback path (next to the archive) or the remote URL was being looked up. Existing test assertions updated to match the new wording.

Local validation: npm run test:scripts -> 160 passed | 9 skipped (was 158 | 9).

* fix: stamp release version in hosted installers and add Zip Slip protection [skip ci]

1. The hosted installation asset build now accepts --version and stamps it
   into the copied .sh/.bat installers so they default to the tagged release
   version instead of 'latest'. The release workflow passes the version.

2. install-qwen-with-source.bat now validates archive entries before calling
   Expand-Archive, rejecting paths with '..', leading '/', drive-rooted
   paths, empty names, or control characters — matching the protection
   already present in install-qwen-standalone.bat and the .sh installer.

* fix(installer): add SOURCE to PowerShell unsafe-character validation [skip ci]

The SOURCE variable is user-provided and used in path operations but was
not included in the :ValidateOptions unsafe-character check. Add it
alongside the other validated variables.

* fix: correct copyright year 2025 -> 2026 in new files [skip ci]

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: yiliang114 <effortyiliang@gmail.com>
2026-05-21 11:57:10 +08:00
pomelo
a552df8998
refactor(auth): unify provider config in core, simplify /auth as "Connect a Provider" (#4287)
* refactor(providers): unify provider config into core, remove CLI re-exports

Move all ProviderConfig definitions, registry (ALL_PROVIDERS), and
utility functions (buildInstallPlan, resolveBaseUrl, etc.) from
packages/cli/src/auth/ into packages/core/src/providers/ so both
CLI and VSCode can share the same provider system.

- Add core providers module with types, presets, install logic
- Rewrite VSCode AuthMessageHandler to dynamically generate provider
  choices from ALL_PROVIDERS instead of hardcoding 3 providers
- Add applyProviderInstallPlanToFile in VSCode settingsWriter using
  the ProviderSettingsAdapter abstraction
- Delete 11 CLI re-export wrapper files, update ~20 import sites
- Keep CLI-specific applyProviderInstallPlan (uses LoadedSettings)
  and openrouterOAuth.ts (CLI-only OAuth runtime)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(cli): drop OpenRouter OAuth + /manage-models, simplify /auth

OpenRouter now uses the standard API-key flow under "Third-party Providers"
(issue #4108). The whole OpenRouter OAuth implementation (PKCE, callback
server, model auto-install) and the /manage-models command (only OpenRouter
was wired in; /auth Step 2 already covers model selection) are removed.

/auth is renamed around the "Connect a Provider" mental model:
- Dialog title is now "Connect a Provider"; the OAuth main entry is gone
- handleAuthSelect (mixed close + auth trigger) is split into a single-purpose
  closeAuthDialog; legacy wrappers (handleSubscriptionPlanSubmit,
  handleApiKeyProviderSubmit, handleCustomApiKeySubmit, ...) are dropped in
  favor of the unified handleProviderSubmit

Core: openRouterProvider switches to authMethod='input', uiGroup='third-party',
ships with two recommended free models, and is reordered to the end of the
third-party list to keep DeepSeek as the default highlight.

Net diff: 34 files, +124 / -3835.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(auth): unify applyProviderInstallPlan in core, drop cli/auth

CLI and vscode now share core's applyProviderInstallPlan instead of keeping
two parallel implementations. The CLI-only env rollback (snapshot
process.env, restore on error) is folded into the core version so vscode
also benefits from it.

CLI ships a LoadedSettingsAdapter that maps LoadedSettings to core's
ProviderSettingsAdapter contract. Backup/restore is layered: write a .orig
file, structuredClone settings + originalSettings, then recomputeMerged()
on restore — same guarantees as before, just routed through the adapter.

Tests for the install logic are migrated to core and rewritten against the
adapter mock (more focused than the previous LoadedSettings/Config mocks).

packages/cli/src/auth/ is gone entirely.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(providers): drop unused authMethod field from ProviderConfig

Every preset has had authMethod='input' since OpenRouter switched to the
standard API-key flow, making the field a dead dimension. Removing it
cleans up three never-taken branches and aligns the type with reality:
connecting a provider always means entering an API key.

- core: remove ProviderConfig.authMethod; shouldShowStep('apiKey') is
  now unconditionally true; drop authMethod from 9 presets
- vscode AuthMessageHandler: drop the OAuth branch in handleAuthInteractive
- vscode WebViewProvider: simplify the apiKey-required guard
- tests: update provider-config.test and custom-provider.test

If a future provider needs a browser-based flow, the field can be
re-introduced; for now the smaller surface is worth more.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(providers): prefix Alibaba plan presets with alibaba-

Rename coding-plan.{ts,test.ts} → alibaba-coding-plan.{ts,test.ts} and
token-plan.{ts,test.ts} → alibaba-token-plan.{ts,test.ts} so the file
names line up with the existing alibaba-standard preset and make it
obvious at a glance which presets belong to Alibaba ModelStudio.

Export names (codingPlanProvider, tokenPlanProvider, TOKEN_PLAN_*,
CODING_PLAN_*) are unchanged — only the file paths and the two
imports in all-providers.ts / index.ts move.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(vscode): guard ProviderSettingsAdapter against prototype pollution

The dotted-key writer in createFileSettingsAdapter walked through any
segment, including __proto__/constructor/prototype, which would let a
malicious or malformed ProviderInstallPlan reach Object.prototype.

Refuse to write paths containing reserved segments and use
hasOwnProperty when traversing intermediate objects so that inherited
properties cannot redirect the walk.

Addresses CodeQL alert #226 surfaced on PR #4287.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(auth): default Audio modality to off in provider advanced config

In the /auth Custom Provider advanced-config step, "Enable modality"
should default to Image + Video only. Audio was on by default, which
implied the model accepts audio input even though most providers
people configure here don't.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(auth): show base URL default as placeholder, not prefilled value

In Custom Provider Step 2/6 (and on protocol switch), the base URL
input started with the protocol's default URL pre-filled. Users who
wanted a non-default endpoint had to manually clear the field first.

Switch to placeholder semantics: the input starts empty, the default
URL is shown as a hint, and submitting blank falls back to that
default (then writes it back to baseUrl so downstream steps see a
real value).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(cli): rename /auth description to "Connect an LLM provider"

The old description ("Configure authentication information for login")
implied a Qwen-account login. After the /auth refactor it's really
about picking an LLM provider and entering credentials, so the menu
entry should say that.

Also add 'connect' as an alt-name alongside the existing 'login' so
users can type /connect when 'auth' feels wrong. Keep 'login' for
muscle-memory compatibility.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* i18n(cli): translate "Connect an LLM provider" in all locales

Strict-parity locales (zh, zh-TW) require every built-in command
description to be translated; the renamed /auth description was
falling back to English and breaking the must-translate test.

Add translations for zh / zh-TW (required) and refresh the other
seven locales (en, ru, de, ja, fr, ca, pt) so the old
"Configure authentication information for login" key is removed
everywhere rather than left as a dangling dictionary entry.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(vscode): await applyProviderInstallPlanToFile and grow test coverage

Critical: applyProviderInstallPlanToFile fired the install plan with
`void`, so any rejection (EACCES from persist(), prototype-pollution
guard throw, etc.) was silently swallowed and WebViewProvider proceeded
to disconnect/reconnect the agent as if the write had succeeded.
Make the wrapper `async` and `await` it in the only caller.

Tests added:
- core/install.test: isSameModelIdentity fallback path
  (prepend-and-remove-owned with no ownsModel) — verifies models are
  matched on id+baseUrl, not just id.
- vscode/AuthMessageHandler.test: happy-path with a fixed-baseUrl
  third-party provider, validateApiKey error branch, and BaseUrlOption
  picker presentation.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(auth): address PR #4287 review (critical + suggestion)

vscode AuthMessageHandler (Critical):
- Add the missing protocol-selection step so custom-provider users can
  pick Anthropic/Gemini instead of being silently locked to OpenAI.
- Validate free-form base URL with the same /^https?:\/\// check the
  CLI uses; reject file:/javascript: schemes.

vscode AuthMessageHandler (Suggestion):
- Stop filtering separator entries from the provider QuickPick so
  groups (Alibaba Cloud / Third Party / Custom) actually show as
  headers instead of a flat list.
- Treat a null authInteractiveHandler as an error: surface an
  authError + cancellation notification instead of silently dropping
  the user's input.
- Call notifyAuthCancelled when validateApiKey rejects so the
  webview state resets and the user can retry.

core/providers/presets/openrouter.ts (Critical):
- Replace the substring includes() in ownsModel with a URL-hostname
  match so paths like https://api.example.com/openrouter.ai/v1 stop
  being misidentified as OpenRouter models (and getting removed on
  re-install).

vscode/services/settingsWriter.ts (Critical):
- stripTrailingCommas() so JSONC files with trailing commas (VSCode's
  default style) parse instead of silently returning {} and then
  overwriting the entire settings file.
- readSettings() distinguishes ENOENT (return {}) from parse errors
  (log + rethrow) so a malformed file never gets clobbered.
- writeSettings() writes through a temp file + fs.renameSync atomic
  rename, eliminating the half-written file window on EACCES /
  disk-full / crash.
- setValue() refuses to overwrite a scalar at an intermediate path
  segment (would have silently destroyed e.g. {"env": "legacy-string"}).

core/providers/install.ts (Suggestion):
- Move settings.backup?.() inside the try block so a backup failure
  still triggers the env-rollback path in catch.

cli/config/loadedSettingsAdapter.ts (Suggestion):
- Add the same UNSAFE_KEY_PARTS guard the vscode adapter has, so
  __proto__/constructor/prototype segments are rejected before
  reaching the underlying setNestedPropertySafe walker. Defense in
  depth: not exploitable today but the utility has no built-in guard.

vscode/webview/providers/WebViewProvider.ts (Suggestion):
- Hoist buildInstallPlan / applyProviderInstallPlanToFile to static
  imports (both modules already top-level imported); drops two
  per-call await import() round-trips.

cli/utils/doctorChecks.ts (Suggestion):
- Whitespace nit before the comma in the qwen-code-core import.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(auth): second round of PR #4287 review fixes

Critical:
- settingsWriter: stripTrailingCommas now uses a char-by-char scanner so
  literal ",]" inside a string value is preserved (the previous regex
  silently corrupted it).
- install.ts: wrap settings.restore() in try/catch so a restore failure
  doesn't mask the original error or skip the env-rollback loop.
- install.ts: snapshot the runtime ModelProvidersConfig before applying
  patches and reload it in the catch path, so an in-flight refreshAuth()
  failure doesn't leave the live session holding providers that were
  never successfully installed.
- AuthMessageHandler: custom-provider Base URL is now a placeholder
  instead of a pre-filled value, with the default selected by the
  user's chosen protocol (openai/anthropic/gemini). Empty input falls
  back to the protocol-appropriate URL, preventing the
  pick-Anthropic-but-keep-OpenAI-URL footgun.

Suggestion:
- AuthDialog: replace the isCurrentlyCodingPlan misnomer with a uiGroup
  check — resolveMetadataKey returns config.id for *any* provider with
  a static models[], so the old guard made DeepSeek/MiniMax/OpenRouter
  users land on the Alibaba tab instead of Third-party Providers.
- AuthMessageHandler: guard against modelIds being [] after splitting
  comma input (matches the CLI's "Model IDs cannot be empty.").
- WebViewProvider: restore the explanatory comment for the
  authState === true success-toast guard that the previous diff
  accidentally dropped.

Tests:
- settingsWriter.test: new applyProviderInstallPlanToFile suite covering
  happy path, prototype-pollution guard (built via Object.defineProperty
  to bypass __proto__ literal semantics), intermediate-scalar rejection,
  malformed-file no-clobber, JSONC-with-trailing-commas parsing
  (including a string containing ",]"), and the atomic-write tmp-file
  cleanup.
- loadedSettingsAdapter.test: new file — forwarding, UNSAFE_KEY_PARTS
  rejection, getValue against merged settings, backup/restore round-trip,
  cleanupBackup semantics.
- provider-config.test: added findProviderByCredentials and
  getAllProviderBaseUrls coverage (preset hits, unknown-key misses,
  BaseUrlOption[] preset expansion).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): satisfy strict tsc --build in loadedSettingsAdapter.test

CI's `tsc --build` (with emit) enforced two strict checks that
`tsc --noEmit` had been letting through:

- `noPropertyAccessFromIndexSignature` flagged `file.settings['env']`
  reads against `Record<string, unknown>`. Switched the test fixture
  shape to a named `SettingsShape` interface with explicit `env` and
  `modelProviders` keys (plus an index signature for setValue's
  arbitrary writes), so dot access on the known keys is no longer
  "through" the index signature.
- Calling optional methods via `adapter.backup?.()` produced TS2722
  (`Cannot invoke an object which is possibly 'undefined'`) under the
  build flags. createLoadedSettingsAdapter always installs
  backup/restore/cleanupBackup, so the tests now assert
  `toBeTypeOf('function')` first and then call via non-null assertion,
  which both documents the invariant and makes the call typesafe.
- Dropped the `({} as Record<string, unknown>)['polluted']` sanity
  check; `expect(setValue).not.toHaveBeenCalled()` already proves the
  guard short-circuits before any write reaches LoadedSettings.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): guard mock setValue against prototype pollution in adapter test

CodeQL flagged the mock setValue's recursive property assignment as a prototype-pollution sink. Add UNSAFE_KEY_PARTS check at the top of the mock to align with the real setNestedPropertySafe contract.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): use literal === guards for CodeQL prototype-pollution sanitiser

CodeQL re-flagged the mock setValue write even after the Set.has guard added in 2e6adf8a6d — the scanner only recognises inline literal === comparisons as prototype-pollution sanitisers, not Set lookups.

Reworked the mock to (1) merge the guard into the loop so every current[part] write is preceded by a literal === check against '__proto__'/'constructor'/'prototype', and (2) collapse the dual leaf/branch logic into a single loop body. Runtime behaviour is identical; CodeQL should now treat the write as sanitised.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(auth): third round of PR #4287 review fixes (8 comments)

Critical:
- useAuth: handleProviderSubmit now calls setPendingAuthType at the start
  of the try, so handleAuthFailure can record the AuthEvent telemetry on
  applyProviderInstallPlan rejection (previously dropped silently because
  pendingAuthType was undefined).
- settingsWriter: readQwenSettingsForVSCode wraps readSettings in
  try/catch so a malformed settings.json no longer crashes the VSCode
  extension on activation; the write paths (writeCodingPlanConfig,
  writeModelProvidersConfig) deliberately keep propagating to avoid
  silently overwriting a corrupt file with partial data.

Suggestions:
- settingsWriter.setValue: intermediate-segment guard now also rejects
  arrays (typeof [] === 'object' previously slipped through and would
  let us set string keys on an array). Loop restructured so the
  literal-=== prototype-pollution guard runs at every step, satisfying
  CodeQL's sanitiser detector on both the leaf and intermediate writes.
- settingsWriter atomic write: SETTINGS_FILE_MODE = 0o600 +
  SETTINGS_DIR_MODE = 0o700 + best-effort chmod on existing files. API
  keys persisted into env.* are no longer world-readable on multi-user
  systems.
- loadedSettingsAdapter: switched its prototype-pollution guard to the
  same inline literal === pattern so the two adapters stay symmetric
  and CodeQL recognises both as sanitisers (Comment 6 — explicit
  'keep in sync' comment + same shape rather than a shared helper that
  CodeQL wouldn't trace through).
- AuthMessageHandler: protocol QuickPick now shows 'OpenAI Compatible'
  / 'Anthropic' / 'Gemini' instead of the raw AuthType enum values.
- WebViewProvider: authInteractive log now records only the parsed
  hostname, not the full inputs.baseUrl, so credentials embedded in
  userinfo or query strings don't leak into extension-host logs.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(auth): cover the rollback safety nets in applyProviderInstallPlan + useAuth failure path

Addresses the missing-coverage points in the latest review pass: every deliberately-engineered rollback path in install.ts and the visible side effects of handleAuthFailure now have a regression test, so a future refactor that 'simplifies' these paths can't silently break them.

applyProviderInstallPlan (install.test.ts, +4 cases):
- restores runtime model providers when refreshAuth rejects after
  reloadModelProviders ran (asserts the second reloadModelProviders call
  receives the pre-install snapshot).
- still rolls back env vars when backup() throws before persist (pins
  the 'backup inside try' invariant added in 38a214d0ec).
- continues env rollback even when settings.restore itself throws
  (pins the nested try/catch around restore added in 38a214d0ec).
- continues throw + env rollback when the rollback-time
  reloadModelProviders itself throws (the original error must still
  surface; env vars must still revert).

useAuth (useAuth.test.ts, +1 case):
- surfaces install-plan rejection as an auth error and records
  telemetry — refreshAuth throws, the test asserts authError is set,
  the dialog reopens, isAuthenticating clears, no success toast is
  added, and pendingAuthType is populated (which is what the new
  setPendingAuthType call lets handleAuthFailure key the AuthEvent on).
- createSettings now mocks recomputeMerged + forScope.settings so the
  loaded-settings-adapter restore() path doesn't emit a noisy stderr.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(auth): fourth round of PR #4287 review fixes

Critical:
- settingsWriter JSONC scanner: \uXXXX is a 6-char escape, not 2.
  The previous stripJsonComments / stripTrailingCommas used j+=2 for
  every backslash, so a value containing \u0022 would let the embedded
  quote terminate the string early — turning a single string value into
  multiple top-level keys after the strip passes. That's a parser
  differential vs JSON.parse and enables settings.json key injection
  (e.g. an attacker-controlled API_KEY string could inject env.NODE_OPTIONS).
  Now we branch on text[j+1] === 'u' and skip 6, satisfying both scanners.
- resolveBaseUrl no longer crashes on an empty baseUrl array. The
  previous config.baseUrl[0].url threw 'Cannot read undefined.url' on []
  and brought down the whole install flow. Falls back to selectedBaseUrl
  or '' instead.
- providerMatchesCredentials now resolves function-typed envKey by
  calling it with (protocol, baseUrl). The previous typeof-string gate
  made the custom provider invisible to findProviderByCredentials —
  /doctor and system-info diagnostics couldn't see custom-provider users.
  Catches the function call so a misbehaving custom envKey can't crash
  the matcher.

Suggestions:
- AuthDialog: defaultMainIndex now also returns 2 for uiGroup === 'custom'
  so a custom-provider user lands on the Custom Provider tab instead of
  Alibaba ModelStudio.
- install.ts: env-var rollback loop is now wrapped in try/catch matching
  the same shape as the settings.restore() and reloadModelProviders
  rollbacks. A process.env write throwing (custom property descriptors,
  some sandboxes) won't skip the runtime-providers rollback below.
- readSettings: SyntaxError is now wrapped in an actionable Error
  ('Cannot parse ~/.qwen/settings.json ($name: $message). Standard
  JSONC is supported... Please fix or delete $path...') so users facing
  a corrupt file get a clear message instead of a bare SyntaxError. The
  cause is preserved via Error.cause.

Tests:
- settingsWriter: new \u0022 injection regression — asserts that a
  string containing \u0022 stays a single string and no injected key
  lands at the top level.
- provider-config: new edge-case suite for resolveBaseUrl with [] and
  providerMatchesCredentials with function-typed envKey (matching path,
  wrong-key path, function-throws path). Re-imports via the relative
  source path so the new behaviour is exercised even before dist/ is
  rebuilt.

Not addressed:
- handleProviderSubmit error-path test (Comment 3264567491) was already
  added in 7d8b4785ad — same test, same surface (refreshAuth rejection
  + authError set + dialog reopen + isAuthenticating false + no success
  toast + pendingAuthType populated).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(vscode): import AuthType as value not type

AuthMessageHandler now references AuthType.USE_OPENAI etc. as enum values (for the protocolLabels map added in cdc17cbba0), but the import was 'import type AuthType' which strips the runtime binding. TS1361 fired in CI's emitting build even though --noEmit was happy locally.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(providers): restore modelscope test + tighten openrouter ownsModel

Two findings from the latest /review pass that survived earlier rounds:

1. modelscope.test.ts was deleted in the move-from-CLI step (60 lines / 4 cases under packages/cli/src/auth/providers/thirdParty/) but never recreated in core's preset test folder. Re-added a 3-case suite (config shape, install plan with per-model metadata for known IDs, graceful fallback for unknown IDs) so the third-party preset coverage is symmetric again. Also exported modelscopeProvider from packages/core/src/providers/index.ts so the public API matches the other presets.

2. openrouter.ts ownsModel previously claimed any model on an openrouter.ai hostname, which would silently delete a user's hand-added entry that happened to route through openrouter.ai under a different envKey (e.g. a personal gateway). Now requires both model.envKey === OPENROUTER_ENV_KEY AND the openrouter.ai hostname match. Existing openrouter.test.ts updated and extended to cover: matching path, envKey mismatch path, host mismatch path, missing/malformed baseUrl.

The remaining findings in that /review were either already addressed in earlier rounds (custom provider visibility / resolveBaseUrl empty array / useAuth telemetry / TS4111 errors — verified 0 locally) or architectural concerns beyond this PR's scope (LoadedSettings.setValue's per-call saveSettings).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(auth): fifth round of PR #4287 review fixes

Critical:
- provider-config.ts providerMatchesCredentials: iterate config.protocolOptions
  when resolving a function-typed envKey instead of relying on the default
  config.protocol. A custom provider configured under USE_ANTHROPIC or
  USE_GEMINI persists an envKey derived from THAT protocol, not from
  USE_OPENAI — without iteration the matcher silently misses them and
  custom-provider users disappear from /doctor + AppHeader +
  systemInfoFields + AuthDialog.defaultMainIndex.
- provider-config.test.ts: the existing test asserting 'returns false for
  function-typed envKey' was holding on the old broken behaviour. Flipped
  to assert toBe(true) for the matching path, and routed it through the
  relative source import so it doesn't run against stale dist.

Suggestions:
- settingsWriter.clearPersistedAuth: now wipes every preset's string envKey
  (iterates ALL_PROVIDERS, plus the existing subscription-plan loop kept
  for explicitness) and every QWEN_CUSTOM_API_KEY_* key by prefix match.
  Previously DeepSeek / MiniMax / Z.AI / IdeaLab / ModelScope / OpenRouter
  / custom keys lingered on disk after clearing auth.
- custom-provider.ts generateCustomEnvKey: the readable-only normalization
  collapsed 'api.example.com', 'api-example.com', and 'api_example.com'
  into the same env key, so two structurally different custom providers
  would overwrite each other's API key. Now appends a 6-hex-char SHA-256
  suffix derived from (protocol, baseUrl-with-trailing-slash-stripped).
  The trailing-slash invariant from the prior implementation is preserved
  (api/v1 and api/v1/ still hash equal). Suffix collision probability at
  6 hex chars is ~1/16M per pair — fine for an interactive flow.

Tests:
- provider-config.test.ts: added a 'iterates protocolOptions' case that
  configures a custom-style provider, derives the key under
  USE_ANTHROPIC, and asserts the matcher finds it.
- custom-provider.test.ts: regex-matches the new readable+hash format
  for the deterministic / special-character / empty-string cases, and a
  new 'disambiguates structurally distinct URLs that normalize
  identically' case that pins down the collision fix
  (api.example.com vs api-example.com vs api_example.com all differ).

Not addressed:
- TS1361 'type AuthType' import — already fixed in 8f94b018bd
- modelscope re-export — already fixed in 7228d73d80

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(custom-provider): replace polynomial regex with linear char scans

CodeQL alerts 225 + 232 flagged `/_+/g`, `/^_+|_+$/g`, and `/\/+$/` in generateCustomEnvKey as polynomial regex on user input. V8 handles these patterns linearly in practice, but the scanner can't see that and any baseUrl with many '_' or '/' would be flagged as a theoretical worst case.

Replaced both passes with single-pass character scans:

- normalizeEnvSegment: walks the string once, emits alphanumerics verbatim, collapses any non-alphanumeric run to a single '_', then trims leading/trailing underscores via charCodeAt index walks. Equivalent to the prior three regexes but with no quantifier backtracking surface.

- stripTrailingSlashes: walks backwards from the end while charCodeAt === 47, then slices. Equivalent to `replace(/\/+$/, '')`.

All 11 custom-provider tests still pass — output format and invariants (trailing-slash equivalence, hash suffix, protocol/URL disambiguation) are unchanged.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(auth): seventh round of PR #4287 review fixes

Critical:
- i18n: 9 locale files updated to replace orphaned 'Select Authentication
  Method' / 'You must select an auth method...' keys with the new
  'Connect a Provider' / 'You must connect a provider...' keys the
  AuthDialog actually references. Non-English users no longer see the
  English fallback for the main heading + exit-prevention warning.
- settingsWriter.writeSettings: renameSync is now wrapped in try/catch
  that unlinks the temp file on failure (EPERM/EBUSY on Windows from
  watchers/AV would otherwise orphan a secret-bearing .tmp file in
  ~/.qwen on every failed write).
- settingsWriter.restore(): write to disk FIRST, then update in-memory
  data. The previous order left memory clean while disk retained the
  failed install's partial state if writeSettings threw. Now matches
  the CLI adapter's order.
- AuthMessageHandler custom-provider tests: added 4 cases covering
  protocol picker → free-form URL → API key → comma-split model IDs →
  advanced config (one happy path), plus the http(s) scheme guard, the
  protocol-aware blank-URL fallback, and the whitespace-only model
  IDs guard. Previously the entire custom path through
  runProviderSetupFlow had zero coverage.
- settingsWriter clearPersistedAuth tests: added cases for the
  expanded preset/custom/subscription cleanup (asserts NODE_OPTIONS
  survives, every QWEN_CUSTOM_API_KEY_* is wiped, providerMetadata
  entries for every preset are gone) plus a no-settings-file no-op.

Suggestions:
- loadedSettingsAdapter.restore(): now checks restoreSettingsFromBackup's
  boolean return value and logs an explicit warning when on-disk rollback
  fails (EACCES / missing .orig). Previously the failure was silent and
  the next CLI restart would read a corrupted file.
- generateCustomEnvKey: hash suffix lengthened from 6 → 12 hex chars
  (24 → 48 bits). Brings collision search out of milliseconds-range
  enumeration; offline 'pick a URL that collides' attack is no longer
  practical at interactive setup time.
- getDefaultBaseUrlForProtocol: new shared helper in core consumed by
  both the CLI (useProviderSetupFlow) and VS Code (AuthMessageHandler)
  flows. Removes the duplicated DEFAULT_BASE_URLS map; one source of
  truth for the OpenAI/Anthropic/Gemini placeholder URLs.
- settingsWriter.clearPersistedAuth: providerMetadata cleanup now
  iterates ALL_PROVIDERS with resolveMetadataKey instead of hardcoding
  coding-plan/token-plan. Stale metadata for deepseek/minimax/zai/
  idealab/modelscope/openrouter no longer lingers after logout.
- resolveMetadataKey: explicit guard against provider ids containing
  '.'. A dotted id would split into multiple nested objects under
  providerMetadata, silently corrupting the settings tree. Now throws
  loudly at registration time.
- customProvider: added explicit ownsModel that prefix-matches against
  QWEN_CUSTOM_API_KEY_*. Reinstalling a custom provider under a
  different baseUrl now reliably replaces (not accumulates) the old
  entries.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(auth): eighth round of PR #4287 review fixes

Suggestions:
- clearPersistedAuth metadata cleanup loop: per-iteration try/catch
  around resolveMetadataKey so a future dotted-id provider can't abort
  the loop and leave secrets on disk.
- VS Code AuthMessageHandler: removed the hardcoded
  || 'https://api.openai.com/v1' fallback after
  getDefaultBaseUrlForProtocol — defaults must live in core. The CLI
  flow has no such fallback, and the silent OpenAI default would mask
  a new AuthType core hadn't been taught about.
- settingsWriter restore() comment: clarified the deliberate divergence
  from the CLI adapter's trade-off (disk-fail-throws here, disk-fail-
  logs-and-continues there) so the comment doesn't read 'same order'.
- useAuth handleAuthFailure: closure staleness — setPendingAuthType
  queues an async React update, so handleAuthFailure's pendingAuthType
  read could see undefined when a synchronous throw beats the next
  render. Added an optional protocolForTelemetry argument that the new
  handleProviderSubmit passes explicitly; closure fallback kept for
  legacy callers. AuthEvent error telemetry is no longer silently
  dropped.
- install.ts: track currentStep before each phase (backup → env →
  modelProviders → authType → legacyCredentials → modelSelection →
  providerState → persist → reloadModelProviders → syncAuthState →
  refreshAuth → cleanupBackup) and annotate the rethrown error with
  the failing step + authType. Original error preserved via Error.cause
  so callers matching on err.code still work.
- custom-provider.ts: stale '6-hex-char' comment updated to 12. Added
  a migration note explaining that old 6-char keys persist as harmless
  orphan disk state until clear-auth.
- settingsUtils.restoreSettingsFromBackup: was swallowing fs errors
  with catch(_e); now logs the underlying cause so the adapter's
  on-disk-rollback-failed warning has something specific to point at.

Tests:
- useAuth: new cancelAuthentication case asserts isAuthenticating
  clears, externalAuthState clears, dialog opens, authError clears.
- provider-config: new resolveMetadataKey suite — normal id, no-models
  → undefined, dotted id → throws.
- install: new case asserting the rethrown error names the failing
  step ('refreshAuth') + authType and preserves the original error
  via Error.cause.

Not addressed:
- 6→12 hash backward compat (Comment 3267562667): The 6-char keys are
  orphan disk state — never read by applyProviderInstallPlan (the new
  model provider entries reference the new 12-char key), so no security
  or correctness issue, just disk noise that clears on next sign-out.
  Documented in custom-provider.ts. A full clean-up pass would need a
  new ProviderSettingsAdapter delete API + a migration scan — better
  as its own PR.
- writeSettings renameSync error path test + loadedSettingsAdapter
  restore-failure log test (terminal-only findings): adding these
  requires fs mocking surgery that's worth its own PR.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(format): four prettier/JSDoc nitpicks from review

All four are Critical-tagged formatter / docs issues caught by the latest /review pass:

- AppHeader.tsx: `AuthType ,` (stray space before comma) → standard newline-after-{ form. Was breaking CI Lint.
- useProviderUpdates.test.ts: same `AuthType ,` pattern → standard form.
- apiPreconnect.ts: double blank line after the closing `}` of the
  import block (left behind when getAllProviderBaseUrls was removed
  from the old auth/allProviders path) → single blank line.
- types.ts (Suggestion): JSDoc for `modelsEditable` said
  "false → skip model step; use models as-is (e.g. Coding Plan)" but
  codingPlanProvider actually sets modelsEditable: true (every preset
  in the registry does), so the example contradicts the registry.
  Dropped the parenthetical.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(scripts): raise install-script suite timeout to survive Windows

Windows CI flaked on `standalone release packaging > rejects unexpected dist assets` with a 5000ms timeout. The test shells out to `node scripts/create-standalone-package.js` which produces a tar.gz; observed real runtimes from sibling tests in the same run: 4780ms / 1666ms / 1079ms — the 4.8s case is already at vitest's default 5s limit, so a slightly slower subprocess startup (antivirus inspection, contended runner) tips it over.

Pre-existing test (added 2026-05-11 in cb7059f54d), unrelated to this PR's auth refactor. Bumped the suite-wide testTimeout to 30s in scripts/tests/vitest.config.ts — the tests still complete in seconds when subprocess startup is healthy; the headroom only kicks in to cover Windows-slow variance.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(auth): ninth round of PR #4287 review fixes

Critical:
- WebViewProvider.handleAuthInteractive: roll back bad credentials when the
  agent reconnect rejects them. applyProviderInstallPlanToFile commits the
  key + calls cleanupBackup before the disconnect/reconnect runs, so the
  plan's own rollback can't cover an authState=false outcome. Now snapshot
  settings before the write (snapshotSettingsForRollback) and restore it
  (restoreSettingsSnapshot) on both the authState!=true branch and the
  catch branch. Without this a rejected key persisted and every VS Code
  restart retried it. Two new helpers added to settingsWriter; never-throw
  snapshot so a malformed pre-state degrades to a no-op restore.

Suggestions:
- AuthMessageHandler: trim the API key before validateApiKey + persistence,
  matching the CLI flow (useProviderSetupFlow trims in two places). A key
  pasted with trailing whitespace no longer causes silent auth failures or
  VS-Code-only validateApiKey rejections.
- install.ts: the annotated rethrow no longer bakes 'step "persist"' into
  the user-facing message. Step + authType are now structured properties on
  a new exported ProviderInstallError (message stays the underlying error
  text, cause preserved). Callers can show a clean message and log
  err.step/err.authType to the dev console.
- provider-config.ts: providerMatchesCredentials no longer swallows a throw
  from a function-typed envKey — console.warn surfaces the programming
  error so a custom provider silently vanishing from /doctor has a trace.
- types.ts: documented that ProviderSettingsAdapter.setValue MAY flush to
  disk eagerly (the CLI LoadedSettings adapter does) and that persist() can
  be a no-op for such adapters — so future authors don't insert pre-persist
  steps assuming atomicity.
- settingsWriter: moved the orphaned stripJsonComments JSDoc off
  jsonEscapeLength (the \u-escape helper inserted between the doc and its
  function) back onto stripJsonComments itself.

Tests:
- settingsWriter: snapshot/restore round-trip, malformed→null→no-op-restore,
  no-file→{} snapshot.
- install: updated the step-annotation test to assert err.step/err.authType
  structured properties + clean message instead of the embedded string.
- WebViewProvider.test: settingsWriter mock extended with
  applyProviderInstallPlanToFile/snapshotSettingsForRollback/
  restoreSettingsSnapshot.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(auth): tenth round of PR #4287 review fixes

Critical (both from the previous round's own changes):
- WebViewProvider.handleAuthInteractive: restoreSettingsSnapshot →
  writeSettings can throw (EPERM on Windows renameSync / disk full /
  EACCES). Both rollback call sites are now routed through a local
  safeRollback() that try/catches and logs, so a rollback failure can
  never (a) re-throw out of the else-branch into the outer catch and
  trigger a second rollback that skips the error message, nor (b) throw
  out of the catch-branch and leave the webview auth dialog hanging with
  no feedback.
- provider-config.providerMatchesCredentials: the new envKey-throw
  console.warn logged the full baseUrl, which can embed credentials
  (https://user:sk-secret@host). Now logs only new URL(baseUrl).hostname
  (with an [invalid] fallback) and err.message, matching the
  sanitization WebViewProvider already uses.

Tests:
- WebViewProvider.test: new 'credential rollback' describe with three
  cases — (1) authState!==true after reconnect → restoreSettingsSnapshot
  called with the snapshot, (2) authState===true → restore NOT called,
  (3) restore throws (EPERM) → handleAuthInteractive still resolves and
  the authError message is still sent. Hoisted mocks extended with
  applyProviderInstallPlanToFile / snapshotSettingsForRollback /
  restoreSettingsSnapshot refs so the scenario is controllable.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(auth): eleventh round of PR #4287 review fixes

Critical:
- AuthMessageHandler: validation-failure paths (bad URL scheme, invalid
  API key, empty model IDs, handler-not-set) no longer call
  notifyAuthCancelled after sendToWebView({authError}). The webview's
  ProviderSetupForm clears the error on authCancelled, so the two
  messages raced and the error flashed away before the user could read
  it. authCancelled is now reserved for genuine user dismissals (Escape
  on a QuickPick/InputBox); authError already clears the connecting state.
- WebViewProvider: after rolling back rejected credentials, also
  disconnect the agent. The reconnect spawned a process holding the bad
  key in memory; without disconnect a subsequent chat message hit a
  stale-credential error unrelated to the original auth failure. Now
  agentManager.disconnect() + agentInitialized=false so the next /auth
  reconnects cleanly.

Suggestions:
- install.ts: added a DENY_ENV_KEYS denylist (NODE_OPTIONS, NODE_PATH,
  LD_PRELOAD, LD_LIBRARY_PATH, DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH,
  PATH, HOME, TMPDIR), checked case-insensitively before writing any
  plan.env entry to settings + process.env. Defense in depth: all callers
  go through buildInstallPlan with hardcoded keys today, but
  ProviderInstallPlan is exported.
- settingsUtils: setNestedPropertySafe AND setNestedPropertyForce now
  refuse __proto__/constructor/prototype path segments (inline literal
  === so CodeQL recognises the sanitiser). migrateProviderMetadata feeds
  field names from Object.entries on user settings.json, and JSON.parse
  keeps __proto__ as an own property — guarding at the utility protects
  every caller, not just the adapters.

Already fixed in f31224bac1 (review ran against 9f45a7536b):
- restoreSettingsSnapshot throw masking the original error → safeRollback.
- baseUrl logged verbatim in providerMatchesCredentials → hostname only.

Tests:
- install: NODE_OPTIONS rejected + not leaked to process.env/settings;
  case-insensitive Path rejection.
- AuthMessageHandler: validation authError is NOT followed by
  authCancelled.
- WebViewProvider: rollback path disconnects the agent + clears
  agentInitialized.
- settingsUtils: setNestedPropertySafe/Force refuse __proto__/
  constructor/prototype and don't pollute Object.prototype.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(test): use bracket access in settingsUtils prototype-pollution tests

The new setNestedProperty guard tests asserted obj.a.b.c / obj.x.y dot-access on Record<string, unknown>, which trips noPropertyAccessFromIndexSignature (TS4111) under the emitting tsc --build the CI 'Install dependencies' step runs. Local npm run typecheck (--noEmit) had a stale tsbuildinfo and didn't re-check the file. Switched to bracket access (obj['a']['b']['c']) to match the strict option. Behaviour unchanged; 78 settingsUtils tests still pass.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(vscode): cover the outer-catch rollback path in handleAuthInteractive

All prior rollback tests exercised the else-branch (authState !== true). The outer catch — reached when applyProviderInstallPlanToFile or doInitializeAgentConnection throws (disk errors, partial writes) — had no coverage, and that's the higher-risk path. New test makes doInitializeAgentConnection reject and asserts (1) restoreSettingsSnapshot called with the snapshot, (2) authError sent containing 'Configuration failed', (3) handleAuthInteractive resolves without throwing. Guards against a regression that drops the safeRollback wrapper in the catch.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(providers): make NODE_OPTIONS denylist test env-independent

The test asserted process.env.NODE_OPTIONS toBeUndefined after the rejected plan, but CI sets NODE_OPTIONS (--max-old-space-size=3072 from the build script), so it failed there while passing locally where NODE_OPTIONS is unset. Snapshot the original value and assert the rejected plan left it UNCHANGED (and specifically not the evil --require value) — that's the actual invariant: the denylist throws before mutating process.env.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(vscode): disconnect stale agent in handleAuthInteractive catch block too

The else-branch (authState !== true) disconnected the agent after rollback, but the outer catch only rolled back. If doInitializeAgentConnection partially initializes (agentInitialized=true, agent process spawned) then throws — e.g. a disk error during post-connect setup — the stale-credential agent stayed connected.

Extracted a disconnectStaleAgent() local helper (alongside safeRollback) and called it in both the else-branch and the catch, so the two paths are symmetric. Extended the outer-catch test to spawn a partial agent before the throw and assert disconnect() is called + agentInitialized cleared.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(auth): twelfth round of PR #4287 review fixes (5 suggestions)

All from DeepSeek's pass, all on recent commits:
- settingsUtils: stale comment referenced a non-existent UNSAFE_PATH_SEGMENTS const; the actual guard is pathHasUnsafeSegment(). Fixed both comment sites.
- settingsWriter.snapshotSettingsForRollback: was silently returning null on a readSettings throw (disabling credential rollback with no signal). Now console.warn's the cause so oncall can tie repeated cross-restart auth failures back to a transient unreadable settings file.
- provider-config.providerMatchesCredentials: the envKey-throw warn logged err.message, which a user-defined envKey fn could populate with the API key (new Error(`bad config: ${apiKey}`)). Now logs only err.constructor.name — no message, no URL.
- install.ProviderInstallError: was an interface (erased at compile time → instanceof always false). Converted to a class extending Error so instanceof works at runtime; exported as a value (not type) from the barrel. Construction simplified to new ProviderInstallError(msg, step, authType, { cause }).
- install.DENY_ENV_KEYS: added Windows TMP/TEMP alongside TMPDIR so a crafted plan can't redirect temp-file creation on Windows.

Tests:
- install: assert the thrown error is instanceof ProviderInstallError; new it.each covering TMP/TEMP/tmp rejection (case-insensitive).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(vscode): log error class name not message in snapshotSettingsForRollback

Consistency with the err.constructor.name approach applied in provider-config.providerMatchesCredentials. The risk here is lower (the catch is filesystem errors from readSettings/structuredClone, not user-defined functions), but logging only the class name keeps the security stance uniform across the codebase.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-20 23:48:52 +08:00
Shaojin Wen
16f0fde19a
fix(test): raise timeout for Windows installer end-to-end tests (#4352)
* fix(test): raise timeout for Windows installer end-to-end tests

The Windows-only end-to-end installer tests spawn cmd.exe to run the
.bat installer and then qwen.cmd --version, which boots a Node process.
On GitHub's windows-latest runners that chain regularly takes >5s, so
the default 5s vitest timeout makes them flaky (recently observed at
5804ms on CI). Bump the describe-block timeout to 30s, which leaves
headroom without masking real regressions.

* fix(test): raise timeout for Linux/macOS installer end-to-end tests

Match the timeout already applied to the Windows e2e block: the
Linux/macOS installer tests also spawn child processes via
execFileSync, so they share the same flake risk near the default 5s
vitest timeout. 15s leaves ample headroom without Windows' cmd.exe
overhead.

Addresses review feedback on #4352.
2026-05-20 17:42:29 +08:00
MikeWang0316tw
02a65f90c4
fix(i18n): Correct zh-TW translations to match Traditional Chinese conventions (#4129)
* fix(i18n): Correct zh-TW translations to match Traditional Chinese conventions

Fix ~131 lines of Traditional Chinese (zh-TW) translations that used
Simplified Chinese character forms instead of standard Traditional
Chinese usage.

Changes:
- 文件 → 檔案 (47 occurrences)
- 爲 → 為 (45 occurrences)
- 啓 → 啟 (44 occurrences)
- 曆史 → 歷史 (6 occurrences)
- 鏈接 → 連結 (4 occurrences)
- 菜單 → 選單 (3 occurrences)

* fix(i18n): Replace 服務器 with 伺服器 (15 occurrences)

Align with Traditional Chinese convention where 伺服器 is the standard
term for 'server' in computing contexts.

* fix(i18n): Update zh-TW.js header comment to prevent accidental overwrite

Clarify that the file is the authoritative source and should not be
overwritten with auto-generated output, to prevent future maintainers
from regenerating with raw OpenCC and losing manual corrections.

* fix(i18n): Add zh-TW regression check and maintenance docs

Addresses reviewer feedback on PR #4129 (points 2 and 3):

- scripts/check-i18n.ts: Iterate over parsed zh-TW translation values
  (not raw file content) and report the offending key. Replace the
  earlier substring list with ZH_TW_FORBIDDEN_PATTERNS, which targets
  the three real regression categories: variant Traditional characters
  produced by OpenCC s2t (爲, 啓), Mainland-Chinese vocabulary (服務器,
  菜單, 鏈接), and pure Simplified characters. Excludes 禁用 / 配置 /
  文件 / 打開 to avoid false positives on Taiwan-valid usage.
- scripts/tests/check-i18n.test.ts: Cover the new check, including
  negative cases for Taiwan-valid vocabulary.
- docs/users/features/language.md: Document zh-TW maintenance — the
  vocabulary table, why raw OpenCC s2t output is not acceptable, and
  where the CI-enforced list lives.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(i18n): Address review feedback on zh-TW check (#4129)

- check-i18n.ts: Sort ZH_TW_FORBIDDEN_PATTERNS longest-first and break
  on first match so e.g. `历史` reports the specific bigram instead of
  also firing the bare `历` rule (no duplicate CI errors).
- check-i18n.ts: Add ZH_TW_ALLOWED_EXCEPTIONS escape hatch so a future
  legitimate translation (e.g. 區塊鏈 in a UI string) can opt out by key
  without weakening the global pattern list.
- docs/users/features/language.md: Add a "CI enforced?" column so
  contributors can tell which rows block CI vs. which are review-only
  style guidance. Replace bare `曆` in the table with the `曆史` bigram
  and note that `曆` is correct in calendar terms (日曆, 農曆, 西曆) —
  prevents a future maintainer from globally replacing 曆→歷.
- Tests: Cover the dedup behavior on overlapping patterns.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(i18n): Note word-boundary limitation of zh-TW substring check

Document the known limitation that `includes()`-based pattern matching
does not respect Chinese word boundaries — a bigram like `鏈接` will
false-positive on `區塊鏈接口` (區塊鏈 + 接口). Direct contributors to
`ZH_TW_ALLOWED_EXCEPTIONS` when this happens instead of weakening the
pattern list.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 15:26:12 +08:00
易良
cb7059f54d
feat(installer): add standalone archive installation (#3776)
* feat(installer): add standalone archive installation

* fix(installer): harden standalone archive installs

* fix(installer): address standalone review findings

* chore(installer): clarify review followups

* fix(installer): stabilize standalone script checks

* chore(installer): remove internal planning docs

* chore(installer): simplify standalone release review fixes

* test(installer): add Windows batch install smoke

* test(installer): fix Windows batch smoke quoting

* test(installer): preserve Windows cmd quotes

* fix(installer): use robust Windows checksum hashing

* ci: narrow installer debug matrix

* fix(installer): address standalone review hardening

* fix(installer): avoid Windows validation parse errors

* fix(installer): simplify Windows option validation

* fix(installer): harden standalone review fixes
2026-05-11 13:25:48 +08:00
Yan Shen
9bd5a0180b
feat(cli): core built-in i18n coverage (#3871)
* feat(i18n): expand built-in locale coverage

* feat(cli): add dynamic slash command translation

* test(cli): stabilize session picker assertions

* fix(core): close jsonl readers before cleanup

* fix: address i18n review regressions

* fix(cli): address dynamic i18n review findings

* fix(cli): address i18n review follow-ups

* fix(cli): address i18n review feedback

* test(cli): align i18n parity coverage with strict locales

* fix(cli): address i18n review findings
2026-05-10 22:35:03 +08:00
jinye
07441cc1e3
feat(sdk-python): add network timeouts to release version helper (#3833) 2026-05-05 19:25:00 +08:00
jinye
2c93fd670c
refactor: extract shared release helper utilities (#3834)
Move four duplicated utility functions (getArgs, readJson,
validateVersion, isExpectedMissingGitHubRelease) from the three
get-release-version.js scripts into a shared module at
scripts/lib/release-helpers.js so that changes only need to happen
in one place.

Also fixes a pre-existing bug in getArgs where argument values
containing '=' were silently truncated (e.g. --msg=a=b produced
{msg:'a'} instead of {msg:'a=b'}).

Closes #3795

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
2026-05-05 10:15:17 +08:00
jinye
03f66bada5
feat(sdk-python): add PyPI release workflow (#3685)
* feat(sdk-python): add pypi release workflow

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): build cli before smoke test

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): tighten release conflict handling

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): harden python release workflow

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): tighten stable release guards

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): harden prerelease publish flow

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): reuse release branches on rerun

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): resume incomplete releases

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(release): tighten missing-release checks

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): resume stable release reruns

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): tighten release recovery guards

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(sdk-python): cover release version edge cases

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): address release workflow review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* refactor(sdk-python): address review feedback on release version script

- Remove unreachable `if (type === 'stable')` branch in bumpVersion();
  the stable path was dead code since getVersion() throws for all
  stable conflicts before calling bumpVersion(). Move nightly conflict
  throw to the call site for symmetry.
- Rename getNextPatchBaseVersion → getNextBaseVersion to reflect that
  the function can return a prerelease base without incrementing patch.
- Add test for preview+nightly coexistence where nightly base is higher.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(sdk-python): address remaining review feedback on release workflow

- Fix failure-issue gate to read github.event.inputs.dry_run directly
  instead of steps.vars.outputs.is_dry_run (which is empty when early
  steps fail). Add --repo flag for gh issue create when checkout failed.
- Add diagnostic state table to failure-issue body (RELEASE_TAG,
  PACKAGE_VERSION, PUBLISH_CHANNEL, RESUME_EXISTING_RELEASE, etc.)
- Fix release-notes error swallow: only silence release not found /
  Not Found / HTTP 404, emit :⚠️: for other gh release view errors.
- Improve validateVersion error messages to use human-readable format
  keys (X.Y.Z, X.Y.Z-preview.N) matching TS sibling convention.
- Filter fully-yanked versions in getAllVersionsFromPyPI.
- Add console.error log when stable is derived from nightly.
- Add bash regex guard for inputs.version to prevent shell injection.
- Use per-release-type concurrency groups (nightly/preview/stable).
- Add jq null-guard checks for all 6 field extractions.
- Remove misleading --follow-tags from git push (lightweight tags).

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(sdk-python): rename misleading test description

The test asserts that preview/nightly releases return empty
previousReleaseTag, but the name said "same-channel previous
release tags" which implied non-empty values.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(sdk-python): address unresolved review comments on release workflow

- Remove -z check in extract_field() that blocked preview/nightly releases
  (previousReleaseTag is legitimately empty for non-stable releases)
- Use static environment.url since step outputs aren't available at job startup
- Use skip-existing for resumed PyPI publish to fill in missing artifacts
- Add AbortSignal.timeout(30s) to PyPI fetch to prevent indefinite hangs
- Add downgrade guard for stable_version_override
- Use GHA :⚠️: annotation instead of console.error for visibility
- Separate yanked/non-yanked version lists so conflict detection includes
  yanked versions (PyPI still reserves those slots)
- Filter current release from previousReleaseTag to avoid self-reference on resume
- Add tests for yanked conflict detection, downgrade guard, and resume previousReleaseTag

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(sdk-python): address final review round on release version script

- Fix getNextBaseVersion() first-release skip: use pyproject.toml version
  directly when PyPI has no stable versions instead of unconditionally
  incrementing
- Fix getNextBaseVersion() off-by-one: change > to >= so equal prerelease
  base continues the existing line instead of incrementing patch
- Add :⚠️: annotation when preview auto-bumps due to orphan git
  tags (tag exists without PyPI version or GitHub release)
- Add set -euo pipefail to 5 workflow steps missing it: release_branch,
  persist_source, Create GitHub release, Delete prerelease branch, Create
  issue on failure
- Fix 2 existing tests affected by first-release change, add 4 new tests

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

* fix(sdk-python): use stderr for GHA warning annotations to avoid corrupting JSON stdout

console.log writes to stdout, which gets captured by VERSION_JSON=$(node ...)
in the workflow and corrupts the JSON output for jq. Switch to console.error
so :⚠️: annotations go to stderr (GHA recognizes workflow commands on
both streams). Also add set -euo pipefail to the "Get the version" step for
consistency with other workflow steps.

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)

---------

Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-04 21:07:21 +08:00
tanzhenxin
be633a80cc
📦 Release qwen-code CLI as a Standalone Bundled Package (#866) 2025-10-24 17:08:59 +08:00
tanzhenxin
eb95c131be
Sync upstream Gemini-CLI v0.8.2 (#838) 2025-10-23 09:27:04 +08:00
mingholy.lmh
14ea33063f Merge tag 'v0.3.0' into chore/sync-gemini-cli-v0.3.0 2025-09-11 16:26:56 +08:00
Pascal Birchler
ee4feea006
chore: consistently import node modules with prefix (#3013)
Co-authored-by: N. Taylor Mullen <ntaylormullen@google.com>
2025-08-25 20:11:27 +00:00
matt korwel
0e24805806
feat(release): update release process for nightly and preview builds (#6643)
Co-authored-by: Bryan Morgan <bryanmorgan@google.com>
2025-08-20 15:50:00 +00:00
Yiheng Xu
999f3af098
fix release workflow (#172) 2025-08-01 17:13:07 +08:00
koalazf.99
a9d6965bef pre-release commit 2025-07-22 23:26:01 +08:00
matt korwel
75a128e7ee
chore(release): v0.1.10 (#3749)
Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com>
Co-authored-by: Aryan Sawant <156219699+aryanjsawant@users.noreply.github.com>
Co-authored-by: neo.alienson <neo@01man.com>
2025-07-10 16:55:22 +00:00
matt korwel
a7256f630c
Relase: Clean up and condensing (#3321) 2025-07-05 20:58:59 +00:00