Files
qwen-code/docs/design/explicit-update-version.md
edde639992 feat(cli): support exact-version updates without npm discovery (#11486)
* feat(cli): support exact-version updates without npm discovery

* fix(cli): repair build and close exact-version update review gaps (#11486)

The merge into this branch left two copies of the semver default import
in standalone-update.ts, so packages/cli no longer compiled (TS2300) and
every downstream gate went red. Drop the added duplicate, keeping main's
import position.

Also address the remaining review findings on the exact-version update:

- Verify the extracted archive's manifest.json version matches the
  requested version before activation, alongside the existing executable
  smoke test. The manifest is the field the installation reads back and
  the prepared-update gate compares against; an inflated manifest version
  would otherwise silently freeze all future prepared updates.
- Pin the yargs builder contract: --target-version must reach the handler
  as argv.targetVersion under the production .strict() parser, going red
  if the builder is dropped or the option renamed.
- Add the issue #11484 acceptance-criterion-7 E2E test: update a
  disposable standalone install to an exact version served from a local
  HTTPS release root while the npm registry sink records zero requests,
  asserting the installed manifest version and rollback retention.
- Document that an explicit target is not persisted: automatic updates
  move a standalone install forward again next session, and keeping an
  exact older version requires general.enableAutoUpdate: false.
- Add the Chinese version of the design doc with reciprocal language
  links.

* fix(cli): close standalone update review gaps from round 2 (#11486)

Address the second-round review findings on the exact-version update:

- Validate the extracted archive's full manifest identity (name, target,
  version) before activation instead of version alone. A mirror artifact
  with the right version but no name passed every check and installed,
  after which the directory silently stopped classifying as a managed
  standalone install; the gate now rejects it and preserves the install.
- Guard both version comparisons with normalizeVersion's own predicates
  (SEMVER_RE and semver.valid), so a regex-valid but semver-invalid
  version like 01.2.3 hits the intended mismatch message instead of
  throwing "Invalid version format" out of the comparison.
- Rename the manifest gate's message to name the archive check rather
  than the smoke test, which has not run yet at that point.
- Make the target-version E2E hermetic: pin QWEN_REQUIRE_SIGNATURE,
  QWEN_CODE_LANG, and QWEN_HOME per spawn (witnessed by staging hostile
  ambient values inside the test), re-seed the fixture installation in
  beforeEach so vitest task retries never inherit the post-activation
  tree, and guard the afterAll server-close promises so an early
  beforeAll failure no longer hangs the hook and leaks the scratch root.
- Pin the documented "explicit targets permit same-version reinstalls
  and downgrades" guarantee with performStandaloneUpdate cases that go
  red if the prepared-route staleness check ever moves into the shared
  path.
- Correct the fixture's header comment: local-git-clone classification
  requires the resolved CLI path inside the cwd, not merely a cwd inside
  a git worktree.

* fix(cli): close standalone archive gate review gaps from round 3 (#11486)

Validate the archive's runtime flavor and executable layout against the
installed build before activation, share one version predicate and one
runtime-layout predicate instead of hand-inlined copies, give every gate
predicate a discriminating test, keep the E2E registry sink honest with a
discovery control, and document the manifest identity contract.

* fix(cli): pin the git shim's module scope in the reverse-apply screen test (#11486)

The test shadows git with an extensionless Node shim under tmpdir. Node
derives the module system of an extensionless file from the nearest
ancestor package.json, so a "type": "module" package.json above tmpdir
(the runner image ships /tmp/package.json) loads the CJS shim as ESM:
it dies on "require is not defined" before forwarding to real git, the
restore's ls-files -v spawn never arms the stamp, and the screen test
fails without the screen ever being asked. Pin {"type":"commonjs"} in
the shim directory so the shim loads the way its body is written,
regardless of what sits above tmpdir.

* fix(cli): flavor-aware standalone archive routing and launcher entry-point gate (#11486)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): sanitize --target-version rejection echo and bun test fixture

normalizeVersion throws with the raw flag value interpolated, and the new
--target-version error path echoed it to stderr unsanitized, so terminal
escape sequences (OSC-52, C0/C1, bidi overrides) supplied via argv reached
the user's terminal verbatim. Collapse whitespace and strip escapes,
control and format characters at the write site, mirroring the existing
sanitizeForStderr pipeline, and pin it with an escape-carrying reject case.

Also stage the real bun archive layout in the standalone-update fixture
(bun/bin/bun plus the node/bin/node compat shim installBunRuntime writes)
so the smoke test's two-hop execution chain has a witness: shortening the
staged shim now reds the bun-flavor update test with "Smoke test failed".

* fix(cli): accept pre-v0.23.0 standalone manifests without runtime (#11486)

* fix(cli): address target-version review feedback (#11486)

* fix(cli): keep update recovery guidance and gate win32 test (#11486)

Preserve the updater's manual-recovery command by using a 4,096-character bound for its own failure text; the 500-character bound remains for the untrusted --target-version echo.

Gate the pre-v0.23.0 manifest success-path test with the same win32 skip as its sibling tests, since atomicReplace returns 'deferred' there.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): keep update echo sanitizer at its call site (#11486)

Move the bounded stderr sanitizer back next to the update command so the shared stdio helper keeps its existing surface, and raise the hosted plan wait to the 10s bound the rest of its file uses so the full-suite gate no longer times out under load.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): raise hosted plan wait to the file's 10s bound (#11486)

* test(cli): describe the registry sink's real enforcement (#11486)

Co-authored-by: Qwen-Coder <[email protected]>

* fix(ci): stage the standalone archive layout in the runtime harness (#11486)

applyStandaloneUpdate rejects an extracted archive that lacks bin/qwen and
the bundled runtime, but the harness payload carried neither, so
npm run test:standalone:runtimes:ci died in node/clean before any scenario
ran. Stage the layout create-standalone-package.js ships - bin/qwen plus the
bun flavor's node/bin/node mirror shim - so the fixture matches a real
archive and the update scenarios exercise the gate instead of tripping it.

* fix(cli): gate standalone archives on cli-entry.js (#11486)

The pre-activation layout gate required the installer's launcher and runtime paths, but the repository's standalone-runtime harness repacks a runnable archive without them, so npm run test:standalone:runtimes:ci died in node/clean and the required Test job stayed red. The harness lives in maintainer-only scripts/ territory, so relax the updater's archive gate to the documented v0.18.1 floor: lib/cli-entry.js. smokeTest still runs the bundled runtime, lib/cli.js and lib/cli-entry.js before activation, and isStandaloneInstallDir keeps using the full layout for classification.

Revert the harness edit rejected by the footprint gate, and update the design/user docs and the binless fixture test to the relaxed contract.

Co-authored-by: Qwen-Coder <[email protected]>

* fix(cli): make launcher-less standalone updates launchable (#11486)

The update gate accepts archives from the v0.18.1 floor without the
installer's bin/qwen launcher (the standalone-runtime harness repacks one
that way), but installation detection and the PATH wrapper still required
it. A successful update could therefore leave a tree that neither launched
nor classified as standalone.

Share one layout contract between the gate and detection: the CLI entry
point plus the bundled runtime. Detection still accepts the legacy
launcher-only layout. When a tree has no launcher, ensureBinWrapper writes
a wrapper that execs lib/cli-entry.js through the bundled interpreter and
refreshes a stale wrapper that targeted the removed launcher. Update the
design and user docs and cover the classifier, wrapper and refresh paths.

* fix(cli): resolve a restartable launcher for launcher-less standalone installs (#11486)

Accepting launcher-less install trees made the daemon's one-click update
arm a restart against a hardcoded bin/qwen that such a tree does not have:
activation succeeded, the restart threw ENOENT, and the update was
reported as failed. Resolve the PATH wrapper the updater maintains, which
lives outside the install and is refreshed when an archive ships no
launcher, and fall back to the archive's own launcher; offer manual
instructions when neither exists. Only a wrapper that names the install
may restart it.

Skip the launcher-less acceptance test on win32 like its siblings: its
fixture is Unix-shaped and a Windows host derives win32-x64, which
validateTarget rejects.

---------

Co-authored-by: 西岐 <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: Shaojin Wen <[email protected]>
Co-authored-by: qwen-code-dev-bot <[email protected]>
Co-authored-by: qwen-code-ci-bot <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: qwen-code-bot <[email protected]>
2026-10-31 13:42:54 +00:00

3.1 KiB

Explicit standalone update versions

Issue: #11484

English | 简体中文

qwen update currently discovers a release through npm before downloading it. An operator who already selected an exact release cannot install it when registry access is unavailable, even if release artifacts are reachable.

Add --target-version to the CLI update command. Validate and normalize the concrete version locally, skip version discovery, and reuse the standalone updater with that target. Without the option, preserve discovery and existing installation instructions. The existing --version option keeps its display-only meaning.

Explicit selection permits same-version reinstalls and downgrades: it is a request to install exactly that release, not to find a newer release. Supported explicit targets begin at v0.18.1, the first standalone release that ships the lib/cli-entry.js entry point; older archives lack that entry point and fail the update check, leaving the installation unchanged. The selection is not persisted: automatic updates move a standalone installation forward again on the next session, so keeping an exact older version requires general.enableAutoUpdate: false. Stable and concrete prerelease versions are accepted, with an optional leading v; mutable tags and malformed versions are rejected. Unsupported installation methods return an error directing the user to install the exact version manually, without registry discovery or suggesting an unpinned upgrade.

Artifact routing continues to honor QWEN_UPDATE_BASE_URL. The archive, checksums and signature use the same resolved release root. The requested archive name carries the installed build's flavor (qwen-code-<target>-opentui-preview for bun runtime installs), so OpenTUI preview installations keep a working update path and the gate's runtime clause stays satisfiable for them. Before replacing the installation, the updater validates the extracted archive's manifest.json against the requested release — name must be @qwen-code/qwen-code, target must match the installation's platform target, version must equal the requested version and runtime must match the installed build's flavor — and requires the archive to ship the lib/cli-entry.js entry point and the bundled runtime; the smoke test then runs both. Archives that omit the installer's bin/qwen launcher are accepted, and the updater's PATH wrapper launches the entry point directly. This archive identity gate runs on every standalone update path, including background, on-exit and daemon-driven updates, not only explicit targets. The updater checks the new executable's version against the requested version before replacing the installation. Locking, extraction safeguards, atomic replacement and rollback remain unchanged.

Changes are limited to the CLI command and its tests, standalone archive validation and smoke testing, and user documentation. No daemon/UI endpoints, automatic update policies, registry mirrors or release infrastructure are introduced. Any remaining product questions are tracked on the issue.