From e8d320e91abd2075551586d3380d8e1e32d49617 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 9 Oct 2026 00:17:31 +0800 Subject: [PATCH 01/24] feat(install): add release-aware shell and PowerShell installers --- .github/workflows/installation.yml | 57 +++ README.md | 24 +- README_CN.md | 23 +- README_JP.md | 22 +- docs/en/development/releasing.md | 6 +- docs/en/docs/get-started/install-and-run.md | 106 ++++- docs/en/docs/get-started/quickstart.md | 17 +- docs/en/docs/operate/connect-remote-server.md | 16 +- docs/en/rfcs/0000-script-installation.md | 218 +++++++++++ docs/zh/development/releasing.md | 6 +- docs/zh/docs/get-started/install-and-run.md | 95 ++++- docs/zh/docs/get-started/quickstart.md | 15 +- docs/zh/docs/operate/connect-remote-server.md | 14 +- docs/zh/rfcs/0000-script-installation.md | 191 +++++++++ scripts/release_version.py | 9 +- tests/native/test_installation.py | 366 +++++++++++++++++ tests/test_release_version.py | 19 +- website/public/install.ps1 | 346 ++++++++++++++++ website/public/install.sh | 369 ++++++++++++++++++ 19 files changed, 1839 insertions(+), 80 deletions(-) create mode 100644 .github/workflows/installation.yml create mode 100644 docs/en/rfcs/0000-script-installation.md create mode 100644 docs/zh/rfcs/0000-script-installation.md create mode 100644 tests/native/test_installation.py create mode 100644 website/public/install.ps1 create mode 100644 website/public/install.sh diff --git a/.github/workflows/installation.yml b/.github/workflows/installation.yml new file mode 100644 index 0000000000..ad6500e17f --- /dev/null +++ b/.github/workflows/installation.yml @@ -0,0 +1,57 @@ +name: Script installation + +on: + push: + branches: [master] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + +concurrency: + group: installation-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + installation: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + steps: + - name: Check out + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: Set up the environment + uses: ./.github/actions/setup-python-env + + - name: Build the installation fixture + shell: bash + env: + # Qualify this commit using release-shaped metadata, without publishing it. + SETUPTOOLS_SCM_PRETEND_VERSION: "1.2.0" + run: uv build --wheel --out-dir dist/installation + + - name: Exercise bootstrap, version selection, profiles, and persistent memory + shell: bash + env: + POWERCONTEXT_INSTALL_WHEEL: ${{ github.workspace }}/dist/installation/powercontext-1.2.0-py3-none-any.whl + run: >- + uv run --no-sync pytest -q tests/native/test_installation.py + --junitxml "$RUNNER_TEMP/installation.xml" + --basetemp "$RUNNER_TEMP/installation-tests" + + - name: Upload installation diagnostics after failure + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: installation-${{ matrix.os }} + path: | + ${{ runner.temp }}/installation.xml + ${{ runner.temp }}/installation-tests/**/*.log + if-no-files-found: ignore diff --git a/README.md b/README.md index 5ba54fa7a5..8d6050635a 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,7 @@ PowerContext keeps context with the work across conversations. When you return, [Website](https://powercontext.oceanbase.io/) · [Installation walkthrough](https://powercontext.oceanbase.io/en/docs/get-started/quickstart/) -PowerContext 1.2.0 includes the guided setup. The commands below install this version and connect -the matching Agent integration. +The installer selects the latest stable PowerContext release from your package index. Agent integrations use the installed version. ## Pick up where the work left off @@ -27,13 +26,24 @@ You decide what will matter later and what needs to move with the task. PowerCon ## Install, configure, and connect your Agent -You need Git, [uv](https://docs.astral.sh/uv/getting-started/installation/), and your Agent's CLI. -Python 3.11+ is required; uv can provision it. macOS and Linux are supported; Windows support is `experimental`. +The installer provisions uv and Python when needed. Agent integration setup also needs Git and your Agent's CLI. +macOS and Linux are supported; Windows support is `experimental`. -Install 1.2.0 and open the interactive configuration wizard in a dedicated directory: +Install with the recommended script: + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +``` + +On Windows, use PowerShell: + +```powershell +powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1))) --no-hosts" +``` + +Apply the PATH command printed by the installer, then configure PowerContext: ```bash -uv tool install --force "powercontext[cli,server]==1.2.0" mkdir -p powercontext-config cd powercontext-config powercontext config init --language en --output .env @@ -71,7 +81,7 @@ covers Codex and Claude Code, Dashboard login, SSH forwarding, HTTPS prerequisit For example, the matching Codex installation is: ```bash -powercontext setup codex --ref powercontext-v1.2.0 +powercontext setup codex --ref "powercontext-v$(powercontext --version)" powercontext doctor codex ``` diff --git a/README_CN.md b/README_CN.md index 7e927956d1..49e698a945 100644 --- a/README_CN.md +++ b/README_CN.md @@ -16,7 +16,7 @@ PowerContext 让上下文跟随工作,跨越不同的对话。你回来时, [网站](https://powercontext.oceanbase.io/zh/) · [完整安装流程](https://powercontext.oceanbase.io/zh/docs/get-started/quickstart/) -PowerContext 1.2.0 包含交互式配置向导。下面的命令安装这一版本,并接入相同版本的 Agent 集成。 +安装器默认选择包源中的最新稳定版 PowerContext,Agent 集成与实际安装版本保持一致。 ## 从当前进展继续 @@ -26,13 +26,24 @@ PowerContext 1.2.0 包含交互式配置向导。下面的命令安装这一版 ## 安装、配置并接入 Agent -准备 Git、[uv](https://docs.astral.sh/uv/getting-started/installation/) 和你使用的 Agent CLI。 -需要 Python 3.11+,uv 可以按需安装。支持 macOS 和 Linux;Windows 支持为 `experimental`。 +安装器会按需补齐 uv 和 Python;接入 Agent 时另需 Git 和对应的 Agent CLI。 +支持 macOS 和 Linux;Windows 支持为 `experimental`。 -安装 1.2.0,然后在独立目录里打开交互式配置向导: +推荐使用安装脚本: + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +``` + +Windows 使用 PowerShell: + +```powershell +powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1))) --no-hosts" +``` + +先执行安装器打印的 PATH 命令,再继续配置: ```bash -uv tool install --force "powercontext[cli,server]==1.2.0" mkdir -p powercontext-config cd powercontext-config powercontext config init --language zh --output .env @@ -66,7 +77,7 @@ powercontext capabilities SSH 隧道、HTTPS 前提及逐项验收。例如,匹配本版本的 Codex 安装命令是: ```bash -powercontext setup codex --ref powercontext-v1.2.0 +powercontext setup codex --ref "powercontext-v$(powercontext --version)" powercontext doctor codex ``` diff --git a/README_JP.md b/README_JP.md index dc0eccb45a..3ea3d6e8ab 100644 --- a/README_JP.md +++ b/README_JP.md @@ -16,8 +16,7 @@ PowerContext は、会話をまたいでもコンテキストを作業ととも [Web サイト](https://powercontext.oceanbase.io/en/) · [インストール手順](https://powercontext.oceanbase.io/en/docs/get-started/quickstart/) -PowerContext 1.2.0 には対話式セットアップが含まれています。 -以下のコマンドでこのバージョンの PowerContext と、対応する Agent 連携をインストールします。 +インストーラーはパッケージソースの最新安定版を選びます。Agent 連携にはインストール済みバージョンを使用します。 ## 作業の続きをそのまま引き継ぐ @@ -27,10 +26,21 @@ PowerContext 1.2.0 には対話式セットアップが含まれています。 ## 利用中の Agent と接続する -Git、uv、Agent CLI を用意して、1.2.0 をインストールします: +推奨のインストールスクリプトは、必要に応じて uv と Python を用意します。Agent 連携には Git と Agent CLI が必要です: + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +``` + +Windows では PowerShell を使用します: + +```powershell +powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1))) --no-hosts" +``` + +表示された PATH 設定を実行してから、設定を続けます: ```bash -uv tool install --force "powercontext[cli,server]==1.2.0" mkdir -p powercontext-config cd powercontext-config powercontext config init --language en --output .env @@ -50,14 +60,14 @@ Server を起動したまま、別のターミナルで同じ設定ディレク クライアント環境の読み込み、Scope の作成・紐付け、同じバージョンの Agent プラグインのインストールを行います。例: ```bash -powercontext setup codex --ref powercontext-v1.2.0 +powercontext setup codex --ref "powercontext-v$(powercontext --version)" powercontext doctor codex ``` PowerContext ツールと Agent 連携には、常に同じ Git ref を使用してください。 [Quick Start](https://powercontext.oceanbase.io/en/docs/get-started/quickstart/) で Dashboard、SSH、HTTPS、 Source から Topic 生成・更新、新しいセッションでの検索まで確認できます。 -Python 3.11+ が必要です。macOS と Linux をサポートし、Windows のサポートは `experimental` です。 +Python 3.11+ はインストーラーが用意します。macOS と Linux をサポートし、Windows のサポートは `experimental` です。 Codex は `official`、他のホストと Python Agent フレームワークは `community`、Bub は評価専用の `evaluation` です。 これらは PowerContext 連携のメンテナンス主体と用途を示すタグです。対応機能と利用可能なバージョンは diff --git a/docs/en/development/releasing.md b/docs/en/development/releasing.md index aecc4f5da4..cf4d4a5dbd 100644 --- a/docs/en/development/releasing.md +++ b/docs/en/development/releasing.md @@ -57,15 +57,15 @@ the website lists stable releases only. `make docs-test` also runs this check as | --- | --- | | `openapi/powercontext.yaml` | `info.version`, the prepared release version. | | `src/powercontext/http/_generated/` | `API_VERSION` in `operations.py` and `info.version` in `schema.py`, regenerated through `make api-generate`. | -| `README.md`, `README_CN.md`, `README_JP.md` | Current release prose, pinned package installation commands, and matching Agent integration tag. | -| `docs/en/docs/get-started/quickstart.md`, `docs/zh/docs/get-started/quickstart.md` | Current release and matching installation/tag instructions. | -| `docs/en/docs/get-started/install-and-run.md`, `docs/zh/docs/get-started/install-and-run.md` | Page description, current release prose, package/tag pins, upgrade target, and Python Client installation. | +| `docs/en/docs/get-started/install-and-run.md`, `docs/zh/docs/get-started/install-and-run.md` | Explicit version examples, package/tag pins, and Python Client installation. | | `integrations/dsh/plugins/powercontext/README.md` | Server installation pin and the matching `setup dsh --ref` tag. | The installation guides also contain historical migration versions. Those references are preserved. When adding another current-release installation example, include it in the version tooling's managed paths and patterns so the next release updates and checks it. +README and Quick Start install the latest stable release through the scripts and derive integration tags from the installed CLI. Release bumps preserve these instructions and the independent uv bootstrap version. + ### Derived from another source | Value | Source and handling | diff --git a/docs/en/docs/get-started/install-and-run.md b/docs/en/docs/get-started/install-and-run.md index e806f7432a..107850959f 100644 --- a/docs/en/docs/get-started/install-and-run.md +++ b/docs/en/docs/get-started/install-and-run.md @@ -1,6 +1,6 @@ --- title: Install and run -description: Install PowerContext 1.2.0 and run the local Server. +description: Install the latest PowerContext release with Bash or PowerShell, configure mirrors, and run the Server. --- # Install and run @@ -22,29 +22,109 @@ Windows CLI, Server, and personal-service support is experimental. Each Agent Ho requirements. Examples using Bash syntax require a Bash environment and cannot be pasted directly into PowerShell. Embedded seekDB is unavailable on Windows. +## Install with the recommended script + +The script installs the CLI and local Server in an isolated uv tool environment. It reuses compatible uv/Python, +or installs uv and Python 3.12 in user-owned directories. It does not need administrator privileges. + +macOS or Linux (Bash; `curl` or `wget` is required): + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +``` + +Windows (PowerShell 5.1 or newer): + +```powershell +powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1))) --no-hosts" +``` + +Apply the PATH command printed at completion before running `powercontext` in your terminal. The installer does not +edit shell startup files or the persistent Windows PATH. `UV_INSTALL_DIR` selects where a missing uv is installed; +`UV_TOOL_BIN_DIR` selects the PowerContext executable directory. + +`--no-hosts` installs software without prompting for Agent integrations. Omit it in an interactive terminal to open +`powercontext setup select`, or pass `--host codex` (repeatable) to select integrations explicitly. Agent setup needs +Git and each host's prerequisites. A pipeline still reads host choices from the terminal; unattended execution must +use `--no-hosts` or `--host`. The [capability matrix](../integrations/capabilities.md) describes host support. + +For an existing remote Server, add `--profile client` to install only the CLI and Client dependencies. The default +`--profile local` includes the local Server. Neither profile starts a Server, registers a service, or overwrites +configuration or data. For a local installation, continue with `powercontext config init` and [Quick Start](quickstart.md). +Client-only installations use endpoint settings in the [remote connection guide](../operate/connect-remote-server.md). + ## Choose a version -These instructions use PowerContext 1.2.0. Keep the package and Agent integration on -the same version: package `1.2.0` and Git tag `powercontext-v1.2.0`. +The default `--version latest` installs or upgrades to the newest stable release available from the selected index +and compatible with the chosen Python. It excludes prereleases. `--version` accepts an exact release, including an +explicit prerelease such as `1.3.0rc1`; it never substitutes a different version if that release is unavailable. + +For example, install PowerContext 1.2.0 with its Codex integration: ```bash -uv tool install --force "powercontext[cli,server]==1.2.0" -powercontext setup codex --ref powercontext-v1.2.0 +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.2.0 --host codex ``` -Check the [capability matrix](../integrations/capabilities.md) for host support and maintenance status. -Capabilities marked `experimental` remain experimental in this release. +This selects package `1.2.0` and Git tag `powercontext-v1.2.0`. With `latest`, the installer reads the installed CLI's +version and uses its matching tag; it never uses the moving `master` branch for host setup. To add an integration later: -## Install the application +```bash +powercontext setup codex --ref "powercontext-v$(powercontext --version)" +``` -You need Python 3.11 or newer, Git, and [`uv`](https://docs.astral.sh/uv/) on macOS, Linux, or Windows. Then install -PowerContext from PyPI: +In PowerShell, the same double-quoted expression works. An integration failure leaves the installed Runtime usable +and exits with an error and a retry instruction. Installation success does not establish Server readiness or host +workflow correctness; use the checks below and the integration's own guide. + +## Retry dependency downloads with a mirror + +Package indexes, uv binaries, and Python distributions are separate downloads. Changing the package index does not +change the uv or Python download location. + +| Download | Explicit setting | Automatic China source | Global source | +| --- | --- | --- | --- | +| PowerContext and Python packages | `--index-url URL`, uv index environment variables or `uv.toml` | Tsinghua PyPI mirror | PyPI | +| uv installer | `POWERCONTEXT_UV_INSTALLER_URL` | USTC uv release mirror | Astral installer | +| uv binaries | `UV_DOWNLOAD_URL` or `UV_INSTALLER_GITHUB_BASE_URL` | USTC uv release mirror | Astral release channels | +| Python distributions | `UV_PYTHON_INSTALL_MIRROR` | NJU python-build-standalone mirror | uv default channels | + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts --region cn +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts --index-url https://pypi.org/simple +``` + +`--region auto|cn|global` overrides `POWERCONTEXT_INSTALL_REGION`. Auto selection uses a named local timezone, then the +locale territory, then global; it makes no geolocation request. An unavailable automatic mirror can fall back to the +official source. Once a package index passes its availability/version check, uv reports subsequent resolution or +artifact failures without retrying installation against another index. Explicit sources never fall back automatically. +A mirror may lag PyPI: `latest` means the newest compatible stable release on the chosen index. + +Existing uv index settings and configuration files take precedence over automatic package mirrors. An explicit +`--index-url` overrides the default index only; additional uv indexes keep their priority. Private index credentials +belong in uv's authentication configuration, not in script arguments. uv does not read `PIP_INDEX_URL` or +`PIP_EXTRA_INDEX_URL`. Existing Python mirror settings and `uv.toml` also suppress automatic Python mirror selection. +`UV_ASTRAL_MIRROR_URL` is passed through for uv versions that support it. These settings are scoped to the installation; +your persistent package-manager configuration is unchanged. An existing uv is reused without upgrading it. + +To inspect the installer or pass several options in PowerShell, save it first: + +```powershell +irm https://powercontext.oceanbase.io/install.ps1 -OutFile install.ps1 +powershell -ExecutionPolicy Bypass -File .\install.ps1 --no-hosts --region cn --version 1.2.0 +``` + +Cached reinstallation with `UV_OFFLINE=1` can work when uv, compatible Python, and all required packages are already +present. This is not an offline distribution bundle; missing downloads fail explicitly. + +## Manual package or source installation + +If you already manage Python 3.11+ and [uv](https://docs.astral.sh/uv/), you can install the package directly: ```bash uv tool install --force "powercontext[cli,server]==1.2.0" ``` -For a source installation of the same version: +For a source installation of the same version (requires Git): ```bash uv tool install --force "powercontext[cli,server] @ git+https://github.com/oceanbase/powercontext.git@powercontext-v1.2.0" @@ -179,10 +259,10 @@ The Dashboard must be explicitly enabled; static Bearer authentication is option [Deploy the Server](../operate/deploy-server.md). Remote plaintext HTTP connections require explicit client consent; see [Connect to a remote Server](../operate/connect-remote-server.md). -To upgrade to 1.2.0: +To upgrade to the latest stable version, rerun the installer. To keep an exact release, add `--version`: ```bash -uv tool install --force "powercontext[cli,server]==1.2.0" +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts ``` To replace the installed tool with another Git ref: diff --git a/docs/en/docs/get-started/quickstart.md b/docs/en/docs/get-started/quickstart.md index 50dc3453b4..f9a107e5ef 100644 --- a/docs/en/docs/get-started/quickstart.md +++ b/docs/en/docs/get-started/quickstart.md @@ -6,11 +6,11 @@ description: Configure full memory, connect Codex, and verify Source capture, To # Quick Start Start with installation, discuss a project in Codex, watch its input become Source evidence and an evolving topic, -then recover the decisions in a new session. These instructions use PowerContext 1.2.0, -with the Agent plugin from the matching `powercontext-v1.2.0` tag. +then recover the decisions in a new session. The installer selects the latest stable release from your package index; +the Agent plugin uses the tag matching the installed version. -You need macOS or Linux, Python 3.11+, Git, [uv](https://docs.astral.sh/uv/getting-started/installation/), -and an installed Codex CLI. Full memory also needs working Generation and Embedding model APIs: +You need macOS or Linux, Git, and an installed Codex CLI. The script provisions uv and Python 3.11+ when needed. +For Windows PowerShell, version selection, and mirrors, see [Install and run](install-and-run.md). Full memory also needs working Generation and Embedding model APIs: prepare their base URLs, model names, and API keys. Signing into a Codex or Claude subscription does not automatically provide these APIs to the PowerContext Server. Without separate model APIs, select basic memory to test explicit saves and recall; that does not enable automatic Topic Memory. @@ -18,7 +18,12 @@ and recall; that does not enable automatic Topic Memory. ## 1. Install and open the wizard ```bash -uv tool install --force "powercontext[cli,server]==1.2.0" +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +``` + +Apply the PATH command printed by the installer, then configure PowerContext: + +```bash mkdir -p ~/powercontext-demo cd ~/powercontext-demo powercontext config init --language en --output .env @@ -96,7 +101,7 @@ Reload the client settings and install the matching plugin: set -a . ./.env set +a -powercontext setup codex --ref powercontext-v1.2.0 +powercontext setup codex --ref "powercontext-v$(powercontext --version)" powercontext doctor codex codex ``` diff --git a/docs/en/docs/operate/connect-remote-server.md b/docs/en/docs/operate/connect-remote-server.md index f777b1f678..92225caa48 100644 --- a/docs/en/docs/operate/connect-remote-server.md +++ b/docs/en/docs/operate/connect-remote-server.md @@ -9,12 +9,26 @@ Remote deployments should use HTTPS. IPv4/IPv6 loopback HTTP (including `localho PowerContext-owned clients reject other HTTP URLs unless you explicitly opt in. Private IP addresses and VPN addresses are still non-loopback: a private network does not itself encrypt HTTP. +## Install the Client CLI + +Use the installation script on the machine running your Agent. It provisions uv and Python if needed, and installs +the latest stable CLI and Client without local Server dependencies: + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --profile client --no-hosts +``` + +For Windows PowerShell, exact versions, and mirrors, see [Install and run](../get-started/install-and-run.md). +Apply the printed PATH command, then set `POWERCONTEXT_CLIENT_SERVER_URL` and, when required, +`POWERCONTEXT_CLIENT_API_TOKEN` in the client environment. Use `powercontext ready` to check the remote Server. +The local Server configuration wizard requires the `local` installation profile. + ## Guided setup Use the same source/ref for the installed PowerContext tool and the integration. On the machine that runs your agent: ```bash -powercontext setup claude-code --source oceanbase/powercontext --ref master \ +powercontext setup claude-code --source oceanbase/powercontext --ref "powercontext-v$(powercontext --version)" \ --server-url http://192.0.2.10:8000 ``` diff --git a/docs/en/rfcs/0000-script-installation.md b/docs/en/rfcs/0000-script-installation.md new file mode 100644 index 0000000000..52318f2812 --- /dev/null +++ b/docs/en/rfcs/0000-script-installation.md @@ -0,0 +1,218 @@ +--- +title: Script Installation +description: Install the latest or an exact release with Bash and PowerShell, independent package and runtime mirrors, and explicit Agent selection. +--- + +- Proposal Name: `script_installation` +- Start Date: 2026-10-09 +- RFC PR: [oceanbase/powercontext#0000](https://github.com/oceanbase/powercontext/pull/0000) +- Related RFCs: [RFC 1733](1733-usability-and-agent-workflows.md), [RFC 1299](1299_local_server_availability_and_service_installation.md) + +# Summary + +Provide distribution-owned Bash and PowerShell entry points that install PowerContext through uv. The default +release is the latest stable version from the selected package index; `--version` selects an exact release. +The scripts provision missing uv and Python, install the selected Runtime profile, and optionally invoke the +existing Agent integration adapters with a tag matching the installed Runtime. + +Installation, configuration, diagnostics, and service operation have separate responsibilities. Installing software +does not configure inference providers, start a Server, or register a persistent service. The scripts and their +installation guide are the first recommended path in the READMEs and Quick Start. + +# Motivation + +A normal installation should not require users to coordinate Python discovery, uv installation, package extras, +package indexes, and integration Git refs. Package downloads, uv binaries, and Python distributions also use different +sources: changing the PyPI index alone cannot fix an inaccessible Python download. + +Installation places a versioned component and registers explicitly selected integrations. Configuration controls +mutable values such as Server URL, Scope, and capture policy. Diagnostics observe the resulting environment. +Keeping these responsibilities distinct lets installation improve independently of configuration storage, shared +Hooks, an operations tool, or service lifecycle changes. + +The intended outcome is one installation entry point per shell, reuse of existing dependencies, predictable version +selection, explicit host selection, recoverable partial failures, and verification through the installed product. + +# Guide-level explanation + +## Install a Runtime profile + +On macOS or Linux: + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +``` + +On Windows, using PowerShell 5.1 or newer: + +```powershell +powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1))) --no-hosts" +``` + +`local`, the default profile, installs CLI, Client, and local Server dependencies. `--profile client` installs CLI and +Client dependencies for an existing Server. Profile and host selection are independent: choosing a database role does +not select an Agent host, and selecting Codex does not change the Runtime profile. + +The installer reuses uv and compatible system or uv-managed Python. Otherwise it provisions uv and Python 3.12 in +user-owned locations. It prints the actual Runtime version, profile, executable directories, PATH command, and next +configuration command. It does not change persistent PATH settings. Apply the printed PATH command before continuing. + +## Select releases and integrations + +`--version latest` is the default. Repeating it checks for newer stable releases on the configured index. For a +repeatable package selection, use an exact version: + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.2.0 --host codex +``` + +`--host` is repeatable. `--no-hosts` skips integration setup. With neither option, an interactive terminal opens +`powercontext setup select`; without a terminal the installer fails before provisioning. Piped Bash installation +uses `/dev/tty` for selection, separate from script input. Git and each selected host's own prerequisites are required. + +The installed CLI supplies the exact version for `powercontext-vVERSION`. Host setup never resolves `latest` again +and never defaults to `master`. If a host installation fails, the Runtime remains installed and the script returns +nonzero with instructions to retry setup at that tag. Each adapter supplies its existing host-specific result. + +For a local installation, continue with `powercontext config init`, then `powercontext server run --env-file .env`. +Client-only installations follow the remote connection guide to set the Server endpoint and authentication. Persistent services remain an explicit operation. + +## Choose download sources + +`--region auto|cn|global` overrides `POWERCONTEXT_INSTALL_REGION`. Automatic selection checks local named timezone, +then locale territory, then uses global sources. It does not call a network location service. + +| Component | Global source | Automatic China source | Explicit control | +| --- | --- | --- | --- | +| PowerContext and dependencies | PyPI | Tsinghua PyPI mirror | `--index-url`, uv index settings and configuration | +| uv installer and binaries | Astral channels | USTC release mirror | `POWERCONTEXT_UV_INSTALLER_URL`, `UV_DOWNLOAD_URL`, uv installer mirror variables | +| Python | uv default channels | NJU python-build-standalone mirror | `UV_PYTHON_INSTALL_MIRROR`, uv Python download configuration | + +Existing uv configuration suppresses automatic package mirror selection. An explicit `--index-url` changes only the +default index; additional indexes keep uv's priority rules. uv owns authentication and index resolution. Script URL +arguments must use HTTPS and omit credentials; credentials belong in uv configuration. pip index environment variables +are not uv configuration. Python and uv download overrides are independent of package indexes. + +# Reference-level explanation + +## Installation responsibilities + +The entry points are `website/public/install.sh` and `website/public/install.ps1`, served as static website assets. +Their supported options and failure behavior are equivalent. They own bootstrap and sequencing; uv owns environment +creation and package resolution, while existing `powercontext setup` adapters own integration installation. + +The effective installation inputs are: + +```text +Package requirement + Runtime profile + explicit Host selection = installation operation +``` + +The package manager resolves the requirement before replacing the tool environment. Once installed, the CLI's exact +version is verified and used for all host setup. This keeps one release coordinate for the operation without adding a +second package resolver or introducing a release manifest dependency. + +The scripts preserve `powercontext setup`, `config`, `doctor`, and service commands. They do not move domain behavior +into shell, add a standalone Python installer engine, or change Runtime/public HTTP APIs. + +## Version and profile semantics + +- `latest` passes an unpinned profile requirement to `uv tool install --upgrade --prerelease disallow`. It means the + newest stable version compatible with the selected interpreter and configured sources, not necessarily the version + most recently uploaded to another mirror. Resolution failures are reported without substituting another requirement. +- An exact `X.Y.Z`, optionally followed by `aN`, `bN`, or `rcN`, uses `==VERSION`. Explicit prereleases are supported; + source refs, ranges, and URLs are not `--version` values. Releases before 0.1.0 are excluded. +- `local` uses `powercontext[cli,server]`; `client` uses `powercontext[cli]`, whose dependencies include the Client. + Rerunning with a different profile replaces that tool's dependency set. Independently added extras must be managed + through the documented manual installation path. +- The installed executable must report a release version. Exact requests must match it. The script prints success + only after this check. The user receives a usable Runtime even if subsequent host setup fails. + +uv is bootstrapped at an installer-controlled version; that version is independent of the PowerContext release. +An existing uv is reused rather than upgraded silently. Python discovery excludes virtual environments so a project +venv cannot become an accidental installation prerequisite. If no compatible Python 3.11+ is found, uv provisions +Python 3.12. uv remains responsible for platform, architecture, and wheel compatibility errors. + +## Mirror precedence and recovery + +User-specified uv/Python sources are preserved without fallback. For automatic China selection, an unavailable uv +mirror installer falls back to Astral; failed mirrored uv artifacts may use official sources. Python's exact build +URL is obtained from uv, and its mirrored artifact is checked before installation. If the automatic Python mirror +fails, installation retries with uv's default channels. + +Automatic package selection checks the PowerContext index page and, for exact requests, the listed version. An +unavailable or unsynchronized China index falls back to PyPI. After selection, uv owns dependency resolution and +artifact errors; the script does not retry a partially attempted tool installation against another index. +Explicit indexes bypass shell probing so uv can apply its authentication and configuration correctly. + +Shell environment changes are local to the installer process; PowerShell restores temporary source variables in +`finally`. Existing configuration files are neither parsed by shell nor rewritten. Only installer-owned temporary +downloads are cleaned. `UV_INSTALL_DIR`, `UV_TOOL_DIR`, and `UV_TOOL_BIN_DIR` retain their location controls. + +## Persistence and compatibility + +The installer does not read or rewrite `.env`, credentials, data directories, or database schemas. It does not stop +or restart a running service. Users follow the existing upgrade and migration instructions when starting an updated +Server. An exact-version retry may reuse cached packages; `latest` intentionally allows upgrades. + +`UV_OFFLINE=1` permits cached reinstallation only when uv, a compatible Python, and all dependencies are present. +It does not promise a complete offline distribution. A failure after bootstrapping leaves installed prerequisites +available for retry; a failure in host setup preserves the Runtime. There is no cross-component rollback or repair +of unrelated host configuration. + +Windows retains its experimental product status. Native acceptance runs on Linux, macOS, and Windows; host-specific +support still comes from each integration's capability contract. + +## Acceptance + +`tests/native/test_installation.py` runs the actual shell installer, uv, installed CLI, and HTTP Server. A wheel built +from the tested commit uses release-shaped metadata and an exact file/checksum constraint. Qualification includes: + +- missing uv/Python under global and China source selection, existing tools/configuration, and spaces/Unicode paths; +- real stable/prerelease wheel resolution through a controlled package index, default upgrade, exact selection, and + preserving the installed version when a requested release is unavailable; +- Client-only installation and profile changes, with no local Server state created by installation; +- `.env` generation and validation, readiness, Memory remember/search, cached offline reinstall, and restart readback; +- explicit index failure, non-interactive missing choices, piped Bash terminal selection, and the downloaded + PowerShell scriptblock entry point. + +A separate CI matrix executes the suite on all three operating systems. Release reference checks preserve script +`latest` defaults while updating explicit version examples. Website validation confirms documentation links and the +static build. Syntax checks alone do not establish Windows or macOS installation support, and these tests do not +claim that a real Agent host completes its capture/recall workflow. + +# Drawbacks + +Bash and PowerShell duplicate some sequencing and source-selection policy. Automatic mirrors add operational +availability dependencies. `latest` deliberately changes over time and is unsuitable for reproducible deployment +without an exact version. Matching tags coordinate releases but do not independently prove host compatibility. + +# Rationale and alternatives + +A uv-only command is retained for users who already manage Python and uv; it cannot bootstrap a clean machine. +Source installation is useful for development but is not the default release channel. Pinning the public script to +a PowerContext version would leave new users on stale releases until the website is republished. + +A standalone installer engine and immutable component manifest can serve richer installation plans. They are not +prerequisites for package bootstrap or reliable source controls. Delegating to existing package and host adapters +keeps this change independently deliverable without removing working CLI contracts. + +# Prior art + +[RFC 1408](https://github.com/oceanbase/powercontext/pull/1408) defines the separation of installation, configuration, +and diagnostics; independent Runtime profiles and host selection; component-level recovery; and explicit service +registration. [RFC 1299](1299_local_server_availability_and_service_installation.md) defines personal service lifecycle. +[Bub](https://github.com/bubbuild/bub/tree/main/website/public) provides Bash/PowerShell bootstrap entry points that +reuse uv and report configuration next steps. [uv](https://docs.astral.sh/uv/guides/tools/) supplies isolated tools, +Python provisioning, package indexes, and caches. + +# Unresolved questions + +No further cross-subsystem decision is required for this contract. Complete offline bundles, cryptographic installer +manifests, independently packaged host artifacts, and changes to configuration storage need separate designs. +The platform matrix must remain green before promotion; Windows product support remains experimental. + +# Future possibilities + +The scripts can bootstrap a distribution-owned installer engine if installation plans later need immutable component +manifests, independent integration versions, or durable per-component repair records. An offline bundle can include +all required artifacts explicitly. Neither extension should make Server startup or service registration implicit. diff --git a/docs/zh/development/releasing.md b/docs/zh/development/releasing.md index 21be90fa68..294d8ca23f 100644 --- a/docs/zh/development/releasing.md +++ b/docs/zh/development/releasing.md @@ -54,14 +54,14 @@ beta 和 RC 跳过网站条目检查。`make docs-test` 也会通过网站测试 | --- | --- | | `openapi/powercontext.yaml` | `info.version`,作为待发布版本。 | | `src/powercontext/http/_generated/` | `operations.py` 的 `API_VERSION` 和 `schema.py` 的 `info.version`,通过 `make api-generate` 重新生成。 | -| `README.md`、`README_CN.md`、`README_JP.md` | 当前版本介绍、固定包版本的安装命令及配套 Agent 集成 tag。 | -| `docs/en/docs/get-started/quickstart.md`、`docs/zh/docs/get-started/quickstart.md` | 当前版本介绍及匹配的安装命令、tag。 | -| `docs/en/docs/get-started/install-and-run.md`、`docs/zh/docs/get-started/install-and-run.md` | 页面描述、当前版本介绍、包版本和 tag、升级目标及 Python Client 安装命令。 | +| `docs/en/docs/get-started/install-and-run.md`、`docs/zh/docs/get-started/install-and-run.md` | 显式版本示例、包版本和 tag,以及 Python Client 安装命令。 | | `integrations/dsh/plugins/powercontext/README.md` | Server 安装版本及对应的 `setup dsh --ref` tag。 | 安装指南也包含历史迁移版本,这些引用会保留。新增表示当前发布版本的安装示例时,应同时将其纳入版本工具的 管理路径及匹配规则,使下次发布可以自动更新和检查。 +README 和快速开始通过脚本安装最新稳定版,并从实际安装的 CLI 派生集成 tag。版本更新保留这些说明及独立的 uv 引导版本。 + ### 从其他来源派生的版本 | 内容 | 来源及处理方式 | diff --git a/docs/zh/docs/get-started/install-and-run.md b/docs/zh/docs/get-started/install-and-run.md index c7eedae837..825a227961 100644 --- a/docs/zh/docs/get-started/install-and-run.md +++ b/docs/zh/docs/get-started/install-and-run.md @@ -1,6 +1,6 @@ --- title: 安装和运行 -description: 安装 PowerContext 1.2.0,并运行本地 Server。 +description: 使用 Bash 或 PowerShell 安装最新版 PowerContext,配置镜像并运行 Server。 --- # 安装和运行 @@ -20,29 +20,98 @@ description: 安装 PowerContext 1.2.0,并运行本地 Server。 Windows 的 CLI、Server 和个人服务支持为试验性;各 Agent Host 仍需满足自身的平台要求。 使用 Bash 语法的示例需要 Bash 环境,不可直接粘贴到 PowerShell。嵌入式 seekDB 不支持 Windows。 +## 使用推荐安装脚本 + +脚本在独立的 uv tool 环境中安装 CLI 和本地 Server。已有兼容的 uv/Python 时会直接复用;缺少时在用户目录安装 +uv 和 Python 3.12,无需管理员权限。 + +macOS 或 Linux(使用 Bash,需要 `curl` 或 `wget`): + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +``` + +Windows(PowerShell 5.1 或更新版本): + +```powershell +powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1))) --no-hosts" +``` + +先执行安装完成时打印的 PATH 命令,再在终端运行 `powercontext`。安装器不会修改 shell 启动文件或 Windows 的持久 +PATH。`UV_INSTALL_DIR` 指定缺少 uv 时的安装目录,`UV_TOOL_BIN_DIR` 指定 PowerContext 可执行文件目录。 + +`--no-hosts` 只安装软件,不询问 Agent 集成。在交互终端省略它会打开 `powercontext setup select`,也可以使用 +`--host codex` 显式选择集成;多个宿主可重复传入 `--host`。集成安装需要 Git 及对应宿主的前置条件。管道安装仍从终端 +读取宿主选择;无人值守执行必须传入 `--no-hosts` 或 `--host`。宿主支持范围见[能力矩阵](../integrations/capabilities.md)。 + +连接已有远程 Server 时,加上 `--profile client`,只安装 CLI 和 Client 依赖。默认 `--profile local` 包含本地 Server。 +两种模式都不会启动 Server、注册服务或覆盖配置与数据。本地安装使用 `powercontext config init`,继续阅读 +[快速开始](quickstart.md);Client-only 安装按[远程连接指南](../operate/connect-remote-server.md)设置地址和认证。 + ## 选择版本 -本页使用 PowerContext 1.2.0。包与 Agent 集成保持版本一致: -Python 包版本为 `1.2.0`,对应 Git tag 为 `powercontext-v1.2.0`。 +默认 `--version latest` 安装或升级到所选包源中与当前 Python 兼容的最新稳定版,排除预发布版本。 +`--version` 也支持指定准确版本,包括 `1.3.0rc1` 这样的显式预发布版本;源中没有指定版本时会报错,不会换成其他版本。 + +例如,安装 PowerContext 1.2.0 及其 Codex 集成: ```bash -uv tool install --force "powercontext[cli,server]==1.2.0" -powercontext setup codex --ref powercontext-v1.2.0 +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.2.0 --host codex ``` -宿主支持范围和维护状态见[能力矩阵](../integrations/capabilities.md)。 -标为 `experimental` 的能力在此版本中仍属于试验性能力。 +Python 包版本为 `1.2.0`,对应 Git tag 为 `powercontext-v1.2.0`。使用 `latest` 时,安装器读取实际安装的 CLI 版本, +并使用对应的 tag 安装集成,不会使用会移动的 `master` 分支。稍后添加集成时可以执行: -## 安装应用 +```bash +powercontext setup codex --ref "powercontext-v$(powercontext --version)" +``` -需要在 macOS、Linux 或 Windows 上准备 Python 3.11 或更新版本、Git 和 -[`uv`](https://docs.astral.sh/uv/),然后从 PyPI 安装 PowerContext: +PowerShell 也支持这里的双引号表达式。集成安装失败时,已安装的 Runtime 会保留,脚本返回错误并给出重试提示。 +安装成功不代表 Server 就绪或宿主工作流可用;仍需执行下文检查及集成文档中的验证。 + +## 使用镜像重试依赖下载 + +包索引、uv 二进制和 Python 发行版是三类独立下载。更换包索引不会改变 uv 或 Python 的下载位置。 + +| 下载内容 | 显式配置 | 中国区域自动源 | 全球默认源 | +| --- | --- | --- | --- | +| PowerContext 和 Python 包 | `--index-url URL`、uv 索引环境变量或 `uv.toml` | 清华 PyPI 镜像 | PyPI | +| uv 安装器 | `POWERCONTEXT_UV_INSTALLER_URL` | USTC uv release 镜像 | Astral 安装器 | +| uv 二进制 | `UV_DOWNLOAD_URL` 或 `UV_INSTALLER_GITHUB_BASE_URL` | USTC uv release 镜像 | Astral release 渠道 | +| Python 发行版 | `UV_PYTHON_INSTALL_MIRROR` | NJU python-build-standalone 镜像 | uv 默认渠道 | + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts --region cn +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts --index-url https://pypi.org/simple +``` + +`--region auto|cn|global` 优先于 `POWERCONTEXT_INSTALL_REGION`。自动选择依次参考本地命名时区、locale 地区,最后采用 +全球源,不请求网络定位服务。自动镜像不可用时可回退官方源;包索引通过可用性和版本检查后,后续解析或文件下载失败 +由 uv 报告,不会再换源重试安装。显式配置的源不会自动回退。镜像可能有同步延迟:`latest` 指所选源中的最新兼容稳定版。 + +已有的 uv 索引环境变量和配置文件优先于自动包镜像。`--index-url` 只覆盖默认索引,额外 uv 索引仍保留其优先级。 +私有源凭据应使用 uv 认证配置,不放在脚本参数里。uv 不读取 `PIP_INDEX_URL` 或 `PIP_EXTRA_INDEX_URL`。 +已有 Python 镜像设置和 `uv.toml` 同样会禁用自动 Python 镜像选择。`UV_ASTRAL_MIRROR_URL` 会透传给支持它的 uv 版本。 +这些设置只作用于本次安装,不改写持久包管理配置;已有 uv 会直接复用,不会自动升级。 + +需要检查脚本内容,或在 PowerShell 传入多个选项时,可以先保存脚本: + +```powershell +irm https://powercontext.oceanbase.io/install.ps1 -OutFile install.ps1 +powershell -ExecutionPolicy Bypass -File .\install.ps1 --no-hosts --region cn --version 1.2.0 +``` + +已有 uv、兼容 Python 和完整依赖缓存时,可以使用 `UV_OFFLINE=1` 重装。这不等于离线发行包;缺少下载内容时会明确报错。 + +## 手动安装包或源码 + +已自行管理 Python 3.11+ 和 [uv](https://docs.astral.sh/uv/) 时,可以直接安装包: ```bash uv tool install --force "powercontext[cli,server]==1.2.0" ``` -如需从源码安装同一版本: +如需从源码安装同一版本(需要 Git): ```bash uv tool install --force "powercontext[cli,server] @ git+https://github.com/oceanbase/powercontext.git@powercontext-v1.2.0" @@ -164,10 +233,10 @@ Server、客户端和 Agent 集成需一起升级。Dashboard 需要显式启用 见[部署 Server](../operate/deploy-server.md);远程明文 HTTP 连接需要客户端明确同意, 见[连接远程 Server](../operate/connect-remote-server.md)。 -升级到 1.2.0: +重新运行安装器可升级到最新稳定版;需要保持指定版本时,加上 `--version`: ```bash -uv tool install --force "powercontext[cli,server]==1.2.0" +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts ``` 使用其他 Git ref 替换现有工具: diff --git a/docs/zh/docs/get-started/quickstart.md b/docs/zh/docs/get-started/quickstart.md index 8cfd5add34..f9d23b9cf4 100644 --- a/docs/zh/docs/get-started/quickstart.md +++ b/docs/zh/docs/get-started/quickstart.md @@ -6,10 +6,10 @@ description: 通过配置向导安装完整记忆能力,接入 Codex,并验 # 快速开始 本页从安装开始,带你完成一次真实的记忆体验:在 Codex 中讨论项目,看到原始输入进入 Source、主题记忆生成并演进, -再在新会话中找回决策。以下命令使用 PowerContext 1.2.0,Agent 插件使用对应的 -`powercontext-v1.2.0` tag。 +再在新会话中找回决策。安装脚本默认选择包源中的最新稳定版,Agent 插件使用实际安装版本对应的 tag。 -需要 macOS 或 Linux、Python 3.11+、Git、[uv](https://docs.astral.sh/uv/getting-started/installation/) 和已安装的 Codex CLI。 +需要 macOS 或 Linux、Git 和已安装的 Codex CLI。脚本会按需安装 uv 和 Python 3.11+。 +Windows 的 PowerShell 入口和指定版本、镜像选项见[安装和运行](install-and-run.md)。 完整记忆还需要可用的 Generation 和 Embedding 模型 API;准备好各自的地址、模型名和 API key。 Codex 或 Claude 的订阅登录不会自动为 PowerContext Server 提供这些 API。只有 Agent 登录、没有模型 API 时, 仍可选择基础记忆,验收显式保存与召回,但不能据此期待自动 Topic Memory。 @@ -17,7 +17,12 @@ Codex 或 Claude 的订阅登录不会自动为 PowerContext Server 提供这些 ## 1. 安装并进入配置向导 ```bash -uv tool install --force "powercontext[cli,server]==1.2.0" +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +``` + +先执行安装器打印的 PATH 命令,再继续配置: + +```bash mkdir -p ~/powercontext-demo cd ~/powercontext-demo powercontext config init --language zh --output .env @@ -92,7 +97,7 @@ POWERCONTEXT_CODEX_SCOPE_ID=替换为返回的scope_id set -a . ./.env set +a -powercontext setup codex --ref powercontext-v1.2.0 +powercontext setup codex --ref "powercontext-v$(powercontext --version)" powercontext doctor codex codex ``` diff --git a/docs/zh/docs/operate/connect-remote-server.md b/docs/zh/docs/operate/connect-remote-server.md index e9b34116b1..5858814ce6 100644 --- a/docs/zh/docs/operate/connect-remote-server.md +++ b/docs/zh/docs/operate/connect-remote-server.md @@ -8,12 +8,24 @@ description: 配置客户端地址,并在需要时明确允许非环回明文 远程部署推荐使用 HTTPS。IPv4/IPv6 环回 HTTP(包括 `localhost`)默认可用;PowerContext 自己发起的其他 HTTP 连接需要显式同意。内网 IP、VPN 地址仍是非环回地址,处于内网不等于 HTTP 已加密。 +## 安装 Client CLI + +在运行 Agent 的机器上使用安装脚本。它会按需补齐 uv 和 Python,安装最新稳定版 CLI 和 Client,不安装本地 Server 依赖: + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --profile client --no-hosts +``` + +Windows PowerShell、指定版本和镜像选项见[安装和运行](../get-started/install-and-run.md)。 +先执行打印的 PATH 命令,再在客户端环境设置 `POWERCONTEXT_CLIENT_SERVER_URL`,需要认证时还要设置 +`POWERCONTEXT_CLIENT_API_TOKEN`。运行 `powercontext ready` 检查远程 Server。本地 Server 配置向导需要 `local` 安装模式。 + ## 使用引导安装 PowerContext 工具与插件应使用同一份 source/ref。在运行 Agent 的机器上执行: ```bash -powercontext setup claude-code --source oceanbase/powercontext --ref master \ +powercontext setup claude-code --source oceanbase/powercontext --ref "powercontext-v$(powercontext --version)" \ --server-url http://192.0.2.10:8000 ``` diff --git a/docs/zh/rfcs/0000-script-installation.md b/docs/zh/rfcs/0000-script-installation.md new file mode 100644 index 0000000000..0c214c8e53 --- /dev/null +++ b/docs/zh/rfcs/0000-script-installation.md @@ -0,0 +1,191 @@ +--- +title: 脚本安装 +description: 通过 Bash 和 PowerShell 安装最新或指定版本,独立管理包与运行时镜像,并显式选择 Agent。 +--- + +- Proposal Name: `script_installation` +- Start Date: 2026-10-09 +- RFC PR: [oceanbase/powercontext#0000](https://github.com/oceanbase/powercontext/pull/0000) +- Related RFCs: [RFC 1733](1733-usability-and-agent-workflows.md)、[RFC 1299](1299_local_server_availability_and_service_installation.md) + +# Summary + +提供由发行版维护的 Bash 和 PowerShell 入口,通过 uv 安装 PowerContext。默认版本是所选包索引中的最新稳定版, +`--version` 可以指定准确版本。脚本补齐缺少的 uv 和 Python,安装所选 Runtime profile,并按需调用现有 Agent 集成 +适配器,使用与实际安装的 Runtime 对应的 tag。 + +安装、配置、诊断和服务运行各自承担独立职责。安装软件不会配置推理服务、启动 Server 或注册持久服务。 +README 和快速开始将脚本及其安装指南作为首选路径。 + +# Motivation + +正常安装不应要求用户自行协调 Python 发现、uv 安装、包 extras、包索引及集成 Git ref。Python 包、uv 二进制和 +Python 发行版也来自不同渠道:只更换 PyPI 索引无法解决 Python 下载不可达的问题。 + +安装负责放置版本化组件并注册显式选择的集成;配置管理 Server URL、Scope、采集策略等可变值;诊断观察安装结果。 +分清这些职责,安装体验就可以独立于配置存储、统一 Hooks、运维工具或服务生命周期改进。 + +预期结果是每种 shell 一个入口、复用已有依赖、明确的版本选择、显式的宿主选择、可恢复的部分失败, +以及通过实际安装产物进行验证。 + +# Guide-level explanation + +## 安装 Runtime profile + +macOS 或 Linux: + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +``` + +Windows,使用 PowerShell 5.1 或更新版本: + +```powershell +powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1))) --no-hosts" +``` + +默认 `local` profile 安装 CLI、Client 和本地 Server 依赖。`--profile client` 只安装连接已有 Server 所需的 CLI 和 +Client 依赖。Profile 与宿主选择相互独立:选择数据库角色不隐含选择 Agent,选择 Codex 也不会更改 Runtime profile。 + +安装器复用已有 uv,以及兼容的系统 Python 或 uv 管理的 Python;否则在用户目录补齐 uv 和 Python 3.12。 +完成后打印实际 Runtime 版本、profile、可执行文件目录、PATH 命令及下一步配置命令,不修改持久 PATH 设置。 +继续操作前,先执行打印的 PATH 命令。 + +## 选择发行版和集成 + +默认值为 `--version latest`,重复执行会检查已配置索引上的更新稳定版。需要可重复的包版本选择时,指定准确版本: + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.2.0 --host codex +``` + +`--host` 可以重复,`--no-hosts` 跳过集成安装。两者都不指定时,交互终端会打开 `powercontext setup select`; +没有交互终端则在安装依赖前报错。Bash 管道安装使用 `/dev/tty` 读取选择,将终端输入与脚本输入分开。 +安装集成仍需 Git 和各宿主自己的前置条件。 + +已安装的 CLI 提供准确版本,形成 `powercontext-vVERSION`。宿主安装不再次解析 `latest`,也不默认使用 `master`。 +宿主安装失败时,保留已安装的 Runtime,脚本返回非零状态,并提示按该 tag 重试 setup;各适配器提供其已有的宿主结果。 + +本地模式随后运行 `powercontext config init` 和 `powercontext server run --env-file .env`;Client-only 模式 +则按远程连接指南设置 Server 地址和认证。持久服务仍由用户显式注册。 + +## 选择下载来源 + +`--region auto|cn|global` 优先于 `POWERCONTEXT_INSTALL_REGION`。自动模式依次参考本地命名时区、locale 地区, +最后采用全球源,不调用网络定位服务。 + +| 组件 | 全球默认源 | 中国区域自动源 | 显式控制 | +| --- | --- | --- | --- | +| PowerContext 及依赖 | PyPI | 清华 PyPI 镜像 | `--index-url`、uv 索引设置及配置文件 | +| uv 安装器及二进制 | Astral 渠道 | USTC release 镜像 | `POWERCONTEXT_UV_INSTALLER_URL`、`UV_DOWNLOAD_URL`、uv 安装器镜像变量 | +| Python | uv 默认渠道 | NJU python-build-standalone 镜像 | `UV_PYTHON_INSTALL_MIRROR`、uv Python 下载配置 | + +已有 uv 配置时不自动选择包镜像。显式 `--index-url` 只更改默认索引,额外索引沿用 uv 的优先级。 +认证和索引解析由 uv 负责。脚本 URL 参数必须使用 HTTPS,且不包含凭据;凭据通过 uv 配置管理。 +pip 的索引环境变量不属于 uv 配置。Python 和 uv 下载覆盖项与包索引彼此独立。 + +# Reference-level explanation + +## 安装职责 + +入口为 `website/public/install.sh` 和 `website/public/install.ps1`,作为网站静态资源发布。 +两者支持相同的选项和失败语义。脚本负责引导与编排;uv 负责环境创建和包解析;现有 `powercontext setup` 适配器 +负责集成安装。 + +安装操作由以下输入确定: + +```text +包 requirement + Runtime profile + 显式宿主选择 = 安装操作 +``` + +包管理器在替换工具环境前解析 requirement。安装完成后验证 CLI 的准确版本,再用于全部宿主安装。 +这样一次操作具有统一的发行版坐标,无需新增包解析器,也不依赖另一个发行清单系统。 + +保留 `powercontext setup`、`config`、`doctor` 和服务命令。不把领域行为搬到 shell,不引入独立 Python 安装引擎, +也不修改 Runtime 或公共 HTTP API。 + +## 版本和 profile 语义 + +- `latest` 向 `uv tool install --upgrade --prerelease disallow` 传入未固定版本的 profile requirement。 + 它表示所选解释器及已配置源兼容的最新稳定版,不一定是另一镜像上最近上传的版本。解析失败直接报告,不改换 requirement。 +- 准确版本使用 `==VERSION`,格式为 `X.Y.Z`,可追加 `aN`、`bN` 或 `rcN`。支持显式预发布版本; + `--version` 不接受源码 ref、版本范围或 URL,也不支持 0.1.0 之前的版本。 +- `local` 使用 `powercontext[cli,server]`;`client` 使用 `powercontext[cli]`,后者已经包含 Client 依赖。 + 使用不同 profile 重跑会替换该工具的依赖集合;自行添加的 extras 应通过文档中的手动安装路径维护。 +- 安装后的可执行文件必须报告发行版本;显式请求必须与其一致。通过检查后才打印安装成功。 + 后续宿主安装失败不会移除可用的 Runtime。 + +uv 使用安装器维护的固定引导版本,与 PowerContext 发行版本无关。已有 uv 直接复用,不悄悄升级。 +Python 发现排除虚拟环境,避免项目 venv 意外成为安装前提。没有兼容的 Python 3.11+ 时,通过 uv 安装 Python 3.12。 +平台、架构和 wheel 兼容性错误由 uv 报告。 + +## 镜像优先级与恢复 + +保留用户显式指定的 uv/Python 来源,不做自动回退。中国区域自动选择时,uv 镜像安装器不可用则回退 Astral; +uv 镜像文件下载失败时可以尝试官方源。Python 的准确构建 URL 由 uv 提供,安装前检查对应镜像文件; +自动 Python 镜像安装失败后,使用 uv 默认渠道重试。 + +自动包源选择检查 PowerContext 索引页;准确版本还检查索引是否列出该版本。中国区域索引不可达或尚未同步指定版本时, +回退 PyPI。索引选定后,依赖解析和文件下载错误交给 uv 报告,不再换源重试已经尝试的工具安装。 +显式索引不经过 shell 探测,由 uv 正确处理认证和配置。 + +Shell 环境变化局限于安装器进程;PowerShell 在 `finally` 恢复临时来源变量。不用 shell 解析或改写已有配置文件, +只清理由安装器创建的临时下载目录。保留 `UV_INSTALL_DIR`、`UV_TOOL_DIR` 和 `UV_TOOL_BIN_DIR` 的位置控制。 + +## 持久化与兼容性 + +安装器不读取或改写 `.env`、凭据、数据目录或数据库 schema,也不停止或重启运行中的服务。 +启动升级后的 Server 时,用户按现有升级和迁移指南操作。准确版本重试可以使用缓存;`latest` 则明确允许升级。 + +`UV_OFFLINE=1` 只支持已有 uv、兼容 Python 和全部依赖时的缓存重装,不承诺完整离线发行包。 +引导依赖安装后发生失败,会保留这些依赖供重试;宿主安装失败保留 Runtime。不提供跨组件回滚, +也不修复无关宿主配置。 + +Windows 保持产品的试验性支持状态。原生验收在 Linux、macOS 和 Windows 执行;具体宿主支持范围仍由各集成的能力契约决定。 + +## 验收 + +`tests/native/test_installation.py` 执行真实 shell 安装器、uv、安装后的 CLI 和 HTTP Server。 +从被测提交构建 wheel,使用发行版形式的元数据和带校验和的准确文件约束。验收包括: + +- 全球源及中国区域源下缺少 uv/Python 的安装、已有工具与配置、包含空格和中文的路径; +- 通过受控包索引解析真实稳定版和预发布版 wheel、默认升级、指定版本,以及版本不存在时保留原安装; +- Client-only 安装及 profile 切换,安装过程不创建本地 Server 状态; +- `.env` 生成与验证、就绪检查、Memory 保存与搜索、离线缓存重装及重启后的回读; +- 显式索引失败、非交互模式缺少选择、Bash 管道终端选择,以及 PowerShell 下载脚本后执行 scriptblock 的入口。 + +独立 CI 矩阵在三个操作系统执行上述套件。发布引用检查保持脚本的 `latest` 默认值,同时更新显式版本示例。 +网站验证检查文档链接和静态构建。语法检查本身不证明 Windows 或 macOS 安装可用;这些测试也不声称 +真实 Agent 宿主已完成采集与召回工作流。 + +# Drawbacks + +Bash 和 PowerShell 会重复部分编排和来源选择策略。自动镜像增加外部可用性依赖。 +`latest` 会随时间变化,可重复部署需要指定版本。匹配 tag 能协调发行版,但不能单独证明宿主兼容性。 + +# Rationale and alternatives + +保留直接运行 uv 命令的路径,供已有 Python 和 uv 的用户使用;它无法引导干净机器。 +源码安装适合开发,不作为默认发行渠道。把公共脚本固定在某个 PowerContext 版本,会使新用户在网站重新发布前 +一直安装过期版本。 + +独立安装引擎和不可变组件清单可以支持更丰富的安装计划,但不是包引导及可靠来源控制的前提。 +复用现有包管理器和宿主适配器,使本项工作可以独立交付,同时保留可用的 CLI 契约。 + +# Prior art + +[RFC 1408](https://github.com/oceanbase/powercontext/pull/1408) 定义了安装、配置和诊断的职责分离, +独立的 Runtime profile 与宿主选择、组件级恢复和显式服务注册。 +[RFC 1299](1299_local_server_availability_and_service_installation.md) 定义个人服务生命周期。 +[Bub](https://github.com/bubbuild/bub/tree/main/website/public) 提供复用 uv 并报告后续配置步骤的 Bash/PowerShell 引导入口。 +[uv](https://docs.astral.sh/uv/guides/tools/) 提供独立工具环境、Python 安装、包索引和缓存。 + +# Unresolved questions + +接受本契约无需额外的跨子系统决策。完整离线包、带密码学校验的安装清单、独立打包的宿主产物以及配置存储变化, +需要各自设计。推广前必须保持平台矩阵通过,Windows 产品支持仍为试验性。 + +# Future possibilities + +当安装计划需要不可变组件清单、独立集成版本或持久组件修复记录时,脚本可以引导发行版维护的独立安装引擎。 +离线包也可以显式携带全部所需产物。这些扩展都不应隐式启动 Server 或注册服务。 diff --git a/scripts/release_version.py b/scripts/release_version.py index 0fbb6288a9..6509c33567 100644 --- a/scripts/release_version.py +++ b/scripts/release_version.py @@ -34,22 +34,17 @@ # independent plugin versions and third-party dependencies must retain their meaning. REFERENCE_PREFIXES = { "openapi/powercontext.yaml": (r"^ version: ",), - "README.md": (*INSTALL_REFERENCES, r"PowerContext ", r"^Install "), - "README_CN.md": (*INSTALL_REFERENCES, r"PowerContext ", r"^安装 "), - "README_JP.md": (*INSTALL_REFERENCES, r"PowerContext ", r"Git、uv、Agent CLI を用意して、"), - "docs/en/docs/get-started/quickstart.md": (*INSTALL_REFERENCES, r"PowerContext "), - "docs/zh/docs/get-started/quickstart.md": (*INSTALL_REFERENCES, r"PowerContext "), "docs/en/docs/get-started/install-and-run.md": ( *INSTALL_REFERENCES, r"PowerContext ", r"package `", - r"^To upgrade to ", + r"--version ", ), "docs/zh/docs/get-started/install-and-run.md": ( *INSTALL_REFERENCES, r"PowerContext ", r"Python 包版本为 `", - r"^升级到 ", + r"--version ", ), "integrations/dsh/plugins/powercontext/README.md": INSTALL_REFERENCES, } diff --git a/tests/native/test_installation.py b/tests/native/test_installation.py new file mode 100644 index 0000000000..62abd2ed8a --- /dev/null +++ b/tests/native/test_installation.py @@ -0,0 +1,366 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Acceptance through the installed CLI and a real HTTP Server, without model services.""" + +from __future__ import annotations + +import email +import hashlib +import os +import shutil +import socket +import subprocess +import sys +import time +import zipfile +from collections.abc import Iterator +from contextlib import contextmanager +from functools import partial +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from threading import Thread +from typing import Any + +import httpx +import pytest +from dotenv import dotenv_values + +ROOT = Path(__file__).parents[2] +WHEEL = os.environ.get("POWERCONTEXT_INSTALL_WHEEL") +pytestmark = pytest.mark.skipif(not WHEEL, reason="set POWERCONTEXT_INSTALL_WHEEL to run installation acceptance") + + +def run( + command: list[str], root: Path, env: dict[str, str], name: str, *, stdin: str = "", success: bool = True +) -> str: + with (root / f"{name}.log").open("w", encoding="utf-8") as log: + result = subprocess.run( + command, + cwd=root, + env=env, + input=stdin, + text=True, + stdout=log, + stderr=subprocess.STDOUT, + timeout=300, + check=False, + ) + output = (root / f"{name}.log").read_text(encoding="utf-8", errors="replace") + assert (result.returncode == 0) == success, output + return output + + +def request(url: str, token: str, body: dict[str, Any] | None = None) -> dict[str, Any]: + headers = {"Authorization": f"Bearer {token}"} if token else {} + with httpx.Client(trust_env=False, timeout=5, headers=headers) as client: + response = client.get(url) if body is None else client.post(url, json=body) + response.raise_for_status() + return response.json() + + +@contextmanager +def running_server(cli: str, root: Path, env: dict[str, str], token: str, name: str) -> Iterator[str]: + with socket.socket() as listener: + listener.bind(("127.0.0.1", 0)) + port = listener.getsockname()[1] + url = f"http://127.0.0.1:{port}" + log_path = root / f"{name}.log" + with log_path.open("w", encoding="utf-8") as log: + process = subprocess.Popen( + [cli, "server", "run", "--env-file", str(root / ".env"), "--host", "127.0.0.1", "--port", str(port)], + cwd=root, + env=env, + stdin=subprocess.DEVNULL, + stdout=log, + stderr=subprocess.STDOUT, + ) + try: + deadline = time.monotonic() + 90 + while time.monotonic() < deadline and process.poll() is None: + try: + request(url + "/health/ready", token) + break + except (httpx.TransportError, httpx.HTTPStatusError): + time.sleep(0.2) + else: + pytest.fail(log_path.read_text(encoding="utf-8", errors="replace")) + yield url + finally: + process.terminate() + try: + process.wait(timeout=15) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + + +def reinstall_offline( + installer: list[str], root: Path, env: dict[str, str], version: str, *, interactive: bool +) -> None: + if not interactive: + run(installer, root, env, "reinstall-offline") + return + # Regression: a piped installer must still allow terminal input at Agent selection. + controller = """import errno, os, pty, sys +pid, terminal = pty.fork() +if pid == 0: + os.execv('/bin/bash', ['/bin/bash', '-o', 'pipefail', '-c', + 'cat "$1" | /bin/bash -s -- --version "$2"', 'installer-pipeline', *sys.argv[1:]]) +try: + os.write(terminal, b'\\n') + while True: + try: + data = os.read(terminal, 4096) + except OSError as error: + if error.errno == errno.EIO: + break + raise + if not data: + break + os.write(1, data) +finally: + os.close(terminal) +_, status = os.waitpid(pid, 0) +sys.exit(os.waitstatus_to_exitcode(status)) +""" + output = run( + [sys.executable, "-c", controller, str(ROOT / "website/public/install.sh"), str(version)], + root, + env, + "reinstall-offline", + ) + assert "Select hosts" in output + + +def utilities_without_python_or_uv(root: Path, system_path: str) -> Path: + """Keep OS utilities available while removing Python and uv from PATH discovery.""" + utilities = root / "utilities" + utilities.mkdir() + for directory in system_path.split(os.pathsep): + for executable in Path(directory).iterdir(): + if executable.name.startswith(("python", "pypy", "uv")) or not executable.is_file(): + continue + target = utilities / executable.name + if not target.exists(): + target.symlink_to(executable) + return utilities + + +@pytest.mark.parametrize("environment", ["bootstrap-global", "bootstrap-cn", "existing"]) +def test_install_configure_remember_and_reinstall(tmp_path: Path, environment: str) -> None: + assert WHEEL is not None + wheel = Path(WHEEL).resolve() + with zipfile.ZipFile(wheel) as archive: + metadata = next(name for name in archive.namelist() if name.endswith(".dist-info/METADATA")) + version = email.message_from_bytes(archive.read(metadata))["Version"] + # The direct URL and checksum constraint make this a test of the current build. + constraints = tmp_path / "constraints.txt" + checksum = hashlib.sha256(wheel.read_bytes()).hexdigest() + constraints.write_text(f"powercontext @ {wheel.as_uri()}#sha256={checksum}\n", encoding="utf-8") + home_dir = tmp_path / "home with spaces 测试" + home_dir.mkdir() + env = { + key: value + for key, value in os.environ.items() + if not key.startswith(("UV_", "PIP_", "PYTHON", "POWERCONTEXT_")) and key != "VIRTUAL_ENV" + } + env.update( + HOME=str(home_dir), + USERPROFILE=str(home_dir), + UV_TOOL_DIR=str(tmp_path / "tools"), + UV_TOOL_BIN_DIR=str(tmp_path / "bin"), + UV_CACHE_DIR=str(tmp_path / "cache"), + UV_PYTHON_INSTALL_DIR=str(tmp_path / "python"), + UV_NO_CONFIG="1", + UV_CONSTRAINT=str(constraints), + UV_NO_PROGRESS="1", + POWERCONTEXT_HOME=str(tmp_path / "state"), + ) + if sys.platform == "win32": + # Python inherits PowerShell 7 module paths, which Windows PowerShell cannot load. + env.pop("PSMODULEPATH", None) + shell = shutil.which("powershell.exe") + assert shell is not None + installer = [ + shell, + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + str(ROOT / "website/public/install.ps1"), + ] + if environment == "bootstrap-cn": + # Exercise the documented downloaded-scriptblock entry point as well as -File. + wrapper = tmp_path / "downloaded-installer.ps1" + script_path = str(ROOT / "website/public/install.ps1").replace("'", "''") + wrapper.write_text( + f"& ([scriptblock]::Create([IO.File]::ReadAllText('{script_path}'))) @args\n", encoding="utf-8" + ) + installer[-1] = str(wrapper) + cli = str(tmp_path / "bin/powercontext.exe") + system_path = os.pathsep.join([str(Path(os.environ["SYSTEMROOT"]) / "System32"), os.environ["SYSTEMROOT"]]) + else: + installer = ["/bin/bash", str(ROOT / "website/public/install.sh")] + cli = str(tmp_path / "bin/powercontext") + system_path = os.defpath + installer += ["--no-hosts", "--version", str(version)] + if environment.startswith("bootstrap"): + if sys.platform == "win32": + env["PATH"] = system_path + env["UV_PYTHON_NO_REGISTRY"] = "1" + else: + env["PATH"] = str(utilities_without_python_or_uv(tmp_path, system_path)) + env.update(TZ="Asia/Shanghai", LC_ALL="C", LANG="en_US.UTF-8") + if environment == "bootstrap-global": + env["POWERCONTEXT_INSTALL_REGION"] = "cn" + installer += ["--region", "global"] + else: + env.pop("UV_NO_CONFIG") + uv_config = tmp_path / "uv.toml" + uv_config.write_text('[[index]]\nurl = "https://pypi.org/simple"\ndefault = true\n', encoding="utf-8") + env.update( + TZ="UTC", + LC_ALL="zh_CN.UTF-8", + UV_CONFIG_FILE=str(uv_config), + UV_PYTHON_DOWNLOADS="never", + UV_PYTHON_INSTALL_MIRROR="https://127.0.0.1:9/unavailable", + POWERCONTEXT_UV_INSTALLER_URL="https://127.0.0.1:9/unavailable", + ) + run(installer, tmp_path, env, "install") + if environment == "existing": + # An explicitly selected source must not silently fall back to a public index. + run([*installer, "--index-url", "https://127.0.0.1:9/simple"], tmp_path, env, "explicit-index", success=False) + assert run([cli, "--version"], tmp_path, env, "version").strip() == version + run([cli, "config", "init", "--template", "--output", ".env"], tmp_path, env, "configure", stdin="\n") + config = (tmp_path / ".env").read_bytes() + run([cli, "config", "validate", "--env-file", ".env"], tmp_path, env, "validate") + token = dotenv_values(tmp_path / ".env").get("POWERCONTEXT_SERVER_AUTH_TOKEN") or "" + text = "Installation acceptance preserves the release decision." + with running_server(cli, tmp_path, env, token, "server") as url: + scope = request(url + "/v1/scopes/default", token)["scope_id"] + request(url + "/v1/memory/remember", token, {"scope_id": scope, "kind": "decision", "text": text}) + query = {"scope_id": scope, "query": "release decision", "mode": "fts"} + found = request(url + "/v1/memory/search", token, query) + assert text in [hit["text"] for hit in found["hits"]] + env.update( + UV_OFFLINE="1", UV_PYTHON_DOWNLOADS="never", POWERCONTEXT_UV_INSTALLER_URL="https://127.0.0.1:9/unavailable" + ) + reinstall_offline( + installer, tmp_path, env, str(version), interactive=environment == "existing" and sys.platform != "win32" + ) + assert (tmp_path / ".env").read_bytes() == config + with running_server(cli, tmp_path, env, token, "server-restarted") as url: + found = request(url + "/v1/memory/search", token, query) + assert text in [hit["text"] for hit in found["hits"]] + + +def test_latest_exact_versions_and_client_profile(tmp_path: Path) -> None: + """Resolve real wheels from a controlled index with stable and prerelease candidates.""" + assert WHEEL is not None + uv = shutil.which("uv") + assert uv is not None + repository = tmp_path / "index" + packages = repository / "powercontext" + packages.mkdir(parents=True) + shutil.copyfile(WHEEL, packages / Path(WHEEL).name) + for version in ("1.2.1", "1.2.2rc1"): + run( + [uv, "build", "--project", str(ROOT), "--wheel", "--out-dir", str(packages)], + tmp_path, + {**os.environ, "SETUPTOOLS_SCM_PRETEND_VERSION": version}, + f"build-{version}", + ) + (packages / "index.html").write_text( + "\n".join(f'{wheel.name}' for wheel in sorted(packages.glob("*.whl"))), + encoding="utf-8", + ) + env = { + key: value + for key, value in os.environ.items() + if not key.startswith(("UV_", "PIP_", "PYTHON", "POWERCONTEXT_")) and key != "VIRTUAL_ENV" + } + env.update( + UV_NO_CONFIG="1", + UV_TOOL_DIR=str(tmp_path / "tools"), + UV_TOOL_BIN_DIR=str(tmp_path / "bin with spaces"), + UV_CACHE_DIR=str(tmp_path / "cache"), + UV_NO_PROGRESS="1", + POWERCONTEXT_HOME=str(tmp_path / "state"), + ) + if sys.platform == "win32": + shell = shutil.which("powershell.exe") + assert shell is not None + installer = [ + shell, + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + str(ROOT / "website/public/install.ps1"), + ] + cli = str(tmp_path / "bin with spaces/powercontext.exe") + python = str(tmp_path / "tools/powercontext/Scripts/python.exe") + else: + installer = ["/bin/bash", str(ROOT / "website/public/install.sh")] + cli = str(tmp_path / "bin with spaces/powercontext") + python = str(tmp_path / "tools/powercontext/bin/python") + server = ThreadingHTTPServer(("127.0.0.1", 0), partial(SimpleHTTPRequestHandler, directory=str(repository))) + worker = Thread(target=server.serve_forever, daemon=True) + worker.start() + env["UV_INDEX"] = f"http://127.0.0.1:{server.server_port}" + try: + # Invalid unattended input must fail before any tool installation. + run(installer, tmp_path, env, "missing-host-choice", success=False) + assert not Path(cli).exists() + client_install = [*installer, "--no-hosts", "--profile", "client"] + run([*client_install, "--version", "1.2.0"], tmp_path, env, "install-old") + assert run([cli, "--version"], tmp_path, env, "old-version").strip() == "1.2.0" + run(client_install, tmp_path, env, "install-latest") + assert run([cli, "--version"], tmp_path, env, "latest-version").strip() == "1.2.1" + if shutil.which("git"): + output = run( + [*installer, "--profile", "client", "--host", "unknown-host"], + tmp_path, + env, + "integration-failure", + success=False, + ) + assert "Runtime installed" in output + assert "powercontext-v1.2.1" in output + assert run([cli, "--version"], tmp_path, env, "retained-runtime").strip() == "1.2.1" + run( + [python, "-c", "from importlib.util import find_spec; assert find_spec('fastapi') is None"], + tmp_path, + env, + "client-without-server", + ) + assert not (tmp_path / "state").exists() + run([*client_install, "--version", "1.2.2rc1"], tmp_path, env, "install-prerelease") + assert run([cli, "--version"], tmp_path, env, "prerelease-version").strip() == "1.2.2rc1" + run([*client_install, "--version", "99.0.0"], tmp_path, env, "missing-version", success=False) + assert run([cli, "--version"], tmp_path, env, "preserved-version").strip() == "1.2.2rc1" + run([*installer, "--no-hosts", "--version", "1.2.0"], tmp_path, env, "switch-to-local") + run([python, "-c", "import fastapi"], tmp_path, env, "local-includes-server") + run([*client_install, "--version", "1.2.0"], tmp_path, env, "switch-to-client") + run( + [python, "-c", "from importlib.util import find_spec; assert find_spec('fastapi') is None"], + tmp_path, + env, + "server-dependencies-removed", + ) + finally: + server.shutdown() + worker.join(timeout=5) + server.server_close() diff --git a/tests/test_release_version.py b/tests/test_release_version.py index 16e4e2350c..4ace9ba1c7 100644 --- a/tests/test_release_version.py +++ b/tests/test_release_version.py @@ -24,16 +24,18 @@ import pytest INSTALL_PAGES = ( - "README.md", - "README_CN.md", - "README_JP.md", - "docs/en/docs/get-started/quickstart.md", - "docs/zh/docs/get-started/quickstart.md", "docs/en/docs/get-started/install-and-run.md", "docs/zh/docs/get-started/install-and-run.md", "integrations/dsh/plugins/powercontext/README.md", ) HISTORICAL_AND_INDEPENDENT_FILES = ( + "README.md", + "README_CN.md", + "README_JP.md", + "docs/en/docs/get-started/quickstart.md", + "docs/zh/docs/get-started/quickstart.md", + "website/public/install.sh", + "website/public/install.ps1", "docs/en/docs/operate/artifact-processing-migration.md", "docs/zh/docs/operate/artifact-processing-migration.md", "website/src/lib/releases.ts", @@ -84,11 +86,10 @@ def test_release_updates_installation_references_and_preserves_history(release_r for relative in INSTALL_PAGES: content = (release_repo / relative).read_text(encoding="utf-8") assert set(re.findall(r"powercontext\[[^\]\n]+\]==([^\s\"`]+)", content)) == {version}, relative - assert set(re.findall(r"powercontext-v([^\s\"`]+)", content)) == {version}, relative + assert set(re.findall(r"powercontext-v([0-9][^\s\"`]+)", content)) == {version}, relative if relative.startswith(("README", "docs/")): assert f"PowerContext {version}" in content, relative if re.search(r"(?:a|b|rc)[0-9]+$", version): - assert "stable release" not in content, relative assert f"stable PowerContext {version}" not in content, relative assert "正式版本" not in content, relative assert "正式リリース" not in content, relative @@ -96,9 +97,9 @@ def test_release_updates_installation_references_and_preserves_history(release_r english = (release_repo / "docs/en/docs/get-started/install-and-run.md").read_text(encoding="utf-8") chinese = (release_repo / "docs/zh/docs/get-started/install-and-run.md").read_text(encoding="utf-8") assert f"package `{version}`" in english - assert f"To upgrade to {version}:" in english + assert f"--version {version}" in english assert f"Python 包版本为 `{version}`" in chinese - assert f"升级到 {version}" in chinese + assert f"--version {version}" in chinese assert "Version 1.1.0 upgrades legacy" in english assert "1.0.0 also need" in english assert "1.1.0 会在 Server 启动时升级旧标签表约束" in chinese diff --git a/website/public/install.ps1 b/website/public/install.ps1 new file mode 100644 index 0000000000..74b4d6d55b --- /dev/null +++ b/website/public/install.ps1 @@ -0,0 +1,346 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +$Version = 'latest' +$UvVersion = '0.12.23' +$RuntimeProfile = 'local' +$Region = 'auto' +if ($env:POWERCONTEXT_INSTALL_REGION) { $Region = $env:POWERCONTEXT_INSTALL_REGION } +$IndexUrl = '' +$Hosts = @() +$NoHosts = $false +$Uv = '' +$TempDir = $null + +function Show-Help { + Write-Host @' +Install PowerContext on Windows. Python and uv need not be installed. + +Usage: powershell -ExecutionPolicy Bypass -File install.ps1 [options] + + --version VERSION Exact release version, or latest (default: latest stable). + --profile PROFILE local (CLI and Server, default) or client (CLI and Client). + --region REGION auto, cn, or global (default: POWERCONTEXT_INSTALL_REGION or auto). + --index-url URL HTTPS default package index for this installation. + --host HOST Install an Agent integration; repeat for multiple hosts. + --no-hosts Skip Agent integration setup. + -h, --help Show this help. + +Without host options, an interactive terminal opens powercontext setup select. +Without a terminal, --host or --no-hosts is required. + +Region priority: --region, POWERCONTEXT_INSTALL_REGION, named timezone, locale +territory, then global. No network location service is queried. +CN defaults: Tsinghua for PyPI, USTC for uv, NJU for Python. Global defaults use +PyPI and Astral's download channels. Unavailable automatic mirrors fall back to +official sources; explicit download settings are preserved without fallback. + +Download configuration (independent of the package index): + POWERCONTEXT_UV_INSTALLER_URL HTTPS uv PowerShell installer URL. + UV_DOWNLOAD_URL uv release artifact directory. + UV_INSTALLER_GITHUB_BASE_URL GitHub mirror base URL for uv binaries. + UV_PYTHON_INSTALL_MIRROR Mirror of Python distribution downloads. + UV_ASTRAL_MIRROR_URL Astral mirror for uv versions supporting it. +Existing additional uv indexes still take precedence over the default index. +'@ +} + +function Assert-HttpsUrl([string]$Value, [string]$Option) { + $Uri = $null + if (-not [Uri]::TryCreate($Value, [UriKind]::Absolute, [ref]$Uri) -or + $Uri.Scheme -ne 'https' -or -not $Uri.Host -or $Value -match '[@?#\s]') { + throw "$Option must be an HTTPS URL without credentials, query parameters, or fragments." + } +} + +function Save-Download([string]$Url, [string]$Path, [int]$Timeout = 30) { + Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec $Timeout -OutFile $Path +} + +function Select-Region { + $Source = 'explicit' + if ($Region -eq 'auto') { + $Source = 'timezone' + $Timezone = [TimeZoneInfo]::Local.Id + if ($env:TZ) { $Timezone = $env:TZ.TrimStart(':') } + if ($Timezone -in @('Asia/Shanghai', 'Asia/Chongqing', 'Asia/Chungking', 'Asia/Harbin', 'Asia/Urumqi', 'PRC', 'China Standard Time')) { + $Region = 'cn' + } + elseif ($Timezone -match '^(Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Europe|Indian|Pacific)/' -or + ($Timezone -ne 'UTC' -and -not $env:TZ -and $Timezone -match 'Standard Time$')) { + $Region = 'global' + } + else { + $Source = 'locale' + $LocaleName = '' + foreach ($Name in @('LC_ALL', 'LC_MESSAGES', 'LANG')) { + $LocaleName = [Environment]::GetEnvironmentVariable($Name) + if ($LocaleName) { break } + } + if (-not $LocaleName -or $LocaleName -match '^(C($|\.)|POSIX$)') { $LocaleName = [Globalization.CultureInfo]::CurrentCulture.Name } + $Region = 'global' + if ($LocaleName -match '[_-]CN($|[.@])') { $Region = 'cn' } + } + } + Write-Host "Download region: $Region ($Source)." + return $Region +} + +function Test-UvConfigFile { + if ($env:UV_CONFIG_FILE) { return $true } + if ($env:UV_NO_CONFIG -in @('1', 'true')) { return $false } + foreach ($Directory in @($env:APPDATA, $env:PROGRAMDATA)) { + if ($Directory -and (Test-Path -LiteralPath (Join-Path $Directory 'uv\uv.toml'))) { return $true } + } + return $false +} + +function Test-UvConfiguration { + foreach ($Name in @('UV_DEFAULT_INDEX', 'UV_INDEX', 'UV_INDEX_URL', 'UV_EXTRA_INDEX_URL', + 'UV_OFFLINE', 'UV_NO_INDEX', 'UV_FIND_LINKS')) { + if ([Environment]::GetEnvironmentVariable($Name)) { return $true } + } + return (Test-UvConfigFile) +} + +function Test-DownloadUrl([string]$Url) { + try { + Invoke-WebRequest -Uri $Url -UseBasicParsing -Method Head -TimeoutSec 10 | Out-Null + return $true + } + catch { return $false } +} + +function Test-IndexVersion([string]$Url) { + $IndexFile = Join-Path $TempDir 'index.html' + try { Save-Download ($Url.TrimEnd('/') + '/powercontext/') $IndexFile } + catch { + Write-Host "Package index is unreachable: $Url" + return $false + } + if ($Version -ne 'latest' -and (Get-Content -LiteralPath $IndexFile -Raw) -notmatch ('powercontext-' + [regex]::Escape($Version) + '(-|\.)')) { + Write-Host "Package index does not list PowerContext ${Version}: $Url" + return $false + } + return $true +} + +function Select-Index { + if (-not $IndexUrl -and (Test-UvConfiguration)) { + Write-Host 'Using existing uv configuration; uv will check the configured indexes.' + return + } + if ($env:PIP_INDEX_URL -or $env:PIP_EXTRA_INDEX_URL) { + Write-Host 'uv does not read pip index settings. Use --index-url or uv configuration.' + } + if ($IndexUrl) { + # uv owns index authentication and package resolution, including private indexes. + } + else { + $Indexes = @('https://pypi.org/simple') + if ($Region -eq 'cn') { $Indexes = @('https://pypi.tuna.tsinghua.edu.cn/simple', 'https://pypi.org/simple') } + foreach ($Candidate in $Indexes) { + Write-Host "Checking package index: $Candidate" + if (Test-IndexVersion $Candidate) { $IndexUrl = $Candidate; break } + } + if (-not $IndexUrl) { throw "No reachable package index lists PowerContext $Version. Use --index-url to select one." } + } + Write-Host "Package index: $IndexUrl" + $env:UV_DEFAULT_INDEX = $IndexUrl + return $IndexUrl +} + +function Install-UvIfMissing { + $Command = Get-Command uv -CommandType Application -ErrorAction SilentlyContinue + $UserUv = Join-Path $HOME '.local\bin\uv.exe' + if ($env:UV_INSTALL_DIR) { $UserUv = Join-Path $env:UV_INSTALL_DIR 'uv.exe' } + if ($Command) { $Uv = $Command.Source } + elseif (Test-Path -LiteralPath $UserUv -PathType Leaf) { $Uv = $UserUv } + else { + if ($env:UV_OFFLINE -in @('1', 'true')) { throw 'uv is not installed and offline mode disables downloads.' } + Write-Host "Installing uv $UvVersion in the user executable directory." + $Url = "https://astral.sh/uv/$UvVersion/install.ps1" + if ($env:POWERCONTEXT_UV_INSTALLER_URL) { $Url = $env:POWERCONTEXT_UV_INSTALLER_URL } + $Installer = Join-Path $TempDir 'uv-install.ps1' + $CustomDownload = $env:POWERCONTEXT_UV_INSTALLER_URL -or $env:UV_DOWNLOAD_URL -or $env:INSTALLER_DOWNLOAD_URL -or + $env:UV_INSTALLER_GITHUB_BASE_URL -or $env:UV_INSTALLER_GHE_BASE_URL -or $env:UV_ASTRAL_MIRROR_URL + $Downloaded = $false + if ($Region -eq 'cn' -and -not $CustomDownload) { + $Mirror = "https://mirrors.ustc.edu.cn/github-release/astral-sh/uv/$UvVersion" + try { + Write-Host "uv mirror: $Mirror" + Save-Download "$Mirror/uv-installer.ps1" $Installer + $Downloaded = $true + $env:UV_DOWNLOAD_URL = $Mirror + } + catch { Write-Host 'uv mirror installer unavailable; using the official installer.' } + } + if (-not $Downloaded) { + try { Save-Download $Url $Installer } + catch { throw 'Could not download uv. Check POWERCONTEXT_UV_INSTALLER_URL.' } + } + $env:UV_INSTALL_DIR = Split-Path -Parent $UserUv + $env:UV_NO_MODIFY_PATH = '1' + $PowerShell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' + & $PowerShell -NoProfile -ExecutionPolicy Bypass -File $Installer | Out-Host + if ($LASTEXITCODE -ne 0 -and $Downloaded) { + # The official PowerShell installer accepts only one UV_DOWNLOAD_URL. + Write-Host 'uv mirror installation failed; retrying uv with official sources.' + Remove-Item Env:UV_DOWNLOAD_URL + & $PowerShell -NoProfile -ExecutionPolicy Bypass -File $Installer | Out-Host + } + if ($LASTEXITCODE -ne 0) { throw 'uv installation failed. Check the installer output and configured download source.' } + $Uv = $UserUv + } + if (-not (Test-Path -LiteralPath $Uv -PathType Leaf)) { throw 'uv executable was not found after installation.' } + Write-Host "Using uv: $Uv" + $env:PATH = "$(Split-Path -Parent $Uv);$env:PATH" + return $Uv +} + +$SavedEnvironment = @{} +foreach ($Name in @('UV_DEFAULT_INDEX', 'UV_INSTALL_DIR', 'UV_NO_MODIFY_PATH', 'UV_DOWNLOAD_URL')) { + $SavedEnvironment[$Name] = [Environment]::GetEnvironmentVariable($Name) +} +try { + for ($Index = 0; $Index -lt $args.Count; $Index++) { + $Option = $args[$Index] + switch ($Option) { + { $_ -in '--version', '--profile', '--region', '--index-url', '--host' } { + $Index++ + if ($Index -ge $args.Count -or -not $args[$Index] -or $args[$Index].StartsWith('--')) { + throw "$Option requires a value." + } + switch ($Option) { + '--version' { $Version = $args[$Index] } + '--profile' { $RuntimeProfile = $args[$Index] } + '--region' { $Region = $args[$Index] } + '--index-url' { $IndexUrl = $args[$Index] } + '--host' { $Hosts += @('--host', $args[$Index]) } + } + } + '--no-hosts' { $NoHosts = $true } + { $_ -in '-h', '--help' } { Show-Help; exit 0 } + default { throw 'Unknown option. Run install.ps1 --help.' } + } + } + if ($Region -cnotin @('auto', 'cn', 'global')) { throw 'Use --region auto, cn, or global.' } + if ($RuntimeProfile -cnotin @('local', 'client')) { throw 'Use --profile local or client.' } + if (($Version -cne 'latest' -and $Version -notmatch '^\d+\.\d+\.\d+((a|b|rc)\d+)?$') -or $Version.StartsWith('0.0.')) { + throw 'Use latest or an exact package version starting at 0.1.0.' + } + if ($IndexUrl) { Assert-HttpsUrl $IndexUrl '--index-url' } + if ($env:POWERCONTEXT_UV_INSTALLER_URL) { Assert-HttpsUrl $env:POWERCONTEXT_UV_INSTALLER_URL 'POWERCONTEXT_UV_INSTALLER_URL' } + if ($NoHosts -and $Hosts.Count) { throw '--host and --no-hosts cannot be combined.' } + if (-not $NoHosts -and -not $Hosts.Count -and (-not [Environment]::UserInteractive -or [Console]::IsInputRedirected)) { + throw 'No interactive input. Pass --host HOST or --no-hosts.' + } + if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Use install.sh on macOS and Linux.' } + if (-not $NoHosts -and -not (Get-Command git -CommandType Application -ErrorAction SilentlyContinue)) { + throw 'Agent integration setup requires Git. Install Git or use --no-hosts.' + } + $TempDir = Join-Path ([IO.Path]::GetTempPath()) ('powercontext-install-' + [Guid]::NewGuid()) + [IO.Directory]::CreateDirectory($TempDir) | Out-Null + $Region = Select-Region + $Uv = Install-UvIfMissing + + $Python = $null + $FoundPython = $false + try { + $Python = & $Uv python find --system --no-project --no-python-downloads '>=3.11,<4' 2>$null + $FoundPython = $LASTEXITCODE -eq 0 -and $Python + } + catch { $FoundPython = $false } + if ($FoundPython) { + Write-Host "Using local Python: $Python" + } + else { + if ($env:UV_PYTHON_DOWNLOADS -in @('never', 'false', '0')) { throw 'No compatible local Python; UV_PYTHON_DOWNLOADS disables downloads.' } + if ($env:UV_OFFLINE -in @('1', 'true')) { throw 'No compatible local Python in offline mode.' } + $PythonArgs = @('python', 'install', '3.12') + $AutomaticMirror = $false + if ($Region -eq 'cn' -and -not ($env:UV_PYTHON_INSTALL_MIRROR -or $env:UV_ASTRAL_MIRROR_URL -or + $env:UV_PYTHON_DOWNLOADS_JSON_URL -or (Test-UvConfigFile))) { + $Downloads = & $Uv python list 'cpython@3.12' --only-downloads --show-urls --color never + if ($LASTEXITCODE -ne 0) { throw 'Could not resolve a Python download.' } + $Url = (@($Downloads)[0] -split '\s+')[-1] + if ($Url -match '^https://[^/]+/(?:github/|astral-sh/)?python-build-standalone/releases/download/(.+)$') { + $Mirror = 'https://mirror.nju.edu.cn/github-release/astral-sh/python-build-standalone' + if (Test-DownloadUrl "$Mirror/$($Matches[1])") { + Write-Host "Python mirror: $Mirror" + $PythonArgs += @('--mirror', $Mirror) + $AutomaticMirror = $true + } + else { Write-Host 'Python mirror does not provide the requested build; using uv default sources.' } + } + } + Write-Host 'No compatible local Python found. Installing Python 3.12.' + & $Uv @PythonArgs + if ($LASTEXITCODE -ne 0 -and $AutomaticMirror) { + Write-Host 'Python mirror installation failed; retrying with uv default sources.' + & $Uv python install 3.12 + } + if ($LASTEXITCODE -ne 0) { throw 'Python installation failed. Check uv output and UV_PYTHON_INSTALL_MIRROR.' } + $Python = & $Uv python find --system --no-project --no-python-downloads 3.12 + if ($LASTEXITCODE -ne 0 -or -not $Python) { throw 'Installed Python was not found.' } + } + $IndexUrl = Select-Index + $Requirement = 'powercontext[cli,server]' + if ($RuntimeProfile -eq 'client') { $Requirement = 'powercontext[cli]' } + $InstallArgs = @('tool', 'install', '--python', $Python.Trim(), '--no-python-downloads') + if ($Version -eq 'latest') { $InstallArgs += @('--upgrade', '--prerelease', 'disallow') } + else { $Requirement += "==$Version" } + $InstallArgs += $Requirement + if ($IndexUrl) { $InstallArgs += @('--default-index', $IndexUrl) } + & $Uv @InstallArgs + if ($LASTEXITCODE -ne 0) { throw 'Installation failed. Check uv indexes for packages and UV_PYTHON_INSTALL_MIRROR for Python.' } + $ToolBin = & $Uv tool dir --bin + if ($LASTEXITCODE -ne 0) { throw 'Could not locate the installed CLI.' } + $Cli = Join-Path $ToolBin.Trim() 'powercontext.exe' + $env:PATH = "$($ToolBin.Trim());$env:PATH" + $InstalledVersion = & $Cli --version + if ($LASTEXITCODE -ne 0) { throw 'The installed CLI could not start.' } + if ($InstalledVersion -notmatch '^\d+\.\d+\.\d+((a|b|rc)\d+)?$') { throw 'The installed CLI did not report a release version.' } + if ($Version -ne 'latest' -and $Version -ne $InstalledVersion) { throw 'The installed version differs from the requested release.' } + $Version = $InstalledVersion.Trim() + Write-Host "Runtime installed: $Version ($RuntimeProfile)" + $PathPrefix = ("$($ToolBin.Trim());$(Split-Path -Parent $Uv)").Replace("'", "''") + Write-Host ('For a new terminal: $env:Path = ''' + $PathPrefix + ';'' + $env:Path') + $SetupStatus = 0 + if (-not $NoHosts) { + & $Cli setup select --source oceanbase/powercontext --ref "powercontext-v$Version" @Hosts + $SetupStatus = $LASTEXITCODE + } + if ($RuntimeProfile -eq 'local') { + Write-Host "`nConfigure PowerContext with: powercontext config init" + Write-Host ' https://powercontext.oceanbase.io/en/docs/get-started/quickstart/' + Write-Host 'Then run: powercontext server run --env-file .env' + } + else { + Write-Host "`nConnect to your existing Server:" + Write-Host ' https://powercontext.oceanbase.io/en/docs/operate/connect-remote-server/' + } + if ($SetupStatus -ne 0) { throw "Runtime installed, but integration setup did not complete. Retry setup with powercontext-v$Version." } +} +catch { + [Console]::Error.WriteLine("error: $($_.Exception.Message)") + exit 1 +} +finally { + foreach ($Name in $SavedEnvironment.Keys) { [Environment]::SetEnvironmentVariable($Name, $SavedEnvironment[$Name]) } + if ($TempDir) { Remove-Item -LiteralPath $TempDir -Recurse -Force -ErrorAction SilentlyContinue } +} diff --git a/website/public/install.sh b/website/public/install.sh new file mode 100644 index 0000000000..fd878bfcad --- /dev/null +++ b/website/public/install.sh @@ -0,0 +1,369 @@ +#!/usr/bin/env bash +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -euo pipefail + +VERSION=latest +UV_VERSION=0.12.23 +PROFILE=local +REGION=${POWERCONTEXT_INSTALL_REGION:-auto} +PYTHON_BIN="" +INDEX_URL="" +HOSTS=() +NO_HOSTS=false +SETUP_INPUT=/dev/stdin +TEMP_DIR="" +UV_BIN="" +GUIDE=https://powercontext.oceanbase.io/en/docs/get-started/quickstart/ + +fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +cleanup() { + if [[ -n "$TEMP_DIR" ]]; then + rm -rf -- "$TEMP_DIR" + fi +} + +usage() { + cat <<'EOF' +Install PowerContext on macOS or Linux. Python and uv need not be installed. + +Usage: bash install.sh [--version VERSION] [--index-url URL] [--host HOST]... [--no-hosts] + + --version VERSION Exact package version, or latest (default: latest stable). + Integrations use the matching powercontext-vVERSION tag. + --profile PROFILE local (CLI and Server, default) or client (CLI and Client). + --region REGION auto, cn, or global (default: POWERCONTEXT_INSTALL_REGION or auto). + --index-url URL HTTPS default package index for this installation. + Existing additional uv indexes still take precedence. + --host HOST Install this Agent integration; repeat for multiple hosts. + --no-hosts Skip Agent integration setup. + -h, --help Show this help. + +Without host options, an interactive terminal opens powercontext setup select. +Without a terminal, --host or --no-hosts is required. + +Region priority: --region, POWERCONTEXT_INSTALL_REGION, named timezone, locale +territory, then global. No network location service is queried. +CN defaults: Tsinghua for PyPI, USTC for uv, NJU for Python. Global defaults use +PyPI and Astral's download channels. Unavailable automatic mirrors fall back to +official sources; explicit download settings are preserved without fallback. + +Download configuration (independent of the package index): + POWERCONTEXT_UV_INSTALLER_URL HTTPS uv installer script URL. + UV_DOWNLOAD_URL uv release artifact directory. + UV_INSTALLER_GITHUB_BASE_URL GitHub mirror base URL for uv binaries. + UV_PYTHON_INSTALL_MIRROR Mirror of python-build-standalone downloads. + UV_ASTRAL_MIRROR_URL Astral mirror for uv versions supporting it. +These settings are passed to the official installer and uv without modification. +Existing uv and compatible Python installations are reused. +EOF +} + +parse_args() { + while (($#)); do + case "$1" in + --version|--profile|--region|--index-url|--host) + (($# >= 2)) && [[ -n "$2" && "$2" != --* ]] || fail "$1 requires a value" + case "$1" in + --version) VERSION=$2 ;; + --profile) PROFILE=$2 ;; + --region) REGION=$2 ;; + --index-url) INDEX_URL=$2 ;; + --host) HOSTS+=(--host "$2") ;; + esac + shift 2 + ;; + --no-hosts) NO_HOSTS=true; shift ;; + -h|--help) usage; exit 0 ;; + *) fail "Unknown option. Run bash install.sh --help." ;; + esac + done + case "$REGION" in auto|cn|global) ;; *) fail "Use --region auto, cn, or global." ;; esac + case "$PROFILE" in local|client) ;; *) fail "Use --profile local or client." ;; esac + [[ "$VERSION" == latest || "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?$ ]] || fail "Use latest or an exact package version." + [[ "$VERSION" != 0.0.* ]] || fail "This installer supports releases starting at 0.1.0." + if [[ -n "$INDEX_URL" ]]; then + [[ "$INDEX_URL" == https://?* && "$INDEX_URL" != *[@\?#[:space:]]* ]] || + fail "Use an HTTPS index URL without credentials, query parameters, or fragments." + fi + if [[ -n "${POWERCONTEXT_UV_INSTALLER_URL:-}" ]]; then + [[ "$POWERCONTEXT_UV_INSTALLER_URL" == https://?* && "$POWERCONTEXT_UV_INSTALLER_URL" != *[@\?#[:space:]]* ]] || + fail "POWERCONTEXT_UV_INSTALLER_URL must be an HTTPS URL without credentials, query parameters, or fragments." + fi + if [[ "$NO_HOSTS" == true && ${#HOSTS[@]} -gt 0 ]]; then + fail "--host and --no-hosts cannot be combined." + fi + if [[ "$NO_HOSTS" == false && ${#HOSTS[@]} -eq 0 && ! -t 0 ]]; then + if [[ -t 1 ]] && { : /dev/null; then + SETUP_INPUT=/dev/tty + else + fail "No interactive input. Pass --host HOST or --no-hosts." + fi + fi +} + +download() { + local timeout=${3:-30} + if command -v curl >/dev/null 2>&1; then + curl --fail --location --silent --show-error --connect-timeout 5 --max-time "$timeout" "$1" -o "$2" + else + timeout=${3:-15} + wget --quiet --timeout="$timeout" --tries=1 "$1" -O "$2" + fi +} + +detect_region() { + local timezone=${TZ:-} locale_name source=explicit + if [[ "$REGION" == auto ]]; then + source=timezone + if [[ -z "$timezone" ]]; then + timezone=$(readlink /etc/localtime 2>/dev/null || true) + if [[ "$timezone" != */zoneinfo/* && -f /etc/timezone ]]; then + IFS= read -r timezone /dev/null || true) + fi + fi + case "$locale_name" in + *_CN|*_CN.*|*_CN@*|*-CN|*-CN.*|*-CN@*) REGION=cn ;; + *) REGION=global ;; + esac + ;; + esac + fi + printf 'Download region: %s (%s).\n' "$REGION" "$source" +} + +has_uv_config_file() { + [[ -n "${UV_CONFIG_FILE:-}" ]] && return 0 + case "${UV_NO_CONFIG:-}" in 1|true) return 1 ;; esac + [[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/uv/uv.toml" || -f /etc/uv/uv.toml ]] && return 0 + local directory + local config_dirs=() + IFS=: read -r -a config_dirs <<< "${XDG_CONFIG_DIRS:-/etc/xdg}" + for directory in "${config_dirs[@]}"; do + [[ -f "$directory/uv/uv.toml" ]] && return 0 + done + return 1 +} + +has_uv_configuration() { + # Let uv resolve its configuration and authentication; do not parse TOML in shell. + [[ -n "${UV_DEFAULT_INDEX:-}${UV_INDEX:-}${UV_INDEX_URL:-}${UV_EXTRA_INDEX_URL:-}${UV_OFFLINE:-}${UV_NO_INDEX:-}${UV_FIND_LINKS:-}" ]] && return 0 + has_uv_config_file +} + +url_available() { + if command -v curl >/dev/null 2>&1; then + curl --fail --location --silent --head --connect-timeout 5 --max-time 10 "$1" >/dev/null + else + wget --quiet --spider --timeout=10 --tries=1 "$1" + fi +} + +index_has_version() { + if ! download "${1%/}/powercontext/" "$TEMP_DIR/index.html"; then + printf 'Package index is unreachable: %s\n' "$1" >&2 + return 1 + fi + if [[ "$VERSION" != latest ]] && ! grep -Eq "powercontext-${VERSION//./\\.}(-|\.)" "$TEMP_DIR/index.html"; then + printf 'Package index does not list PowerContext %s: %s\n' "$VERSION" "$1" >&2 + return 1 + fi +} + +check_index() { + if [[ -z "$INDEX_URL" ]] && has_uv_configuration; then + printf '%s\n' 'Using existing uv configuration; uv will check the configured indexes.' + return + fi + if [[ -n "${PIP_INDEX_URL:-}${PIP_EXTRA_INDEX_URL:-}" ]]; then + printf '%s\n' 'uv does not read PIP_INDEX_URL or PIP_EXTRA_INDEX_URL. Use --index-url or uv configuration.' + fi + if [[ -n "$INDEX_URL" ]]; then + # uv owns index authentication and package resolution, including private indexes. + printf 'Package index: %s\n' "$INDEX_URL" + return + fi + + local selected_index + local indexes=(https://pypi.org/simple) + if [[ "$REGION" == cn ]]; then + indexes=(https://pypi.tuna.tsinghua.edu.cn/simple https://pypi.org/simple) + fi + for selected_index in "${indexes[@]}"; do + printf 'Checking package index: %s\n' "$selected_index" + if index_has_version "$selected_index"; then + INDEX_URL=$selected_index + printf 'Package index: %s\n' "$INDEX_URL" + return + fi + done + fail "No reachable package index lists PowerContext $VERSION. Check connectivity, the version, or use --index-url URL." +} + +ensure_uv() { + local uv_install_dir=${UV_INSTALL_DIR:-$HOME/.local/bin} + if command -v uv >/dev/null 2>&1; then + UV_BIN=$(command -v uv) + elif [[ -x "$uv_install_dir/uv" ]]; then + UV_BIN="$uv_install_dir/uv" + else + case "${UV_OFFLINE:-}" in 1|true) fail "uv is not installed and offline mode disables downloads." ;; esac + printf 'Installing uv %s in the user executable directory.\n' "$UV_VERSION" + local installer_url=${POWERCONTEXT_UV_INSTALLER_URL:-https://astral.sh/uv/$UV_VERSION/install.sh} + local mirror=https://mirrors.ustc.edu.cn/github-release/astral-sh/uv/$UV_VERSION + local official=https://releases.astral.sh/github/uv/releases/download/$UV_VERSION + # A custom installer may install another version; never pair it with our pinned mirror. + if [[ "$REGION" == cn && -z "${POWERCONTEXT_UV_INSTALLER_URL:-}${UV_DOWNLOAD_URL:-}${INSTALLER_DOWNLOAD_URL:-}${UV_INSTALLER_GITHUB_BASE_URL:-}${UV_INSTALLER_GHE_BASE_URL:-}${UV_ASTRAL_MIRROR_URL:-}" ]]; then + printf 'uv mirror: %s\n' "$mirror" + if download "$mirror/uv-installer.sh" "$TEMP_DIR/uv-install.sh"; then + # The official shell installer supports an ordered list of artifact sources. + UV_DOWNLOAD_URL="$mirror $official https://github.com/astral-sh/uv/releases/download/$UV_VERSION" \ + UV_INSTALL_DIR="$uv_install_dir" UV_NO_MODIFY_PATH=1 sh "$TEMP_DIR/uv-install.sh" || fail "uv installation failed." + else + printf '%s\n' 'uv mirror unavailable; using the official installer.' + download "$installer_url" "$TEMP_DIR/uv-install.sh" || fail "Could not download the uv installer." + UV_INSTALL_DIR="$uv_install_dir" UV_NO_MODIFY_PATH=1 sh "$TEMP_DIR/uv-install.sh" || fail "uv installation failed." + fi + else + download "$installer_url" "$TEMP_DIR/uv-install.sh" || fail "Could not download the uv installer. Check POWERCONTEXT_UV_INSTALLER_URL." + UV_INSTALL_DIR="$uv_install_dir" UV_NO_MODIFY_PATH=1 sh "$TEMP_DIR/uv-install.sh" || fail "uv installation failed. Check the configured download source." + fi + UV_BIN="$uv_install_dir/uv" + fi + [[ -x "$UV_BIN" ]] || fail "uv executable was not found after installation." + printf 'Using uv: %s\n' "$UV_BIN" + PATH="$(dirname "$UV_BIN"):$PATH" + export PATH +} + +ensure_python() { + if PYTHON_BIN=$("$UV_BIN" python find --system --no-project --no-python-downloads '>=3.11,<4' 2>/dev/null); then + printf 'Using local Python: %s\n' "$PYTHON_BIN" + return + fi + case "${UV_PYTHON_DOWNLOADS:-}" in never|false|0) fail "No compatible local Python; UV_PYTHON_DOWNLOADS disables downloads." ;; esac + case "${UV_OFFLINE:-}" in 1|true) fail "No compatible local Python in offline mode." ;; esac + local mirror=https://mirror.nju.edu.cn/github-release/astral-sh/python-build-standalone + local key url suffix automatic_mirror=false + local python_args=(python install 3.12) + if [[ "$REGION" == cn && -z "${UV_PYTHON_INSTALL_MIRROR:-}${UV_ASTRAL_MIRROR_URL:-}${UV_PYTHON_DOWNLOADS_JSON_URL:-}" ]] && ! has_uv_config_file; then + # Ask uv for the exact build URL; platform and archive selection stay with uv. + "$UV_BIN" python list 'cpython@3.12' --only-downloads --show-urls --color never >"$TEMP_DIR/python-downloads.txt" || fail "Could not resolve a Python download." + read -r key url <"$TEMP_DIR/python-downloads.txt" || true + case "$url" in + https://*/python-build-standalone/releases/download/*) + suffix=${url#*/python-build-standalone/releases/download/} + if url_available "$mirror/$suffix"; then + printf 'Python mirror: %s\n' "$mirror" + python_args+=(--mirror "$mirror") + automatic_mirror=true + else + printf '%s\n' 'Python mirror does not provide the requested build; using uv default sources.' + fi + ;; + esac + fi + printf '%s\n' 'No compatible local Python found. Installing Python 3.12.' + if ! "$UV_BIN" "${python_args[@]}"; then + if [[ "$automatic_mirror" == true ]]; then + printf '%s\n' 'Python mirror installation failed; retrying with uv default sources.' + "$UV_BIN" python install 3.12 || fail "Python installation failed. Check UV_PYTHON_INSTALL_MIRROR." + else + fail "Python installation failed. Check uv output and UV_PYTHON_INSTALL_MIRROR." + fi + fi + PYTHON_BIN=$("$UV_BIN" python find --system --no-project --no-python-downloads 3.12) || fail "Installed Python was not found." +} + +main() { + parse_args "$@" + case "$(uname -s)" in + Darwin|Linux) ;; + *) fail "Use the Windows instructions in the installation guide. This script supports macOS and Linux." ;; + esac + [[ -n "${HOME:-}" ]] || fail "HOME is not set." + command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 || fail "Install curl or wget first." + if [[ "$NO_HOSTS" == false ]]; then + command -v git >/dev/null 2>&1 || fail "Agent integration setup requires Git. Install Git or use --no-hosts." + fi + TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/powercontext-install.XXXXXX") + trap cleanup EXIT + detect_region + ensure_uv + ensure_python + check_index + [[ -z "$INDEX_URL" ]] || export UV_DEFAULT_INDEX="$INDEX_URL" + + local requirement='powercontext[cli,server]' + [[ "$PROFILE" != client ]] || requirement='powercontext[cli]' + local install_args=(tool install --python "$PYTHON_BIN" --no-python-downloads) + if [[ "$VERSION" == latest ]]; then + install_args+=(--upgrade --prerelease disallow) + else + requirement="$requirement==$VERSION" + fi + install_args+=("$requirement") + [[ -z "$INDEX_URL" ]] || install_args+=(--default-index "$INDEX_URL") + printf 'Installing PowerContext %s.\n' "$VERSION" + "$UV_BIN" "${install_args[@]}" || + fail "Installation failed. Check the uv error: Python downloads use UV_PYTHON_INSTALL_MIRROR or UV_ASTRAL_MIRROR_URL; Python packages use uv indexes." + + local tool_bin + tool_bin=$("$UV_BIN" tool dir --bin) + export PATH="$tool_bin:$PATH" + local installed_version + installed_version=$("$tool_bin/powercontext" --version) || fail "The installed CLI could not start." + [[ "$installed_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?$ ]] || fail "The installed CLI did not report a release version." + [[ "$VERSION" == latest || "$VERSION" == "$installed_version" ]] || fail "The installed version differs from the requested release." + VERSION=$installed_version + printf 'Runtime installed: %s (%s)\n' "$VERSION" "$PROFILE" + printf 'For a new terminal, add these directories to PATH if needed:\n' + printf ' export PATH=%q:%q:"%s"\n' "$(dirname "$UV_BIN")" "$tool_bin" "\$PATH" + + local setup_status=0 + if [[ "$NO_HOSTS" == false ]]; then + "$tool_bin/powercontext" setup select --source oceanbase/powercontext \ + --ref "powercontext-v$VERSION" ${HOSTS[@]+"${HOSTS[@]}"} <"$SETUP_INPUT" || setup_status=$? + fi + if [[ "$PROFILE" == local ]]; then + printf '\nConfigure PowerContext with: powercontext config init\n %s\n' "$GUIDE" + printf '%s\n' 'Then run: powercontext server run --env-file .env' + else + printf '\nConnect to your existing Server:\n %s\n' \ + 'https://powercontext.oceanbase.io/en/docs/operate/connect-remote-server/' + fi + if ((setup_status != 0)); then + fail "Runtime installed, but integration setup did not complete. Review its results and retry setup with powercontext-v$VERSION." + fi +} + +main "$@" From ba06b03881307ea0f03f1505e2e2c3fecb912a19 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 9 Oct 2026 00:18:28 +0800 Subject: [PATCH 02/24] docs(rfc): assign installation proposal number --- ...{0000-script-installation.md => 1892-script-installation.md} | 2 +- ...{0000-script-installation.md => 1892-script-installation.md} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/en/rfcs/{0000-script-installation.md => 1892-script-installation.md} (99%) rename docs/zh/rfcs/{0000-script-installation.md => 1892-script-installation.md} (99%) diff --git a/docs/en/rfcs/0000-script-installation.md b/docs/en/rfcs/1892-script-installation.md similarity index 99% rename from docs/en/rfcs/0000-script-installation.md rename to docs/en/rfcs/1892-script-installation.md index 52318f2812..ee330fc5fe 100644 --- a/docs/en/rfcs/0000-script-installation.md +++ b/docs/en/rfcs/1892-script-installation.md @@ -5,7 +5,7 @@ description: Install the latest or an exact release with Bash and PowerShell, in - Proposal Name: `script_installation` - Start Date: 2026-10-09 -- RFC PR: [oceanbase/powercontext#0000](https://github.com/oceanbase/powercontext/pull/0000) +- RFC PR: [oceanbase/powercontext#1892](https://github.com/oceanbase/powercontext/pull/1892) - Related RFCs: [RFC 1733](1733-usability-and-agent-workflows.md), [RFC 1299](1299_local_server_availability_and_service_installation.md) # Summary diff --git a/docs/zh/rfcs/0000-script-installation.md b/docs/zh/rfcs/1892-script-installation.md similarity index 99% rename from docs/zh/rfcs/0000-script-installation.md rename to docs/zh/rfcs/1892-script-installation.md index 0c214c8e53..c45cb3f2a2 100644 --- a/docs/zh/rfcs/0000-script-installation.md +++ b/docs/zh/rfcs/1892-script-installation.md @@ -5,7 +5,7 @@ description: 通过 Bash 和 PowerShell 安装最新或指定版本,独立管 - Proposal Name: `script_installation` - Start Date: 2026-10-09 -- RFC PR: [oceanbase/powercontext#0000](https://github.com/oceanbase/powercontext/pull/0000) +- RFC PR: [oceanbase/powercontext#1892](https://github.com/oceanbase/powercontext/pull/1892) - Related RFCs: [RFC 1733](1733-usability-and-agent-workflows.md)、[RFC 1299](1299_local_server_availability_and_service_installation.md) # Summary From 8b2ea9576a92154cd00a2a0609afdb302010cddf Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 9 Oct 2026 00:25:32 +0800 Subject: [PATCH 03/24] fix(install): select the first uv executable on Windows --- tests/native/test_installation.py | 3 +++ website/public/install.ps1 | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/native/test_installation.py b/tests/native/test_installation.py index 62abd2ed8a..2b055060de 100644 --- a/tests/native/test_installation.py +++ b/tests/native/test_installation.py @@ -179,6 +179,8 @@ def test_install_configure_remember_and_reinstall(tmp_path: Path, environment: s env.update( HOME=str(home_dir), USERPROFILE=str(home_dir), + # These subprocesses write UTF-8 log files, rather than a Windows console. + PYTHONIOENCODING="utf-8", UV_TOOL_DIR=str(tmp_path / "tools"), UV_TOOL_BIN_DIR=str(tmp_path / "bin"), UV_CACHE_DIR=str(tmp_path / "cache"), @@ -292,6 +294,7 @@ def test_latest_exact_versions_and_client_profile(tmp_path: Path) -> None: if not key.startswith(("UV_", "PIP_", "PYTHON", "POWERCONTEXT_")) and key != "VIRTUAL_ENV" } env.update( + PYTHONIOENCODING="utf-8", UV_NO_CONFIG="1", UV_TOOL_DIR=str(tmp_path / "tools"), UV_TOOL_BIN_DIR=str(tmp_path / "bin with spaces"), diff --git a/website/public/install.ps1 b/website/public/install.ps1 index 74b4d6d55b..7f63c9748e 100644 --- a/website/public/install.ps1 +++ b/website/public/install.ps1 @@ -165,7 +165,7 @@ function Select-Index { } function Install-UvIfMissing { - $Command = Get-Command uv -CommandType Application -ErrorAction SilentlyContinue + $Command = Get-Command uv -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 $UserUv = Join-Path $HOME '.local\bin\uv.exe' if ($env:UV_INSTALL_DIR) { $UserUv = Join-Path $env:UV_INSTALL_DIR 'uv.exe' } if ($Command) { $Uv = $Command.Source } From 8fa9f15f992b176379c8bad1296849fc949522f6 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 9 Oct 2026 00:59:07 +0800 Subject: [PATCH 04/24] fix(install): make Agent setup explicitly opt-in --- tests/fixtures/installation/hosts.json | 12 ++ tests/native/test_installation.py | 5 +- tests/test_installation_contract.py | 164 +++++++++++++++++++++++++ website/public/install.ps1 | 13 +- website/public/install.sh | 20 +-- 5 files changed, 190 insertions(+), 24 deletions(-) create mode 100644 tests/fixtures/installation/hosts.json create mode 100644 tests/test_installation_contract.py diff --git a/tests/fixtures/installation/hosts.json b/tests/fixtures/installation/hosts.json new file mode 100644 index 0000000000..5094f497fa --- /dev/null +++ b/tests/fixtures/installation/hosts.json @@ -0,0 +1,12 @@ +{ + "schema_version": 1, + "cases": [ + {"id": "unattended-default", "args": [], "git": false, "expected": {"exit_code": 0, "runtime_installed": true, "selected_hosts": [], "server_extra": true}}, + {"id": "compatibility-no-hosts", "args": ["--no-hosts"], "git": false, "expected": {"exit_code": 0, "runtime_installed": true, "selected_hosts": [], "server_extra": true}}, + {"id": "client-profile", "args": ["--profile", "client"], "git": false, "expected": {"exit_code": 0, "runtime_installed": true, "selected_hosts": [], "server_extra": false}}, + {"id": "explicit-hosts", "args": ["--host", "codex", "--host", "claude"], "git": true, "expected": {"exit_code": 0, "runtime_installed": true, "selected_hosts": ["codex", "claude"], "setup_ref": "powercontext-v1.2.0", "server_extra": true}}, + {"id": "host-partial-failure", "args": ["--host", "codex"], "git": true, "setup_exit": 17, "expected": {"exit_code": 1, "runtime_installed": true, "selected_hosts": ["codex"], "setup_ref": "powercontext-v1.2.0", "server_extra": true, "error_contains": "Runtime installed, but integration setup did not complete"}}, + {"id": "host-choice-conflict", "args": ["--host", "codex", "--no-hosts"], "git": true, "expected": {"exit_code": 1, "runtime_installed": false, "selected_hosts": [], "error_contains": "--host and --no-hosts cannot be combined"}}, + {"id": "explicit-host-needs-git", "args": ["--host", "codex"], "git": false, "expected": {"exit_code": 1, "runtime_installed": false, "selected_hosts": [], "error_contains": "requires Git"}} + ] +} diff --git a/tests/native/test_installation.py b/tests/native/test_installation.py index 2b055060de..e3c283917a 100644 --- a/tests/native/test_installation.py +++ b/tests/native/test_installation.py @@ -112,7 +112,7 @@ def reinstall_offline( if not interactive: run(installer, root, env, "reinstall-offline") return - # Regression: a piped installer must still allow terminal input at Agent selection. + # A piped default install succeeds without implicitly opening Agent selection. controller = """import errno, os, pty, sys pid, terminal = pty.fork() if pid == 0: @@ -141,7 +141,8 @@ def reinstall_offline( env, "reinstall-offline", ) - assert "Select hosts" in output + assert "Runtime installed:" in output + assert "Select hosts" not in output def utilities_without_python_or_uv(root: Path, system_path: str) -> Path: diff --git a/tests/test_installation_contract.py b/tests/test_installation_contract.py new file mode 100644 index 0000000000..042be98aca --- /dev/null +++ b/tests/test_installation_contract.py @@ -0,0 +1,164 @@ +"""Native installer conformance with real uv and offline synthetic distributions.""" + +import csv +import io +import json +import os +import shutil +import subprocess +import sys +import zipfile +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +CASES = [ + case + for path in sorted((ROOT / "tests/fixtures/installation").glob("*.json")) + for case in json.loads(path.read_text())["cases"] +] +CLI = """import importlib.util +import json +import os +from pathlib import Path +import sys + +def main(): + args = sys.argv[1:] + if args == ["--version"]: + print(os.environ.get("CONTRACT_REPORTED_VERSION", "1.2.0")) + elif not args or args[-1] == "--help": + command = " ".join(args[:-1]) if args else "" + missing = json.loads(os.environ.get("CONTRACT_MISSING_COMMANDS", "[]")) + if command in missing: + return 2 + if command == "server run": + import contract_server + print("Synthetic installation contract CLI") + elif args[:2] == ["setup", "select"]: + hosts = [args[i + 1] for i, arg in enumerate(args) if arg == "--host"] + ref = args[args.index("--ref") + 1] + Path(os.environ["CONTRACT_SETUP_RESULT"]).write_text(json.dumps({"hosts": hosts, "ref": ref})) + return int(os.environ.get("CONTRACT_SETUP_EXIT", "0")) + elif args == ["contract-capabilities"]: + print(json.dumps({"server_extra": importlib.util.find_spec("contract_server") is not None})) + else: + print("Synthetic installation contract CLI") + return 0 +""" + + +def wheel(directory: Path, name: str, files: dict[str, str], extra_metadata: str = "") -> None: + """Build a tiny standards-shaped wheel using only stdlib ZIP/CSV.""" + info = f"{name}-1.2.0.dist-info" + entries = files | { + f"{info}/METADATA": f"Metadata-Version: 2.3\nName: {name}\nVersion: 1.2.0\n{extra_metadata}\n", + f"{info}/WHEEL": "Wheel-Version: 1.0\nGenerator: installation-contract\nRoot-Is-Purelib: true\nTag: py3-none-any\n", + } + record = io.StringIO() + writer = csv.writer(record) + writer.writerows((path, "", "") for path in entries) + writer.writerow((f"{info}/RECORD", "", "")) + entries[f"{info}/RECORD"] = record.getvalue() + with zipfile.ZipFile(directory / f"{name}-1.2.0-py3-none-any.whl", "w") as archive: + for path, content in entries.items(): + archive.writestr(path, content) + + +def executable_path(directory: Path, uv: str, windows: bool, include_git: bool) -> str: + """Expose real bootstrap prerequisites, optionally excluding Git.""" + tools = directory / "tools" + tools.mkdir() + # Real executables only; uv creates the platform-native console launcher. + executables = {"uv": uv} if windows else {"uv": uv, "python": sys.executable} + for name, executable in executables.items(): + destination = tools / (name + ".exe" if windows else name) + if windows: + shutil.copy2(executable, destination) + else: + destination.symlink_to(executable) + if not windows: + for name in ("curl", "wget", "uname", "mktemp", "rm", "dirname", "cat", "sh", "readlink"): + executable = shutil.which(name) + if executable: + (tools / name).symlink_to(executable) + if include_git: + (tools / "git").symlink_to(shutil.which("git")) + elif include_git: + shutil.copy2(shutil.which("git"), tools / "git.exe") + if windows: + return os.pathsep.join(( + str(tools), + str(Path(sys.executable).parent), + str(Path(os.environ["SYSTEMROOT"]) / "System32"), + )) + return str(tools) + + +@pytest.mark.parametrize("case", CASES, ids=lambda case: case["id"]) +def test_installation_contract(case: dict, tmp_path: Path) -> None: + uv = shutil.which("uv") + if not uv: + pytest.skip("native installation contract requires real uv") + windows = sys.platform == "win32" + shell = shutil.which("powershell") or shutil.which("pwsh") if windows else shutil.which("bash") + if not shell: + pytest.skip("native installer shell is unavailable") + path = executable_path(tmp_path, uv, windows, case.get("git", False)) + packages = tmp_path / "packages" + packages.mkdir() + wheel(packages, "contract_server", {"contract_server.py": ""}) + wheel( + packages, + "powercontext", + { + "powercontext_contract.py": CLI, + "powercontext-1.2.0.dist-info/entry_points.txt": "[console_scripts]\npowercontext = powercontext_contract:main\n", + }, + "Provides-Extra: cli\nProvides-Extra: server\nRequires-Dist: contract-server==1.2.0; extra == 'server'\n", + ) + environment = { + key: value for key, value in os.environ.items() if not key.startswith(("UV_", "POWERCONTEXT_", "PIP_")) + } + environment.update({ + "HOME": str(tmp_path), + "USERPROFILE": str(tmp_path), + "TMPDIR": str(tmp_path), + "PATH": path, + "UV_TOOL_DIR": str(tmp_path / "tool"), + "UV_TOOL_BIN_DIR": str(tmp_path / "bin"), + "UV_CACHE_DIR": str(tmp_path / "cache"), + "UV_NO_CONFIG": "1", + "UV_NO_INDEX": "1", + "UV_FIND_LINKS": str(packages), + "UV_PYTHON_DOWNLOADS": "never", + "UV_OFFLINE": "1", + "CONTRACT_SETUP_RESULT": str(tmp_path / "setup.json"), + "CONTRACT_SETUP_EXIT": str(case.get("setup_exit", 0)), + "CONTRACT_REPORTED_VERSION": case.get("reported_version", "1.2.0"), + "CONTRACT_MISSING_COMMANDS": json.dumps(case.get("missing_commands", [])), + }) + script = ROOT / "website/public" / ("install.ps1" if windows else "install.sh") + command = ( + [shell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(script)] if windows else [shell, str(script)] + ) + result = subprocess.run( + [*command, "--region", "global", *case["args"]], env=environment, capture_output=True, text=True, timeout=60 + ) + expected = case["expected"] + assert result.returncode == expected["exit_code"], result.stdout + result.stderr + executable = tmp_path / "bin" / ("powercontext.exe" if windows else "powercontext") + assert executable.exists() == expected["runtime_installed"] + setup = tmp_path / "setup.json" + observed = json.loads(setup.read_text()) if setup.exists() else {"hosts": []} + assert observed["hosts"] == expected.get("selected_hosts", []) + if "setup_ref" in expected: + assert observed["ref"] == expected["setup_ref"] + if expected.get("verification_failed"): + assert "Runtime installed:" not in result.stdout + if "error_contains" in expected: + assert expected["error_contains"] in result.stderr + if expected["runtime_installed"] and "server_extra" in expected: + capabilities = subprocess.check_output([str(executable), "contract-capabilities"], env=environment, text=True) + assert json.loads(capabilities)["server_extra"] == expected["server_extra"] diff --git a/website/public/install.ps1 b/website/public/install.ps1 index 7f63c9748e..740677605f 100644 --- a/website/public/install.ps1 +++ b/website/public/install.ps1 @@ -37,11 +37,11 @@ Usage: powershell -ExecutionPolicy Bypass -File install.ps1 [options] --region REGION auto, cn, or global (default: POWERCONTEXT_INSTALL_REGION or auto). --index-url URL HTTPS default package index for this installation. --host HOST Install an Agent integration; repeat for multiple hosts. - --no-hosts Skip Agent integration setup. + --no-hosts Compatibility option; integration setup is skipped by default. -h, --help Show this help. -Without host options, an interactive terminal opens powercontext setup select. -Without a terminal, --host or --no-hosts is required. +Agent integration setup runs only for explicitly selected --host values. +After installation, run powercontext setup select to choose hosts interactively. Region priority: --region, POWERCONTEXT_INSTALL_REGION, named timezone, locale territory, then global. No network location service is queried. @@ -246,11 +246,8 @@ try { if ($IndexUrl) { Assert-HttpsUrl $IndexUrl '--index-url' } if ($env:POWERCONTEXT_UV_INSTALLER_URL) { Assert-HttpsUrl $env:POWERCONTEXT_UV_INSTALLER_URL 'POWERCONTEXT_UV_INSTALLER_URL' } if ($NoHosts -and $Hosts.Count) { throw '--host and --no-hosts cannot be combined.' } - if (-not $NoHosts -and -not $Hosts.Count -and (-not [Environment]::UserInteractive -or [Console]::IsInputRedirected)) { - throw 'No interactive input. Pass --host HOST or --no-hosts.' - } if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Use install.sh on macOS and Linux.' } - if (-not $NoHosts -and -not (Get-Command git -CommandType Application -ErrorAction SilentlyContinue)) { + if ($Hosts.Count -and -not (Get-Command git -CommandType Application -ErrorAction SilentlyContinue)) { throw 'Agent integration setup requires Git. Install Git or use --no-hosts.' } $TempDir = Join-Path ([IO.Path]::GetTempPath()) ('powercontext-install-' + [Guid]::NewGuid()) @@ -321,7 +318,7 @@ try { $PathPrefix = ("$($ToolBin.Trim());$(Split-Path -Parent $Uv)").Replace("'", "''") Write-Host ('For a new terminal: $env:Path = ''' + $PathPrefix + ';'' + $env:Path') $SetupStatus = 0 - if (-not $NoHosts) { + if ($Hosts.Count) { & $Cli setup select --source oceanbase/powercontext --ref "powercontext-v$Version" @Hosts $SetupStatus = $LASTEXITCODE } diff --git a/website/public/install.sh b/website/public/install.sh index fd878bfcad..852efb09d0 100644 --- a/website/public/install.sh +++ b/website/public/install.sh @@ -23,7 +23,6 @@ PYTHON_BIN="" INDEX_URL="" HOSTS=() NO_HOSTS=false -SETUP_INPUT=/dev/stdin TEMP_DIR="" UV_BIN="" GUIDE=https://powercontext.oceanbase.io/en/docs/get-started/quickstart/ @@ -52,11 +51,11 @@ Usage: bash install.sh [--version VERSION] [--index-url URL] [--host HOST]... [- --index-url URL HTTPS default package index for this installation. Existing additional uv indexes still take precedence. --host HOST Install this Agent integration; repeat for multiple hosts. - --no-hosts Skip Agent integration setup. + --no-hosts Compatibility option; integration setup is skipped by default. -h, --help Show this help. -Without host options, an interactive terminal opens powercontext setup select. -Without a terminal, --host or --no-hosts is required. +Agent integration setup runs only for explicitly selected --host values. +After installation, run powercontext setup select to choose hosts interactively. Region priority: --region, POWERCONTEXT_INSTALL_REGION, named timezone, locale territory, then global. No network location service is queried. @@ -109,13 +108,6 @@ parse_args() { if [[ "$NO_HOSTS" == true && ${#HOSTS[@]} -gt 0 ]]; then fail "--host and --no-hosts cannot be combined." fi - if [[ "$NO_HOSTS" == false && ${#HOSTS[@]} -eq 0 && ! -t 0 ]]; then - if [[ -t 1 ]] && { : /dev/null; then - SETUP_INPUT=/dev/tty - else - fail "No interactive input. Pass --host HOST or --no-hosts." - fi - fi } download() { @@ -312,7 +304,7 @@ main() { esac [[ -n "${HOME:-}" ]] || fail "HOME is not set." command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 || fail "Install curl or wget first." - if [[ "$NO_HOSTS" == false ]]; then + if [[ ${#HOSTS[@]} -gt 0 ]]; then command -v git >/dev/null 2>&1 || fail "Agent integration setup requires Git. Install Git or use --no-hosts." fi TEMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/powercontext-install.XXXXXX") @@ -350,9 +342,9 @@ main() { printf ' export PATH=%q:%q:"%s"\n' "$(dirname "$UV_BIN")" "$tool_bin" "\$PATH" local setup_status=0 - if [[ "$NO_HOSTS" == false ]]; then + if [[ ${#HOSTS[@]} -gt 0 ]]; then "$tool_bin/powercontext" setup select --source oceanbase/powercontext \ - --ref "powercontext-v$VERSION" ${HOSTS[@]+"${HOSTS[@]}"} <"$SETUP_INPUT" || setup_status=$? + --ref "powercontext-v$VERSION" ${HOSTS[@]+"${HOSTS[@]}"} || setup_status=$? fi if [[ "$PROFILE" == local ]]; then printf '\nConfigure PowerContext with: powercontext config init\n %s\n' "$GUIDE" From 29d2eaf132582516533f1ea6dcaea3dab7c2d579 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 9 Oct 2026 00:59:40 +0800 Subject: [PATCH 05/24] test(install): isolate and type native contract fixtures --- tests/fixtures/installation/hosts.json | 113 +++++++++++++++++++++++-- tests/test_installation_contract.py | 34 ++++++-- 2 files changed, 134 insertions(+), 13 deletions(-) diff --git a/tests/fixtures/installation/hosts.json b/tests/fixtures/installation/hosts.json index 5094f497fa..cfd3007aa5 100644 --- a/tests/fixtures/installation/hosts.json +++ b/tests/fixtures/installation/hosts.json @@ -1,12 +1,111 @@ { "schema_version": 1, "cases": [ - {"id": "unattended-default", "args": [], "git": false, "expected": {"exit_code": 0, "runtime_installed": true, "selected_hosts": [], "server_extra": true}}, - {"id": "compatibility-no-hosts", "args": ["--no-hosts"], "git": false, "expected": {"exit_code": 0, "runtime_installed": true, "selected_hosts": [], "server_extra": true}}, - {"id": "client-profile", "args": ["--profile", "client"], "git": false, "expected": {"exit_code": 0, "runtime_installed": true, "selected_hosts": [], "server_extra": false}}, - {"id": "explicit-hosts", "args": ["--host", "codex", "--host", "claude"], "git": true, "expected": {"exit_code": 0, "runtime_installed": true, "selected_hosts": ["codex", "claude"], "setup_ref": "powercontext-v1.2.0", "server_extra": true}}, - {"id": "host-partial-failure", "args": ["--host", "codex"], "git": true, "setup_exit": 17, "expected": {"exit_code": 1, "runtime_installed": true, "selected_hosts": ["codex"], "setup_ref": "powercontext-v1.2.0", "server_extra": true, "error_contains": "Runtime installed, but integration setup did not complete"}}, - {"id": "host-choice-conflict", "args": ["--host", "codex", "--no-hosts"], "git": true, "expected": {"exit_code": 1, "runtime_installed": false, "selected_hosts": [], "error_contains": "--host and --no-hosts cannot be combined"}}, - {"id": "explicit-host-needs-git", "args": ["--host", "codex"], "git": false, "expected": {"exit_code": 1, "runtime_installed": false, "selected_hosts": [], "error_contains": "requires Git"}} + { + "id": "unattended-default", + "args": [], + "git": false, + "expected": { + "exit_code": 0, + "runtime_installed": true, + "selected_hosts": [], + "server_extra": true + } + }, + { + "id": "compatibility-no-hosts", + "args": [ + "--no-hosts" + ], + "git": false, + "expected": { + "exit_code": 0, + "runtime_installed": true, + "selected_hosts": [], + "server_extra": true + } + }, + { + "id": "client-profile", + "args": [ + "--profile", + "client" + ], + "git": false, + "expected": { + "exit_code": 0, + "runtime_installed": true, + "selected_hosts": [], + "server_extra": false + } + }, + { + "id": "explicit-hosts", + "args": [ + "--host", + "codex", + "--host", + "claude" + ], + "git": true, + "expected": { + "exit_code": 0, + "runtime_installed": true, + "selected_hosts": [ + "codex", + "claude" + ], + "setup_ref": "powercontext-v1.2.0", + "server_extra": true + } + }, + { + "id": "host-partial-failure", + "args": [ + "--host", + "codex" + ], + "git": true, + "setup_exit": 17, + "expected": { + "exit_code": 1, + "runtime_installed": true, + "selected_hosts": [ + "codex" + ], + "setup_ref": "powercontext-v1.2.0", + "server_extra": true, + "error_contains": "Runtime installed, but integration setup did not complete" + } + }, + { + "id": "host-choice-conflict", + "args": [ + "--host", + "codex", + "--no-hosts" + ], + "git": true, + "expected": { + "exit_code": 1, + "runtime_installed": false, + "selected_hosts": [], + "error_contains": "--host and --no-hosts cannot be combined" + } + }, + { + "id": "explicit-host-needs-git", + "args": [ + "--host", + "codex" + ], + "git": false, + "expected": { + "exit_code": 1, + "runtime_installed": false, + "selected_hosts": [], + "error_contains": "requires Git" + } + } ] } diff --git a/tests/test_installation_contract.py b/tests/test_installation_contract.py index 042be98aca..c9f222db5d 100644 --- a/tests/test_installation_contract.py +++ b/tests/test_installation_contract.py @@ -1,3 +1,17 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """Native installer conformance with real uv and offline synthetic distributions.""" import csv @@ -9,6 +23,7 @@ import sys import zipfile from pathlib import Path +from typing import Any import pytest @@ -78,15 +93,19 @@ def executable_path(directory: Path, uv: str, windows: bool, include_git: bool) shutil.copy2(executable, destination) else: destination.symlink_to(executable) + git = shutil.which("git") + if include_git and not git: + pytest.skip("explicit-host contract requires Git discovery") if not windows: for name in ("curl", "wget", "uname", "mktemp", "rm", "dirname", "cat", "sh", "readlink"): executable = shutil.which(name) if executable: (tools / name).symlink_to(executable) - if include_git: - (tools / "git").symlink_to(shutil.which("git")) - elif include_git: - shutil.copy2(shutil.which("git"), tools / "git.exe") + if include_git and git: + (tools / "git").symlink_to(git) + elif include_git and git: + # Presence check only: synthetic host setup never executes Git. + shutil.copy2(git, tools / "git.exe") if windows: return os.pathsep.join(( str(tools), @@ -97,7 +116,7 @@ def executable_path(directory: Path, uv: str, windows: bool, include_git: bool) @pytest.mark.parametrize("case", CASES, ids=lambda case: case["id"]) -def test_installation_contract(case: dict, tmp_path: Path) -> None: +def test_installation_contract(case: dict[str, Any], tmp_path: Path) -> None: uv = shutil.which("uv") if not uv: pytest.skip("native installation contract requires real uv") @@ -119,9 +138,12 @@ def test_installation_contract(case: dict, tmp_path: Path) -> None: "Provides-Extra: cli\nProvides-Extra: server\nRequires-Dist: contract-server==1.2.0; extra == 'server'\n", ) environment = { - key: value for key, value in os.environ.items() if not key.startswith(("UV_", "POWERCONTEXT_", "PIP_")) + key: value + for key, value in os.environ.items() + if not key.startswith(("UV_", "POWERCONTEXT_", "PIP_", "PYTHON", "CONTRACT_")) and key != "VIRTUAL_ENV" } environment.update({ + "PYTHONIOENCODING": "utf-8", "HOME": str(tmp_path), "USERPROFILE": str(tmp_path), "TMPDIR": str(tmp_path), From dcfd479ab51552cbffd401fff37db0e60896d019 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 9 Oct 2026 01:00:20 +0800 Subject: [PATCH 06/24] fix(installation): verify advertised profile commands before success --- tests/fixtures/installation/capabilities.json | 118 ++++++++++++++++++ website/public/install.ps1 | 19 ++- website/public/install.sh | 19 ++- 3 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/installation/capabilities.json diff --git a/tests/fixtures/installation/capabilities.json b/tests/fixtures/installation/capabilities.json new file mode 100644 index 0000000000..168f1d0928 --- /dev/null +++ b/tests/fixtures/installation/capabilities.json @@ -0,0 +1,118 @@ +{ + "schema_version": 1, + "cases": [ + { + "id": "missing-root-help", + "args": [ + "--no-hosts" + ], + "missing_commands": [ + "" + ], + "expected": { + "exit_code": 1, + "runtime_installed": true, + "server_extra": true, + "verification_failed": true, + "error_contains": "Package installed, but" + } + }, + { + "id": "missing-capabilities", + "args": [ + "--no-hosts" + ], + "missing_commands": [ + "capabilities" + ], + "expected": { + "exit_code": 1, + "runtime_installed": true, + "server_extra": true, + "verification_failed": true, + "error_contains": "Package installed, but" + } + }, + { + "id": "missing-setup-select", + "args": [ + "--no-hosts" + ], + "missing_commands": [ + "setup select" + ], + "expected": { + "exit_code": 1, + "runtime_installed": true, + "server_extra": true, + "verification_failed": true, + "error_contains": "Package installed, but" + } + }, + { + "id": "missing-config-init", + "args": [ + "--no-hosts" + ], + "missing_commands": [ + "config init" + ], + "expected": { + "exit_code": 1, + "runtime_installed": true, + "server_extra": true, + "verification_failed": true, + "error_contains": "Package installed, but" + } + }, + { + "id": "missing-server-run", + "args": [ + "--no-hosts" + ], + "missing_commands": [ + "server run" + ], + "expected": { + "exit_code": 1, + "runtime_installed": true, + "server_extra": true, + "verification_failed": true, + "error_contains": "Package installed, but" + } + }, + { + "id": "client-does-not-require-local-commands", + "args": [ + "--profile", + "client", + "--no-hosts" + ], + "missing_commands": [ + "config init", + "server run" + ], + "expected": { + "exit_code": 0, + "runtime_installed": true, + "server_extra": false + } + }, + { + "id": "exact-version-report-mismatch", + "args": [ + "--version", + "1.2.0", + "--no-hosts" + ], + "reported_version": "9.9.9", + "expected": { + "exit_code": 1, + "runtime_installed": true, + "server_extra": true, + "verification_failed": true, + "error_contains": "differs from the requested release" + } + } + ] +} diff --git a/website/public/install.ps1 b/website/public/install.ps1 index 740677605f..44e70ac2a9 100644 --- a/website/public/install.ps1 +++ b/website/public/install.ps1 @@ -310,9 +310,22 @@ try { $Cli = Join-Path $ToolBin.Trim() 'powercontext.exe' $env:PATH = "$($ToolBin.Trim());$env:PATH" $InstalledVersion = & $Cli --version - if ($LASTEXITCODE -ne 0) { throw 'The installed CLI could not start.' } - if ($InstalledVersion -notmatch '^\d+\.\d+\.\d+((a|b|rc)\d+)?$') { throw 'The installed CLI did not report a release version.' } - if ($Version -ne 'latest' -and $Version -ne $InstalledVersion) { throw 'The installed version differs from the requested release.' } + if ($LASTEXITCODE -ne 0) { throw 'Package installed, but CLI version verification failed. Check uv output and retry installation.' } + if ($InstalledVersion -notmatch '^\d+\.\d+\.\d+((a|b|rc)\d+)?$') { throw 'Package installed, but CLI release version verification failed. Check uv output and retry installation.' } + if ($Version -ne 'latest' -and $Version -ne $InstalledVersion) { throw 'Package installed, but the CLI version differs from the requested release. Retry installation with the intended version.' } + # Help checks import and command availability without configuring or starting a service. + & $Cli --help | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Package installed, but CLI help verification failed. Retry installation after reviewing the command error.' } + & $Cli capabilities --help | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Package installed, but Client command verification failed (capabilities --help). Retry installation after reviewing the command error.' } + & $Cli setup select --help | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Package installed, but integration command verification failed (setup select --help). Retry installation after reviewing the command error.' } + if ($RuntimeProfile -eq 'local') { + & $Cli config init --help | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Package installed, but configuration command verification failed (config init --help). Retry installation after reviewing the command error.' } + & $Cli server run --help | Out-Null + if ($LASTEXITCODE -ne 0) { throw 'Package installed, but Server command verification failed (server run --help). Retry installation after reviewing the command error.' } + } $Version = $InstalledVersion.Trim() Write-Host "Runtime installed: $Version ($RuntimeProfile)" $PathPrefix = ("$($ToolBin.Trim());$(Split-Path -Parent $Uv)").Replace("'", "''") diff --git a/website/public/install.sh b/website/public/install.sh index 852efb09d0..6dbfc5aaa4 100644 --- a/website/public/install.sh +++ b/website/public/install.sh @@ -333,9 +333,22 @@ main() { tool_bin=$("$UV_BIN" tool dir --bin) export PATH="$tool_bin:$PATH" local installed_version - installed_version=$("$tool_bin/powercontext" --version) || fail "The installed CLI could not start." - [[ "$installed_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?$ ]] || fail "The installed CLI did not report a release version." - [[ "$VERSION" == latest || "$VERSION" == "$installed_version" ]] || fail "The installed version differs from the requested release." + installed_version=$("$tool_bin/powercontext" --version) || fail "Package installed, but CLI version verification failed. Check uv output and retry installation." + [[ "$installed_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+((a|b|rc)[0-9]+)?$ ]] || fail "Package installed, but CLI release version verification failed. Check uv output and retry installation." + [[ "$VERSION" == latest || "$VERSION" == "$installed_version" ]] || fail "Package installed, but the CLI version differs from the requested release. Retry installation with the intended version." + # Help checks import and command availability without configuring or starting a service. + "$tool_bin/powercontext" --help >/dev/null || + fail "Package installed, but CLI help verification failed. Retry installation after reviewing the command error." + "$tool_bin/powercontext" capabilities --help >/dev/null || + fail "Package installed, but Client command verification failed (capabilities --help). Retry installation after reviewing the command error." + "$tool_bin/powercontext" setup select --help >/dev/null || + fail "Package installed, but integration command verification failed (setup select --help). Retry installation after reviewing the command error." + if [[ "$PROFILE" == local ]]; then + "$tool_bin/powercontext" config init --help >/dev/null || + fail "Package installed, but configuration command verification failed (config init --help). Retry installation after reviewing the command error." + "$tool_bin/powercontext" server run --help >/dev/null || + fail "Package installed, but Server command verification failed (server run --help). Retry installation after reviewing the command error." + fi VERSION=$installed_version printf 'Runtime installed: %s (%s)\n' "$VERSION" "$PROFILE" printf 'For a new terminal, add these directories to PATH if needed:\n' From c291e99de0c78773034cff49227b132fceafc19e Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 9 Oct 2026 01:05:57 +0800 Subject: [PATCH 07/24] fix(installation): delegate package version resolution to uv --- tests/native/test_install_sources.py | 226 +++++++++++++++++++++++++++ website/public/install.ps1 | 28 ++-- website/public/install.sh | 32 ++-- 3 files changed, 252 insertions(+), 34 deletions(-) create mode 100644 tests/native/test_install_sources.py diff --git a/tests/native/test_install_sources.py b/tests/native/test_install_sources.py new file mode 100644 index 0000000000..f1dbc9b18e --- /dev/null +++ b/tests/native/test_install_sources.py @@ -0,0 +1,226 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Linux installer source acceptance with real uv and a loopback package index.""" + +from __future__ import annotations + +import hashlib +import http.server +import io +import os +import shutil +import subprocess +import sys +import urllib.parse +import zipfile +from pathlib import Path +from threading import Thread +from typing import ClassVar + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +BASH = shutil.which("bash") +UV = os.environ.get("POWERCONTEXT_INSTALL_UV") or shutil.which("uv") +pytestmark = pytest.mark.skipif(sys.platform != "linux" or not UV, reason="requires Linux and real uv") + + +def build_wheel(version: str) -> bytes: + data = io.BytesIO() + metadata = f"powercontext-{version}.dist-info" + with zipfile.ZipFile(data, "w") as archive: + archive.writestr( + "fixture_cli.py", + "from importlib.metadata import version\ndef main():\n print(version('powercontext'))\n", + ) + archive.writestr( + f"{metadata}/METADATA", + f"Metadata-Version: 2.1\nName: powercontext\nVersion: {version}\nProvides-Extra: cli\nProvides-Extra: server\n", + ) + archive.writestr(f"{metadata}/WHEEL", "Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n") + archive.writestr(f"{metadata}/entry_points.txt", "[console_scripts]\npowercontext = fixture_cli:main\n") + archive.writestr(f"{metadata}/RECORD", "") + return data.getvalue() + + +@pytest.fixture +def installation(tmp_path): # noqa: C901 - one fixture owns the server and subprocess environment + assert BASH + bash_command: str = BASH + wheels = {version: build_wheel(version) for version in ("1.0.0", "1.1.0", "1.2.0rc1")} + + class Index(http.server.BaseHTTPRequestHandler): + mirror_status = 200 + mirror_versions: ClassVar[list[str]] = ["1.0.0", "1.1.0", "1.2.0rc1"] + requests: ClassVar[list[str]] = [] + + # Keep this controlled fixture compatible with standalone Python 3.11. + def log_message(self, format: str, *args: object) -> None: # noqa: A002 + pass + + def do_HEAD(self): + self.do_GET() + + def do_GET(self): + self.requests.append(self.path) + path = urllib.parse.unquote(self.path) + if path.startswith("/cn/") and self.mirror_status != 200: + self.send_error(self.mirror_status) + return + if path.endswith("/powercontext/"): + versions = self.mirror_versions if path.startswith("/cn/") else list(wheels) + source = path.split("/")[1] + links = [] + for version in versions: + name = f"powercontext-{version}-py3-none-any.whl" + # Valid URL encoding and opaque anchor text reproduce the filename-probe defect. + href = f"/{source}/files/{name}".replace(".", "%2E") + digest = hashlib.sha256(wheels[version]).hexdigest() + links.append(f'download') + payload = ("" + "\n".join(links)).encode() + content_type = "text/html" + elif path.endswith(".whl"): + payload = wheels[path.rsplit("/", 1)[-1].split("-")[1]] + content_type = "application/octet-stream" + else: + self.send_error(404) + return + self.send_response(200) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + if self.command != "HEAD": + self.wfile.write(payload) + + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Index) + worker = Thread(target=server.serve_forever, daemon=True) + worker.start() + base = f"http://127.0.0.1:{server.server_port}" + utilities = tmp_path / "utilities" + utilities.mkdir() + # Translate only distribution endpoints to fixtures; run the real downloader and uv. + mappings = { + "https://pypi.tuna.tsinghua.edu.cn/simple": base + "/cn/simple", + "https://pypi.org/simple": base + "/global/simple", + "https://explicit.invalid/simple": base + "/cn/simple", + } + for name, executable in (("uv", UV), ("curl", shutil.which("curl"))): + assert executable + wrapper = utilities / name + wrapper.write_text( + f"#!{sys.executable}\nimport os, sys\nmapping={mappings!r}\n" + "args=[mapping.get(value, value) for value in sys.argv[1:]]\n" + "args=[next((target + value[len(source):] for source,target in mapping.items() " + "if value.startswith(source)), value) for value in args]\n" + "for key in ('UV_DEFAULT_INDEX','UV_INDEX','UV_INDEX_URL'):\n" + " if key in os.environ: os.environ[key]=mapping.get(os.environ[key],os.environ[key])\n" + f"os.execv({executable!r}, [{executable!r}, *args])\n", + encoding="utf-8", + ) + wrapper.chmod(0o755) + env = { + "PATH": str(utilities) + os.pathsep + os.environ["PATH"], + "HOME": str(tmp_path / "home"), + "TMPDIR": str(tmp_path), + "LANG": "C.UTF-8", + "XDG_CONFIG_HOME": str(tmp_path / "config"), + "XDG_CONFIG_DIRS": str(tmp_path / "system-config"), + "UV_TOOL_DIR": str(tmp_path / "tools"), + "UV_TOOL_BIN_DIR": str(tmp_path / "bin"), + "UV_CACHE_DIR": str(tmp_path / "cache"), + "UV_PYTHON_INSTALL_DIR": str(tmp_path / "python"), + "UV_HTTP_RETRIES": "0", + "NO_PROXY": "127.0.0.1", + } + Path(env["HOME"]).mkdir() + + def install(*arguments, success=True, extra=None): + completed = subprocess.run( + [bash_command, str(ROOT / "website/public/install.sh"), "--no-hosts", *arguments], + cwd=tmp_path, + env=env | (extra or {}), + text=True, + capture_output=True, + timeout=30, + ) + assert (completed.returncode == 0) == success, completed.stdout + completed.stderr + return completed.stdout + completed.stderr + + def version(): + return subprocess.check_output([str(tmp_path / "bin/powercontext"), "--version"], env=env, text=True).strip() + + try: + yield install, version, Index, env + finally: + server.shutdown() + server.server_close() + worker.join() + + +@pytest.mark.parametrize("region", ["global", "cn"]) +def test_encoded_release_installs_through_public_script(installation, region): + install, version, _index, _env = installation + output = install("--region", region, "--version", "1.0.0") + assert version() == "1.0.0" + assert "Runtime installed: 1.0.0" in output + + +def test_latest_stable_and_explicit_prerelease(installation): + install, version, _index, _env = installation + install("--region", "cn") + assert version() == "1.1.0" + install("--region", "cn", "--version", "1.2.0rc1") + assert version() == "1.2.0rc1" + + +def test_unavailable_automatic_mirror_falls_back_before_tool_install(installation): + install, version, index, _env = installation + index.mirror_status = 503 + output = install("--region", "cn", "--version", "1.0.0") + assert version() == "1.0.0" + assert "Automatic package mirror unavailable; using PyPI." in output + assert any(path.startswith("/global/") for path in index.requests) + + +def test_reachable_stale_mirror_preserves_request_and_installed_runtime(installation): + install, version, index, _env = installation + install("--region", "global", "--version", "1.1.0") + index.requests.clear() + index.mirror_versions = ["1.0.0"] + output = install("--region", "cn", "--version", "1.2.0rc1", success=False) + assert "Installation failed" in output + assert version() == "1.1.0" + assert not any(path.startswith("/global/") for path in index.requests) + + +def test_explicit_failed_index_has_no_automatic_fallback(installation): + install, _version, index, _env = installation + index.mirror_status = 403 + install("--region", "cn", "--index-url", "https://explicit.invalid/simple", "--version", "1.0.0", success=False) + assert not any(path.startswith("/global/") for path in index.requests) + + +def test_unrelated_config_preserved_and_region_does_not_override_index(installation): + install, version, index, env = installation + config = Path(env["XDG_CONFIG_HOME"]) / "uv/uv.toml" + config.parent.mkdir(parents=True) + config.write_text("compile-bytecode = false\n", encoding="utf-8") + original = config.read_bytes() + # Existing uv settings provide the local source while conservative suppression preserves config. + output = install("--region", "cn", "--version", "1.0.0", extra={"UV_DEFAULT_INDEX": "https://pypi.org/simple"}) + assert version() == "1.0.0" + assert "Using existing uv configuration" in output + assert config.read_bytes() == original + assert not any(path.startswith("/cn/") for path in index.requests) diff --git a/website/public/install.ps1 b/website/public/install.ps1 index 44e70ac2a9..78160e812b 100644 --- a/website/public/install.ps1 +++ b/website/public/install.ps1 @@ -125,15 +125,12 @@ function Test-DownloadUrl([string]$Url) { catch { return $false } } -function Test-IndexVersion([string]$Url) { +function Test-IndexAvailable([string]$Url) { $IndexFile = Join-Path $TempDir 'index.html' + # Probe transport availability only; uv interprets the Simple API and versions. try { Save-Download ($Url.TrimEnd('/') + '/powercontext/') $IndexFile } catch { - Write-Host "Package index is unreachable: $Url" - return $false - } - if ($Version -ne 'latest' -and (Get-Content -LiteralPath $IndexFile -Raw) -notmatch ('powercontext-' + [regex]::Escape($Version) + '(-|\.)')) { - Write-Host "Package index does not list PowerContext ${Version}: $Url" + Write-Host "Package mirror is unreachable: $Url" return $false } return $true @@ -147,18 +144,17 @@ function Select-Index { if ($env:PIP_INDEX_URL -or $env:PIP_EXTRA_INDEX_URL) { Write-Host 'uv does not read pip index settings. Use --index-url or uv configuration.' } - if ($IndexUrl) { - # uv owns index authentication and package resolution, including private indexes. - } - else { - $Indexes = @('https://pypi.org/simple') - if ($Region -eq 'cn') { $Indexes = @('https://pypi.tuna.tsinghua.edu.cn/simple', 'https://pypi.org/simple') } - foreach ($Candidate in $Indexes) { - Write-Host "Checking package index: $Candidate" - if (Test-IndexVersion $Candidate) { $IndexUrl = $Candidate; break } + if (-not $IndexUrl) { + $IndexUrl = 'https://pypi.org/simple' + if ($Region -eq 'cn') { + $Mirror = 'https://pypi.tuna.tsinghua.edu.cn/simple' + Write-Host "Checking package mirror availability: $Mirror" + if (Test-IndexAvailable $Mirror) { $IndexUrl = $Mirror } + else { Write-Host 'Automatic package mirror unavailable; using PyPI.' } } - if (-not $IndexUrl) { throw "No reachable package index lists PowerContext $Version. Use --index-url to select one." } } + # A reachable mirror may be stale or incompatible. Let uv report that failure + # without retrying a tool installation or changing the requested requirement. Write-Host "Package index: $IndexUrl" $env:UV_DEFAULT_INDEX = $IndexUrl return $IndexUrl diff --git a/website/public/install.sh b/website/public/install.sh index 6dbfc5aaa4..8bd8246d44 100644 --- a/website/public/install.sh +++ b/website/public/install.sh @@ -180,13 +180,10 @@ url_available() { fi } -index_has_version() { +index_available() { + # Probe transport availability only; uv interprets the Simple API and versions. if ! download "${1%/}/powercontext/" "$TEMP_DIR/index.html"; then - printf 'Package index is unreachable: %s\n' "$1" >&2 - return 1 - fi - if [[ "$VERSION" != latest ]] && ! grep -Eq "powercontext-${VERSION//./\\.}(-|\.)" "$TEMP_DIR/index.html"; then - printf 'Package index does not list PowerContext %s: %s\n' "$VERSION" "$1" >&2 + printf 'Package mirror is unreachable: %s\n' "$1" >&2 return 1 fi } @@ -205,20 +202,19 @@ check_index() { return fi - local selected_index - local indexes=(https://pypi.org/simple) + INDEX_URL=https://pypi.org/simple if [[ "$REGION" == cn ]]; then - indexes=(https://pypi.tuna.tsinghua.edu.cn/simple https://pypi.org/simple) - fi - for selected_index in "${indexes[@]}"; do - printf 'Checking package index: %s\n' "$selected_index" - if index_has_version "$selected_index"; then - INDEX_URL=$selected_index - printf 'Package index: %s\n' "$INDEX_URL" - return + local mirror=https://pypi.tuna.tsinghua.edu.cn/simple + printf 'Checking package mirror availability: %s\n' "$mirror" + if index_available "$mirror"; then + INDEX_URL=$mirror + else + printf '%s\n' 'Automatic package mirror unavailable; using PyPI.' fi - done - fail "No reachable package index lists PowerContext $VERSION. Check connectivity, the version, or use --index-url URL." + fi + # A reachable mirror may be stale or incompatible. Let uv report that failure + # without retrying a tool installation or changing the requested requirement. + printf 'Package index: %s\n' "$INDEX_URL" } ensure_uv() { From e5dd846b6e7d9657fdef579e67262f99be67f6af Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 9 Oct 2026 01:09:47 +0800 Subject: [PATCH 08/24] fix(installation): verify Runtime independently of optional setup --- tests/fixtures/installation/capabilities.json | 8 +++----- website/public/install.ps1 | 2 -- website/public/install.sh | 2 -- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/fixtures/installation/capabilities.json b/tests/fixtures/installation/capabilities.json index 168f1d0928..8afd5080a3 100644 --- a/tests/fixtures/installation/capabilities.json +++ b/tests/fixtures/installation/capabilities.json @@ -34,7 +34,7 @@ } }, { - "id": "missing-setup-select", + "id": "runtime-without-optional-setup", "args": [ "--no-hosts" ], @@ -42,11 +42,9 @@ "setup select" ], "expected": { - "exit_code": 1, + "exit_code": 0, "runtime_installed": true, - "server_extra": true, - "verification_failed": true, - "error_contains": "Package installed, but" + "server_extra": true } }, { diff --git a/website/public/install.ps1 b/website/public/install.ps1 index 78160e812b..0d0578154c 100644 --- a/website/public/install.ps1 +++ b/website/public/install.ps1 @@ -314,8 +314,6 @@ try { if ($LASTEXITCODE -ne 0) { throw 'Package installed, but CLI help verification failed. Retry installation after reviewing the command error.' } & $Cli capabilities --help | Out-Null if ($LASTEXITCODE -ne 0) { throw 'Package installed, but Client command verification failed (capabilities --help). Retry installation after reviewing the command error.' } - & $Cli setup select --help | Out-Null - if ($LASTEXITCODE -ne 0) { throw 'Package installed, but integration command verification failed (setup select --help). Retry installation after reviewing the command error.' } if ($RuntimeProfile -eq 'local') { & $Cli config init --help | Out-Null if ($LASTEXITCODE -ne 0) { throw 'Package installed, but configuration command verification failed (config init --help). Retry installation after reviewing the command error.' } diff --git a/website/public/install.sh b/website/public/install.sh index 8bd8246d44..1990b039d5 100644 --- a/website/public/install.sh +++ b/website/public/install.sh @@ -337,8 +337,6 @@ main() { fail "Package installed, but CLI help verification failed. Retry installation after reviewing the command error." "$tool_bin/powercontext" capabilities --help >/dev/null || fail "Package installed, but Client command verification failed (capabilities --help). Retry installation after reviewing the command error." - "$tool_bin/powercontext" setup select --help >/dev/null || - fail "Package installed, but integration command verification failed (setup select --help). Retry installation after reviewing the command error." if [[ "$PROFILE" == local ]]; then "$tool_bin/powercontext" config init --help >/dev/null || fail "Package installed, but configuration command verification failed (config init --help). Retry installation after reviewing the command error." From 9a676f146a0e55276a6c4bc941ab971118da7b3c Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 9 Oct 2026 01:09:46 +0800 Subject: [PATCH 09/24] fix(cli): keep Agent setup available without Click dependency --- src/powercontext/cli/system.py | 3 +-- tests/test_dsh_profiles.py | 8 +++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/powercontext/cli/system.py b/src/powercontext/cli/system.py index 6928219c47..559d6db91f 100644 --- a/src/powercontext/cli/system.py +++ b/src/powercontext/cli/system.py @@ -36,7 +36,6 @@ from urllib.parse import urlsplit, urlunsplit from urllib.request import Request, urlopen -import click import typer from pydantic import ValidationError @@ -607,7 +606,7 @@ def setup_dsh( try: if profile is None: profile = ( - DshProfile(typer.prompt("DSH profile", default="web", type=click.Choice(["web", "desktop"]))) + DshProfile(typer.prompt("DSH profile (web/desktop)", default="web", type=DshProfile)) if not json_output and stdin_is_tty() else DshProfile.WEB ) diff --git a/tests/test_dsh_profiles.py b/tests/test_dsh_profiles.py index 3c5e34d0fe..4f427a589e 100644 --- a/tests/test_dsh_profiles.py +++ b/tests/test_dsh_profiles.py @@ -57,7 +57,7 @@ def setup_boundary(tmp_path, monkeypatch): return plugin, run, inspect -@pytest.mark.parametrize("mode", ["interactive", "explicit", "json", "noninteractive"]) +@pytest.mark.parametrize("mode", ["interactive", "interactive-retry", "explicit", "json", "noninteractive"]) def test_setup_selects_profile_and_reports_destination(tmp_path, monkeypatch, desktop_command, setup_boundary, mode): import powercontext.cli.system as system @@ -66,13 +66,15 @@ def test_setup_selects_profile_and_reports_destination(tmp_path, monkeypatch, de desktop_dir = Path(os.environ["DSH_HOME"]) / "profiles/desktop" desktop_dir.mkdir(parents=True) (desktop_dir / "package.json").write_text("{}") - monkeypatch.setattr(system, "stdin_is_tty", lambda: mode in {"interactive", "json"}) + monkeypatch.setattr(system, "stdin_is_tty", lambda: mode in {"interactive", "interactive-retry", "json"}) args = ["setup", "dsh", "--source", str(plugin), "--dsh-command", str(desktop_command)] if mode == "explicit": args += ["--profile", "desktop"] if mode == "json": args += ["--json"] - result = CliRunner().invoke(create_cli([setup_app]), args, input="desktop\n") + result = CliRunner().invoke( + create_cli([setup_app]), args, input="invalid\ndesktop\n" if mode == "interactive-retry" else "desktop\n" + ) assert result.exit_code == 0, result.output assert run.call_args_list[0].args[2] == profile assert run.call_args_list[0].kwargs["executable"] == str(desktop_command.resolve()) From e61ca58824d5951c9f85288d112a9f0ae6f1165e Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 9 Oct 2026 01:19:05 +0800 Subject: [PATCH 10/24] fix(install): return prerelease installations to the stable channel --- tests/native/test_install_sources.py | 2 ++ website/public/install.ps1 | 3 ++- website/public/install.sh | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/native/test_install_sources.py b/tests/native/test_install_sources.py index f1dbc9b18e..041c4c0832 100644 --- a/tests/native/test_install_sources.py +++ b/tests/native/test_install_sources.py @@ -183,6 +183,8 @@ def test_latest_stable_and_explicit_prerelease(installation): assert version() == "1.1.0" install("--region", "cn", "--version", "1.2.0rc1") assert version() == "1.2.0rc1" + install("--region", "cn") + assert version() == "1.1.0" def test_unavailable_automatic_mirror_falls_back_before_tool_install(installation): diff --git a/website/public/install.ps1 b/website/public/install.ps1 index 0d0578154c..bccb76cdb7 100644 --- a/website/public/install.ps1 +++ b/website/public/install.ps1 @@ -295,7 +295,8 @@ try { $Requirement = 'powercontext[cli,server]' if ($RuntimeProfile -eq 'client') { $Requirement = 'powercontext[cli]' } $InstallArgs = @('tool', 'install', '--python', $Python.Trim(), '--no-python-downloads') - if ($Version -eq 'latest') { $InstallArgs += @('--upgrade', '--prerelease', 'disallow') } + # An installed prerelease otherwise remains eligible even with prereleases disallowed. + if ($Version -eq 'latest') { $InstallArgs += @('--upgrade', '--reinstall-package', 'powercontext', '--prerelease', 'disallow') } else { $Requirement += "==$Version" } $InstallArgs += $Requirement if ($IndexUrl) { $InstallArgs += @('--default-index', $IndexUrl) } diff --git a/website/public/install.sh b/website/public/install.sh index 1990b039d5..783b430aee 100644 --- a/website/public/install.sh +++ b/website/public/install.sh @@ -315,7 +315,8 @@ main() { [[ "$PROFILE" != client ]] || requirement='powercontext[cli]' local install_args=(tool install --python "$PYTHON_BIN" --no-python-downloads) if [[ "$VERSION" == latest ]]; then - install_args+=(--upgrade --prerelease disallow) + # An installed prerelease otherwise remains eligible even with prereleases disallowed. + install_args+=(--upgrade --reinstall-package powercontext --prerelease disallow) else requirement="$requirement==$VERSION" fi From 06071400a12c51d8acea71f840719a629de99b25 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 9 Oct 2026 01:21:21 +0800 Subject: [PATCH 11/24] test(install): qualify shared contracts and real release transitions --- .github/workflows/installation.yml | 8 ++++++ tests/fixtures/installation/README.md | 40 +++++++++++++++++++++++++++ tests/native/test_installation.py | 28 +++++++++++++------ 3 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 tests/fixtures/installation/README.md diff --git a/.github/workflows/installation.yml b/.github/workflows/installation.yml index ad6500e17f..8d004cc417 100644 --- a/.github/workflows/installation.yml +++ b/.github/workflows/installation.yml @@ -37,6 +37,13 @@ jobs: SETUPTOOLS_SCM_PRETEND_VERSION: "1.2.0" run: uv build --wheel --out-dir dist/installation + - name: Check shared installer behavior and source resolution + shell: bash + run: >- + uv run --no-sync pytest -q tests/test_installation_contract.py tests/native/test_install_sources.py + --junitxml "$RUNNER_TEMP/installation-contract.xml" + --basetemp "$RUNNER_TEMP/installation-contract-tests" + - name: Exercise bootstrap, version selection, profiles, and persistent memory shell: bash env: @@ -53,5 +60,6 @@ jobs: name: installation-${{ matrix.os }} path: | ${{ runner.temp }}/installation.xml + ${{ runner.temp }}/installation-contract.xml ${{ runner.temp }}/installation-tests/**/*.log if-no-files-found: ignore diff --git a/tests/fixtures/installation/README.md b/tests/fixtures/installation/README.md new file mode 100644 index 0000000000..b700a8ccd5 --- /dev/null +++ b/tests/fixtures/installation/README.md @@ -0,0 +1,40 @@ +# Installer behavior contract + +The JSON catalogs describe public installer inputs and observable outcomes. Both native shell entry points use +the same cases through `tests/test_installation_contract.py`. The catalogs are test inputs, not bootstrap downloads +or an additional installation manifest. RFC 1892 defines the overall installation and recovery boundary. + +Run the offline contract with an existing Python and uv: + +```bash +uv run pytest tests/test_installation_contract.py -q +``` + +Each file has `schema_version: 1` and a `cases` array. Case IDs must be unique across catalogs. + +| Field | Meaning | +| --- | --- | +| `id` | Stable scenario name shown in test results | +| `args` | Arguments passed to the native installer | +| `git` | Whether Git is discoverable; defaults to false | +| `setup_exit` | Exit status of the fixture host operation; defaults to success | +| `reported_version` | Fixture CLI version; defaults to the wheel's `1.2.0` | +| `missing_commands` | Public command help paths omitted by the fixture | +| `expected.exit_code` | Installer completion or failure | +| `expected.runtime_installed` | Whether uv installed the tool launcher, including verification failures | +| `expected.server_extra` | Whether the selected profile contains the fixture Server dependency | +| `expected.selected_hosts` | Hosts actually submitted to setup; defaults to none | +| `expected.setup_ref` | Release tag actually submitted to setup | +| `expected.verification_failed` | Whether success must remain unannounced | +| `expected.error_contains` | Required diagnostic fragment | + +The harness builds small standards-shaped wheels, installs them through real uv in private directories without +network access, and executes the resulting native launcher. Profile cases inspect an optional installed dependency. +Host cases observe the setup request and result through the fixture CLI. They do not start a real Agent, execute Git, +or establish Server readiness. No case requires a particular shell function name, private call order or subprocess count. + +The installation CI matrix runs these cases on Linux, macOS and Windows. The same workflow also builds the actual +PowerContext wheel and runs `tests/native/test_installation.py` for bootstrap, configuration, HTTP readiness, +Memory persistence, profile replacement and version selection. `tests/native/test_install_sources.py` adds Linux +loopback index fault tests using the real downloader and uv, with only distribution endpoints redirected to fixtures. +Those source tests do not qualify public mirror availability or native Windows source handling. diff --git a/tests/native/test_installation.py b/tests/native/test_installation.py index e3c283917a..5b9c20d39b 100644 --- a/tests/native/test_installation.py +++ b/tests/native/test_installation.py @@ -17,7 +17,6 @@ from __future__ import annotations import email -import hashlib import os import shutil import socket @@ -166,10 +165,9 @@ def test_install_configure_remember_and_reinstall(tmp_path: Path, environment: s with zipfile.ZipFile(wheel) as archive: metadata = next(name for name in archive.namelist() if name.endswith(".dist-info/METADATA")) version = email.message_from_bytes(archive.read(metadata))["Version"] - # The direct URL and checksum constraint make this a test of the current build. + # The direct URL selects this build; a file URL hash fragment is not an integrity check. constraints = tmp_path / "constraints.txt" - checksum = hashlib.sha256(wheel.read_bytes()).hexdigest() - constraints.write_text(f"powercontext @ {wheel.as_uri()}#sha256={checksum}\n", encoding="utf-8") + constraints.write_text(f"powercontext @ {wheel.as_uri()}\n", encoding="utf-8") home_dir = tmp_path / "home with spaces 测试" home_dir.mkdir() env = { @@ -243,8 +241,20 @@ def test_install_configure_remember_and_reinstall(tmp_path: Path, environment: s ) run(installer, tmp_path, env, "install") if environment == "existing": - # An explicitly selected source must not silently fall back to a public index. - run([*installer, "--index-url", "https://127.0.0.1:9/simple"], tmp_path, env, "explicit-index", success=False) + # A fresh tool/cache forces resolution; exact reinstallation may legitimately reuse the existing tool. + failed_source_env = { + **env, + "UV_TOOL_DIR": str(tmp_path / "failed-source-tools"), + "UV_TOOL_BIN_DIR": str(tmp_path / "failed-source-bin"), + "UV_CACHE_DIR": str(tmp_path / "failed-source-cache"), + } + run( + [*installer, "--index-url", "https://127.0.0.1:9/simple"], + tmp_path, + failed_source_env, + "explicit-index", + success=False, + ) assert run([cli, "--version"], tmp_path, env, "version").strip() == version run([cli, "config", "init", "--template", "--output", ".env"], tmp_path, env, "configure", stdin="\n") config = (tmp_path / ".env").read_bytes() @@ -325,8 +335,8 @@ def test_latest_exact_versions_and_client_profile(tmp_path: Path) -> None: worker.start() env["UV_INDEX"] = f"http://127.0.0.1:{server.server_port}" try: - # Invalid unattended input must fail before any tool installation. - run(installer, tmp_path, env, "missing-host-choice", success=False) + # Contradictory host intent must fail before any tool installation. + run([*installer, "--host", "codex", "--no-hosts"], tmp_path, env, "host-choice-conflict", success=False) assert not Path(cli).exists() client_install = [*installer, "--no-hosts", "--profile", "client"] run([*client_install, "--version", "1.2.0"], tmp_path, env, "install-old") @@ -355,6 +365,8 @@ def test_latest_exact_versions_and_client_profile(tmp_path: Path) -> None: assert run([cli, "--version"], tmp_path, env, "prerelease-version").strip() == "1.2.2rc1" run([*client_install, "--version", "99.0.0"], tmp_path, env, "missing-version", success=False) assert run([cli, "--version"], tmp_path, env, "preserved-version").strip() == "1.2.2rc1" + run(client_install, tmp_path, env, "stable-after-prerelease") + assert run([cli, "--version"], tmp_path, env, "restored-stable-version").strip() == "1.2.1" run([*installer, "--no-hosts", "--version", "1.2.0"], tmp_path, env, "switch-to-local") run([python, "-c", "import fastapi"], tmp_path, env, "local-includes-server") run([*client_install, "--version", "1.2.0"], tmp_path, env, "switch-to-client") From 0d79b70781a0c5200d685d1015a2c3a39d11d4dc Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 9 Oct 2026 01:21:21 +0800 Subject: [PATCH 12/24] docs(rfc): define the executable installation contract --- README.md | 4 +- README_CN.md | 4 +- README_JP.md | 4 +- docs/en/docs/get-started/install-and-run.md | 30 ++++--- docs/en/docs/get-started/quickstart.md | 2 +- docs/en/docs/operate/connect-remote-server.md | 2 +- docs/en/rfcs/1892-script-installation.md | 90 +++++++++++++------ docs/zh/docs/get-started/install-and-run.md | 28 +++--- docs/zh/docs/get-started/quickstart.md | 2 +- docs/zh/docs/operate/connect-remote-server.md | 2 +- docs/zh/rfcs/1892-script-installation.md | 77 +++++++++++----- 11 files changed, 163 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 8d6050635a..f22ec43d9e 100644 --- a/README.md +++ b/README.md @@ -32,13 +32,13 @@ macOS and Linux are supported; Windows support is `experimental`. Install with the recommended script: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash ``` On Windows, use PowerShell: ```powershell -powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1))) --no-hosts" +powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` Apply the PATH command printed by the installer, then configure PowerContext: diff --git a/README_CN.md b/README_CN.md index 49e698a945..5bfe7056d8 100644 --- a/README_CN.md +++ b/README_CN.md @@ -32,13 +32,13 @@ PowerContext 让上下文跟随工作,跨越不同的对话。你回来时, 推荐使用安装脚本: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash ``` Windows 使用 PowerShell: ```powershell -powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1))) --no-hosts" +powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` 先执行安装器打印的 PATH 命令,再继续配置: diff --git a/README_JP.md b/README_JP.md index 3ea3d6e8ab..7ed0c55a09 100644 --- a/README_JP.md +++ b/README_JP.md @@ -29,13 +29,13 @@ PowerContext は、会話をまたいでもコンテキストを作業ととも 推奨のインストールスクリプトは、必要に応じて uv と Python を用意します。Agent 連携には Git と Agent CLI が必要です: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash ``` Windows では PowerShell を使用します: ```powershell -powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1))) --no-hosts" +powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` 表示された PATH 設定を実行してから、設定を続けます: diff --git a/docs/en/docs/get-started/install-and-run.md b/docs/en/docs/get-started/install-and-run.md index 107850959f..57e8d68610 100644 --- a/docs/en/docs/get-started/install-and-run.md +++ b/docs/en/docs/get-started/install-and-run.md @@ -30,23 +30,22 @@ or installs uv and Python 3.12 in user-owned directories. It does not need admin macOS or Linux (Bash; `curl` or `wget` is required): ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash ``` Windows (PowerShell 5.1 or newer): ```powershell -powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1))) --no-hosts" +powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` Apply the PATH command printed at completion before running `powercontext` in your terminal. The installer does not edit shell startup files or the persistent Windows PATH. `UV_INSTALL_DIR` selects where a missing uv is installed; `UV_TOOL_BIN_DIR` selects the PowerContext executable directory. -`--no-hosts` installs software without prompting for Agent integrations. Omit it in an interactive terminal to open -`powercontext setup select`, or pass `--host codex` (repeatable) to select integrations explicitly. Agent setup needs -Git and each host's prerequisites. A pipeline still reads host choices from the terminal; unattended execution must -use `--no-hosts` or `--host`. The [capability matrix](../integrations/capabilities.md) describes host support. +Installation runs unattended by default. Pass `--host codex` (repeatable) to install selected Agent integrations. +Only Agent setup needs Git and each host's prerequisites. `--no-hosts` remains an optional way to state the default; +it cannot be combined with `--host`. The [capability matrix](../integrations/capabilities.md) describes host support. For an existing remote Server, add `--profile client` to install only the CLI and Client dependencies. The default `--profile local` includes the local Server. Neither profile starts a Server, registers a service, or overwrites @@ -72,10 +71,16 @@ version and uses its matching tag; it never uses the moving `master` branch for powercontext setup codex --ref "powercontext-v$(powercontext --version)" ``` +Use `powercontext setup select --ref "powercontext-v$(powercontext --version)"` for an interactive host picker. + In PowerShell, the same double-quoted expression works. An integration failure leaves the installed Runtime usable and exits with an error and a retry instruction. Installation success does not establish Server readiness or host workflow correctness; use the checks below and the integration's own guide. +The installer verifies the installed version and advertised commands before reporting success. A package can install +successfully while command verification fails; in that case the old executable may already have been replaced. Review +the named command error and rerun with a known working `--version`. The installer does not roll package files back. + ## Retry dependency downloads with a mirror Package indexes, uv binaries, and Python distributions are separate downloads. Changing the package index does not @@ -89,14 +94,15 @@ change the uv or Python download location. | Python distributions | `UV_PYTHON_INSTALL_MIRROR` | NJU python-build-standalone mirror | uv default channels | ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts --region cn -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts --index-url https://pypi.org/simple +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --region cn +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --index-url https://pypi.org/simple ``` `--region auto|cn|global` overrides `POWERCONTEXT_INSTALL_REGION`. Auto selection uses a named local timezone, then the locale territory, then global; it makes no geolocation request. An unavailable automatic mirror can fall back to the -official source. Once a package index passes its availability/version check, uv reports subsequent resolution or -artifact failures without retrying installation against another index. Explicit sources never fall back automatically. +official source. A reachable China package mirror remains selected even if it lacks the requested release; uv reports +resolution or artifact failures without retrying against another index. Use `--region global` or an explicit source +to select another index. Explicit sources never fall back automatically. A mirror may lag PyPI: `latest` means the newest compatible stable release on the chosen index. Existing uv index settings and configuration files take precedence over automatic package mirrors. An explicit @@ -110,7 +116,7 @@ To inspect the installer or pass several options in PowerShell, save it first: ```powershell irm https://powercontext.oceanbase.io/install.ps1 -OutFile install.ps1 -powershell -ExecutionPolicy Bypass -File .\install.ps1 --no-hosts --region cn --version 1.2.0 +powershell -ExecutionPolicy Bypass -File .\install.ps1 --region cn --version 1.2.0 ``` Cached reinstallation with `UV_OFFLINE=1` can work when uv, compatible Python, and all required packages are already @@ -262,7 +268,7 @@ see [Connect to a remote Server](../operate/connect-remote-server.md). To upgrade to the latest stable version, rerun the installer. To keep an exact release, add `--version`: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash ``` To replace the installed tool with another Git ref: diff --git a/docs/en/docs/get-started/quickstart.md b/docs/en/docs/get-started/quickstart.md index f9a107e5ef..2569bdfa2f 100644 --- a/docs/en/docs/get-started/quickstart.md +++ b/docs/en/docs/get-started/quickstart.md @@ -18,7 +18,7 @@ and recall; that does not enable automatic Topic Memory. ## 1. Install and open the wizard ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash ``` Apply the PATH command printed by the installer, then configure PowerContext: diff --git a/docs/en/docs/operate/connect-remote-server.md b/docs/en/docs/operate/connect-remote-server.md index 92225caa48..1481e1db47 100644 --- a/docs/en/docs/operate/connect-remote-server.md +++ b/docs/en/docs/operate/connect-remote-server.md @@ -15,7 +15,7 @@ Use the installation script on the machine running your Agent. It provisions uv the latest stable CLI and Client without local Server dependencies: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --profile client --no-hosts +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --profile client ``` For Windows PowerShell, exact versions, and mirrors, see [Install and run](../get-started/install-and-run.md). diff --git a/docs/en/rfcs/1892-script-installation.md b/docs/en/rfcs/1892-script-installation.md index ee330fc5fe..bc4d07d992 100644 --- a/docs/en/rfcs/1892-script-installation.md +++ b/docs/en/rfcs/1892-script-installation.md @@ -40,13 +40,13 @@ selection, explicit host selection, recoverable partial failures, and verificati On macOS or Linux: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash ``` On Windows, using PowerShell 5.1 or newer: ```powershell -powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1))) --no-hosts" +powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` `local`, the default profile, installs CLI, Client, and local Server dependencies. `--profile client` installs CLI and @@ -66,9 +66,10 @@ repeatable package selection, use an exact version: curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.2.0 --host codex ``` -`--host` is repeatable. `--no-hosts` skips integration setup. With neither option, an interactive terminal opens -`powercontext setup select`; without a terminal the installer fails before provisioning. Piped Bash installation -uses `/dev/tty` for selection, separate from script input. Git and each selected host's own prerequisites are required. +Installation runs unattended by default, without Git or a host picker. `--host` is repeatable and opts into integration +setup; selected hosts require Git and their own prerequisites. `--no-hosts` explicitly states the default and cannot +be combined with `--host`. For an interactive picker after installation, run +`powercontext setup select --ref "powercontext-v$(powercontext --version)"`. The installed CLI supplies the exact version for `powercontext-vVERSION`. Host setup never resolves `latest` again and never defaults to `master`. If a host installation fails, the Runtime remains installed and the script returns @@ -116,16 +117,19 @@ into shell, add a standalone Python installer engine, or change Runtime/public H ## Version and profile semantics -- `latest` passes an unpinned profile requirement to `uv tool install --upgrade --prerelease disallow`. It means the +- `latest` passes an unpinned profile requirement to `uv tool install --upgrade --reinstall-package powercontext --prerelease disallow`. It means the newest stable version compatible with the selected interpreter and configured sources, not necessarily the version - most recently uploaded to another mirror. Resolution failures are reported without substituting another requirement. + most recently uploaded to another mirror. The PowerContext package is reinstalled so an existing prerelease cannot bypass stable selection. Resolution failures + are reported without substituting another requirement. - An exact `X.Y.Z`, optionally followed by `aN`, `bN`, or `rcN`, uses `==VERSION`. Explicit prereleases are supported; source refs, ranges, and URLs are not `--version` values. Releases before 0.1.0 are excluded. - `local` uses `powercontext[cli,server]`; `client` uses `powercontext[cli]`, whose dependencies include the Client. Rerunning with a different profile replaces that tool's dependency set. Independently added extras must be managed through the documented manual installation path. -- The installed executable must report a release version. Exact requests must match it. The script prints success - only after this check. The user receives a usable Runtime even if subsequent host setup fails. +- The executable in `uv tool dir --bin` must report a release version. Exact requests must match it. All profiles + must expose CLI help and `capabilities --help`; `local` additionally requires + `config init --help` and `server run --help`. The script prints success only after these commands succeed. + These checks establish imports and command availability, not Server readiness or Agent workflow correctness. uv is bootstrapped at an installer-controlled version; that version is independent of the PowerContext release. An existing uv is reused rather than upgraded silently. Python discovery excludes virtual environments so a project @@ -139,10 +143,11 @@ mirror installer falls back to Astral; failed mirrored uv artifacts may use offi URL is obtained from uv, and its mirrored artifact is checked before installation. If the automatic Python mirror fails, installation retries with uv's default channels. -Automatic package selection checks the PowerContext index page and, for exact requests, the listed version. An -unavailable or unsynchronized China index falls back to PyPI. After selection, uv owns dependency resolution and -artifact errors; the script does not retry a partially attempted tool installation against another index. -Explicit indexes bypass shell probing so uv can apply its authentication and configuration correctly. +Automatic China package selection makes a bounded availability request to the PowerContext index page. An +unreachable mirror falls back to PyPI before tool installation. A reachable mirror remains selected even if it lacks +the requested release; uv reports that resolution failure. Global and explicit sources go directly to uv. The scripts +never interpret index HTML, wheel filenames, encoded URLs, package compatibility, or authentication. They do not +switch indexes after a tool installation attempt, which may already have changed installed files. Shell environment changes are local to the installer process; PowerShell restores temporary source variables in `finally`. Existing configuration files are neither parsed by shell nor rewritten. Only installer-owned temporary @@ -155,24 +160,33 @@ or restart a running service. Users follow the existing upgrade and migration in Server. An exact-version retry may reuse cached packages; `latest` intentionally allows upgrades. `UV_OFFLINE=1` permits cached reinstallation only when uv, a compatible Python, and all dependencies are present. -It does not promise a complete offline distribution. A failure after bootstrapping leaves installed prerequisites -available for retry; a failure in host setup preserves the Runtime. There is no cross-component rollback or repair -of unrelated host configuration. +It disables installer source probes as well as uv networking. It does not promise a complete offline distribution. +Prerequisites remain available after later failures. A resolution failure for an unavailable exact version preserves +the existing tool. Once uv accepts a package, verification can fail after replacing the old executable: report +"package installed, verification failed" and allow an explicit version retry, without claiming rollback. A later host +setup failure preserves the verified Runtime. There is no cross-component rollback or repair of unrelated host state. Windows retains its experimental product status. Native acceptance runs on Linux, macOS, and Windows; host-specific support still comes from each integration's capability contract. -## Acceptance +## Executable contract and acceptance -`tests/native/test_installation.py` runs the actual shell installer, uv, installed CLI, and HTTP Server. A wheel built -from the tested commit uses release-shaped metadata and an exact file/checksum constraint. Qualification includes: +`tests/fixtures/installation/*.json` defines shared public inputs and expected outcomes for both shell adapters. +`tests/test_installation_contract.py` executes those cases with real uv and small offline fixture wheels: host opt-in, +profiles, exact-version verification, unavailable commands, and partial host failure. Cases observe installed +capabilities, selected hosts, release refs and exit results. They do not fix internal function boundaries or call order. +The same catalog runs on each native operating system; fixture host execution is not real Agent acceptance. + +`tests/native/test_installation.py` runs the actual shell installer, uv, installed PowerContext CLI, and HTTP Server. +A wheel built from the tested commit uses release-shaped metadata and a direct local file constraint. That constraint +selects the tested artifact; it does not establish an independently enforced checksum. Qualification includes: - missing uv/Python under global and China source selection, existing tools/configuration, and spaces/Unicode paths; - real stable/prerelease wheel resolution through a controlled package index, default upgrade, exact selection, and preserving the installed version when a requested release is unavailable; - Client-only installation and profile changes, with no local Server state created by installation; - `.env` generation and validation, readiness, Memory remember/search, cached offline reinstall, and restart readback; -- explicit index failure, non-interactive missing choices, piped Bash terminal selection, and the downloaded +- explicit index failure, conflicting host choices, unattended and piped Bash installation, and the downloaded PowerShell scriptblock entry point. A separate CI matrix executes the suite on all three operating systems. Release reference checks preserve script @@ -180,6 +194,12 @@ A separate CI matrix executes the suite on all three operating systems. Release static build. Syntax checks alone do not establish Windows or macOS installation support, and these tests do not claim that a real Agent host completes its capture/recall workflow. +[Reproducible first-principles and ablation studies](https://github.com/PsiACE/powercontext/tree/feat/installation-contract/experiments/installation) pin upstream +sources, installer baselines, uv versions, controlled inputs and observed results. They support these decisions: +removing implicit host selection preserves explicit host failure semantics; a filename-based preflight rejects valid +encoded package URLs; prerelease-only indexes require explicit stable policy; and an accepted package can still have +an unusable entry point. They remain separate from maintained product acceptance and carry their own execution limits. + # Drawbacks Bash and PowerShell duplicate some sequencing and source-selection policy. Automatic mirrors add operational @@ -192,18 +212,36 @@ A uv-only command is retained for users who already manage Python and uv; it can Source installation is useful for development but is not the default release channel. Pinning the public script to a PowerContext version would leave new users on stale releases until the website is republished. -A standalone installer engine and immutable component manifest can serve richer installation plans. They are not -prerequisites for package bootstrap or reliable source controls. Delegating to existing package and host adapters -keeps this change independently deliverable without removing working CLI contracts. +A Python engine still needs a bootstrap on machines without Python. Generating the shell adapters would still require +native behavioral tests and would add generator/versioning machinery. Shared conformance cases provide executable +parity without another runtime asset. Introduce an engine or generator only when a concrete policy needs it. + +Immutable manifests and a second installation ledger are useful when distributing independently versioned raw +artifacts. Here uv owns wheel resolution, tool environments and its records. The installer observes the actual launcher +and delegates repair to an explicit uv-backed retry. This boundary avoids two authorities for the same installation. # Prior art [RFC 1408](https://github.com/oceanbase/powercontext/pull/1408) defines the separation of installation, configuration, and diagnostics; independent Runtime profiles and host selection; component-level recovery; and explicit service registration. [RFC 1299](1299_local_server_availability_and_service_installation.md) defines personal service lifecycle. -[Bub](https://github.com/bubbuild/bub/tree/main/website/public) provides Bash/PowerShell bootstrap entry points that -reuse uv and report configuration next steps. [uv](https://docs.astral.sh/uv/guides/tools/) supplies isolated tools, -Python provisioning, package indexes, and caches. + +[Magpie's installer](https://github.com/yetone/magpie/blob/023f5aaad2ecd41ae04390166b9cac9a0b300d81/site/public/install.sh) +separates an authoritative release feed from artifact mirrors and verifies a binary checksum before replacement. +Its raw binary ownership justifies that verification; its manifest is not a replacement for Python package standards. + +[Lody's daemon installation contract](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/specs/daemon-upgrade-installation.md) +resolves and executes the actual npm installation destination and checks replacement readiness. Its separate +[managed Agent runtime](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/apps/cli/src/agent/managed-agent-runtime.ts) +owns raw archives and therefore owns manifests and completion records. Apply the actual-executable check here; +retain service readiness in the service boundary. The public tag-release workflow does not publish installers. + +[Bub](https://github.com/bubbuild/bub/tree/b4a61bf1326729a024161d22ba20019b8500f907/website/public) bootstraps uv before +running its Python preset resolver. Its terminal and macOS Bash fixes motivate explicit host setup and native tests. +[uv tools](https://docs.astral.sh/uv/concepts/tools/) and +[configuration](https://docs.astral.sh/uv/concepts/configuration-files/) define the reused environment and source +semantics; the [Python Simple API](https://packaging.python.org/en/latest/specifications/simple-repository-api/) +defines package index interpretation. A shell regex cannot implement those contracts. # Unresolved questions diff --git a/docs/zh/docs/get-started/install-and-run.md b/docs/zh/docs/get-started/install-and-run.md index 825a227961..e888f8afc2 100644 --- a/docs/zh/docs/get-started/install-and-run.md +++ b/docs/zh/docs/get-started/install-and-run.md @@ -28,21 +28,21 @@ uv 和 Python 3.12,无需管理员权限。 macOS 或 Linux(使用 Bash,需要 `curl` 或 `wget`): ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash ``` Windows(PowerShell 5.1 或更新版本): ```powershell -powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1))) --no-hosts" +powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` 先执行安装完成时打印的 PATH 命令,再在终端运行 `powercontext`。安装器不会修改 shell 启动文件或 Windows 的持久 PATH。`UV_INSTALL_DIR` 指定缺少 uv 时的安装目录,`UV_TOOL_BIN_DIR` 指定 PowerContext 可执行文件目录。 -`--no-hosts` 只安装软件,不询问 Agent 集成。在交互终端省略它会打开 `powercontext setup select`,也可以使用 -`--host codex` 显式选择集成;多个宿主可重复传入 `--host`。集成安装需要 Git 及对应宿主的前置条件。管道安装仍从终端 -读取宿主选择;无人值守执行必须传入 `--no-hosts` 或 `--host`。宿主支持范围见[能力矩阵](../integrations/capabilities.md)。 +默认安装可以无人值守执行。使用 `--host codex` 安装选定的 Agent 集成;多个宿主可重复传入 `--host`。 +只有集成安装需要 Git 及对应宿主的前置条件。`--no-hosts` 可以显式表达默认行为,不能与 `--host` 同时使用。 +宿主支持范围见[能力矩阵](../integrations/capabilities.md)。 连接已有远程 Server 时,加上 `--profile client`,只安装 CLI 和 Client 依赖。默认 `--profile local` 包含本地 Server。 两种模式都不会启动 Server、注册服务或覆盖配置与数据。本地安装使用 `powercontext config init`,继续阅读 @@ -66,9 +66,14 @@ Python 包版本为 `1.2.0`,对应 Git tag 为 `powercontext-v1.2.0`。使用 powercontext setup codex --ref "powercontext-v$(powercontext --version)" ``` +需要交互选择宿主时,运行 `powercontext setup select --ref "powercontext-v$(powercontext --version)"`。 + PowerShell 也支持这里的双引号表达式。集成安装失败时,已安装的 Runtime 会保留,脚本返回错误并给出重试提示。 安装成功不代表 Server 就绪或宿主工作流可用;仍需执行下文检查及集成文档中的验证。 +安装器在报告成功前验证版本和所选模式的命令。包安装成功后仍可能无法通过命令验证,此时旧可执行文件可能已被替换。 +请查看错误中指出的命令,再指定已知可用的 `--version` 重试。安装器不回滚包文件。 + ## 使用镜像重试依赖下载 包索引、uv 二进制和 Python 发行版是三类独立下载。更换包索引不会改变 uv 或 Python 的下载位置。 @@ -81,13 +86,14 @@ PowerShell 也支持这里的双引号表达式。集成安装失败时,已安 | Python 发行版 | `UV_PYTHON_INSTALL_MIRROR` | NJU python-build-standalone 镜像 | uv 默认渠道 | ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts --region cn -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts --index-url https://pypi.org/simple +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --region cn +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --index-url https://pypi.org/simple ``` `--region auto|cn|global` 优先于 `POWERCONTEXT_INSTALL_REGION`。自动选择依次参考本地命名时区、locale 地区,最后采用 -全球源,不请求网络定位服务。自动镜像不可用时可回退官方源;包索引通过可用性和版本检查后,后续解析或文件下载失败 -由 uv 报告,不会再换源重试安装。显式配置的源不会自动回退。镜像可能有同步延迟:`latest` 指所选源中的最新兼容稳定版。 +全球源,不请求网络定位服务。自动镜像不可用时可回退官方源;中国区域包镜像可达时,即使缺少请求版本,也保持选中。 +解析或文件下载失败由 uv 报告,不会再换源重试安装。需要换源时使用 `--region global` 或显式指定来源。 +显式配置的源不会自动回退。镜像可能有同步延迟:`latest` 指所选源中的最新兼容稳定版。 已有的 uv 索引环境变量和配置文件优先于自动包镜像。`--index-url` 只覆盖默认索引,额外 uv 索引仍保留其优先级。 私有源凭据应使用 uv 认证配置,不放在脚本参数里。uv 不读取 `PIP_INDEX_URL` 或 `PIP_EXTRA_INDEX_URL`。 @@ -98,7 +104,7 @@ curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts ```powershell irm https://powercontext.oceanbase.io/install.ps1 -OutFile install.ps1 -powershell -ExecutionPolicy Bypass -File .\install.ps1 --no-hosts --region cn --version 1.2.0 +powershell -ExecutionPolicy Bypass -File .\install.ps1 --region cn --version 1.2.0 ``` 已有 uv、兼容 Python 和完整依赖缓存时,可以使用 `UV_OFFLINE=1` 重装。这不等于离线发行包;缺少下载内容时会明确报错。 @@ -236,7 +242,7 @@ Server、客户端和 Agent 集成需一起升级。Dashboard 需要显式启用 重新运行安装器可升级到最新稳定版;需要保持指定版本时,加上 `--version`: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash ``` 使用其他 Git ref 替换现有工具: diff --git a/docs/zh/docs/get-started/quickstart.md b/docs/zh/docs/get-started/quickstart.md index f9d23b9cf4..cf98780ce8 100644 --- a/docs/zh/docs/get-started/quickstart.md +++ b/docs/zh/docs/get-started/quickstart.md @@ -17,7 +17,7 @@ Codex 或 Claude 的订阅登录不会自动为 PowerContext Server 提供这些 ## 1. 安装并进入配置向导 ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash ``` 先执行安装器打印的 PATH 命令,再继续配置: diff --git a/docs/zh/docs/operate/connect-remote-server.md b/docs/zh/docs/operate/connect-remote-server.md index 5858814ce6..b90c972a0d 100644 --- a/docs/zh/docs/operate/connect-remote-server.md +++ b/docs/zh/docs/operate/connect-remote-server.md @@ -13,7 +13,7 @@ description: 配置客户端地址,并在需要时明确允许非环回明文 在运行 Agent 的机器上使用安装脚本。它会按需补齐 uv 和 Python,安装最新稳定版 CLI 和 Client,不安装本地 Server 依赖: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --profile client --no-hosts +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --profile client ``` Windows PowerShell、指定版本和镜像选项见[安装和运行](../get-started/install-and-run.md)。 diff --git a/docs/zh/rfcs/1892-script-installation.md b/docs/zh/rfcs/1892-script-installation.md index c45cb3f2a2..1d4e22c71f 100644 --- a/docs/zh/rfcs/1892-script-installation.md +++ b/docs/zh/rfcs/1892-script-installation.md @@ -35,13 +35,13 @@ Python 发行版也来自不同渠道:只更换 PyPI 索引无法解决 Python macOS 或 Linux: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --no-hosts +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash ``` Windows,使用 PowerShell 5.1 或更新版本: ```powershell -powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1))) --no-hosts" +powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` 默认 `local` profile 安装 CLI、Client 和本地 Server 依赖。`--profile client` 只安装连接已有 Server 所需的 CLI 和 @@ -59,9 +59,9 @@ Client 依赖。Profile 与宿主选择相互独立:选择数据库角色不 curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.2.0 --host codex ``` -`--host` 可以重复,`--no-hosts` 跳过集成安装。两者都不指定时,交互终端会打开 `powercontext setup select`; -没有交互终端则在安装依赖前报错。Bash 管道安装使用 `/dev/tty` 读取选择,将终端输入与脚本输入分开。 -安装集成仍需 Git 和各宿主自己的前置条件。 +默认安装无需 Git 或宿主选择界面,可以无人值守执行。`--host` 可以重复,用于显式安装集成;这时需要 Git 和 +各宿主自己的前置条件。`--no-hosts` 显式表达默认行为,不能与 `--host` 同时使用。安装后需要交互选择宿主时,运行 +`powercontext setup select --ref "powercontext-v$(powercontext --version)"`。 已安装的 CLI 提供准确版本,形成 `powercontext-vVERSION`。宿主安装不再次解析 `latest`,也不默认使用 `master`。 宿主安装失败时,保留已安装的 Runtime,脚本返回非零状态,并提示按该 tag 重试 setup;各适配器提供其已有的宿主结果。 @@ -106,14 +106,15 @@ pip 的索引环境变量不属于 uv 配置。Python 和 uv 下载覆盖项与 ## 版本和 profile 语义 -- `latest` 向 `uv tool install --upgrade --prerelease disallow` 传入未固定版本的 profile requirement。 - 它表示所选解释器及已配置源兼容的最新稳定版,不一定是另一镜像上最近上传的版本。解析失败直接报告,不改换 requirement。 +- `latest` 向 `uv tool install --upgrade --reinstall-package powercontext --prerelease disallow` 传入未固定版本的 profile requirement。 + 它表示所选解释器及已配置源兼容的最新稳定版,不一定是另一镜像上最近上传的版本。重新安装 PowerContext 包,避免已有预发布版本绕过稳定版选择。解析失败直接报告,不改换 requirement。 - 准确版本使用 `==VERSION`,格式为 `X.Y.Z`,可追加 `aN`、`bN` 或 `rcN`。支持显式预发布版本; `--version` 不接受源码 ref、版本范围或 URL,也不支持 0.1.0 之前的版本。 - `local` 使用 `powercontext[cli,server]`;`client` 使用 `powercontext[cli]`,后者已经包含 Client 依赖。 使用不同 profile 重跑会替换该工具的依赖集合;自行添加的 extras 应通过文档中的手动安装路径维护。 -- 安装后的可执行文件必须报告发行版本;显式请求必须与其一致。通过检查后才打印安装成功。 - 后续宿主安装失败不会移除可用的 Runtime。 +- `uv tool dir --bin` 中的可执行文件必须报告发行版本;显式请求必须与其一致。所有 profile 都需要通过 CLI help、 + `capabilities --help`;`local` 还需要通过 `config init --help` 和 `server run --help`。 + 全部通过后才打印安装成功。这些检查验证导入和命令可用性,不代表 Server 就绪或 Agent 工作流可用。 uv 使用安装器维护的固定引导版本,与 PowerContext 发行版本无关。已有 uv 直接复用,不悄悄升级。 Python 发现排除虚拟环境,避免项目 venv 意外成为安装前提。没有兼容的 Python 3.11+ 时,通过 uv 安装 Python 3.12。 @@ -125,9 +126,9 @@ Python 发现排除虚拟环境,避免项目 venv 意外成为安装前提。 uv 镜像文件下载失败时可以尝试官方源。Python 的准确构建 URL 由 uv 提供,安装前检查对应镜像文件; 自动 Python 镜像安装失败后,使用 uv 默认渠道重试。 -自动包源选择检查 PowerContext 索引页;准确版本还检查索引是否列出该版本。中国区域索引不可达或尚未同步指定版本时, -回退 PyPI。索引选定后,依赖解析和文件下载错误交给 uv 报告,不再换源重试已经尝试的工具安装。 -显式索引不经过 shell 探测,由 uv 正确处理认证和配置。 +中国区域自动包源选择对 PowerContext 索引页发起有超时限制的可用性请求;镜像不可达时,在工具安装前回退 PyPI。 +可达镜像即使缺少请求版本也保持选中,由 uv 报告解析失败。全球源和显式来源直接交给 uv。脚本不解释索引 HTML、 +wheel 文件名、编码 URL、包兼容性或认证,也不在一次可能已经改变安装文件的工具安装尝试后换源重试。 Shell 环境变化局限于安装器进程;PowerShell 在 `finally` 恢复临时来源变量。不用 shell 解析或改写已有配置文件, 只清理由安装器创建的临时下载目录。保留 `UV_INSTALL_DIR`、`UV_TOOL_DIR` 和 `UV_TOOL_BIN_DIR` 的位置控制。 @@ -137,27 +138,40 @@ Shell 环境变化局限于安装器进程;PowerShell 在 `finally` 恢复临 安装器不读取或改写 `.env`、凭据、数据目录或数据库 schema,也不停止或重启运行中的服务。 启动升级后的 Server 时,用户按现有升级和迁移指南操作。准确版本重试可以使用缓存;`latest` 则明确允许升级。 -`UV_OFFLINE=1` 只支持已有 uv、兼容 Python 和全部依赖时的缓存重装,不承诺完整离线发行包。 -引导依赖安装后发生失败,会保留这些依赖供重试;宿主安装失败保留 Runtime。不提供跨组件回滚, -也不修复无关宿主配置。 +`UV_OFFLINE=1` 只支持已有 uv、兼容 Python 和全部依赖时的缓存重装,同时禁用安装器源探测和 uv 网络访问, +不承诺完整离线发行包。后续步骤失败会保留已安装的前置依赖。准确版本不存在导致的解析失败会保留原工具; +但 uv 接受新包后,验证失败可能发生在旧可执行文件已被替换之后。此时明确报告“包已安装,验证失败”,允许通过准确 +版本重试,不声称已经回滚。后续宿主安装失败则保留已验证的 Runtime。不提供跨组件回滚,也不修复无关宿主状态。 Windows 保持产品的试验性支持状态。原生验收在 Linux、macOS 和 Windows 执行;具体宿主支持范围仍由各集成的能力契约决定。 -## 验收 +## 可执行契约与验收 -`tests/native/test_installation.py` 执行真实 shell 安装器、uv、安装后的 CLI 和 HTTP Server。 -从被测提交构建 wheel,使用发行版形式的元数据和带校验和的准确文件约束。验收包括: +`tests/fixtures/installation/*.json` 定义两个 shell 适配器共享的公开输入与预期结果。 +`tests/test_installation_contract.py` 使用真实 uv 和小型离线 fixture wheel 执行宿主显式选择、profile、准确版本 +校验、命令缺失和宿主部分失败用例。用例观察安装后的能力、所选宿主、发行 ref 和退出结果,不固定内部函数边界或 +调用顺序。同一组用例在各操作系统原生执行;模拟宿主执行不等于真实 Agent 验收。 + +`tests/native/test_installation.py` 执行真实 shell 安装器、uv、安装后的 PowerContext CLI 和 HTTP Server。 +从被测提交构建 wheel,使用发行版形式的元数据和直接本地文件约束。该约束选定被测产物,不构成独立执行的校验和验证。 +验收包括: - 全球源及中国区域源下缺少 uv/Python 的安装、已有工具与配置、包含空格和中文的路径; - 通过受控包索引解析真实稳定版和预发布版 wheel、默认升级、指定版本,以及版本不存在时保留原安装; - Client-only 安装及 profile 切换,安装过程不创建本地 Server 状态; - `.env` 生成与验证、就绪检查、Memory 保存与搜索、离线缓存重装及重启后的回读; -- 显式索引失败、非交互模式缺少选择、Bash 管道终端选择,以及 PowerShell 下载脚本后执行 scriptblock 的入口。 +- 显式索引失败、宿主选项冲突、无人值守与 Bash 管道安装,以及 PowerShell 下载脚本后执行 scriptblock 的入口。 独立 CI 矩阵在三个操作系统执行上述套件。发布引用检查保持脚本的 `latest` 默认值,同时更新显式版本示例。 网站验证检查文档链接和静态构建。语法检查本身不证明 Windows 或 macOS 安装可用;这些测试也不声称 真实 Agent 宿主已完成采集与召回工作流。 +[可复现的第一性原理和消融研究](https://github.com/PsiACE/powercontext/tree/feat/installation-contract/experiments/installation) +固定上游源码、安装器基线、uv 版本、受控输入和 +观测结果。它们支持以下决策:删除隐式宿主选择仍可保留显式宿主失败语义;基于文件名的预检查会拒绝合法编码包 URL; +只有预发布版本的索引需要显式稳定版策略;包安装成功后入口仍可能不可用。研究与持续维护的产品验收分开,分别说明 +实际执行范围和限制。 + # Drawbacks Bash 和 PowerShell 会重复部分编排和来源选择策略。自动镜像增加外部可用性依赖。 @@ -169,16 +183,33 @@ Bash 和 PowerShell 会重复部分编排和来源选择策略。自动镜像增 源码安装适合开发,不作为默认发行渠道。把公共脚本固定在某个 PowerContext 版本,会使新用户在网站重新发布前 一直安装过期版本。 -独立安装引擎和不可变组件清单可以支持更丰富的安装计划,但不是包引导及可靠来源控制的前提。 -复用现有包管理器和宿主适配器,使本项工作可以独立交付,同时保留可用的 CLI 契约。 +没有 Python 的机器仍需要为 Python 引擎提供引导。生成 shell 适配器仍需原生行为测试,还会增加生成器和版本管理。 +共享契约用例可以提供可执行的一致性约束,无需分发新的运行时资源。仅当具体策略确有需要时再引入引擎或生成器。 + +分发独立版本的原始产物时,不可变清单和额外安装账本有其用途。这里由 uv 管理 wheel 解析、工具环境及自身记录; +安装器观察实际入口,通过显式的 uv 安装重试修复组件,避免同一安装状态出现两个权威来源。 # Prior art [RFC 1408](https://github.com/oceanbase/powercontext/pull/1408) 定义了安装、配置和诊断的职责分离, 独立的 Runtime profile 与宿主选择、组件级恢复和显式服务注册。 [RFC 1299](1299_local_server_availability_and_service_installation.md) 定义个人服务生命周期。 -[Bub](https://github.com/bubbuild/bub/tree/main/website/public) 提供复用 uv 并报告后续配置步骤的 Bash/PowerShell 引导入口。 -[uv](https://docs.astral.sh/uv/guides/tools/) 提供独立工具环境、Python 安装、包索引和缓存。 + +[Magpie 安装器](https://github.com/yetone/magpie/blob/023f5aaad2ecd41ae04390166b9cac9a0b300d81/site/public/install.sh) +将权威发行源与产物镜像分开,在替换二进制前检查校验和。它直接拥有原始二进制的分发职责,因此需要这层验证; +其清单不能替代 Python 包标准。 + +[Lody daemon 安装契约](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/specs/daemon-upgrade-installation.md) +解析并执行实际 npm 安装位置,检查替换后的就绪状态。它另有直接管理原始压缩包的 +[Agent runtime](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/apps/cli/src/agent/managed-agent-runtime.ts), +因此拥有清单和完成记录。本方案采用实际可执行文件检查,服务就绪仍归服务边界负责。其公开 tag-release 工作流不发布安装器。 + +[Bub](https://github.com/bubbuild/bub/tree/b4a61bf1326729a024161d22ba20019b8500f907/website/public) 先引导 uv,再运行 +Python preset 解析器。它对终端和 macOS Bash 的修复支持显式宿主安装与原生测试的必要性。 +[uv 工具环境](https://docs.astral.sh/uv/concepts/tools/)及 +[配置规则](https://docs.astral.sh/uv/concepts/configuration-files/)定义复用的环境与来源语义; +[Python Simple API](https://packaging.python.org/en/latest/specifications/simple-repository-api/)定义包索引解释规则。 +Shell 正则无法实现这些契约。 # Unresolved questions From 9ed1ea89994997c6c654e1c1bfdb7b16d824b8e2 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 9 Oct 2026 16:44:39 +0800 Subject: [PATCH 13/24] feat(install): configure and verify personal services explicitly --- .github/workflows/native-service.yml | 34 +++- README.md | 14 +- README_CN.md | 13 +- README_JP.md | 10 +- docs/en/docs/get-started/install-and-run.md | 69 ++++++- docs/en/docs/get-started/quickstart.md | 33 +++- docs/en/docs/operate/deploy-server.md | 9 +- docs/en/rfcs/1892-script-installation.md | 60 +++++- docs/zh/docs/get-started/install-and-run.md | 61 +++++- docs/zh/docs/get-started/quickstart.md | 30 ++- docs/zh/docs/operate/deploy-server.md | 8 +- docs/zh/rfcs/1892-script-installation.md | 53 +++++- src/powercontext/cli/config.py | 18 +- src/powercontext/cli/config_startup.py | 39 ++++ src/powercontext/cli/config_wizard.py | 88 ++++++--- src/powercontext/cli/system.py | 50 ++++- src/powercontext/service/controller.py | 8 + tests/fixtures/installation/README.md | 10 +- tests/fixtures/installation/services.json | 200 ++++++++++++++++++++ tests/native/test_service_installation.py | 145 ++++++++++++++ tests/test_config_cli.py | 16 +- tests/test_config_wizard.py | 21 +- tests/test_config_wizard_next_steps.py | 27 +++ tests/test_doctor_environment.py | 118 ++++++++++++ tests/test_installation_contract.py | 84 +++++++- tests/test_service.py | 15 ++ website/public/install.ps1 | 63 +++++- website/public/install.sh | 63 +++++- 28 files changed, 1254 insertions(+), 105 deletions(-) create mode 100644 src/powercontext/cli/config_startup.py create mode 100644 tests/fixtures/installation/services.json create mode 100644 tests/native/test_service_installation.py create mode 100644 tests/test_doctor_environment.py diff --git a/.github/workflows/native-service.yml b/.github/workflows/native-service.yml index 99c42b8b38..ef5236ac4d 100644 --- a/.github/workflows/native-service.yml +++ b/.github/workflows/native-service.yml @@ -38,6 +38,8 @@ jobs: echo "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus" >> "$GITHUB_ENV" - name: Install the built distribution in a fresh virtual environment + env: + SETUPTOOLS_SCM_PRETEND_VERSION: "1.2.0" shell: bash run: | uv build --wheel @@ -52,6 +54,16 @@ jobs: --basetemp "$RUNNER_TEMP/powercontext-native-tests" tests/native/test_personal_service_lifecycle.py + - name: Exercise script installation and service reconciliation + env: + POWERCONTEXT_INSTALL_WHEEL: ${{ github.workspace }}/dist/powercontext-1.2.0-py3-none-any.whl + shell: bash + run: >- + .native-venv/bin/python -m pytest -q + --junitxml "$RUNNER_TEMP/installer-service.xml" + --basetemp "$RUNNER_TEMP/installer-service-tests" + tests/native/test_service_installation.py + - name: Report Linux lifecycle failures if: failure() run: >- @@ -70,7 +82,10 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: linux-native-service-logs - path: ${{ runner.temp }}/powercontext-native-tests/**/logs/* + path: | + ${{ runner.temp }}/powercontext-native-tests/**/logs/* + ${{ runner.temp }}/installer-service-tests/**/*.log + ${{ runner.temp }}/installer-service.xml if-no-files-found: ignore macos-launchagent: @@ -89,6 +104,8 @@ jobs: uses: ./.github/actions/setup-python-env - name: Install the built distribution in a fresh virtual environment + env: + SETUPTOOLS_SCM_PRETEND_VERSION: "1.2.0" shell: bash run: | uv build --wheel @@ -105,6 +122,16 @@ jobs: --basetemp "$RUNNER_TEMP/powercontext-native-tests" tests/native/test_personal_service_lifecycle.py + - name: Exercise script installation and service reconciliation + env: + POWERCONTEXT_INSTALL_WHEEL: ${{ github.workspace }}/dist/powercontext-1.2.0-py3-none-any.whl + shell: bash + run: >- + .native-venv/bin/python -m pytest -q + --junitxml "$RUNNER_TEMP/installer-service.xml" + --basetemp "$RUNNER_TEMP/installer-service-tests" + tests/native/test_service_installation.py + - name: Report macOS lifecycle failures if: failure() run: >- @@ -121,7 +148,10 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: macos-native-service-logs - path: ${{ runner.temp }}/powercontext-native-tests/**/logs/* + path: | + ${{ runner.temp }}/powercontext-native-tests/**/logs/* + ${{ runner.temp }}/installer-service-tests/**/*.log + ${{ runner.temp }}/installer-service.xml if-no-files-found: ignore windows-task-scheduler: diff --git a/README.md b/README.md index f22ec43d9e..4ac00b48f3 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,16 @@ Choose **Basic memory** to save and retrieve memories explicitly without additio The wizard writes one `.env` environment file and `.env.next-steps.md`. If seekdb needs installing, it asks once and installs the dependency in the background. Follow the printed -connection details and start Server in this terminal: +connection details. On personal macOS/Linux, install the native user service: ```bash -powercontext server run --env-file .env +powercontext config validate --env-file .env +powercontext service install --env-file .env +powercontext service status +powercontext doctor --env-file .env ``` -Keep Server running. In another terminal, return to `powercontext-config`, load only the client settings, +The service manager keeps Server running after you close the terminal. In `powercontext-config`, load the client settings and check the connection: ```bash @@ -75,8 +78,9 @@ powercontext ready powercontext capabilities ``` -Continue with `.env.next-steps.md` to create and bind the selected Scopes, install the matching plugins, and launch -a new Agent session. The [complete walkthrough](https://powercontext.oceanbase.io/en/docs/get-started/quickstart/) +Continue with `.env.next-steps.md` to create and bind Scopes, then rerun `powercontext service install --env-file .env` +after saving their IDs. Use the same file after upgrades or any configuration edit. Install matching plugins and launch +a new Agent session. For development, temporary use, or an unavailable native user manager, use `powercontext server run`. The [complete walkthrough](https://powercontext.oceanbase.io/en/docs/get-started/quickstart/) covers Codex and Claude Code, Dashboard login, SSH forwarding, HTTPS prerequisites, and observable acceptance checks. For example, the matching Codex installation is: diff --git a/README_CN.md b/README_CN.md index 5bfe7056d8..ca5068a561 100644 --- a/README_CN.md +++ b/README_CN.md @@ -56,13 +56,16 @@ Python 依赖下载缓慢或失败时,可按[镜像重试步骤](https://power Agent 官方订阅不会自动给 PowerContext Server 提供这些凭据。选择**基础记忆**则可显式保存与召回,无需额外模型 API。 向导生成一个 `.env` 环境文件和 `.env.next-steps.md`。 -选择 seekdb 且缺少依赖时,会在确认后后台增量安装。按照最后打印的连接信息,在当前终端启动 Server: +选择 seekdb 且缺少依赖时,会在确认后后台增量安装。个人 macOS/Linux 使用原生用户服务管理器运行 Server: ```bash -powercontext server run --env-file .env +powercontext config validate --env-file .env +powercontext service install --env-file .env +powercontext service status +powercontext doctor --env-file .env ``` -保持 Server 运行,在另一个终端回到 `powercontext-config` 目录,仅加载客户端配置并检查服务: +服务管理器会在关闭终端后继续运行 Server。在 `powercontext-config` 目录加载客户端配置并检查服务: ```bash set -a @@ -72,7 +75,9 @@ powercontext ready powercontext capabilities ``` -接着按 `.env.next-steps.md` 创建并绑定 Scope、安装相同版本的插件,再打开新 Agent 会话。 +接着按 `.env.next-steps.md` 创建并绑定 Scope;写入返回的 ID 后,重新执行 `powercontext service install --env-file .env`。 +升级或修改配置后也使用同一文件重新注册。安装相同版本的插件,再打开新 Agent 会话。开发、临时使用或缺少原生 +用户服务管理器时,可以使用 `powercontext server run`。 [完整安装流程](https://powercontext.oceanbase.io/zh/docs/get-started/quickstart/)包含 Codex、Claude Code、Dashboard 登录、 SSH 隧道、HTTPS 前提及逐项验收。例如,匹配本版本的 Codex 安装命令是: diff --git a/README_JP.md b/README_JP.md index 7ed0c55a09..1b6463613a 100644 --- a/README_JP.md +++ b/README_JP.md @@ -50,13 +50,17 @@ powercontext config init --language en --output .env Generation と Embedding の API 接続を設定してください。Agent のサブスクリプションとは別の認証情報が必要です。 Basic memory は追加のモデル API なしで明示的な保存・検索を利用できます。 -生成された設定で Server を起動します: +個人用 macOS/Linux では、生成された設定でネイティブユーザーサービスを登録します: ```bash -powercontext server run --env-file .env +powercontext config validate --env-file .env +powercontext service install --env-file .env +powercontext service status +powercontext doctor --env-file .env ``` -Server を起動したまま、別のターミナルで同じ設定ディレクトリに移動し、`.env.next-steps.md` に従って +サービスはターミナルを閉じても動作します。更新や設定ファイルの変更後は、同じ `--env-file` で `service install` を再実行してください。 +開発、一時的な利用、非対応環境では `powercontext server run` を使用します。`.env.next-steps.md` に従って クライアント環境の読み込み、Scope の作成・紐付け、同じバージョンの Agent プラグインのインストールを行います。例: ```bash diff --git a/docs/en/docs/get-started/install-and-run.md b/docs/en/docs/get-started/install-and-run.md index 57e8d68610..6b1c885276 100644 --- a/docs/en/docs/get-started/install-and-run.md +++ b/docs/en/docs/get-started/install-and-run.md @@ -52,6 +52,51 @@ For an existing remote Server, add `--profile client` to install only the CLI an configuration or data. For a local installation, continue with `powercontext config init` and [Quick Start](quickstart.md). Client-only installations use endpoint settings in the [remote connection guide](../operate/connect-remote-server.md). +## Configure and install a personal service + +For personal macOS/Linux use, the recommended lifecycle is a native current-user service. The installer can +explicitly compose configuration and service registration: + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- \ + --configure --service --env-file "$HOME/.config/powercontext/powercontext.env" +``` + +`--configure` opens the existing wizard through the controlling terminal and requires `--env-file`. Save the file +to continue; cancellation stops service and host setup. Choose loopback binding for a personal service. Linux needs +an available `systemd --user` manager. In a non-interactive environment, provide an existing protected file instead: + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- \ + --service --env-file /path/to/powercontext.env +``` + +The installer validates that file, runs `service install`, checks `service status`, and runs `doctor --env-file` +against the Server settings in the same file. An explicit `--host` also receives this file and the installed release +ref. Client-only installations reject `--configure` and `--service` before installation. `--env-file` alone can supply +connection settings to an explicitly selected host without installing a local service. + +Windows supports the same explicit options experimentally; `--service` opts into login auto-start. Run `--configure` +in an interactive console. Protect an existing file using the ACL steps in [Deploy the Server](../operate/deploy-server.md). + +These options need an installed release providing `config init --require-write` and `doctor --env-file`. If the selected +release lacks them, the installer reports the failed stage and retains the Runtime; use a compatible release or the +separate configuration/service commands. Runtime installation, saved configuration, service registration, and Server +readiness are separate outcomes. `degraded` diagnostics return nonzero. A failed post-install stage does not roll back +packages, delete configuration, or remove an already committed service registration. + +After any file edit, including writing returned Scope IDs, reconcile using the original file: + +```bash +powercontext service install --env-file /path/to/powercontext.env +powercontext service status +powercontext doctor --env-file /path/to/powercontext.env +``` + +`doctor --env-file` gives the file authority over shell defaults. A Server file selects its listener; a client-only +file selects its Client URL. `--server-url` can explicitly override the diagnostic target. Generated next-step instructions +cover Scope creation and Agent acceptance; a running service does not establish an Agent workflow. + ## Choose a version The default `--version latest` installs or upgrades to the newest stable release available from the selected index @@ -146,10 +191,17 @@ Follow the [guide for each integration](../integrations/index.md) for Agent inst ## Run the local Server +On personal macOS/Linux, install the native current-user service: + ```bash -powercontext server run +powercontext service install +powercontext service status +powercontext doctor ``` +Use `powercontext server run` for development, debugging, temporary use, and platforms without a supported native manager. +Stop an existing foreground instance before installing the service. Service installation remains an explicit operation. + Without environment variables or an environment file, the Server: - binds to `127.0.0.1:8000`; @@ -158,7 +210,8 @@ Without environment variables or an environment file, the Server: - creates a persistent SQLite database in the operating system's user data directory; - supports explicit Memory operations without an inference provider. -`Ctrl-C` performs a clean shutdown. Restarting the command reopens the same database. +The service manager owns startup and restart. Reinstall its definition after relevant upgrades or configuration changes. +A foreground `server run` stops cleanly on `Ctrl-C`. Both entry points reopen the same configured database. The Dashboard is an optional content viewer for personal use and demonstrations. It is disabled by default and needs no separate frontend installation or model configuration. To enable it locally without a token, save these settings @@ -177,7 +230,9 @@ also offers this choice when enabling Dashboard locally. ```bash chmod 600 /path/to/powercontext.env powercontext config validate --env-file /path/to/powercontext.env -powercontext server run --env-file /path/to/powercontext.env +powercontext service install --env-file /path/to/powercontext.env +powercontext service status +powercontext doctor --env-file /path/to/powercontext.env ``` Open `http://127.0.0.1:8000/dashboard/home`, using the actual port if you change it. With authentication disabled, the @@ -265,6 +320,9 @@ The Dashboard must be explicitly enabled; static Bearer authentication is option [Deploy the Server](../operate/deploy-server.md). Remote plaintext HTTP connections require explicit client consent; see [Connect to a remote Server](../operate/connect-remote-server.md). +For a registered personal service, use `powercontext service uninstall` to stop it while preserving data. After +the package update, repeat `service install --env-file` with the original path. + To upgrade to the latest stable version, rerun the installer. To keep an exact release, add `--version`: ```bash @@ -277,8 +335,9 @@ To replace the installed tool with another Git ref: uv tool install --force "powercontext[cli,server] @ git+https://github.com/oceanbase/powercontext.git@" ``` -Update each installed host using its [integration guide](../integrations/index.md) and the same ref. Restart the Server and open a new host session -after updating. Existing SQLite data remains in the user data directory unless `POWERCONTEXT_HOME` or the database URL +Update each installed host using its [integration guide](../integrations/index.md) and the same ref. After updating, +rerun `powercontext service install --env-file /path/to/powercontext.env` with the original file, check `service status` +and `doctor --env-file`, then open a new host session. Existing SQLite data remains in the user data directory unless `POWERCONTEXT_HOME` or the database URL changes. ## Install a Python role diff --git a/docs/en/docs/get-started/quickstart.md b/docs/en/docs/get-started/quickstart.md index 2569bdfa2f..8a7c6c37fb 100644 --- a/docs/en/docs/get-started/quickstart.md +++ b/docs/en/docs/get-started/quickstart.md @@ -52,23 +52,30 @@ These files can contain credentials; do not commit them. If seekdb is still installing, the wizard waits with an activity indicator. Complete any reported dependency recovery before starting the Server. Saving files or installing dependencies does not start the Server. +To combine installation, the wizard, and explicit service registration in a terminal, use the +[guided installer options](install-and-run.md#configure-and-install-a-personal-service). + ## 2. Start the Server In this terminal, run: ```bash powercontext config validate --env-file .env -powercontext server run --env-file .env +powercontext service install --env-file .env +powercontext service status +powercontext doctor --env-file .env ``` -Keep the terminal running. Open the Dashboard URL printed by the wizard, using the port saved as -`POWERCONTEXT_SERVER_HTTP_PORT` in `.env`. With authentication disabled, the page opens directly. Otherwise, sign in -with the **Server token**, not a model API key. -An empty Dashboard is expected before you capture data. For operation after closing the terminal, stop the foreground -Server and install a [persistent personal service](../operate/deploy-server.md#run-a-persistent-personal-server) -with `powercontext service install --env-file .env` to reuse the same configuration. +The [native user-service manager](../operate/deploy-server.md#run-a-persistent-personal-server) owns the Server +lifecycle. You can close the terminal. Linux requires a working `systemd --user` manager; macOS uses a LaunchAgent. +For development, debugging, temporary use, or an unavailable manager, run `powercontext server run --env-file .env` +in the foreground. Stop a registered service with `powercontext service uninstall` before starting a foreground instance. + +Open the Dashboard URL printed by the wizard, using `POWERCONTEXT_SERVER_HTTP_PORT` from `.env`. +With authentication disabled, the page opens directly. Otherwise, sign in with the **Server token**, not a model API key. +An empty Dashboard is expected before you capture data. -Open another terminal, load the client settings, and check the running service: +In the same or another terminal, load the client settings and check the running service: ```bash cd ~/powercontext-demo @@ -95,7 +102,15 @@ The planned `codex-xxxxxxxx` value is a title, not an ID. For Claude Code, put t in `POWERCONTEXT_CLAUDE_SCOPE_ID`. Agents can have separate Scopes or explicitly share an existing one. Changing directories does not create isolation. Use the same Scope in the Dashboard and Agent during this check. -Reload the client settings and install the matching plugin: +After saving Scope IDs, reconcile the service so its registered environment-file identity matches the edited file: + +```bash +powercontext service install --env-file .env +powercontext service status +powercontext doctor --env-file .env +``` + +Repeat this after any environment-file edit and after upgrades. Then reload the client settings and install the matching plugin: ```bash set -a diff --git a/docs/en/docs/operate/deploy-server.md b/docs/en/docs/operate/deploy-server.md index 311f30f895..429345302a 100644 --- a/docs/en/docs/operate/deploy-server.md +++ b/docs/en/docs/operate/deploy-server.md @@ -17,7 +17,7 @@ container platform or an administrator-owned service manager. ## Run a persistent personal Server -Install and start the optional current-user service: +For supported personal macOS/Linux use, install the recommended current-user service. Registration is explicit: ```bash powercontext service install @@ -41,6 +41,8 @@ For an explicit Server configuration, protect the environment file before instal chmod 600 /path/to/powercontext.env powercontext config validate --env-file /path/to/powercontext.env powercontext service install --env-file /path/to/powercontext.env +powercontext service status +powercontext doctor --env-file /path/to/powercontext.env ``` The successful installation summary prints the environment file actually used. If Bearer authentication is enabled, @@ -59,7 +61,10 @@ owner first. The native definition stores only the absolute file path and non-content file identity metadata. On Windows this includes the current user's owner SID, which is revalidated whenever the launcher starts. It does not copy credentials or the caller's shell environment. Re-run `service install` after upgrading PowerContext or changing the -environment file. Remove the registration without deleting Server data or logs with: +environment file, including edits to Agent Scope IDs in a shared file. Always repeat `--env-file` with the original path; +omitting it does not reuse a registered file. `service status` observes manager state and liveness; `doctor --env-file` +also checks readiness. For upgrades requiring a stopped Server, first use `service uninstall`, which preserves data. +Remove the registration without deleting Server data or logs with: ```bash powercontext service uninstall diff --git a/docs/en/rfcs/1892-script-installation.md b/docs/en/rfcs/1892-script-installation.md index bc4d07d992..2d620f4473 100644 --- a/docs/en/rfcs/1892-script-installation.md +++ b/docs/en/rfcs/1892-script-installation.md @@ -16,7 +16,8 @@ The scripts provision missing uv and Python, install the selected Runtime profil existing Agent integration adapters with a tag matching the installed Runtime. Installation, configuration, diagnostics, and service operation have separate responsibilities. Installing software -does not configure inference providers, start a Server, or register a persistent service. The scripts and their +does not configure inference providers, start a Server, or register a persistent service by default. Explicit +`--configure` and `--service` compose those existing CLI operations using one selected `--env-file`. The scripts and their installation guide are the first recommended path in the READMEs and Quick Start. # Motivation @@ -75,8 +76,50 @@ The installed CLI supplies the exact version for `powercontext-vVERSION`. Host s and never defaults to `master`. If a host installation fails, the Runtime remains installed and the script returns nonzero with instructions to retry setup at that tag. Each adapter supplies its existing host-specific result. -For a local installation, continue with `powercontext config init`, then `powercontext server run --env-file .env`. -Client-only installations follow the remote connection guide to set the Server endpoint and authentication. Persistent services remain an explicit operation. +For personal macOS/Linux, configure a protected file, then run `powercontext service install --env-file .env`, +`powercontext service status`, and `powercontext doctor --env-file .env`. The native user manager owns the Server lifecycle. +Development, debugging, temporary use, and unavailable managers keep the foreground `server run` path. +Client-only installations follow the remote connection guide. Service registration remains explicit. + +## Explicit configuration and personal service setup + +The installer supports three composable options: + +| Option | Contract | +| --- | --- | +| `--configure` | Run `config init --require-write --output PATH` through a controlling terminal | +| `--service` | Validate the selected file, install the native user service, inspect status, and diagnose readiness | +| `--env-file PATH` | Select the same explicit configuration for configuration, service, diagnostics, and host setup | + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- \ + --configure --service --env-file "$HOME/.config/powercontext/powercontext.env" +``` + +`--configure` and `--service` require the local profile and an explicit file. Unattended setup requires an existing +file and omits `--configure`. Invalid combinations, a missing existing file, and unavailable interactive input fail +before Runtime installation. The Bash wizard receives `/dev/tty` independently of the downloaded script's stdin; +PowerShell requires an interactive console. Ordinary installation remains unattended. + +The configuration CLI exposes `--require-write` without changing its normal cancellation behavior. With this option, +cancelled saving returns 130 and prevents service/host changes, including when a previous valid file remains present. +Both wrappers report saved configuration, validation, service verification, and any later failure separately. + +Service setup delegates protected-file loading, loopback restrictions, manager support, registration ownership, +reconciliation, and startup to the existing service layer. Windows remains experimental; the wrapper's explicit +`--service` also opts into login startup. A registered service is not sufficient for full completion: `service status` +must pass and `doctor --env-file` must report healthy readiness. Degraded readiness returns nonzero. + +`doctor --env-file` uses the strict Python environment loader and restores process settings afterwards. Server files +select the configured listener, independently of Client URLs used for SSH forwarding or stale caller defaults. +Client-only files select their Client URL; an explicit diagnostic `--server-url` takes priority. Shell does not source +the file or interpret its contents. Host setup receives `setup --env-file PATH select` at the installed release tag. + +The installed release must provide the added CLI options; an older release reports the failed stage and leaves the +Runtime installed. Distribution publication must coordinate the package containing these options with the website. +After any file edit, including Scope IDs, or an upgrade, users rerun `service install --env-file` with the original file. +Generated configuration output and next-step instructions recommend this flow, including reconciliation after Scope +creation. A post-install failure retains packages, saved files, and committed registrations for explicit recovery. ## Choose download sources @@ -155,8 +198,9 @@ downloads are cleaned. `UV_INSTALL_DIR`, `UV_TOOL_DIR`, and `UV_TOOL_BIN_DIR` re ## Persistence and compatibility -The installer does not read or rewrite `.env`, credentials, data directories, or database schemas. It does not stop -or restart a running service. Users follow the existing upgrade and migration instructions when starting an updated +Ordinary package installation does not read or rewrite `.env`, credentials, data directories, or database schemas, +and does not stop or restart a service. Explicit `--configure` asks the CLI to save only the selected file; explicit +`--service` asks the native layer to reconcile its owned registration and restart when its existing contract requires it. Users follow the existing upgrade and migration instructions when starting an updated Server. An exact-version retry may reuse cached packages; `latest` intentionally allows upgrades. `UV_OFFLINE=1` permits cached reinstallation only when uv, a compatible Python, and all dependencies are present. @@ -175,7 +219,9 @@ support still comes from each integration's capability contract. `tests/test_installation_contract.py` executes those cases with real uv and small offline fixture wheels: host opt-in, profiles, exact-version verification, unavailable commands, and partial host failure. Cases observe installed capabilities, selected hosts, release refs and exit results. They do not fix internal function boundaries or call order. -The same catalog runs on each native operating system; fixture host execution is not real Agent acceptance. +The same catalog runs on each native operating system; fixture host/service execution is not native service or Agent +acceptance. POSIX controlling-terminal cases cover a piped configuration request and cancellation. Native service CI +adds installed-script acceptance for persistent memory, environment-file reconciliation, and a stopped-service upgrade. `tests/native/test_installation.py` runs the actual shell installer, uv, installed PowerContext CLI, and HTTP Server. A wheel built from the tested commit uses release-shaped metadata and a direct local file constraint. That constraint @@ -254,3 +300,5 @@ The platform matrix must remain green before promotion; Windows product support The scripts can bootstrap a distribution-owned installer engine if installation plans later need immutable component manifests, independent integration versions, or durable per-component repair records. An offline bundle can include all required artifacts explicitly. Neither extension should make Server startup or service registration implicit. + +The personal-service onboarding work tracks [Issue #1900](https://github.com/oceanbase/powercontext/issues/1900). diff --git a/docs/zh/docs/get-started/install-and-run.md b/docs/zh/docs/get-started/install-and-run.md index e888f8afc2..852bb14c2e 100644 --- a/docs/zh/docs/get-started/install-and-run.md +++ b/docs/zh/docs/get-started/install-and-run.md @@ -48,6 +48,45 @@ PATH。`UV_INSTALL_DIR` 指定缺少 uv 时的安装目录,`UV_TOOL_BIN_DIR` 两种模式都不会启动 Server、注册服务或覆盖配置与数据。本地安装使用 `powercontext config init`,继续阅读 [快速开始](quickstart.md);Client-only 安装按[远程连接指南](../operate/connect-remote-server.md)设置地址和认证。 +## 配置并安装个人服务 + +个人 macOS/Linux 推荐使用原生当前用户服务。安装器可以显式串联配置向导和服务注册: + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- \ + --configure --service --env-file "$HOME/.config/powercontext/powercontext.env" +``` + +`--configure` 通过控制终端打开现有向导,需要显式 `--env-file`。保存后才能继续;取消会中止后续服务和宿主安装。 +个人服务选择 loopback 绑定;Linux 需要可用的 `systemd --user`。无人值守环境使用已有受保护文件: + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- \ + --service --env-file /path/to/powercontext.env +``` + +安装器验证配置,执行 `service install`、`service status`,再通过 `doctor --env-file` 检查同一文件中的 Server。 +显式 `--host` 也会使用该文件及实际安装版本对应的 ref。Client-only 模式在安装前拒绝 `--configure` 和 `--service`; +单独使用 `--env-file` 可以为显式宿主安装提供连接配置,而不安装本地服务。 + +Windows 的相同选项仍为试验性支持;`--service` 显式启用登录自启动。`--configure` 需要交互控制台。 +已有文件的 ACL 保护方式见[部署 Server](../operate/deploy-server.md)。 + +这些选项要求所选发行版提供 `config init --require-write` 和 `doctor --env-file`。缺少时,安装器报告失败阶段并保留 +Runtime;使用兼容发行版,或分开执行配置与服务命令。Runtime 安装、配置保存、服务注册和 Server 就绪分别报告。 +`degraded` 诊断返回非零状态;后续失败不会回滚包文件、删除配置或移除已提交的服务注册。 + +任何文件修改,包括写入返回的 Scope ID 后,都要使用原文件重新注册: + +```bash +powercontext service install --env-file /path/to/powercontext.env +powercontext service status +powercontext doctor --env-file /path/to/powercontext.env +``` + +`doctor --env-file` 以文件为准,覆盖调用者 shell 的默认配置。Server 文件选择其监听地址,Client-only 文件选择 Client URL; +显式 `--server-url` 可以覆盖诊断目标。生成的后续步骤仍负责引导 Scope 创建和 Agent 验收;服务运行不代表 Agent 工作流已完成。 + ## 选择版本 默认 `--version latest` 安装或升级到所选包源中与当前 Python 兼容的最新稳定版,排除预发布版本。 @@ -132,10 +171,17 @@ Agent 的安装、连接参数和验证步骤见[各自的集成文档](../integ ## 运行本地 Server +个人 macOS/Linux 推荐安装原生当前用户服务: + ```bash -powercontext server run +powercontext service install +powercontext service status +powercontext doctor ``` +开发、调试、临时使用或没有可用原生 manager 的平台使用 `powercontext server run`。已有前台实例时,先停止再安装服务。 +服务安装始终是显式操作。 + 没有环境变量或环境文件时,Server 会: - 监听 `127.0.0.1:8000`; @@ -144,7 +190,8 @@ powercontext server run - 在操作系统的用户数据目录中创建持久化 SQLite 数据库; - 无需推理服务即可支持显式 Memory 操作。 -按 `Ctrl-C` 可正常关闭。再次运行该命令会打开同一个数据库。 +原生服务管理器负责启动和重启;升级或修改配置后重新注册。前台 `server run` 可以通过 `Ctrl-C` 正常关闭。 +两种入口都会打开同一个已配置数据库。 Dashboard 是个人使用和演示的可选内容查看器,默认关闭。它不需要单独安装前端或配置模型。 本地免 token 启用时,在环境文件中设置以下值: @@ -161,7 +208,9 @@ POWERCONTEXT_SERVER_ACCESS_MODE=disabled ```bash chmod 600 /path/to/powercontext.env powercontext config validate --env-file /path/to/powercontext.env -powercontext server run --env-file /path/to/powercontext.env +powercontext service install --env-file /path/to/powercontext.env +powercontext service status +powercontext doctor --env-file /path/to/powercontext.env ``` 打开 `http://127.0.0.1:8000/dashboard/home`,更改端口后使用实际端口。未启用认证时可直接进入页面; @@ -239,6 +288,9 @@ Server、客户端和 Agent 集成需一起升级。Dashboard 需要显式启用 见[部署 Server](../operate/deploy-server.md);远程明文 HTTP 连接需要客户端明确同意, 见[连接远程 Server](../operate/connect-remote-server.md)。 +个人服务可以通过 `powercontext service uninstall` 停止,数据会保留。包升级后,使用原配置路径重新执行 +`service install --env-file`。 + 重新运行安装器可升级到最新稳定版;需要保持指定版本时,加上 `--version`: ```bash @@ -251,7 +303,8 @@ curl -fsSL https://powercontext.oceanbase.io/install.sh | bash uv tool install --force "powercontext[cli,server] @ git+https://github.com/oceanbase/powercontext.git@" ``` -按[各自的集成文档](../integrations/index.md)更新已安装宿主,并使用同一个 ref。更新后重启 Server,再开启新的宿主会话。只要没有修改 +按[各自的集成文档](../integrations/index.md)更新已安装宿主,并使用同一个 ref。更新后使用原来的文件重新执行 `powercontext service install --env-file /path/to/powercontext.env`, +检查 `service status` 和 `doctor --env-file`,再开启新的宿主会话。只要没有修改 `POWERCONTEXT_HOME` 或数据库 URL,现有 SQLite 数据会继续保留。 ## 为 Python 项目安装角色 diff --git a/docs/zh/docs/get-started/quickstart.md b/docs/zh/docs/get-started/quickstart.md index cf98780ce8..3516476271 100644 --- a/docs/zh/docs/get-started/quickstart.md +++ b/docs/zh/docs/get-started/quickstart.md @@ -50,21 +50,27 @@ powercontext config init --language zh --output .env 若 seekdb 仍在安装,向导会在当前界面显示活动进度并等待;安装失败时先按提示完成依赖安装。 保存配置或装好依赖,都不代表 Server 已经启动。 +需要在终端中一次完成安装、向导和显式服务注册时,可使用[安装器的引导选项](install-and-run.md#配置并安装个人服务)。 + ## 2. 启动 Server 在当前终端执行: ```bash powercontext config validate --env-file .env -powercontext server run --env-file .env +powercontext service install --env-file .env +powercontext service status +powercontext doctor --env-file .env ``` -保持终端运行。在浏览器打开向导输出的 Dashboard 地址,端口以 `.env` 中保存的 -`POWERCONTEXT_SERVER_HTTP_PORT` 为准。未启用认证时可直接进入页面;启用后使用 **Server Token** 登录,不是模型 API key。 -首次没有数据是正常现象。需要关闭终端后继续运行时,先停止前台 Server,再通过 -`powercontext service install --env-file .env` 安装[个人后台服务](../operate/deploy-server.md#运行持久个人-server),复用同一份配置。 +[原生用户服务管理器](../operate/deploy-server.md#运行持久个人-server)负责 Server 生命周期,可以关闭终端。 +Linux 需要可用的 `systemd --user`,macOS 使用 LaunchAgent。开发、调试、临时使用或缺少可用 manager 时,可以使用 +`powercontext server run --env-file .env` 前台运行。已有个人服务时,先用 `powercontext service uninstall` 停止并移除注册,避免重复实例。 + +在浏览器打开向导输出的 Dashboard 地址,端口以 `.env` 中保存的 `POWERCONTEXT_SERVER_HTTP_PORT` 为准。 +未启用认证时可直接进入页面;启用后使用 **Server Token** 登录,不是模型 API key。首次没有数据是正常现象。 -另开终端,加载客户端连接配置并检查服务: +在当前或另一终端中加载客户端连接配置并检查服务: ```bash cd ~/powercontext-demo @@ -84,14 +90,22 @@ powercontext capabilities 将它写入 `.env`: ```dotenv -POWERCONTEXT_CODEX_SCOPE_ID=替换为返回的scope_id +POWERCONTEXT_CODEX_SCOPE_ID=scope_id_from_server ``` `codex-xxxxxxxx` 是向导规划的标题,不是 ID。配置 Claude Code 时,将其创建请求返回的 ID 写入 `POWERCONTEXT_CLAUDE_SCOPE_ID`。不同 Agent 可以各自隔离,也可以显式绑定同一已有 Scope。 切换目录本身不会创建隔离。验收期间,Dashboard 和 Agent 必须使用相同的 Scope。 -重新加载客户端文件并安装匹配的插件: +保存 Scope ID 后,重新注册服务,使注册时记录的环境文件身份与修改后的文件一致: + +```bash +powercontext service install --env-file .env +powercontext service status +powercontext doctor --env-file .env +``` + +任何环境文件修改或升级后都要使用同一文件重新注册。然后加载客户端文件并安装匹配的插件: ```bash set -a diff --git a/docs/zh/docs/operate/deploy-server.md b/docs/zh/docs/operate/deploy-server.md index a4aa911a11..48c71e0e80 100644 --- a/docs/zh/docs/operate/deploy-server.md +++ b/docs/zh/docs/operate/deploy-server.md @@ -14,7 +14,7 @@ Windows 支持为 `experimental`。 ## 运行持久个人 Server -安装并启动可选的当前用户服务: +个人 macOS/Linux 推荐安装当前用户服务;注册保持显式操作: ```bash powercontext service install @@ -38,6 +38,8 @@ loopback Server。 chmod 600 /path/to/powercontext.env powercontext config validate --env-file /path/to/powercontext.env powercontext service install --env-file /path/to/powercontext.env +powercontext service status +powercontext doctor --env-file /path/to/powercontext.env ``` 安装成功后的摘要会显示实际使用的环境文件路径。若启用了 Bearer 鉴权,请从该文件中的 @@ -53,7 +55,9 @@ icacls $env:USERPROFILE\powercontext.env /inheritance:r /grant:r "${env:USERNAME 原生定义只记录环境文件的绝对路径和不含内容的文件 identity metadata;在 Windows 上还记录当前用户的 owner SID, launcher 每次启动都会重新校验它。不复制 credential 或调用者的 shell environment。 -升级 PowerContext 或修改环境文件后应重新执行 `service install`。以下命令会删除注册,但保留 Server 数据和日志: +升级 PowerContext 或修改环境文件(包括共享文件中的 Agent Scope ID)后,使用原路径重新执行 `service install --env-file`。 +省略 `--env-file` 不会复用已注册文件。`service status` 检查 manager 和存活状态,`doctor --env-file` 还检查就绪状态。 +需要停止 Server 的升级,先使用 `service uninstall`,该命令保留数据。以下命令会删除注册,但保留 Server 数据和日志: ```bash powercontext service uninstall diff --git a/docs/zh/rfcs/1892-script-installation.md b/docs/zh/rfcs/1892-script-installation.md index 1d4e22c71f..10e576b94c 100644 --- a/docs/zh/rfcs/1892-script-installation.md +++ b/docs/zh/rfcs/1892-script-installation.md @@ -14,7 +14,8 @@ description: 通过 Bash 和 PowerShell 安装最新或指定版本,独立管 `--version` 可以指定准确版本。脚本补齐缺少的 uv 和 Python,安装所选 Runtime profile,并按需调用现有 Agent 集成 适配器,使用与实际安装的 Runtime 对应的 tag。 -安装、配置、诊断和服务运行各自承担独立职责。安装软件不会配置推理服务、启动 Server 或注册持久服务。 +安装、配置、诊断和服务运行各自承担独立职责。默认安装软件不会配置推理服务、启动 Server 或注册持久服务。显式 `--configure` 和 `--service` 可以通过 +现有 CLI 串联这些操作,并使用同一份 `--env-file`。 README 和快速开始将脚本及其安装指南作为首选路径。 # Motivation @@ -66,8 +67,44 @@ curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1 已安装的 CLI 提供准确版本,形成 `powercontext-vVERSION`。宿主安装不再次解析 `latest`,也不默认使用 `master`。 宿主安装失败时,保留已安装的 Runtime,脚本返回非零状态,并提示按该 tag 重试 setup;各适配器提供其已有的宿主结果。 -本地模式随后运行 `powercontext config init` 和 `powercontext server run --env-file .env`;Client-only 模式 -则按远程连接指南设置 Server 地址和认证。持久服务仍由用户显式注册。 +个人 macOS/Linux 配置受保护文件后,运行 `powercontext service install --env-file .env`、`powercontext service status` +和 `powercontext doctor --env-file .env`,由原生用户服务管理器负责生命周期。开发、调试、临时使用或缺少 manager 时, +保留前台 `server run`。Client-only 模式按远程连接指南设置地址和认证。持久服务仍由用户显式注册。 + +## 显式配置与个人服务安装 + +安装器提供三个可以组合的选项: + +| 选项 | 契约 | +| --- | --- | +| `--configure` | 通过控制终端运行 `config init --require-write --output PATH` | +| `--service` | 验证所选文件,安装原生用户服务,检查状态与就绪 | +| `--env-file PATH` | 为配置、服务、诊断和宿主安装选择同一份显式文件 | + +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- \ + --configure --service --env-file "$HOME/.config/powercontext/powercontext.env" +``` + +`--configure` 和 `--service` 要求 local profile 及显式文件。无人值守安装省略 `--configure`,提供已有文件。 +无效组合、缺少已有文件或交互终端时,在 Runtime 安装前失败。Bash 将 `/dev/tty` 单独交给向导,避免与下载脚本的 +stdin 混用;PowerShell 要求交互控制台。普通安装继续支持无人值守。 + +配置 CLI 增加 `--require-write`,保留独立使用时的取消行为。指定该选项时,取消保存返回 130,即使已有有效旧文件, +也不会继续修改服务或宿主。两个入口分别报告配置保存、验证、服务验证以及后续失败。 + +服务安装复用现有服务层的文件保护、loopback、manager 支持、注册归属、reconcile 和启动规则。 +Windows 仍为试验性;入口的显式 `--service` 同时同意登录自启动。完成注册后,还必须通过 `service status` 和 +`doctor --env-file` 的就绪诊断;`degraded` 返回非零状态。 + +`doctor --env-file` 复用严格的 Python 环境加载器,结束后恢复进程设置。Server 文件选择其监听地址,不受 SSH 转发 +所用 Client URL 或调用者旧配置影响;Client-only 文件选择 Client URL,显式诊断 `--server-url` 优先。 +Shell 不执行文件内容。宿主安装使用 `setup --env-file PATH select`,并绑定实际安装版本对应的 tag。 + +所选发行版必须提供新增 CLI 选项;旧版缺少时,报告失败阶段并保留 Runtime。发布需要协调包含这些选项的包与网站。 +任何文件修改(包括 Scope ID)或升级后,使用原文件重新执行 `service install --env-file`。 +配置输出和后续步骤统一推荐这一路径,并在 Scope 创建后提示重新注册。后续失败保留包、已保存文件和已提交的服务注册, +供用户显式恢复。 ## 选择下载来源 @@ -135,7 +172,8 @@ Shell 环境变化局限于安装器进程;PowerShell 在 `finally` 恢复临 ## 持久化与兼容性 -安装器不读取或改写 `.env`、凭据、数据目录或数据库 schema,也不停止或重启运行中的服务。 +普通包安装不读取或改写 `.env`、凭据、数据目录或数据库 schema,也不停止或重启服务。显式 `--configure` +由 CLI 保存所选文件;显式 `--service` 由原生层 reconcile 自有注册,并按既有契约在需要时重启。 启动升级后的 Server 时,用户按现有升级和迁移指南操作。准确版本重试可以使用缓存;`latest` 则明确允许升级。 `UV_OFFLINE=1` 只支持已有 uv、兼容 Python 和全部依赖时的缓存重装,同时禁用安装器源探测和 uv 网络访问, @@ -150,7 +188,8 @@ Windows 保持产品的试验性支持状态。原生验收在 Linux、macOS 和 `tests/fixtures/installation/*.json` 定义两个 shell 适配器共享的公开输入与预期结果。 `tests/test_installation_contract.py` 使用真实 uv 和小型离线 fixture wheel 执行宿主显式选择、profile、准确版本 校验、命令缺失和宿主部分失败用例。用例观察安装后的能力、所选宿主、发行 ref 和退出结果,不固定内部函数边界或 -调用顺序。同一组用例在各操作系统原生执行;模拟宿主执行不等于真实 Agent 验收。 +调用顺序。同一组用例在各操作系统原生执行;模拟宿主或服务执行不等于真实原生服务或 Agent 验收。POSIX 控制终端用例覆盖管道安装中的配置请求与取消; +原生服务 CI 通过安装后的脚本验证记忆持久化、文件修改后的重新注册和停止服务后的升级。 `tests/native/test_installation.py` 执行真实 shell 安装器、uv、安装后的 PowerContext CLI 和 HTTP Server。 从被测提交构建 wheel,使用发行版形式的元数据和直接本地文件约束。该约束选定被测产物,不构成独立执行的校验和验证。 @@ -219,4 +258,6 @@ Shell 正则无法实现这些契约。 # Future possibilities 当安装计划需要不可变组件清单、独立集成版本或持久组件修复记录时,脚本可以引导发行版维护的独立安装引擎。 -离线包也可以显式携带全部所需产物。这些扩展都不应隐式启动 Server 或注册服务。 +离线包也可以显式携带全部所需产物。这些扩展继续保持 Server 启动与服务注册的显式同意。 + +个人服务引导工作关联 [Issue #1900](https://github.com/oceanbase/powercontext/issues/1900)。 diff --git a/src/powercontext/cli/config.py b/src/powercontext/cli/config.py index bb62c720da..549e10d2ad 100644 --- a/src/powercontext/cli/config.py +++ b/src/powercontext/cli/config.py @@ -288,13 +288,18 @@ def init_command( template: Annotated[ bool, typer.Option("--template", help="Use the basic model-free template instead of the guided setup.") ] = False, + require_write: Annotated[ + bool, typer.Option(help="Exit with status 130 if configuration is cancelled without saving.") + ] = False, ) -> None: """Create a working configuration through a short guided setup.""" if not template: from powercontext.cli.config_wizard import run_wizard - run_wizard(output, language=None if language is None else language.value, advanced=advanced) + written = run_wizard(output, language=None if language is None else language.value, advanced=advanced) + if require_write and not written: + raise typer.Exit(130) return if output.exists() and not force: @@ -307,6 +312,8 @@ def init_command( content = update_environment_document(existing, configuration) if not _confirm_environment_write(output, existing=existing, updated=content): typer.echo("No changes written.") + if require_write: + raise typer.Exit(130) return backup = write_environment(output, content, backup=output.exists()) except (ConfigError, EnvironmentFileError, OSError, UnicodeError, ValidationError) as error: @@ -1025,6 +1032,8 @@ def _removes_inference_configuration(existing: str, updated: str) -> bool: def _print_next_steps(path: Path) -> None: + from powercontext.cli.config_startup import server_startup_commands + quoted = shlex.quote(str(path.resolve())) typer.secho("\nEnvironment file", bold=True, fg=typer.colors.CYAN) typer.echo(f" Path {path.resolve()} (mode 0600)") @@ -1033,7 +1042,12 @@ def _print_next_steps(path: Path) -> None: " If Bearer authentication is enabled, read POWERCONTEXT_SERVER_AUTH_TOKEN from this file;" " the value is never printed." ) - typer.echo(f"\nStart Server:\n powercontext server run --env-file {quoted}") + typer.echo("\nStart Server:") + for command in server_startup_commands( + path.resolve(), + host=parse_environment(path.read_text(encoding="utf-8")).get("POWERCONTEXT_SERVER_HTTP_HOST", "127.0.0.1"), + ): + typer.echo(f" {command}") write_inference_capability_notice(generation_model=None, embedding_model=None) typer.secho("\nSupported Coding Agents (choose one):", bold=True, fg=typer.colors.CYAN) for host, (name, setup, launch) in AGENTS.items(): diff --git a/src/powercontext/cli/config_startup.py b/src/powercontext/cli/config_startup.py new file mode 100644 index 0000000000..f596a8739c --- /dev/null +++ b/src/powercontext/cli/config_startup.py @@ -0,0 +1,39 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Startup guidance shared by configuration output and saved instructions.""" + +import shlex +import sys +from pathlib import Path + +from powercontext.transport import is_loopback_host + + +def personal_service_recommended(host: str) -> bool: + return sys.platform in {"darwin", "linux"} and is_loopback_host(host) + + +def server_startup_commands(path: Path, *, host: str = "127.0.0.1") -> list[str]: + quoted = shlex.quote(str(path)) + commands = [f"powercontext config validate --env-file {quoted}"] + if personal_service_recommended(host): + commands.extend(( + f"powercontext service install --env-file {quoted}", + "powercontext service status", + f"powercontext doctor --env-file {quoted}", + )) + else: + commands.append(f"powercontext server run --env-file {quoted}") + return commands diff --git a/src/powercontext/cli/config_wizard.py b/src/powercontext/cli/config_wizard.py index 3222300cb5..4dbf31cdcc 100644 --- a/src/powercontext/cli/config_wizard.py +++ b/src/powercontext/cli/config_wizard.py @@ -111,7 +111,7 @@ def note(self, en: str, zh: str) -> None: self.ui.say(en, zh) -def run_wizard(output: Path, *, language: str | None = None, advanced: bool = False) -> None: +def run_wizard(output: Path, *, language: str | None = None, advanced: bool = False) -> bool: """Collect answers and write files only after explicit final confirmation.""" from powercontext.cli.config import ConfigError @@ -162,7 +162,7 @@ def run_wizard(output: Path, *, language: str | None = None, advanced: bool = Fa _agents(state) elif mode == "edit": _edit_modules(state) - _finish(state, output, content) + return _finish(state, output, content) except (ConfigError, EnvironmentFileError, OSError, ValueError) as error: if isinstance(error, (WizardInputError, EnvironmentFileError, OSError)): message = str(error) @@ -1103,7 +1103,7 @@ def _validate_files(state: Wizard, values: dict[str, str]) -> None: raise WizardInputError(message) from None -def _finish(state: Wizard, output: Path, original_content: str) -> None: +def _finish(state: Wizard, output: Path, original_content: str) -> bool: ui = state.ui ui.section("8. Review files", "8. 检查配置文件") @@ -1153,10 +1153,11 @@ def _finish(state: Wizard, output: Path, original_content: str) -> None: typer.echo(f" {path}") if not ui.confirm("Save these files?", "保存这些文件?"): ui.say("No changes written.", "未写入任何文件。") - return + return False _save_bundle(state, bundle, snapshots) _finish_seekdb_install(state) _show_connection_details(state, output, values, notes_file) + return True def _save_bundle(state: Wizard, bundle: dict[Path, str], snapshots: dict[Path, str]) -> None: @@ -1215,8 +1216,10 @@ def _show_connection_details(state: Wizard, output: Path, values: dict[str, str] ui = state.ui ui.section("Connection details", "连接与下一步") if not state.client_only: - command = f"powercontext server run --env-file {shlex.quote(str(output))}" - ui.say(f"Start Server: {command}", f"启动 Server:{command}") + from powercontext.cli.config_startup import server_startup_commands + + for command in server_startup_commands(output, host=values.get(f"{SERVER}HTTP_HOST", "127.0.0.1")): + ui.say(f"Start Server: {command}", f"启动 Server:{command}") if state.ssh_tunnel_command: ui.say( f"Run on the client computer: {state.ssh_tunnel_command}", @@ -1257,8 +1260,50 @@ def _connection_base_url(state: Wizard, values: dict[str, str]) -> str: return f"http://{host}:{port}" +def _server_start_steps(state: Wizard, output: Path) -> list[str]: + from powercontext.cli.config_startup import personal_service_recommended, server_startup_commands + + if state.client_only: + return [] + ui = state.ui + personal_service = personal_service_recommended(state.values.get(f"{SERVER}HTTP_HOST", "127.0.0.1")) + lines: list[str] = [] + lines += [ + "```bash", + *server_startup_commands(output, host=state.values.get(f"{SERVER}HTTP_HOST", "127.0.0.1")), + "```", + "", + ] + if personal_service: + lines += [ + ui.text( + "The native user-service manager owns the Server lifecycle. After upgrades or any edit to " + "this environment file, rerun these commands with the same file. Use `powercontext server run` " + "for development or temporary foreground use after stopping the personal service.", + "原生用户服务管理器负责 Server 生命周期。升级或修改这份环境文件后,使用同一文件重新执行上述命令。" + "开发或临时前台运行可先停止个人服务,再使用 `powercontext server run`。", + ), + "", + ] + if state.values.get(f"{SERVER}AUTH_TOKEN"): + lines += [ + ui.text( + f"The Server token is stored as `POWERCONTEXT_SERVER_AUTH_TOKEN` in `{output}`. " + "Do not paste it into logs or issues.", + f"Server Token 保存在 `{output}` 的 `POWERCONTEXT_SERVER_AUTH_TOKEN` 中;请勿将其粘贴到日志或 Issue。", + ), + "", + ] + return lines + + def _next_steps(state: Wizard, output: Path, client_file: Path) -> str: + from powercontext.cli.config_startup import personal_service_recommended, server_startup_commands + ui = state.ui + personal_service = not state.client_only and personal_service_recommended( + state.values.get(f"{SERVER}HTTP_HOST", "127.0.0.1") + ) lines = [ "# " + ui.text("PowerContext next steps", "PowerContext 后续步骤"), "", @@ -1268,24 +1313,7 @@ def _next_steps(state: Wizard, output: Path, client_file: Path) -> str: ), "", ] - if not state.client_only: - lines += [ - "```bash", - f"powercontext config validate --env-file {shlex.quote(str(output))}", - f"powercontext server run --env-file {shlex.quote(str(output))}", - "```", - "", - ] - if state.values.get(f"{SERVER}AUTH_TOKEN"): - lines += [ - ui.text( - f"The Server token is stored as `POWERCONTEXT_SERVER_AUTH_TOKEN` in `{output}`. " - "Do not paste it into logs or issues.", - f"Server Token 保存在 `{output}` 的 `POWERCONTEXT_SERVER_AUTH_TOKEN` 中;" - "请勿将其粘贴到日志或 Issue。", - ), - "", - ] + lines += _server_start_steps(state, output) if state.agents or state.client_only: if state.scenario != "local": lines += [ @@ -1328,6 +1356,18 @@ def _next_steps(state: Wizard, output: Path, client_file: Path) -> str: lines += _alternate_client_check_steps(state) if state.planned_scopes: lines += _scope_creation_steps(state) + if personal_service: + lines += [ + ui.text( + "After saving the returned Scope IDs, reconcile the service's environment-file identity:", + "保存返回的 Scope ID 后,重新注册服务以更新环境文件身份:", + ), + "", + "```bash", + *server_startup_commands(output, host=state.values.get(f"{SERVER}HTTP_HOST", "127.0.0.1")), + "```", + "", + ] lines += [ ui.text( "After saving the returned Scope IDs in the environment file, reload it in each Agent's " diff --git a/src/powercontext/cli/system.py b/src/powercontext/cli/system.py index 559d6db91f..5ecbfc21f9 100644 --- a/src/powercontext/cli/system.py +++ b/src/powercontext/cli/system.py @@ -40,6 +40,7 @@ from pydantic import ValidationError from powercontext.cli.dsh_runtime import DshProfile, resolve_dsh_target +from powercontext.cli.env_file import EnvironmentFileError, environment_context, read_environment_file from powercontext.cli.hosts import setup_host, stdin_is_tty from powercontext.cli.transport import add_transport_diagnostic, is_remote_http from powercontext.client.settings import normalize_server_url @@ -1036,7 +1037,6 @@ def doctor( server_url: Annotated[ str | None, typer.Option( - envvar="POWERCONTEXT_CLIENT_SERVER_URL", help="PowerContext Server base URL.", ), ] = None, @@ -1050,12 +1050,27 @@ def doctor( bool, typer.Option("--json", help="Write the result as JSON."), ] = False, + env_file: Annotated[ + Path | None, + typer.Option(help="Diagnose the Server defined in this environment file, independently of shell defaults."), + ] = None, ) -> None: """Check the installed package and configured Server.""" if context.invoked_subcommand is not None: + if env_file is not None: + message = "Run doctor without a subcommand." + raise typer.BadParameter(message, param_hint="--env-file") return - diagnostics = run_diagnostics(server_url=server_url, allow_insecure_http=allow_insecure_http) + try: + diagnostics = ( + run_diagnostics(server_url=server_url, allow_insecure_http=allow_insecure_http) + if env_file is None + else run_environment_diagnostics(env_file, server_url=server_url, allow_insecure_http=allow_insecure_http) + ) + except (EnvironmentFileError, OSError, ValueError) as error: + typer.echo("Cannot diagnose the environment file; check its path, assignments, and Server settings.", err=True) + raise typer.Exit(2) from error _write_diagnostics(diagnostics, json_output=json_output) if not _diagnostics_ok(diagnostics): raise typer.Exit(code=1) @@ -1447,6 +1462,37 @@ def install_claude_code_plugin( ) +def run_environment_diagnostics( + path: Path, *, server_url: str | None = None, allow_insecure_http: bool | None = None +) -> dict[str, Diagnostic]: + """Observe the same Server configuration used by a personal service.""" + + loaded = read_environment_file(path.expanduser()) + cleared = { + name + for name in os.environ + if name.casefold().startswith(("powercontext_client_", "powercontext_server_")) or name == "POWERCONTEXT_HOME" + } + with environment_context(loaded, override=True, clear=cleared): + if server_url is None and any(name.casefold().startswith("powercontext_server_") for name in loaded): + from powercontext.server.configuration import server_settings_context + + with server_settings_context(environment=loaded) as settings: + host = settings.http.host + if host == "0.0.0.0": # noqa: S104 - diagnose wildcard binds via loopback + host = "127.0.0.1" + elif host in {"::", "[::]"}: + host = "::1" + if ":" in host and not host.startswith("["): + host = f"[{host}]" + server_url = f"http://{host}:{settings.http.port}" + else: + server_url = server_url or next( + (value for name, value in loaded.items() if name.casefold() == "powercontext_client_server_url"), None + ) + return run_diagnostics(server_url=server_url, allow_insecure_http=allow_insecure_http) + + def run_diagnostics(*, server_url: str | None = None, allow_insecure_http: bool | None = None) -> dict[str, Diagnostic]: """Collect installed-environment diagnostics without changing state.""" diff --git a/src/powercontext/service/controller.py b/src/powercontext/service/controller.py index 8aa7462549..0697f51b2d 100644 --- a/src/powercontext/service/controller.py +++ b/src/powercontext/service/controller.py @@ -254,6 +254,14 @@ def _build_definition(self, env_file: Path | None, *, start_on_login: bool) -> S raise ServiceError( # noqa: TRY003 f"invalid personal service configuration: {error}", exit_code=2 ) from error + if loaded_env is not None and ( + any(name.casefold().startswith("powercontext_client_") for name in loaded_env.values) + and not any(name.casefold().startswith("powercontext_server_") for name in loaded_env.values) + ): + raise ServiceError( # noqa: TRY003 + "the selected environment file contains only Client settings; configure a local Server before installing a service", + exit_code=2, + ) if loaded_env is None: inherited = sorted( name diff --git a/tests/fixtures/installation/README.md b/tests/fixtures/installation/README.md index b700a8ccd5..5ada199593 100644 --- a/tests/fixtures/installation/README.md +++ b/tests/fixtures/installation/README.md @@ -19,12 +19,18 @@ Each file has `schema_version: 1` and a `cases` array. Case IDs must be unique a | `git` | Whether Git is discoverable; defaults to false | | `setup_exit` | Exit status of the fixture host operation; defaults to success | | `reported_version` | Fixture CLI version; defaults to the wheel's `1.2.0` | +| `environment_file` | Create a protected existing configuration file for the case | +| `terminal` | Run a piped Bash installer through a POSIX controlling terminal | +| `config_exit`, `validate_exit`, `service_exit`, `doctor_exit` | Exit status of the fixture operation | | `missing_commands` | Public command help paths omitted by the fixture | | `expected.exit_code` | Installer completion or failure | | `expected.runtime_installed` | Whether uv installed the tool launcher, including verification failures | | `expected.server_extra` | Whether the selected profile contains the fixture Server dependency | | `expected.selected_hosts` | Hosts actually submitted to setup; defaults to none | | `expected.setup_ref` | Release tag actually submitted to setup | +| `expected.configuration_saved` | Whether interactive configuration wrote its selected file | +| `expected.service`, `expected.doctor` | Whether registration and same-file diagnostics produced their fixture results | +| `expected.setup_env_file` | Whether host setup received the selected configuration file | | `expected.verification_failed` | Whether success must remain unannounced | | `expected.error_contains` | Required diagnostic fragment | @@ -37,4 +43,6 @@ The installation CI matrix runs these cases on Linux, macOS and Windows. The sam PowerContext wheel and runs `tests/native/test_installation.py` for bootstrap, configuration, HTTP readiness, Memory persistence, profile replacement and version selection. `tests/native/test_install_sources.py` adds Linux loopback index fault tests using the real downloader and uv, with only distribution endpoints redirected to fixtures. -Those source tests do not qualify public mirror availability or native Windows source handling. +Those source tests do not qualify public mirror availability or native Windows source handling. Native service CI +also runs `tests/native/test_service_installation.py` on disposable Linux/macOS runners with the built wheel, a real +user-service manager, configuration reconciliation, an offline stopped-service upgrade, and persistent Memory readback. diff --git a/tests/fixtures/installation/services.json b/tests/fixtures/installation/services.json new file mode 100644 index 0000000000..8ed88773ba --- /dev/null +++ b/tests/fixtures/installation/services.json @@ -0,0 +1,200 @@ +{ + "schema_version": 1, + "cases": [ + { + "id": "service-existing-file", + "environment_file": true, + "args": [ + "--service", + "--env-file", + "{env_file}" + ], + "expected": { + "exit_code": 0, + "runtime_installed": true, + "service": true, + "doctor": true + } + }, + { + "id": "service-registration-failure", + "environment_file": true, + "args": [ + "--service", + "--env-file", + "{env_file}" + ], + "service_exit": 1, + "expected": { + "exit_code": 1, + "runtime_installed": true, + "service": true, + "error_contains": "personal service setup did not complete" + } + }, + { + "id": "service-readiness-failure", + "environment_file": true, + "args": [ + "--service", + "--env-file", + "{env_file}" + ], + "doctor_exit": 1, + "expected": { + "exit_code": 1, + "runtime_installed": true, + "service": true, + "doctor": true, + "error_contains": "Server diagnostics did not pass" + } + }, + { + "id": "invalid-service-configuration", + "environment_file": true, + "args": [ + "--service", + "--env-file", + "{env_file}" + ], + "validate_exit": 2, + "expected": { + "exit_code": 1, + "runtime_installed": true, + "error_contains": "configuration validation failed" + } + }, + { + "id": "service-and-host-use-same-file", + "environment_file": true, + "git": true, + "args": [ + "--service", + "--env-file", + "{env_file}", + "--host", + "codex" + ], + "expected": { + "exit_code": 0, + "runtime_installed": true, + "service": true, + "doctor": true, + "selected_hosts": [ + "codex" + ], + "setup_ref": "powercontext-v1.2.0", + "setup_env_file": true + } + }, + { + "id": "client-rejects-local-service", + "environment_file": true, + "args": [ + "--profile", + "client", + "--service", + "--env-file", + "{env_file}" + ], + "expected": { + "exit_code": 1, + "runtime_installed": false, + "error_contains": "require --profile local" + } + }, + { + "id": "service-requires-explicit-file", + "args": [ + "--service" + ], + "expected": { + "exit_code": 1, + "runtime_installed": false, + "error_contains": "require --env-file" + } + }, + { + "id": "service-requires-existing-file", + "args": [ + "--service", + "--env-file", + "{env_file}" + ], + "expected": { + "exit_code": 1, + "runtime_installed": false, + "error_contains": "existing file" + } + }, + { + "id": "configuration-requires-terminal", + "args": [ + "--configure", + "--service", + "--env-file", + "{env_file}" + ], + "expected": { + "exit_code": 1, + "runtime_installed": false, + "error_contains": "requires a terminal" + } + }, + { + "id": "piped-configuration-and-service", + "terminal": true, + "args": [ + "--configure", + "--service", + "--env-file", + "{env_file}" + ], + "expected": { + "exit_code": 0, + "runtime_installed": true, + "configuration_saved": true, + "service": true, + "doctor": true + } + }, + { + "id": "cancelled-configuration-stops-service", + "terminal": true, + "config_exit": 130, + "args": [ + "--configure", + "--service", + "--env-file", + "{env_file}" + ], + "expected": { + "exit_code": 1, + "runtime_installed": true, + "configuration_saved": false, + "error_contains": "configuration did not complete" + } + }, + { + "id": "existing-configuration-cancellation-stops-hosts", + "terminal": true, + "environment_file": true, + "git": true, + "config_exit": 130, + "args": [ + "--configure", + "--service", + "--env-file", + "{env_file}", + "--host", + "codex" + ], + "expected": { + "exit_code": 1, + "runtime_installed": true, + "selected_hosts": [], + "error_contains": "configuration did not complete" + } + } + ] +} diff --git a/tests/native/test_service_installation.py b/tests/native/test_service_installation.py new file mode 100644 index 0000000000..a2d7cc9135 --- /dev/null +++ b/tests/native/test_service_installation.py @@ -0,0 +1,145 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Installed-script acceptance on disposable native macOS/Linux user-service runners.""" + +from __future__ import annotations + +import json +import os +import shutil +import socket +import subprocess +import sys +from pathlib import Path + +import httpx +import pytest + +from powercontext.cli.config import write_environment + +ROOT = Path(__file__).resolve().parents[2] +WHEEL = os.environ.get("POWERCONTEXT_INSTALL_WHEEL") +pytestmark = [ + pytest.mark.native_service, + pytest.mark.skipif( + os.environ.get("POWERCONTEXT_RUN_NATIVE_SERVICE_TESTS") != "1" + or not WHEEL + or sys.platform not in {"linux", "darwin"}, + reason="requires a release-shaped wheel and a disposable native user-service runner", + ), +] + + +def test_installer_service_reconciles_configuration_and_preserves_memory_after_upgrade(tmp_path: Path) -> None: + assert WHEEL is not None + uv = shutil.which("uv") + assert uv is not None + environment = { + key: value + for key, value in os.environ.items() + if not key.startswith(("POWERCONTEXT_", "UV_", "PIP_", "PYTHON")) and key != "VIRTUAL_ENV" + } + constraints = tmp_path / "constraints.txt" + constraints.write_text(f"powercontext @ {Path(WHEEL).resolve().as_uri()}\n") + environment.update( + UV_CACHE_DIR=os.environ.get("UV_CACHE_DIR", str(Path.home() / ".cache" / "uv")), + UV_NO_CONFIG="1", + UV_OFFLINE="1", + UV_PYTHON_DOWNLOADS="never", + UV_CONSTRAINT=str(constraints), + UV_TOOL_DIR=str(tmp_path / "tools"), + UV_TOOL_BIN_DIR=str(tmp_path / "bin"), + ) + cli = str(tmp_path / "bin" / "powercontext") + configuration = tmp_path / "personal server.env" + with socket.socket() as listener: + listener.bind(("127.0.0.1", 0)) + port = listener.getsockname()[1] + token = "native-installation-token" # noqa: S105 + content = ( + f"POWERCONTEXT_HOME='{tmp_path / 'data'}'\n" + "POWERCONTEXT_SERVER_HTTP_HOST=127.0.0.1\n" + f"POWERCONTEXT_SERVER_HTTP_PORT={port}\n" + "POWERCONTEXT_SERVER_ACCESS_MODE=enforced\n" + f"POWERCONTEXT_SERVER_AUTH_TOKEN={token}\n" + ) + write_environment(configuration, content, backup=False) + installer = ["/bin/bash", str(ROOT / "website/public/install.sh"), "--region", "global"] + + def run(command: list[str], name: str) -> str: + result = subprocess.run(command, env=environment, capture_output=True, text=True, timeout=300) + output = result.stdout + result.stderr + (tmp_path / f"{name}.log").write_text(output) + assert result.returncode == 0, output + return result.stdout + + # This runner is disposable. Never replace an existing personal service. + before = subprocess.run( + [str(Path(sys.executable).with_name("powercontext")), "service", "status", "--json"], + env=environment, + capture_output=True, + text=True, + timeout=30, + ) + assert json.loads(before.stdout)["registration"] == "not_installed", before.stdout + before.stderr + try: + output = run([*installer, "--version", "1.2.0", "--service", "--env-file", str(configuration)], "install") + assert "Personal service verified with:" in output + status = json.loads(run([cli, "service", "status", "--json"], "status")) + assert status["manager"] == "active" + assert status["server_liveness"] == "live" + endpoint = f"http://127.0.0.1:{port}" + with httpx.Client(base_url=endpoint, headers={"Authorization": f"Bearer {token}"}, trust_env=False) as client: + scope = client.get("/v1/scopes/default").json()["scope_id"] + response = client.post( + "/v1/memory/remember", + json={"scope_id": scope, "kind": "decision", "text": "Keep native service memory."}, + ) + response.raise_for_status() + + content += "POWERCONTEXT_CODEX_SCOPE_ID=scope_example\n" + write_environment(configuration, content, backup=False) + # Stale status intentionally exits nonzero; inspect its public JSON separately. + stale = subprocess.run( + [cli, "service", "status", "--json"], env=environment, capture_output=True, text=True, timeout=30 + ) + assert stale.returncode == 1 + assert json.loads(stale.stdout)["definition"] == "stale" + run([*installer, "--version", "1.2.0", "--service", "--env-file", str(configuration)], "reconcile") + + run([cli, "service", "uninstall"], "stop-before-upgrade") + upgraded = tmp_path / "upgraded" + build = subprocess.run( + [uv, "build", "--offline", "--wheel", "--out-dir", str(upgraded)], + cwd=ROOT, + env={**environment, "SETUPTOOLS_SCM_PRETEND_VERSION": "1.2.1"}, + capture_output=True, + text=True, + timeout=300, + ) + assert build.returncode == 0, build.stdout + build.stderr + constraints.write_text(f"powercontext @ {next(upgraded.glob('*.whl')).as_uri()}\n") + run([*installer, "--version", "1.2.1", "--service", "--env-file", str(configuration)], "upgrade") + assert run([cli, "--version"], "upgraded-version").strip() == "1.2.1" + assert configuration.read_text() == content + with httpx.Client(base_url=endpoint, headers={"Authorization": f"Bearer {token}"}, trust_env=False) as client: + response = client.post( + "/v1/memory/search", json={"scope_id": scope, "query": "native service memory", "mode": "fts"} + ) + response.raise_for_status() + assert any(hit["text"] == "Keep native service memory." for hit in response.json()["hits"]) + finally: + if Path(cli).exists(): + run([cli, "service", "uninstall"], "cleanup") diff --git a/tests/test_config_cli.py b/tests/test_config_cli.py index d23f48f76b..b2597ca8d0 100644 --- a/tests/test_config_cli.py +++ b/tests/test_config_cli.py @@ -404,18 +404,28 @@ def test_init_refuses_to_replace_an_existing_environment_without_force( assert environment.read_text(encoding="utf-8") == "EXISTING=value\n" -def test_init_force_defaults_to_preserving_existing_inference_configuration(tmp_path: Path) -> None: +@pytest.mark.parametrize("require_write", [False, True]) +def test_init_force_defaults_to_preserving_existing_inference_configuration( + tmp_path: Path, require_write: bool +) -> None: environment = tmp_path / ".env" original = config_cli.render_managed_block(_configuration()) environment.write_text(original, encoding="utf-8") result = CliRunner().invoke( config_cli.app, - ["init", "--template", "--output", str(environment), "--force"], + [ + "init", + "--template", + "--output", + str(environment), + "--force", + *(["--require-write"] if require_write else []), + ], input="\n", ) - assert result.exit_code == 0 + assert result.exit_code == (130 if require_write else 0) assert "will remove existing model, embedding, inference schedule, or provider credential settings" in result.output assert "A mode-0600 backup will be created" in result.output assert "Replace them with a model-free configuration? [y/N]" in result.output diff --git a/tests/test_config_wizard.py b/tests/test_config_wizard.py index fe8094d20d..f9ea653252 100644 --- a/tests/test_config_wizard.py +++ b/tests/test_config_wizard.py @@ -298,19 +298,34 @@ def test_base_wizard_writes_private_environment_without_models(tmp_path: Path) - assert CliRunner().invoke(app, ["validate", "--env-file", str(output)]).exit_code == 0 -def test_cancel_in_chinese_does_not_write_files(tmp_path: Path) -> None: +@pytest.mark.parametrize("require_write", [False, True]) +def test_cancel_in_chinese_does_not_write_files(tmp_path: Path, require_write: bool) -> None: output = tmp_path / "server.env" result = CliRunner().invoke( app, - ["init", "--language", "zh", "--output", str(output)], + ["init", "--language", "zh", "--output", str(output), *(["--require-write"] if require_write else [])], input=f"sqlite\n{tmp_path / 'context.db'}\nlocal\nbase\nn\n\nnone\ny\nn\n", ) - assert result.exit_code == 0, result.output + assert result.exit_code == (130 if require_write else 0), result.output assert "配置向导" in result.output assert "未写入" in result.output assert not list(tmp_path.iterdir()) +def test_required_write_cancellation_preserves_existing_configuration(tmp_path: Path) -> None: + output = tmp_path / "server.env" + original = "POWERCONTEXT_SERVER_DATABASE_KIND=sqlite\nPOWERCONTEXT_SERVER_HTTP_PORT=18000\nCUSTOM_FLAG=keep\n" + output.write_text(original) + result = CliRunner().invoke( + app, + ["init", "--language", "en", "--output", str(output), "--require-write"], + input=f"sqlite\n{tmp_path / 'context.db'}\nreuse\nn\n", + ) + assert result.exit_code == 130, result.output + assert output.read_text() == original + assert not output.with_name("server.env.next-steps.md").exists() + + def test_existing_server_skips_server_models_and_storage(tmp_path: Path) -> None: output = tmp_path / "client.env" result = CliRunner().invoke( diff --git a/tests/test_config_wizard_next_steps.py b/tests/test_config_wizard_next_steps.py index fe8b9eb2e7..9f9e53eca3 100644 --- a/tests/test_config_wizard_next_steps.py +++ b/tests/test_config_wizard_next_steps.py @@ -82,6 +82,33 @@ def test_scope_binding_reloads_client_environment_before_agent_installation(tmp_ assert "Reload the edited client environment before starting a new Agent" in steps +@pytest.mark.parametrize("platform", ["linux", "darwin", "win32"]) +def test_startup_guidance_matches_personal_service_support(tmp_path, monkeypatch, platform) -> None: + import powercontext.cli.config_startup as startup + + monkeypatch.setattr(startup.sys, "platform", platform) + state = _state() + environment = tmp_path / ".env" + steps = wizard._next_steps(state, environment, environment) + creation = steps.index("POST http://127.0.0.1:8000/v1/scopes") + installation = steps.index("powercontext setup claude-code") + if platform == "win32": + assert "powercontext server run --env-file" in steps[:creation] + assert "powercontext service install" not in steps + else: + assert "powercontext service install --env-file" in steps[:creation] + assert "powercontext doctor --env-file" in steps[:creation] + assert "powercontext service install --env-file" in steps[creation:installation] + + +def test_managed_bind_keeps_foreground_startup_guidance(tmp_path) -> None: + state = _state() + state.values["POWERCONTEXT_SERVER_HTTP_HOST"] = "0.0.0.0" # noqa: S104 - managed-bind guidance + steps = wizard._next_steps(state, tmp_path / ".env", tmp_path / ".env") + assert "powercontext server run --env-file" in steps + assert "powercontext service install" not in steps + + def _profile_script() -> str: state = _state() state.features = {"profile"} diff --git a/tests/test_doctor_environment.py b/tests/test_doctor_environment.py new file mode 100644 index 0000000000..a6e123043a --- /dev/null +++ b/tests/test_doctor_environment.py @@ -0,0 +1,118 @@ +# Copyright (c) 2026 OceanBase. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Explicit-file diagnostics reach the configured HTTP endpoint without shell evaluation.""" + +import json +import os +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from threading import Thread + +import pytest +from typer.testing import CliRunner + +from powercontext.cli.app import create_cli +from powercontext.cli.system import doctor_app + + +@pytest.fixture +def health_server(): + class Handler(BaseHTTPRequestHandler): + readiness = "ready" + + def log_message(self, format, *args): # noqa: A002 + pass + + def do_GET(self): + payload = ( + {"status": "ok"} + if self.path == "/health/live" + else {"status": self.readiness, "checks": {"database": "ready"}} + ) + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(payload).encode()) + + server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + worker = Thread(target=server.serve_forever, daemon=True) + worker.start() + try: + yield server.server_port, Handler + finally: + server.shutdown() + server.server_close() + worker.join() + + +@pytest.mark.parametrize("server_file", [True, False]) +@pytest.mark.parametrize("readiness", ["ready", "degraded"]) +@pytest.mark.parametrize("lowercase_names", [False, True]) +def test_doctor_uses_selected_file_and_preserves_shell( + tmp_path, monkeypatch, health_server, server_file, readiness, lowercase_names +): + port, handler = health_server + handler.readiness = readiness + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config")) + monkeypatch.setenv("POWERCONTEXT_CLIENT_SERVER_URL", "http://127.0.0.1:1") + monkeypatch.setenv("POWERCONTEXT_SERVER_HTTP_PORT", "1") + monkeypatch.setenv("POWERCONTEXT_CLIENT_API_TOKEN", "caller-secret") + marker = tmp_path / "executed" + environment = tmp_path / "selected.env" + endpoint = f"http://127.0.0.1:{port}" + content = ( + f"POWERCONTEXT_SERVER_HTTP_HOST=127.0.0.1\nPOWERCONTEXT_SERVER_HTTP_PORT={port}\n" + "POWERCONTEXT_CLIENT_SERVER_URL=http://127.0.0.1:1\n" + if server_file + else f"POWERCONTEXT_CLIENT_SERVER_URL={endpoint}\n" + ) + if lowercase_names: + content = content.lower() + environment.write_text(content + f"POWERCONTEXT_CLIENT_API_TOKEN='file-secret'\nCUSTOM='$(touch {marker})'\n") + result = CliRunner().invoke(create_cli([doctor_app]), ["doctor", "--env-file", str(environment), "--json"]) + + assert result.exit_code == (0 if readiness == "ready" else 1), result.output + response = json.loads(result.output) + assert response["checks"]["server_readiness"]["detail"] == f"{endpoint} status={readiness}" + assert "file-secret" not in result.output + assert not marker.exists() + assert os.environ["POWERCONTEXT_CLIENT_API_TOKEN"] == "caller-secret" # noqa: S105 + assert os.environ["POWERCONTEXT_SERVER_HTTP_PORT"] == "1" + + +def test_doctor_explicit_url_overrides_file(tmp_path, health_server): + port, _handler = health_server + environment = tmp_path / "selected.env" + environment.write_text("POWERCONTEXT_SERVER_HTTP_PORT=1\n") + endpoint = f"http://127.0.0.1:{port}" + result = CliRunner().invoke( + create_cli([doctor_app]), ["doctor", "--env-file", str(environment), "--server-url", endpoint, "--json"] + ) + assert result.exit_code == 0, result.output + assert endpoint in json.loads(result.output)["checks"]["server_readiness"]["detail"] + + +def test_doctor_rejects_missing_environment_file(tmp_path): + result = CliRunner().invoke(create_cli([doctor_app]), ["doctor", "--env-file", str(tmp_path / "missing.env")]) + assert result.exit_code == 2 + assert "Cannot diagnose the environment file" in result.output + + +def test_doctor_rejects_file_option_for_integration_diagnostics(tmp_path): + environment = tmp_path / "selected.env" + environment.write_text("POWERCONTEXT_SERVER_HTTP_PORT=8000\n") + result = CliRunner().invoke(create_cli([doctor_app]), ["doctor", "--env-file", str(environment), "codex"]) + assert result.exit_code == 2 + assert "without a subcommand" in result.output diff --git a/tests/test_installation_contract.py b/tests/test_installation_contract.py index c9f222db5d..2b32297642 100644 --- a/tests/test_installation_contract.py +++ b/tests/test_installation_contract.py @@ -51,10 +51,32 @@ def main(): if command == "server run": import contract_server print("Synthetic installation contract CLI") - elif args[:2] == ["setup", "select"]: + elif args[:2] == ["config", "init"]: + if not sys.stdin.isatty(): + return 2 + status = int(os.environ.get("CONTRACT_CONFIG_EXIT", "0")) + if status == 0: + Path(args[args.index("--output") + 1]).write_text("POWERCONTEXT_SERVER_HTTP_PORT=18000\\n") + return status + elif args[:2] == ["config", "validate"]: + return int(os.environ.get("CONTRACT_VALIDATE_EXIT", "0")) + elif args[:2] == ["service", "install"]: + env_file = args[args.index("--env-file") + 1] + Path(os.environ["CONTRACT_SERVICE_RESULT"]).write_text(json.dumps({"env_file": env_file})) + return int(os.environ.get("CONTRACT_SERVICE_EXIT", "0")) + elif args[:2] == ["service", "status"]: + return 0 + elif args and args[0] == "doctor": + env_file = args[args.index("--env-file") + 1] + Path(os.environ["CONTRACT_DOCTOR_RESULT"]).write_text(json.dumps({"env_file": env_file})) + return int(os.environ.get("CONTRACT_DOCTOR_EXIT", "0")) + elif args and args[0] == "setup" and "select" in args: hosts = [args[i + 1] for i, arg in enumerate(args) if arg == "--host"] ref = args[args.index("--ref") + 1] - Path(os.environ["CONTRACT_SETUP_RESULT"]).write_text(json.dumps({"hosts": hosts, "ref": ref})) + selected = {"hosts": hosts, "ref": ref} + if "--env-file" in args: + selected["env_file"] = args[args.index("--env-file") + 1] + Path(os.environ["CONTRACT_SETUP_RESULT"]).write_text(json.dumps(selected)) return int(os.environ.get("CONTRACT_SETUP_EXIT", "0")) elif args == ["contract-capabilities"]: print(json.dumps({"server_extra": importlib.util.find_spec("contract_server") is not None})) @@ -121,6 +143,8 @@ def test_installation_contract(case: dict[str, Any], tmp_path: Path) -> None: if not uv: pytest.skip("native installation contract requires real uv") windows = sys.platform == "win32" + if case.get("terminal") and windows: + pytest.skip("controlling-terminal pipe regression requires POSIX") shell = shutil.which("powershell") or shutil.which("pwsh") if windows else shutil.which("bash") if not shell: pytest.skip("native installer shell is unavailable") @@ -160,15 +184,58 @@ def test_installation_contract(case: dict[str, Any], tmp_path: Path) -> None: "CONTRACT_SETUP_EXIT": str(case.get("setup_exit", 0)), "CONTRACT_REPORTED_VERSION": case.get("reported_version", "1.2.0"), "CONTRACT_MISSING_COMMANDS": json.dumps(case.get("missing_commands", [])), + "CONTRACT_VALIDATE_EXIT": str(case.get("validate_exit", 0)), + "CONTRACT_CONFIG_EXIT": str(case.get("config_exit", 0)), + "CONTRACT_SERVICE_EXIT": str(case.get("service_exit", 0)), + "CONTRACT_DOCTOR_EXIT": str(case.get("doctor_exit", 0)), + "CONTRACT_SERVICE_RESULT": str(tmp_path / "service.json"), + "CONTRACT_DOCTOR_RESULT": str(tmp_path / "doctor.json"), }) + env_file = tmp_path / "existing configuration.env" + if case.get("environment_file"): + env_file.write_text("POWERCONTEXT_SERVER_HTTP_PORT=18000\n", encoding="utf-8") + env_file.chmod(0o600) + arguments = [str(env_file) if value == "{env_file}" else value for value in case["args"]] script = ROOT / "website/public" / ("install.ps1" if windows else "install.sh") command = ( [shell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(script)] if windows else [shell, str(script)] ) + if case.get("terminal"): + controller = """import errno, os, pty, sys +pid, terminal = pty.fork() +if pid == 0: + shell, script, *args = sys.argv[1:] + os.execv(shell, [shell, '-o', 'pipefail', '-c', + 'cat "$1" | "$2" -s -- "${@:3}"', 'installation', script, shell, *args]) +try: + while True: + try: + data = os.read(terminal, 4096) + except OSError as error: + if error.errno == errno.EIO: + break + raise + if not data: + break + os.write(1, data) +finally: + os.close(terminal) +_, status = os.waitpid(pid, 0) +sys.exit(os.waitstatus_to_exitcode(status)) +""" + command = [sys.executable, "-c", controller, *command] result = subprocess.run( - [*command, "--region", "global", *case["args"]], env=environment, capture_output=True, text=True, timeout=60 + [*command, "--region", "global", *arguments], env=environment, capture_output=True, text=True, timeout=60 ) + assert_installation_outcome(case, tmp_path, result, environment) + + +def assert_installation_outcome(case, tmp_path, result, environment) -> None: expected = case["expected"] + windows = sys.platform == "win32" + env_file = tmp_path / "existing configuration.env" + if "configuration_saved" in expected: + assert env_file.exists() == expected["configuration_saved"] assert result.returncode == expected["exit_code"], result.stdout + result.stderr executable = tmp_path / "bin" / ("powercontext.exe" if windows else "powercontext") assert executable.exists() == expected["runtime_installed"] @@ -177,10 +244,19 @@ def test_installation_contract(case: dict[str, Any], tmp_path: Path) -> None: assert observed["hosts"] == expected.get("selected_hosts", []) if "setup_ref" in expected: assert observed["ref"] == expected["setup_ref"] + if expected.get("setup_env_file"): + assert observed["env_file"] == str(env_file) + for action in ("service", "doctor"): + observed_file = tmp_path / f"{action}.json" + assert observed_file.exists() == expected.get(action, False) + if observed_file.exists(): + assert json.loads(observed_file.read_text())["env_file"] == str(env_file) + if case.get("environment_file"): + assert env_file.read_text() == "POWERCONTEXT_SERVER_HTTP_PORT=18000\n" if expected.get("verification_failed"): assert "Runtime installed:" not in result.stdout if "error_contains" in expected: - assert expected["error_contains"] in result.stderr + assert expected["error_contains"] in result.stdout + result.stderr if expected["runtime_installed"] and "server_extra" in expected: capabilities = subprocess.check_output([str(executable), "contract-capabilities"], env=environment, text=True) assert json.loads(capabilities)["server_extra"] == expected["server_extra"] diff --git a/tests/test_service.py b/tests/test_service.py index 80c0b5ca15..f22c9e96a9 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -372,6 +372,21 @@ def test_service_controller_installs_and_starts_one_native_registration(tmp_path assert adapter.events == ["write", "reload", "enable", "start:True"] +def test_service_install_rejects_client_only_configuration(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + environment = tmp_path / "client.env" + environment.write_text("POWERCONTEXT_CLIENT_SERVER_URL=https://memory.example.com\n") + environment.chmod(0o600) + _secure_windows_file(environment) + controller = ServiceController(FakeAdapter(tmp_path)) + monkeypatch.setattr(service_cli, "_controller", lambda: controller) + + result = CliRunner().invoke(service_app, ["install", "--env-file", str(environment), "--start-on-login"]) + + assert result.exit_code == 2, result.output + assert "contains only Client settings" in result.output + assert controller.registration_status().registration is RegistrationState.NOT_INSTALLED + + def test_service_controller_allows_slow_native_startup(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: adapter = FakeAdapter(tmp_path) clock = 0.0 diff --git a/website/public/install.ps1 b/website/public/install.ps1 index bccb76cdb7..6541f21aad 100644 --- a/website/public/install.ps1 +++ b/website/public/install.ps1 @@ -23,6 +23,9 @@ if ($env:POWERCONTEXT_INSTALL_REGION) { $Region = $env:POWERCONTEXT_INSTALL_REGI $IndexUrl = '' $Hosts = @() $NoHosts = $false +$Configure = $false +$InstallService = $false +$EnvFile = '' $Uv = '' $TempDir = $null @@ -38,10 +41,15 @@ Usage: powershell -ExecutionPolicy Bypass -File install.ps1 [options] --index-url URL HTTPS default package index for this installation. --host HOST Install an Agent integration; repeat for multiple hosts. --no-hosts Compatibility option; integration setup is skipped by default. + --configure Open the configuration wizard in a terminal; requires --env-file. + --service Install and verify the personal service using --env-file. + --env-file PATH Explicit configuration file for configuration, service, and hosts. -h, --help Show this help. Agent integration setup runs only for explicitly selected --host values. -After installation, run powercontext setup select to choose hosts interactively. +After installation, run powercontext setup select --ref "powercontext-v$(powercontext --version)". +Unattended service setup requires an existing protected environment file. +Windows service setup is experimental; --service enables login auto-start explicitly. Region priority: --region, POWERCONTEXT_INSTALL_REGION, named timezone, locale territory, then global. No network location service is queried. @@ -216,7 +224,7 @@ try { for ($Index = 0; $Index -lt $args.Count; $Index++) { $Option = $args[$Index] switch ($Option) { - { $_ -in '--version', '--profile', '--region', '--index-url', '--host' } { + { $_ -in '--version', '--profile', '--region', '--index-url', '--host', '--env-file' } { $Index++ if ($Index -ge $args.Count -or -not $args[$Index] -or $args[$Index].StartsWith('--')) { throw "$Option requires a value." @@ -227,9 +235,12 @@ try { '--region' { $Region = $args[$Index] } '--index-url' { $IndexUrl = $args[$Index] } '--host' { $Hosts += @('--host', $args[$Index]) } + '--env-file' { $EnvFile = $args[$Index] } } } '--no-hosts' { $NoHosts = $true } + '--configure' { $Configure = $true } + '--service' { $InstallService = $true } { $_ -in '-h', '--help' } { Show-Help; exit 0 } default { throw 'Unknown option. Run install.ps1 --help.' } } @@ -242,6 +253,16 @@ try { if ($IndexUrl) { Assert-HttpsUrl $IndexUrl '--index-url' } if ($env:POWERCONTEXT_UV_INSTALLER_URL) { Assert-HttpsUrl $env:POWERCONTEXT_UV_INSTALLER_URL 'POWERCONTEXT_UV_INSTALLER_URL' } if ($NoHosts -and $Hosts.Count) { throw '--host and --no-hosts cannot be combined.' } + if ($Configure -or $InstallService) { + if ($RuntimeProfile -ne 'local') { throw '--configure and --service require --profile local.' } + if (-not $EnvFile) { throw '--configure and --service require --env-file PATH.' } + } + if ($EnvFile -and -not $Configure -and -not (Test-Path -LiteralPath $EnvFile -PathType Leaf)) { + throw '--env-file must name an existing file unless --configure is selected.' + } + if ($Configure -and ([Console]::IsInputRedirected -or [Console]::IsOutputRedirected)) { + throw '--configure requires a terminal. For unattended setup, use --service with an existing --env-file.' + } if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Use install.sh on macOS and Linux.' } if ($Hosts.Count -and -not (Get-Command git -CommandType Application -ErrorAction SilentlyContinue)) { throw 'Agent integration setup requires Git. Install Git or use --no-hosts.' @@ -325,15 +346,47 @@ try { Write-Host "Runtime installed: $Version ($RuntimeProfile)" $PathPrefix = ("$($ToolBin.Trim());$(Split-Path -Parent $Uv)").Replace("'", "''") Write-Host ('For a new terminal: $env:Path = ''' + $PathPrefix + ';'' + $env:Path') + if ($Configure) { + & $Cli config init --output $EnvFile --require-write + if ($LASTEXITCODE -ne 0) { throw 'Runtime installed, but configuration did not complete. Review the command error or cancellation; retry config init before service setup.' } + Write-Host "Configuration saved: $EnvFile" + } + if ($EnvFile -and $RuntimeProfile -eq 'local') { + & $Cli config validate --env-file $EnvFile + if ($LASTEXITCODE -ne 0) { throw 'Runtime installed, but configuration validation failed. Correct the selected environment file before service setup.' } + Write-Host "Configuration validated: $EnvFile" + } + if ($InstallService) { + & $Cli service install --env-file $EnvFile --start-on-login + if ($LASTEXITCODE -ne 0) { throw 'Runtime installed, but personal service setup did not complete. Inspect service status and retry service install with the same --env-file.' } + & $Cli service status + if ($LASTEXITCODE -ne 0) { throw 'Runtime installed, but personal service verification failed. Inspect the native service logs.' } + & $Cli doctor --env-file $EnvFile + if ($LASTEXITCODE -ne 0) { throw 'Personal service installed, but Server diagnostics did not pass. Review readiness and retry doctor with the same --env-file.' } + Write-Host "Personal service verified with: $EnvFile" + } $SetupStatus = 0 if ($Hosts.Count) { - & $Cli setup select --source oceanbase/powercontext --ref "powercontext-v$Version" @Hosts + $SetupArgs = @('setup') + if ($EnvFile) { $SetupArgs += @('--env-file', $EnvFile) } + & $Cli @SetupArgs select --source oceanbase/powercontext --ref "powercontext-v$Version" @Hosts $SetupStatus = $LASTEXITCODE } if ($RuntimeProfile -eq 'local') { - Write-Host "`nConfigure PowerContext with: powercontext config init" + if ($InstallService) { + Write-Host 'After upgrades or any environment-file edit, rerun service install with the same --env-file.' + } + else { + $NextEnv = '.env' + if ($EnvFile) { $NextEnv = $EnvFile } + $QuotedEnv = "'" + $NextEnv.Replace("'", "''") + "'" + if (-not $EnvFile) { Write-Host "`nConfigure: powercontext config init --output $QuotedEnv" } + Write-Host "Then run: powercontext service install --env-file $QuotedEnv" + Write-Host ' powercontext service status' + Write-Host " powercontext doctor --env-file $QuotedEnv" + Write-Host 'Windows personal services are experimental. For development or temporary use, run powercontext server run.' + } Write-Host ' https://powercontext.oceanbase.io/en/docs/get-started/quickstart/' - Write-Host 'Then run: powercontext server run --env-file .env' } else { Write-Host "`nConnect to your existing Server:" diff --git a/website/public/install.sh b/website/public/install.sh index 783b430aee..d4d7ff5537 100644 --- a/website/public/install.sh +++ b/website/public/install.sh @@ -23,6 +23,9 @@ PYTHON_BIN="" INDEX_URL="" HOSTS=() NO_HOSTS=false +CONFIGURE=false +INSTALL_SERVICE=false +ENV_FILE="" TEMP_DIR="" UV_BIN="" GUIDE=https://powercontext.oceanbase.io/en/docs/get-started/quickstart/ @@ -52,10 +55,15 @@ Usage: bash install.sh [--version VERSION] [--index-url URL] [--host HOST]... [- Existing additional uv indexes still take precedence. --host HOST Install this Agent integration; repeat for multiple hosts. --no-hosts Compatibility option; integration setup is skipped by default. + --configure Open the configuration wizard in a terminal; requires --env-file. + --service Install and verify the personal service using --env-file. + --env-file PATH Explicit configuration file for configuration, service, and hosts. -h, --help Show this help. Agent integration setup runs only for explicitly selected --host values. -After installation, run powercontext setup select to choose hosts interactively. +After installation, run powercontext setup select --ref "powercontext-v$(powercontext --version)". +Personal macOS/Linux use: --configure --service --env-file /path/to/powercontext.env. +For unattended service setup, omit --configure and provide an existing protected file. Region priority: --region, POWERCONTEXT_INSTALL_REGION, named timezone, locale territory, then global. No network location service is queried. @@ -77,7 +85,7 @@ EOF parse_args() { while (($#)); do case "$1" in - --version|--profile|--region|--index-url|--host) + --version|--profile|--region|--index-url|--host|--env-file) (($# >= 2)) && [[ -n "$2" && "$2" != --* ]] || fail "$1 requires a value" case "$1" in --version) VERSION=$2 ;; @@ -85,10 +93,13 @@ parse_args() { --region) REGION=$2 ;; --index-url) INDEX_URL=$2 ;; --host) HOSTS+=(--host "$2") ;; + --env-file) ENV_FILE=$2 ;; esac shift 2 ;; --no-hosts) NO_HOSTS=true; shift ;; + --configure) CONFIGURE=true; shift ;; + --service) INSTALL_SERVICE=true; shift ;; -h|--help) usage; exit 0 ;; *) fail "Unknown option. Run bash install.sh --help." ;; esac @@ -108,6 +119,13 @@ parse_args() { if [[ "$NO_HOSTS" == true && ${#HOSTS[@]} -gt 0 ]]; then fail "--host and --no-hosts cannot be combined." fi + if [[ "$CONFIGURE" == true || "$INSTALL_SERVICE" == true ]]; then + [[ "$PROFILE" == local ]] || fail "--configure and --service require --profile local." + [[ -n "$ENV_FILE" ]] || fail "--configure and --service require --env-file PATH." + fi + if [[ -n "$ENV_FILE" && "$CONFIGURE" == false ]]; then + [[ -f "$ENV_FILE" ]] || fail "--env-file must name an existing file unless --configure is selected." + fi } download() { @@ -300,6 +318,11 @@ main() { esac [[ -n "${HOME:-}" ]] || fail "HOME is not set." command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 || fail "Install curl or wget first." + if [[ "$CONFIGURE" == true ]]; then + # The downloaded script owns stdin; give only the opt-in wizard the controlling terminal. + [[ -t 1 ]] && { exec 3/dev/null || + fail "--configure requires a terminal. For unattended setup, use --service with an existing --env-file." + fi if [[ ${#HOSTS[@]} -gt 0 ]]; then command -v git >/dev/null 2>&1 || fail "Agent integration setup requires Git. Install Git or use --no-hosts." fi @@ -349,14 +372,44 @@ main() { printf 'For a new terminal, add these directories to PATH if needed:\n' printf ' export PATH=%q:%q:"%s"\n' "$(dirname "$UV_BIN")" "$tool_bin" "\$PATH" + if [[ "$CONFIGURE" == true ]]; then + "$tool_bin/powercontext" config init --output "$ENV_FILE" --require-write <&3 || + fail "Runtime installed, but configuration did not complete. Review the command error or cancellation; retry config init before service setup." + printf 'Configuration saved: %s\n' "$ENV_FILE" + fi + if [[ -n "$ENV_FILE" && "$PROFILE" == local ]]; then + "$tool_bin/powercontext" config validate --env-file "$ENV_FILE" || + fail "Runtime installed, but configuration validation failed. Correct the selected environment file before service setup." + printf 'Configuration validated: %s\n' "$ENV_FILE" + fi + if [[ "$INSTALL_SERVICE" == true ]]; then + "$tool_bin/powercontext" service install --env-file "$ENV_FILE" || + fail "Runtime installed, but personal service setup did not complete. Inspect service status and retry service install with the same --env-file." + "$tool_bin/powercontext" service status || + fail "Runtime installed, but personal service verification failed. Inspect the native service logs." + "$tool_bin/powercontext" doctor --env-file "$ENV_FILE" || + fail "Personal service installed, but Server diagnostics did not pass. Review readiness and retry doctor with the same --env-file." + printf 'Personal service verified with: %s\n' "$ENV_FILE" + fi + local setup_status=0 if [[ ${#HOSTS[@]} -gt 0 ]]; then - "$tool_bin/powercontext" setup select --source oceanbase/powercontext \ + local setup_args=(setup) + [[ -z "$ENV_FILE" ]] || setup_args+=(--env-file "$ENV_FILE") + "$tool_bin/powercontext" "${setup_args[@]}" select --source oceanbase/powercontext \ --ref "powercontext-v$VERSION" ${HOSTS[@]+"${HOSTS[@]}"} || setup_status=$? fi if [[ "$PROFILE" == local ]]; then - printf '\nConfigure PowerContext with: powercontext config init\n %s\n' "$GUIDE" - printf '%s\n' 'Then run: powercontext server run --env-file .env' + if [[ "$INSTALL_SERVICE" == true ]]; then + printf '%s\n' 'After upgrades or any environment-file edit, rerun service install with the same --env-file.' + else + local next_env=${ENV_FILE:-.env} + [[ -n "$ENV_FILE" ]] || printf '\nConfigure: powercontext config init --output %q\n' "$next_env" + printf 'Then run: powercontext service install --env-file %q\n' "$next_env" + printf ' powercontext service status\n powercontext doctor --env-file %q\n' "$next_env" + printf '%s\n' 'A native user-service manager and loopback Server bind are required. For development or temporary use, run powercontext server run.' + fi + printf ' %s\n' "$GUIDE" else printf '\nConnect to your existing Server:\n %s\n' \ 'https://powercontext.oceanbase.io/en/docs/operate/connect-remote-server/' From b3747dc156cd8c37b81580edd11f1f816fbeed50 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Fri, 9 Oct 2026 17:00:26 +0800 Subject: [PATCH 14/24] fix(config): bind generated setup to its file and release --- src/powercontext/cli/config.py | 57 +++++++------------ src/powercontext/cli/config_wizard.py | 9 ++- .../cli/config_wizard_installation.py | 18 ++++-- tests/test_config_cli.py | 22 +------ tests/test_config_wizard.py | 2 +- tests/test_config_wizard_installation.py | 35 ++++++++++++ tests/test_config_wizard_next_steps.py | 5 +- 7 files changed, 81 insertions(+), 67 deletions(-) diff --git a/src/powercontext/cli/config.py b/src/powercontext/cli/config.py index 549e10d2ad..288ab61775 100644 --- a/src/powercontext/cli/config.py +++ b/src/powercontext/cli/config.py @@ -28,7 +28,6 @@ from dataclasses import dataclass from datetime import UTC, datetime from enum import StrEnum -from importlib.metadata import version from pathlib import Path from typing import TYPE_CHECKING, Annotated, Never from urllib.parse import urlsplit @@ -139,35 +138,15 @@ class ApiProtocol: _PROTOCOL_BY_ID = {protocol.identifier: protocol for protocol in API_PROTOCOLS} -AGENTS: dict[str, tuple[str, str, str]] = { - "codex": ("Codex", "powercontext setup codex --source oceanbase/powercontext --ref master", "codex"), - "claude-code": ( - "Claude Code", - "powercontext setup claude-code --source oceanbase/powercontext --ref master", - "claude", - ), - "dsh": ("DeepSeek Harness", "powercontext setup dsh --source oceanbase/powercontext --ref master", "dsh web"), - "opencode": ( - "OpenCode", - "powercontext setup opencode --source oceanbase/powercontext --ref master", - "opencode", - ), - "pi": ("Pi", "powercontext setup pi --source oceanbase/powercontext --ref master", "pi"), - "openclaw": ( - "OpenClaw", - "powercontext setup openclaw --source oceanbase/powercontext --ref master", - "openclaw", - ), - "hermes": ( - "Hermes", - "powercontext setup hermes --source oceanbase/powercontext --ref master", - "hermes", - ), - "workbuddy": ( - "WorkBuddy", - "powercontext setup workbuddy --source oceanbase/powercontext --ref master", - "重启 WorkBuddy", - ), +AGENTS: dict[str, tuple[str, str]] = { + "codex": ("Codex", "codex"), + "claude-code": ("Claude Code", "claude"), + "dsh": ("DeepSeek Harness", "dsh web"), + "opencode": ("OpenCode", "opencode"), + "pi": ("Pi", "pi"), + "openclaw": ("OpenClaw", "openclaw"), + "hermes": ("Hermes", "hermes"), + "workbuddy": ("WorkBuddy", "重启 WorkBuddy"), } # Input hints only, not a provider allowlist. Unknown prefixes can attach arbitrary variables. @@ -1033,8 +1012,10 @@ def _removes_inference_configuration(existing: str, updated: str) -> bool: def _print_next_steps(path: Path) -> None: from powercontext.cli.config_startup import server_startup_commands + from powercontext.cli.config_wizard_installation import installation_source quoted = shlex.quote(str(path.resolve())) + source = installation_source(Path(__file__)) typer.secho("\nEnvironment file", bold=True, fg=typer.colors.CYAN) typer.echo(f" Path {path.resolve()} (mode 0600)") typer.echo(" Authentication disabled by default.") @@ -1050,14 +1031,14 @@ def _print_next_steps(path: Path) -> None: typer.echo(f" {command}") write_inference_capability_notice(generation_model=None, embedding_model=None) typer.secho("\nSupported Coding Agents (choose one):", bold=True, fg=typer.colors.CYAN) - for host, (name, setup, launch) in AGENTS.items(): - if host == "dsh": - installed = version("powercontext") - setup = ( - f"powercontext setup dsh --source oceanbase/powercontext --ref powercontext-v{installed}" - if re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:(?:a|b|rc)[0-9]+)?", installed) - else "powercontext setup dsh --source /path/to/matching-powercontext-checkout" - ) + for host, (name, launch) in AGENTS.items(): + setup = f"powercontext setup --env-file {quoted} {host}" + if source is None: + setup += " --help" + else: + setup += f" --source {shlex.quote(source.source)}" + if source.ref: + setup += f" --ref {shlex.quote(source.ref)}" typer.echo(f"\n{name}:\n {setup}") if launch.startswith("重启"): typer.echo(f" {launch}") diff --git a/src/powercontext/cli/config_wizard.py b/src/powercontext/cli/config_wizard.py index 4dbf31cdcc..79bd442ea7 100644 --- a/src/powercontext/cli/config_wizard.py +++ b/src/powercontext/cli/config_wizard.py @@ -1384,7 +1384,7 @@ def _next_steps(state: Wizard, output: Path, client_file: Path) -> str: "```", "", ] - lines += _agent_installation_steps(state) + lines += _agent_installation_steps(state, env_file=client_file) if "profile" in state.features: lines += _profile_policy_steps(state) lines += ["## " + ui.text("Checks and pending steps", "验收与待完成项"), ""] @@ -1538,7 +1538,7 @@ def _profile_policy_script(address: str, scope_name: str, authorization_name: st """).splitlines() -def _agent_installation_steps(state: Wizard) -> list[str]: +def _agent_installation_steps(state: Wizard, *, env_file: Path | None = None) -> list[str]: if not state.agents: return [] source = installation_source(Path(__file__)) @@ -1562,7 +1562,10 @@ def _agent_installation_steps(state: Wizard) -> list[str]: if source is None: lines.append(f"powercontext setup {agent} --help") continue - command = f"powercontext setup {agent} --source {shlex.quote(source.source)}" + command = "powercontext setup" + if env_file is not None: + command += f" --env-file {shlex.quote(str(env_file))}" + command += f" {agent} --source {shlex.quote(source.source)}" if source.ref: command += f" --ref {shlex.quote(source.ref)}" if spec.setup_server_url: diff --git a/src/powercontext/cli/config_wizard_installation.py b/src/powercontext/cli/config_wizard_installation.py index 3bd832fe5b..5a9602a84e 100644 --- a/src/powercontext/cli/config_wizard_installation.py +++ b/src/powercontext/cli/config_wizard_installation.py @@ -27,17 +27,18 @@ @dataclass(frozen=True) class InstallationSource: - """A verified checkout or the Git source and requested ref of an installation.""" + """A verified checkout, preserved Git origin, or tagged package-index release.""" source: str ref: str | None = None def installation_source(module_path: Path) -> InstallationSource | None: - """Return a usable plugin source without treating site-packages as a checkout. + """Return plugin coordinates without treating site-packages as a checkout. Wheels exclude the integration marketplaces. PEP 610 metadata preserves the - origin of Git installs, including forks and requested branches or tags. + origin of Git installs, including forks and requested branches or tags. Package + index releases use the distribution's version at the official release tag. """ checkout = module_path.resolve().parents[3] if ( @@ -49,7 +50,16 @@ def installation_source(module_path: Path) -> InstallationSource | None: return InstallationSource(str(checkout)) try: - direct_url_text = importlib.metadata.distribution("powercontext").read_text("direct_url.json") + distribution = importlib.metadata.distribution("powercontext") + direct_url_text = distribution.read_text("direct_url.json") + if direct_url_text is None: + metadata = distribution.metadata + # PackageMetadata's protocol does not expose get(). + installed_version = metadata["Version"] if "Version" in metadata else None # noqa: SIM401 + if isinstance(installed_version, str) and re.fullmatch( + r"[0-9]+\.[0-9]+\.[0-9]+(?:(?:a|b|rc)[0-9]+)?", installed_version + ): + return InstallationSource("oceanbase/powercontext", f"powercontext-v{installed_version}") direct_url = json.loads(direct_url_text) if direct_url_text else None except (importlib.metadata.PackageNotFoundError, OSError, UnicodeError, ValueError): return None diff --git a/tests/test_config_cli.py b/tests/test_config_cli.py index b2597ca8d0..008eb9a70e 100644 --- a/tests/test_config_cli.py +++ b/tests/test_config_cli.py @@ -48,10 +48,10 @@ def test_init_creates_a_model_free_deployment_and_explains_capability_limits(tmp assert "the value is never printed" in result.output assert "Configuration" in result.output assert "Supported Coding Agents (choose one)" in result.output - for host, (name, setup, launch) in config_cli.AGENTS.items(): + for host, (name, launch) in config_cli.AGENTS.items(): assert name in result.output - if host != "dsh": - assert setup in result.output + assert f" {host} --source " in result.output + assert f"--env-file {environment}" in result.output assert launch in result.output values = config_cli.parse_environment(environment.read_text(encoding="utf-8")) assert "POWERCONTEXT_SERVER_INFERENCE_GENERATION_MODEL" not in values @@ -63,22 +63,6 @@ def test_init_creates_a_model_free_deployment_and_explains_capability_limits(tmp assert shown.exit_code == 0 -@pytest.mark.parametrize("installed", ["0.2.0", "1.0.0rc1", "1.0.0rc2", "1.0.0", "1.1.0", "0.2.1.dev1+g1234567"]) -def test_init_matches_dsh_setup_to_installed_server(installed: str, tmp_path: Path, monkeypatch) -> None: - monkeypatch.setattr(config_cli, "version", lambda _name: installed) - monkeypatch.setattr(config_cli, "collect_configuration", lambda **_kwargs: _configuration()) - result = CliRunner().invoke( - config_cli.app, ["init", "--template", "--output", str(tmp_path / "server.env")], input="\n" - ) - assert result.exit_code == 0 - command = next(line.strip() for line in result.output.splitlines() if "powercontext setup dsh" in line) - if installed in {"0.2.0", "1.0.0rc1", "1.0.0rc2", "1.0.0", "1.1.0"}: - assert command.endswith(f"--ref powercontext-v{installed}") - else: - assert "--source /path/to/matching-powercontext-checkout" in command - assert "--ref master" not in command - - def test_arbitrary_model_providers_and_environment_variables_are_not_rejected() -> None: configuration = _configuration( generation=config_cli.ModelSelection( diff --git a/tests/test_config_wizard.py b/tests/test_config_wizard.py index f9ea653252..0b453fb38f 100644 --- a/tests/test_config_wizard.py +++ b/tests/test_config_wizard.py @@ -269,7 +269,7 @@ def test_openclaw_next_steps_use_plugin_configuration_contract(tmp_path: Path) - client = parse_environment(output.read_text()) assert not any(name.startswith("POWERCONTEXT_OPENCLAW_") for name in client) steps = output.with_name("server.env.next-steps.md").read_text() - assert "powercontext setup openclaw" in steps + assert f"powercontext setup --env-file {output} openclaw" in steps assert "--server-url http://127.0.0.1:17429" in steps assert "plugins.entries.memory-powercontext.config.endpoint" in steps assert "plugins.entries.memory-powercontext.config.autoCapture true" in steps diff --git a/tests/test_config_wizard_installation.py b/tests/test_config_wizard_installation.py index 1412b38c05..0b427fc894 100644 --- a/tests/test_config_wizard_installation.py +++ b/tests/test_config_wizard_installation.py @@ -19,7 +19,9 @@ from pathlib import Path import pytest +from typer.testing import CliRunner +import powercontext.cli.config as config_cli import powercontext.cli.config_wizard as wizard from powercontext.cli.config_wizard_ui import WizardUI @@ -84,6 +86,39 @@ def test_checkout_instructions_keep_a_verified_local_source(tmp_path, monkeypatc assert "--ref" not in instructions +@pytest.mark.parametrize("installed", ["0.2.0", "1.0.0rc1", "1.0.0", "1.1.0"]) +def test_index_release_instructions_pin_every_agent_and_the_selected_file(tmp_path, monkeypatch, installed) -> None: + module = _installed_package(tmp_path, monkeypatch, None) + metadata = module.parents[2] / "powercontext-0.1.dist-info" / "METADATA" + metadata.write_text(f"Metadata-Version: 2.1\nName: powercontext\nVersion: {installed}\n") + monkeypatch.setattr(config_cli, "__file__", str(module)) + environment = tmp_path / "server.env" + + result = CliRunner().invoke(config_cli.app, ["init", "--template", "--output", str(environment)], input="\n") + + assert result.exit_code == 0, result.output + for agent in config_cli.AGENTS: + assert ( + f"powercontext setup --env-file {environment} {agent} --source oceanbase/powercontext " + f"--ref powercontext-v{installed}" + ) in result.output + instructions = _instructions() + for agent in ("codex", "claude-code"): + assert ( + f"powercontext setup {agent} --source oceanbase/powercontext --ref powercontext-v{installed}" + in instructions + ) + assert "--ref master" not in instructions + + +def test_unreleased_index_package_requires_an_explicit_matching_source(tmp_path, monkeypatch) -> None: + module = _installed_package(tmp_path, monkeypatch, None) + metadata = module.parents[2] / "powercontext-0.1.dist-info" / "METADATA" + metadata.write_text("Metadata-Version: 2.1\nName: powercontext\nVersion: 0.2.1.dev1+g1234567\n") + assert "powercontext setup codex --help" in _instructions() + assert "--ref master" not in _instructions() + + @pytest.mark.parametrize( "direct_url", [ diff --git a/tests/test_config_wizard_next_steps.py b/tests/test_config_wizard_next_steps.py index 9f9e53eca3..e4855aebfd 100644 --- a/tests/test_config_wizard_next_steps.py +++ b/tests/test_config_wizard_next_steps.py @@ -77,8 +77,9 @@ def test_scope_binding_reloads_client_environment_before_agent_installation(tmp_ steps = wizard._next_steps(state, tmp_path / ".env", client_file) creation = steps.index("POST http://127.0.0.1:8000/v1/scopes") - installation = steps.index("powercontext setup claude-code") + installation = steps.index(" claude-code --source") assert f". {client_file}" in steps[creation:installation] + assert f"powercontext setup --env-file {client_file} claude-code" in steps assert "Reload the edited client environment before starting a new Agent" in steps @@ -91,7 +92,7 @@ def test_startup_guidance_matches_personal_service_support(tmp_path, monkeypatch environment = tmp_path / ".env" steps = wizard._next_steps(state, environment, environment) creation = steps.index("POST http://127.0.0.1:8000/v1/scopes") - installation = steps.index("powercontext setup claude-code") + installation = steps.index(" claude-code --source") if platform == "win32": assert "powercontext server run --env-file" in steps[:creation] assert "powercontext service install" not in steps From afb17dff83bdf237d856070c134397faebc07430 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Mon, 2 Nov 2026 11:35:56 +0800 Subject: [PATCH 15/24] fix(install): keep bootstrap independent of CLI changes --- .github/workflows/native-service.yml | 34 +-- README.md | 18 +- README_CN.md | 17 +- README_JP.md | 16 +- docs/en/docs/get-started/install-and-run.md | 51 ++--- docs/en/docs/get-started/quickstart.md | 19 +- docs/en/rfcs/1892-script-installation.md | 69 ++---- docs/zh/docs/get-started/install-and-run.md | 43 ++-- docs/zh/docs/get-started/quickstart.md | 18 +- docs/zh/rfcs/1892-script-installation.md | 61 ++---- src/powercontext/cli/config.py | 75 ++++--- src/powercontext/cli/config_startup.py | 39 ---- src/powercontext/cli/config_wizard.py | 97 +++------ .../cli/config_wizard_installation.py | 18 +- src/powercontext/cli/system.py | 57 ----- src/powercontext/service/controller.py | 8 - tests/fixtures/installation/README.md | 9 +- tests/fixtures/installation/hosts.json | 57 +++++ tests/fixtures/installation/services.json | 200 ------------------ tests/native/test_install_sources.py | 20 +- tests/native/test_installation.py | 4 +- tests/native/test_service_installation.py | 145 ------------- tests/test_config_cli.py | 38 ++-- tests/test_config_wizard.py | 23 +- tests/test_config_wizard_installation.py | 35 --- tests/test_config_wizard_next_steps.py | 30 +-- tests/test_doctor_environment.py | 118 ----------- tests/test_installation_contract.py | 60 +----- tests/test_service.py | 15 -- website/public/install.ps1 | 66 ++---- website/public/install.sh | 63 ++---- 31 files changed, 350 insertions(+), 1173 deletions(-) delete mode 100644 src/powercontext/cli/config_startup.py delete mode 100644 tests/fixtures/installation/services.json delete mode 100644 tests/native/test_service_installation.py delete mode 100644 tests/test_doctor_environment.py diff --git a/.github/workflows/native-service.yml b/.github/workflows/native-service.yml index 724ba33dc7..50a1907fdc 100644 --- a/.github/workflows/native-service.yml +++ b/.github/workflows/native-service.yml @@ -49,8 +49,6 @@ jobs: echo "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus" >> "$GITHUB_ENV" - name: Install the built distribution in a fresh virtual environment - env: - SETUPTOOLS_SCM_PRETEND_VERSION: "1.2.0" shell: bash run: | uv build --wheel @@ -68,16 +66,6 @@ jobs: --basetemp "$RUNNER_TEMP/powercontext-native-tests" tests/native/test_personal_service_lifecycle.py - - name: Exercise script installation and service reconciliation - env: - POWERCONTEXT_INSTALL_WHEEL: ${{ github.workspace }}/dist/powercontext-1.2.0-py3-none-any.whl - shell: bash - run: >- - .native-venv/bin/python -m pytest -q - --junitxml "$RUNNER_TEMP/installer-service.xml" - --basetemp "$RUNNER_TEMP/installer-service-tests" - tests/native/test_service_installation.py - - name: Report Linux lifecycle failures if: failure() run: >- @@ -96,10 +84,7 @@ jobs: uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: linux-native-service-logs - path: | - ${{ runner.temp }}/powercontext-native-tests/**/logs/* - ${{ runner.temp }}/installer-service-tests/**/*.log - ${{ runner.temp }}/installer-service.xml + path: ${{ runner.temp }}/powercontext-native-tests/**/logs/* if-no-files-found: ignore macos-launchagent: @@ -120,8 +105,6 @@ jobs: uses: ./.github/actions/setup-python-env - name: Install the built distribution in a fresh virtual environment - env: - SETUPTOOLS_SCM_PRETEND_VERSION: "1.2.0" shell: bash run: | uv build --wheel @@ -141,16 +124,6 @@ jobs: --basetemp "$RUNNER_TEMP/powercontext-native-tests" tests/native/test_personal_service_lifecycle.py - - name: Exercise script installation and service reconciliation - env: - POWERCONTEXT_INSTALL_WHEEL: ${{ github.workspace }}/dist/powercontext-1.2.0-py3-none-any.whl - shell: bash - run: >- - .native-venv/bin/python -m pytest -q - --junitxml "$RUNNER_TEMP/installer-service.xml" - --basetemp "$RUNNER_TEMP/installer-service-tests" - tests/native/test_service_installation.py - - name: Report macOS lifecycle failures if: failure() run: >- @@ -167,10 +140,7 @@ jobs: uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: macos-native-service-logs - path: | - ${{ runner.temp }}/powercontext-native-tests/**/logs/* - ${{ runner.temp }}/installer-service-tests/**/*.log - ${{ runner.temp }}/installer-service.xml + path: ${{ runner.temp }}/powercontext-native-tests/**/logs/* if-no-files-found: ignore windows-task-scheduler: diff --git a/README.md b/README.md index e61fefd76e..031ed470c9 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,7 @@ also install its CLI. In the wizard, choose Basic memory for explicit saves and requires separate Generation and Embedding API credentials. For a local Server without model APIs, see [Install and run](docs/en/docs/get-started/install-and-run.md). macOS and Linux are supported; Windows support is `experimental`. +To provision Python and uv automatically, use the [installation script](docs/en/docs/get-started/install-and-run.md#install-with-the-recommended-script). Choose the path that matches your goal: @@ -183,15 +184,21 @@ Choose the path that matches your goal: uv tool install --force "powercontext[cli,server]==1.4.1" mkdir -p powercontext-config && cd powercontext-config powercontext config init --language en --output .env -powercontext config validate --env-file .env -powercontext server run --env-file .env # keep this terminal open ``` The wizard asks about storage, local or remote usage, memory capabilities, Dashboard/access, and Agent connections. SQLite is the simplest first storage choice; choose embedded seekdb only when you need it and can approve its dependency install. +After saving `.env`, validate it and register the personal service: + +```bash +powercontext config validate --env-file .env +powercontext service install --env-file .env +powercontext service status +``` + The wizard writes one `.env` environment file and `.env.next-steps.md`. These files can contain credentials; do not -commit them. Keep Server running; in another terminal, return to `powercontext-config`, reload the generated environment -file, and check the connection: +commit them. The native current-user service manages the Server lifecycle; Linux requires an +available `systemd --user` manager. Load the generated environment file and check the connection: ```bash set -a @@ -203,7 +210,8 @@ powercontext capabilities Continue with `.env.next-steps.md` to create and bind the selected Scopes. Use the real `scope_id` returned by the creation request; a planned `codex-...` title is not an ID. Changing directories does not create isolation; use the -same Server and Scope in the Dashboard and Agent. Then install the matching plugins and launch a new Agent session. +same Server and Scope in the Dashboard and Agent. After editing `.env`, rerun `powercontext service install --env-file .env` +to reconcile the service. Then install the matching plugins and launch a new Agent session. The [complete walkthrough](https://powercontext.oceanbase.io/en/docs/get-started/quickstart/) covers Codex and Claude Code, Dashboard login, SSH forwarding, HTTPS prerequisites, and observable acceptance checks. For example, the matching Codex installation is: diff --git a/README_CN.md b/README_CN.md index 6baede2fd5..f623a5ef5e 100644 --- a/README_CN.md +++ b/README_CN.md @@ -132,6 +132,7 @@ PowerContext 1.4.1 包含交互式配置向导。需要 Python 3.11+、Git 和 选择“完整记忆能力”则需要独立的 Generation、Embedding API 凭据。只想启动不依赖模型 API 的本地 Server 时, 请参考[安装与运行](docs/zh/docs/get-started/install-and-run.md)。 支持 macOS 和 Linux;Windows 支持为 `experimental`。 +需要自动安装 Python 和 uv 时,使用[安装脚本](docs/zh/docs/get-started/install-and-run.md#使用推荐安装脚本)。 请选择与你目标匹配的路径: @@ -147,13 +148,20 @@ PowerContext 1.4.1 包含交互式配置向导。需要 Python 3.11+、Git 和 uv tool install --force "powercontext[cli,server]==1.4.1" mkdir -p powercontext-config && cd powercontext-config powercontext config init --language zh --output .env -powercontext config validate --env-file .env -powercontext server run --env-file .env # 保持这个终端不关 ``` 向导会询问存储、本机或远程使用场景、记忆能力、Dashboard/访问控制和 Agent 连接。首次体验可以使用 SQLite;只有确实需要时再选择嵌入式 seekdb,并同意依赖安装。 -向导生成一个 `.env` 环境文件和 `.env.next-steps.md`。这些文件可能包含凭据,请勿提交到 Git。保持 Server 运行,在另一个终端回到 `powercontext-config` 目录,重新加载生成的环境文件并检查服务: +保存 `.env` 后,验证配置并注册个人服务: + +```bash +powercontext config validate --env-file .env +powercontext service install --env-file .env +powercontext service status +``` + +向导生成一个 `.env` 环境文件和 `.env.next-steps.md`。这些文件可能包含凭据,请勿提交到 Git。 +原生当前用户服务负责 Server 生命周期;Linux 需要可用的 `systemd --user` 管理器。加载生成的环境文件并检查服务: ```bash set -a @@ -164,7 +172,8 @@ powercontext capabilities ``` 接着按 `.env.next-steps.md` 创建并绑定 Scope。使用创建请求返回的真实 `scope_id`;向导规划的 `codex-...` 标题不是 ID。 -切换目录不会创建隔离;Dashboard 和 Agent 要使用同一个 Server 和 Scope。然后安装相同版本的插件,再打开新 Agent 会话。 +切换目录不会创建隔离;Dashboard 和 Agent 要使用同一个 Server 和 Scope。修改 `.env` 后,重新执行 +`powercontext service install --env-file .env` 以协调服务配置。然后安装相同版本的插件,再打开新 Agent 会话。 [完整安装流程](https://powercontext.oceanbase.io/zh/docs/get-started/quickstart/)包含 Codex、Claude Code、Dashboard 登录、SSH 隧道、HTTPS 前提及逐项验收。例如,匹配本版本的 Codex 安装命令是: ```bash diff --git a/README_JP.md b/README_JP.md index 02b3072eba..f79aaed130 100644 --- a/README_JP.md +++ b/README_JP.md @@ -121,6 +121,7 @@ Agent の入力 → Source の証拠 → Memory / Topic Memory → PreparedConte ``` PowerContext 1.4.1 には対話式セットアップウィザードが含まれています。Python 3.11+、Git、[uv](https://docs.astral.sh/uv/getting-started/installation/) が必要です。必要に応じて uv が Python を用意できます。Agent に接続するには、その CLI もインストールしてください。ウィザードでは、モデル API なしで明示的な保存と想起を試す場合は Basic memory、自動抽出を有効にする場合は Full memory を選択してください。Full memory には Generation と Embedding API の個別の認証情報が必要です。モデル API なしでローカル Server だけを起動する場合は[インストールと実行](docs/en/docs/get-started/install-and-run.md)を参照してください。macOS と Linux をサポートし、Windows のサポートは `experimental` です。 +Python と uv を自動で用意する場合は、[インストールスクリプト](docs/en/docs/get-started/install-and-run.md#install-with-the-recommended-script)を使ってください。 目的に合う経路を選択してください: @@ -136,13 +137,20 @@ PowerContext 1.4.1 には対話式セットアップウィザードが含まれ uv tool install --force "powercontext[cli,server]==1.4.1" mkdir -p powercontext-config && cd powercontext-config powercontext config init --language en --output .env -powercontext config validate --env-file .env -powercontext server run --env-file .env # このターミナルは開いたままにする ``` ウィザードでは、ストレージ、ローカルまたはリモート利用、メモリ機能、Dashboard/アクセス制御、Agent 接続を選択します。最初は SQLite が簡単です。組み込み seekdb は必要な場合だけ選び、依存関係のインストールを許可してください。 -ウィザードは `.env` と `.env.next-steps.md` を生成します。これらのファイルには認証情報が含まれることがあるため、コミットしないでください。Server を起動したまま、別のターミナルで `powercontext-config` に戻り、生成された環境ファイルを読み込んで接続を確認します。 +`.env` を保存してから、設定を検証してユーザーサービスを登録します。 + +```bash +powercontext config validate --env-file .env +powercontext service install --env-file .env +powercontext service status +``` + +ウィザードは `.env` と `.env.next-steps.md` を生成します。これらのファイルには認証情報が含まれることがあるため、コミットしないでください。 +Server のライフサイクルはネイティブのユーザーサービスが管理します。Linux では利用可能な `systemd --user` マネージャーが必要です。生成された環境ファイルを読み込んで接続を確認します。 ```bash set -a @@ -154,7 +162,7 @@ powercontext capabilities 続いて `.env.next-steps.md` の手順に従い、Scope を作成して紐付けます。作成リクエストが返す実際の `scope_id` を使ってください。 計画された `codex-...` タイトルは ID ではありません。ディレクトリを変えても分離は作られないため、Dashboard と Agent で同じ Server と -Scope を使います。その後、対応するプラグインをインストールして新しい Agent セッションを起動します。[完全な手順](https://powercontext.oceanbase.io/en/docs/get-started/quickstart/)では、Codex と Claude Code、Dashboard へのログイン、SSH 転送、HTTPS の前提条件、確認可能な受け入れチェックを扱っています。対応する Codex のインストールは次のとおりです。 +Scope を使います。`.env` を編集したら、`powercontext service install --env-file .env` を再実行してサービスの設定を反映します。その後、対応するプラグインをインストールして新しい Agent セッションを起動します。[完全な手順](https://powercontext.oceanbase.io/en/docs/get-started/quickstart/)では、Codex と Claude Code、Dashboard へのログイン、SSH 転送、HTTPS の前提条件、確認可能な受け入れチェックを扱っています。対応する Codex のインストールは次のとおりです。 ```bash powercontext setup codex --ref powercontext-v1.4.1 diff --git a/docs/en/docs/get-started/install-and-run.md b/docs/en/docs/get-started/install-and-run.md index fedf9f11d6..67ba5bf857 100644 --- a/docs/en/docs/get-started/install-and-run.md +++ b/docs/en/docs/get-started/install-and-run.md @@ -54,48 +54,41 @@ Client-only installations use endpoint settings in the [remote connection guide] ## Configure and install a personal service -For personal macOS/Linux use, the recommended lifecycle is a native current-user service. The installer can -explicitly compose configuration and service registration: +For personal macOS/Linux use, configure the local Server after installation: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- \ - --configure --service --env-file "$HOME/.config/powercontext/powercontext.env" +powercontext config init --output .env ``` -`--configure` opens the existing wizard through the controlling terminal and requires `--env-file`. Save the file -to continue; cancellation stops service and host setup. Choose loopback binding for a personal service. Linux needs -an available `systemd --user` manager. In a non-interactive environment, provide an existing protected file instead: +Save `.env` before continuing. If you cancel the wizard, stop here. Then validate the saved file and explicitly +register the native current-user service: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- \ - --service --env-file /path/to/powercontext.env +powercontext config validate --env-file .env +powercontext service install --env-file .env +powercontext service status ``` -The installer validates that file, runs `service install`, checks `service status`, and runs `doctor --env-file` -against the Server settings in the same file. An explicit `--host` also receives this file and the installed release -ref. Client-only installations reject `--configure` and `--service` before installation. `--env-file` alone can supply -connection settings to an explicitly selected host without installing a local service. - -Windows supports the same explicit options experimentally; `--service` opts into login auto-start. Run `--configure` -in an interactive console. Protect an existing file using the ACL steps in [Deploy the Server](../operate/deploy-server.md). +Choose loopback binding for a personal service. Linux needs an available `systemd --user` manager. Windows personal +services remain experimental; see [Deploy the Server](../operate/deploy-server.md) for file protection and login startup. -These options need an installed release providing `config init --require-write` and `doctor --env-file`. If the selected -release lacks them, the installer reports the failed stage and retains the Runtime; use a compatible release or the -separate configuration/service commands. Runtime installation, saved configuration, service registration, and Server -readiness are separate outcomes. `degraded` diagnostics return nonzero. A failed post-install stage does not roll back -packages, delete configuration, or remove an already committed service registration. - -After any file edit, including writing returned Scope IDs, reconcile using the original file: +Load the generated Client settings and check the running Server: ```bash -powercontext service install --env-file /path/to/powercontext.env -powercontext service status -powercontext doctor --env-file /path/to/powercontext.env +set -a +. ./.env +set +a +powercontext ready +powercontext capabilities ``` -`doctor --env-file` gives the file authority over shell defaults. A Server file selects its listener; a client-only -file selects its Client URL. `--server-url` can explicitly override the diagnostic target. Generated next-step instructions -cover Scope creation and Agent acceptance; a running service does not establish an Agent workflow. +After any file edit, including writing returned Scope IDs, rerun `service install --env-file` with the original file. +Use the same path if you saved the configuration elsewhere. A running service does not establish an Agent workflow; +continue with the [Quick Start](quickstart.md) for Scope creation and Agent acceptance. + +The installer only installs software and explicitly selected integrations. `--env-file PATH` can supply an existing +connection file to selected hosts through the existing `setup` command; configuration and service registration remain +separate CLI steps. ## Installer version selection diff --git a/docs/en/docs/get-started/quickstart.md b/docs/en/docs/get-started/quickstart.md index 9f2e0e5909..076654b735 100644 --- a/docs/en/docs/get-started/quickstart.md +++ b/docs/en/docs/get-started/quickstart.md @@ -14,6 +14,7 @@ and an installed Codex CLI. Full memory also needs working Generation and Embedd prepare their base URLs, model names, and API keys. Signing into a Codex or Claude subscription does not automatically provide these APIs to the PowerContext Server. Without separate model APIs, select basic memory to test explicit saves and recall; that does not enable automatic Topic Memory. +To provision Python and uv automatically, use the [installation script](install-and-run.md#install-with-the-recommended-script). ## 1. Install and open the wizard @@ -53,17 +54,19 @@ In this terminal, run: ```bash powercontext config validate --env-file .env -powercontext server run --env-file .env +powercontext service install --env-file .env +powercontext service status ``` -Keep the terminal running. Open the Dashboard URL printed by the wizard, using the port saved as +The native current-user service manages the Server lifecycle. Linux requires an available +`systemd --user` manager. Open the Dashboard URL printed by the wizard, using the port saved as `POWERCONTEXT_SERVER_HTTP_PORT` in `.env`. With authentication disabled, the page opens directly. Otherwise, sign in with the **Server token**, not a model API key. -An empty Dashboard is expected before you capture data. For operation after closing the terminal, stop the foreground -Server and install a [persistent personal service](../operate/deploy-server.md#run-a-persistent-personal-server) -with `powercontext service install --env-file .env` to reuse the same configuration. +An empty Dashboard is expected before you capture data. For development, debugging, temporary use, or systems without +a native user-service manager, use `powercontext server run --env-file .env`; stop the personal service first if it is +running. See [persistent personal services](../operate/deploy-server.md#run-a-persistent-personal-server). -Open another terminal, load the client settings, and check the running service: +Load the client settings and check the running service: ```bash cd ~/powercontext-demo @@ -90,9 +93,11 @@ The planned `codex-xxxxxxxx` value is a title, not an ID. For Claude Code, put t in `POWERCONTEXT_CLAUDE_SCOPE_ID`. Agents can have separate Scopes or explicitly share an existing one. Changing directories does not create isolation. Use the same Scope in the Dashboard and Agent during this check. -Reload the client settings and install the matching plugin: +After editing `.env`, reconcile the service, reload the client settings, and install the matching plugin: ```bash +powercontext service install --env-file .env +powercontext service status set -a . ./.env set +a diff --git a/docs/en/rfcs/1892-script-installation.md b/docs/en/rfcs/1892-script-installation.md index 5dad92b3ae..d41e23b2a9 100644 --- a/docs/en/rfcs/1892-script-installation.md +++ b/docs/en/rfcs/1892-script-installation.md @@ -16,9 +16,9 @@ The scripts provision missing uv and Python, install the selected Runtime profil existing Agent integration adapters with a tag matching the installed Runtime. Installation, configuration, diagnostics, and service operation have separate responsibilities. Installing software -does not configure inference providers, start a Server, or register a persistent service by default. Explicit -`--configure` and `--service` compose those existing CLI operations using one selected `--env-file`. The scripts and their -installation guide are the first recommended path in the READMEs and Quick Start. +does not configure inference providers, start a Server, or register a persistent service. Configuration and service +operation remain separate existing CLI steps. The installation guide documents the scripts alongside the version-pinned +uv commands used in the READMEs and Quick Start. # Motivation @@ -77,49 +77,24 @@ and never defaults to `master`. If a host installation fails, the Runtime remain nonzero with instructions to retry setup at that tag. Each adapter supplies its existing host-specific result. For personal macOS/Linux, configure a protected file, then run `powercontext service install --env-file .env`, -`powercontext service status`, and `powercontext doctor --env-file .env`. The native user manager owns the Server lifecycle. +`powercontext service status`, then load the generated Client settings for `powercontext ready` and `powercontext capabilities`. The native user manager owns the Server lifecycle. Development, debugging, temporary use, and unavailable managers keep the foreground `server run` path. Client-only installations follow the remote connection guide. Service registration remains explicit. -## Explicit configuration and personal service setup +## Configuration and personal services -The installer supports three composable options: +Configuration and service registration use the existing CLI in separate steps. Run `config init --output .env`, +save the configuration, then run `config validate --env-file .env`, `service install --env-file .env`, and `service status`. +Load the generated Client settings before running `ready` and `capabilities`. See the +[installation guide](../docs/get-started/install-and-run.md#configure-and-install-a-personal-service) for the full sequence. -| Option | Contract | -| --- | --- | -| `--configure` | Run `config init --require-write --output PATH` through a controlling terminal | -| `--service` | Validate the selected file, install the native user service, inspect status, and diagnose readiness | -| `--env-file PATH` | Select the same explicit configuration for configuration, service, diagnostics, and host setup | +The installer does not run the configuration wizard or manage a service. Cancelling the wizard cannot cause the +installer to continue with service registration. Service ownership, supported managers, protected files, login startup, +and recovery remain the existing service layer's responsibility. Windows personal services remain experimental. -```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- \ - --configure --service --env-file "$HOME/.config/powercontext/powercontext.env" -``` - -`--configure` and `--service` require the local profile and an explicit file. Unattended setup requires an existing -file and omits `--configure`. Invalid combinations, a missing existing file, and unavailable interactive input fail -before Runtime installation. The Bash wizard receives `/dev/tty` independently of the downloaded script's stdin; -PowerShell requires an interactive console. Ordinary installation remains unattended. - -The configuration CLI exposes `--require-write` without changing its normal cancellation behavior. With this option, -cancelled saving returns 130 and prevents service/host changes, including when a previous valid file remains present. -Both wrappers report saved configuration, validation, service verification, and any later failure separately. - -Service setup delegates protected-file loading, loopback restrictions, manager support, registration ownership, -reconciliation, and startup to the existing service layer. Windows remains experimental; the wrapper's explicit -`--service` also opts into login startup. A registered service is not sufficient for full completion: `service status` -must pass and `doctor --env-file` must report healthy readiness. Degraded readiness returns nonzero. - -`doctor --env-file` uses the strict Python environment loader and restores process settings afterwards. Server files -select the configured listener, independently of Client URLs used for SSH forwarding or stale caller defaults. -Client-only files select their Client URL; an explicit diagnostic `--server-url` takes priority. Shell does not source -the file or interpret its contents. Host setup receives `setup --env-file PATH HOST...` at the installed release tag. - -The installed release must provide the added CLI options; an older release reports the failed stage and leaves the -Runtime installed. Distribution publication must coordinate the package containing these options with the website. -After any file edit, including Scope IDs, or an upgrade, users rerun `service install --env-file` with the original file. -Generated configuration output and next-step instructions recommend this flow, including reconciliation after Scope -creation. A post-install failure retains packages, saved files, and committed registrations for explicit recovery. +`--env-file PATH` selects an existing connection file only for explicitly selected host setup. It is passed to +`setup --env-file PATH HOST...` at the installed release tag; the shell does not parse or source it. After editing a +service configuration, including Scope IDs, follow the service guide using the original file. ## Choose download sources @@ -160,9 +135,9 @@ into shell, add a standalone Python installer engine, or change Runtime/public H ## Version and profile semantics -- `latest` passes an unpinned profile requirement to `uv tool install --upgrade --reinstall-package powercontext --prerelease disallow`. It means the +- `latest` passes an unpinned profile requirement to `uv tool install --upgrade --force --prerelease disallow`. It means the newest stable version compatible with the selected interpreter and configured sources, not necessarily the version - most recently uploaded to another mirror. The PowerContext package is reinstalled so an existing prerelease cannot bypass stable selection. Resolution failures + most recently uploaded to another mirror. A fresh tool environment prevents an installed prerelease from bypassing stable selection in older uv versions. Resolution failures are reported without substituting another requirement. - An exact `X.Y.Z`, optionally followed by `aN`, `bN`, or `rcN`, uses `==VERSION`. Explicit prereleases are supported; source refs, ranges, and URLs are not `--version` values. Releases before 0.1.0 are excluded. @@ -199,8 +174,8 @@ downloads are cleaned. `UV_INSTALL_DIR`, `UV_TOOL_DIR`, and `UV_TOOL_BIN_DIR` re ## Persistence and compatibility Ordinary package installation does not read or rewrite `.env`, credentials, data directories, or database schemas, -and does not stop or restart a service. Explicit `--configure` asks the CLI to save only the selected file; explicit -`--service` asks the native layer to reconcile its owned registration and restart when its existing contract requires it. Users follow the existing upgrade and migration instructions when starting an updated +and does not stop or restart a service. Users configure and register services separately through the existing CLI, +and follow the existing upgrade and migration instructions when starting an updated Server. An exact-version retry may reuse cached packages; `latest` intentionally allows upgrades. `UV_OFFLINE=1` permits cached reinstallation only when uv, a compatible Python, and all dependencies are present. @@ -219,9 +194,9 @@ support still comes from each integration's capability contract. `tests/test_installation_contract.py` executes those cases with real uv and small offline fixture wheels: host opt-in, profiles, exact-version verification, unavailable commands, and partial host failure. Cases observe installed capabilities, selected hosts, release refs and exit results. They do not fix internal function boundaries or call order. -The same catalog runs on each native operating system; fixture host/service execution is not native service or Agent -acceptance. POSIX controlling-terminal cases cover a piped configuration request and cancellation. Native service CI -adds installed-script acceptance for persistent memory, environment-file reconciliation, and a stopped-service upgrade. +The same catalog runs on each native operating system; fixture host execution is not native service or Agent +acceptance. Host cases cover passing an existing connection file. Configuration and personal-service lifecycle +acceptance remain separate from installer conformance. `tests/native/test_installation.py` runs the actual shell installer, uv, installed PowerContext CLI, and HTTP Server. A wheel built from the tested commit uses release-shaped metadata and a direct local file constraint. That constraint diff --git a/docs/zh/docs/get-started/install-and-run.md b/docs/zh/docs/get-started/install-and-run.md index d1c0cf4d43..feda67b05e 100644 --- a/docs/zh/docs/get-started/install-and-run.md +++ b/docs/zh/docs/get-started/install-and-run.md @@ -50,42 +50,39 @@ PATH。`UV_INSTALL_DIR` 指定缺少 uv 时的安装目录,`UV_TOOL_BIN_DIR` ## 配置并安装个人服务 -个人 macOS/Linux 推荐使用原生当前用户服务。安装器可以显式串联配置向导和服务注册: +个人 macOS/Linux 安装完成后,先配置本地 Server: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- \ - --configure --service --env-file "$HOME/.config/powercontext/powercontext.env" +powercontext config init --output .env ``` -`--configure` 通过控制终端打开现有向导,需要显式 `--env-file`。保存后才能继续;取消会中止后续服务和宿主安装。 -个人服务选择 loopback 绑定;Linux 需要可用的 `systemd --user`。无人值守环境使用已有受保护文件: +保存 `.env` 后再继续;如果取消向导,到这里停止。随后验证已保存的文件,并显式注册原生当前用户服务: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- \ - --service --env-file /path/to/powercontext.env +powercontext config validate --env-file .env +powercontext service install --env-file .env +powercontext service status ``` -安装器验证配置,执行 `service install`、`service status`,再通过 `doctor --env-file` 检查同一文件中的 Server。 -显式 `--host` 也会使用该文件及实际安装版本对应的 ref。Client-only 模式在安装前拒绝 `--configure` 和 `--service`; -单独使用 `--env-file` 可以为显式宿主安装提供连接配置,而不安装本地服务。 - -Windows 的相同选项仍为试验性支持;`--service` 显式启用登录自启动。`--configure` 需要交互控制台。 -已有文件的 ACL 保护方式见[部署 Server](../operate/deploy-server.md)。 +个人服务选择 loopback 绑定;Linux 需要可用的 `systemd --user` 管理器。Windows 个人服务仍为试验性支持, +文件保护和登录自启动说明见[部署 Server](../operate/deploy-server.md)。 -这些选项要求所选发行版提供 `config init --require-write` 和 `doctor --env-file`。缺少时,安装器报告失败阶段并保留 -Runtime;使用兼容发行版,或分开执行配置与服务命令。Runtime 安装、配置保存、服务注册和 Server 就绪分别报告。 -`degraded` 诊断返回非零状态;后续失败不会回滚包文件、删除配置或移除已提交的服务注册。 - -任何文件修改,包括写入返回的 Scope ID 后,都要使用原文件重新注册: +加载生成的 Client 配置,检查运行中的 Server: ```bash -powercontext service install --env-file /path/to/powercontext.env -powercontext service status -powercontext doctor --env-file /path/to/powercontext.env +set -a +. ./.env +set +a +powercontext ready +powercontext capabilities ``` -`doctor --env-file` 以文件为准,覆盖调用者 shell 的默认配置。Server 文件选择其监听地址,Client-only 文件选择 Client URL; -显式 `--server-url` 可以覆盖诊断目标。生成的后续步骤仍负责引导 Scope 创建和 Agent 验收;服务运行不代表 Agent 工作流已完成。 +修改文件(包括写入返回的 Scope ID)后,使用原文件重新执行 `service install --env-file`。 +如果把配置保存在其他位置,始终使用同一路径。服务运行不代表 Agent 工作流已完成; +继续按[快速开始](quickstart.md)完成 Scope 创建和 Agent 验收。 + +安装器只安装软件及显式选择的集成。`--env-file PATH` 可以通过现有 `setup` 命令,为选定宿主提供已有连接文件; +配置和服务注册仍由用户分步执行现有 CLI 命令。 ## 选择脚本安装版本 diff --git a/docs/zh/docs/get-started/quickstart.md b/docs/zh/docs/get-started/quickstart.md index 070ec95785..5623ec4b33 100644 --- a/docs/zh/docs/get-started/quickstart.md +++ b/docs/zh/docs/get-started/quickstart.md @@ -13,6 +13,7 @@ description: 通过配置向导安装完整记忆能力,接入 Codex,并验 完整记忆还需要可用的 Generation 和 Embedding 模型 API;准备好各自的地址、模型名和 API key。 Codex 或 Claude 的订阅登录不会自动为 PowerContext Server 提供这些 API。只有 Agent 登录、没有模型 API 时, 仍可选择基础记忆,验收显式保存与召回,但不能据此期待自动 Topic Memory。 +需要自动安装 Python 和 uv 时,使用[安装脚本](install-and-run.md#使用推荐安装脚本)。 ## 1. 安装并进入配置向导 @@ -51,15 +52,18 @@ powercontext config init --language zh --output .env ```bash powercontext config validate --env-file .env -powercontext server run --env-file .env +powercontext service install --env-file .env +powercontext service status ``` -保持终端运行。在浏览器打开向导输出的 Dashboard 地址,端口以 `.env` 中保存的 +原生当前用户服务负责 Server 生命周期;Linux 需要可用的 `systemd --user` 管理器。 +在浏览器打开向导输出的 Dashboard 地址,端口以 `.env` 中保存的 `POWERCONTEXT_SERVER_HTTP_PORT` 为准。未启用认证时可直接进入页面;启用后使用 **Server Token** 登录,不是模型 API key。 -首次没有数据是正常现象。需要关闭终端后继续运行时,先停止前台 Server,再通过 -`powercontext service install --env-file .env` 安装[个人后台服务](../operate/deploy-server.md#运行持久个人-server),复用同一份配置。 +首次没有数据是正常现象。开发、排障、临时使用或没有原生用户服务管理器时,可使用 +`powercontext server run --env-file .env`;如个人服务正在运行,先将其停止。 +服务管理详见[个人后台服务](../operate/deploy-server.md#运行持久个人-server)。 -另开终端,加载客户端连接配置并检查服务: +加载客户端连接配置并检查服务: ```bash cd ~/powercontext-demo @@ -86,9 +90,11 @@ POWERCONTEXT_CODEX_SCOPE_ID=替换为返回的scope_id `POWERCONTEXT_CLAUDE_SCOPE_ID`。不同 Agent 可以各自隔离,也可以显式绑定同一已有 Scope。 切换目录本身不会创建隔离。验收期间,Dashboard 和 Agent 必须使用相同的 Scope。 -重新加载客户端文件并安装匹配的插件: +修改 `.env` 后,协调服务配置,重新加载客户端文件并安装匹配的插件: ```bash +powercontext service install --env-file .env +powercontext service status set -a . ./.env set +a diff --git a/docs/zh/rfcs/1892-script-installation.md b/docs/zh/rfcs/1892-script-installation.md index 45717fa5ca..becd67c5c5 100644 --- a/docs/zh/rfcs/1892-script-installation.md +++ b/docs/zh/rfcs/1892-script-installation.md @@ -14,9 +14,9 @@ description: 通过 Bash 和 PowerShell 安装最新或指定版本,独立管 `--version` 可以指定准确版本。脚本补齐缺少的 uv 和 Python,安装所选 Runtime profile,并按需调用现有 Agent 集成 适配器,使用与实际安装的 Runtime 对应的 tag。 -安装、配置、诊断和服务运行各自承担独立职责。默认安装软件不会配置推理服务、启动 Server 或注册持久服务。显式 `--configure` 和 `--service` 可以通过 -现有 CLI 串联这些操作,并使用同一份 `--env-file`。 -README 和快速开始将脚本及其安装指南作为首选路径。 +安装、配置、诊断和服务运行各自承担独立职责。安装软件不会配置推理服务、启动 Server 或注册持久服务; +配置和服务运行仍分步复用现有 CLI。 +安装指南说明脚本用法;README 和快速开始保留固定版本的 uv 命令,并提供脚本指南入口。 # Motivation @@ -68,43 +68,22 @@ curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1 宿主安装失败时,保留已安装的 Runtime,脚本返回非零状态,并提示按该 tag 重试 setup;各适配器提供其已有的宿主结果。 个人 macOS/Linux 配置受保护文件后,运行 `powercontext service install --env-file .env`、`powercontext service status` -和 `powercontext doctor --env-file .env`,由原生用户服务管理器负责生命周期。开发、调试、临时使用或缺少 manager 时, +再加载生成的 Client 配置,运行 `powercontext ready` 和 `powercontext capabilities`;由原生用户服务管理器负责生命周期。开发、调试、临时使用或缺少 manager 时, 保留前台 `server run`。Client-only 模式按远程连接指南设置地址和认证。持久服务仍由用户显式注册。 -## 显式配置与个人服务安装 +## 配置与个人服务 -安装器提供三个可以组合的选项: +配置和服务注册分步复用现有 CLI。先运行 `config init --output .env`,保存后再执行 +`config validate --env-file .env`、`service install --env-file .env` 和 `service status`。 +运行 `ready` 和 `capabilities` 前,加载生成的 Client 配置。 +完整步骤见[安装指南](../docs/get-started/install-and-run.md#配置并安装个人服务)。 -| 选项 | 契约 | -| --- | --- | -| `--configure` | 通过控制终端运行 `config init --require-write --output PATH` | -| `--service` | 验证所选文件,安装原生用户服务,检查状态与就绪 | -| `--env-file PATH` | 为配置、服务、诊断和宿主安装选择同一份显式文件 | +安装器不运行配置向导,也不管理服务。取消向导后,不会由安装器继续注册服务。 +服务归属、管理器支持、文件保护、登录自启动和恢复规则仍由现有服务层负责。Windows 个人服务保持试验性支持。 -```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- \ - --configure --service --env-file "$HOME/.config/powercontext/powercontext.env" -``` - -`--configure` 和 `--service` 要求 local profile 及显式文件。无人值守安装省略 `--configure`,提供已有文件。 -无效组合、缺少已有文件或交互终端时,在 Runtime 安装前失败。Bash 将 `/dev/tty` 单独交给向导,避免与下载脚本的 -stdin 混用;PowerShell 要求交互控制台。普通安装继续支持无人值守。 - -配置 CLI 增加 `--require-write`,保留独立使用时的取消行为。指定该选项时,取消保存返回 130,即使已有有效旧文件, -也不会继续修改服务或宿主。两个入口分别报告配置保存、验证、服务验证以及后续失败。 - -服务安装复用现有服务层的文件保护、loopback、manager 支持、注册归属、reconcile 和启动规则。 -Windows 仍为试验性;入口的显式 `--service` 同时同意登录自启动。完成注册后,还必须通过 `service status` 和 -`doctor --env-file` 的就绪诊断;`degraded` 返回非零状态。 - -`doctor --env-file` 复用严格的 Python 环境加载器,结束后恢复进程设置。Server 文件选择其监听地址,不受 SSH 转发 -所用 Client URL 或调用者旧配置影响;Client-only 文件选择 Client URL,显式诊断 `--server-url` 优先。 -Shell 不执行文件内容。宿主安装使用 `setup --env-file PATH HOST...`,并绑定实际安装版本对应的 tag。 - -所选发行版必须提供新增 CLI 选项;旧版缺少时,报告失败阶段并保留 Runtime。发布需要协调包含这些选项的包与网站。 -任何文件修改(包括 Scope ID)或升级后,使用原文件重新执行 `service install --env-file`。 -配置输出和后续步骤统一推荐这一路径,并在 Scope 创建后提示重新注册。后续失败保留包、已保存文件和已提交的服务注册, -供用户显式恢复。 +`--env-file PATH` 仅为显式选择的宿主安装指定已有连接文件,通过 +`setup --env-file PATH HOST...` 及实际安装版本对应的 tag 传入;Shell 不解析或执行文件内容。 +修改服务配置(包括 Scope ID)后,按服务指南使用原文件重新注册。 ## 选择下载来源 @@ -143,8 +122,8 @@ pip 的索引环境变量不属于 uv 配置。Python 和 uv 下载覆盖项与 ## 版本和 profile 语义 -- `latest` 向 `uv tool install --upgrade --reinstall-package powercontext --prerelease disallow` 传入未固定版本的 profile requirement。 - 它表示所选解释器及已配置源兼容的最新稳定版,不一定是另一镜像上最近上传的版本。重新安装 PowerContext 包,避免已有预发布版本绕过稳定版选择。解析失败直接报告,不改换 requirement。 +- `latest` 向 `uv tool install --upgrade --force --prerelease disallow` 传入未固定版本的 profile requirement。 + 它表示所选解释器及已配置源兼容的最新稳定版,不一定是另一镜像上最近上传的版本。重新解析工具环境,避免旧版 uv 中已有预发布版本绕过稳定版选择。解析失败直接报告,不改换 requirement。 - 准确版本使用 `==VERSION`,格式为 `X.Y.Z`,可追加 `aN`、`bN` 或 `rcN`。支持显式预发布版本; `--version` 不接受源码 ref、版本范围或 URL,也不支持 0.1.0 之前的版本。 - `local` 使用 `powercontext[cli,server]`;`client` 使用 `powercontext[cli]`,后者已经包含 Client 依赖。 @@ -172,8 +151,8 @@ Shell 环境变化局限于安装器进程;PowerShell 在 `finally` 恢复临 ## 持久化与兼容性 -普通包安装不读取或改写 `.env`、凭据、数据目录或数据库 schema,也不停止或重启服务。显式 `--configure` -由 CLI 保存所选文件;显式 `--service` 由原生层 reconcile 自有注册,并按既有契约在需要时重启。 +普通包安装不读取或改写 `.env`、凭据、数据目录或数据库 schema,也不停止或重启服务。 +配置和服务注册分步复用现有 CLI。 启动升级后的 Server 时,用户按现有升级和迁移指南操作。准确版本重试可以使用缓存;`latest` 则明确允许升级。 `UV_OFFLINE=1` 只支持已有 uv、兼容 Python 和全部依赖时的缓存重装,同时禁用安装器源探测和 uv 网络访问, @@ -188,8 +167,8 @@ Windows 保持产品的试验性支持状态。原生验收在 Linux、macOS 和 `tests/fixtures/installation/*.json` 定义两个 shell 适配器共享的公开输入与预期结果。 `tests/test_installation_contract.py` 使用真实 uv 和小型离线 fixture wheel 执行宿主显式选择、profile、准确版本 校验、命令缺失和宿主部分失败用例。用例观察安装后的能力、所选宿主、发行 ref 和退出结果,不固定内部函数边界或 -调用顺序。同一组用例在各操作系统原生执行;模拟宿主或服务执行不等于真实原生服务或 Agent 验收。POSIX 控制终端用例覆盖管道安装中的配置请求与取消; -原生服务 CI 通过安装后的脚本验证记忆持久化、文件修改后的重新注册和停止服务后的升级。 +调用顺序。同一组用例在各操作系统原生执行;模拟宿主执行不等于真实原生服务或 Agent 验收。 +宿主用例覆盖传递已有连接文件;配置和个人服务生命周期验收与安装器契约分开。 `tests/native/test_installation.py` 执行真实 shell 安装器、uv、安装后的 PowerContext CLI 和 HTTP Server。 从被测提交构建 wheel,使用发行版形式的元数据和直接本地文件约束。该约束选定被测产物,不构成独立执行的校验和验证。 diff --git a/src/powercontext/cli/config.py b/src/powercontext/cli/config.py index f0b52b4bb5..6bca3bf554 100644 --- a/src/powercontext/cli/config.py +++ b/src/powercontext/cli/config.py @@ -28,6 +28,7 @@ from dataclasses import dataclass from datetime import UTC, datetime from enum import StrEnum +from importlib.metadata import version from pathlib import Path from typing import TYPE_CHECKING, Annotated, Never from urllib.parse import urlsplit @@ -138,15 +139,35 @@ class ApiProtocol: _PROTOCOL_BY_ID = {protocol.identifier: protocol for protocol in API_PROTOCOLS} -AGENTS: dict[str, tuple[str, str]] = { - "codex": ("Codex", "codex"), - "claude-code": ("Claude Code", "claude"), - "dsh": ("DeepSeek Harness", "dsh web"), - "opencode": ("OpenCode", "opencode"), - "pi": ("Pi", "pi"), - "openclaw": ("OpenClaw", "openclaw"), - "hermes": ("Hermes", "hermes"), - "workbuddy": ("WorkBuddy", "重启 WorkBuddy"), +AGENTS: dict[str, tuple[str, str, str]] = { + "codex": ("Codex", "powercontext setup codex --source oceanbase/powercontext --ref master", "codex"), + "claude-code": ( + "Claude Code", + "powercontext setup claude-code --source oceanbase/powercontext --ref master", + "claude", + ), + "dsh": ("DeepSeek Harness", "powercontext setup dsh --source oceanbase/powercontext --ref master", "dsh web"), + "opencode": ( + "OpenCode", + "powercontext setup opencode --source oceanbase/powercontext --ref master", + "opencode", + ), + "pi": ("Pi", "powercontext setup pi --source oceanbase/powercontext --ref master", "pi"), + "openclaw": ( + "OpenClaw", + "powercontext setup openclaw --source oceanbase/powercontext --ref master", + "openclaw", + ), + "hermes": ( + "Hermes", + "powercontext setup hermes --source oceanbase/powercontext --ref master", + "hermes", + ), + "workbuddy": ( + "WorkBuddy", + "powercontext setup workbuddy --source oceanbase/powercontext --ref master", + "重启 WorkBuddy", + ), } # Input hints only, not a provider allowlist. Unknown prefixes can attach arbitrary variables. @@ -343,18 +364,13 @@ def init_command( template: Annotated[ bool, typer.Option("--template", help="Use the basic model-free template instead of the guided setup.") ] = False, - require_write: Annotated[ - bool, typer.Option(help="Exit with status 130 if configuration is cancelled without saving.") - ] = False, ) -> None: """Create a working configuration through a short guided setup.""" if not template: from powercontext.cli.config_wizard import run_wizard - written = run_wizard(output, language=None if language is None else language.value, advanced=advanced) - if require_write and not written: - raise typer.Exit(130) + run_wizard(output, language=None if language is None else language.value, advanced=advanced) return if output.exists() and not force: @@ -367,8 +383,6 @@ def init_command( content = update_environment_document(existing, configuration) if not _confirm_environment_write(output, existing=existing, updated=content): typer.echo("No changes written.") - if require_write: - raise typer.Exit(130) return backup = write_environment(output, content, backup=output.exists()) except (ConfigError, EnvironmentFileError, OSError, UnicodeError, ValidationError) as error: @@ -1087,11 +1101,7 @@ def _removes_inference_configuration(existing: str, updated: str) -> bool: def _print_next_steps(path: Path) -> None: - from powercontext.cli.config_startup import server_startup_commands - from powercontext.cli.config_wizard_installation import installation_source - quoted = shlex.quote(str(path.resolve())) - source = installation_source(Path(__file__)) typer.secho("\nEnvironment file", bold=True, fg=typer.colors.CYAN) typer.echo(f" Path {path.resolve()} (mode 0600)") typer.echo(" Authentication disabled by default.") @@ -1099,22 +1109,17 @@ def _print_next_steps(path: Path) -> None: " If Bearer authentication is enabled, read POWERCONTEXT_SERVER_AUTH_TOKEN from this file;" " the value is never printed." ) - typer.echo("\nStart Server:") - for command in server_startup_commands( - path.resolve(), - host=parse_environment(path.read_text(encoding="utf-8")).get("POWERCONTEXT_SERVER_HTTP_HOST", "127.0.0.1"), - ): - typer.echo(f" {command}") + typer.echo(f"\nStart Server:\n powercontext server run --env-file {quoted}") write_inference_capability_notice(generation_model=None, embedding_model=None) typer.secho("\nSupported Coding Agents (choose one):", bold=True, fg=typer.colors.CYAN) - for host, (name, launch) in AGENTS.items(): - setup = f"powercontext setup --env-file {quoted} {host}" - if source is None: - setup += " --help" - else: - setup += f" --source {shlex.quote(source.source)}" - if source.ref: - setup += f" --ref {shlex.quote(source.ref)}" + for host, (name, setup, launch) in AGENTS.items(): + if host == "dsh": + installed = version("powercontext") + setup = ( + f"powercontext setup dsh --source oceanbase/powercontext --ref powercontext-v{installed}" + if re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:(?:a|b|rc)[0-9]+)?", installed) + else "powercontext setup dsh --source /path/to/matching-powercontext-checkout" + ) typer.echo(f"\n{name}:\n {setup}") if launch.startswith("重启"): typer.echo(f" {launch}") diff --git a/src/powercontext/cli/config_startup.py b/src/powercontext/cli/config_startup.py deleted file mode 100644 index f596a8739c..0000000000 --- a/src/powercontext/cli/config_startup.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) 2026 OceanBase. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Startup guidance shared by configuration output and saved instructions.""" - -import shlex -import sys -from pathlib import Path - -from powercontext.transport import is_loopback_host - - -def personal_service_recommended(host: str) -> bool: - return sys.platform in {"darwin", "linux"} and is_loopback_host(host) - - -def server_startup_commands(path: Path, *, host: str = "127.0.0.1") -> list[str]: - quoted = shlex.quote(str(path)) - commands = [f"powercontext config validate --env-file {quoted}"] - if personal_service_recommended(host): - commands.extend(( - f"powercontext service install --env-file {quoted}", - "powercontext service status", - f"powercontext doctor --env-file {quoted}", - )) - else: - commands.append(f"powercontext server run --env-file {quoted}") - return commands diff --git a/src/powercontext/cli/config_wizard.py b/src/powercontext/cli/config_wizard.py index 3f2324f07c..cd11450382 100644 --- a/src/powercontext/cli/config_wizard.py +++ b/src/powercontext/cli/config_wizard.py @@ -111,7 +111,7 @@ def note(self, en: str, zh: str) -> None: self.ui.say(en, zh) -def run_wizard(output: Path, *, language: str | None = None, advanced: bool = False) -> bool: +def run_wizard(output: Path, *, language: str | None = None, advanced: bool = False) -> None: """Collect answers and write files only after explicit final confirmation.""" from powercontext.cli.config import ConfigError @@ -162,7 +162,7 @@ def run_wizard(output: Path, *, language: str | None = None, advanced: bool = Fa _agents(state) elif mode == "edit": _edit_modules(state) - return _finish(state, output, content) + _finish(state, output, content) except (ConfigError, EnvironmentFileError, OSError, ValueError) as error: if isinstance(error, (WizardInputError, EnvironmentFileError, OSError)): message = str(error) @@ -1119,7 +1119,7 @@ def _validate_files(state: Wizard, values: dict[str, str]) -> None: raise WizardInputError(message) from None -def _finish(state: Wizard, output: Path, original_content: str) -> bool: +def _finish(state: Wizard, output: Path, original_content: str) -> None: ui = state.ui ui.section("8. Review files", "8. 检查配置文件") @@ -1169,11 +1169,10 @@ def _finish(state: Wizard, output: Path, original_content: str) -> bool: typer.echo(f" {path}") if not ui.confirm("Save these files?", "保存这些文件?"): ui.say("No changes written.", "未写入任何文件。") - return False + return _save_bundle(state, bundle, snapshots) _finish_seekdb_install(state) _show_connection_details(state, output, values, notes_file) - return True def _save_bundle(state: Wizard, bundle: dict[Path, str], snapshots: dict[Path, str]) -> None: @@ -1232,10 +1231,8 @@ def _show_connection_details(state: Wizard, output: Path, values: dict[str, str] ui = state.ui ui.section("Connection details", "连接与下一步") if not state.client_only: - from powercontext.cli.config_startup import server_startup_commands - - for command in server_startup_commands(output, host=values.get(f"{SERVER}HTTP_HOST", "127.0.0.1")): - ui.say(f"Start Server: {command}", f"启动 Server:{command}") + command = f"powercontext server run --env-file {shlex.quote(str(output))}" + ui.say(f"Start Server: {command}", f"启动 Server:{command}") if state.ssh_tunnel_command: ui.say( f"Run on the client computer: {state.ssh_tunnel_command}", @@ -1276,50 +1273,8 @@ def _connection_base_url(state: Wizard, values: dict[str, str]) -> str: return f"http://{host}:{port}" -def _server_start_steps(state: Wizard, output: Path) -> list[str]: - from powercontext.cli.config_startup import personal_service_recommended, server_startup_commands - - if state.client_only: - return [] - ui = state.ui - personal_service = personal_service_recommended(state.values.get(f"{SERVER}HTTP_HOST", "127.0.0.1")) - lines: list[str] = [] - lines += [ - "```bash", - *server_startup_commands(output, host=state.values.get(f"{SERVER}HTTP_HOST", "127.0.0.1")), - "```", - "", - ] - if personal_service: - lines += [ - ui.text( - "The native user-service manager owns the Server lifecycle. After upgrades or any edit to " - "this environment file, rerun these commands with the same file. Use `powercontext server run` " - "for development or temporary foreground use after stopping the personal service.", - "原生用户服务管理器负责 Server 生命周期。升级或修改这份环境文件后,使用同一文件重新执行上述命令。" - "开发或临时前台运行可先停止个人服务,再使用 `powercontext server run`。", - ), - "", - ] - if state.values.get(f"{SERVER}AUTH_TOKEN"): - lines += [ - ui.text( - f"The Server token is stored as `POWERCONTEXT_SERVER_AUTH_TOKEN` in `{output}`. " - "Do not paste it into logs or issues.", - f"Server Token 保存在 `{output}` 的 `POWERCONTEXT_SERVER_AUTH_TOKEN` 中;请勿将其粘贴到日志或 Issue。", - ), - "", - ] - return lines - - def _next_steps(state: Wizard, output: Path, client_file: Path) -> str: - from powercontext.cli.config_startup import personal_service_recommended, server_startup_commands - ui = state.ui - personal_service = not state.client_only and personal_service_recommended( - state.values.get(f"{SERVER}HTTP_HOST", "127.0.0.1") - ) lines = [ "# " + ui.text("PowerContext next steps", "PowerContext 后续步骤"), "", @@ -1329,7 +1284,24 @@ def _next_steps(state: Wizard, output: Path, client_file: Path) -> str: ), "", ] - lines += _server_start_steps(state, output) + if not state.client_only: + lines += [ + "```bash", + f"powercontext config validate --env-file {shlex.quote(str(output))}", + f"powercontext server run --env-file {shlex.quote(str(output))}", + "```", + "", + ] + if state.values.get(f"{SERVER}AUTH_TOKEN"): + lines += [ + ui.text( + f"The Server token is stored as `POWERCONTEXT_SERVER_AUTH_TOKEN` in `{output}`. " + "Do not paste it into logs or issues.", + f"Server Token 保存在 `{output}` 的 `POWERCONTEXT_SERVER_AUTH_TOKEN` 中;" + "请勿将其粘贴到日志或 Issue。", + ), + "", + ] if state.agents or state.client_only: if state.scenario != "local": lines += [ @@ -1372,18 +1344,6 @@ def _next_steps(state: Wizard, output: Path, client_file: Path) -> str: lines += _alternate_client_check_steps(state) if state.planned_scopes: lines += _scope_creation_steps(state) - if personal_service: - lines += [ - ui.text( - "After saving the returned Scope IDs, reconcile the service's environment-file identity:", - "保存返回的 Scope ID 后,重新注册服务以更新环境文件身份:", - ), - "", - "```bash", - *server_startup_commands(output, host=state.values.get(f"{SERVER}HTTP_HOST", "127.0.0.1")), - "```", - "", - ] lines += [ ui.text( "After saving the returned Scope IDs in the environment file, reload it in each Agent's " @@ -1400,7 +1360,7 @@ def _next_steps(state: Wizard, output: Path, client_file: Path) -> str: "```", "", ] - lines += _agent_installation_steps(state, env_file=client_file) + lines += _agent_installation_steps(state) if "profile" in state.features: lines += _profile_policy_steps(state) lines += ["## " + ui.text("Checks and pending steps", "验收与待完成项"), ""] @@ -1554,7 +1514,7 @@ def _profile_policy_script(address: str, scope_name: str, authorization_name: st """).splitlines() -def _agent_installation_steps(state: Wizard, *, env_file: Path | None = None) -> list[str]: +def _agent_installation_steps(state: Wizard) -> list[str]: if not state.agents: return [] source = installation_source(Path(__file__)) @@ -1578,10 +1538,7 @@ def _agent_installation_steps(state: Wizard, *, env_file: Path | None = None) -> if source is None: lines.append(f"powercontext setup {agent} --help") continue - command = "powercontext setup" - if env_file is not None: - command += f" --env-file {shlex.quote(str(env_file))}" - command += f" {agent} --source {shlex.quote(source.source)}" + command = f"powercontext setup {agent} --source {shlex.quote(source.source)}" if source.ref: command += f" --ref {shlex.quote(source.ref)}" if spec.setup_server_url: diff --git a/src/powercontext/cli/config_wizard_installation.py b/src/powercontext/cli/config_wizard_installation.py index 5a9602a84e..3bd832fe5b 100644 --- a/src/powercontext/cli/config_wizard_installation.py +++ b/src/powercontext/cli/config_wizard_installation.py @@ -27,18 +27,17 @@ @dataclass(frozen=True) class InstallationSource: - """A verified checkout, preserved Git origin, or tagged package-index release.""" + """A verified checkout or the Git source and requested ref of an installation.""" source: str ref: str | None = None def installation_source(module_path: Path) -> InstallationSource | None: - """Return plugin coordinates without treating site-packages as a checkout. + """Return a usable plugin source without treating site-packages as a checkout. Wheels exclude the integration marketplaces. PEP 610 metadata preserves the - origin of Git installs, including forks and requested branches or tags. Package - index releases use the distribution's version at the official release tag. + origin of Git installs, including forks and requested branches or tags. """ checkout = module_path.resolve().parents[3] if ( @@ -50,16 +49,7 @@ def installation_source(module_path: Path) -> InstallationSource | None: return InstallationSource(str(checkout)) try: - distribution = importlib.metadata.distribution("powercontext") - direct_url_text = distribution.read_text("direct_url.json") - if direct_url_text is None: - metadata = distribution.metadata - # PackageMetadata's protocol does not expose get(). - installed_version = metadata["Version"] if "Version" in metadata else None # noqa: SIM401 - if isinstance(installed_version, str) and re.fullmatch( - r"[0-9]+\.[0-9]+\.[0-9]+(?:(?:a|b|rc)[0-9]+)?", installed_version - ): - return InstallationSource("oceanbase/powercontext", f"powercontext-v{installed_version}") + direct_url_text = importlib.metadata.distribution("powercontext").read_text("direct_url.json") direct_url = json.loads(direct_url_text) if direct_url_text else None except (importlib.metadata.PackageNotFoundError, OSError, UnicodeError, ValueError): return None diff --git a/src/powercontext/cli/system.py b/src/powercontext/cli/system.py index 31fe31f609..7ff21b25d0 100644 --- a/src/powercontext/cli/system.py +++ b/src/powercontext/cli/system.py @@ -40,7 +40,6 @@ from pydantic import ValidationError from powercontext.cli.dsh_runtime import DshProfile, resolve_dsh_target -from powercontext.cli.env_file import EnvironmentFileError, environment_context, read_environment_file from powercontext.cli.hosts import stdin_is_tty from powercontext.cli.installation import SetupError, setup from powercontext.cli.transport import is_remote_http @@ -277,20 +276,8 @@ def doctor( session_id: Annotated[str | None, typer.Option(help="Exact Kimi session.")] = None, runtime_data_dir: Annotated[Path | None, typer.Option(help="Actual ZCode plugin data directory.")] = None, prepare: Annotated[bool, typer.Option(help="Probe ZCode readonly context preparation.")] = False, - env_file: Annotated[ - Path | None, typer.Option(help="Diagnose the Server defined in this file, independently of shell defaults.") - ] = None, ) -> None: """Check selected integrations, or saved integrations and the environment by default.""" - if env_file is not None: - if agents: - message = "Run doctor without an Agent selection when using --env-file." - raise typer.BadParameter(message, param_hint="--env-file") - _doctor_environment_file( - env_file, server_url=server_url, allow_insecure_http=allow_insecure_http, json_output=json_output - ) - return - from powercontext.cli.hosts import ( diagnostic_result, doctor_checks, @@ -381,50 +368,6 @@ def _codex_endpoint_updates(marketplace: str, plugin_version: str, server_url: s ) from error -def _doctor_environment_file( - path: Path, *, server_url: str | None, allow_insecure_http: bool | None, json_output: bool -) -> None: - try: - checks = run_environment_diagnostics(path, server_url=server_url, allow_insecure_http=allow_insecure_http) - except (EnvironmentFileError, OSError, ValueError) as error: - typer.echo("Cannot diagnose the environment file; check its path, assignments, and Server settings.", err=True) - raise typer.Exit(2) from error - _write_diagnostics(checks, json_output=json_output) - if not _diagnostics_ok(checks): - raise typer.Exit(code=1) - - -def run_environment_diagnostics( - path: Path, *, server_url: str | None = None, allow_insecure_http: bool | None = None -) -> dict[str, Diagnostic]: - """Observe the same Server configuration used by a personal service.""" - - loaded = read_environment_file(path.expanduser()) - cleared = { - name - for name in os.environ - if name.casefold().startswith(("powercontext_client_", "powercontext_server_")) or name == "POWERCONTEXT_HOME" - } - with environment_context(loaded, override=True, clear=cleared): - if server_url is None and any(name.casefold().startswith("powercontext_server_") for name in loaded): - from powercontext.server.configuration import server_settings_context - - with server_settings_context(environment=loaded) as settings: - host = settings.http.host - if host == "0.0.0.0": # noqa: S104 - diagnose wildcard binds via loopback - host = "127.0.0.1" - elif host in {"::", "[::]"}: - host = "::1" - if ":" in host and not host.startswith("["): - host = f"[{host}]" - server_url = f"http://{host}:{settings.http.port}" - else: - server_url = server_url or next( - (value for name, value in loaded.items() if name.casefold() == "powercontext_client_server_url"), None - ) - return run_diagnostics(server_url=server_url, allow_insecure_http=allow_insecure_http) - - def run_diagnostics(*, server_url: str | None = None, allow_insecure_http: bool | None = None) -> dict[str, Diagnostic]: """Collect installed-environment diagnostics without changing state.""" diff --git a/src/powercontext/service/controller.py b/src/powercontext/service/controller.py index 8f7875c604..0c349f1fab 100644 --- a/src/powercontext/service/controller.py +++ b/src/powercontext/service/controller.py @@ -1002,14 +1002,6 @@ def preview_definition(self, env_file: Path | None = None, *, start_on_login: bo raise ServiceError( # noqa: TRY003 f"invalid personal service configuration: {error}", exit_code=2 ) from error - if loaded_env is not None and ( - any(name.casefold().startswith("powercontext_client_") for name in loaded_env.values) - and not any(name.casefold().startswith("powercontext_server_") for name in loaded_env.values) - ): - raise ServiceError( # noqa: TRY003 - "the selected environment file contains only Client settings; configure a local Server before installing a service", - exit_code=2, - ) if loaded_env is None: inherited = sorted( name diff --git a/tests/fixtures/installation/README.md b/tests/fixtures/installation/README.md index 5ada199593..37e9948c3b 100644 --- a/tests/fixtures/installation/README.md +++ b/tests/fixtures/installation/README.md @@ -20,16 +20,12 @@ Each file has `schema_version: 1` and a `cases` array. Case IDs must be unique a | `setup_exit` | Exit status of the fixture host operation; defaults to success | | `reported_version` | Fixture CLI version; defaults to the wheel's `1.2.0` | | `environment_file` | Create a protected existing configuration file for the case | -| `terminal` | Run a piped Bash installer through a POSIX controlling terminal | -| `config_exit`, `validate_exit`, `service_exit`, `doctor_exit` | Exit status of the fixture operation | | `missing_commands` | Public command help paths omitted by the fixture | | `expected.exit_code` | Installer completion or failure | | `expected.runtime_installed` | Whether uv installed the tool launcher, including verification failures | | `expected.server_extra` | Whether the selected profile contains the fixture Server dependency | | `expected.selected_hosts` | Hosts actually submitted to setup; defaults to none | | `expected.setup_ref` | Release tag actually submitted to setup | -| `expected.configuration_saved` | Whether interactive configuration wrote its selected file | -| `expected.service`, `expected.doctor` | Whether registration and same-file diagnostics produced their fixture results | | `expected.setup_env_file` | Whether host setup received the selected configuration file | | `expected.verification_failed` | Whether success must remain unannounced | | `expected.error_contains` | Required diagnostic fragment | @@ -43,6 +39,5 @@ The installation CI matrix runs these cases on Linux, macOS and Windows. The sam PowerContext wheel and runs `tests/native/test_installation.py` for bootstrap, configuration, HTTP readiness, Memory persistence, profile replacement and version selection. `tests/native/test_install_sources.py` adds Linux loopback index fault tests using the real downloader and uv, with only distribution endpoints redirected to fixtures. -Those source tests do not qualify public mirror availability or native Windows source handling. Native service CI -also runs `tests/native/test_service_installation.py` on disposable Linux/macOS runners with the built wheel, a real -user-service manager, configuration reconciliation, an offline stopped-service upgrade, and persistent Memory readback. +Those source tests do not qualify public mirror availability or native Windows source handling. Personal-service +lifecycle acceptance remains in the existing native service workflow, separate from installer conformance. diff --git a/tests/fixtures/installation/hosts.json b/tests/fixtures/installation/hosts.json index cfd3007aa5..fc650a6fe0 100644 --- a/tests/fixtures/installation/hosts.json +++ b/tests/fixtures/installation/hosts.json @@ -106,6 +106,63 @@ "selected_hosts": [], "error_contains": "requires Git" } + }, + { + "id": "host-uses-existing-file", + "environment_file": true, + "git": true, + "args": [ + "--host", + "codex", + "--env-file", + "{env_file}" + ], + "expected": { + "exit_code": 0, + "runtime_installed": true, + "selected_hosts": [ + "codex" + ], + "setup_ref": "powercontext-v1.2.0", + "setup_env_file": true + } + }, + { + "id": "host-requires-existing-file", + "git": true, + "args": [ + "--host", + "codex", + "--env-file", + "{env_file}" + ], + "expected": { + "exit_code": 1, + "runtime_installed": false, + "error_contains": "existing file" + } + }, + { + "id": "unsupported-configure", + "args": [ + "--configure" + ], + "expected": { + "exit_code": 1, + "runtime_installed": false, + "error_contains": "Unknown option" + } + }, + { + "id": "unsupported-service", + "args": [ + "--service" + ], + "expected": { + "exit_code": 1, + "runtime_installed": false, + "error_contains": "Unknown option" + } } ] } diff --git a/tests/fixtures/installation/services.json b/tests/fixtures/installation/services.json deleted file mode 100644 index 8ed88773ba..0000000000 --- a/tests/fixtures/installation/services.json +++ /dev/null @@ -1,200 +0,0 @@ -{ - "schema_version": 1, - "cases": [ - { - "id": "service-existing-file", - "environment_file": true, - "args": [ - "--service", - "--env-file", - "{env_file}" - ], - "expected": { - "exit_code": 0, - "runtime_installed": true, - "service": true, - "doctor": true - } - }, - { - "id": "service-registration-failure", - "environment_file": true, - "args": [ - "--service", - "--env-file", - "{env_file}" - ], - "service_exit": 1, - "expected": { - "exit_code": 1, - "runtime_installed": true, - "service": true, - "error_contains": "personal service setup did not complete" - } - }, - { - "id": "service-readiness-failure", - "environment_file": true, - "args": [ - "--service", - "--env-file", - "{env_file}" - ], - "doctor_exit": 1, - "expected": { - "exit_code": 1, - "runtime_installed": true, - "service": true, - "doctor": true, - "error_contains": "Server diagnostics did not pass" - } - }, - { - "id": "invalid-service-configuration", - "environment_file": true, - "args": [ - "--service", - "--env-file", - "{env_file}" - ], - "validate_exit": 2, - "expected": { - "exit_code": 1, - "runtime_installed": true, - "error_contains": "configuration validation failed" - } - }, - { - "id": "service-and-host-use-same-file", - "environment_file": true, - "git": true, - "args": [ - "--service", - "--env-file", - "{env_file}", - "--host", - "codex" - ], - "expected": { - "exit_code": 0, - "runtime_installed": true, - "service": true, - "doctor": true, - "selected_hosts": [ - "codex" - ], - "setup_ref": "powercontext-v1.2.0", - "setup_env_file": true - } - }, - { - "id": "client-rejects-local-service", - "environment_file": true, - "args": [ - "--profile", - "client", - "--service", - "--env-file", - "{env_file}" - ], - "expected": { - "exit_code": 1, - "runtime_installed": false, - "error_contains": "require --profile local" - } - }, - { - "id": "service-requires-explicit-file", - "args": [ - "--service" - ], - "expected": { - "exit_code": 1, - "runtime_installed": false, - "error_contains": "require --env-file" - } - }, - { - "id": "service-requires-existing-file", - "args": [ - "--service", - "--env-file", - "{env_file}" - ], - "expected": { - "exit_code": 1, - "runtime_installed": false, - "error_contains": "existing file" - } - }, - { - "id": "configuration-requires-terminal", - "args": [ - "--configure", - "--service", - "--env-file", - "{env_file}" - ], - "expected": { - "exit_code": 1, - "runtime_installed": false, - "error_contains": "requires a terminal" - } - }, - { - "id": "piped-configuration-and-service", - "terminal": true, - "args": [ - "--configure", - "--service", - "--env-file", - "{env_file}" - ], - "expected": { - "exit_code": 0, - "runtime_installed": true, - "configuration_saved": true, - "service": true, - "doctor": true - } - }, - { - "id": "cancelled-configuration-stops-service", - "terminal": true, - "config_exit": 130, - "args": [ - "--configure", - "--service", - "--env-file", - "{env_file}" - ], - "expected": { - "exit_code": 1, - "runtime_installed": true, - "configuration_saved": false, - "error_contains": "configuration did not complete" - } - }, - { - "id": "existing-configuration-cancellation-stops-hosts", - "terminal": true, - "environment_file": true, - "git": true, - "config_exit": 130, - "args": [ - "--configure", - "--service", - "--env-file", - "{env_file}", - "--host", - "codex" - ], - "expected": { - "exit_code": 1, - "runtime_installed": true, - "selected_hosts": [], - "error_contains": "configuration did not complete" - } - } - ] -} diff --git a/tests/native/test_install_sources.py b/tests/native/test_install_sources.py index 041c4c0832..2c7cde7eb6 100644 --- a/tests/native/test_install_sources.py +++ b/tests/native/test_install_sources.py @@ -177,16 +177,28 @@ def test_encoded_release_installs_through_public_script(installation, region): assert "Runtime installed: 1.0.0" in output -def test_latest_stable_and_explicit_prerelease(installation): +@pytest.mark.parametrize("region", ["global", "cn"]) +def test_latest_stable_and_explicit_prerelease(installation, region): install, version, _index, _env = installation - install("--region", "cn") + install("--region", region) assert version() == "1.1.0" - install("--region", "cn", "--version", "1.2.0rc1") + install("--region", region, "--version", "1.2.0rc1") assert version() == "1.2.0rc1" - install("--region", "cn") + install("--region", region) assert version() == "1.1.0" +def test_latest_without_a_stable_release_preserves_installed_prerelease(installation): + install, version, index, _env = installation + install("--region", "cn", "--version", "1.2.0rc1") + index.mirror_versions = ["1.2.0rc1"] + + output = install("--region", "cn", success=False) + + assert "Installation failed" in output + assert version() == "1.2.0rc1" + + def test_unavailable_automatic_mirror_falls_back_before_tool_install(installation): install, version, index, _env = installation index.mirror_status = 503 diff --git a/tests/native/test_installation.py b/tests/native/test_installation.py index 5b9c20d39b..ea78e9b39a 100644 --- a/tests/native/test_installation.py +++ b/tests/native/test_installation.py @@ -266,7 +266,7 @@ def test_install_configure_remember_and_reinstall(tmp_path: Path, environment: s request(url + "/v1/memory/remember", token, {"scope_id": scope, "kind": "decision", "text": text}) query = {"scope_id": scope, "query": "release decision", "mode": "fts"} found = request(url + "/v1/memory/search", token, query) - assert text in [hit["text"] for hit in found["hits"]] + assert text in [hit["memory"]["text"] for hit in found["hits"]] env.update( UV_OFFLINE="1", UV_PYTHON_DOWNLOADS="never", POWERCONTEXT_UV_INSTALLER_URL="https://127.0.0.1:9/unavailable" ) @@ -276,7 +276,7 @@ def test_install_configure_remember_and_reinstall(tmp_path: Path, environment: s assert (tmp_path / ".env").read_bytes() == config with running_server(cli, tmp_path, env, token, "server-restarted") as url: found = request(url + "/v1/memory/search", token, query) - assert text in [hit["text"] for hit in found["hits"]] + assert text in [hit["memory"]["text"] for hit in found["hits"]] def test_latest_exact_versions_and_client_profile(tmp_path: Path) -> None: diff --git a/tests/native/test_service_installation.py b/tests/native/test_service_installation.py deleted file mode 100644 index a2d7cc9135..0000000000 --- a/tests/native/test_service_installation.py +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright (c) 2026 OceanBase. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Installed-script acceptance on disposable native macOS/Linux user-service runners.""" - -from __future__ import annotations - -import json -import os -import shutil -import socket -import subprocess -import sys -from pathlib import Path - -import httpx -import pytest - -from powercontext.cli.config import write_environment - -ROOT = Path(__file__).resolve().parents[2] -WHEEL = os.environ.get("POWERCONTEXT_INSTALL_WHEEL") -pytestmark = [ - pytest.mark.native_service, - pytest.mark.skipif( - os.environ.get("POWERCONTEXT_RUN_NATIVE_SERVICE_TESTS") != "1" - or not WHEEL - or sys.platform not in {"linux", "darwin"}, - reason="requires a release-shaped wheel and a disposable native user-service runner", - ), -] - - -def test_installer_service_reconciles_configuration_and_preserves_memory_after_upgrade(tmp_path: Path) -> None: - assert WHEEL is not None - uv = shutil.which("uv") - assert uv is not None - environment = { - key: value - for key, value in os.environ.items() - if not key.startswith(("POWERCONTEXT_", "UV_", "PIP_", "PYTHON")) and key != "VIRTUAL_ENV" - } - constraints = tmp_path / "constraints.txt" - constraints.write_text(f"powercontext @ {Path(WHEEL).resolve().as_uri()}\n") - environment.update( - UV_CACHE_DIR=os.environ.get("UV_CACHE_DIR", str(Path.home() / ".cache" / "uv")), - UV_NO_CONFIG="1", - UV_OFFLINE="1", - UV_PYTHON_DOWNLOADS="never", - UV_CONSTRAINT=str(constraints), - UV_TOOL_DIR=str(tmp_path / "tools"), - UV_TOOL_BIN_DIR=str(tmp_path / "bin"), - ) - cli = str(tmp_path / "bin" / "powercontext") - configuration = tmp_path / "personal server.env" - with socket.socket() as listener: - listener.bind(("127.0.0.1", 0)) - port = listener.getsockname()[1] - token = "native-installation-token" # noqa: S105 - content = ( - f"POWERCONTEXT_HOME='{tmp_path / 'data'}'\n" - "POWERCONTEXT_SERVER_HTTP_HOST=127.0.0.1\n" - f"POWERCONTEXT_SERVER_HTTP_PORT={port}\n" - "POWERCONTEXT_SERVER_ACCESS_MODE=enforced\n" - f"POWERCONTEXT_SERVER_AUTH_TOKEN={token}\n" - ) - write_environment(configuration, content, backup=False) - installer = ["/bin/bash", str(ROOT / "website/public/install.sh"), "--region", "global"] - - def run(command: list[str], name: str) -> str: - result = subprocess.run(command, env=environment, capture_output=True, text=True, timeout=300) - output = result.stdout + result.stderr - (tmp_path / f"{name}.log").write_text(output) - assert result.returncode == 0, output - return result.stdout - - # This runner is disposable. Never replace an existing personal service. - before = subprocess.run( - [str(Path(sys.executable).with_name("powercontext")), "service", "status", "--json"], - env=environment, - capture_output=True, - text=True, - timeout=30, - ) - assert json.loads(before.stdout)["registration"] == "not_installed", before.stdout + before.stderr - try: - output = run([*installer, "--version", "1.2.0", "--service", "--env-file", str(configuration)], "install") - assert "Personal service verified with:" in output - status = json.loads(run([cli, "service", "status", "--json"], "status")) - assert status["manager"] == "active" - assert status["server_liveness"] == "live" - endpoint = f"http://127.0.0.1:{port}" - with httpx.Client(base_url=endpoint, headers={"Authorization": f"Bearer {token}"}, trust_env=False) as client: - scope = client.get("/v1/scopes/default").json()["scope_id"] - response = client.post( - "/v1/memory/remember", - json={"scope_id": scope, "kind": "decision", "text": "Keep native service memory."}, - ) - response.raise_for_status() - - content += "POWERCONTEXT_CODEX_SCOPE_ID=scope_example\n" - write_environment(configuration, content, backup=False) - # Stale status intentionally exits nonzero; inspect its public JSON separately. - stale = subprocess.run( - [cli, "service", "status", "--json"], env=environment, capture_output=True, text=True, timeout=30 - ) - assert stale.returncode == 1 - assert json.loads(stale.stdout)["definition"] == "stale" - run([*installer, "--version", "1.2.0", "--service", "--env-file", str(configuration)], "reconcile") - - run([cli, "service", "uninstall"], "stop-before-upgrade") - upgraded = tmp_path / "upgraded" - build = subprocess.run( - [uv, "build", "--offline", "--wheel", "--out-dir", str(upgraded)], - cwd=ROOT, - env={**environment, "SETUPTOOLS_SCM_PRETEND_VERSION": "1.2.1"}, - capture_output=True, - text=True, - timeout=300, - ) - assert build.returncode == 0, build.stdout + build.stderr - constraints.write_text(f"powercontext @ {next(upgraded.glob('*.whl')).as_uri()}\n") - run([*installer, "--version", "1.2.1", "--service", "--env-file", str(configuration)], "upgrade") - assert run([cli, "--version"], "upgraded-version").strip() == "1.2.1" - assert configuration.read_text() == content - with httpx.Client(base_url=endpoint, headers={"Authorization": f"Bearer {token}"}, trust_env=False) as client: - response = client.post( - "/v1/memory/search", json={"scope_id": scope, "query": "native service memory", "mode": "fts"} - ) - response.raise_for_status() - assert any(hit["text"] == "Keep native service memory." for hit in response.json()["hits"]) - finally: - if Path(cli).exists(): - run([cli, "service", "uninstall"], "cleanup") diff --git a/tests/test_config_cli.py b/tests/test_config_cli.py index 008eb9a70e..d23f48f76b 100644 --- a/tests/test_config_cli.py +++ b/tests/test_config_cli.py @@ -48,10 +48,10 @@ def test_init_creates_a_model_free_deployment_and_explains_capability_limits(tmp assert "the value is never printed" in result.output assert "Configuration" in result.output assert "Supported Coding Agents (choose one)" in result.output - for host, (name, launch) in config_cli.AGENTS.items(): + for host, (name, setup, launch) in config_cli.AGENTS.items(): assert name in result.output - assert f" {host} --source " in result.output - assert f"--env-file {environment}" in result.output + if host != "dsh": + assert setup in result.output assert launch in result.output values = config_cli.parse_environment(environment.read_text(encoding="utf-8")) assert "POWERCONTEXT_SERVER_INFERENCE_GENERATION_MODEL" not in values @@ -63,6 +63,22 @@ def test_init_creates_a_model_free_deployment_and_explains_capability_limits(tmp assert shown.exit_code == 0 +@pytest.mark.parametrize("installed", ["0.2.0", "1.0.0rc1", "1.0.0rc2", "1.0.0", "1.1.0", "0.2.1.dev1+g1234567"]) +def test_init_matches_dsh_setup_to_installed_server(installed: str, tmp_path: Path, monkeypatch) -> None: + monkeypatch.setattr(config_cli, "version", lambda _name: installed) + monkeypatch.setattr(config_cli, "collect_configuration", lambda **_kwargs: _configuration()) + result = CliRunner().invoke( + config_cli.app, ["init", "--template", "--output", str(tmp_path / "server.env")], input="\n" + ) + assert result.exit_code == 0 + command = next(line.strip() for line in result.output.splitlines() if "powercontext setup dsh" in line) + if installed in {"0.2.0", "1.0.0rc1", "1.0.0rc2", "1.0.0", "1.1.0"}: + assert command.endswith(f"--ref powercontext-v{installed}") + else: + assert "--source /path/to/matching-powercontext-checkout" in command + assert "--ref master" not in command + + def test_arbitrary_model_providers_and_environment_variables_are_not_rejected() -> None: configuration = _configuration( generation=config_cli.ModelSelection( @@ -388,28 +404,18 @@ def test_init_refuses_to_replace_an_existing_environment_without_force( assert environment.read_text(encoding="utf-8") == "EXISTING=value\n" -@pytest.mark.parametrize("require_write", [False, True]) -def test_init_force_defaults_to_preserving_existing_inference_configuration( - tmp_path: Path, require_write: bool -) -> None: +def test_init_force_defaults_to_preserving_existing_inference_configuration(tmp_path: Path) -> None: environment = tmp_path / ".env" original = config_cli.render_managed_block(_configuration()) environment.write_text(original, encoding="utf-8") result = CliRunner().invoke( config_cli.app, - [ - "init", - "--template", - "--output", - str(environment), - "--force", - *(["--require-write"] if require_write else []), - ], + ["init", "--template", "--output", str(environment), "--force"], input="\n", ) - assert result.exit_code == (130 if require_write else 0) + assert result.exit_code == 0 assert "will remove existing model, embedding, inference schedule, or provider credential settings" in result.output assert "A mode-0600 backup will be created" in result.output assert "Replace them with a model-free configuration? [y/N]" in result.output diff --git a/tests/test_config_wizard.py b/tests/test_config_wizard.py index cb3d4d0bb5..8e42a46157 100644 --- a/tests/test_config_wizard.py +++ b/tests/test_config_wizard.py @@ -269,7 +269,7 @@ def test_openclaw_next_steps_use_plugin_configuration_contract(tmp_path: Path) - client = parse_environment(output.read_text()) assert not any(name.startswith("POWERCONTEXT_OPENCLAW_") for name in client) steps = output.with_name("server.env.next-steps.md").read_text() - assert f"powercontext setup --env-file {output} openclaw" in steps + assert "powercontext setup openclaw" in steps assert "--server-url http://127.0.0.1:17429" in steps assert "plugins.entries.memory-powercontext.config.endpoint" in steps assert "plugins.entries.memory-powercontext.config.autoCapture true" in steps @@ -298,34 +298,19 @@ def test_base_wizard_writes_private_environment_without_models(tmp_path: Path) - assert CliRunner().invoke(app, ["validate", "--env-file", str(output)]).exit_code == 0 -@pytest.mark.parametrize("require_write", [False, True]) -def test_cancel_in_chinese_does_not_write_files(tmp_path: Path, require_write: bool) -> None: +def test_cancel_in_chinese_does_not_write_files(tmp_path: Path) -> None: output = tmp_path / "server.env" result = CliRunner().invoke( app, - ["init", "--language", "zh", "--output", str(output), *(["--require-write"] if require_write else [])], + ["init", "--language", "zh", "--output", str(output)], input=f"sqlite\n{tmp_path / 'context.db'}\nlocal\nbase\nn\n\nnone\ny\nn\n", ) - assert result.exit_code == (130 if require_write else 0), result.output + assert result.exit_code == 0, result.output assert "配置向导" in result.output assert "未写入" in result.output assert not list(tmp_path.iterdir()) -def test_required_write_cancellation_preserves_existing_configuration(tmp_path: Path) -> None: - output = tmp_path / "server.env" - original = "POWERCONTEXT_SERVER_DATABASE_KIND=sqlite\nPOWERCONTEXT_SERVER_HTTP_PORT=18000\nCUSTOM_FLAG=keep\n" - output.write_text(original) - result = CliRunner().invoke( - app, - ["init", "--language", "en", "--output", str(output), "--require-write"], - input=f"sqlite\n{tmp_path / 'context.db'}\nreuse\nn\n", - ) - assert result.exit_code == 130, result.output - assert output.read_text() == original - assert not output.with_name("server.env.next-steps.md").exists() - - def test_existing_server_skips_server_models_and_storage(tmp_path: Path) -> None: output = tmp_path / "client.env" result = CliRunner().invoke( diff --git a/tests/test_config_wizard_installation.py b/tests/test_config_wizard_installation.py index 0b427fc894..1412b38c05 100644 --- a/tests/test_config_wizard_installation.py +++ b/tests/test_config_wizard_installation.py @@ -19,9 +19,7 @@ from pathlib import Path import pytest -from typer.testing import CliRunner -import powercontext.cli.config as config_cli import powercontext.cli.config_wizard as wizard from powercontext.cli.config_wizard_ui import WizardUI @@ -86,39 +84,6 @@ def test_checkout_instructions_keep_a_verified_local_source(tmp_path, monkeypatc assert "--ref" not in instructions -@pytest.mark.parametrize("installed", ["0.2.0", "1.0.0rc1", "1.0.0", "1.1.0"]) -def test_index_release_instructions_pin_every_agent_and_the_selected_file(tmp_path, monkeypatch, installed) -> None: - module = _installed_package(tmp_path, monkeypatch, None) - metadata = module.parents[2] / "powercontext-0.1.dist-info" / "METADATA" - metadata.write_text(f"Metadata-Version: 2.1\nName: powercontext\nVersion: {installed}\n") - monkeypatch.setattr(config_cli, "__file__", str(module)) - environment = tmp_path / "server.env" - - result = CliRunner().invoke(config_cli.app, ["init", "--template", "--output", str(environment)], input="\n") - - assert result.exit_code == 0, result.output - for agent in config_cli.AGENTS: - assert ( - f"powercontext setup --env-file {environment} {agent} --source oceanbase/powercontext " - f"--ref powercontext-v{installed}" - ) in result.output - instructions = _instructions() - for agent in ("codex", "claude-code"): - assert ( - f"powercontext setup {agent} --source oceanbase/powercontext --ref powercontext-v{installed}" - in instructions - ) - assert "--ref master" not in instructions - - -def test_unreleased_index_package_requires_an_explicit_matching_source(tmp_path, monkeypatch) -> None: - module = _installed_package(tmp_path, monkeypatch, None) - metadata = module.parents[2] / "powercontext-0.1.dist-info" / "METADATA" - metadata.write_text("Metadata-Version: 2.1\nName: powercontext\nVersion: 0.2.1.dev1+g1234567\n") - assert "powercontext setup codex --help" in _instructions() - assert "--ref master" not in _instructions() - - @pytest.mark.parametrize( "direct_url", [ diff --git a/tests/test_config_wizard_next_steps.py b/tests/test_config_wizard_next_steps.py index e4855aebfd..fe8b9eb2e7 100644 --- a/tests/test_config_wizard_next_steps.py +++ b/tests/test_config_wizard_next_steps.py @@ -77,39 +77,11 @@ def test_scope_binding_reloads_client_environment_before_agent_installation(tmp_ steps = wizard._next_steps(state, tmp_path / ".env", client_file) creation = steps.index("POST http://127.0.0.1:8000/v1/scopes") - installation = steps.index(" claude-code --source") + installation = steps.index("powercontext setup claude-code") assert f". {client_file}" in steps[creation:installation] - assert f"powercontext setup --env-file {client_file} claude-code" in steps assert "Reload the edited client environment before starting a new Agent" in steps -@pytest.mark.parametrize("platform", ["linux", "darwin", "win32"]) -def test_startup_guidance_matches_personal_service_support(tmp_path, monkeypatch, platform) -> None: - import powercontext.cli.config_startup as startup - - monkeypatch.setattr(startup.sys, "platform", platform) - state = _state() - environment = tmp_path / ".env" - steps = wizard._next_steps(state, environment, environment) - creation = steps.index("POST http://127.0.0.1:8000/v1/scopes") - installation = steps.index(" claude-code --source") - if platform == "win32": - assert "powercontext server run --env-file" in steps[:creation] - assert "powercontext service install" not in steps - else: - assert "powercontext service install --env-file" in steps[:creation] - assert "powercontext doctor --env-file" in steps[:creation] - assert "powercontext service install --env-file" in steps[creation:installation] - - -def test_managed_bind_keeps_foreground_startup_guidance(tmp_path) -> None: - state = _state() - state.values["POWERCONTEXT_SERVER_HTTP_HOST"] = "0.0.0.0" # noqa: S104 - managed-bind guidance - steps = wizard._next_steps(state, tmp_path / ".env", tmp_path / ".env") - assert "powercontext server run --env-file" in steps - assert "powercontext service install" not in steps - - def _profile_script() -> str: state = _state() state.features = {"profile"} diff --git a/tests/test_doctor_environment.py b/tests/test_doctor_environment.py deleted file mode 100644 index 94b3bce673..0000000000 --- a/tests/test_doctor_environment.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright (c) 2026 OceanBase. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Explicit-file diagnostics reach the configured HTTP endpoint without shell evaluation.""" - -import json -import os -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from threading import Thread - -import pytest -from typer.testing import CliRunner - -from powercontext.cli.app import create_cli -from powercontext.cli.system import doctor_app - - -@pytest.fixture -def health_server(): - class Handler(BaseHTTPRequestHandler): - readiness = "ready" - - def log_message(self, format, *args): # noqa: A002 - pass - - def do_GET(self): - payload = ( - {"status": "ok"} - if self.path == "/health/live" - else {"status": self.readiness, "checks": {"database": "ready"}} - ) - self.send_response(200) - self.send_header("Content-Type", "application/json") - self.end_headers() - self.wfile.write(json.dumps(payload).encode()) - - server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) - worker = Thread(target=server.serve_forever, daemon=True) - worker.start() - try: - yield server.server_port, Handler - finally: - server.shutdown() - server.server_close() - worker.join() - - -@pytest.mark.parametrize("server_file", [True, False]) -@pytest.mark.parametrize("readiness", ["ready", "degraded"]) -@pytest.mark.parametrize("lowercase_names", [False, True]) -def test_doctor_uses_selected_file_and_preserves_shell( - tmp_path, monkeypatch, health_server, server_file, readiness, lowercase_names -): - port, handler = health_server - handler.readiness = readiness - monkeypatch.setenv("HOME", str(tmp_path)) - monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config")) - monkeypatch.setenv("POWERCONTEXT_CLIENT_SERVER_URL", "http://127.0.0.1:1") - monkeypatch.setenv("POWERCONTEXT_SERVER_HTTP_PORT", "1") - monkeypatch.setenv("POWERCONTEXT_CLIENT_API_TOKEN", "caller-secret") - marker = tmp_path / "executed" - environment = tmp_path / "selected.env" - endpoint = f"http://127.0.0.1:{port}" - content = ( - f"POWERCONTEXT_SERVER_HTTP_HOST=127.0.0.1\nPOWERCONTEXT_SERVER_HTTP_PORT={port}\n" - "POWERCONTEXT_CLIENT_SERVER_URL=http://127.0.0.1:1\n" - if server_file - else f"POWERCONTEXT_CLIENT_SERVER_URL={endpoint}\n" - ) - if lowercase_names: - content = content.lower() - environment.write_text(content + f"POWERCONTEXT_CLIENT_API_TOKEN='file-secret'\nCUSTOM='$(touch {marker})'\n") - result = CliRunner().invoke(create_cli([doctor_app]), ["doctor", "--env-file", str(environment), "--json"]) - - assert result.exit_code == (0 if readiness == "ready" else 1), result.output - response = json.loads(result.output) - assert response["checks"]["server_readiness"]["detail"] == f"{endpoint} status={readiness}" - assert "file-secret" not in result.output - assert not marker.exists() - assert os.environ["POWERCONTEXT_CLIENT_API_TOKEN"] == "caller-secret" # noqa: S105 - assert os.environ["POWERCONTEXT_SERVER_HTTP_PORT"] == "1" - - -def test_doctor_explicit_url_overrides_file(tmp_path, health_server): - port, _handler = health_server - environment = tmp_path / "selected.env" - environment.write_text("POWERCONTEXT_SERVER_HTTP_PORT=1\n") - endpoint = f"http://127.0.0.1:{port}" - result = CliRunner().invoke( - create_cli([doctor_app]), ["doctor", "--env-file", str(environment), "--server-url", endpoint, "--json"] - ) - assert result.exit_code == 0, result.output - assert endpoint in json.loads(result.output)["checks"]["server_readiness"]["detail"] - - -def test_doctor_rejects_missing_environment_file(tmp_path): - result = CliRunner().invoke(create_cli([doctor_app]), ["doctor", "--env-file", str(tmp_path / "missing.env")]) - assert result.exit_code == 2 - assert "Cannot diagnose the environment file" in result.output - - -def test_doctor_rejects_file_option_for_integration_diagnostics(tmp_path): - environment = tmp_path / "selected.env" - environment.write_text("POWERCONTEXT_SERVER_HTTP_PORT=8000\n") - result = CliRunner().invoke(create_cli([doctor_app]), ["doctor", "--env-file", str(environment), "codex"]) - assert result.exit_code == 2 - assert "without an Agent selection" in result.output diff --git a/tests/test_installation_contract.py b/tests/test_installation_contract.py index f5581aae70..81b54e135b 100644 --- a/tests/test_installation_contract.py +++ b/tests/test_installation_contract.py @@ -51,25 +51,6 @@ def main(): if command == "server run": import contract_server print("Synthetic installation contract CLI") - elif args[:2] == ["config", "init"]: - if not sys.stdin.isatty(): - return 2 - status = int(os.environ.get("CONTRACT_CONFIG_EXIT", "0")) - if status == 0: - Path(args[args.index("--output") + 1]).write_text("POWERCONTEXT_SERVER_HTTP_PORT=18000\\n") - return status - elif args[:2] == ["config", "validate"]: - return int(os.environ.get("CONTRACT_VALIDATE_EXIT", "0")) - elif args[:2] == ["service", "install"]: - env_file = args[args.index("--env-file") + 1] - Path(os.environ["CONTRACT_SERVICE_RESULT"]).write_text(json.dumps({"env_file": env_file})) - return int(os.environ.get("CONTRACT_SERVICE_EXIT", "0")) - elif args[:2] == ["service", "status"]: - return 0 - elif args and args[0] == "doctor": - env_file = args[args.index("--env-file") + 1] - Path(os.environ["CONTRACT_DOCTOR_RESULT"]).write_text(json.dumps({"env_file": env_file})) - return int(os.environ.get("CONTRACT_DOCTOR_EXIT", "0")) elif args and args[0] == "setup": hosts = [] index = 1 @@ -88,7 +69,7 @@ def main(): elif args == ["contract-capabilities"]: print(json.dumps({"server_extra": importlib.util.find_spec("contract_server") is not None})) else: - print("Synthetic installation contract CLI") + return 2 return 0 """ @@ -150,8 +131,6 @@ def test_installation_contract(case: dict[str, Any], tmp_path: Path) -> None: if not uv: pytest.skip("native installation contract requires real uv") windows = sys.platform == "win32" - if case.get("terminal") and windows: - pytest.skip("controlling-terminal pipe regression requires POSIX") shell = shutil.which("powershell") or shutil.which("pwsh") if windows else shutil.which("bash") if not shell: pytest.skip("native installer shell is unavailable") @@ -191,12 +170,6 @@ def test_installation_contract(case: dict[str, Any], tmp_path: Path) -> None: "CONTRACT_SETUP_EXIT": str(case.get("setup_exit", 0)), "CONTRACT_REPORTED_VERSION": case.get("reported_version", "1.2.0"), "CONTRACT_MISSING_COMMANDS": json.dumps(case.get("missing_commands", [])), - "CONTRACT_VALIDATE_EXIT": str(case.get("validate_exit", 0)), - "CONTRACT_CONFIG_EXIT": str(case.get("config_exit", 0)), - "CONTRACT_SERVICE_EXIT": str(case.get("service_exit", 0)), - "CONTRACT_DOCTOR_EXIT": str(case.get("doctor_exit", 0)), - "CONTRACT_SERVICE_RESULT": str(tmp_path / "service.json"), - "CONTRACT_DOCTOR_RESULT": str(tmp_path / "doctor.json"), }) env_file = tmp_path / "existing configuration.env" if case.get("environment_file"): @@ -207,30 +180,6 @@ def test_installation_contract(case: dict[str, Any], tmp_path: Path) -> None: command = ( [shell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(script)] if windows else [shell, str(script)] ) - if case.get("terminal"): - controller = """import errno, os, pty, sys -pid, terminal = pty.fork() -if pid == 0: - shell, script, *args = sys.argv[1:] - os.execv(shell, [shell, '-o', 'pipefail', '-c', - 'cat "$1" | "$2" -s -- "${@:3}"', 'installation', script, shell, *args]) -try: - while True: - try: - data = os.read(terminal, 4096) - except OSError as error: - if error.errno == errno.EIO: - break - raise - if not data: - break - os.write(1, data) -finally: - os.close(terminal) -_, status = os.waitpid(pid, 0) -sys.exit(os.waitstatus_to_exitcode(status)) -""" - command = [sys.executable, "-c", controller, *command] result = subprocess.run( [*command, "--region", "global", *arguments], env=environment, capture_output=True, text=True, timeout=60 ) @@ -241,8 +190,6 @@ def assert_installation_outcome(case, tmp_path, result, environment) -> None: expected = case["expected"] windows = sys.platform == "win32" env_file = tmp_path / "existing configuration.env" - if "configuration_saved" in expected: - assert env_file.exists() == expected["configuration_saved"] assert result.returncode == expected["exit_code"], result.stdout + result.stderr executable = tmp_path / "bin" / ("powercontext.exe" if windows else "powercontext") assert executable.exists() == expected["runtime_installed"] @@ -253,11 +200,6 @@ def assert_installation_outcome(case, tmp_path, result, environment) -> None: assert observed["ref"] == expected["setup_ref"] if expected.get("setup_env_file"): assert observed["env_file"] == str(env_file) - for action in ("service", "doctor"): - observed_file = tmp_path / f"{action}.json" - assert observed_file.exists() == expected.get(action, False) - if observed_file.exists(): - assert json.loads(observed_file.read_text())["env_file"] == str(env_file) if case.get("environment_file"): assert env_file.read_text() == "POWERCONTEXT_SERVER_HTTP_PORT=18000\n" if expected.get("verification_failed"): diff --git a/tests/test_service.py b/tests/test_service.py index 4dc55160e4..ad99cbf753 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -385,21 +385,6 @@ def test_service_controller_installs_and_starts_one_native_registration(tmp_path assert adapter.events == ["write", "reload", "enable", "start:True"] -def test_service_install_rejects_client_only_configuration(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - environment = tmp_path / "client.env" - environment.write_text("POWERCONTEXT_CLIENT_SERVER_URL=https://memory.example.com\n") - environment.chmod(0o600) - _secure_windows_file(environment) - controller = ServiceController(FakeAdapter(tmp_path)) - monkeypatch.setattr(service_cli, "_controller", lambda: controller) - - result = CliRunner().invoke(service_app, ["install", "--env-file", str(environment), "--start-on-login"]) - - assert result.exit_code == 2, result.output - assert "contains only Client settings" in result.output - assert controller.registration_status().registration is RegistrationState.NOT_INSTALLED - - def test_service_controller_allows_slow_native_startup(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: adapter = FakeAdapter(tmp_path) clock = 0.0 diff --git a/website/public/install.ps1 b/website/public/install.ps1 index 8c51287048..9bedf0f6f9 100644 --- a/website/public/install.ps1 +++ b/website/public/install.ps1 @@ -23,8 +23,6 @@ if ($env:POWERCONTEXT_INSTALL_REGION) { $Region = $env:POWERCONTEXT_INSTALL_REGI $IndexUrl = '' $Hosts = @() $NoHosts = $false -$Configure = $false -$InstallService = $false $EnvFile = '' $Uv = '' $TempDir = $null @@ -41,15 +39,13 @@ Usage: powershell -ExecutionPolicy Bypass -File install.ps1 [options] --index-url URL HTTPS default package index for this installation. --host HOST Install an Agent integration; repeat for multiple hosts. --no-hosts Compatibility option; integration setup is skipped by default. - --configure Open the configuration wizard in a terminal; requires --env-file. - --service Install and verify the personal service using --env-file. - --env-file PATH Explicit configuration file for configuration, service, and hosts. + --env-file PATH Existing configuration file for selected Agent integrations. -h, --help Show this help. Agent integration setup runs only for explicitly selected --host values. After installation, run powercontext setup --ref "powercontext-v$(powercontext --version)". -Unattended service setup requires an existing protected environment file. -Windows service setup is experimental; --service enables login auto-start explicitly. +Configure and register a personal service separately after installation. +Windows personal services remain experimental. Region priority: --region, POWERCONTEXT_INSTALL_REGION, named timezone, locale territory, then global. No network location service is queried. @@ -239,8 +235,6 @@ try { } } '--no-hosts' { $NoHosts = $true } - '--configure' { $Configure = $true } - '--service' { $InstallService = $true } { $_ -in '-h', '--help' } { Show-Help; exit 0 } default { throw 'Unknown option. Run install.ps1 --help.' } } @@ -253,15 +247,8 @@ try { if ($IndexUrl) { Assert-HttpsUrl $IndexUrl '--index-url' } if ($env:POWERCONTEXT_UV_INSTALLER_URL) { Assert-HttpsUrl $env:POWERCONTEXT_UV_INSTALLER_URL 'POWERCONTEXT_UV_INSTALLER_URL' } if ($NoHosts -and $Hosts.Count) { throw '--host and --no-hosts cannot be combined.' } - if ($Configure -or $InstallService) { - if ($RuntimeProfile -ne 'local') { throw '--configure and --service require --profile local.' } - if (-not $EnvFile) { throw '--configure and --service require --env-file PATH.' } - } - if ($EnvFile -and -not $Configure -and -not (Test-Path -LiteralPath $EnvFile -PathType Leaf)) { - throw '--env-file must name an existing file unless --configure is selected.' - } - if ($Configure -and ([Console]::IsInputRedirected -or [Console]::IsOutputRedirected)) { - throw '--configure requires a terminal. For unattended setup, use --service with an existing --env-file.' + if ($EnvFile -and -not (Test-Path -LiteralPath $EnvFile -PathType Leaf)) { + throw '--env-file must name an existing file.' } if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { throw 'Use install.sh on macOS and Linux.' } if ($Hosts.Count -and -not (Get-Command git -CommandType Application -ErrorAction SilentlyContinue)) { @@ -316,8 +303,8 @@ try { $Requirement = 'powercontext[cli,server]' if ($RuntimeProfile -eq 'client') { $Requirement = 'powercontext[cli]' } $InstallArgs = @('tool', 'install', '--python', $Python.Trim(), '--no-python-downloads') - # An installed prerelease otherwise remains eligible even with prereleases disallowed. - if ($Version -eq 'latest') { $InstallArgs += @('--upgrade', '--reinstall-package', 'powercontext', '--prerelease', 'disallow') } + # Older uv retains installed prereleases unless it resolves a fresh tool environment. + if ($Version -eq 'latest') { $InstallArgs += @('--upgrade', '--force', '--prerelease', 'disallow') } else { $Requirement += "==$Version" } $InstallArgs += $Requirement if ($IndexUrl) { $InstallArgs += @('--default-index', $IndexUrl) } @@ -346,25 +333,6 @@ try { Write-Host "Runtime installed: $Version ($RuntimeProfile)" $PathPrefix = ("$($ToolBin.Trim());$(Split-Path -Parent $Uv)").Replace("'", "''") Write-Host ('For a new terminal: $env:Path = ''' + $PathPrefix + ';'' + $env:Path') - if ($Configure) { - & $Cli config init --output $EnvFile --require-write - if ($LASTEXITCODE -ne 0) { throw 'Runtime installed, but configuration did not complete. Review the command error or cancellation; retry config init before service setup.' } - Write-Host "Configuration saved: $EnvFile" - } - if ($EnvFile -and $RuntimeProfile -eq 'local') { - & $Cli config validate --env-file $EnvFile - if ($LASTEXITCODE -ne 0) { throw 'Runtime installed, but configuration validation failed. Correct the selected environment file before service setup.' } - Write-Host "Configuration validated: $EnvFile" - } - if ($InstallService) { - & $Cli service install --env-file $EnvFile --start-on-login - if ($LASTEXITCODE -ne 0) { throw 'Runtime installed, but personal service setup did not complete. Inspect service status and retry service install with the same --env-file.' } - & $Cli service status - if ($LASTEXITCODE -ne 0) { throw 'Runtime installed, but personal service verification failed. Inspect the native service logs.' } - & $Cli doctor --env-file $EnvFile - if ($LASTEXITCODE -ne 0) { throw 'Personal service installed, but Server diagnostics did not pass. Review readiness and retry doctor with the same --env-file.' } - Write-Host "Personal service verified with: $EnvFile" - } $SetupStatus = 0 if ($Hosts.Count) { $SetupArgs = @('setup') @@ -373,19 +341,13 @@ try { $SetupStatus = $LASTEXITCODE } if ($RuntimeProfile -eq 'local') { - if ($InstallService) { - Write-Host 'After upgrades or any environment-file edit, rerun service install with the same --env-file.' - } - else { - $NextEnv = '.env' - if ($EnvFile) { $NextEnv = $EnvFile } - $QuotedEnv = "'" + $NextEnv.Replace("'", "''") + "'" - if (-not $EnvFile) { Write-Host "`nConfigure: powercontext config init --output $QuotedEnv" } - Write-Host "Then run: powercontext service install --env-file $QuotedEnv" - Write-Host ' powercontext service status' - Write-Host " powercontext doctor --env-file $QuotedEnv" - Write-Host 'Windows personal services are experimental. For development or temporary use, run powercontext server run.' - } + Write-Host "`nConfigure a local Server: powercontext config init --output .env" + Write-Host 'After saving, validate the file and register the personal service:' + Write-Host ' powercontext config validate --env-file .env' + Write-Host ' powercontext service install --env-file .env' + Write-Host ' powercontext service status' + Write-Host 'Load the generated Client settings, then run powercontext ready and powercontext capabilities.' + Write-Host 'Windows personal services are experimental. For development or temporary use, run powercontext server run.' Write-Host ' https://powercontext.oceanbase.io/en/docs/get-started/quickstart/' } else { diff --git a/website/public/install.sh b/website/public/install.sh index fec764240a..0c06d799d4 100644 --- a/website/public/install.sh +++ b/website/public/install.sh @@ -23,8 +23,6 @@ PYTHON_BIN="" INDEX_URL="" HOSTS=() NO_HOSTS=false -CONFIGURE=false -INSTALL_SERVICE=false ENV_FILE="" TEMP_DIR="" UV_BIN="" @@ -55,15 +53,12 @@ Usage: bash install.sh [--version VERSION] [--index-url URL] [--host HOST]... [- Existing additional uv indexes still take precedence. --host HOST Install this Agent integration; repeat for multiple hosts. --no-hosts Compatibility option; integration setup is skipped by default. - --configure Open the configuration wizard in a terminal; requires --env-file. - --service Install and verify the personal service using --env-file. - --env-file PATH Explicit configuration file for configuration, service, and hosts. + --env-file PATH Existing configuration file for selected Agent integrations. -h, --help Show this help. Agent integration setup runs only for explicitly selected --host values. After installation, run powercontext setup --ref "powercontext-v$(powercontext --version)". -Personal macOS/Linux use: --configure --service --env-file /path/to/powercontext.env. -For unattended service setup, omit --configure and provide an existing protected file. +Configure and register a personal service separately after installation. Region priority: --region, POWERCONTEXT_INSTALL_REGION, named timezone, locale territory, then global. No network location service is queried. @@ -98,8 +93,6 @@ parse_args() { shift 2 ;; --no-hosts) NO_HOSTS=true; shift ;; - --configure) CONFIGURE=true; shift ;; - --service) INSTALL_SERVICE=true; shift ;; -h|--help) usage; exit 0 ;; *) fail "Unknown option. Run bash install.sh --help." ;; esac @@ -119,12 +112,8 @@ parse_args() { if [[ "$NO_HOSTS" == true && ${#HOSTS[@]} -gt 0 ]]; then fail "--host and --no-hosts cannot be combined." fi - if [[ "$CONFIGURE" == true || "$INSTALL_SERVICE" == true ]]; then - [[ "$PROFILE" == local ]] || fail "--configure and --service require --profile local." - [[ -n "$ENV_FILE" ]] || fail "--configure and --service require --env-file PATH." - fi - if [[ -n "$ENV_FILE" && "$CONFIGURE" == false ]]; then - [[ -f "$ENV_FILE" ]] || fail "--env-file must name an existing file unless --configure is selected." + if [[ -n "$ENV_FILE" ]]; then + [[ -f "$ENV_FILE" ]] || fail "--env-file must name an existing file." fi } @@ -318,11 +307,6 @@ main() { esac [[ -n "${HOME:-}" ]] || fail "HOME is not set." command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 || fail "Install curl or wget first." - if [[ "$CONFIGURE" == true ]]; then - # The downloaded script owns stdin; give only the opt-in wizard the controlling terminal. - [[ -t 1 ]] && { exec 3/dev/null || - fail "--configure requires a terminal. For unattended setup, use --service with an existing --env-file." - fi if [[ ${#HOSTS[@]} -gt 0 ]]; then command -v git >/dev/null 2>&1 || fail "Agent integration setup requires Git. Install Git or use --no-hosts." fi @@ -338,8 +322,8 @@ main() { [[ "$PROFILE" != client ]] || requirement='powercontext[cli]' local install_args=(tool install --python "$PYTHON_BIN" --no-python-downloads) if [[ "$VERSION" == latest ]]; then - # An installed prerelease otherwise remains eligible even with prereleases disallowed. - install_args+=(--upgrade --reinstall-package powercontext --prerelease disallow) + # Older uv retains installed prereleases unless it resolves a fresh tool environment. + install_args+=(--upgrade --force --prerelease disallow) else requirement="$requirement==$VERSION" fi @@ -372,26 +356,6 @@ main() { printf 'For a new terminal, add these directories to PATH if needed:\n' printf ' export PATH=%q:%q:"%s"\n' "$(dirname "$UV_BIN")" "$tool_bin" "\$PATH" - if [[ "$CONFIGURE" == true ]]; then - "$tool_bin/powercontext" config init --output "$ENV_FILE" --require-write <&3 || - fail "Runtime installed, but configuration did not complete. Review the command error or cancellation; retry config init before service setup." - printf 'Configuration saved: %s\n' "$ENV_FILE" - fi - if [[ -n "$ENV_FILE" && "$PROFILE" == local ]]; then - "$tool_bin/powercontext" config validate --env-file "$ENV_FILE" || - fail "Runtime installed, but configuration validation failed. Correct the selected environment file before service setup." - printf 'Configuration validated: %s\n' "$ENV_FILE" - fi - if [[ "$INSTALL_SERVICE" == true ]]; then - "$tool_bin/powercontext" service install --env-file "$ENV_FILE" || - fail "Runtime installed, but personal service setup did not complete. Inspect service status and retry service install with the same --env-file." - "$tool_bin/powercontext" service status || - fail "Runtime installed, but personal service verification failed. Inspect the native service logs." - "$tool_bin/powercontext" doctor --env-file "$ENV_FILE" || - fail "Personal service installed, but Server diagnostics did not pass. Review readiness and retry doctor with the same --env-file." - printf 'Personal service verified with: %s\n' "$ENV_FILE" - fi - local setup_status=0 if [[ ${#HOSTS[@]} -gt 0 ]]; then local setup_args=(setup) @@ -400,15 +364,12 @@ main() { --ref "powercontext-v$VERSION" ${HOSTS[@]+"${HOSTS[@]}"} || setup_status=$? fi if [[ "$PROFILE" == local ]]; then - if [[ "$INSTALL_SERVICE" == true ]]; then - printf '%s\n' 'After upgrades or any environment-file edit, rerun service install with the same --env-file.' - else - local next_env=${ENV_FILE:-.env} - [[ -n "$ENV_FILE" ]] || printf '\nConfigure: powercontext config init --output %q\n' "$next_env" - printf 'Then run: powercontext service install --env-file %q\n' "$next_env" - printf ' powercontext service status\n powercontext doctor --env-file %q\n' "$next_env" - printf '%s\n' 'A native user-service manager and loopback Server bind are required. For development or temporary use, run powercontext server run.' - fi + printf '\nConfigure a local Server: powercontext config init --output .env\n' + printf 'After saving, validate the file and register the personal service:\n' + printf ' powercontext config validate --env-file .env\n' + printf ' powercontext service install --env-file .env\n powercontext service status\n' + printf '%s\n' 'Load the generated Client settings, then run powercontext ready and powercontext capabilities.' + printf '%s\n' 'A native user-service manager and loopback Server bind are required. For development or temporary use, run powercontext server run.' printf ' %s\n' "$GUIDE" else printf '\nConnect to your existing Server:\n %s\n' \ From 64055dc4025e6e5eeccd3eeabb57c4ffeecba06a Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Mon, 2 Nov 2026 11:43:42 +0800 Subject: [PATCH 16/24] docs(install): group platform commands in tabs --- docs/en/docs/get-started/install-and-run.md | 8 +++----- docs/zh/docs/get-started/install-and-run.md | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/en/docs/get-started/install-and-run.md b/docs/en/docs/get-started/install-and-run.md index 67ba5bf857..95dc80e46b 100644 --- a/docs/en/docs/get-started/install-and-run.md +++ b/docs/en/docs/get-started/install-and-run.md @@ -27,15 +27,13 @@ Embedded seekDB is unavailable on Windows. The script installs the CLI and local Server in an isolated uv tool environment. It reuses compatible uv/Python, or installs uv and Python 3.12 in user-owned directories. It does not need administrator privileges. -macOS or Linux (Bash; `curl` or `wget` is required): +macOS or Linux uses Bash and requires `curl` or `wget`; Windows requires PowerShell 5.1 or newer. -```bash +```bash tab="macOS / Linux" curl -fsSL https://powercontext.oceanbase.io/install.sh | bash ``` -Windows (PowerShell 5.1 or newer): - -```powershell +```powershell tab="Windows" powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` diff --git a/docs/zh/docs/get-started/install-and-run.md b/docs/zh/docs/get-started/install-and-run.md index feda67b05e..e1b91b0c9c 100644 --- a/docs/zh/docs/get-started/install-and-run.md +++ b/docs/zh/docs/get-started/install-and-run.md @@ -25,15 +25,13 @@ Windows 的 CLI、Server 和个人服务支持为试验性;各 Agent Host 仍 脚本在独立的 uv tool 环境中安装 CLI 和本地 Server。已有兼容的 uv/Python 时会直接复用;缺少时在用户目录安装 uv 和 Python 3.12,无需管理员权限。 -macOS 或 Linux(使用 Bash,需要 `curl` 或 `wget`): +macOS 或 Linux 使用 Bash,需要 `curl` 或 `wget`;Windows 需要 PowerShell 5.1 或更新版本。 -```bash +```bash tab="macOS / Linux" curl -fsSL https://powercontext.oceanbase.io/install.sh | bash ``` -Windows(PowerShell 5.1 或更新版本): - -```powershell +```powershell tab="Windows" powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` From 66f609faee447b0049c35a31a3a3b7993c4acf66 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Mon, 2 Nov 2026 11:51:22 +0800 Subject: [PATCH 17/24] docs(install): remove research references and prose wrapping --- README.md | 148 +++--------- README_CN.md | 49 +--- README_JP.md | 37 +-- docs/en/docs/get-started/install-and-run.md | 251 +++++--------------- docs/en/docs/get-started/quickstart.md | 83 ++----- docs/en/rfcs/1892-script-installation.md | 207 ++++------------ docs/zh/docs/get-started/install-and-run.md | 189 ++++----------- docs/zh/docs/get-started/quickstart.md | 73 ++---- docs/zh/rfcs/1892-script-installation.md | 160 ++++--------- tests/fixtures/installation/README.md | 16 +- 10 files changed, 292 insertions(+), 921 deletions(-) diff --git a/README.md b/README.md index 031ed470c9..617df53815 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,7 @@ ## What is PowerContext? -**PowerContext is an open-source context service for AI agents.** It keeps the durable part of your work — decisions, -constraints, evidence, progress — outside any single conversation, so the next session, the next agent, or the next -person can pick up from the current state instead of rereading the transcript. +**PowerContext is an open-source context service for AI agents.** It keeps the durable part of your work — decisions, constraints, evidence, progress — outside any single conversation, so the next session, the next agent, or the next person can pick up from the current state instead of rereading the transcript. You can use it as a ready-to-run Server or embed the same contracts in another application: @@ -25,14 +23,11 @@ You can use it as a ready-to-run Server or embed the same contracts in another a | Builtin runtime | Memory, Handoff, Experience, and Skill services, scheduling, inference, and supported storage backends | Running the standard local Server | | Server and integrations | HTTP/MCP, Python Client, CLI, Dashboard, and Agent plugins | Connecting an Agent or application | -Work rarely ends with whoever starts it. You hand a task to an agent, the agent gets part of the way, and later you or -someone else takes over. The reasoning and current state often stay behind in that conversation. PowerContext moves them -out of the conversation and into revisioned artifacts you can inspect: +Work rarely ends with whoever starts it. You hand a task to an agent, the agent gets part of the way, and later you or someone else takes over. The reasoning and current state often stay behind in that conversation. PowerContext moves them out of the conversation and into revisioned artifacts you can inspect: ![You and agents hand work off and continue with stored context](docs/assets/readme-workflow.svg) -Everything belongs to a **Scope** — one isolated Source journal, Memory lifecycle, review inbox, and Handoff history. -A Scope ID selects data; it does not prove user identity, grant tool access, or authorize execution. +Everything belongs to a **Scope** — one isolated Source journal, Memory lifecycle, review inbox, and Handoff history. A Scope ID selects data; it does not prove user identity, grant tool access, or authorize execution. ```text Scope @@ -49,10 +44,7 @@ For Kubernetes deployments with external OceanBase, see the [Helm chart and depl ### Memory — decisions and constraints that outlive a session -Memory is a revisioned Artifact family for reusable decisions, constraints, facts, state, and next steps. An entry is -`active`, `forgotten`, `merged`, or `retired`. Forgetting one through `POST /v1/atomic-memory/lifecycle` takes it out of -active recall while its content and history stay available and restorable, without advancing the content revision. -Explicit Memory writes need no model at all; extraction from captured Sources needs a configured generation pipeline. +Memory is a revisioned Artifact family for reusable decisions, constraints, facts, state, and next steps. An entry is `active`, `forgotten`, `merged`, or `retired`. Forgetting one through `POST /v1/atomic-memory/lifecycle` takes it out of active recall while its content and history stay available and restorable, without advancing the content revision. Explicit Memory writes need no model at all; extraction from captured Sources needs a configured generation pipeline. > "Don't refactor the old auth module — mobile is still using it." > Context this expensive shouldn't depend on someone repeating it in every session. @@ -67,52 +59,28 @@ Handoff records work continuity as a four-step loop: Work Contract → Prepared Handoff → Acknowledgement → Task Outcome ``` -`handoff_current_work` captures an inspected boundary and returns a temporary Prepared Handoff. Committing it creates a -durable Revision only when the user wants a milestone. The receiver resolves the Handoff and records an Acknowledgement; -a Task Outcome preserves the final status and checks as Source evidence. The -[Handoff Report](https://powercontext.oceanbase.io/en/docs/workflows/use-handoff-report/) projects the latest Revision in -each selected Scope — read-only, and it never rewrites Memory. +`handoff_current_work` captures an inspected boundary and returns a temporary Prepared Handoff. Committing it creates a durable Revision only when the user wants a milestone. The receiver resolves the Handoff and records an Acknowledgement; a Task Outcome preserves the final status and checks as Source evidence. The [Handoff Report](https://powercontext.oceanbase.io/en/docs/workflows/use-handoff-report/) projects the latest Revision in each selected Scope — read-only, and it never rewrites Memory. ![Dashboard showing a committed Handoff Revision with recorded progress, next action, and linked source evidence](docs/assets/readme-dashboard-handoff.png) ### Experience — what worked, reviewed before it is reused -An Experience records a situation, an action, an observed outcome, and a reusable lesson. It enters a scope-local Review -Inbox as a pending Candidate. Review writes use `expected_version`, so a decision cannot silently apply after another -writer changes the proposal. Approval writes an immutable Revision; rejection records a reason. Both are terminal, and -only the approved current head takes part in recall — pending, rejected, and historical Revisions are excluded. +An Experience records a situation, an action, an observed outcome, and a reusable lesson. It enters a scope-local Review Inbox as a pending Candidate. Review writes use `expected_version`, so a decision cannot silently apply after another writer changes the proposal. Approval writes an immutable Revision; rejection records a reason. Both are terminal, and only the approved current head takes part in recall — pending, rejected, and historical Revisions are excluded. ### Skill — a reviewed procedure, exported on purpose -A managed Skill holds a name, a discovery description, instructions, validation checks, and lineage. Approval does not -install or execute it: an exact approved Revision must be exported explicitly before an Agent host can discover the -host-local projection. +A managed Skill holds a name, a discovery description, instructions, validation checks, and lineage. Approval does not install or execute it: an exact approved Revision must be exported explicitly before an Agent host can discover the host-local projection. [Operational metrics](docs/en/docs/operate/observability.md) expose dependency observations, processing freshness, and attempt latency, including a protected metrics-only endpoint for standalone background deployments. ## Why PowerContext? -- **Context lives outside the conversation.** Memory, Handoff, Experience, and Skill are server-side artifacts, not - files inside one agent's home directory. Switch from Codex to Claude Code — or hand the work to a teammate — and the - work travels with you. -- **Everything is revisioned and traceable.** Each Artifact is addressed as `FAMILY/ARTIFACT_ID@REVISION`. The Artifact - ID stays stable while approved replacements create later Revisions, and reading an exact reference returns that - historical snapshot even after the family head advances. -- **Generation, review, and execution are separate steps.** A model proposes content, review approves a Revision, and - export creates a host-local copy. None of those steps grants an Agent permission to execute instructions. See - [Interfaces](https://powercontext.oceanbase.io/en/docs/develop/interfaces/) for what each surface exposes today. -- **Recall is bounded, not dumped.** `PreparedContext` selects configured sections from active Memory and approved - Experience, with optional Profile snapshots and Topic Memory, then applies a shared byte budget and returns `ready` - content or `empty`. Recalled content is history, not an instruction authority: the receiving Agent still follows - current instructions and checks live workspace state. -- **Review before reuse.** Experience and Skill pass through a review decision that is terminal either way, and - approval writes an immutable Revision. With access control enforced, approval requires `scope.review`. An - authorized Agent can approve its own Candidate; - [separate submission and review](https://powercontext.oceanbase.io/en/docs/workflows/review-candidates/#separate-agent-submission-from-review) - with distinct identities and permissions when a person must decide. -- **One Server, several views of the same contract.** HTTP is the complete remote application contract. The Python Client - gives typed access, MCP is a curated Agent-facing projection, and Core protocols let an application assemble its own - Source, Artifact, and Trigger implementations. +- **Context lives outside the conversation.** Memory, Handoff, Experience, and Skill are server-side artifacts, not files inside one agent's home directory. Switch from Codex to Claude Code — or hand the work to a teammate — and the work travels with you. +- **Everything is revisioned and traceable.** Each Artifact is addressed as `FAMILY/ARTIFACT_ID@REVISION`. The Artifact ID stays stable while approved replacements create later Revisions, and reading an exact reference returns that historical snapshot even after the family head advances. +- **Generation, review, and execution are separate steps.** A model proposes content, review approves a Revision, and export creates a host-local copy. None of those steps grants an Agent permission to execute instructions. See [Interfaces](https://powercontext.oceanbase.io/en/docs/develop/interfaces/) for what each surface exposes today. +- **Recall is bounded, not dumped.** `PreparedContext` selects configured sections from active Memory and approved Experience, with optional Profile snapshots and Topic Memory, then applies a shared byte budget and returns `ready` content or `empty`. Recalled content is history, not an instruction authority: the receiving Agent still follows current instructions and checks live workspace state. +- **Review before reuse.** Experience and Skill pass through a review decision that is terminal either way, and approval writes an immutable Revision. With access control enforced, approval requires `scope.review`. An authorized Agent can approve its own Candidate; [separate submission and review](https://powercontext.oceanbase.io/en/docs/workflows/review-candidates/#separate-agent-submission-from-review) with distinct identities and permissions when a person must decide. +- **One Server, several views of the same contract.** HTTP is the complete remote application contract. The Python Client gives typed access, MCP is a curated Agent-facing projection, and Core protocols let an application assemble its own Source, Artifact, and Trigger implementations. ### How it compares @@ -133,13 +101,9 @@ The comparison assumes typical single-agent or host-local setups; exact behavior ![Compact comparison of PowerContext results on LoCoMo and SWE-bench Pro](docs/assets/readme-benchmark-summary.svg) -- **Long-conversation memory (LoCoMo; 1,540 scored questions across categories 1–4): 90.78% accuracy**, against 87.79% - for PowerMem and 52.9% for full-context replay — **+37.88 points** over full context. -- **Coding tasks (SWE-bench Pro; a paired project run on all 731 public v2 tasks, not an official submission): 86.73% - task resolution with PowerContext on, 82.35% with it off** — **+4.38 points** and 32 more tasks resolved. -- **Retrieval quality (LoCoMo Plus)**, native PowerContext against PowerContext with Jev candidate filtering after - retrieval. Each model is used for both Memory extraction and answering; embeddings use `qwen3.7-text-embedding` - (1024 dimensions): +- **Long-conversation memory (LoCoMo; 1,540 scored questions across categories 1–4): 90.78% accuracy**, against 87.79% for PowerMem and 52.9% for full-context replay — **+37.88 points** over full context. +- **Coding tasks (SWE-bench Pro; a paired project run on all 731 public v2 tasks, not an official submission): 86.73% task resolution with PowerContext on, 82.35% with it off** — **+4.38 points** and 32 more tasks resolved. +- **Retrieval quality (LoCoMo Plus)**, native PowerContext against PowerContext with Jev candidate filtering after retrieval. Each model is used for both Memory extraction and answering; embeddings use `qwen3.7-text-embedding` (1024 dimensions): | Model | PowerContext | PowerContext + Jev | Difference | |---|---|---|---| @@ -147,10 +111,7 @@ The comparison assumes typical single-agent or host-local setups; exact behavior | Qwen3.7-plus | 60.37% | 68.947% | +8.577 points | | GPT-4o | 65.082% | 69.28% | +4.198 points | -Full methods, results, and limitations: [benchmarks](https://powercontext.oceanbase.io/en/benchmarks/). Two caveats we -would rather state than bury: the six LoCoMo Plus scores above were supplied by the project owner, and their run -artifacts are not publicly available for verification — the published instructions reproduce the harness, not those -scores. Windows support is `experimental`. +Full methods, results, and limitations: [benchmarks](https://powercontext.oceanbase.io/en/benchmarks/). Two caveats we would rather state than bury: the six LoCoMo Plus scores above were supplied by the project owner, and their run artifacts are not publicly available for verification — the published instructions reproduce the harness, not those scores. Windows support is `experimental`. ## Quick start @@ -162,13 +123,7 @@ PowerContext keeps durable project context outside any single chat. A first run Agent input → Source evidence → Memory / Topic Memory → PreparedContext → next session or Agent ``` -PowerContext 1.4.1 includes an interactive setup wizard. You need Python 3.11+, Git, and -[uv](https://docs.astral.sh/uv/getting-started/installation/); uv can provision Python when needed. To connect an Agent, -also install its CLI. In the wizard, choose Basic memory for explicit saves and recall without model APIs; Full memory -requires separate Generation and Embedding API credentials. For a local Server without model APIs, see -[Install and run](docs/en/docs/get-started/install-and-run.md). -macOS and Linux are supported; Windows support is `experimental`. -To provision Python and uv automatically, use the [installation script](docs/en/docs/get-started/install-and-run.md#install-with-the-recommended-script). +PowerContext 1.4.1 includes an interactive setup wizard. You need Python 3.11+, Git, and [uv](https://docs.astral.sh/uv/getting-started/installation/); uv can provision Python when needed. To connect an Agent, also install its CLI. In the wizard, choose Basic memory for explicit saves and recall without model APIs; Full memory requires separate Generation and Embedding API credentials. For a local Server without model APIs, see [Install and run](docs/en/docs/get-started/install-and-run.md). macOS and Linux are supported; Windows support is `experimental`. To provision Python and uv automatically, use the [installation script](docs/en/docs/get-started/install-and-run.md#install-with-the-recommended-script). Choose the path that matches your goal: @@ -196,9 +151,7 @@ powercontext service install --env-file .env powercontext service status ``` -The wizard writes one `.env` environment file and `.env.next-steps.md`. These files can contain credentials; do not -commit them. The native current-user service manages the Server lifecycle; Linux requires an -available `systemd --user` manager. Load the generated environment file and check the connection: +The wizard writes one `.env` environment file and `.env.next-steps.md`. These files can contain credentials; do not commit them. The native current-user service manages the Server lifecycle; Linux requires an available `systemd --user` manager. Load the generated environment file and check the connection: ```bash set -a @@ -208,29 +161,18 @@ powercontext ready powercontext capabilities ``` -Continue with `.env.next-steps.md` to create and bind the selected Scopes. Use the real `scope_id` returned by the -creation request; a planned `codex-...` title is not an ID. Changing directories does not create isolation; use the -same Server and Scope in the Dashboard and Agent. After editing `.env`, rerun `powercontext service install --env-file .env` -to reconcile the service. Then install the matching plugins and launch a new Agent session. -The [complete walkthrough](https://powercontext.oceanbase.io/en/docs/get-started/quickstart/) covers Codex and Claude -Code, Dashboard login, SSH forwarding, HTTPS prerequisites, and observable acceptance checks. For example, the matching -Codex installation is: +Continue with `.env.next-steps.md` to create and bind the selected Scopes. Use the real `scope_id` returned by the creation request; a planned `codex-...` title is not an ID. Changing directories does not create isolation; use the same Server and Scope in the Dashboard and Agent. After editing `.env`, rerun `powercontext service install --env-file .env` to reconcile the service. Then install the matching plugins and launch a new Agent session. The [complete walkthrough](https://powercontext.oceanbase.io/en/docs/get-started/quickstart/) covers Codex and Claude Code, Dashboard login, SSH forwarding, HTTPS prerequisites, and observable acceptance checks. For example, the matching Codex installation is: ```bash powercontext setup codex --ref powercontext-v1.4.1 powercontext doctor codex ``` -Before sending the first message in a new Codex session, open `/hooks` and, when prompted, trust the PowerContext -hooks. Without an enabled `UserPromptSubmit` hook, automatic prompt capture and recall do not run, even if MCP connects. -See [first-session checks and recovery](docs/en/docs/integrations/codex.md#first-session-hook-trust). -After the first check below, use the [capability and trigger guide](docs/en/docs/get-started/quickstart.md#5-choose-your-next-workflow) -to try all seven Artifact families, Source, and Tag. Basic memory needs no separate Server model API; the Agent still uses its own model. +Before sending the first message in a new Codex session, open `/hooks` and, when prompted, trust the PowerContext hooks. Without an enabled `UserPromptSubmit` hook, automatic prompt capture and recall do not run, even if MCP connects. See [first-session checks and recovery](docs/en/docs/integrations/codex.md#first-session-hook-trust). After the first check below, use the [capability and trigger guide](docs/en/docs/get-started/quickstart.md#5-choose-your-next-workflow) to try all seven Artifact families, Source, and Tag. Basic memory needs no separate Server model API; the Agent still uses its own model. ### 2. Verify the first end-to-end result -This checks the path that makes PowerContext useful: Source capture → background processing → cross-session recall. In -the new Agent session bound to the same Scope, send a concrete decision such as: +This checks the path that makes PowerContext useful: Source capture → background processing → cross-session recall. In the new Agent session bound to the same Scope, send a concrete decision such as: ```text We are designing the Aurora acceptance project. Use uv for Python dependencies and .env for configuration. @@ -245,49 +187,31 @@ If you enabled Dashboard, open the URL printed by the wizard; an empty Dashboard 3. Send a related update and confirm that the new Source arrives and the topic content or revision history reflects it. 4. Start a new Agent session with the same Scope and ask for the Aurora decisions with citations. -`doctor codex` checks installation and connectivity only. Source capture does not by itself create Memory or Topic -Memory, and processing can wait for configured schedules and model work. In Basic memory mode, explicitly ask the Agent -to save a decision, then search for it in a new session; that does not test automatic extraction. +`doctor codex` checks installation and connectivity only. Source capture does not by itself create Memory or Topic Memory, and processing can wait for configured schedules and model work. In Basic memory mode, explicitly ask the Agent to save a decision, then search for it in a new session; that does not test automatic extraction. ### 3. Explore Python first — no API key -The tutorials run from a repository checkout and take their dependencies from a `uv` dependency group, so they are not -part of the `powercontext` package. From the repository root: +The tutorials run from a repository checkout and take their dependencies from a `uv` dependency group, so they are not part of the `powercontext` package. From the repository root: ```bash uv sync --locked --group notebooks uv run --locked --group notebooks jupyter lab --notebook-dir=examples/jupyter ``` -Start with [01 · Memory across sessions](examples/jupyter/01_memory_across_sessions.ipynb), then work through the -[22 Chinese Jupyter tutorials and a complete team workflow](examples/jupyter/README.md) to run Memory, context -preparation, Handoff, Experience, Skill, and a real Agent step by step. The first seven tutorials need no model or API -key. +Start with [01 · Memory across sessions](examples/jupyter/01_memory_across_sessions.ipynb), then work through the [22 Chinese Jupyter tutorials and a complete team workflow](examples/jupyter/README.md) to run Memory, context preparation, Handoff, Experience, Skill, and a real Agent step by step. The first seven tutorials need no model or API key. ### Common issues -- `doctor codex` verifies Codex installation and connectivity. To verify automatic memory, check that a real prompt - becomes a Source, produces a Topic, evolves after a related prompt, and can be recalled in a new session using the - same Scope. -- If Python dependency downloads are slow or fail, configure a package index for - [uv](https://docs.astral.sh/uv/configuration/indexes/) and retry. -- Choose **Full memory capabilities** to test automatic Memory and Topic Memory; this requires separate Generation and - Embedding API credentials. An Agent subscription does not provide those Server credentials. Choose **Basic memory** to - save and retrieve memories explicitly without additional model APIs. +- `doctor codex` verifies Codex installation and connectivity. To verify automatic memory, check that a real prompt becomes a Source, produces a Topic, evolves after a related prompt, and can be recalled in a new session using the same Scope. +- If Python dependency downloads are slow or fail, configure a package index for [uv](https://docs.astral.sh/uv/configuration/indexes/) and retry. +- Choose **Full memory capabilities** to test automatic Memory and Topic Memory; this requires separate Generation and Embedding API credentials. An Agent subscription does not provide those Server credentials. Choose **Basic memory** to save and retrieve memories explicitly without additional model APIs. - If seekdb needs installing, the wizard asks once and installs the dependency in the background. -- The optional recall-sufficiency gate records scope-local daily search effort and budget omissions for later policy - evaluation. See the [configuration reference](docs/en/docs/operate/configuration.md) for its recording and failure - behavior. -- For a Server on another machine, use HTTPS or follow the - [remote connection guide](docs/en/docs/operate/connect-remote-server.md). Setup recognizes remote HTTP URLs from flags - or environment variables and asks for explicit consent; automated setup uses `--allow-insecure-http`. +- The optional recall-sufficiency gate records scope-local daily search effort and budget omissions for later policy evaluation. See the [configuration reference](docs/en/docs/operate/configuration.md) for its recording and failure behavior. +- For a Server on another machine, use HTTPS or follow the [remote connection guide](docs/en/docs/operate/connect-remote-server.md). Setup recognizes remote HTTP URLs from flags or environment variables and asks for explicit consent; automated setup uses `--allow-insecure-http`. ## Connect your Agent -Codex is `official`; other maintained hosts and Python Agent frameworks are `community`; Bub is `evaluation` only. -These tags describe PowerContext integration maintenance and use. The -[capability matrix](https://powercontext.oceanbase.io/en/docs/integrations/capabilities/) records which hosts are -qualified, along with their supported features and availability. +Codex is `official`; other maintained hosts and Python Agent frameworks are `community`; Bub is `evaluation` only. These tags describe PowerContext integration maintenance and use. The [capability matrix](https://powercontext.oceanbase.io/en/docs/integrations/capabilities/) records which hosts are qualified, along with their supported features and availability. @@ -311,8 +235,7 @@ qualified, along with their supported features and availability.
-Applications can use PowerContext through the async Python client, HTTP API, MCP, or the in-process Core SDK. See the -[interface reference](https://powercontext.oceanbase.io/en/docs/develop/interfaces/) to choose an entry point. +Applications can use PowerContext through the async Python client, HTTP API, MCP, or the in-process Core SDK. See the [interface reference](https://powercontext.oceanbase.io/en/docs/develop/interfaces/) to choose an entry point. ## Build PowerContext @@ -322,9 +245,7 @@ make check make test ``` -See [CONTRIBUTING.md](CONTRIBUTING.md) for the complete development workflow. -Coding and memory evaluations, performance benchmarks, and Skill regressions live under -[`evaluation/`](evaluation/README.md). +See [CONTRIBUTING.md](CONTRIBUTING.md) for the complete development workflow. Coding and memory evaluations, performance benchmarks, and Skill regressions live under [`evaluation/`](evaluation/README.md). ## Learn more @@ -344,8 +265,7 @@ Thank you to everyone who contributes to PowerContext. ❤️ PowerContext contributors -[See all contributors](https://github.com/oceanbase/powercontext/graphs/contributors) · -[Start contributing](CONTRIBUTING.md) +[See all contributors](https://github.com/oceanbase/powercontext/graphs/contributors) · [Start contributing](CONTRIBUTING.md) ## License diff --git a/README_CN.md b/README_CN.md index f623a5ef5e..fbd758da20 100644 --- a/README_CN.md +++ b/README_CN.md @@ -42,10 +42,7 @@ Scope ### Memory —— 活得比会话更久的决定与约束 -Memory 是一个带版本的 Artifact 家族,存放可以复用的决定、约束、事实、状态和下一步。条目状态为 `active`、 -`forgotten`、`merged` 或 `retired`。通过 `POST /v1/atomic-memory/lifecycle` 遗忘一条,会把它移出主动召回,但内容 -和历史仍然可用、可恢复,且不会推进内容 revision。显式写入 Memory 完全不需要模型;从已捕获的 Source 中提取, -才需要配置生成流水线。 +Memory 是一个带版本的 Artifact 家族,存放可以复用的决定、约束、事实、状态和下一步。条目状态为 `active`、`forgotten`、`merged` 或 `retired`。通过 `POST /v1/atomic-memory/lifecycle` 遗忘一条,会把它移出主动召回,但内容和历史仍然可用、可恢复,且不会推进内容 revision。显式写入 Memory 完全不需要模型;从已捕获的 Source 中提取,才需要配置生成流水线。 > "别重构老的 auth 模块 —— 移动端还在用。" > 这么贵的上下文,不应该依赖有人每个会话都重讲一遍。 @@ -102,10 +99,8 @@ Experience 记录一个场景、一次动作、观察到的结果,以及可复 ![PowerContext 在 LoCoMo 和 SWE-bench Pro 上的紧凑对比图](docs/assets/readme-benchmark-summary.svg) -- **长对话记忆(LoCoMo;类别 1–4 的 1,540 个计分问题):准确率 90.78%**,PowerMem 为 87.79%,全量上下文 - 重放为 52.9% —— 相对后者 **+37.88 个百分点**。 -- **编码任务(SWE-bench Pro;731 个公开 v2 任务的配对项目运行,非官方提交):开启 PowerContext 后任务解决率 - 86.73%,关闭时 82.35%** —— **+4.38 个百分点**,多解出 32 题。 +- **长对话记忆(LoCoMo;类别 1–4 的 1,540 个计分问题):准确率 90.78%**,PowerMem 为 87.79%,全量上下文重放为 52.9% —— 相对后者 **+37.88 个百分点**。 +- **编码任务(SWE-bench Pro;731 个公开 v2 任务的配对项目运行,非官方提交):开启 PowerContext 后任务解决率 86.73%,关闭时 82.35%** —— **+4.38 个百分点**,多解出 32 题。 - **召回质量(LoCoMo Plus)**:原生 PowerContext 对比检索后用 Jev 做候选筛选。每组模型同时用于 Memory 抽取与回答,向量模型为 `qwen3.7-text-embedding`(1024 维): | 模型 | PowerContext | PowerContext + Jev | 差值 | @@ -126,13 +121,7 @@ PowerContext 把可长期复用的项目上下文放在单次对话之外。一 Agent 输入 → Source 证据 → Memory / Topic Memory → PreparedContext → 新会话或新 Agent ``` -PowerContext 1.4.1 包含交互式配置向导。需要 Python 3.11+、Git 和 -[uv](https://docs.astral.sh/uv/getting-started/installation/);需要时 uv 可以按需提供 Python。 -要接入 Agent,还需要安装对应的 CLI。向导中选择“基础记忆”即可在没有模型 API 的情况下显式保存与召回; -选择“完整记忆能力”则需要独立的 Generation、Embedding API 凭据。只想启动不依赖模型 API 的本地 Server 时, -请参考[安装与运行](docs/zh/docs/get-started/install-and-run.md)。 -支持 macOS 和 Linux;Windows 支持为 `experimental`。 -需要自动安装 Python 和 uv 时,使用[安装脚本](docs/zh/docs/get-started/install-and-run.md#使用推荐安装脚本)。 +PowerContext 1.4.1 包含交互式配置向导。需要 Python 3.11+、Git 和 [uv](https://docs.astral.sh/uv/getting-started/installation/);需要时 uv 可以按需提供 Python。要接入 Agent,还需要安装对应的 CLI。向导中选择“基础记忆”即可在没有模型 API 的情况下显式保存与召回;选择“完整记忆能力”则需要独立的 Generation、Embedding API 凭据。只想启动不依赖模型 API 的本地 Server 时,请参考[安装与运行](docs/zh/docs/get-started/install-and-run.md)。支持 macOS 和 Linux;Windows 支持为 `experimental`。需要自动安装 Python 和 uv 时,使用[安装脚本](docs/zh/docs/get-started/install-and-run.md#使用推荐安装脚本)。 请选择与你目标匹配的路径: @@ -160,8 +149,7 @@ powercontext service install --env-file .env powercontext service status ``` -向导生成一个 `.env` 环境文件和 `.env.next-steps.md`。这些文件可能包含凭据,请勿提交到 Git。 -原生当前用户服务负责 Server 生命周期;Linux 需要可用的 `systemd --user` 管理器。加载生成的环境文件并检查服务: +向导生成一个 `.env` 环境文件和 `.env.next-steps.md`。这些文件可能包含凭据,请勿提交到 Git。原生当前用户服务负责 Server 生命周期;Linux 需要可用的 `systemd --user` 管理器。加载生成的环境文件并检查服务: ```bash set -a @@ -171,26 +159,18 @@ powercontext ready powercontext capabilities ``` -接着按 `.env.next-steps.md` 创建并绑定 Scope。使用创建请求返回的真实 `scope_id`;向导规划的 `codex-...` 标题不是 ID。 -切换目录不会创建隔离;Dashboard 和 Agent 要使用同一个 Server 和 Scope。修改 `.env` 后,重新执行 -`powercontext service install --env-file .env` 以协调服务配置。然后安装相同版本的插件,再打开新 Agent 会话。 -[完整安装流程](https://powercontext.oceanbase.io/zh/docs/get-started/quickstart/)包含 Codex、Claude Code、Dashboard 登录、SSH 隧道、HTTPS 前提及逐项验收。例如,匹配本版本的 Codex 安装命令是: +接着按 `.env.next-steps.md` 创建并绑定 Scope。使用创建请求返回的真实 `scope_id`;向导规划的 `codex-...` 标题不是 ID。切换目录不会创建隔离;Dashboard 和 Agent 要使用同一个 Server 和 Scope。修改 `.env` 后,重新执行 `powercontext service install --env-file .env` 以协调服务配置。然后安装相同版本的插件,再打开新 Agent 会话。[完整安装流程](https://powercontext.oceanbase.io/zh/docs/get-started/quickstart/)包含 Codex、Claude Code、Dashboard 登录、SSH 隧道、HTTPS 前提及逐项验收。例如,匹配本版本的 Codex 安装命令是: ```bash powercontext setup codex --ref powercontext-v1.4.1 powercontext doctor codex ``` -在新 Codex 会话发送第一条消息前,打开 `/hooks` 检查 PowerContext Hook,并在收到提示时授予信任。 -`UserPromptSubmit` Hook 未启用时,即使 MCP 连通,也不会自动采集输入或召回上下文。 -检查与恢复步骤见[首次会话 Hook 信任](docs/zh/docs/integrations/codex.md#首次会话-hook-信任)。 -完成下面的首次验收后,可按[能力与触发指南](docs/zh/docs/get-started/quickstart.md#5-选择下一项工作流) -体验七类 Artifact、Source 和 Tag。基础记忆无需独立的 Server 模型 API;Agent 仍使用自己的模型。 +在新 Codex 会话发送第一条消息前,打开 `/hooks` 检查 PowerContext Hook,并在收到提示时授予信任。`UserPromptSubmit` Hook 未启用时,即使 MCP 连通,也不会自动采集输入或召回上下文。检查与恢复步骤见[首次会话 Hook 信任](docs/zh/docs/integrations/codex.md#首次会话-hook-信任)。完成下面的首次验收后,可按[能力与触发指南](docs/zh/docs/get-started/quickstart.md#5-选择下一项工作流) 体验七类 Artifact、Source 和 Tag。基础记忆无需独立的 Server 模型 API;Agent 仍使用自己的模型。 ### 2. 验收第一条端到端结果 -这一步检查 PowerContext 真正有用的链路:Source 采集 → 后台处理 → 跨会话召回。 -在绑定了同一 Scope 的新 Agent 会话中,发送一条具体决定,例如: +这一步检查 PowerContext 真正有用的链路:Source 采集 → 后台处理 → 跨会话召回。在绑定了同一 Scope 的新 Agent 会话中,发送一条具体决定,例如: ```text 我们正在设计 Aurora 验收项目。决定用 uv 管理 Python 依赖,配置文件使用 .env。 @@ -204,8 +184,7 @@ powercontext doctor codex 3. 发送一条相关更新,确认新的 Source 入库,且主题内容或修订记录反映了更新。 4. 使用同一 Scope 开启新的 Agent 会话,要求它带引用找回 Aurora 的决定。 -`doctor codex` 只检查安装与连接。Source 采集本身不会创建 Memory 或 Topic Memory,后台处理还要等待配置的调度和模型运行。 -基础记忆模式下,可以显式要求 Agent 保存一条决定,再在新会话中搜索;这不能验收自动提取。 +`doctor codex` 只检查安装与连接。Source 采集本身不会创建 Memory 或 Topic Memory,后台处理还要等待配置的调度和模型运行。基础记忆模式下,可以显式要求 Agent 保存一条决定,再在新会话中搜索;这不能验收自动提取。 ### 3. 先用 Python 体验 @@ -216,9 +195,7 @@ uv sync --locked --group notebooks uv run --locked --group notebooks jupyter lab --notebook-dir=examples/jupyter ``` -先从 [01 · 让新会话用上项目约定](examples/jupyter/01_memory_across_sessions.ipynb)开始,再通过 -[22 篇中文 Jupyter 教程与完整团队工作流](examples/jupyter/README.md)亲手运行 Memory、上下文、交接、Experience、Skill -和真实 Agent。前七篇不需要模型或 API Key。 +先从 [01 · 让新会话用上项目约定](examples/jupyter/01_memory_across_sessions.ipynb)开始,再通过 [22 篇中文 Jupyter 教程与完整团队工作流](examples/jupyter/README.md)亲手运行 Memory、上下文、交接、Experience、Skill 和真实 Agent。前七篇不需要模型或 API Key。 ### 常见问题 @@ -265,8 +242,7 @@ make check make test ``` -完整开发流程请阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。 -编程能力评测、记忆质量评测、性能压测和 Skill 回归统一放在 [`evaluation/`](evaluation/README.md),按用途选择对应目录。 +完整开发流程请阅读 [CONTRIBUTING.md](CONTRIBUTING.md)。编程能力评测、记忆质量评测、性能压测和 Skill 回归统一放在 [`evaluation/`](evaluation/README.md),按用途选择对应目录。 ## 进一步了解 @@ -286,8 +262,7 @@ PowerContext 是 [PowerMem](https://www.powermem.ai/) 的后续项目。 PowerContext 贡献者 -[查看全部贡献者](https://github.com/oceanbase/powercontext/graphs/contributors) · -[参与贡献](CONTRIBUTING.md) +[查看全部贡献者](https://github.com/oceanbase/powercontext/graphs/contributors) · [参与贡献](CONTRIBUTING.md) ## 许可证 diff --git a/README_JP.md b/README_JP.md index f79aaed130..54a2e7c1a3 100644 --- a/README_JP.md +++ b/README_JP.md @@ -15,8 +15,7 @@ **PowerContext は、AI Agent 向けのオープンソース・コンテキストサービスです。** 作業のうち長く使う価値のある部分 —— 確定した判断、制約、根拠、進捗 —— を単一の会話の外に置きます。これにより、次のセッション、次の Agent、あるいは次の担当者が、記録をすべて読み返すのではなく現在の状態から続けられます。 -PowerContext は、そのまま使える Server としても、同じ契約を別のアプリケーションに組み込む形でも利用できます。 -まずは次の 3 層で理解すると分かりやすくなります。 +PowerContext は、そのまま使える Server としても、同じ契約を別のアプリケーションに組み込む形でも利用できます。まずは次の 3 層で理解すると分かりやすくなります。 | レイヤー | 担当 | 使う場面 | | --- | --- | --- | @@ -120,8 +119,7 @@ PowerContext は、長く使うプロジェクトのコンテキストを 1 つ Agent の入力 → Source の証拠 → Memory / Topic Memory → PreparedContext → 新しいセッションまたは Agent ``` -PowerContext 1.4.1 には対話式セットアップウィザードが含まれています。Python 3.11+、Git、[uv](https://docs.astral.sh/uv/getting-started/installation/) が必要です。必要に応じて uv が Python を用意できます。Agent に接続するには、その CLI もインストールしてください。ウィザードでは、モデル API なしで明示的な保存と想起を試す場合は Basic memory、自動抽出を有効にする場合は Full memory を選択してください。Full memory には Generation と Embedding API の個別の認証情報が必要です。モデル API なしでローカル Server だけを起動する場合は[インストールと実行](docs/en/docs/get-started/install-and-run.md)を参照してください。macOS と Linux をサポートし、Windows のサポートは `experimental` です。 -Python と uv を自動で用意する場合は、[インストールスクリプト](docs/en/docs/get-started/install-and-run.md#install-with-the-recommended-script)を使ってください。 +PowerContext 1.4.1 には対話式セットアップウィザードが含まれています。Python 3.11+、Git、[uv](https://docs.astral.sh/uv/getting-started/installation/) が必要です。必要に応じて uv が Python を用意できます。Agent に接続するには、その CLI もインストールしてください。ウィザードでは、モデル API なしで明示的な保存と想起を試す場合は Basic memory、自動抽出を有効にする場合は Full memory を選択してください。Full memory には Generation と Embedding API の個別の認証情報が必要です。モデル API なしでローカル Server だけを起動する場合は[インストールと実行](docs/en/docs/get-started/install-and-run.md)を参照してください。macOS と Linux をサポートし、Windows のサポートは `experimental` です。Python と uv を自動で用意する場合は、[インストールスクリプト](docs/en/docs/get-started/install-and-run.md#install-with-the-recommended-script)を使ってください。 目的に合う経路を選択してください: @@ -149,8 +147,7 @@ powercontext service install --env-file .env powercontext service status ``` -ウィザードは `.env` と `.env.next-steps.md` を生成します。これらのファイルには認証情報が含まれることがあるため、コミットしないでください。 -Server のライフサイクルはネイティブのユーザーサービスが管理します。Linux では利用可能な `systemd --user` マネージャーが必要です。生成された環境ファイルを読み込んで接続を確認します。 +ウィザードは `.env` と `.env.next-steps.md` を生成します。これらのファイルには認証情報が含まれることがあるため、コミットしないでください。Server のライフサイクルはネイティブのユーザーサービスが管理します。Linux では利用可能な `systemd --user` マネージャーが必要です。生成された環境ファイルを読み込んで接続を確認します。 ```bash set -a @@ -160,25 +157,18 @@ powercontext ready powercontext capabilities ``` -続いて `.env.next-steps.md` の手順に従い、Scope を作成して紐付けます。作成リクエストが返す実際の `scope_id` を使ってください。 -計画された `codex-...` タイトルは ID ではありません。ディレクトリを変えても分離は作られないため、Dashboard と Agent で同じ Server と -Scope を使います。`.env` を編集したら、`powercontext service install --env-file .env` を再実行してサービスの設定を反映します。その後、対応するプラグインをインストールして新しい Agent セッションを起動します。[完全な手順](https://powercontext.oceanbase.io/en/docs/get-started/quickstart/)では、Codex と Claude Code、Dashboard へのログイン、SSH 転送、HTTPS の前提条件、確認可能な受け入れチェックを扱っています。対応する Codex のインストールは次のとおりです。 +続いて `.env.next-steps.md` の手順に従い、Scope を作成して紐付けます。作成リクエストが返す実際の `scope_id` を使ってください。計画された `codex-...` タイトルは ID ではありません。ディレクトリを変えても分離は作られないため、Dashboard と Agent で同じ Server と Scope を使います。`.env` を編集したら、`powercontext service install --env-file .env` を再実行してサービスの設定を反映します。その後、対応するプラグインをインストールして新しい Agent セッションを起動します。[完全な手順](https://powercontext.oceanbase.io/en/docs/get-started/quickstart/)では、Codex と Claude Code、Dashboard へのログイン、SSH 転送、HTTPS の前提条件、確認可能な受け入れチェックを扱っています。対応する Codex のインストールは次のとおりです。 ```bash powercontext setup codex --ref powercontext-v1.4.1 powercontext doctor codex ``` -新しい Codex セッションで最初のメッセージを送る前に、`/hooks` で PowerContext Hook を確認し、求められたら信頼を許可してください。 -`UserPromptSubmit` Hook が無効な場合、MCP に接続できても入力の自動取得と想起は動作しません。 -[初回確認と復旧](docs/en/docs/integrations/codex.md#first-session-hook-trust)を参照してください。 -以下の確認後は、[機能と操作のガイド](docs/en/docs/get-started/quickstart.md#5-choose-your-next-workflow)から -7 種類の Artifact、Source、Tag を試せます。Basic memory は Server 用の独立したモデル API が不要ですが、Agent 自身のモデルは使用します。 +新しい Codex セッションで最初のメッセージを送る前に、`/hooks` で PowerContext Hook を確認し、求められたら信頼を許可してください。`UserPromptSubmit` Hook が無効な場合、MCP に接続できても入力の自動取得と想起は動作しません。[初回確認と復旧](docs/en/docs/integrations/codex.md#first-session-hook-trust)を参照してください。以下の確認後は、[機能と操作のガイド](docs/en/docs/get-started/quickstart.md#5-choose-your-next-workflow)から 7 種類の Artifact、Source、Tag を試せます。Basic memory は Server 用の独立したモデル API が不要ですが、Agent 自身のモデルは使用します。 ### 2. 最初のエンドツーエンド結果を確認する -この確認では、PowerContext が役立つ流れを検証します:Source の取得 → バックグラウンド処理 → セッションをまたぐ想起。 -同じ Scope に紐付けた新しい Agent セッションで、次のような具体的な決定を送ります。 +この確認では、PowerContext が役立つ流れを検証します:Source の取得 → バックグラウンド処理 → セッションをまたぐ想起。同じ Scope に紐付けた新しい Agent セッションで、次のような具体的な決定を送ります。 ```text Aurora の受け入れプロジェクトを設計しています。Python の依存関係には uv、設定には .env を使います。 @@ -186,16 +176,14 @@ Aurora の受け入れプロジェクトを設計しています。Python の依 これらの制約を変えずに言い直してください。コードは変更しないでください。 ``` -Dashboard を有効にした場合は、ウィザードが表示した URL を開いてください。取得前に Dashboard が空なのは正常です。 -その後、次を確認します。 +Dashboard を有効にした場合は、ウィザードが表示した URL を開いてください。取得前に Dashboard が空なのは正常です。その後、次を確認します。 1. 入力が Source として表示されること。 2. 設定した検査間隔とモデル処理の後、関連する Topic Memory が現れること。 3. 関連する更新を送り、新しい Source が入り、トピックの内容またはリビジョン履歴に反映されること。 4. 同じ Scope で新しい Agent セッションを開始し、Aurora の決定を引用付きで探すこと。 -`doctor codex` はインストールと接続だけを確認します。Source の取得だけでは Memory や Topic Memory は作成されず、バックグラウンド処理には設定したスケジュールとモデルの実行が必要です。 -Basic memory では Agent に決定を明示的に保存させ、新しいセッションで検索してください。ただし、これは自動抽出の検証ではありません。 +`doctor codex` はインストールと接続だけを確認します。Source の取得だけでは Memory や Topic Memory は作成されず、バックグラウンド処理には設定したスケジュールとモデルの実行が必要です。Basic memory では Agent に決定を明示的に保存させ、新しいセッションで検索してください。ただし、これは自動抽出の検証ではありません。 ### 3. Python で最初に試す @@ -206,8 +194,7 @@ uv sync --locked --group notebooks uv run --locked --group notebooks jupyter lab --notebook-dir=examples/jupyter ``` -まず [01 · Memory across sessions](examples/jupyter/01_memory_across_sessions.ipynb) を実行し、その後 [22 本の Jupyter チュートリアル(中国語)](examples/jupyter/README.md)で、Memory、コンテキストの準備、Handoff、Experience、Skill、実際の Agent を動かしながら学べます。 -最初の 7 本はモデルや API キーなしで実行できます。 +まず [01 · Memory across sessions](examples/jupyter/01_memory_across_sessions.ipynb) を実行し、その後 [22 本の Jupyter チュートリアル(中国語)](examples/jupyter/README.md)で、Memory、コンテキストの準備、Handoff、Experience、Skill、実際の Agent を動かしながら学べます。最初の 7 本はモデルや API キーなしで実行できます。 ### よくある問題 @@ -254,8 +241,7 @@ make check make test ``` -開発ワークフロー全体については [CONTRIBUTING.md](CONTRIBUTING.md) を参照してください。 -コーディング評価、メモリ品質評価、性能ベンチマーク、Skill 回帰テストは [`evaluation/`](evaluation/README.md) にまとめています。 +開発ワークフロー全体については [CONTRIBUTING.md](CONTRIBUTING.md) を参照してください。コーディング評価、メモリ品質評価、性能ベンチマーク、Skill 回帰テストは [`evaluation/`](evaluation/README.md) にまとめています。 ## さらに詳しく @@ -275,8 +261,7 @@ PowerContext に貢献してくださる皆さまに感謝します。❤️ PowerContext のコントリビューター -[すべてのコントリビューターを見る](https://github.com/oceanbase/powercontext/graphs/contributors) · -[開発に参加する](CONTRIBUTING.md) +[すべてのコントリビューターを見る](https://github.com/oceanbase/powercontext/graphs/contributors) · [開発に参加する](CONTRIBUTING.md) ## ライセンス diff --git a/docs/en/docs/get-started/install-and-run.md b/docs/en/docs/get-started/install-and-run.md index 95dc80e46b..0672b5ca48 100644 --- a/docs/en/docs/get-started/install-and-run.md +++ b/docs/en/docs/get-started/install-and-run.md @@ -5,11 +5,9 @@ description: Install PowerContext 1.4.1 and run the local Server. # Install and run -For an Agent connecting from another machine, see [Connect to a remote Server](../operate/connect-remote-server.md) -for guided URL confirmation, unattended setup, and endpoint-bound HTTP consent. +For an Agent connecting from another machine, see [Connect to a remote Server](../operate/connect-remote-server.md) for guided URL confirmation, unattended setup, and endpoint-bound HTTP consent. -Start with the [Quick Start](quickstart.md) for your first session. This page covers version selection, -platforms, installation roles, startup, diagnostics, and updates. +Start with the [Quick Start](quickstart.md) for your first session. This page covers version selection, platforms, installation roles, startup, diagnostics, and updates. ## Platform support @@ -18,14 +16,11 @@ platforms, installation roles, startup, diagnostics, and updates. | macOS, Linux | Supported | | Windows | `experimental` | -Windows CLI, Server, and personal-service support is experimental. Each Agent Host still has its own platform -requirements. Examples using Bash syntax require a Bash environment and cannot be pasted directly into PowerShell. -Embedded seekDB is unavailable on Windows. +Windows CLI, Server, and personal-service support is experimental. Each Agent Host still has its own platform requirements. Examples using Bash syntax require a Bash environment and cannot be pasted directly into PowerShell. Embedded seekDB is unavailable on Windows. ## Install with the recommended script -The script installs the CLI and local Server in an isolated uv tool environment. It reuses compatible uv/Python, -or installs uv and Python 3.12 in user-owned directories. It does not need administrator privileges. +The script installs the CLI and local Server in an isolated uv tool environment. It reuses compatible uv/Python, or installs uv and Python 3.12 in user-owned directories. It does not need administrator privileges. macOS or Linux uses Bash and requires `curl` or `wget`; Windows requires PowerShell 5.1 or newer. @@ -37,18 +32,11 @@ curl -fsSL https://powercontext.oceanbase.io/install.sh | bash powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` -Apply the PATH command printed at completion before running `powercontext` in your terminal. The installer does not -edit shell startup files or the persistent Windows PATH. `UV_INSTALL_DIR` selects where a missing uv is installed; -`UV_TOOL_BIN_DIR` selects the PowerContext executable directory. +Apply the PATH command printed at completion before running `powercontext` in your terminal. The installer does not edit shell startup files or the persistent Windows PATH. `UV_INSTALL_DIR` selects where a missing uv is installed; `UV_TOOL_BIN_DIR` selects the PowerContext executable directory. -Installation runs unattended by default. Pass `--host codex` (repeatable) to install selected Agent integrations. -Only Agent setup needs Git and each host's prerequisites. `--no-hosts` remains an optional way to state the default; -it cannot be combined with `--host`. The [capability matrix](../integrations/capabilities.md) describes host support. +Installation runs unattended by default. Pass `--host codex` (repeatable) to install selected Agent integrations. Only Agent setup needs Git and each host's prerequisites. `--no-hosts` remains an optional way to state the default; it cannot be combined with `--host`. The [capability matrix](../integrations/capabilities.md) describes host support. -For an existing remote Server, add `--profile client` to install only the CLI and Client dependencies. The default -`--profile local` includes the local Server. Neither profile starts a Server, registers a service, or overwrites -configuration or data. For a local installation, continue with `powercontext config init` and [Quick Start](quickstart.md). -Client-only installations use endpoint settings in the [remote connection guide](../operate/connect-remote-server.md). +For an existing remote Server, add `--profile client` to install only the CLI and Client dependencies. The default `--profile local` includes the local Server. Neither profile starts a Server, registers a service, or overwrites configuration or data. For a local installation, continue with `powercontext config init` and [Quick Start](quickstart.md). Client-only installations use endpoint settings in the [remote connection guide](../operate/connect-remote-server.md). ## Configure and install a personal service @@ -58,8 +46,7 @@ For personal macOS/Linux use, configure the local Server after installation: powercontext config init --output .env ``` -Save `.env` before continuing. If you cancel the wizard, stop here. Then validate the saved file and explicitly -register the native current-user service: +Save `.env` before continuing. If you cancel the wizard, stop here. Then validate the saved file and explicitly register the native current-user service: ```bash powercontext config validate --env-file .env @@ -67,8 +54,7 @@ powercontext service install --env-file .env powercontext service status ``` -Choose loopback binding for a personal service. Linux needs an available `systemd --user` manager. Windows personal -services remain experimental; see [Deploy the Server](../operate/deploy-server.md) for file protection and login startup. +Choose loopback binding for a personal service. Linux needs an available `systemd --user` manager. Windows personal services remain experimental; see [Deploy the Server](../operate/deploy-server.md) for file protection and login startup. Load the generated Client settings and check the running Server: @@ -80,19 +66,13 @@ powercontext ready powercontext capabilities ``` -After any file edit, including writing returned Scope IDs, rerun `service install --env-file` with the original file. -Use the same path if you saved the configuration elsewhere. A running service does not establish an Agent workflow; -continue with the [Quick Start](quickstart.md) for Scope creation and Agent acceptance. +After any file edit, including writing returned Scope IDs, rerun `service install --env-file` with the original file. Use the same path if you saved the configuration elsewhere. A running service does not establish an Agent workflow; continue with the [Quick Start](quickstart.md) for Scope creation and Agent acceptance. -The installer only installs software and explicitly selected integrations. `--env-file PATH` can supply an existing -connection file to selected hosts through the existing `setup` command; configuration and service registration remain -separate CLI steps. +The installer only installs software and explicitly selected integrations. `--env-file PATH` can supply an existing connection file to selected hosts through the existing `setup` command; configuration and service registration remain separate CLI steps. ## Installer version selection -The default `--version latest` installs or upgrades to the newest stable release available from the selected index -and compatible with the chosen Python. It excludes prereleases. `--version` accepts an exact release, including an -explicit prerelease such as `1.3.0rc1`; it never substitutes a different version if that release is unavailable. +The default `--version latest` installs or upgrades to the newest stable release available from the selected index and compatible with the chosen Python. It excludes prereleases. `--version` accepts an exact release, including an explicit prerelease such as `1.3.0rc1`; it never substitutes a different version if that release is unavailable. For example, install PowerContext 1.4.1 with its Codex integration: @@ -100,8 +80,7 @@ For example, install PowerContext 1.4.1 with its Codex integration: curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.4.1 --host codex ``` -This selects package `1.4.1` and Git tag `powercontext-v1.4.1`. With `latest`, the installer reads the installed CLI's -version and uses its matching tag; it never uses the moving `master` branch for host setup. To add an integration later: +This selects package `1.4.1` and Git tag `powercontext-v1.4.1`. With `latest`, the installer reads the installed CLI's version and uses its matching tag; it never uses the moving `master` branch for host setup. To add an integration later: ```bash powercontext setup codex --ref "powercontext-v$(powercontext --version)" @@ -109,18 +88,13 @@ powercontext setup codex --ref "powercontext-v$(powercontext --version)" Use `powercontext setup --ref "powercontext-v$(powercontext --version)"` for an interactive host picker. -In PowerShell, the same double-quoted expression works. An integration failure leaves the installed Runtime usable -and exits with an error and a retry instruction. Installation success does not establish Server readiness or host -workflow correctness; use the checks below and the integration's own guide. +In PowerShell, the same double-quoted expression works. An integration failure leaves the installed Runtime usable and exits with an error and a retry instruction. Installation success does not establish Server readiness or host workflow correctness; use the checks below and the integration's own guide. -The installer verifies the installed version and advertised commands before reporting success. A package can install -successfully while command verification fails; in that case the old executable may already have been replaced. Review -the named command error and rerun with a known working `--version`. The installer does not roll package files back. +The installer verifies the installed version and advertised commands before reporting success. A package can install successfully while command verification fails; in that case the old executable may already have been replaced. Review the named command error and rerun with a known working `--version`. The installer does not roll package files back. ## Retry dependency downloads with a mirror -Package indexes, uv binaries, and Python distributions are separate downloads. Changing the package index does not -change the uv or Python download location. +Package indexes, uv binaries, and Python distributions are separate downloads. Changing the package index does not change the uv or Python download location. | Download | Explicit setting | Automatic China source | Global source | | --- | --- | --- | --- | @@ -134,19 +108,9 @@ curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --region cn curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --index-url https://pypi.org/simple ``` -`--region auto|cn|global` overrides `POWERCONTEXT_INSTALL_REGION`. Auto selection uses a named local timezone, then the -locale territory, then global; it makes no geolocation request. An unavailable automatic mirror can fall back to the -official source. A reachable China package mirror remains selected even if it lacks the requested release; uv reports -resolution or artifact failures without retrying against another index. Use `--region global` or an explicit source -to select another index. Explicit sources never fall back automatically. -A mirror may lag PyPI: `latest` means the newest compatible stable release on the chosen index. +`--region auto|cn|global` overrides `POWERCONTEXT_INSTALL_REGION`. Auto selection uses a named local timezone, then the locale territory, then global; it makes no geolocation request. An unavailable automatic mirror can fall back to the official source. A reachable China package mirror remains selected even if it lacks the requested release; uv reports resolution or artifact failures without retrying against another index. Use `--region global` or an explicit source to select another index. Explicit sources never fall back automatically. A mirror may lag PyPI: `latest` means the newest compatible stable release on the chosen index. -Existing uv index settings and configuration files take precedence over automatic package mirrors. An explicit -`--index-url` overrides the default index only; additional uv indexes keep their priority. Private index credentials -belong in uv's authentication configuration, not in script arguments. uv does not read `PIP_INDEX_URL` or -`PIP_EXTRA_INDEX_URL`. Existing Python mirror settings and `uv.toml` also suppress automatic Python mirror selection. -`UV_ASTRAL_MIRROR_URL` is passed through for uv versions that support it. These settings are scoped to the installation; -your persistent package-manager configuration is unchanged. An existing uv is reused without upgrading it. +Existing uv index settings and configuration files take precedence over automatic package mirrors. An explicit `--index-url` overrides the default index only; additional uv indexes keep their priority. Private index credentials belong in uv's authentication configuration, not in script arguments. uv does not read `PIP_INDEX_URL` or `PIP_EXTRA_INDEX_URL`. Existing Python mirror settings and `uv.toml` also suppress automatic Python mirror selection. `UV_ASTRAL_MIRROR_URL` is passed through for uv versions that support it. These settings are scoped to the installation; your persistent package-manager configuration is unchanged. An existing uv is reused without upgrading it. To inspect the installer or pass several options in PowerShell, save it first: @@ -155,26 +119,22 @@ irm https://powercontext.oceanbase.io/install.ps1 -OutFile install.ps1 powershell -ExecutionPolicy Bypass -File .\install.ps1 --region cn --version 1.4.1 ``` -Cached reinstallation with `UV_OFFLINE=1` can work when uv, compatible Python, and all required packages are already -present. This is not an offline distribution bundle; missing downloads fail explicitly. +Cached reinstallation with `UV_OFFLINE=1` can work when uv, compatible Python, and all required packages are already present. This is not an offline distribution bundle; missing downloads fail explicitly. ## Choose a version -These instructions use PowerContext 1.4.1. Keep the package and Agent integration on -the same version: package `1.4.1` and Git tag `powercontext-v1.4.1`. +These instructions use PowerContext 1.4.1. Keep the package and Agent integration on the same version: package `1.4.1` and Git tag `powercontext-v1.4.1`. ```bash uv tool install --force "powercontext[cli,server]==1.4.1" powercontext setup codex --ref powercontext-v1.4.1 ``` -Check the [capability matrix](../integrations/capabilities.md) for host support and maintenance status. -Capabilities marked `experimental` remain experimental in this release. +Check the [capability matrix](../integrations/capabilities.md) for host support and maintenance status. Capabilities marked `experimental` remain experimental in this release. ## Install the application -You need Python 3.11 or newer, Git, and [`uv`](https://docs.astral.sh/uv/) on macOS, Linux, or Windows. Then install -PowerContext from PyPI: +You need Python 3.11 or newer, Git, and [`uv`](https://docs.astral.sh/uv/) on macOS, Linux, or Windows. Then install PowerContext from PyPI: ```bash uv tool install --force "powercontext[cli,server]==1.4.1" @@ -186,17 +146,11 @@ For a source installation of the same version: uv tool install --force "powercontext[cli,server] @ git+https://github.com/oceanbase/powercontext.git@powercontext-v1.4.1" ``` -For a machine that only connects to a remote Server, the `cli` extra includes `setup`, `doctor`, and remote content -commands. Add `server` to run a local Server or use the `service` commands. If a command provider has a missing -dependency, the CLI reports the provider and missing module so the installation can be repaired. +For a machine that only connects to a remote Server, the `cli` extra includes `setup`, `doctor`, and remote content commands. Add `server` to run a local Server or use the `service` commands. If a command provider has a missing dependency, the CLI reports the provider and missing module so the installation can be repaired. -The Git command does not leave a repository checkout for you to manage. Git uses its normal credential configuration, -including credential helpers and SSH settings. For an SSH-based install, replace the HTTPS URL with the Git URL -approved for your environment. `--force` also refreshes an existing tool from the current commit behind the selected -Git ref; without it, `uv` may report the same requirement as already installed without fetching a newer `master`. +The Git command does not leave a repository checkout for you to manage. Git uses its normal credential configuration, including credential helpers and SSH settings. For an SSH-based install, replace the HTTPS URL with the Git URL approved for your environment. `--force` also refreshes an existing tool from the current commit behind the selected Git ref; without it, `uv` may report the same requirement as already installed without fetching a newer `master`. -To install another branch or tag, replace the ref after the final `@`. The `master` branch can include unreleased changes. -Follow the [guide for each integration](../integrations/index.md) for Agent installation, connection options, and verification, using the same ref as the Server. +To install another branch or tag, replace the ref after the final `@`. The `master` branch can include unreleased changes. Follow the [guide for each integration](../integrations/index.md) for Agent installation, connection options, and verification, using the same ref as the Server. ## Run the local Server @@ -214,9 +168,7 @@ Without environment variables or an environment file, the Server: `Ctrl-C` performs a clean shutdown. Restarting the command reopens the same database. -The Dashboard is an optional content viewer for personal use and demonstrations. It is disabled by default and needs -no separate frontend installation or model configuration. To enable it locally without a token, save these settings -in an environment file: +The Dashboard is an optional content viewer for personal use and demonstrations. It is disabled by default and needs no separate frontend installation or model configuration. To enable it locally without a token, save these settings in an environment file: ```dotenv POWERCONTEXT_SERVER_HTTP_HOST=127.0.0.1 @@ -224,9 +176,7 @@ POWERCONTEXT_SERVER_DASHBOARD_ENABLED=true POWERCONTEXT_SERVER_ACCESS_MODE=disabled ``` -To require authentication, set `POWERCONTEXT_SERVER_ACCESS_MODE=enforced` and set `POWERCONTEXT_SERVER_AUTH_TOKEN` to -your own long random credential. The [configuration wizard](configure-server-environment.md#local-dashboard-and-optional-authentication) -also offers this choice when enabling Dashboard locally. +To require authentication, set `POWERCONTEXT_SERVER_ACCESS_MODE=enforced` and set `POWERCONTEXT_SERVER_AUTH_TOKEN` to your own long random credential. The [configuration wizard](configure-server-environment.md#local-dashboard-and-optional-authentication) also offers this choice when enabling Dashboard locally. ```bash chmod 600 /path/to/powercontext.env @@ -234,39 +184,25 @@ powercontext config validate --env-file /path/to/powercontext.env powercontext server run --env-file /path/to/powercontext.env ``` -Open `http://127.0.0.1:8000/dashboard/home`, using the actual port if you change it. With authentication disabled, the -page opens directly. When enabled, sign in with the Server token and configure connected Agents to use it for API -and MCP requests. The CLI does not automatically load a directory's `.env` file. +Open `http://127.0.0.1:8000/dashboard/home`, using the actual port if you change it. With authentication disabled, the page opens directly. When enabled, sign in with the Server token and configure connected Agents to use it for API and MCP requests. The CLI does not automatically load a directory's `.env` file. -The first visit selects the Server default Scope. Pages are empty until content is saved. Save a Memory through an -Agent or public API, then refresh Memories in the same Scope. Experiences, skills, handoffs, and usage also come from -saved records. The Dashboard does not capture sessions, run generation, or approve candidates. The Dashboard and Agent -must use the same Server and Scope. +The first visit selects the Server default Scope. Pages are empty until content is saved. Save a Memory through an Agent or public API, then refresh Memories in the same Scope. Experiences, skills, handoffs, and usage also come from saved records. The Dashboard does not capture sessions, run generation, or approve candidates. The Dashboard and Agent must use the same Server and Scope. -Open **Profile** to read the saved profile, inspect **Version history**, or verify its sources. Selecting a historical -revision does not change the current profile. In **Handoff**, use **Export Markdown** on a collection entry or its -detail page to download that exact revision, including its full text, omissions, and citations. If sign-in expires, -sign in again to return to the selected detail, then repeat the download. +Open **Profile** to read the saved profile, inspect **Version history**, or verify its sources. Selecting a historical revision does not change the current profile. In **Handoff**, use **Export Markdown** on a collection entry or its detail page to download that exact revision, including its full text, omissions, and citations. If sign-in expires, sign in again to return to the selected detail, then repeat the download. -When authentication is enabled, all token holders use one identity. Multi-user RBAC deployments should leave the -Dashboard disabled and use the API, MCP, or host integrations. See [Deploy the Server](../operate/deploy-server.md) -for network and credential configuration. +When authentication is enabled, all token holders use one identity. Multi-user RBAC deployments should leave the Dashboard disabled and use the API, MCP, or host integrations. See [Deploy the Server](../operate/deploy-server.md) for network and credential configuration. -This minimal launch does not enable model-backed extraction or vector search. To generate and validate one explicit -environment file for those capabilities, continue with the -[Enable extraction and vector search](configure-models.md). +This minimal launch does not enable model-backed extraction or vector search. To generate and validate one explicit environment file for those capabilities, continue with the [Enable extraction and vector search](configure-models.md). ## Use embedded seekDB -Embedded seekDB is available on Linux and macOS when a compatible `pylibseekdb` wheel is available. Windows does not -support this embedded backend. Install or replace the tool with the optional seekDB extra: +Embedded seekDB is available on Linux and macOS when a compatible `pylibseekdb` wheel is available. Windows does not support this embedded backend. Install or replace the tool with the optional seekDB extra: ```bash uv tool install --force "powercontext[cli,server,seekdb]==1.4.1" ``` -When switching from SQLite, remove `POWERCONTEXT_SERVER_DATABASE_URL` from the Server process environment. An explicit -SQLAlchemy database URL is not valid for seekDB. Then select the backend and start the Server: +When switching from SQLite, remove `POWERCONTEXT_SERVER_DATABASE_URL` from the Server process environment. An explicit SQLAlchemy database URL is not valid for seekDB. Then select the backend and start the Server: ```bash unset POWERCONTEXT_SERVER_DATABASE_URL @@ -274,17 +210,9 @@ export POWERCONTEXT_SERVER_DATABASE_KIND=seekdb powercontext server run ``` -`server run` loads `.env` from the current directory when present. Export values in the shell to override that file, -pass `--env-file ` to select another file, or pass `--no-env-file` to ignore environment files. Process managers -and containers should normally provide an explicit environment instead of relying on their working directory. +`server run` loads `.env` from the current directory when present. Export values in the shell to override that file, pass `--env-file ` to select another file, or pass `--no-env-file` to ignore environment files. Process managers and containers should normally provide an explicit environment instead of relying on their working directory. -PowerContext defaults to a dedicated `powercontext` database and creates it before initializing application tables. -To select another database, set `POWERCONTEXT_SERVER_DATABASE_DATABASE=my_context`; a missing database is created, -and an existing one is reused. Names containing backticks are rejected before initialization. -Leave `POWERCONTEXT_SERVER_DATABASE_PATH` unset to store -the instance in the `seekdb` subdirectory of the PowerContext user data directory. If `POWERCONTEXT_HOME` is set, the -default is `$POWERCONTEXT_HOME/seekdb`; set `POWERCONTEXT_SERVER_DATABASE_PATH` only when a different location is -required. +PowerContext defaults to a dedicated `powercontext` database and creates it before initializing application tables. To select another database, set `POWERCONTEXT_SERVER_DATABASE_DATABASE=my_context`; a missing database is created, and an existing one is reused. Names containing backticks are rejected before initialization. Leave `POWERCONTEXT_SERVER_DATABASE_PATH` unset to store the instance in the `seekdb` subdirectory of the PowerContext user data directory. If `POWERCONTEXT_HOME` is set, the default is `$POWERCONTEXT_HOME/seekdb`; set `POWERCONTEXT_SERVER_DATABASE_PATH` only when a different location is required. In another terminal, verify that the Server and database are ready: @@ -304,34 +232,17 @@ export POWERCONTEXT_SERVER_DATABASE_DATABASE=test powercontext server run ``` -Keep the original `POWERCONTEXT_SERVER_DATABASE_PATH` or `POWERCONTEXT_HOME`. The instance directory and database name -are separate settings: changing either can select different data. Configure services and containers with the same values. +Keep the original `POWERCONTEXT_SERVER_DATABASE_PATH` or `POWERCONTEXT_HOME`. The instance directory and database name are separate settings: changing either can select different data. Configure services and containers with the same values. -If no database name was explicitly configured and `test` contains tables named with PowerContext's `pc_` prefix, -startup stops before creating application tables and asks you to choose a database. It never automatically selects, -copies, or deletes the old database. Explicitly choose `test` to retain old data, or `powercontext` to use the new database. -The configuration wizard also asks for the database name; existing users should enter `test`. +If no database name was explicitly configured and `test` contains tables named with PowerContext's `pc_` prefix, startup stops before creating application tables and asks you to choose a database. It never automatically selects, copies, or deletes the old database. Explicitly choose `test` to retain old data, or `powercontext` to use the new database. The configuration wizard also asks for the database name; existing users should enter `test`. -To move data, keep using `test` until you have a backup and a validated export/import procedure for your engine version. -Stop all source writers, provision the target database, export/import the schema and data using that procedure, verify -identities and relationships, and read the migrated data through PowerContext before switching the configuration. -Automatic database creation does not perform this migration. Do not assume an arbitrary SQL dump is compatible across -engine or schema versions; existing schemas remain subject to compatibility checks and explicit schema maintenance. -Code graph references identify their instance and database: copying relational rows alone does not migrate graph storage. -Include target Code index rebuilding and queries in your validation; explicitly retaining `test` preserves legacy graph identities. +To move data, keep using `test` until you have a backup and a validated export/import procedure for your engine version. Stop all source writers, provision the target database, export/import the schema and data using that procedure, verify identities and relationships, and read the migrated data through PowerContext before switching the configuration. Automatic database creation does not perform this migration. Do not assume an arbitrary SQL dump is compatible across engine or schema versions; existing schemas remain subject to compatibility checks and explicit schema maintenance. Code graph references identify their instance and database: copying relational rows alone does not migrate graph storage. Include target Code index rebuilding and queries in your validation; explicitly retaining `test` preserves legacy graph identities. -After switching, keep the old database without writers for a retention period you choose. Record the source and target -instance paths and names, backup location, stop-write and cutover times, validation results, and a review date. Before -manually deleting the source, confirm that no deployment uses it and that a usable backup remains. New writes to the -target are not copied back to the source, so keeping the source does not provide lossless rollback. PowerContext does -not automatically migrate data, send retention reminders, compare databases, or delete old databases. +After switching, keep the old database without writers for a retention period you choose. Record the source and target instance paths and names, backup location, stop-write and cutover times, validation results, and a review date. Before manually deleting the source, confirm that no deployment uses it and that a usable backup remains. New writes to the target are not copied back to the source, so keeping the source does not provide lossless rollback. PowerContext does not automatically migrate data, send retention reminders, compare databases, or delete old databases. ## Use OceanBase -Connect to an existing OceanBase **MySQL-compatible user tenant**. Prepare the selected database and initialize its -full Server schema through explicit maintenance before starting the Server. PowerContext does not provision an -OceanBase instance, tenant, or account. Ensure network access and complete the Server -installation steps above, then configure the connection: +Connect to an existing OceanBase **MySQL-compatible user tenant**. Prepare the selected database and initialize its full Server schema through explicit maintenance before starting the Server. PowerContext does not provision an OceanBase instance, tenant, or account. Ensure network access and complete the Server installation steps above, then configure the connection: ```bash export POWERCONTEXT_SERVER_DATABASE_KIND=oceanbase @@ -339,52 +250,23 @@ export POWERCONTEXT_SERVER_DATABASE_KIND=oceanbase export POWERCONTEXT_SERVER_DATABASE_URL='mysql+aoceanbase://user%40tenant:encoded-password@host:2881/?charset=utf8mb4' ``` -Use the tenant account and the actual direct-connection or OBProxy endpoint and port. Percent-encode reserved characters -in the URL username and password; for example, `user@tenant` becomes `user%40tenant`. The URL requires the -`mysql+aoceanbase` driver, an explicit port, and `charset=utf8mb4`. +Use the tenant account and the actual direct-connection or OBProxy endpoint and port. Percent-encode reserved characters in the URL username and password; for example, `user@tenant` becomes `user%40tenant`. The URL requires the `mysql+aoceanbase` driver, an explicit port, and `charset=utf8mb4`. -The example omits the database name and therefore selects `powercontext`. To select a custom database, put its name in -the URL path, for example `/team_context?charset=utf8mb4`. Existing URLs with an explicit database continue to select -that database. Select the database only in the URL path; `db` and `database` query parameters are rejected. -Database identifiers support 1–64 BMP characters, excluding control characters, slash, backslash, dot, backticks, -leading/trailing whitespace, and system database names; names are never silently trimmed or renamed by PowerContext. The server's identifier case rules still apply. -Names containing backticks are rejected during configuration validation, before connecting or creating a database. -Choose a name such as `powercontext`, `team_context`, or a Chinese name without backticks. +The example omits the database name and therefore selects `powercontext`. To select a custom database, put its name in the URL path, for example `/team_context?charset=utf8mb4`. Existing URLs with an explicit database continue to select that database. Select the database only in the URL path; `db` and `database` query parameters are rejected. Database identifiers support 1–64 BMP characters, excluding control characters, slash, backslash, dot, backticks, leading/trailing whitespace, and system database names; names are never silently trimmed or renamed by PowerContext. The server's identifier case rules still apply. Names containing backticks are rejected during configuration validation, before connecting or creating a database. Choose a name such as `powercontext`, `team_context`, or a Chinese name without backticks. -Python SDK initialization first connects to the selected database. If it is usable, no database-creation privileges are required. Only a -confirmed unknown-database error triggers an instance/tenant connection and `CREATE DATABASE IF NOT EXISTS`, followed -by a new connection to the selected database and application-table initialization. Authentication and network errors -do not trigger creation or fallback. If access is denied without proving whether the database exists, the error asks -an administrator to check both existence and privileges. +Python SDK initialization first connects to the selected database. If it is usable, no database-creation privileges are required. Only a confirmed unknown-database error triggers an instance/tenant connection and `CREATE DATABASE IF NOT EXISTS`, followed by a new connection to the selected database and application-table initialization. Authentication and network errors do not trigger creation or fallback. If access is denied without proving whether the database exists, the error asks an administrator to check both existence and privileges. -For a new full Server deployment, create the selected database first. If SDK initialization cannot create a missing -database, the same administrator step applies. Run the following in the **same tenant**, using your configured name, -then grant the application account the required database privileges: +For a new full Server deployment, create the selected database first. If SDK initialization cannot create a missing database, the same administrator step applies. Run the following in the **same tenant**, using your configured name, then grant the application account the required database privileges: ```sql CREATE DATABASE IF NOT EXISTS `powercontext` CHARACTER SET utf8mb4; ``` -For the full Server, follow [database migrations](../operate/database-migrations.md) to plan, apply, and verify the -registered Server bundle with an explicit evidence directory and coordination mode. Start `powercontext server run` -only after the database is ready. Server startup checks readiness and does not initialize OceanBase schemas or -automatically migrate existing databases. SDK callers can retry initialization using the same URL after provisioning. -Creating missing tables requires table-creation privileges; an existing fully initialized database does not acquire a -new database-creation privilege requirement. Initialization never drops or -recreates an existing database, never switches to another database after a failure, and does not authorize schema upgrades. -Concurrent creation uses idempotent database/table DDL and handles duplicate-index creation races. This does not provide -coordination for existing-schema migrations or change embedded seekDB's process-ownership restrictions. - -For Python callers, `OceanBaseProfile.open()` resolves an omitted URL database and provisions it when necessary. -`OceanBaseProfile.open_existing()` connects only to an already provisioned database, and `attach()` requires a -caller-owned engine that already selects an existing database; neither replaces that engine or provisions a different -connection target. Read-only inspection (`plan`, `verify`, and the `db-migrate` inspection commands) never creates a -missing OceanBase or embedded seekDB database; explicit writable seekDB maintenance can provision its configured database -before applying its bundle. - -Read-only seekDB inspection requires an initialized instance directory. A missing or empty path is rejected before -starting the native engine, without creating parent directories or storage files. A file or a nonempty directory without -recognized native storage is also rejected without changing its contents. Ordinary startup still initializes new instances. +For the full Server, follow [database migrations](../operate/database-migrations.md) to plan, apply, and verify the registered Server bundle with an explicit evidence directory and coordination mode. Start `powercontext server run` only after the database is ready. Server startup checks readiness and does not initialize OceanBase schemas or automatically migrate existing databases. SDK callers can retry initialization using the same URL after provisioning. Creating missing tables requires table-creation privileges; an existing fully initialized database does not acquire a new database-creation privilege requirement. Initialization never drops or recreates an existing database, never switches to another database after a failure, and does not authorize schema upgrades. Concurrent creation uses idempotent database/table DDL and handles duplicate-index creation races. This does not provide coordination for existing-schema migrations or change embedded seekDB's process-ownership restrictions. + +For Python callers, `OceanBaseProfile.open()` resolves an omitted URL database and provisions it when necessary. `OceanBaseProfile.open_existing()` connects only to an already provisioned database, and `attach()` requires a caller-owned engine that already selects an existing database; neither replaces that engine or provisions a different connection target. Read-only inspection (`plan`, `verify`, and the `db-migrate` inspection commands) never creates a missing OceanBase or embedded seekDB database; explicit writable seekDB maintenance can provision its configured database before applying its bundle. + +Read-only seekDB inspection requires an initialized instance directory. A missing or empty path is rejected before starting the native engine, without creating parent directories or storage files. A file or a nonempty directory without recognized native storage is also rejected without changing its contents. Ordinary startup still initializes new instances. ## Verify the installation @@ -394,14 +276,9 @@ powercontext ready powercontext capabilities ``` -`doctor` checks the installed package, Server liveness, and Server readiness without requiring an integration. Server -readiness covers the database and each configured inference provider. Runtime or database failures return -`not_ready`; an inference failure returns `degraded` without removing database-backed operations from traffic. -`ready` and `capabilities` show the readiness and enabled capabilities of the running service. -For Agent diagnostics, use the [guide for each integration](../integrations/index.md). For Server status definitions and recovery steps, see [Troubleshoot](../operate/troubleshoot.md). +`doctor` checks the installed package, Server liveness, and Server readiness without requiring an integration. Server readiness covers the database and each configured inference provider. Runtime or database failures return `not_ready`; an inference failure returns `degraded` without removing database-backed operations from traffic. `ready` and `capabilities` show the readiness and enabled capabilities of the running service. For Agent diagnostics, use the [guide for each integration](../integrations/index.md). For Server status definitions and recovery steps, see [Troubleshoot](../operate/troubleshoot.md). -For a long-running process, Docker, authentication, or remote access, continue with -[Deploy the Server](../operate/deploy-server.md). +For a long-running process, Docker, authentication, or remote access, continue with [Deploy the Server](../operate/deploy-server.md). ## Update or replace an installation @@ -409,21 +286,11 @@ For a long-running process, Docker, authentication, or remote access, continue w Upgrades from 1.3.0 and 1.4.0 require passing full Server acceptance on SQLite, seekDB and OceanBase. These migration paths are not yet verified; do not upgrade existing databases. -Keep existing deployments on their working version until their source version and backend have an accepted upgrade -path. To evaluate a fresh installation, use a separate program environment, database location, and port. Replacing -the installed package does not migrate its database. Backups do not make an unsupported migration supported. +Keep existing deployments on their working version until their source version and backend have an accepted upgrade path. To evaluate a fresh installation, use a separate program environment, database location, and port. Replacing the installed package does not migrate its database. Backups do not make an unsupported migration supported. -The support declaration is checked against the release matrix and installed-artifact acceptance; see -[Preparing a release](../../development/releasing.md). Domain-specific -[Processing](../operate/artifact-processing-migration.md) and -[Atomic Memory](../operate/atomic-memory-migration.md) maintenance guides do not establish full Server readiness. +The support declaration is checked against the release matrix and installed-artifact acceptance; see [Preparing a release](../../development/releasing.md). Domain-specific [Processing](../operate/artifact-processing-migration.md) and [Atomic Memory](../operate/atomic-memory-migration.md) maintenance guides do not establish full Server readiness. -Review the [Dream coordinated upgrade requirements](../workflows/review-dream-revisions.md#coordinated-upgrade) -before upgrading an existing database. The unified Candidate routes replace `/v1/artifact-candidates/*` with -`/v1/candidates/*` without aliases, and Dream Clients require contract 2. Existing Dream storage needs an explicit -migration after unfinished runs have drained. [Full Server migration](../operate/database-migrations.md) requires -`state=ready` and `server_ready=true`; partial Artifact or Atomic Memory verification cannot replace it. Do not bypass -the migration gate. +Review the [Dream coordinated upgrade requirements](../workflows/review-dream-revisions.md#coordinated-upgrade) before upgrading an existing database. The unified Candidate routes replace `/v1/artifact-candidates/*` with `/v1/candidates/*` without aliases, and Dream Clients require contract 2. Existing Dream storage needs an explicit migration after unfinished runs have drained. [Full Server migration](../operate/database-migrations.md) requires `state=ready` and `server_ready=true`; partial Artifact or Atomic Memory verification cannot replace it. Do not bypass the migration gate. To install 1.4.1 in a separate environment for a fresh deployment: @@ -438,9 +305,7 @@ To install another Git ref in the separate environment: uv pip install --python /path/to/powercontext-preview/bin/python "powercontext[cli,server] @ git+https://github.com/oceanbase/powercontext.git@" ``` -Use the matching ref for clients and Agent integrations connected to the fresh deployment. Preserve the original -deployment's database location and configuration; do not point the new Server at it as an installation check. -On Windows, the virtual environment interpreter is at `Scripts/python.exe`. +Use the matching ref for clients and Agent integrations connected to the fresh deployment. Preserve the original deployment's database location and configuration; do not point the new Server at it as an installation check. On Windows, the virtual environment interpreter is at `Scripts/python.exe`. ## Install a Python role @@ -450,6 +315,4 @@ An application that imports the async Client SDK should add it to that applicati uv add "powercontext[client]==1.4.1" ``` -Use `builtin` for in-process Python composition, `server` for the service, `client` for the Python SDK, or `cli` for -the Server-backed command line. An extra that is only present in the isolated `uv tool` environment is not importable -by an unrelated Python project. +Use `builtin` for in-process Python composition, `server` for the service, `client` for the Python SDK, or `cli` for the Server-backed command line. An extra that is only present in the isolated `uv tool` environment is not importable by an unrelated Python project. diff --git a/docs/en/docs/get-started/quickstart.md b/docs/en/docs/get-started/quickstart.md index 076654b735..5fe5a563c9 100644 --- a/docs/en/docs/get-started/quickstart.md +++ b/docs/en/docs/get-started/quickstart.md @@ -5,16 +5,9 @@ description: Configure full memory, connect Codex, and verify Source capture, To # Quick Start -Start with installation, discuss a project in Codex, watch its input become Source evidence and an evolving topic, -then recover the decisions in a new session. These instructions use PowerContext 1.4.1, -with the Agent plugin from the matching `powercontext-v1.4.1` tag. +Start with installation, discuss a project in Codex, watch its input become Source evidence and an evolving topic, then recover the decisions in a new session. These instructions use PowerContext 1.4.1, with the Agent plugin from the matching `powercontext-v1.4.1` tag. -You need macOS or Linux, Python 3.11+, Git, [uv](https://docs.astral.sh/uv/getting-started/installation/), -and an installed Codex CLI. Full memory also needs working Generation and Embedding model APIs: -prepare their base URLs, model names, and API keys. Signing into a Codex or Claude subscription does not automatically -provide these APIs to the PowerContext Server. Without separate model APIs, select basic memory to test explicit saves -and recall; that does not enable automatic Topic Memory. -To provision Python and uv automatically, use the [installation script](install-and-run.md#install-with-the-recommended-script). +You need macOS or Linux, Python 3.11+, Git, [uv](https://docs.astral.sh/uv/getting-started/installation/), and an installed Codex CLI. Full memory also needs working Generation and Embedding model APIs: prepare their base URLs, model names, and API keys. Signing into a Codex or Claude subscription does not automatically provide these APIs to the PowerContext Server. Without separate model APIs, select basic memory to test explicit saves and recall; that does not enable automatic Topic Memory. To provision Python and uv automatically, use the [installation script](install-and-run.md#install-with-the-recommended-script). ## 1. Install and open the wizard @@ -42,11 +35,7 @@ The wizard writes: | `.env` | Server, client, Agent, database, and model settings, including any configured credentials | | `.env.next-steps.md` | Startup, Scope creation, plugin connection, and checks for your choices | -The final screen shows the Dashboard URL and the SSH command when selected. If authentication is enabled, it also -shows a newly generated Server token once; later, look up `POWERCONTEXT_SERVER_AUTH_TOKEN` in `.env`. -These files can contain credentials; do not commit them. -If seekdb is still installing, the wizard waits with an activity indicator. Complete any reported dependency recovery -before starting the Server. Saving files or installing dependencies does not start the Server. +The final screen shows the Dashboard URL and the SSH command when selected. If authentication is enabled, it also shows a newly generated Server token once; later, look up `POWERCONTEXT_SERVER_AUTH_TOKEN` in `.env`. These files can contain credentials; do not commit them. If seekdb is still installing, the wizard waits with an activity indicator. Complete any reported dependency recovery before starting the Server. Saving files or installing dependencies does not start the Server. ## 2. Start the Server @@ -58,13 +47,7 @@ powercontext service install --env-file .env powercontext service status ``` -The native current-user service manages the Server lifecycle. Linux requires an available -`systemd --user` manager. Open the Dashboard URL printed by the wizard, using the port saved as -`POWERCONTEXT_SERVER_HTTP_PORT` in `.env`. With authentication disabled, the page opens directly. Otherwise, sign in -with the **Server token**, not a model API key. -An empty Dashboard is expected before you capture data. For development, debugging, temporary use, or systems without -a native user-service manager, use `powercontext server run --env-file .env`; stop the personal service first if it is -running. See [persistent personal services](../operate/deploy-server.md#run-a-persistent-personal-server). +The native current-user service manages the Server lifecycle. Linux requires an available `systemd --user` manager. Open the Dashboard URL printed by the wizard, using the port saved as `POWERCONTEXT_SERVER_HTTP_PORT` in `.env`. With authentication disabled, the page opens directly. Otherwise, sign in with the **Server token**, not a model API key. An empty Dashboard is expected before you capture data. For development, debugging, temporary use, or systems without a native user-service manager, use `powercontext server run --env-file .env`; stop the personal service first if it is running. See [persistent personal services](../operate/deploy-server.md#run-a-persistent-personal-server). Load the client settings and check the running service: @@ -77,21 +60,17 @@ powercontext ready powercontext capabilities ``` -Confirm readiness and the capabilities you selected. A `degraded` result means some dependencies still need attention; -it is not a successful full-memory check. `config validate` checks configuration, while these commands check the running service. +Confirm readiness and the capabilities you selected. A `degraded` result means some dependencies still need attention; it is not a successful full-memory check. `config validate` checks configuration, while these commands check the running service. ## 3. Create a Scope and install the Codex plugin -Open `.env.next-steps.md` and run the request under “Create the planned isolated Scopes”. The Server returns the real -`scope_id`. Add it to `.env`: +Open `.env.next-steps.md` and run the request under “Create the planned isolated Scopes”. The Server returns the real `scope_id`. Add it to `.env`: ```dotenv POWERCONTEXT_CODEX_SCOPE_ID=replace-with-returned-scope-id ``` -The planned `codex-xxxxxxxx` value is a title, not an ID. For Claude Code, put the ID returned by its creation request -in `POWERCONTEXT_CLAUDE_SCOPE_ID`. Agents can have separate Scopes or explicitly share an existing one. -Changing directories does not create isolation. Use the same Scope in the Dashboard and Agent during this check. +The planned `codex-xxxxxxxx` value is a title, not an ID. For Claude Code, put the ID returned by its creation request in `POWERCONTEXT_CLAUDE_SCOPE_ID`. Agents can have separate Scopes or explicitly share an existing one. Changing directories does not create isolation. Use the same Scope in the Dashboard and Agent during this check. After editing `.env`, reconcile the service, reload the client settings, and install the matching plugin: @@ -108,29 +87,19 @@ codex Before sending the first message in the new session: -1. Open `/hooks`, inspect the PowerContext hooks, and grant trust when prompted. `SessionStart` resolves the Session Scope; - `UserPromptSubmit` automatically prepares relevant context and captures your input as a Source. +1. Open `/hooks`, inspect the PowerContext hooks, and grant trust when prompted. `SessionStart` resolves the Session Scope; `UserPromptSubmit` automatically prepares relevant context and captures your input as a Source. 2. Confirm that PowerContext MCP tools are available. Tool discovery and Hook execution are separate checks. -3. If you skipped or denied trust, use the [Hook recovery steps](../integrations/codex.md#first-session-hook-trust) - before testing capture. Without an enabled `UserPromptSubmit` hook, automatic capture and recall do not run. +3. If you skipped or denied trust, use the [Hook recovery steps](../integrations/codex.md#first-session-hook-trust) before testing capture. Without an enabled `UserPromptSubmit` hook, automatic capture and recall do not run. -For a non-default address, follow the Codex connection instructions in `.env.next-steps.md`: the installed plugin's -`.mcp.json` must use the same Server as the Hook, with credentials configured when authentication is enabled. -Installing the plugin does not start the Server. `doctor codex` checks installation, connection, and tool coverage; -it does not establish Hook trust, execution, or successful memory processing. +For a non-default address, follow the Codex connection instructions in `.env.next-steps.md`: the installed plugin's `.mcp.json` must use the same Server as the Hook, with credentials configured when authentication is enabled. Installing the plugin does not start the Server. `doctor codex` checks installation, connection, and tool coverage; it does not establish Hook trust, execution, or successful memory processing. -These commands use Codex CLI. A desktop app may not inherit terminal environment variables. Before testing in the -desktop app, verify that both its Hook and MCP receive the same URL and Scope, and credentials when authentication -is enabled. See [Codex](../integrations/codex.md) and [Claude Code](../integrations/claude-code.md) for host-specific behavior. +These commands use Codex CLI. A desktop app may not inherit terminal environment variables. Before testing in the desktop app, verify that both its Hook and MCP receive the same URL and Scope, and credentials when authentication is enabled. See [Codex](../integrations/codex.md) and [Claude Code](../integrations/claude-code.md) for host-specific behavior. -Full memory also needs a generation policy for this real Scope to use Profile. Follow the -[Profile policy steps](configure-models.md#enable-a-profile-policy-for-the-scope) to read its current version and update it; -saving the configuration does not perform this operation. +Full memory also needs a generation policy for this real Scope to use Profile. Follow the [Profile policy steps](configure-models.md#enable-a-profile-policy-for-the-scope) to read its current version and update it; saving the configuration does not perform this operation. ## 4. Verify memory in your selected mode -Both modes can capture Source evidence when the Hook is enabled. Basic memory needs no separate Server model API; -the Agent still uses its own model. Choose the check matching your configuration. +Both modes can capture Source evidence when the Hook is enabled. Basic memory needs no separate Server model API; the Agent still uses its own model. Choose the check matching your configuration. ### Basic memory: explicitly save and retrieve @@ -147,8 +116,7 @@ Check the actual write result, then open a new session with the same Scope and a Search PowerContext for Aurora's Python dependency-management decision, with its citation. ``` -Verify the saved content and citation. A conversational acknowledgement is not proof of a write; this check verifies -explicit save and retrieval, not automatic extraction or Topic Memory generation. +Verify the saved content and citation. A conversational acknowledgement is not proof of a write; this check verifies explicit save and retrieval, not automatic extraction or Topic Memory generation. ### Full memory: Source processing and Topic Memory @@ -168,12 +136,9 @@ Select the bound Scope in the Dashboard and check the following: 4. Confirm that the second Source arrived and that topic content or revision history reflects the update. Waiting one minute is not itself a check. 5. Open a new Codex session with the same Scope and ask: “Find Aurora's dependency-management and deployment decisions in PowerContext, with citations.” -Correct content and its citation complete the capture → evolution → cross-session recall check. A successful -`doctor codex` only checks installation and connectivity; it does not prove this workflow. The model may merge evidence -or decide that no update is needed, so not every message creates a new memory. +Correct content and its citation complete the capture → evolution → cross-session recall check. A successful `doctor codex` only checks installation and connectivity; it does not prove this workflow. The model may merge evidence or decide that no update is needed, so not every message creates a new memory. -Full memory does not generate every Artifact from every message. Profile, Experience, and Skill have additional -triggers or review requirements; see [Capability behavior](configure-models.md#full-capabilities-do-not-generate-every-artifact-from-every-message). +Full memory does not generate every Artifact from every message. Profile, Experience, and Skill have additional triggers or review requirements; see [Capability behavior](configure-models.md#full-capabilities-do-not-generate-every-artifact-from-every-message). | Check | Evidence of success | | --- | --- | @@ -185,10 +150,7 @@ triggers or review requirements; see [Capability behavior](configure-models.md#f ## 5. Choose your next workflow -The seven Artifact families have different inputs and consumption paths. Source is evidence and Tag is metadata; -neither is an Artifact family. The examples below are requests, not guaranteed keyword triggers. Use the actual tools -available in your connection; follow the linked HTTP/CLI workflow for operations without a dedicated MCP tool. -Read the [Artifact overview](faq.md#artifact-families-at-a-glance) for conceptual differences. +The seven Artifact families have different inputs and consumption paths. Source is evidence and Tag is metadata; neither is an Artifact family. The examples below are requests, not guaranteed keyword triggers. Use the actual tools available in your connection; follow the linked HTTP/CLI workflow for operations without a dedicated MCP tool. Read the [Artifact overview](faq.md#artifact-families-at-a-glance) for conceptual differences. | Capability and purpose | Request or starting point | Requirements and result | | --- | --- | --- | @@ -204,16 +166,9 @@ Read the [Artifact overview](faq.md#artifact-families-at-a-glance) for conceptua ### Plugin guidance and managed Skills -The installed plugin includes `powercontext-project-context`, which routes requests to the relevant workflow. It is -separate from a managed Skill stored in PowerContext. If the routing Skill is absent from the host catalog, check plugin -installation; a natural-language example does not add missing tools or grant permission. +The installed plugin includes `powercontext-project-context`, which routes requests to the relevant workflow. It is separate from a managed Skill stored in PowerContext. If the routing Skill is absent from the host catalog, check plugin installation; a natural-language example does not add missing tools or grant permission. -For a generated managed Skill, inspect the Candidate and obtain an authorized review decision. Approval creates an -immutable Revision; it does not install or execute it. Follow [Create and export a Skill](../workflows/create-and-export-skill.md) -to export the exact approved Revision with `powercontext skill export --target codex` to the intended host-local -`.agents/skills/` directory. Confirm that Codex discovers the exported Skill; restart if it does not appear. -Direct Artifact writes have their own contract; the Candidate sequence here describes generation and proposal workflows. -Review authority depends on permissions, not on whether the reviewer is human. Discovery does not grant execution authority. +For a generated managed Skill, inspect the Candidate and obtain an authorized review decision. Approval creates an immutable Revision; it does not install or execute it. Follow [Create and export a Skill](../workflows/create-and-export-skill.md) to export the exact approved Revision with `powercontext skill export --target codex` to the intended host-local `.agents/skills/` directory. Confirm that Codex discovers the exported Skill; restart if it does not appear. Direct Artifact writes have their own contract; the Candidate sequence here describes generation and proposal workflows. Review authority depends on permissions, not on whether the reviewer is human. Discovery does not grant execution authority. ## Continue diff --git a/docs/en/rfcs/1892-script-installation.md b/docs/en/rfcs/1892-script-installation.md index d41e23b2a9..291e3baaec 100644 --- a/docs/en/rfcs/1892-script-installation.md +++ b/docs/en/rfcs/1892-script-installation.md @@ -10,29 +10,17 @@ description: Install the latest or an exact release with Bash and PowerShell, in # Summary -Provide distribution-owned Bash and PowerShell entry points that install PowerContext through uv. The default -release is the latest stable version from the selected package index; `--version` selects an exact release. -The scripts provision missing uv and Python, install the selected Runtime profile, and optionally invoke the -existing Agent integration adapters with a tag matching the installed Runtime. +Provide distribution-owned Bash and PowerShell entry points that install PowerContext through uv. The default release is the latest stable version from the selected package index; `--version` selects an exact release. The scripts provision missing uv and Python, install the selected Runtime profile, and optionally invoke the existing Agent integration adapters with a tag matching the installed Runtime. -Installation, configuration, diagnostics, and service operation have separate responsibilities. Installing software -does not configure inference providers, start a Server, or register a persistent service. Configuration and service -operation remain separate existing CLI steps. The installation guide documents the scripts alongside the version-pinned -uv commands used in the READMEs and Quick Start. +Installation, configuration, diagnostics, and service operation have separate responsibilities. Installing software does not configure inference providers, start a Server, or register a persistent service. Configuration and service operation remain separate existing CLI steps. The installation guide documents the scripts alongside the version-pinned uv commands used in the READMEs and Quick Start. # Motivation -A normal installation should not require users to coordinate Python discovery, uv installation, package extras, -package indexes, and integration Git refs. Package downloads, uv binaries, and Python distributions also use different -sources: changing the PyPI index alone cannot fix an inaccessible Python download. +A normal installation should not require users to coordinate Python discovery, uv installation, package extras, package indexes, and integration Git refs. Package downloads, uv binaries, and Python distributions also use different sources: changing the PyPI index alone cannot fix an inaccessible Python download. -Installation places a versioned component and registers explicitly selected integrations. Configuration controls -mutable values such as Server URL, Scope, and capture policy. Diagnostics observe the resulting environment. -Keeping these responsibilities distinct lets installation improve independently of configuration storage, shared -Hooks, an operations tool, or service lifecycle changes. +Installation places a versioned component and registers explicitly selected integrations. Configuration controls mutable values such as Server URL, Scope, and capture policy. Diagnostics observe the resulting environment. Keeping these responsibilities distinct lets installation improve independently of configuration storage, shared Hooks, an operations tool, or service lifecycle changes. -The intended outcome is one installation entry point per shell, reuse of existing dependencies, predictable version -selection, explicit host selection, recoverable partial failures, and verification through the installed product. +The intended outcome is one installation entry point per shell, reuse of existing dependencies, predictable version selection, explicit host selection, recoverable partial failures, and verification through the installed product. # Guide-level explanation @@ -50,56 +38,35 @@ On Windows, using PowerShell 5.1 or newer: powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` -`local`, the default profile, installs CLI, Client, and local Server dependencies. `--profile client` installs CLI and -Client dependencies for an existing Server. Profile and host selection are independent: choosing a database role does -not select an Agent host, and selecting Codex does not change the Runtime profile. +`local`, the default profile, installs CLI, Client, and local Server dependencies. `--profile client` installs CLI and Client dependencies for an existing Server. Profile and host selection are independent: choosing a database role does not select an Agent host, and selecting Codex does not change the Runtime profile. -The installer reuses uv and compatible system or uv-managed Python. Otherwise it provisions uv and Python 3.12 in -user-owned locations. It prints the actual Runtime version, profile, executable directories, PATH command, and next -configuration command. It does not change persistent PATH settings. Apply the printed PATH command before continuing. +The installer reuses uv and compatible system or uv-managed Python. Otherwise it provisions uv and Python 3.12 in user-owned locations. It prints the actual Runtime version, profile, executable directories, PATH command, and next configuration command. It does not change persistent PATH settings. Apply the printed PATH command before continuing. ## Select releases and integrations -`--version latest` is the default. Repeating it checks for newer stable releases on the configured index. For a -repeatable package selection, use an exact version: +`--version latest` is the default. Repeating it checks for newer stable releases on the configured index. For a repeatable package selection, use an exact version: ```bash curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.2.0 --host codex ``` -Installation runs unattended by default, without Git or a host picker. `--host` is repeatable and opts into integration -setup; selected hosts require Git and their own prerequisites. `--no-hosts` explicitly states the default and cannot -be combined with `--host`. For an interactive picker after installation, run -`powercontext setup --ref "powercontext-v$(powercontext --version)"`. +Installation runs unattended by default, without Git or a host picker. `--host` is repeatable and opts into integration setup; selected hosts require Git and their own prerequisites. `--no-hosts` explicitly states the default and cannot be combined with `--host`. For an interactive picker after installation, run `powercontext setup --ref "powercontext-v$(powercontext --version)"`. -The installed CLI supplies the exact version for `powercontext-vVERSION`. Host setup never resolves `latest` again -and never defaults to `master`. If a host installation fails, the Runtime remains installed and the script returns -nonzero with instructions to retry setup at that tag. Each adapter supplies its existing host-specific result. +The installed CLI supplies the exact version for `powercontext-vVERSION`. Host setup never resolves `latest` again and never defaults to `master`. If a host installation fails, the Runtime remains installed and the script returns nonzero with instructions to retry setup at that tag. Each adapter supplies its existing host-specific result. -For personal macOS/Linux, configure a protected file, then run `powercontext service install --env-file .env`, -`powercontext service status`, then load the generated Client settings for `powercontext ready` and `powercontext capabilities`. The native user manager owns the Server lifecycle. -Development, debugging, temporary use, and unavailable managers keep the foreground `server run` path. -Client-only installations follow the remote connection guide. Service registration remains explicit. +For personal macOS/Linux, configure a protected file, then run `powercontext service install --env-file .env`, `powercontext service status`, then load the generated Client settings for `powercontext ready` and `powercontext capabilities`. The native user manager owns the Server lifecycle. Development, debugging, temporary use, and unavailable managers keep the foreground `server run` path. Client-only installations follow the remote connection guide. Service registration remains explicit. ## Configuration and personal services -Configuration and service registration use the existing CLI in separate steps. Run `config init --output .env`, -save the configuration, then run `config validate --env-file .env`, `service install --env-file .env`, and `service status`. -Load the generated Client settings before running `ready` and `capabilities`. See the -[installation guide](../docs/get-started/install-and-run.md#configure-and-install-a-personal-service) for the full sequence. +Configuration and service registration use the existing CLI in separate steps. Run `config init --output .env`, save the configuration, then run `config validate --env-file .env`, `service install --env-file .env`, and `service status`. Load the generated Client settings before running `ready` and `capabilities`. See the [installation guide](../docs/get-started/install-and-run.md#configure-and-install-a-personal-service) for the full sequence. -The installer does not run the configuration wizard or manage a service. Cancelling the wizard cannot cause the -installer to continue with service registration. Service ownership, supported managers, protected files, login startup, -and recovery remain the existing service layer's responsibility. Windows personal services remain experimental. +The installer does not run the configuration wizard or manage a service. Cancelling the wizard cannot cause the installer to continue with service registration. Service ownership, supported managers, protected files, login startup, and recovery remain the existing service layer's responsibility. Windows personal services remain experimental. -`--env-file PATH` selects an existing connection file only for explicitly selected host setup. It is passed to -`setup --env-file PATH HOST...` at the installed release tag; the shell does not parse or source it. After editing a -service configuration, including Scope IDs, follow the service guide using the original file. +`--env-file PATH` selects an existing connection file only for explicitly selected host setup. It is passed to `setup --env-file PATH HOST...` at the installed release tag; the shell does not parse or source it. After editing a service configuration, including Scope IDs, follow the service guide using the original file. ## Choose download sources -`--region auto|cn|global` overrides `POWERCONTEXT_INSTALL_REGION`. Automatic selection checks local named timezone, -then locale territory, then uses global sources. It does not call a network location service. +`--region auto|cn|global` overrides `POWERCONTEXT_INSTALL_REGION`. Automatic selection checks local named timezone, then locale territory, then uses global sources. It does not call a network location service. | Component | Global source | Automatic China source | Explicit control | | --- | --- | --- | --- | @@ -107,18 +74,13 @@ then locale territory, then uses global sources. It does not call a network loca | uv installer and binaries | Astral channels | USTC release mirror | `POWERCONTEXT_UV_INSTALLER_URL`, `UV_DOWNLOAD_URL`, uv installer mirror variables | | Python | uv default channels | NJU python-build-standalone mirror | `UV_PYTHON_INSTALL_MIRROR`, uv Python download configuration | -Existing uv configuration suppresses automatic package mirror selection. An explicit `--index-url` changes only the -default index; additional indexes keep uv's priority rules. uv owns authentication and index resolution. Script URL -arguments must use HTTPS and omit credentials; credentials belong in uv configuration. pip index environment variables -are not uv configuration. Python and uv download overrides are independent of package indexes. +Existing uv configuration suppresses automatic package mirror selection. An explicit `--index-url` changes only the default index; additional indexes keep uv's priority rules. uv owns authentication and index resolution. Script URL arguments must use HTTPS and omit credentials; credentials belong in uv configuration. pip index environment variables are not uv configuration. Python and uv download overrides are independent of package indexes. # Reference-level explanation ## Installation responsibilities -The entry points are `website/public/install.sh` and `website/public/install.ps1`, served as static website assets. -Their supported options and failure behavior are equivalent. They own bootstrap and sequencing; uv owns environment -creation and package resolution, while existing `powercontext setup` adapters own integration installation. +The entry points are `website/public/install.sh` and `website/public/install.ps1`, served as static website assets. Their supported options and failure behavior are equivalent. They own bootstrap and sequencing; uv owns environment creation and package resolution, while existing `powercontext setup` adapters own integration installation. The effective installation inputs are: @@ -126,154 +88,77 @@ The effective installation inputs are: Package requirement + Runtime profile + explicit Host selection = installation operation ``` -The package manager resolves the requirement before replacing the tool environment. Once installed, the CLI's exact -version is verified and used for all host setup. This keeps one release coordinate for the operation without adding a -second package resolver or introducing a release manifest dependency. +The package manager resolves the requirement before replacing the tool environment. Once installed, the CLI's exact version is verified and used for all host setup. This keeps one release coordinate for the operation without adding a second package resolver or introducing a release manifest dependency. -The scripts preserve `powercontext setup`, `config`, `doctor`, and service commands. They do not move domain behavior -into shell, add a standalone Python installer engine, or change Runtime/public HTTP APIs. +The scripts preserve `powercontext setup`, `config`, `doctor`, and service commands. They do not move domain behavior into shell, add a standalone Python installer engine, or change Runtime/public HTTP APIs. ## Version and profile semantics -- `latest` passes an unpinned profile requirement to `uv tool install --upgrade --force --prerelease disallow`. It means the - newest stable version compatible with the selected interpreter and configured sources, not necessarily the version - most recently uploaded to another mirror. A fresh tool environment prevents an installed prerelease from bypassing stable selection in older uv versions. Resolution failures - are reported without substituting another requirement. -- An exact `X.Y.Z`, optionally followed by `aN`, `bN`, or `rcN`, uses `==VERSION`. Explicit prereleases are supported; - source refs, ranges, and URLs are not `--version` values. Releases before 0.1.0 are excluded. -- `local` uses `powercontext[cli,server]`; `client` uses `powercontext[cli]`, whose dependencies include the Client. - Rerunning with a different profile replaces that tool's dependency set. Independently added extras must be managed - through the documented manual installation path. -- The executable in `uv tool dir --bin` must report a release version. Exact requests must match it. All profiles - must expose CLI help and `capabilities --help`; `local` additionally requires - `config init --help` and `server run --help`. The script prints success only after these commands succeed. - These checks establish imports and command availability, not Server readiness or Agent workflow correctness. - -uv is bootstrapped at an installer-controlled version; that version is independent of the PowerContext release. -An existing uv is reused rather than upgraded silently. Python discovery excludes virtual environments so a project -venv cannot become an accidental installation prerequisite. If no compatible Python 3.11+ is found, uv provisions -Python 3.12. uv remains responsible for platform, architecture, and wheel compatibility errors. +- `latest` passes an unpinned profile requirement to `uv tool install --upgrade --force --prerelease disallow`. It means the newest stable version compatible with the selected interpreter and configured sources, not necessarily the version most recently uploaded to another mirror. A fresh tool environment prevents an installed prerelease from bypassing stable selection in older uv versions. Resolution failures are reported without substituting another requirement. +- An exact `X.Y.Z`, optionally followed by `aN`, `bN`, or `rcN`, uses `==VERSION`. Explicit prereleases are supported; source refs, ranges, and URLs are not `--version` values. Releases before 0.1.0 are excluded. +- `local` uses `powercontext[cli,server]`; `client` uses `powercontext[cli]`, whose dependencies include the Client. Rerunning with a different profile replaces that tool's dependency set. Independently added extras must be managed through the documented manual installation path. +- The executable in `uv tool dir --bin` must report a release version. Exact requests must match it. All profiles must expose CLI help and `capabilities --help`; `local` additionally requires `config init --help` and `server run --help`. The script prints success only after these commands succeed. These checks establish imports and command availability, not Server readiness or Agent workflow correctness. + +uv is bootstrapped at an installer-controlled version; that version is independent of the PowerContext release. An existing uv is reused rather than upgraded silently. Python discovery excludes virtual environments so a project venv cannot become an accidental installation prerequisite. If no compatible Python 3.11+ is found, uv provisions Python 3.12. uv remains responsible for platform, architecture, and wheel compatibility errors. ## Mirror precedence and recovery -User-specified uv/Python sources are preserved without fallback. For automatic China selection, an unavailable uv -mirror installer falls back to Astral; failed mirrored uv artifacts may use official sources. Python's exact build -URL is obtained from uv, and its mirrored artifact is checked before installation. If the automatic Python mirror -fails, installation retries with uv's default channels. +User-specified uv/Python sources are preserved without fallback. For automatic China selection, an unavailable uv mirror installer falls back to Astral; failed mirrored uv artifacts may use official sources. Python's exact build URL is obtained from uv, and its mirrored artifact is checked before installation. If the automatic Python mirror fails, installation retries with uv's default channels. -Automatic China package selection makes a bounded availability request to the PowerContext index page. An -unreachable mirror falls back to PyPI before tool installation. A reachable mirror remains selected even if it lacks -the requested release; uv reports that resolution failure. Global and explicit sources go directly to uv. The scripts -never interpret index HTML, wheel filenames, encoded URLs, package compatibility, or authentication. They do not -switch indexes after a tool installation attempt, which may already have changed installed files. +Automatic China package selection makes a bounded availability request to the PowerContext index page. An unreachable mirror falls back to PyPI before tool installation. A reachable mirror remains selected even if it lacks the requested release; uv reports that resolution failure. Global and explicit sources go directly to uv. The scripts never interpret index HTML, wheel filenames, encoded URLs, package compatibility, or authentication. They do not switch indexes after a tool installation attempt, which may already have changed installed files. -Shell environment changes are local to the installer process; PowerShell restores temporary source variables in -`finally`. Existing configuration files are neither parsed by shell nor rewritten. Only installer-owned temporary -downloads are cleaned. `UV_INSTALL_DIR`, `UV_TOOL_DIR`, and `UV_TOOL_BIN_DIR` retain their location controls. +Shell environment changes are local to the installer process; PowerShell restores temporary source variables in `finally`. Existing configuration files are neither parsed by shell nor rewritten. Only installer-owned temporary downloads are cleaned. `UV_INSTALL_DIR`, `UV_TOOL_DIR`, and `UV_TOOL_BIN_DIR` retain their location controls. ## Persistence and compatibility -Ordinary package installation does not read or rewrite `.env`, credentials, data directories, or database schemas, -and does not stop or restart a service. Users configure and register services separately through the existing CLI, -and follow the existing upgrade and migration instructions when starting an updated -Server. An exact-version retry may reuse cached packages; `latest` intentionally allows upgrades. +Ordinary package installation does not read or rewrite `.env`, credentials, data directories, or database schemas, and does not stop or restart a service. Users configure and register services separately through the existing CLI, and follow the existing upgrade and migration instructions when starting an updated Server. An exact-version retry may reuse cached packages; `latest` intentionally allows upgrades. -`UV_OFFLINE=1` permits cached reinstallation only when uv, a compatible Python, and all dependencies are present. -It disables installer source probes as well as uv networking. It does not promise a complete offline distribution. -Prerequisites remain available after later failures. A resolution failure for an unavailable exact version preserves -the existing tool. Once uv accepts a package, verification can fail after replacing the old executable: report -"package installed, verification failed" and allow an explicit version retry, without claiming rollback. A later host -setup failure preserves the verified Runtime. There is no cross-component rollback or repair of unrelated host state. +`UV_OFFLINE=1` permits cached reinstallation only when uv, a compatible Python, and all dependencies are present. It disables installer source probes as well as uv networking. It does not promise a complete offline distribution. Prerequisites remain available after later failures. A resolution failure for an unavailable exact version preserves the existing tool. Once uv accepts a package, verification can fail after replacing the old executable: report "package installed, verification failed" and allow an explicit version retry, without claiming rollback. A later host setup failure preserves the verified Runtime. There is no cross-component rollback or repair of unrelated host state. -Windows retains its experimental product status. Native acceptance runs on Linux, macOS, and Windows; host-specific -support still comes from each integration's capability contract. +Windows retains its experimental product status. Native acceptance runs on Linux, macOS, and Windows; host-specific support still comes from each integration's capability contract. ## Executable contract and acceptance -`tests/fixtures/installation/*.json` defines shared public inputs and expected outcomes for both shell adapters. -`tests/test_installation_contract.py` executes those cases with real uv and small offline fixture wheels: host opt-in, -profiles, exact-version verification, unavailable commands, and partial host failure. Cases observe installed -capabilities, selected hosts, release refs and exit results. They do not fix internal function boundaries or call order. -The same catalog runs on each native operating system; fixture host execution is not native service or Agent -acceptance. Host cases cover passing an existing connection file. Configuration and personal-service lifecycle -acceptance remain separate from installer conformance. +`tests/fixtures/installation/*.json` defines shared public inputs and expected outcomes for both shell adapters. `tests/test_installation_contract.py` executes those cases with real uv and small offline fixture wheels: host opt-in, profiles, exact-version verification, unavailable commands, and partial host failure. Cases observe installed capabilities, selected hosts, release refs and exit results. They do not fix internal function boundaries or call order. The same catalog runs on each native operating system; fixture host execution is not native service or Agent acceptance. Host cases cover passing an existing connection file. Configuration and personal-service lifecycle acceptance remain separate from installer conformance. -`tests/native/test_installation.py` runs the actual shell installer, uv, installed PowerContext CLI, and HTTP Server. -A wheel built from the tested commit uses release-shaped metadata and a direct local file constraint. That constraint -selects the tested artifact; it does not establish an independently enforced checksum. Qualification includes: +`tests/native/test_installation.py` runs the actual shell installer, uv, installed PowerContext CLI, and HTTP Server. A wheel built from the tested commit uses release-shaped metadata and a direct local file constraint. That constraint selects the tested artifact; it does not establish an independently enforced checksum. Qualification includes: - missing uv/Python under global and China source selection, existing tools/configuration, and spaces/Unicode paths; -- real stable/prerelease wheel resolution through a controlled package index, default upgrade, exact selection, and - preserving the installed version when a requested release is unavailable; +- real stable/prerelease wheel resolution through a controlled package index, default upgrade, exact selection, and preserving the installed version when a requested release is unavailable; - Client-only installation and profile changes, with no local Server state created by installation; - `.env` generation and validation, readiness, Memory remember/search, cached offline reinstall, and restart readback; -- explicit index failure, conflicting host choices, unattended and piped Bash installation, and the downloaded - PowerShell scriptblock entry point. - -A separate CI matrix executes the suite on all three operating systems. Release reference checks preserve script -`latest` defaults while updating explicit version examples. Website validation confirms documentation links and the -static build. Syntax checks alone do not establish Windows or macOS installation support, and these tests do not -claim that a real Agent host completes its capture/recall workflow. +- explicit index failure, conflicting host choices, unattended and piped Bash installation, and the downloaded PowerShell scriptblock entry point. -[Reproducible first-principles and ablation studies](https://github.com/PsiACE/powercontext/tree/feat/installation-contract/experiments/installation) pin upstream -sources, installer baselines, uv versions, controlled inputs and observed results. They support these decisions: -removing implicit host selection preserves explicit host failure semantics; a filename-based preflight rejects valid -encoded package URLs; prerelease-only indexes require explicit stable policy; and an accepted package can still have -an unusable entry point. They remain separate from maintained product acceptance and carry their own execution limits. +A separate CI matrix executes the suite on all three operating systems. Release reference checks preserve script `latest` defaults while updating explicit version examples. Website validation confirms documentation links and the static build. Syntax checks alone do not establish Windows or macOS installation support, and these tests do not claim that a real Agent host completes its capture/recall workflow. # Drawbacks -Bash and PowerShell duplicate some sequencing and source-selection policy. Automatic mirrors add operational -availability dependencies. `latest` deliberately changes over time and is unsuitable for reproducible deployment -without an exact version. Matching tags coordinate releases but do not independently prove host compatibility. +Bash and PowerShell duplicate some sequencing and source-selection policy. Automatic mirrors add operational availability dependencies. `latest` deliberately changes over time and is unsuitable for reproducible deployment without an exact version. Matching tags coordinate releases but do not independently prove host compatibility. # Rationale and alternatives -A uv-only command is retained for users who already manage Python and uv; it cannot bootstrap a clean machine. -Source installation is useful for development but is not the default release channel. Pinning the public script to -a PowerContext version would leave new users on stale releases until the website is republished. +A uv-only command is retained for users who already manage Python and uv; it cannot bootstrap a clean machine. Source installation is useful for development but is not the default release channel. Pinning the public script to a PowerContext version would leave new users on stale releases until the website is republished. -A Python engine still needs a bootstrap on machines without Python. Generating the shell adapters would still require -native behavioral tests and would add generator/versioning machinery. Shared conformance cases provide executable -parity without another runtime asset. Introduce an engine or generator only when a concrete policy needs it. +A Python engine still needs a bootstrap on machines without Python. Generating the shell adapters would still require native behavioral tests and would add generator/versioning machinery. Shared conformance cases provide executable parity without another runtime asset. Introduce an engine or generator only when a concrete policy needs it. -Immutable manifests and a second installation ledger are useful when distributing independently versioned raw -artifacts. Here uv owns wheel resolution, tool environments and its records. The installer observes the actual launcher -and delegates repair to an explicit uv-backed retry. This boundary avoids two authorities for the same installation. +Immutable manifests and a second installation ledger are useful when distributing independently versioned raw artifacts. Here uv owns wheel resolution, tool environments and its records. The installer observes the actual launcher and delegates repair to an explicit uv-backed retry. This boundary avoids two authorities for the same installation. # Prior art -[RFC 1408](https://github.com/oceanbase/powercontext/pull/1408) defines the separation of installation, configuration, -and diagnostics; independent Runtime profiles and host selection; component-level recovery; and explicit service -registration. [RFC 1299](1299_local_server_availability_and_service_installation.md) defines personal service lifecycle. +[RFC 1408](https://github.com/oceanbase/powercontext/pull/1408) defines the separation of installation, configuration, and diagnostics; independent Runtime profiles and host selection; component-level recovery; and explicit service registration. [RFC 1299](1299_local_server_availability_and_service_installation.md) defines personal service lifecycle. -[Magpie's installer](https://github.com/yetone/magpie/blob/023f5aaad2ecd41ae04390166b9cac9a0b300d81/site/public/install.sh) -separates an authoritative release feed from artifact mirrors and verifies a binary checksum before replacement. -Its raw binary ownership justifies that verification; its manifest is not a replacement for Python package standards. +[Magpie's installer](https://github.com/yetone/magpie/blob/023f5aaad2ecd41ae04390166b9cac9a0b300d81/site/public/install.sh) separates an authoritative release feed from artifact mirrors and verifies a binary checksum before replacement. Its raw binary ownership justifies that verification; its manifest is not a replacement for Python package standards. -[Lody's daemon installation contract](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/specs/daemon-upgrade-installation.md) -resolves and executes the actual npm installation destination and checks replacement readiness. Its separate -[managed Agent runtime](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/apps/cli/src/agent/managed-agent-runtime.ts) -owns raw archives and therefore owns manifests and completion records. Apply the actual-executable check here; -retain service readiness in the service boundary. The public tag-release workflow does not publish installers. +[Lody's daemon installation contract](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/specs/daemon-upgrade-installation.md) resolves and executes the actual npm installation destination and checks replacement readiness. Its separate [managed Agent runtime](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/apps/cli/src/agent/managed-agent-runtime.ts) owns raw archives and therefore owns manifests and completion records. Apply the actual-executable check here; retain service readiness in the service boundary. The public tag-release workflow does not publish installers. -[Bub](https://github.com/bubbuild/bub/tree/b4a61bf1326729a024161d22ba20019b8500f907/website/public) bootstraps uv before -running its Python preset resolver. Its terminal and macOS Bash fixes motivate explicit host setup and native tests. -[uv tools](https://docs.astral.sh/uv/concepts/tools/) and -[configuration](https://docs.astral.sh/uv/concepts/configuration-files/) define the reused environment and source -semantics; the [Python Simple API](https://packaging.python.org/en/latest/specifications/simple-repository-api/) -defines package index interpretation. A shell regex cannot implement those contracts. +[Bub](https://github.com/bubbuild/bub/tree/b4a61bf1326729a024161d22ba20019b8500f907/website/public) bootstraps uv before running its Python preset resolver. Its terminal and macOS Bash fixes motivate explicit host setup and native tests. [uv tools](https://docs.astral.sh/uv/concepts/tools/) and [configuration](https://docs.astral.sh/uv/concepts/configuration-files/) define the reused environment and source semantics; the [Python Simple API](https://packaging.python.org/en/latest/specifications/simple-repository-api/) defines package index interpretation. A shell regex cannot implement those contracts. # Unresolved questions -No further cross-subsystem decision is required for this contract. Complete offline bundles, cryptographic installer -manifests, independently packaged host artifacts, and changes to configuration storage need separate designs. -The platform matrix must remain green before promotion; Windows product support remains experimental. +No further cross-subsystem decision is required for this contract. Complete offline bundles, cryptographic installer manifests, independently packaged host artifacts, and changes to configuration storage need separate designs. The platform matrix must remain green before promotion; Windows product support remains experimental. # Future possibilities -The scripts can bootstrap a distribution-owned installer engine if installation plans later need immutable component -manifests, independent integration versions, or durable per-component repair records. An offline bundle can include -all required artifacts explicitly. Neither extension should make Server startup or service registration implicit. +The scripts can bootstrap a distribution-owned installer engine if installation plans later need immutable component manifests, independent integration versions, or durable per-component repair records. An offline bundle can include all required artifacts explicitly. Neither extension should make Server startup or service registration implicit. The personal-service onboarding work tracks [Issue #1900](https://github.com/oceanbase/powercontext/issues/1900). diff --git a/docs/zh/docs/get-started/install-and-run.md b/docs/zh/docs/get-started/install-and-run.md index e1b91b0c9c..b80e903d25 100644 --- a/docs/zh/docs/get-started/install-and-run.md +++ b/docs/zh/docs/get-started/install-and-run.md @@ -5,8 +5,7 @@ description: 安装 PowerContext 1.4.1,并运行本地 Server。 # 安装和运行 -跨机器连接 Agent 时,请阅读[连接远程 Server](../operate/connect-remote-server.md),了解地址引导确认、 -非交互安装及绑定地址的明文 HTTP 同意设置。 +跨机器连接 Agent 时,请阅读[连接远程 Server](../operate/connect-remote-server.md),了解地址引导确认、非交互安装及绑定地址的明文 HTTP 同意设置。 首次使用请从 [Quick Start](quickstart.md)开始。本页说明版本选择、平台要求、安装角色、启动、诊断和更新。 @@ -17,13 +16,11 @@ description: 安装 PowerContext 1.4.1,并运行本地 Server。 | macOS、Linux | 支持 | | Windows | `experimental` | -Windows 的 CLI、Server 和个人服务支持为试验性;各 Agent Host 仍需满足自身的平台要求。 -使用 Bash 语法的示例需要 Bash 环境,不可直接粘贴到 PowerShell。嵌入式 seekDB 不支持 Windows。 +Windows 的 CLI、Server 和个人服务支持为试验性;各 Agent Host 仍需满足自身的平台要求。使用 Bash 语法的示例需要 Bash 环境,不可直接粘贴到 PowerShell。嵌入式 seekDB 不支持 Windows。 ## 使用推荐安装脚本 -脚本在独立的 uv tool 环境中安装 CLI 和本地 Server。已有兼容的 uv/Python 时会直接复用;缺少时在用户目录安装 -uv 和 Python 3.12,无需管理员权限。 +脚本在独立的 uv tool 环境中安装 CLI 和本地 Server。已有兼容的 uv/Python 时会直接复用;缺少时在用户目录安装 uv 和 Python 3.12,无需管理员权限。 macOS 或 Linux 使用 Bash,需要 `curl` 或 `wget`;Windows 需要 PowerShell 5.1 或更新版本。 @@ -35,16 +32,11 @@ curl -fsSL https://powercontext.oceanbase.io/install.sh | bash powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` -先执行安装完成时打印的 PATH 命令,再在终端运行 `powercontext`。安装器不会修改 shell 启动文件或 Windows 的持久 -PATH。`UV_INSTALL_DIR` 指定缺少 uv 时的安装目录,`UV_TOOL_BIN_DIR` 指定 PowerContext 可执行文件目录。 +先执行安装完成时打印的 PATH 命令,再在终端运行 `powercontext`。安装器不会修改 shell 启动文件或 Windows 的持久 PATH。`UV_INSTALL_DIR` 指定缺少 uv 时的安装目录,`UV_TOOL_BIN_DIR` 指定 PowerContext 可执行文件目录。 -默认安装可以无人值守执行。使用 `--host codex` 安装选定的 Agent 集成;多个宿主可重复传入 `--host`。 -只有集成安装需要 Git 及对应宿主的前置条件。`--no-hosts` 可以显式表达默认行为,不能与 `--host` 同时使用。 -宿主支持范围见[能力矩阵](../integrations/capabilities.md)。 +默认安装可以无人值守执行。使用 `--host codex` 安装选定的 Agent 集成;多个宿主可重复传入 `--host`。只有集成安装需要 Git 及对应宿主的前置条件。`--no-hosts` 可以显式表达默认行为,不能与 `--host` 同时使用。宿主支持范围见[能力矩阵](../integrations/capabilities.md)。 -连接已有远程 Server 时,加上 `--profile client`,只安装 CLI 和 Client 依赖。默认 `--profile local` 包含本地 Server。 -两种模式都不会启动 Server、注册服务或覆盖配置与数据。本地安装使用 `powercontext config init`,继续阅读 -[快速开始](quickstart.md);Client-only 安装按[远程连接指南](../operate/connect-remote-server.md)设置地址和认证。 +连接已有远程 Server 时,加上 `--profile client`,只安装 CLI 和 Client 依赖。默认 `--profile local` 包含本地 Server。两种模式都不会启动 Server、注册服务或覆盖配置与数据。本地安装使用 `powercontext config init`,继续阅读 [快速开始](quickstart.md);Client-only 安装按[远程连接指南](../operate/connect-remote-server.md)设置地址和认证。 ## 配置并安装个人服务 @@ -62,8 +54,7 @@ powercontext service install --env-file .env powercontext service status ``` -个人服务选择 loopback 绑定;Linux 需要可用的 `systemd --user` 管理器。Windows 个人服务仍为试验性支持, -文件保护和登录自启动说明见[部署 Server](../operate/deploy-server.md)。 +个人服务选择 loopback 绑定;Linux 需要可用的 `systemd --user` 管理器。Windows 个人服务仍为试验性支持,文件保护和登录自启动说明见[部署 Server](../operate/deploy-server.md)。 加载生成的 Client 配置,检查运行中的 Server: @@ -75,17 +66,13 @@ powercontext ready powercontext capabilities ``` -修改文件(包括写入返回的 Scope ID)后,使用原文件重新执行 `service install --env-file`。 -如果把配置保存在其他位置,始终使用同一路径。服务运行不代表 Agent 工作流已完成; -继续按[快速开始](quickstart.md)完成 Scope 创建和 Agent 验收。 +修改文件(包括写入返回的 Scope ID)后,使用原文件重新执行 `service install --env-file`。如果把配置保存在其他位置,始终使用同一路径。服务运行不代表 Agent 工作流已完成;继续按[快速开始](quickstart.md)完成 Scope 创建和 Agent 验收。 -安装器只安装软件及显式选择的集成。`--env-file PATH` 可以通过现有 `setup` 命令,为选定宿主提供已有连接文件; -配置和服务注册仍由用户分步执行现有 CLI 命令。 +安装器只安装软件及显式选择的集成。`--env-file PATH` 可以通过现有 `setup` 命令,为选定宿主提供已有连接文件;配置和服务注册仍由用户分步执行现有 CLI 命令。 ## 选择脚本安装版本 -默认 `--version latest` 安装或升级到所选包源中与当前 Python 兼容的最新稳定版,排除预发布版本。 -`--version` 也支持指定准确版本,包括 `1.3.0rc1` 这样的显式预发布版本;源中没有指定版本时会报错,不会换成其他版本。 +默认 `--version latest` 安装或升级到所选包源中与当前 Python 兼容的最新稳定版,排除预发布版本。`--version` 也支持指定准确版本,包括 `1.3.0rc1` 这样的显式预发布版本;源中没有指定版本时会报错,不会换成其他版本。 例如,安装 PowerContext 1.4.1 及其 Codex 集成: @@ -93,8 +80,7 @@ powercontext capabilities curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.4.1 --host codex ``` -Python 包版本为 `1.4.1`,对应 Git tag 为 `powercontext-v1.4.1`。使用 `latest` 时,安装器读取实际安装的 CLI 版本, -并使用对应的 tag 安装集成,不会使用会移动的 `master` 分支。稍后添加集成时可以执行: +Python 包版本为 `1.4.1`,对应 Git tag 为 `powercontext-v1.4.1`。使用 `latest` 时,安装器读取实际安装的 CLI 版本,并使用对应的 tag 安装集成,不会使用会移动的 `master` 分支。稍后添加集成时可以执行: ```bash powercontext setup codex --ref "powercontext-v$(powercontext --version)" @@ -102,11 +88,9 @@ powercontext setup codex --ref "powercontext-v$(powercontext --version)" 需要交互选择宿主时,运行 `powercontext setup --ref "powercontext-v$(powercontext --version)"`。 -PowerShell 也支持这里的双引号表达式。集成安装失败时,已安装的 Runtime 会保留,脚本返回错误并给出重试提示。 -安装成功不代表 Server 就绪或宿主工作流可用;仍需执行下文检查及集成文档中的验证。 +PowerShell 也支持这里的双引号表达式。集成安装失败时,已安装的 Runtime 会保留,脚本返回错误并给出重试提示。安装成功不代表 Server 就绪或宿主工作流可用;仍需执行下文检查及集成文档中的验证。 -安装器在报告成功前验证版本和所选模式的命令。包安装成功后仍可能无法通过命令验证,此时旧可执行文件可能已被替换。 -请查看错误中指出的命令,再指定已知可用的 `--version` 重试。安装器不回滚包文件。 +安装器在报告成功前验证版本和所选模式的命令。包安装成功后仍可能无法通过命令验证,此时旧可执行文件可能已被替换。请查看错误中指出的命令,再指定已知可用的 `--version` 重试。安装器不回滚包文件。 ## 使用镜像重试依赖下载 @@ -124,15 +108,9 @@ curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --region cn curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --index-url https://pypi.org/simple ``` -`--region auto|cn|global` 优先于 `POWERCONTEXT_INSTALL_REGION`。自动选择依次参考本地命名时区、locale 地区,最后采用 -全球源,不请求网络定位服务。自动镜像不可用时可回退官方源;中国区域包镜像可达时,即使缺少请求版本,也保持选中。 -解析或文件下载失败由 uv 报告,不会再换源重试安装。需要换源时使用 `--region global` 或显式指定来源。 -显式配置的源不会自动回退。镜像可能有同步延迟:`latest` 指所选源中的最新兼容稳定版。 +`--region auto|cn|global` 优先于 `POWERCONTEXT_INSTALL_REGION`。自动选择依次参考本地命名时区、locale 地区,最后采用全球源,不请求网络定位服务。自动镜像不可用时可回退官方源;中国区域包镜像可达时,即使缺少请求版本,也保持选中。解析或文件下载失败由 uv 报告,不会再换源重试安装。需要换源时使用 `--region global` 或显式指定来源。显式配置的源不会自动回退。镜像可能有同步延迟:`latest` 指所选源中的最新兼容稳定版。 -已有的 uv 索引环境变量和配置文件优先于自动包镜像。`--index-url` 只覆盖默认索引,额外 uv 索引仍保留其优先级。 -私有源凭据应使用 uv 认证配置,不放在脚本参数里。uv 不读取 `PIP_INDEX_URL` 或 `PIP_EXTRA_INDEX_URL`。 -已有 Python 镜像设置和 `uv.toml` 同样会禁用自动 Python 镜像选择。`UV_ASTRAL_MIRROR_URL` 会透传给支持它的 uv 版本。 -这些设置只作用于本次安装,不改写持久包管理配置;已有 uv 会直接复用,不会自动升级。 +已有的 uv 索引环境变量和配置文件优先于自动包镜像。`--index-url` 只覆盖默认索引,额外 uv 索引仍保留其优先级。私有源凭据应使用 uv 认证配置,不放在脚本参数里。uv 不读取 `PIP_INDEX_URL` 或 `PIP_EXTRA_INDEX_URL`。已有 Python 镜像设置和 `uv.toml` 同样会禁用自动 Python 镜像选择。`UV_ASTRAL_MIRROR_URL` 会透传给支持它的 uv 版本。这些设置只作用于本次安装,不改写持久包管理配置;已有 uv 会直接复用,不会自动升级。 需要检查脚本内容,或在 PowerShell 传入多个选项时,可以先保存脚本: @@ -145,21 +123,18 @@ powershell -ExecutionPolicy Bypass -File .\install.ps1 --region cn --version 1.4 ## 选择版本 -本页使用 PowerContext 1.4.1。包与 Agent 集成保持版本一致: -Python 包版本为 `1.4.1`,对应 Git tag 为 `powercontext-v1.4.1`。 +本页使用 PowerContext 1.4.1。包与 Agent 集成保持版本一致:Python 包版本为 `1.4.1`,对应 Git tag 为 `powercontext-v1.4.1`。 ```bash uv tool install --force "powercontext[cli,server]==1.4.1" powercontext setup codex --ref powercontext-v1.4.1 ``` -宿主支持范围和维护状态见[能力矩阵](../integrations/capabilities.md)。 -标为 `experimental` 的能力在此版本中仍属于试验性能力。 +宿主支持范围和维护状态见[能力矩阵](../integrations/capabilities.md)。标为 `experimental` 的能力在此版本中仍属于试验性能力。 ## 安装应用 -需要在 macOS、Linux 或 Windows 上准备 Python 3.11 或更新版本、Git 和 -[`uv`](https://docs.astral.sh/uv/),然后从 PyPI 安装 PowerContext: +需要在 macOS、Linux 或 Windows 上准备 Python 3.11 或更新版本、Git 和 [`uv`](https://docs.astral.sh/uv/),然后从 PyPI 安装 PowerContext: ```bash uv tool install --force "powercontext[cli,server]==1.4.1" @@ -171,15 +146,11 @@ uv tool install --force "powercontext[cli,server]==1.4.1" uv tool install --force "powercontext[cli,server] @ git+https://github.com/oceanbase/powercontext.git@powercontext-v1.4.1" ``` -只连接远程 Server 的机器安装 `cli` extra 即可使用 `setup`、`doctor` 和远程内容命令。运行本地 Server 或使用 -`service` 命令时,还需安装 `server`。如果命令 provider 缺少依赖,CLI 会报告 provider 和缺失模块,便于修复安装。 +只连接远程 Server 的机器安装 `cli` extra 即可使用 `setup`、`doctor` 和远程内容命令。运行本地 Server 或使用 `service` 命令时,还需安装 `server`。如果命令 provider 缺少依赖,CLI 会报告 provider 和缺失模块,便于修复安装。 -Git 安装命令不会留下需要自行管理的仓库工作副本。Git 会沿用本机的凭据配置,包括 credential helper 和 SSH 设置。 -如需使用 SSH,请把 HTTPS URL 换成当前环境允许的 Git URL。`--force` 还会从所选 Git ref 当前指向的 commit -刷新已安装工具;如果不加该参数,`uv` 可能只提示相同 requirement 已安装,而不会获取更新后的 `master`。 +Git 安装命令不会留下需要自行管理的仓库工作副本。Git 会沿用本机的凭据配置,包括 credential helper 和 SSH 设置。如需使用 SSH,请把 HTTPS URL 换成当前环境允许的 Git URL。`--force` 还会从所选 Git ref 当前指向的 commit 刷新已安装工具;如果不加该参数,`uv` 可能只提示相同 requirement 已安装,而不会获取更新后的 `master`。 -安装其他分支或 tag 时,替换最后一个 `@` 后的 ref。`master` 分支可能包含尚未发布的改动。 -Agent 的安装、连接参数和验证步骤见[各自的集成文档](../integrations/index.md),并使用与 Server 相同的 ref。 +安装其他分支或 tag 时,替换最后一个 `@` 后的 ref。`master` 分支可能包含尚未发布的改动。Agent 的安装、连接参数和验证步骤见[各自的集成文档](../integrations/index.md),并使用与 Server 相同的 ref。 ## 运行本地 Server @@ -197,8 +168,7 @@ powercontext server run 按 `Ctrl-C` 可正常关闭。再次运行该命令会打开同一个数据库。 -Dashboard 是个人使用和演示的可选内容查看器,默认关闭。它不需要单独安装前端或配置模型。 -本地免 token 启用时,在环境文件中设置以下值: +Dashboard 是个人使用和演示的可选内容查看器,默认关闭。它不需要单独安装前端或配置模型。本地免 token 启用时,在环境文件中设置以下值: ```dotenv POWERCONTEXT_SERVER_HTTP_HOST=127.0.0.1 @@ -206,8 +176,7 @@ POWERCONTEXT_SERVER_DASHBOARD_ENABLED=true POWERCONTEXT_SERVER_ACCESS_MODE=disabled ``` -需要认证时,将 `POWERCONTEXT_SERVER_ACCESS_MODE` 设为 `enforced`,并将 `POWERCONTEXT_SERVER_AUTH_TOKEN` 设为自己的长随机凭据。 -[配置向导](configure-server-environment.md#本地-dashboard-与可选认证)在本地开启 Dashboard 时也提供这一选择。 +需要认证时,将 `POWERCONTEXT_SERVER_ACCESS_MODE` 设为 `enforced`,并将 `POWERCONTEXT_SERVER_AUTH_TOKEN` 设为自己的长随机凭据。[配置向导](configure-server-environment.md#本地-dashboard-与可选认证)在本地开启 Dashboard 时也提供这一选择。 ```bash chmod 600 /path/to/powercontext.env @@ -215,34 +184,25 @@ powercontext config validate --env-file /path/to/powercontext.env powercontext server run --env-file /path/to/powercontext.env ``` -打开 `http://127.0.0.1:8000/dashboard/home`,更改端口后使用实际端口。未启用认证时可直接进入页面; -启用后使用 Server Token 登录,已连接的 Agent 也需配置该 token 来访问 API 和 MCP。CLI 不会自动读取目录中的 `.env` 文件。 +打开 `http://127.0.0.1:8000/dashboard/home`,更改端口后使用实际端口。未启用认证时可直接进入页面;启用后使用 Server Token 登录,已连接的 Agent 也需配置该 token 来访问 API 和 MCP。CLI 不会自动读取目录中的 `.env` 文件。 -首次访问选择 Server 默认 Scope,未保存内容时显示空状态。通过 Agent 或公开 API 保存一条 Memory, -再刷新同一 Scope 的记忆页即可查看。经验、技能、交接和用量也来自实际保存记录;页面不采集会话、不运行生成, -也不批准候选。Dashboard 和 Agent 必须连接同一个 Server、使用同一个 Scope。 +首次访问选择 Server 默认 Scope,未保存内容时显示空状态。通过 Agent 或公开 API 保存一条 Memory,再刷新同一 Scope 的记忆页即可查看。经验、技能、交接和用量也来自实际保存记录;页面不采集会话、不运行生成,也不批准候选。Dashboard 和 Agent 必须连接同一个 Server、使用同一个 Scope。 -打开**画像**查看已保存内容,通过**版本历史**阅读历史修订或核对来源;阅读旧版本不会改变当前画像。 -在**交接**目录条目或详情页点击**导出 Markdown**,可下载该精确版本的完整正文、遗漏和引用。 -若登录失效,重新登录后会返回所选详情,再次点击导出即可。 +打开**画像**查看已保存内容,通过**版本历史**阅读历史修订或核对来源;阅读旧版本不会改变当前画像。在**交接**目录条目或详情页点击**导出 Markdown**,可下载该精确版本的完整正文、遗漏和引用。若登录失效,重新登录后会返回所选详情,再次点击导出即可。 -启用认证时,所有 token 持有者使用同一个身份。多成员 RBAC 部署应保持 Dashboard 关闭,通过 API、MCP 或宿主集成访问内容。 -网络与凭据配置见[部署 Server](../operate/deploy-server.md)。 +启用认证时,所有 token 持有者使用同一个身份。多成员 RBAC 部署应保持 Dashboard 关闭,通过 API、MCP 或宿主集成访问内容。网络与凭据配置见[部署 Server](../operate/deploy-server.md)。 -这种最小启动方式不会启用依赖模型的抽取或向量搜索。如需生成并校验一份显式环境文件以启用这些能力,请继续阅读 -[启用提取与向量搜索](configure-models.md)。 +这种最小启动方式不会启用依赖模型的抽取或向量搜索。如需生成并校验一份显式环境文件以启用这些能力,请继续阅读 [启用提取与向量搜索](configure-models.md)。 ## 使用嵌入式 seekDB -在有兼容 `pylibseekdb` wheel 的 Linux 和 macOS 系统上可以使用嵌入式 seekDB;Windows 不支持该嵌入式 -后端。安装或替换工具时加入可选的 seekDB extra: +在有兼容 `pylibseekdb` wheel 的 Linux 和 macOS 系统上可以使用嵌入式 seekDB;Windows 不支持该嵌入式后端。安装或替换工具时加入可选的 seekDB extra: ```bash uv tool install --force "powercontext[cli,server,seekdb]==1.4.1" ``` -从 SQLite 切换时,需要从 Server 进程环境中删除 `POWERCONTEXT_SERVER_DATABASE_URL`;seekDB 不接受显式的 -SQLAlchemy 数据库 URL。然后选择 seekDB 后端并启动 Server: +从 SQLite 切换时,需要从 Server 进程环境中删除 `POWERCONTEXT_SERVER_DATABASE_URL`;seekDB 不接受显式的 SQLAlchemy 数据库 URL。然后选择 seekDB 后端并启动 Server: ```bash unset POWERCONTEXT_SERVER_DATABASE_URL @@ -250,16 +210,9 @@ export POWERCONTEXT_SERVER_DATABASE_KIND=seekdb powercontext server run ``` -当前目录存在 `.env` 时,`server run` 会自动加载该文件。可以在 shell 中导出变量来覆盖文件值,使用 -`--env-file ` 选择其他文件,或使用 `--no-env-file` 忽略环境文件。进程管理器和容器通常应提供显式环境, -不要依赖其工作目录。 +当前目录存在 `.env` 时,`server run` 会自动加载该文件。可以在 shell 中导出变量来覆盖文件值,使用 `--env-file ` 选择其他文件,或使用 `--no-env-file` 忽略环境文件。进程管理器和容器通常应提供显式环境,不要依赖其工作目录。 -PowerContext 默认使用专用数据库 `powercontext`,先创建缺失的数据库,再初始化应用表。 -设置 `POWERCONTEXT_SERVER_DATABASE_DATABASE=my_context` 可以指定其他名称;缺失时创建,已有时复用。 -包含反引号的库名会在初始化之前被拒绝。 -未设置 `POWERCONTEXT_SERVER_DATABASE_PATH` 时,实例保存在 -PowerContext 用户数据目录的 `seekdb` 子目录中;如果设置了 `POWERCONTEXT_HOME`,默认路径为 -`$POWERCONTEXT_HOME/seekdb`。只有需要其他位置时才设置 `POWERCONTEXT_SERVER_DATABASE_PATH`。 +PowerContext 默认使用专用数据库 `powercontext`,先创建缺失的数据库,再初始化应用表。设置 `POWERCONTEXT_SERVER_DATABASE_DATABASE=my_context` 可以指定其他名称;缺失时创建,已有时复用。包含反引号的库名会在初始化之前被拒绝。未设置 `POWERCONTEXT_SERVER_DATABASE_PATH` 时,实例保存在 PowerContext 用户数据目录的 `seekdb` 子目录中;如果设置了 `POWERCONTEXT_HOME`,默认路径为 `$POWERCONTEXT_HOME/seekdb`。只有需要其他位置时才设置 `POWERCONTEXT_SERVER_DATABASE_PATH`。 在另一个终端确认 Server 和数据库已经就绪: @@ -279,29 +232,17 @@ export POWERCONTEXT_SERVER_DATABASE_DATABASE=test powercontext server run ``` -保持原来的 `POWERCONTEXT_SERVER_DATABASE_PATH` 或 `POWERCONTEXT_HOME`。实例目录和数据库名称是两个独立配置, -修改其中任何一个都可能连接到不同的数据。服务和容器也需要使用相同配置。 +保持原来的 `POWERCONTEXT_SERVER_DATABASE_PATH` 或 `POWERCONTEXT_HOME`。实例目录和数据库名称是两个独立配置,修改其中任何一个都可能连接到不同的数据。服务和容器也需要使用相同配置。 -如果没有显式配置库名,而 `test` 中存在以 PowerContext 的 `pc_` 为前缀的表,启动会在创建应用表前停止, -要求选择数据库。系统不会自动选择、复制或删除旧库。显式选择 `test` 可以继续使用旧数据;显式选择 -`powercontext` 则使用新库。配置向导也会询问数据库名称,已有用户应填写 `test`。 +如果没有显式配置库名,而 `test` 中存在以 PowerContext 的 `pc_` 为前缀的表,启动会在创建应用表前停止,要求选择数据库。系统不会自动选择、复制或删除旧库。显式选择 `test` 可以继续使用旧数据;显式选择 `powercontext` 则使用新库。配置向导也会询问数据库名称,已有用户应填写 `test`。 -需要迁移时,先继续使用 `test`,直到完成备份并准备好针对当前引擎版本验证过的导出、导入流程。 -停止所有源库写入者,准备目标库,按该流程导出、导入结构和数据,校验身份、关联关系,并通过 PowerContext -读取迁移结果后再切换配置。自动建库不执行数据迁移。不能假定任意 SQL 转储都能跨引擎或表结构版本使用; -已有表仍需通过兼容性检查,表结构升级需要显式维护。 -代码图引用绑定实例和数据库,仅复制关系表数据不等于迁移代码图存储;校验流程应包含目标库的 Code 索引重建和查询。 -显式继续使用 `test` 会保留旧代码图身份。 +需要迁移时,先继续使用 `test`,直到完成备份并准备好针对当前引擎版本验证过的导出、导入流程。停止所有源库写入者,准备目标库,按该流程导出、导入结构和数据,校验身份、关联关系,并通过 PowerContext 读取迁移结果后再切换配置。自动建库不执行数据迁移。不能假定任意 SQL 转储都能跨引擎或表结构版本使用;已有表仍需通过兼容性检查,表结构升级需要显式维护。代码图引用绑定实例和数据库,仅复制关系表数据不等于迁移代码图存储;校验流程应包含目标库的 Code 索引重建和查询。显式继续使用 `test` 会保留旧代码图身份。 -切换后由用户选择保留期,旧库保持停写。记录源和目标实例路径、库名、备份位置、停写及切换时间、校验结果和复核日期。 -手动删除旧库前,确认所有部署均已停止使用它,并且仍有可用备份。目标库的新写入不会同步回源库,保留源库不代表 -可以无损回退。PowerContext 不会自动迁移、发送到期提醒、比较新旧库或删除旧库。 +切换后由用户选择保留期,旧库保持停写。记录源和目标实例路径、库名、备份位置、停写及切换时间、校验结果和复核日期。手动删除旧库前,确认所有部署均已停止使用它,并且仍有可用备份。目标库的新写入不会同步回源库,保留源库不代表可以无损回退。PowerContext 不会自动迁移、发送到期提醒、比较新旧库或删除旧库。 ## 使用 OceanBase -连接已有 OceanBase **MySQL 兼容模式的用户租户**。完整 Server 需要先准备所选数据库,并通过显式维护初始化 -Server 表结构,再启动服务。PowerContext 不负责创建 OceanBase 实例、租户或账户。 -确认网络可达,按上文步骤安装 Server,然后配置连接: +连接已有 OceanBase **MySQL 兼容模式的用户租户**。完整 Server 需要先准备所选数据库,并通过显式维护初始化 Server 表结构,再启动服务。PowerContext 不负责创建 OceanBase 实例、租户或账户。确认网络可达,按上文步骤安装 Server,然后配置连接: ```bash export POWERCONTEXT_SERVER_DATABASE_KIND=oceanbase @@ -309,42 +250,23 @@ export POWERCONTEXT_SERVER_DATABASE_KIND=oceanbase export POWERCONTEXT_SERVER_DATABASE_URL='mysql+aoceanbase://user%40tenant:encoded-password@host:2881/?charset=utf8mb4' ``` -使用租户账户和实际的直连或 OBProxy 地址、端口。URL 用户名和密码中的保留字符需要百分号编码,例如 -`user@tenant` 编码为 `user%40tenant`。URL 必须使用 `mysql+aoceanbase` 驱动、显式端口和 `charset=utf8mb4`。 +使用租户账户和实际的直连或 OBProxy 地址、端口。URL 用户名和密码中的保留字符需要百分号编码,例如 `user@tenant` 编码为 `user%40tenant`。URL 必须使用 `mysql+aoceanbase` 驱动、显式端口和 `charset=utf8mb4`。 -示例省略库名,因此使用 `powercontext`。自定义库名放在 URL 路径中,例如 `/team_context?charset=utf8mb4`。 -已有 URL 中显式指定的库名保持有效。库名仅通过 URL 路径指定,不接受 `db` 或 `database` 查询参数。 -数据库名称支持 1~64 个 BMP 字符,不允许控制字符、斜杠、反斜杠、点、反引号、 -首尾空白或系统数据库名称;PowerContext 不会静默裁剪或改名,服务器自身的标识符大小写规则仍然适用。 -包含反引号的名称会在配置校验阶段被拒绝,不会连接或创建数据库。请使用 `powercontext`、`team_context` -或不含反引号的中文名称。 +示例省略库名,因此使用 `powercontext`。自定义库名放在 URL 路径中,例如 `/team_context?charset=utf8mb4`。已有 URL 中显式指定的库名保持有效。库名仅通过 URL 路径指定,不接受 `db` 或 `database` 查询参数。数据库名称支持 1~64 个 BMP 字符,不允许控制字符、斜杠、反斜杠、点、反引号、首尾空白或系统数据库名称;PowerContext 不会静默裁剪或改名,服务器自身的标识符大小写规则仍然适用。包含反引号的名称会在配置校验阶段被拒绝,不会连接或创建数据库。请使用 `powercontext`、`team_context` 或不含反引号的中文名称。 -Python SDK 初始化时先尝试连接目标库,已有可用库不要求额外建库权限。只有明确的“数据库不存在”错误才触发实例或租户级连接, -执行 `CREATE DATABASE IF NOT EXISTS`,随后重新连接目标库并初始化应用表。认证、网络错误不会触发建库或切库。 -如果服务器拒绝访问但无法确认库是否存在,错误会要求管理员同时检查数据库和权限。 +Python SDK 初始化时先尝试连接目标库,已有可用库不要求额外建库权限。只有明确的“数据库不存在”错误才触发实例或租户级连接,执行 `CREATE DATABASE IF NOT EXISTS`,随后重新连接目标库并初始化应用表。认证、网络错误不会触发建库或切库。如果服务器拒绝访问但无法确认库是否存在,错误会要求管理员同时检查数据库和权限。 -首次部署完整 Server 时,需要先创建所选数据库。SDK 自动初始化因权限不足无法建库时,也可由管理员执行同一步骤。 -请在**同一租户**中执行以下 SQL;自定义部署应替换为实际库名,然后为应用账户授予所需数据库权限: +首次部署完整 Server 时,需要先创建所选数据库。SDK 自动初始化因权限不足无法建库时,也可由管理员执行同一步骤。请在**同一租户**中执行以下 SQL;自定义部署应替换为实际库名,然后为应用账户授予所需数据库权限: ```sql CREATE DATABASE IF NOT EXISTS `powercontext` CHARACTER SET utf8mb4; ``` -完整 Server 接下来应按[数据库迁移](../operate/database-migrations.md)指南,显式指定证据目录和协调模式, -对登记的 Server bundle 执行 plan、apply 和 verify。数据库就绪后再运行 `powercontext server run`。 -Server 启动检查就绪状态,不初始化 OceanBase 表结构,也不会自动升级已有库。SDK 调用方完成建库后可以使用原 URL 重试初始化。 -创建缺失表仍然需要建表权限;已有完整可用库不会新增建库权限要求。 -初始化不会删除或重建已有数据库,不会在失败后切换其他数据库,也不授权升级已有表结构。 -并发创建使用幂等建库、建表 DDL,并处理索引重名竞争;这不提供已有表结构迁移的协调能力,也不改变嵌入式 seekDB -对实例进程所有权的限制。 +完整 Server 接下来应按[数据库迁移](../operate/database-migrations.md)指南,显式指定证据目录和协调模式,对登记的 Server bundle 执行 plan、apply 和 verify。数据库就绪后再运行 `powercontext server run`。Server 启动检查就绪状态,不初始化 OceanBase 表结构,也不会自动升级已有库。SDK 调用方完成建库后可以使用原 URL 重试初始化。创建缺失表仍然需要建表权限;已有完整可用库不会新增建库权限要求。初始化不会删除或重建已有数据库,不会在失败后切换其他数据库,也不授权升级已有表结构。并发创建使用幂等建库、建表 DDL,并处理索引重名竞争;这不提供已有表结构迁移的协调能力,也不改变嵌入式 seekDB 对实例进程所有权的限制。 -Python 调用方使用 `OceanBaseProfile.open()` 时可省略 URL 中的库名,并按需建库。 -`OceanBaseProfile.open_existing()` 只连接已准备好的数据库;`attach()` 要求调用方的 engine 已显式选择一个已有数据库。 -两者都不替换 engine 或改变连接目标。只读巡检(`plan`、`verify` 与 `db-migrate` 的检查类命令)不会创建缺失的 -OceanBase 或内嵌 seekDB 库;显式的 seekDB 写入维护可以在应用迁移包前准备配置指定的数据库。 +Python 调用方使用 `OceanBaseProfile.open()` 时可省略 URL 中的库名,并按需建库。`OceanBaseProfile.open_existing()` 只连接已准备好的数据库;`attach()` 要求调用方的 engine 已显式选择一个已有数据库。两者都不替换 engine 或改变连接目标。只读巡检(`plan`、`verify` 与 `db-migrate` 的检查类命令)不会创建缺失的 OceanBase 或内嵌 seekDB 库;显式的 seekDB 写入维护可以在应用迁移包前准备配置指定的数据库。 -只读 seekDB 巡检要求实例目录已初始化。路径缺失或目录为空时,会在启动 native 引擎前报错,不创建父目录或存储文件。 -路径为文件,或非空目录中没有可识别的 native 存储时,也会拒绝操作并保留原有内容。普通启动仍可初始化新实例。 +只读 seekDB 巡检要求实例目录已初始化。路径缺失或目录为空时,会在启动 native 引擎前报错,不创建父目录或存储文件。路径为文件,或非空目录中没有可识别的 native 存储时,也会拒绝操作并保留原有内容。普通启动仍可初始化新实例。 ## 验证安装 @@ -354,10 +276,7 @@ powercontext ready powercontext capabilities ``` -`doctor` 检查已安装的包、Server 存活状态和 Server 就绪状态,不要求安装集成。Server 就绪检查涵盖数据库和 -每个已配置的推理服务。Runtime 或数据库故障返回 `not_ready`;推理服务故障返回 `degraded`,不会使数据库 -操作退出流量。`ready` 和 `capabilities` 用于查看运行中服务的就绪状态和已启用能力。 -Agent 诊断见[各自的集成文档](../integrations/index.md);Server 状态解释和恢复步骤见[排查问题](../operate/troubleshoot.md)。 +`doctor` 检查已安装的包、Server 存活状态和 Server 就绪状态,不要求安装集成。Server 就绪检查涵盖数据库和每个已配置的推理服务。Runtime 或数据库故障返回 `not_ready`;推理服务故障返回 `degraded`,不会使数据库操作退出流量。`ready` 和 `capabilities` 用于查看运行中服务的就绪状态和已启用能力。Agent 诊断见[各自的集成文档](../integrations/index.md);Server 状态解释和恢复步骤见[排查问题](../operate/troubleshoot.md)。 需要长期运行进程、使用 Docker、启用鉴权或允许远程访问时,请继续阅读[部署 Server](../operate/deploy-server.md)。 @@ -367,17 +286,11 @@ Agent 诊断见[各自的集成文档](../integrations/index.md);Server 状态 1.3.0 和 1.4.0 升级必须通过 SQLite、seekDB 和 OceanBase 的完整 Server 验收。当前迁移路径尚未验证,请勿升级已有数据库。 -已有部署应保持当前可用版本,等待来源版本及后端具备已验收的升级路径。试用新建部署时,请使用独立程序环境、 -数据库位置和端口。替换程序包不会完成数据库迁移;备份也不能使不受支持的迁移变为可用。 +已有部署应保持当前可用版本,等待来源版本及后端具备已验收的升级路径。试用新建部署时,请使用独立程序环境、数据库位置和端口。替换程序包不会完成数据库迁移;备份也不能使不受支持的迁移变为可用。 -支持声明由发布矩阵和安装包验收共同检查,详见[准备发布](../../development/releasing.md)。 -[Artifact 处理迁移](../operate/artifact-processing-migration.md)和 -[Atomic Memory 迁移](../operate/atomic-memory-migration.md)各自的验证结果不代表完整 Server 就绪。 +支持声明由发布矩阵和安装包验收共同检查,详见[准备发布](../../development/releasing.md)。[Artifact 处理迁移](../operate/artifact-processing-migration.md)和 [Atomic Memory 迁移](../operate/atomic-memory-migration.md)各自的验证结果不代表完整 Server 就绪。 -升级已有数据库前,请先检查 [Dream 协调升级要求](../workflows/review-dream-revisions.md#协调升级)。 -统一 Candidate 路由将 `/v1/artifact-candidates/*` 替换为 `/v1/candidates/*`,不提供别名,Dream 客户端要求契约 2。 -已有 Dream 存储需排空未完成任务后显式迁移。[完整 Server 迁移](../operate/database-migrations.md)要求 -`state=ready` 和 `server_ready=true`;局部 Artifact 或 Atomic Memory 验证不能替代它。不要绕过迁移门禁。 +升级已有数据库前,请先检查 [Dream 协调升级要求](../workflows/review-dream-revisions.md#协调升级)。统一 Candidate 路由将 `/v1/artifact-candidates/*` 替换为 `/v1/candidates/*`,不提供别名,Dream 客户端要求契约 2。已有 Dream 存储需排空未完成任务后显式迁移。[完整 Server 迁移](../operate/database-migrations.md)要求 `state=ready` 和 `server_ready=true`;局部 Artifact 或 Atomic Memory 验证不能替代它。不要绕过迁移门禁。 安装 1.4.1 到独立环境以试用新建部署: @@ -392,8 +305,7 @@ uv pip install --python /path/to/powercontext-preview/bin/python "powercontext[c uv pip install --python /path/to/powercontext-preview/bin/python "powercontext[cli,server] @ git+https://github.com/oceanbase/powercontext.git@" ``` -连接新建部署的客户端及 Agent 集成应使用匹配的 ref。保留原部署的数据库位置和配置,不要用原数据库测试新版启动。 -Windows 的虚拟环境解释器位于 `Scripts/python.exe`。 +连接新建部署的客户端及 Agent 集成应使用匹配的 ref。保留原部署的数据库位置和配置,不要用原数据库测试新版启动。Windows 的虚拟环境解释器位于 `Scripts/python.exe`。 ## 为 Python 项目安装角色 @@ -403,5 +315,4 @@ Windows 的虚拟环境解释器位于 `Scripts/python.exe`。 uv add "powercontext[client]==1.4.1" ``` -进程内 Python 组合使用 `builtin`,服务使用 `server`,Python SDK 使用 `client`,基于 Server 的命令行使用 -`cli`。只安装在 `uv tool` 隔离环境中的 extra 不能被另一个 Python 项目直接导入。 +进程内 Python 组合使用 `builtin`,服务使用 `server`,Python SDK 使用 `client`,基于 Server 的命令行使用 `cli`。只安装在 `uv tool` 隔离环境中的 extra 不能被另一个 Python 项目直接导入。 diff --git a/docs/zh/docs/get-started/quickstart.md b/docs/zh/docs/get-started/quickstart.md index 5623ec4b33..e27e5eaec4 100644 --- a/docs/zh/docs/get-started/quickstart.md +++ b/docs/zh/docs/get-started/quickstart.md @@ -5,15 +5,9 @@ description: 通过配置向导安装完整记忆能力,接入 Codex,并验 # 快速开始 -本页从安装开始,带你完成一次真实的记忆体验:在 Codex 中讨论项目,看到原始输入进入 Source、主题记忆生成并演进, -再在新会话中找回决策。以下命令使用 PowerContext 1.4.1,Agent 插件使用对应的 -`powercontext-v1.4.1` tag。 +本页从安装开始,带你完成一次真实的记忆体验:在 Codex 中讨论项目,看到原始输入进入 Source、主题记忆生成并演进,再在新会话中找回决策。以下命令使用 PowerContext 1.4.1,Agent 插件使用对应的 `powercontext-v1.4.1` tag。 -需要 macOS 或 Linux、Python 3.11+、Git、[uv](https://docs.astral.sh/uv/getting-started/installation/) 和已安装的 Codex CLI。 -完整记忆还需要可用的 Generation 和 Embedding 模型 API;准备好各自的地址、模型名和 API key。 -Codex 或 Claude 的订阅登录不会自动为 PowerContext Server 提供这些 API。只有 Agent 登录、没有模型 API 时, -仍可选择基础记忆,验收显式保存与召回,但不能据此期待自动 Topic Memory。 -需要自动安装 Python 和 uv 时,使用[安装脚本](install-and-run.md#使用推荐安装脚本)。 +需要 macOS 或 Linux、Python 3.11+、Git、[uv](https://docs.astral.sh/uv/getting-started/installation/) 和已安装的 Codex CLI。完整记忆还需要可用的 Generation 和 Embedding 模型 API;准备好各自的地址、模型名和 API key。Codex 或 Claude 的订阅登录不会自动为 PowerContext Server 提供这些 API。只有 Agent 登录、没有模型 API 时,仍可选择基础记忆,验收显式保存与召回,但不能据此期待自动 Topic Memory。需要自动安装 Python 和 uv 时,使用[安装脚本](install-and-run.md#使用推荐安装脚本)。 ## 1. 安装并进入配置向导 @@ -41,10 +35,7 @@ powercontext config init --language zh --output .env | `.env` | 本次安装使用的 Server、客户端、Agent、数据库和模型配置,包括已配置的凭据 | | `.env.next-steps.md` | 与本次选择对应的启动、Scope 创建、插件连接和验收说明 | -向导会打印 Dashboard 地址和所选 SSH 转发命令。启用认证时,还会显示一次新生成的 Server Token, -以后可在 `.env` 中查看 `POWERCONTEXT_SERVER_AUTH_TOKEN`。文件可能包含凭据,不要提交到 Git。 -若 seekdb 仍在安装,向导会在当前界面显示活动进度并等待;安装失败时先按提示完成依赖安装。 -保存配置或装好依赖,都不代表 Server 已经启动。 +向导会打印 Dashboard 地址和所选 SSH 转发命令。启用认证时,还会显示一次新生成的 Server Token,以后可在 `.env` 中查看 `POWERCONTEXT_SERVER_AUTH_TOKEN`。文件可能包含凭据,不要提交到 Git。若 seekdb 仍在安装,向导会在当前界面显示活动进度并等待;安装失败时先按提示完成依赖安装。保存配置或装好依赖,都不代表 Server 已经启动。 ## 2. 启动 Server @@ -56,12 +47,7 @@ powercontext service install --env-file .env powercontext service status ``` -原生当前用户服务负责 Server 生命周期;Linux 需要可用的 `systemd --user` 管理器。 -在浏览器打开向导输出的 Dashboard 地址,端口以 `.env` 中保存的 -`POWERCONTEXT_SERVER_HTTP_PORT` 为准。未启用认证时可直接进入页面;启用后使用 **Server Token** 登录,不是模型 API key。 -首次没有数据是正常现象。开发、排障、临时使用或没有原生用户服务管理器时,可使用 -`powercontext server run --env-file .env`;如个人服务正在运行,先将其停止。 -服务管理详见[个人后台服务](../operate/deploy-server.md#运行持久个人-server)。 +原生当前用户服务负责 Server 生命周期;Linux 需要可用的 `systemd --user` 管理器。在浏览器打开向导输出的 Dashboard 地址,端口以 `.env` 中保存的 `POWERCONTEXT_SERVER_HTTP_PORT` 为准。未启用认证时可直接进入页面;启用后使用 **Server Token** 登录,不是模型 API key。首次没有数据是正常现象。开发、排障、临时使用或没有原生用户服务管理器时,可使用 `powercontext server run --env-file .env`;如个人服务正在运行,先将其停止。服务管理详见[个人后台服务](../operate/deploy-server.md#运行持久个人-server)。 加载客户端连接配置并检查服务: @@ -74,21 +60,17 @@ powercontext ready powercontext capabilities ``` -确认服务就绪且所选处理能力可用;`degraded` 表示部分依赖仍有问题,不能算完整记忆验收通过。 -`config validate` 只检查配置,在线结果以启动后的检查为准。 +确认服务就绪且所选处理能力可用;`degraded` 表示部分依赖仍有问题,不能算完整记忆验收通过。`config validate` 只检查配置,在线结果以启动后的检查为准。 ## 3. 创建 Scope 并安装 Codex 插件 -打开 `.env.next-steps.md`,执行其中“创建计划中的独立 Scope”的请求。Server 响应会返回真正的 `scope_id`。 -将它写入 `.env`: +打开 `.env.next-steps.md`,执行其中“创建计划中的独立 Scope”的请求。Server 响应会返回真正的 `scope_id`。将它写入 `.env`: ```dotenv POWERCONTEXT_CODEX_SCOPE_ID=替换为返回的scope_id ``` -`codex-xxxxxxxx` 是向导规划的标题,不是 ID。配置 Claude Code 时,将其创建请求返回的 ID 写入 -`POWERCONTEXT_CLAUDE_SCOPE_ID`。不同 Agent 可以各自隔离,也可以显式绑定同一已有 Scope。 -切换目录本身不会创建隔离。验收期间,Dashboard 和 Agent 必须使用相同的 Scope。 +`codex-xxxxxxxx` 是向导规划的标题,不是 ID。配置 Claude Code 时,将其创建请求返回的 ID 写入 `POWERCONTEXT_CLAUDE_SCOPE_ID`。不同 Agent 可以各自隔离,也可以显式绑定同一已有 Scope。切换目录本身不会创建隔离。验收期间,Dashboard 和 Agent 必须使用相同的 Scope。 修改 `.env` 后,协调服务配置,重新加载客户端文件并安装匹配的插件: @@ -105,26 +87,19 @@ codex 在新会话中发送第一条消息前: -1. 打开 `/hooks` 检查 PowerContext Hook,在收到提示时授予信任。`SessionStart` 解析会话 Scope; - `UserPromptSubmit` 自动准备相关上下文,并将用户输入采集为 Source。 +1. 打开 `/hooks` 检查 PowerContext Hook,在收到提示时授予信任。`SessionStart` 解析会话 Scope;`UserPromptSubmit` 自动准备相关上下文,并将用户输入采集为 Source。 2. 确认 PowerContext MCP 工具可用。工具发现和 Hook 执行是不同的检查。 -3. 如果跳过或拒绝了信任,先按[Hook 恢复步骤](../integrations/codex.md#首次会话-hook-信任)处理,再验收采集。 - `UserPromptSubmit` Hook 未启用时,不会自动采集或召回。 +3. 如果跳过或拒绝了信任,先按[Hook 恢复步骤](../integrations/codex.md#首次会话-hook-信任)处理,再验收采集。`UserPromptSubmit` Hook 未启用时,不会自动采集或召回。 -非默认地址还需按 `.env.next-steps.md` 的 Codex 连接说明检查已安装插件的 `.mcp.json`: -MCP 的 URL 必须和 Hook 使用同一 Server,启用认证时还需配置凭据。仅安装插件不会启动 Server。 -`doctor codex` 检查安装、连接和工具覆盖,不能证明 Hook 已获信任、已执行或记忆处理成功。 +非默认地址还需按 `.env.next-steps.md` 的 Codex 连接说明检查已安装插件的 `.mcp.json`:MCP 的 URL 必须和 Hook 使用同一 Server,启用认证时还需配置凭据。仅安装插件不会启动 Server。`doctor codex` 检查安装、连接和工具覆盖,不能证明 Hook 已获信任、已执行或记忆处理成功。 -这些命令以 Codex CLI 为例。桌面 App 不一定继承终端的环境变量;在桌面中验收前,要确认 App 的 Hook 和 MCP -都拿到了相同的地址和 Scope,启用认证时还需取得对应凭据。更多宿主行为见[Codex](../integrations/codex.md)和[Claude Code](../integrations/claude-code.md)。 +这些命令以 Codex CLI 为例。桌面 App 不一定继承终端的环境变量;在桌面中验收前,要确认 App 的 Hook 和 MCP 都拿到了相同的地址和 Scope,启用认证时还需取得对应凭据。更多宿主行为见[Codex](../integrations/codex.md)和[Claude Code](../integrations/claude-code.md)。 -完整能力中的 Profile 还需要为这个真实 Scope 启用生成策略。按[Profile 策略步骤](configure-models.md#为-scope-启用-profile-策略) -读取当前版本并更新;只生成配置文件不会自动完成它。 +完整能力中的 Profile 还需要为这个真实 Scope 启用生成策略。按[Profile 策略步骤](configure-models.md#为-scope-启用-profile-策略) 读取当前版本并更新;只生成配置文件不会自动完成它。 ## 4. 按所选模式验收记忆 -Hook 启用后,两种模式都可以采集 Source。基础记忆无需独立的 Server 模型 API;Agent 仍使用自己的模型。 -按实际配置选择下面的验收路径。 +Hook 启用后,两种模式都可以采集 Source。基础记忆无需独立的 Server 模型 API;Agent 仍使用自己的模型。按实际配置选择下面的验收路径。 ### 基础记忆:显式保存与检索 @@ -140,8 +115,7 @@ Hook 启用后,两种模式都可以采集 Source。基础记忆无需独立 从 PowerContext 搜索 Aurora 的 Python 依赖管理决定,带上 citation。 ``` -核对已保存内容与引用。对话中的“记住了”不能证明写入成功;这条路径验证显式保存与检索, -不验证自动提取或 Topic Memory 生成。 +核对已保存内容与引用。对话中的“记住了”不能证明写入成功;这条路径验证显式保存与检索,不验证自动提取或 Topic Memory 生成。 ### 完整记忆:Source 处理与 Topic Memory @@ -160,11 +134,9 @@ Hook 启用后,两种模式都可以采集 Source。基础记忆无需独立 4. 确认第二条 Source 入库,主题内容或修订记录反映这次更新。不要以“等了一分钟”代替检查。 5. 开启新的 Codex 会话,保留同一 Scope,询问:“从 PowerContext 找回 Aurora 的依赖管理和部署决定,带上引用。” -找到正确内容及其 citation,才完成从采集到演进再到跨会话召回的验收。`doctor codex` 成功仅说明安装与连接检查通过, -不能替代上述业务检查。模型可能决定合并或不更新主题,不保证每条消息都创建一条记忆。 +找到正确内容及其 citation,才完成从采集到演进再到跨会话召回的验收。`doctor codex` 成功仅说明安装与连接检查通过,不能替代上述业务检查。模型可能决定合并或不更新主题,不保证每条消息都创建一条记忆。 -完整能力不代表每条对话都会生成所有制品。Profile、Experience 和 Skill 还有各自的触发或审核条件, -见[模型与能力配置](configure-models.md#完整能力不等于每条对话都会生成所有制品)。 +完整能力不代表每条对话都会生成所有制品。Profile、Experience 和 Skill 还有各自的触发或审核条件,见[模型与能力配置](configure-models.md#完整能力不等于每条对话都会生成所有制品)。 | 检查 | 成功依据 | | --- | --- | @@ -176,9 +148,7 @@ Hook 启用后,两种模式都可以采集 Source。基础记忆无需独立 ## 5. 选择下一项工作流 -七类 Artifact 的输入和消费路径不同。Source 是证据,Tag 是元数据,两者都不是 Artifact family。 -下面的例句是请求示例,不是保证生效的关键词触发器。以当前连接实际提供的工具为准;没有专用 MCP 工具的操作, -按链接进入 HTTP/CLI 工作流。概念区别见[Artifact 速览](faq.md#artifact-速览)。 +七类 Artifact 的输入和消费路径不同。Source 是证据,Tag 是元数据,两者都不是 Artifact family。下面的例句是请求示例,不是保证生效的关键词触发器。以当前连接实际提供的工具为准;没有专用 MCP 工具的操作,按链接进入 HTTP/CLI 工作流。概念区别见[Artifact 速览](faq.md#artifact-速览)。 | 能力与用途 | 请求示例或入口 | 前提与结果 | | --- | --- | --- | @@ -194,14 +164,9 @@ Hook 启用后,两种模式都可以采集 Source。基础记忆无需独立 ### 插件引导 Skill 与受管 Skill -插件自带的 `powercontext-project-context` 将请求路由到对应工作流,和保存在 PowerContext 的 Managed Skill 是两个对象。 -宿主目录中找不到引导 Skill 时,先检查插件安装;自然语言示例不会增加缺失工具,也不会授予权限。 +插件自带的 `powercontext-project-context` 将请求路由到对应工作流,和保存在 PowerContext 的 Managed Skill 是两个对象。宿主目录中找不到引导 Skill 时,先检查插件安装;自然语言示例不会增加缺失工具,也不会授予权限。 -生成受管 Skill 后,先检查 Candidate,再由有权限的主体作出审核决定。批准创建不可变 Revision,不代表安装或执行。 -按[创建与导出 Skill](../workflows/create-and-export-skill.md),使用 `powercontext skill export --target codex` -将精确的 approved Revision 导出到目标宿主的 `.agents/skills/` 目录。 -确认 Codex 能发现导出的 Skill;没有出现时再重启。直接 Artifact 写入有自己的契约,这里的 Candidate 流程描述生成和提案路径。 -谁能审核取决于权限,不要求一定由人审核;宿主发现 Skill 也不会授予执行权限。 +生成受管 Skill 后,先检查 Candidate,再由有权限的主体作出审核决定。批准创建不可变 Revision,不代表安装或执行。按[创建与导出 Skill](../workflows/create-and-export-skill.md),使用 `powercontext skill export --target codex` 将精确的 approved Revision 导出到目标宿主的 `.agents/skills/` 目录。确认 Codex 能发现导出的 Skill;没有出现时再重启。直接 Artifact 写入有自己的契约,这里的 Candidate 流程描述生成和提案路径。谁能审核取决于权限,不要求一定由人审核;宿主发现 Skill 也不会授予执行权限。 ## 继续使用 diff --git a/docs/zh/rfcs/1892-script-installation.md b/docs/zh/rfcs/1892-script-installation.md index becd67c5c5..2d4db5a26f 100644 --- a/docs/zh/rfcs/1892-script-installation.md +++ b/docs/zh/rfcs/1892-script-installation.md @@ -10,24 +10,17 @@ description: 通过 Bash 和 PowerShell 安装最新或指定版本,独立管 # Summary -提供由发行版维护的 Bash 和 PowerShell 入口,通过 uv 安装 PowerContext。默认版本是所选包索引中的最新稳定版, -`--version` 可以指定准确版本。脚本补齐缺少的 uv 和 Python,安装所选 Runtime profile,并按需调用现有 Agent 集成 -适配器,使用与实际安装的 Runtime 对应的 tag。 +提供由发行版维护的 Bash 和 PowerShell 入口,通过 uv 安装 PowerContext。默认版本是所选包索引中的最新稳定版,`--version` 可以指定准确版本。脚本补齐缺少的 uv 和 Python,安装所选 Runtime profile,并按需调用现有 Agent 集成适配器,使用与实际安装的 Runtime 对应的 tag。 -安装、配置、诊断和服务运行各自承担独立职责。安装软件不会配置推理服务、启动 Server 或注册持久服务; -配置和服务运行仍分步复用现有 CLI。 -安装指南说明脚本用法;README 和快速开始保留固定版本的 uv 命令,并提供脚本指南入口。 +安装、配置、诊断和服务运行各自承担独立职责。安装软件不会配置推理服务、启动 Server 或注册持久服务;配置和服务运行仍分步复用现有 CLI。安装指南说明脚本用法;README 和快速开始保留固定版本的 uv 命令,并提供脚本指南入口。 # Motivation -正常安装不应要求用户自行协调 Python 发现、uv 安装、包 extras、包索引及集成 Git ref。Python 包、uv 二进制和 -Python 发行版也来自不同渠道:只更换 PyPI 索引无法解决 Python 下载不可达的问题。 +正常安装不应要求用户自行协调 Python 发现、uv 安装、包 extras、包索引及集成 Git ref。Python 包、uv 二进制和 Python 发行版也来自不同渠道:只更换 PyPI 索引无法解决 Python 下载不可达的问题。 -安装负责放置版本化组件并注册显式选择的集成;配置管理 Server URL、Scope、采集策略等可变值;诊断观察安装结果。 -分清这些职责,安装体验就可以独立于配置存储、统一 Hooks、运维工具或服务生命周期改进。 +安装负责放置版本化组件并注册显式选择的集成;配置管理 Server URL、Scope、采集策略等可变值;诊断观察安装结果。分清这些职责,安装体验就可以独立于配置存储、统一 Hooks、运维工具或服务生命周期改进。 -预期结果是每种 shell 一个入口、复用已有依赖、明确的版本选择、显式的宿主选择、可恢复的部分失败, -以及通过实际安装产物进行验证。 +预期结果是每种 shell 一个入口、复用已有依赖、明确的版本选择、显式的宿主选择、可恢复的部分失败,以及通过实际安装产物进行验证。 # Guide-level explanation @@ -45,12 +38,9 @@ Windows,使用 PowerShell 5.1 或更新版本: powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` -默认 `local` profile 安装 CLI、Client 和本地 Server 依赖。`--profile client` 只安装连接已有 Server 所需的 CLI 和 -Client 依赖。Profile 与宿主选择相互独立:选择数据库角色不隐含选择 Agent,选择 Codex 也不会更改 Runtime profile。 +默认 `local` profile 安装 CLI、Client 和本地 Server 依赖。`--profile client` 只安装连接已有 Server 所需的 CLI 和 Client 依赖。Profile 与宿主选择相互独立:选择数据库角色不隐含选择 Agent,选择 Codex 也不会更改 Runtime profile。 -安装器复用已有 uv,以及兼容的系统 Python 或 uv 管理的 Python;否则在用户目录补齐 uv 和 Python 3.12。 -完成后打印实际 Runtime 版本、profile、可执行文件目录、PATH 命令及下一步配置命令,不修改持久 PATH 设置。 -继续操作前,先执行打印的 PATH 命令。 +安装器复用已有 uv,以及兼容的系统 Python 或 uv 管理的 Python;否则在用户目录补齐 uv 和 Python 3.12。完成后打印实际 Runtime 版本、profile、可执行文件目录、PATH 命令及下一步配置命令,不修改持久 PATH 设置。继续操作前,先执行打印的 PATH 命令。 ## 选择发行版和集成 @@ -60,35 +50,23 @@ Client 依赖。Profile 与宿主选择相互独立:选择数据库角色不 curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.2.0 --host codex ``` -默认安装无需 Git 或宿主选择界面,可以无人值守执行。`--host` 可以重复,用于显式安装集成;这时需要 Git 和 -各宿主自己的前置条件。`--no-hosts` 显式表达默认行为,不能与 `--host` 同时使用。安装后需要交互选择宿主时,运行 -`powercontext setup --ref "powercontext-v$(powercontext --version)"`。 +默认安装无需 Git 或宿主选择界面,可以无人值守执行。`--host` 可以重复,用于显式安装集成;这时需要 Git 和各宿主自己的前置条件。`--no-hosts` 显式表达默认行为,不能与 `--host` 同时使用。安装后需要交互选择宿主时,运行 `powercontext setup --ref "powercontext-v$(powercontext --version)"`。 -已安装的 CLI 提供准确版本,形成 `powercontext-vVERSION`。宿主安装不再次解析 `latest`,也不默认使用 `master`。 -宿主安装失败时,保留已安装的 Runtime,脚本返回非零状态,并提示按该 tag 重试 setup;各适配器提供其已有的宿主结果。 +已安装的 CLI 提供准确版本,形成 `powercontext-vVERSION`。宿主安装不再次解析 `latest`,也不默认使用 `master`。宿主安装失败时,保留已安装的 Runtime,脚本返回非零状态,并提示按该 tag 重试 setup;各适配器提供其已有的宿主结果。 -个人 macOS/Linux 配置受保护文件后,运行 `powercontext service install --env-file .env`、`powercontext service status` -再加载生成的 Client 配置,运行 `powercontext ready` 和 `powercontext capabilities`;由原生用户服务管理器负责生命周期。开发、调试、临时使用或缺少 manager 时, -保留前台 `server run`。Client-only 模式按远程连接指南设置地址和认证。持久服务仍由用户显式注册。 +个人 macOS/Linux 配置受保护文件后,运行 `powercontext service install --env-file .env`、`powercontext service status` 再加载生成的 Client 配置,运行 `powercontext ready` 和 `powercontext capabilities`;由原生用户服务管理器负责生命周期。开发、调试、临时使用或缺少 manager 时,保留前台 `server run`。Client-only 模式按远程连接指南设置地址和认证。持久服务仍由用户显式注册。 ## 配置与个人服务 -配置和服务注册分步复用现有 CLI。先运行 `config init --output .env`,保存后再执行 -`config validate --env-file .env`、`service install --env-file .env` 和 `service status`。 -运行 `ready` 和 `capabilities` 前,加载生成的 Client 配置。 -完整步骤见[安装指南](../docs/get-started/install-and-run.md#配置并安装个人服务)。 +配置和服务注册分步复用现有 CLI。先运行 `config init --output .env`,保存后再执行 `config validate --env-file .env`、`service install --env-file .env` 和 `service status`。运行 `ready` 和 `capabilities` 前,加载生成的 Client 配置。完整步骤见[安装指南](../docs/get-started/install-and-run.md#配置并安装个人服务)。 -安装器不运行配置向导,也不管理服务。取消向导后,不会由安装器继续注册服务。 -服务归属、管理器支持、文件保护、登录自启动和恢复规则仍由现有服务层负责。Windows 个人服务保持试验性支持。 +安装器不运行配置向导,也不管理服务。取消向导后,不会由安装器继续注册服务。服务归属、管理器支持、文件保护、登录自启动和恢复规则仍由现有服务层负责。Windows 个人服务保持试验性支持。 -`--env-file PATH` 仅为显式选择的宿主安装指定已有连接文件,通过 -`setup --env-file PATH HOST...` 及实际安装版本对应的 tag 传入;Shell 不解析或执行文件内容。 -修改服务配置(包括 Scope ID)后,按服务指南使用原文件重新注册。 +`--env-file PATH` 仅为显式选择的宿主安装指定已有连接文件,通过 `setup --env-file PATH HOST...` 及实际安装版本对应的 tag 传入;Shell 不解析或执行文件内容。修改服务配置(包括 Scope ID)后,按服务指南使用原文件重新注册。 ## 选择下载来源 -`--region auto|cn|global` 优先于 `POWERCONTEXT_INSTALL_REGION`。自动模式依次参考本地命名时区、locale 地区, -最后采用全球源,不调用网络定位服务。 +`--region auto|cn|global` 优先于 `POWERCONTEXT_INSTALL_REGION`。自动模式依次参考本地命名时区、locale 地区,最后采用全球源,不调用网络定位服务。 | 组件 | 全球默认源 | 中国区域自动源 | 显式控制 | | --- | --- | --- | --- | @@ -96,17 +74,13 @@ curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1 | uv 安装器及二进制 | Astral 渠道 | USTC release 镜像 | `POWERCONTEXT_UV_INSTALLER_URL`、`UV_DOWNLOAD_URL`、uv 安装器镜像变量 | | Python | uv 默认渠道 | NJU python-build-standalone 镜像 | `UV_PYTHON_INSTALL_MIRROR`、uv Python 下载配置 | -已有 uv 配置时不自动选择包镜像。显式 `--index-url` 只更改默认索引,额外索引沿用 uv 的优先级。 -认证和索引解析由 uv 负责。脚本 URL 参数必须使用 HTTPS,且不包含凭据;凭据通过 uv 配置管理。 -pip 的索引环境变量不属于 uv 配置。Python 和 uv 下载覆盖项与包索引彼此独立。 +已有 uv 配置时不自动选择包镜像。显式 `--index-url` 只更改默认索引,额外索引沿用 uv 的优先级。认证和索引解析由 uv 负责。脚本 URL 参数必须使用 HTTPS,且不包含凭据;凭据通过 uv 配置管理。pip 的索引环境变量不属于 uv 配置。Python 和 uv 下载覆盖项与包索引彼此独立。 # Reference-level explanation ## 安装职责 -入口为 `website/public/install.sh` 和 `website/public/install.ps1`,作为网站静态资源发布。 -两者支持相同的选项和失败语义。脚本负责引导与编排;uv 负责环境创建和包解析;现有 `powercontext setup` 适配器 -负责集成安装。 +入口为 `website/public/install.sh` 和 `website/public/install.ps1`,作为网站静态资源发布。两者支持相同的选项和失败语义。脚本负责引导与编排;uv 负责环境创建和包解析;现有 `powercontext setup` 适配器负责集成安装。 安装操作由以下输入确定: @@ -114,65 +88,40 @@ pip 的索引环境变量不属于 uv 配置。Python 和 uv 下载覆盖项与 包 requirement + Runtime profile + 显式宿主选择 = 安装操作 ``` -包管理器在替换工具环境前解析 requirement。安装完成后验证 CLI 的准确版本,再用于全部宿主安装。 -这样一次操作具有统一的发行版坐标,无需新增包解析器,也不依赖另一个发行清单系统。 +包管理器在替换工具环境前解析 requirement。安装完成后验证 CLI 的准确版本,再用于全部宿主安装。这样一次操作具有统一的发行版坐标,无需新增包解析器,也不依赖另一个发行清单系统。 -保留 `powercontext setup`、`config`、`doctor` 和服务命令。不把领域行为搬到 shell,不引入独立 Python 安装引擎, -也不修改 Runtime 或公共 HTTP API。 +保留 `powercontext setup`、`config`、`doctor` 和服务命令。不把领域行为搬到 shell,不引入独立 Python 安装引擎,也不修改 Runtime 或公共 HTTP API。 ## 版本和 profile 语义 -- `latest` 向 `uv tool install --upgrade --force --prerelease disallow` 传入未固定版本的 profile requirement。 - 它表示所选解释器及已配置源兼容的最新稳定版,不一定是另一镜像上最近上传的版本。重新解析工具环境,避免旧版 uv 中已有预发布版本绕过稳定版选择。解析失败直接报告,不改换 requirement。 -- 准确版本使用 `==VERSION`,格式为 `X.Y.Z`,可追加 `aN`、`bN` 或 `rcN`。支持显式预发布版本; - `--version` 不接受源码 ref、版本范围或 URL,也不支持 0.1.0 之前的版本。 -- `local` 使用 `powercontext[cli,server]`;`client` 使用 `powercontext[cli]`,后者已经包含 Client 依赖。 - 使用不同 profile 重跑会替换该工具的依赖集合;自行添加的 extras 应通过文档中的手动安装路径维护。 -- `uv tool dir --bin` 中的可执行文件必须报告发行版本;显式请求必须与其一致。所有 profile 都需要通过 CLI help、 - `capabilities --help`;`local` 还需要通过 `config init --help` 和 `server run --help`。 - 全部通过后才打印安装成功。这些检查验证导入和命令可用性,不代表 Server 就绪或 Agent 工作流可用。 +- `latest` 向 `uv tool install --upgrade --force --prerelease disallow` 传入未固定版本的 profile requirement。它表示所选解释器及已配置源兼容的最新稳定版,不一定是另一镜像上最近上传的版本。重新解析工具环境,避免旧版 uv 中已有预发布版本绕过稳定版选择。解析失败直接报告,不改换 requirement。 +- 准确版本使用 `==VERSION`,格式为 `X.Y.Z`,可追加 `aN`、`bN` 或 `rcN`。支持显式预发布版本;`--version` 不接受源码 ref、版本范围或 URL,也不支持 0.1.0 之前的版本。 +- `local` 使用 `powercontext[cli,server]`;`client` 使用 `powercontext[cli]`,后者已经包含 Client 依赖。使用不同 profile 重跑会替换该工具的依赖集合;自行添加的 extras 应通过文档中的手动安装路径维护。 +- `uv tool dir --bin` 中的可执行文件必须报告发行版本;显式请求必须与其一致。所有 profile 都需要通过 CLI help、`capabilities --help`;`local` 还需要通过 `config init --help` 和 `server run --help`。全部通过后才打印安装成功。这些检查验证导入和命令可用性,不代表 Server 就绪或 Agent 工作流可用。 -uv 使用安装器维护的固定引导版本,与 PowerContext 发行版本无关。已有 uv 直接复用,不悄悄升级。 -Python 发现排除虚拟环境,避免项目 venv 意外成为安装前提。没有兼容的 Python 3.11+ 时,通过 uv 安装 Python 3.12。 -平台、架构和 wheel 兼容性错误由 uv 报告。 +uv 使用安装器维护的固定引导版本,与 PowerContext 发行版本无关。已有 uv 直接复用,不悄悄升级。Python 发现排除虚拟环境,避免项目 venv 意外成为安装前提。没有兼容的 Python 3.11+ 时,通过 uv 安装 Python 3.12。平台、架构和 wheel 兼容性错误由 uv 报告。 ## 镜像优先级与恢复 -保留用户显式指定的 uv/Python 来源,不做自动回退。中国区域自动选择时,uv 镜像安装器不可用则回退 Astral; -uv 镜像文件下载失败时可以尝试官方源。Python 的准确构建 URL 由 uv 提供,安装前检查对应镜像文件; -自动 Python 镜像安装失败后,使用 uv 默认渠道重试。 +保留用户显式指定的 uv/Python 来源,不做自动回退。中国区域自动选择时,uv 镜像安装器不可用则回退 Astral;uv 镜像文件下载失败时可以尝试官方源。Python 的准确构建 URL 由 uv 提供,安装前检查对应镜像文件;自动 Python 镜像安装失败后,使用 uv 默认渠道重试。 -中国区域自动包源选择对 PowerContext 索引页发起有超时限制的可用性请求;镜像不可达时,在工具安装前回退 PyPI。 -可达镜像即使缺少请求版本也保持选中,由 uv 报告解析失败。全球源和显式来源直接交给 uv。脚本不解释索引 HTML、 -wheel 文件名、编码 URL、包兼容性或认证,也不在一次可能已经改变安装文件的工具安装尝试后换源重试。 +中国区域自动包源选择对 PowerContext 索引页发起有超时限制的可用性请求;镜像不可达时,在工具安装前回退 PyPI。可达镜像即使缺少请求版本也保持选中,由 uv 报告解析失败。全球源和显式来源直接交给 uv。脚本不解释索引 HTML、wheel 文件名、编码 URL、包兼容性或认证,也不在一次可能已经改变安装文件的工具安装尝试后换源重试。 -Shell 环境变化局限于安装器进程;PowerShell 在 `finally` 恢复临时来源变量。不用 shell 解析或改写已有配置文件, -只清理由安装器创建的临时下载目录。保留 `UV_INSTALL_DIR`、`UV_TOOL_DIR` 和 `UV_TOOL_BIN_DIR` 的位置控制。 +Shell 环境变化局限于安装器进程;PowerShell 在 `finally` 恢复临时来源变量。不用 shell 解析或改写已有配置文件,只清理由安装器创建的临时下载目录。保留 `UV_INSTALL_DIR`、`UV_TOOL_DIR` 和 `UV_TOOL_BIN_DIR` 的位置控制。 ## 持久化与兼容性 -普通包安装不读取或改写 `.env`、凭据、数据目录或数据库 schema,也不停止或重启服务。 -配置和服务注册分步复用现有 CLI。 -启动升级后的 Server 时,用户按现有升级和迁移指南操作。准确版本重试可以使用缓存;`latest` 则明确允许升级。 +普通包安装不读取或改写 `.env`、凭据、数据目录或数据库 schema,也不停止或重启服务。配置和服务注册分步复用现有 CLI。启动升级后的 Server 时,用户按现有升级和迁移指南操作。准确版本重试可以使用缓存;`latest` 则明确允许升级。 -`UV_OFFLINE=1` 只支持已有 uv、兼容 Python 和全部依赖时的缓存重装,同时禁用安装器源探测和 uv 网络访问, -不承诺完整离线发行包。后续步骤失败会保留已安装的前置依赖。准确版本不存在导致的解析失败会保留原工具; -但 uv 接受新包后,验证失败可能发生在旧可执行文件已被替换之后。此时明确报告“包已安装,验证失败”,允许通过准确 -版本重试,不声称已经回滚。后续宿主安装失败则保留已验证的 Runtime。不提供跨组件回滚,也不修复无关宿主状态。 +`UV_OFFLINE=1` 只支持已有 uv、兼容 Python 和全部依赖时的缓存重装,同时禁用安装器源探测和 uv 网络访问,不承诺完整离线发行包。后续步骤失败会保留已安装的前置依赖。准确版本不存在导致的解析失败会保留原工具;但 uv 接受新包后,验证失败可能发生在旧可执行文件已被替换之后。此时明确报告“包已安装,验证失败”,允许通过准确版本重试,不声称已经回滚。后续宿主安装失败则保留已验证的 Runtime。不提供跨组件回滚,也不修复无关宿主状态。 Windows 保持产品的试验性支持状态。原生验收在 Linux、macOS 和 Windows 执行;具体宿主支持范围仍由各集成的能力契约决定。 ## 可执行契约与验收 -`tests/fixtures/installation/*.json` 定义两个 shell 适配器共享的公开输入与预期结果。 -`tests/test_installation_contract.py` 使用真实 uv 和小型离线 fixture wheel 执行宿主显式选择、profile、准确版本 -校验、命令缺失和宿主部分失败用例。用例观察安装后的能力、所选宿主、发行 ref 和退出结果,不固定内部函数边界或 -调用顺序。同一组用例在各操作系统原生执行;模拟宿主执行不等于真实原生服务或 Agent 验收。 -宿主用例覆盖传递已有连接文件;配置和个人服务生命周期验收与安装器契约分开。 +`tests/fixtures/installation/*.json` 定义两个 shell 适配器共享的公开输入与预期结果。`tests/test_installation_contract.py` 使用真实 uv 和小型离线 fixture wheel 执行宿主显式选择、profile、准确版本校验、命令缺失和宿主部分失败用例。用例观察安装后的能力、所选宿主、发行 ref 和退出结果,不固定内部函数边界或调用顺序。同一组用例在各操作系统原生执行;模拟宿主执行不等于真实原生服务或 Agent 验收。宿主用例覆盖传递已有连接文件;配置和个人服务生命周期验收与安装器契约分开。 -`tests/native/test_installation.py` 执行真实 shell 安装器、uv、安装后的 PowerContext CLI 和 HTTP Server。 -从被测提交构建 wheel,使用发行版形式的元数据和直接本地文件约束。该约束选定被测产物,不构成独立执行的校验和验证。 -验收包括: +`tests/native/test_installation.py` 执行真实 shell 安装器、uv、安装后的 PowerContext CLI 和 HTTP Server。从被测提交构建 wheel,使用发行版形式的元数据和直接本地文件约束。该约束选定被测产物,不构成独立执行的校验和验证。验收包括: - 全球源及中国区域源下缺少 uv/Python 的安装、已有工具与配置、包含空格和中文的路径; - 通过受控包索引解析真实稳定版和预发布版 wheel、默认升级、指定版本,以及版本不存在时保留原安装; @@ -180,63 +129,36 @@ Windows 保持产品的试验性支持状态。原生验收在 Linux、macOS 和 - `.env` 生成与验证、就绪检查、Memory 保存与搜索、离线缓存重装及重启后的回读; - 显式索引失败、宿主选项冲突、无人值守与 Bash 管道安装,以及 PowerShell 下载脚本后执行 scriptblock 的入口。 -独立 CI 矩阵在三个操作系统执行上述套件。发布引用检查保持脚本的 `latest` 默认值,同时更新显式版本示例。 -网站验证检查文档链接和静态构建。语法检查本身不证明 Windows 或 macOS 安装可用;这些测试也不声称 -真实 Agent 宿主已完成采集与召回工作流。 - -[可复现的第一性原理和消融研究](https://github.com/PsiACE/powercontext/tree/feat/installation-contract/experiments/installation) -固定上游源码、安装器基线、uv 版本、受控输入和 -观测结果。它们支持以下决策:删除隐式宿主选择仍可保留显式宿主失败语义;基于文件名的预检查会拒绝合法编码包 URL; -只有预发布版本的索引需要显式稳定版策略;包安装成功后入口仍可能不可用。研究与持续维护的产品验收分开,分别说明 -实际执行范围和限制。 +独立 CI 矩阵在三个操作系统执行上述套件。发布引用检查保持脚本的 `latest` 默认值,同时更新显式版本示例。网站验证检查文档链接和静态构建。语法检查本身不证明 Windows 或 macOS 安装可用;这些测试也不声称真实 Agent 宿主已完成采集与召回工作流。 # Drawbacks -Bash 和 PowerShell 会重复部分编排和来源选择策略。自动镜像增加外部可用性依赖。 -`latest` 会随时间变化,可重复部署需要指定版本。匹配 tag 能协调发行版,但不能单独证明宿主兼容性。 +Bash 和 PowerShell 会重复部分编排和来源选择策略。自动镜像增加外部可用性依赖。`latest` 会随时间变化,可重复部署需要指定版本。匹配 tag 能协调发行版,但不能单独证明宿主兼容性。 # Rationale and alternatives -保留直接运行 uv 命令的路径,供已有 Python 和 uv 的用户使用;它无法引导干净机器。 -源码安装适合开发,不作为默认发行渠道。把公共脚本固定在某个 PowerContext 版本,会使新用户在网站重新发布前 -一直安装过期版本。 +保留直接运行 uv 命令的路径,供已有 Python 和 uv 的用户使用;它无法引导干净机器。源码安装适合开发,不作为默认发行渠道。把公共脚本固定在某个 PowerContext 版本,会使新用户在网站重新发布前一直安装过期版本。 -没有 Python 的机器仍需要为 Python 引擎提供引导。生成 shell 适配器仍需原生行为测试,还会增加生成器和版本管理。 -共享契约用例可以提供可执行的一致性约束,无需分发新的运行时资源。仅当具体策略确有需要时再引入引擎或生成器。 +没有 Python 的机器仍需要为 Python 引擎提供引导。生成 shell 适配器仍需原生行为测试,还会增加生成器和版本管理。共享契约用例可以提供可执行的一致性约束,无需分发新的运行时资源。仅当具体策略确有需要时再引入引擎或生成器。 -分发独立版本的原始产物时,不可变清单和额外安装账本有其用途。这里由 uv 管理 wheel 解析、工具环境及自身记录; -安装器观察实际入口,通过显式的 uv 安装重试修复组件,避免同一安装状态出现两个权威来源。 +分发独立版本的原始产物时,不可变清单和额外安装账本有其用途。这里由 uv 管理 wheel 解析、工具环境及自身记录;安装器观察实际入口,通过显式的 uv 安装重试修复组件,避免同一安装状态出现两个权威来源。 # Prior art -[RFC 1408](https://github.com/oceanbase/powercontext/pull/1408) 定义了安装、配置和诊断的职责分离, -独立的 Runtime profile 与宿主选择、组件级恢复和显式服务注册。 -[RFC 1299](1299_local_server_availability_and_service_installation.md) 定义个人服务生命周期。 +[RFC 1408](https://github.com/oceanbase/powercontext/pull/1408) 定义了安装、配置和诊断的职责分离,独立的 Runtime profile 与宿主选择、组件级恢复和显式服务注册。[RFC 1299](1299_local_server_availability_and_service_installation.md) 定义个人服务生命周期。 -[Magpie 安装器](https://github.com/yetone/magpie/blob/023f5aaad2ecd41ae04390166b9cac9a0b300d81/site/public/install.sh) -将权威发行源与产物镜像分开,在替换二进制前检查校验和。它直接拥有原始二进制的分发职责,因此需要这层验证; -其清单不能替代 Python 包标准。 +[Magpie 安装器](https://github.com/yetone/magpie/blob/023f5aaad2ecd41ae04390166b9cac9a0b300d81/site/public/install.sh) 将权威发行源与产物镜像分开,在替换二进制前检查校验和。它直接拥有原始二进制的分发职责,因此需要这层验证;其清单不能替代 Python 包标准。 -[Lody daemon 安装契约](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/specs/daemon-upgrade-installation.md) -解析并执行实际 npm 安装位置,检查替换后的就绪状态。它另有直接管理原始压缩包的 -[Agent runtime](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/apps/cli/src/agent/managed-agent-runtime.ts), -因此拥有清单和完成记录。本方案采用实际可执行文件检查,服务就绪仍归服务边界负责。其公开 tag-release 工作流不发布安装器。 +[Lody daemon 安装契约](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/specs/daemon-upgrade-installation.md) 解析并执行实际 npm 安装位置,检查替换后的就绪状态。它另有直接管理原始压缩包的 [Agent runtime](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/apps/cli/src/agent/managed-agent-runtime.ts),因此拥有清单和完成记录。本方案采用实际可执行文件检查,服务就绪仍归服务边界负责。其公开 tag-release 工作流不发布安装器。 -[Bub](https://github.com/bubbuild/bub/tree/b4a61bf1326729a024161d22ba20019b8500f907/website/public) 先引导 uv,再运行 -Python preset 解析器。它对终端和 macOS Bash 的修复支持显式宿主安装与原生测试的必要性。 -[uv 工具环境](https://docs.astral.sh/uv/concepts/tools/)及 -[配置规则](https://docs.astral.sh/uv/concepts/configuration-files/)定义复用的环境与来源语义; -[Python Simple API](https://packaging.python.org/en/latest/specifications/simple-repository-api/)定义包索引解释规则。 -Shell 正则无法实现这些契约。 +[Bub](https://github.com/bubbuild/bub/tree/b4a61bf1326729a024161d22ba20019b8500f907/website/public) 先引导 uv,再运行 Python preset 解析器。它对终端和 macOS Bash 的修复支持显式宿主安装与原生测试的必要性。[uv 工具环境](https://docs.astral.sh/uv/concepts/tools/)及 [配置规则](https://docs.astral.sh/uv/concepts/configuration-files/)定义复用的环境与来源语义;[Python Simple API](https://packaging.python.org/en/latest/specifications/simple-repository-api/)定义包索引解释规则。Shell 正则无法实现这些契约。 # Unresolved questions -接受本契约无需额外的跨子系统决策。完整离线包、带密码学校验的安装清单、独立打包的宿主产物以及配置存储变化, -需要各自设计。推广前必须保持平台矩阵通过,Windows 产品支持仍为试验性。 +接受本契约无需额外的跨子系统决策。完整离线包、带密码学校验的安装清单、独立打包的宿主产物以及配置存储变化,需要各自设计。推广前必须保持平台矩阵通过,Windows 产品支持仍为试验性。 # Future possibilities -当安装计划需要不可变组件清单、独立集成版本或持久组件修复记录时,脚本可以引导发行版维护的独立安装引擎。 -离线包也可以显式携带全部所需产物。这些扩展继续保持 Server 启动与服务注册的显式同意。 +当安装计划需要不可变组件清单、独立集成版本或持久组件修复记录时,脚本可以引导发行版维护的独立安装引擎。离线包也可以显式携带全部所需产物。这些扩展继续保持 Server 启动与服务注册的显式同意。 个人服务引导工作关联 [Issue #1900](https://github.com/oceanbase/powercontext/issues/1900)。 diff --git a/tests/fixtures/installation/README.md b/tests/fixtures/installation/README.md index 37e9948c3b..82a6161d98 100644 --- a/tests/fixtures/installation/README.md +++ b/tests/fixtures/installation/README.md @@ -1,8 +1,6 @@ # Installer behavior contract -The JSON catalogs describe public installer inputs and observable outcomes. Both native shell entry points use -the same cases through `tests/test_installation_contract.py`. The catalogs are test inputs, not bootstrap downloads -or an additional installation manifest. RFC 1892 defines the overall installation and recovery boundary. +The JSON catalogs describe public installer inputs and observable outcomes. Both native shell entry points use the same cases through `tests/test_installation_contract.py`. The catalogs are test inputs, not bootstrap downloads or an additional installation manifest. RFC 1892 defines the overall installation and recovery boundary. Run the offline contract with an existing Python and uv: @@ -30,14 +28,6 @@ Each file has `schema_version: 1` and a `cases` array. Case IDs must be unique a | `expected.verification_failed` | Whether success must remain unannounced | | `expected.error_contains` | Required diagnostic fragment | -The harness builds small standards-shaped wheels, installs them through real uv in private directories without -network access, and executes the resulting native launcher. Profile cases inspect an optional installed dependency. -Host cases observe the setup request and result through the fixture CLI. They do not start a real Agent, execute Git, -or establish Server readiness. No case requires a particular shell function name, private call order or subprocess count. +The harness builds small standards-shaped wheels, installs them through real uv in private directories without network access, and executes the resulting native launcher. Profile cases inspect an optional installed dependency. Host cases observe the setup request and result through the fixture CLI. They do not start a real Agent, execute Git, or establish Server readiness. No case requires a particular shell function name, private call order or subprocess count. -The installation CI matrix runs these cases on Linux, macOS and Windows. The same workflow also builds the actual -PowerContext wheel and runs `tests/native/test_installation.py` for bootstrap, configuration, HTTP readiness, -Memory persistence, profile replacement and version selection. `tests/native/test_install_sources.py` adds Linux -loopback index fault tests using the real downloader and uv, with only distribution endpoints redirected to fixtures. -Those source tests do not qualify public mirror availability or native Windows source handling. Personal-service -lifecycle acceptance remains in the existing native service workflow, separate from installer conformance. +The installation CI matrix runs these cases on Linux, macOS and Windows. The same workflow also builds the actual PowerContext wheel and runs `tests/native/test_installation.py` for bootstrap, configuration, HTTP readiness, Memory persistence, profile replacement and version selection. `tests/native/test_install_sources.py` adds Linux loopback index fault tests using the real downloader and uv, with only distribution endpoints redirected to fixtures. Those source tests do not qualify public mirror availability or native Windows source handling. Personal-service lifecycle acceptance remains in the existing native service workflow, separate from installer conformance. From fb0843efc3fd2c33b8b74e376f068f7d7d239c2f Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Mon, 2 Nov 2026 12:11:05 +0800 Subject: [PATCH 18/24] docs(install): organize guidance for its readers --- README.md | 6 +- README_CN.md | 6 +- README_JP.md | 4 +- docs/en/docs/get-started/install-and-run.md | 111 ++++++++----- docs/en/docs/get-started/quickstart.md | 6 +- docs/en/rfcs/1892-script-installation.md | 172 +++++++++++--------- docs/zh/docs/get-started/install-and-run.md | 113 ++++++++----- docs/zh/docs/get-started/quickstart.md | 6 +- docs/zh/rfcs/1892-script-installation.md | 170 ++++++++++--------- tests/fixtures/installation/README.md | 28 +++- 10 files changed, 354 insertions(+), 268 deletions(-) diff --git a/README.md b/README.md index 617df53815..d8a332dc8a 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,8 @@ powercontext config init --language en --output .env The wizard asks about storage, local or remote usage, memory capabilities, Dashboard/access, and Agent connections. SQLite is the simplest first storage choice; choose embedded seekdb only when you need it and can approve its dependency install. +The wizard writes `.env` and `.env.next-steps.md`. These files can contain credentials; do not commit them. Linux needs an available `systemd --user` manager for the personal service. + After saving `.env`, validate it and register the personal service: ```bash @@ -151,7 +153,7 @@ powercontext service install --env-file .env powercontext service status ``` -The wizard writes one `.env` environment file and `.env.next-steps.md`. These files can contain credentials; do not commit them. The native current-user service manages the Server lifecycle; Linux requires an available `systemd --user` manager. Load the generated environment file and check the connection: +The service keeps the Server running after you close the terminal. Load the generated environment file and check the connection: ```bash set -a @@ -161,7 +163,7 @@ powercontext ready powercontext capabilities ``` -Continue with `.env.next-steps.md` to create and bind the selected Scopes. Use the real `scope_id` returned by the creation request; a planned `codex-...` title is not an ID. Changing directories does not create isolation; use the same Server and Scope in the Dashboard and Agent. After editing `.env`, rerun `powercontext service install --env-file .env` to reconcile the service. Then install the matching plugins and launch a new Agent session. The [complete walkthrough](https://powercontext.oceanbase.io/en/docs/get-started/quickstart/) covers Codex and Claude Code, Dashboard login, SSH forwarding, HTTPS prerequisites, and observable acceptance checks. For example, the matching Codex installation is: +Continue with `.env.next-steps.md` to create and bind the selected Scopes. Use the real `scope_id` returned by the creation request; a planned `codex-...` title is not an ID. Changing directories does not create isolation; use the same Server and Scope in the Dashboard and Agent. After editing `.env`, rerun `powercontext service install --env-file .env` to apply the updated settings. Then install the matching plugins and launch a new Agent session. The [complete walkthrough](https://powercontext.oceanbase.io/en/docs/get-started/quickstart/) covers Codex and Claude Code, Dashboard login, SSH forwarding, HTTPS prerequisites, and observable acceptance checks. For example, the matching Codex installation is: ```bash powercontext setup codex --ref powercontext-v1.4.1 diff --git a/README_CN.md b/README_CN.md index fbd758da20..b75f8487a4 100644 --- a/README_CN.md +++ b/README_CN.md @@ -141,6 +141,8 @@ powercontext config init --language zh --output .env 向导会询问存储、本机或远程使用场景、记忆能力、Dashboard/访问控制和 Agent 连接。首次体验可以使用 SQLite;只有确实需要时再选择嵌入式 seekdb,并同意依赖安装。 +向导生成 `.env` 和 `.env.next-steps.md`。这些文件可能包含凭据,请勿提交到 Git。Linux 上的个人服务需要可用的 `systemd --user` 管理器。 + 保存 `.env` 后,验证配置并注册个人服务: ```bash @@ -149,7 +151,7 @@ powercontext service install --env-file .env powercontext service status ``` -向导生成一个 `.env` 环境文件和 `.env.next-steps.md`。这些文件可能包含凭据,请勿提交到 Git。原生当前用户服务负责 Server 生命周期;Linux 需要可用的 `systemd --user` 管理器。加载生成的环境文件并检查服务: +服务会在关闭终端后继续运行 Server。加载生成的环境文件并检查服务: ```bash set -a @@ -159,7 +161,7 @@ powercontext ready powercontext capabilities ``` -接着按 `.env.next-steps.md` 创建并绑定 Scope。使用创建请求返回的真实 `scope_id`;向导规划的 `codex-...` 标题不是 ID。切换目录不会创建隔离;Dashboard 和 Agent 要使用同一个 Server 和 Scope。修改 `.env` 后,重新执行 `powercontext service install --env-file .env` 以协调服务配置。然后安装相同版本的插件,再打开新 Agent 会话。[完整安装流程](https://powercontext.oceanbase.io/zh/docs/get-started/quickstart/)包含 Codex、Claude Code、Dashboard 登录、SSH 隧道、HTTPS 前提及逐项验收。例如,匹配本版本的 Codex 安装命令是: +接着按 `.env.next-steps.md` 创建并绑定 Scope。使用创建请求返回的真实 `scope_id`;向导规划的 `codex-...` 标题不是 ID。切换目录不会创建隔离;Dashboard 和 Agent 要使用同一个 Server 和 Scope。修改 `.env` 后,重新执行 `powercontext service install --env-file .env` 使修改后的配置生效。然后安装相同版本的插件,再打开新 Agent 会话。[完整安装流程](https://powercontext.oceanbase.io/zh/docs/get-started/quickstart/)包含 Codex、Claude Code、Dashboard 登录、SSH 隧道、HTTPS 前提及逐项验收。例如,匹配本版本的 Codex 安装命令是: ```bash powercontext setup codex --ref powercontext-v1.4.1 diff --git a/README_JP.md b/README_JP.md index 54a2e7c1a3..8f6ac4bff2 100644 --- a/README_JP.md +++ b/README_JP.md @@ -139,6 +139,8 @@ powercontext config init --language en --output .env ウィザードでは、ストレージ、ローカルまたはリモート利用、メモリ機能、Dashboard/アクセス制御、Agent 接続を選択します。最初は SQLite が簡単です。組み込み seekdb は必要な場合だけ選び、依存関係のインストールを許可してください。 +ウィザードは `.env` と `.env.next-steps.md` を生成します。これらのファイルには認証情報が含まれることがあるため、コミットしないでください。Linux のユーザーサービスには利用可能な `systemd --user` マネージャーが必要です。 + `.env` を保存してから、設定を検証してユーザーサービスを登録します。 ```bash @@ -147,7 +149,7 @@ powercontext service install --env-file .env powercontext service status ``` -ウィザードは `.env` と `.env.next-steps.md` を生成します。これらのファイルには認証情報が含まれることがあるため、コミットしないでください。Server のライフサイクルはネイティブのユーザーサービスが管理します。Linux では利用可能な `systemd --user` マネージャーが必要です。生成された環境ファイルを読み込んで接続を確認します。 +サービスはターミナルを閉じた後も Server を実行します。生成された環境ファイルを読み込んで接続を確認します。 ```bash set -a diff --git a/docs/en/docs/get-started/install-and-run.md b/docs/en/docs/get-started/install-and-run.md index 0672b5ca48..1878a62d5f 100644 --- a/docs/en/docs/get-started/install-and-run.md +++ b/docs/en/docs/get-started/install-and-run.md @@ -5,9 +5,7 @@ description: Install PowerContext 1.4.1 and run the local Server. # Install and run -For an Agent connecting from another machine, see [Connect to a remote Server](../operate/connect-remote-server.md) for guided URL confirmation, unattended setup, and endpoint-bound HTTP consent. - -Start with the [Quick Start](quickstart.md) for your first session. This page covers version selection, platforms, installation roles, startup, diagnostics, and updates. +Use this guide to install PowerContext and run a personal Server. For your first Agent session, follow the [Quick Start](quickstart.md). To connect to an existing Server, use the [remote connection guide](../operate/connect-remote-server.md). ## Platform support @@ -16,13 +14,13 @@ Start with the [Quick Start](quickstart.md) for your first session. This page co | macOS, Linux | Supported | | Windows | `experimental` | -Windows CLI, Server, and personal-service support is experimental. Each Agent Host still has its own platform requirements. Examples using Bash syntax require a Bash environment and cannot be pasted directly into PowerShell. Embedded seekDB is unavailable on Windows. +Windows CLI, Server, and personal-service support is experimental. Each Agent host has its own requirements. Embedded seekDB is unavailable on Windows. Bash examples below require a Bash shell. ## Install with the recommended script -The script installs the CLI and local Server in an isolated uv tool environment. It reuses compatible uv/Python, or installs uv and Python 3.12 in user-owned directories. It does not need administrator privileges. +The script installs the CLI and local Server in an isolated uv tool environment. It reuses compatible Python and uv, or installs the missing prerequisites in your user directory. You do not need administrator privileges. -macOS or Linux uses Bash and requires `curl` or `wget`; Windows requires PowerShell 5.1 or newer. +macOS or Linux needs Bash and `curl` or `wget`; Windows needs PowerShell 5.1 or newer. ```bash tab="macOS / Linux" curl -fsSL https://powercontext.oceanbase.io/install.sh | bash @@ -32,21 +30,23 @@ curl -fsSL https://powercontext.oceanbase.io/install.sh | bash powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` -Apply the PATH command printed at completion before running `powercontext` in your terminal. The installer does not edit shell startup files or the persistent Windows PATH. `UV_INSTALL_DIR` selects where a missing uv is installed; `UV_TOOL_BIN_DIR` selects the PowerContext executable directory. +With `wget`, use `wget -qO- https://powercontext.oceanbase.io/install.sh | bash`. -Installation runs unattended by default. Pass `--host codex` (repeatable) to install selected Agent integrations. Only Agent setup needs Git and each host's prerequisites. `--no-hosts` remains an optional way to state the default; it cannot be combined with `--host`. The [capability matrix](../integrations/capabilities.md) describes host support. +Run the PATH command printed at completion before using `powercontext` in your terminal. The script prints how to make the installed commands available; it leaves shell startup files and the persistent Windows PATH unchanged. -For an existing remote Server, add `--profile client` to install only the CLI and Client dependencies. The default `--profile local` includes the local Server. Neither profile starts a Server, registers a service, or overwrites configuration or data. For a local installation, continue with `powercontext config init` and [Quick Start](quickstart.md). Client-only installations use endpoint settings in the [remote connection guide](../operate/connect-remote-server.md). +The default installs the latest stable release and local Server dependencies. After installation, continue with the personal-service steps below. For a remote Server, use `--profile client` and follow the [remote connection guide](../operate/connect-remote-server.md). ## Configure and install a personal service -For personal macOS/Linux use, configure the local Server after installation: +On macOS or Linux, configure a local Server: ```bash powercontext config init --output .env ``` -Save `.env` before continuing. If you cancel the wizard, stop here. Then validate the saved file and explicitly register the native current-user service: +Choose local usage and loopback binding in the wizard. Save `.env` before continuing. If you cancel, stop here. Linux needs an available `systemd --user` manager. For Windows service setup and login startup, follow [Deploy the Server](../operate/deploy-server.md). + +Validate the saved file and register the service: ```bash powercontext config validate --env-file .env @@ -54,9 +54,7 @@ powercontext service install --env-file .env powercontext service status ``` -Choose loopback binding for a personal service. Linux needs an available `systemd --user` manager. Windows personal services remain experimental; see [Deploy the Server](../operate/deploy-server.md) for file protection and login startup. - -Load the generated Client settings and check the running Server: +The service keeps the Server running after you close the terminal. Load the client settings and check the connection: ```bash set -a @@ -66,62 +64,85 @@ powercontext ready powercontext capabilities ``` -After any file edit, including writing returned Scope IDs, rerun `service install --env-file` with the original file. Use the same path if you saved the configuration elsewhere. A running service does not establish an Agent workflow; continue with the [Quick Start](quickstart.md) for Scope creation and Agent acceptance. +If you edit `.env`, including adding Scope IDs, rerun `powercontext service install --env-file .env` and reload the client settings. Use the original file path if you saved it elsewhere. The [Quick Start](quickstart.md) explains Scope creation and how to verify memory from an Agent session. -The installer only installs software and explicitly selected integrations. `--env-file PATH` can supply an existing connection file to selected hosts through the existing `setup` command; configuration and service registration remain separate CLI steps. - -## Installer version selection +For development, debugging, or systems without a native user-service manager, use the [foreground Server instructions](#run-the-local-server). -The default `--version latest` installs or upgrades to the newest stable release available from the selected index and compatible with the chosen Python. It excludes prereleases. `--version` accepts an exact release, including an explicit prerelease such as `1.3.0rc1`; it never substitutes a different version if that release is unavailable. +## Install Agent integrations -For example, install PowerContext 1.4.1 with its Codex integration: +Install each Agent's CLI first, then add its PowerContext integration. For Codex: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.4.1 --host codex +powercontext setup codex --ref "powercontext-v$(powercontext --version)" ``` -This selects package `1.4.1` and Git tag `powercontext-v1.4.1`. With `latest`, the installer reads the installed CLI's version and uses its matching tag; it never uses the moving `master` branch for host setup. To add an integration later: +This uses the tag matching your installed PowerContext version. Run `powercontext setup --ref "powercontext-v$(powercontext --version)"` to choose hosts interactively. Follow the selected [integration guide](../integrations/index.md) to check the connection and enable its hooks. -```bash -powercontext setup codex --ref "powercontext-v$(powercontext --version)" -``` +To install an integration with the package, pass `--host codex` to the installer. Repeat `--host` for multiple hosts; this needs Git and each host's prerequisites. If you already have a connection file, pass `--env-file PATH` with the selected hosts. The script passes that file to `setup`. Without `--host`, installation skips integrations; `--no-hosts` states this explicitly and cannot be combined with `--host`. See the [capability matrix](../integrations/capabilities.md) for supported hosts. -Use `powercontext setup --ref "powercontext-v$(powercontext --version)"` for an interactive host picker. +## Installer version selection -In PowerShell, the same double-quoted expression works. An integration failure leaves the installed Runtime usable and exits with an error and a retry instruction. Installation success does not establish Server readiness or host workflow correctness; use the checks below and the integration's own guide. +The default `--version latest` installs or upgrades to the newest compatible stable release on the selected index. To install PowerContext 1.4.1, choose the command for your platform: -The installer verifies the installed version and advertised commands before reporting success. A package can install successfully while command verification fails; in that case the old executable may already have been replaced. Review the named command error and rerun with a known working `--version`. The installer does not roll package files back. +```bash tab="macOS / Linux" +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.4.1 +``` + +```powershell tab="Windows" +irm https://powercontext.oceanbase.io/install.ps1 -OutFile install.ps1 +powershell -ExecutionPolicy Bypass -File .\install.ps1 --version 1.4.1 +``` + +You can also request an explicit prerelease, such as `1.3.0rc1`. If the selected index lacks that version, installation fails and preserves the installed tool. `latest` excludes prereleases. A mirror may lag PyPI, so its latest available release may differ. ## Retry dependency downloads with a mirror -Package indexes, uv binaries, and Python distributions are separate downloads. Changing the package index does not change the uv or Python download location. +Choose the China mirrors with `--region cn`: + +```bash tab="macOS / Linux" +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --region cn +``` + +```powershell tab="Windows" +irm https://powercontext.oceanbase.io/install.ps1 -OutFile install.ps1 +powershell -ExecutionPolicy Bypass -File .\install.ps1 --region cn +``` + +`--region auto|cn|global` overrides `POWERCONTEXT_INSTALL_REGION`. Without an explicit region, the script uses your named timezone, then locale territory, then global sources. Use `--region global` to select global defaults. + +For a specific download problem, set the corresponding source. A package index setting does not change where Python or uv is downloaded: | Download | Explicit setting | Automatic China source | Global source | | --- | --- | --- | --- | -| PowerContext and Python packages | `--index-url URL`, uv index environment variables or `uv.toml` | Tsinghua PyPI mirror | PyPI | +| PowerContext and Python packages | `--index-url URL`, uv index variables or `uv.toml` | Tsinghua PyPI mirror | PyPI | | uv installer | `POWERCONTEXT_UV_INSTALLER_URL` | USTC uv release mirror | Astral installer | | uv binaries | `UV_DOWNLOAD_URL` or `UV_INSTALLER_GITHUB_BASE_URL` | USTC uv release mirror | Astral release channels | | Python distributions | `UV_PYTHON_INSTALL_MIRROR` | NJU python-build-standalone mirror | uv default channels | -```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --region cn -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --index-url https://pypi.org/simple -``` +Existing uv settings take precedence over automatic mirrors. `--index-url` changes the default package index; additional uv indexes keep their priority. Store private-index credentials in uv's authentication configuration. uv does not read `PIP_INDEX_URL` or `PIP_EXTRA_INDEX_URL`. Existing Python mirror settings and `uv.toml` also suppress automatic Python mirrors. `UV_ASTRAL_MIRROR_URL` is passed to uv versions that support it. -`--region auto|cn|global` overrides `POWERCONTEXT_INSTALL_REGION`. Auto selection uses a named local timezone, then the locale territory, then global; it makes no geolocation request. An unavailable automatic mirror can fall back to the official source. A reachable China package mirror remains selected even if it lacks the requested release; uv reports resolution or artifact failures without retrying against another index. Use `--region global` or an explicit source to select another index. Explicit sources never fall back automatically. A mirror may lag PyPI: `latest` means the newest compatible stable release on the chosen index. +An unavailable automatic mirror can fall back to an official source. A reachable package mirror that lacks the requested version returns an installation error. Select `--region global` or an explicit index to retry elsewhere. Explicit sources stay selected when they fail. -Existing uv index settings and configuration files take precedence over automatic package mirrors. An explicit `--index-url` overrides the default index only; additional uv indexes keep their priority. Private index credentials belong in uv's authentication configuration, not in script arguments. uv does not read `PIP_INDEX_URL` or `PIP_EXTRA_INDEX_URL`. Existing Python mirror settings and `uv.toml` also suppress automatic Python mirror selection. `UV_ASTRAL_MIRROR_URL` is passed through for uv versions that support it. These settings are scoped to the installation; your persistent package-manager configuration is unchanged. An existing uv is reused without upgrading it. +## Recover a failed installation -To inspect the installer or pass several options in PowerShell, save it first: +Use the error reported by the installer to choose the next step: -```powershell -irm https://powercontext.oceanbase.io/install.ps1 -OutFile install.ps1 -powershell -ExecutionPolicy Bypass -File .\install.ps1 --region cn --version 1.4.1 -``` +| Failure | What to do | +| --- | --- | +| Python or uv download failed | Check the download setting above and rerun the installer | +| Package resolution or download failed | Check the selected index and requested version, then retry | +| Package installed, command verification failed | Review the named command error and rerun with a known working `--version`; the previous executable may already have been replaced | +| Agent integration setup failed | Keep the installed PowerContext and retry `powercontext setup` with the release tag printed in the error | -Cached reinstallation with `UV_OFFLINE=1` can work when uv, compatible Python, and all required packages are already present. This is not an offline distribution bundle; missing downloads fail explicitly. +For cached reinstallation, set `UV_OFFLINE=1` only when uv, compatible Python, and all required packages are available. Missing downloads fail in offline mode. -## Choose a version +To choose installation locations, use `UV_INSTALL_DIR` for a missing uv and `UV_TOOL_BIN_DIR` for the PowerContext commands. Reused uv versions and your persistent package-manager settings remain unchanged. + +## Install manually with uv + +If you already manage Python and uv, you can install a fixed release directly. Choose a version, then install the required package extras. + +### Choose a version These instructions use PowerContext 1.4.1. Keep the package and Agent integration on the same version: package `1.4.1` and Git tag `powercontext-v1.4.1`. @@ -132,7 +153,7 @@ powercontext setup codex --ref powercontext-v1.4.1 Check the [capability matrix](../integrations/capabilities.md) for host support and maintenance status. Capabilities marked `experimental` remain experimental in this release. -## Install the application +### Install the application You need Python 3.11 or newer, Git, and [`uv`](https://docs.astral.sh/uv/) on macOS, Linux, or Windows. Then install PowerContext from PyPI: @@ -154,6 +175,8 @@ To install another branch or tag, replace the ref after the final `@`. The `mast ## Run the local Server +Use this foreground mode for development, debugging, or temporary use. Stop an existing personal service before starting another Server on the same address. + ```bash powercontext server run ``` diff --git a/docs/en/docs/get-started/quickstart.md b/docs/en/docs/get-started/quickstart.md index 5fe5a563c9..9aeef8c90f 100644 --- a/docs/en/docs/get-started/quickstart.md +++ b/docs/en/docs/get-started/quickstart.md @@ -39,7 +39,7 @@ The final screen shows the Dashboard URL and the SSH command when selected. If a ## 2. Start the Server -In this terminal, run: +Register a personal service so the Server keeps running after you close the terminal. Linux needs an available `systemd --user` manager. Run: ```bash powercontext config validate --env-file .env @@ -47,7 +47,7 @@ powercontext service install --env-file .env powercontext service status ``` -The native current-user service manages the Server lifecycle. Linux requires an available `systemd --user` manager. Open the Dashboard URL printed by the wizard, using the port saved as `POWERCONTEXT_SERVER_HTTP_PORT` in `.env`. With authentication disabled, the page opens directly. Otherwise, sign in with the **Server token**, not a model API key. An empty Dashboard is expected before you capture data. For development, debugging, temporary use, or systems without a native user-service manager, use `powercontext server run --env-file .env`; stop the personal service first if it is running. See [persistent personal services](../operate/deploy-server.md#run-a-persistent-personal-server). +Open the Dashboard URL printed by the wizard, using the port saved as `POWERCONTEXT_SERVER_HTTP_PORT` in `.env`. With authentication disabled, the page opens directly. Otherwise, sign in with the **Server token**, not a model API key. An empty Dashboard is expected before you capture data. For development, debugging, temporary use, or systems without a native user-service manager, use `powercontext server run --env-file .env`; stop the personal service first if it is running. See [persistent personal services](../operate/deploy-server.md#run-a-persistent-personal-server). Load the client settings and check the running service: @@ -72,7 +72,7 @@ POWERCONTEXT_CODEX_SCOPE_ID=replace-with-returned-scope-id The planned `codex-xxxxxxxx` value is a title, not an ID. For Claude Code, put the ID returned by its creation request in `POWERCONTEXT_CLAUDE_SCOPE_ID`. Agents can have separate Scopes or explicitly share an existing one. Changing directories does not create isolation. Use the same Scope in the Dashboard and Agent during this check. -After editing `.env`, reconcile the service, reload the client settings, and install the matching plugin: +After editing `.env`, apply the updated service settings, reload the client settings, and install the matching plugin: ```bash powercontext service install --env-file .env diff --git a/docs/en/rfcs/1892-script-installation.md b/docs/en/rfcs/1892-script-installation.md index 291e3baaec..8b918dfdcc 100644 --- a/docs/en/rfcs/1892-script-installation.md +++ b/docs/en/rfcs/1892-script-installation.md @@ -1,164 +1,174 @@ --- -title: Script Installation -description: Install the latest or an exact release with Bash and PowerShell, independent package and runtime mirrors, and explicit Agent selection. +title: Script installation +description: Bootstrap Python and uv, install a PowerContext release, and select matching Agent integrations. --- - Proposal Name: `script_installation` - Start Date: 2026-10-09 - RFC PR: [oceanbase/powercontext#1892](https://github.com/oceanbase/powercontext/pull/1892) +- Tracking Issue: [oceanbase/powercontext#1900](https://github.com/oceanbase/powercontext/issues/1900) - Related RFCs: [RFC 1733](1733-usability-and-agent-workflows.md), [RFC 1299](1299_local_server_availability_and_service_installation.md) # Summary -Provide distribution-owned Bash and PowerShell entry points that install PowerContext through uv. The default release is the latest stable version from the selected package index; `--version` selects an exact release. The scripts provision missing uv and Python, install the selected Runtime profile, and optionally invoke the existing Agent integration adapters with a tag matching the installed Runtime. +Provide Bash and PowerShell scripts that install PowerContext through uv. The scripts reuse or install Python and uv, select a Runtime profile, and optionally install Agent integrations at the matching release tag. The default package version is the latest stable release on the selected index; users can request an exact version. -Installation, configuration, diagnostics, and service operation have separate responsibilities. Installing software does not configure inference providers, start a Server, or register a persistent service. Configuration and service operation remain separate existing CLI steps. The installation guide documents the scripts alongside the version-pinned uv commands used in the READMEs and Quick Start. +Configuration and personal-service registration remain separate CLI operations. The scripts finish with instructions for those steps. User instructions belong in the [installation guide](../docs/get-started/install-and-run.md). # Motivation -A normal installation should not require users to coordinate Python discovery, uv installation, package extras, package indexes, and integration Git refs. Package downloads, uv binaries, and Python distributions also use different sources: changing the PyPI index alone cannot fix an inaccessible Python download. +Installing PowerContext requires a compatible Python, uv, the right package extras, and a matching integration ref. Users also need separate source settings for Python packages, uv binaries, and Python distributions. Changing a package index alone cannot fix a failed Python download. -Installation places a versioned component and registers explicitly selected integrations. Configuration controls mutable values such as Server URL, Scope, and capture policy. Diagnostics observe the resulting environment. Keeping these responsibilities distinct lets installation improve independently of configuration storage, shared Hooks, an operations tool, or service lifecycle changes. - -The intended outcome is one installation entry point per shell, reuse of existing dependencies, predictable version selection, explicit host selection, recoverable partial failures, and verification through the installed product. +The installer should handle these prerequisites and release selection while reusing the existing configuration, integration, and service commands. This keeps package installation usable without starting a Server or selecting an Agent host. # Guide-level explanation -## Install a Runtime profile +## Runtime profiles and prerequisites -On macOS or Linux: +The default `local` profile installs the CLI, Client, and local Server dependencies. `--profile client` installs the CLI and Client dependencies for a remote Server. Profile selection and Agent selection are independent. -```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -``` +The installer reuses existing uv and compatible system or uv-managed Python. If either is missing, it installs the prerequisite in a user directory. It runs unattended by default, requires no administrator privileges, and prints the PATH command needed in a new terminal. + +## Releases and Agent integrations -On Windows, using PowerShell 5.1 or newer: +`--version latest` selects the newest compatible stable release on the configured index. An exact version gives a repeatable package selection. For example: -```powershell -powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.2.0 --host codex ``` -`local`, the default profile, installs CLI, Client, and local Server dependencies. `--profile client` installs CLI and Client dependencies for an existing Server. Profile and host selection are independent: choosing a database role does not select an Agent host, and selecting Codex does not change the Runtime profile. +Only explicit `--host` arguments run integration setup. Each selected host requires Git and its own prerequisites. Multiple hosts can be selected. `--no-hosts` explicitly skips integrations and conflicts with `--host`. -The installer reuses uv and compatible system or uv-managed Python. Otherwise it provisions uv and Python 3.12 in user-owned locations. It prints the actual Runtime version, profile, executable directories, PATH command, and next configuration command. It does not change persistent PATH settings. Apply the printed PATH command before continuing. +The installer reads the installed CLI version and passes its `powercontext-vVERSION` tag to `powercontext setup`. All hosts use that same release ref. `--env-file PATH` passes an existing connection file to host setup; the installer does not parse or source it. -## Select releases and integrations +## Configuration and personal services -`--version latest` is the default. Repeating it checks for newer stable releases on the configured index. For a repeatable package selection, use an exact version: +After installation, users configure a local Server and register a personal service through the existing CLI. The native user-service manager runs the Server after the terminal closes. Client-only installations use the remote connection guide. The [installation guide](../docs/get-started/install-and-run.md#configure-and-install-a-personal-service) documents configuration, service registration, and connection checks. -```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.2.0 --host codex -``` +The installer leaves configuration, Server startup, and service lifecycle to their existing commands. Personal-service constraints, including loopback binding and supported managers, follow [RFC 1299](1299_local_server_availability_and_service_installation.md). Windows support remains experimental. -Installation runs unattended by default, without Git or a host picker. `--host` is repeatable and opts into integration setup; selected hosts require Git and their own prerequisites. `--no-hosts` explicitly states the default and cannot be combined with `--host`. For an interactive picker after installation, run `powercontext setup --ref "powercontext-v$(powercontext --version)"`. +## Download sources -The installed CLI supplies the exact version for `powercontext-vVERSION`. Host setup never resolves `latest` again and never defaults to `master`. If a host installation fails, the Runtime remains installed and the script returns nonzero with instructions to retry setup at that tag. Each adapter supplies its existing host-specific result. +Package indexes, uv binaries, and Python distributions have separate source settings. Users can select a region or specify sources independently. Explicit settings take precedence over automatic mirrors; an automatic mirror can fall back to an official source as described below. -For personal macOS/Linux, configure a protected file, then run `powercontext service install --env-file .env`, `powercontext service status`, then load the generated Client settings for `powercontext ready` and `powercontext capabilities`. The native user manager owns the Server lifecycle. Development, debugging, temporary use, and unavailable managers keep the foreground `server run` path. Client-only installations follow the remote connection guide. Service registration remains explicit. +# Reference-level explanation -## Configuration and personal services +## Responsibilities and execution order -Configuration and service registration use the existing CLI in separate steps. Run `config init --output .env`, save the configuration, then run `config validate --env-file .env`, `service install --env-file .env`, and `service status`. Load the generated Client settings before running `ready` and `capabilities`. See the [installation guide](../docs/get-started/install-and-run.md#configure-and-install-a-personal-service) for the full sequence. +The entry points are `website/public/install.sh` and `website/public/install.ps1`, served as static website assets. Both implement the same public options and failure behavior. -The installer does not run the configuration wizard or manage a service. Cancelling the wizard cannot cause the installer to continue with service registration. Service ownership, supported managers, protected files, login startup, and recovery remain the existing service layer's responsibility. Windows personal services remain experimental. +| Responsibility | Owner | +| --- | --- | +| Validate installer arguments, prepare prerequisites, and sequence installation | Shell script | +| Resolve packages, select compatible artifacts, and maintain the tool environment | uv | +| Install selected Agent integrations | Existing `powercontext setup` adapters | +| Configure and run a Server, diagnose it, and manage a personal service | Existing CLI and service layer | -`--env-file PATH` selects an existing connection file only for explicitly selected host setup. It is passed to `setup --env-file PATH HOST...` at the installed release tag; the shell does not parse or source it. After editing a service configuration, including Scope IDs, follow the service guide using the original file. +The scripts perform these steps in order: -## Choose download sources +1. Validate arguments, including conflicting host options and the existence of a supplied connection file. +2. Reuse or install uv and Python, then select the package index. +3. Install the selected package requirement in a uv tool environment. +4. Locate the installed launcher through `uv tool dir --bin` and verify its version and command availability. +5. Print the installed version and PATH instructions, then run any selected host setup at that version's tag. +6. Print local-service or remote-connection guidance. Return a nonzero status if host setup failed. -`--region auto|cn|global` overrides `POWERCONTEXT_INSTALL_REGION`. Automatic selection checks local named timezone, then locale territory, then uses global sources. It does not call a network location service. +## Prerequisite reuse -| Component | Global source | Automatic China source | Explicit control | -| --- | --- | --- | --- | -| PowerContext and dependencies | PyPI | Tsinghua PyPI mirror | `--index-url`, uv index settings and configuration | -| uv installer and binaries | Astral channels | USTC release mirror | `POWERCONTEXT_UV_INSTALLER_URL`, `UV_DOWNLOAD_URL`, uv installer mirror variables | -| Python | uv default channels | NJU python-build-standalone mirror | `UV_PYTHON_INSTALL_MIRROR`, uv Python download configuration | +A missing uv is installed at a version maintained by the installer, independently of the PowerContext version. An existing uv is reused without an upgrade. -Existing uv configuration suppresses automatic package mirror selection. An explicit `--index-url` changes only the default index; additional indexes keep uv's priority rules. uv owns authentication and index resolution. Script URL arguments must use HTTPS and omit credentials; credentials belong in uv configuration. pip index environment variables are not uv configuration. Python and uv download overrides are independent of package indexes. +Python discovery accepts Python 3.11 or newer below 4.0 and excludes virtual environments. If no compatible interpreter is found, uv installs Python 3.12. uv selects the platform, architecture, and artifact. `UV_INSTALL_DIR`, `UV_TOOL_DIR`, and `UV_TOOL_BIN_DIR` retain their location controls. -# Reference-level explanation +## Version and profile semantics -## Installation responsibilities +| Selection | Package requirement and behavior | +| --- | --- | +| `--profile local` (default) | `powercontext[cli,server]` | +| `--profile client` | `powercontext[cli]`, including Client dependencies | +| `--version latest` (default) | Unpinned profile requirement with `uv tool install --upgrade --force --prerelease disallow` | +| Exact version | Profile requirement with `==VERSION` | -The entry points are `website/public/install.sh` and `website/public/install.ps1`, served as static website assets. Their supported options and failure behavior are equivalent. They own bootstrap and sequencing; uv owns environment creation and package resolution, while existing `powercontext setup` adapters own integration installation. +`latest` means the newest stable version compatible with the selected interpreter and configured index. `--force` makes older uv versions resolve a fresh tool environment, so an installed prerelease cannot bypass stable selection. Resolution failure is reported with the requested requirement unchanged. -The effective installation inputs are: +Exact versions use `X.Y.Z`, optionally followed by `aN`, `bN`, or `rcN`. Releases before 0.1.0, version ranges, source refs, and URLs are outside this option's contract. Changing the profile replaces the tool's dependency set; users who manage additional extras use the manual installation path. -```text -Package requirement + Runtime profile + explicit Host selection = installation operation -``` +The installed launcher must report a release version, and an exact request must match it. All profiles must pass CLI help and `capabilities --help`; `local` must also pass `config init --help` and `server run --help`. These checks verify imports and command availability. Server readiness is checked after the user configures and starts the Server. -The package manager resolves the requirement before replacing the tool environment. Once installed, the CLI's exact version is verified and used for all host setup. This keeps one release coordinate for the operation without adding a second package resolver or introducing a release manifest dependency. +## Source selection and fallback -The scripts preserve `powercontext setup`, `config`, `doctor`, and service commands. They do not move domain behavior into shell, add a standalone Python installer engine, or change Runtime/public HTTP APIs. +Region selection follows `--region`, `POWERCONTEXT_INSTALL_REGION`, local named timezone, locale territory, then global sources. Detection uses local settings without a network location service. -## Version and profile semantics +| Component | Global source | Automatic China source | Explicit control | +| --- | --- | --- | --- | +| PowerContext and dependencies | PyPI | Tsinghua PyPI mirror | `--index-url`, uv index settings and configuration | +| uv installer and binaries | Astral channels | USTC release mirror | `POWERCONTEXT_UV_INSTALLER_URL`, `UV_DOWNLOAD_URL`, uv installer mirror variables | +| Python | uv default channels | NJU python-build-standalone mirror | `UV_PYTHON_INSTALL_MIRROR`, uv Python download configuration | -- `latest` passes an unpinned profile requirement to `uv tool install --upgrade --force --prerelease disallow`. It means the newest stable version compatible with the selected interpreter and configured sources, not necessarily the version most recently uploaded to another mirror. A fresh tool environment prevents an installed prerelease from bypassing stable selection in older uv versions. Resolution failures are reported without substituting another requirement. -- An exact `X.Y.Z`, optionally followed by `aN`, `bN`, or `rcN`, uses `==VERSION`. Explicit prereleases are supported; source refs, ranges, and URLs are not `--version` values. Releases before 0.1.0 are excluded. -- `local` uses `powercontext[cli,server]`; `client` uses `powercontext[cli]`, whose dependencies include the Client. Rerunning with a different profile replaces that tool's dependency set. Independently added extras must be managed through the documented manual installation path. -- The executable in `uv tool dir --bin` must report a release version. Exact requests must match it. All profiles must expose CLI help and `capabilities --help`; `local` additionally requires `config init --help` and `server run --help`. The script prints success only after these commands succeed. These checks establish imports and command availability, not Server readiness or Agent workflow correctness. +Existing uv configuration suppresses automatic package mirrors. `--index-url` replaces the default index; additional indexes retain uv's priority rules. uv handles authentication and the package index protocol. Script URL arguments require HTTPS without embedded credentials. pip index variables do not configure uv. -uv is bootstrapped at an installer-controlled version; that version is independent of the PowerContext release. An existing uv is reused rather than upgraded silently. Python discovery excludes virtual environments so a project venv cannot become an accidental installation prerequisite. If no compatible Python 3.11+ is found, uv provisions Python 3.12. uv remains responsible for platform, architecture, and wheel compatibility errors. +Explicit uv and Python sources are preserved without fallback. For automatic selection, an unavailable uv mirror installer falls back to Astral; mirrored uv artifacts can fall back to official sources. The Python build URL comes from uv. The script checks its mirrored artifact and retries with uv's default channels if the automatic mirror fails. -## Mirror precedence and recovery +An automatic package mirror receives a bounded availability request before tool installation. An unreachable mirror falls back to PyPI. A reachable mirror stays selected even if the requested version is missing. uv reports resolution and artifact failures; the script does not switch indexes after a tool installation attempt. This avoids retrying a package replacement against a different source after files may have changed. uv owns index parsing and compatibility checks. -User-specified uv/Python sources are preserved without fallback. For automatic China selection, an unavailable uv mirror installer falls back to Astral; failed mirrored uv artifacts may use official sources. Python's exact build URL is obtained from uv, and its mirrored artifact is checked before installation. If the automatic Python mirror fails, installation retries with uv's default channels. +## Failure and recovery -Automatic China package selection makes a bounded availability request to the PowerContext index page. An unreachable mirror falls back to PyPI before tool installation. A reachable mirror remains selected even if it lacks the requested release; uv reports that resolution failure. Global and explicit sources go directly to uv. The scripts never interpret index HTML, wheel filenames, encoded URLs, package compatibility, or authentication. They do not switch indexes after a tool installation attempt, which may already have changed installed files. +Each completed component remains available when a later step fails: -Shell environment changes are local to the installer process; PowerShell restores temporary source variables in `finally`. Existing configuration files are neither parsed by shell nor rewritten. Only installer-owned temporary downloads are cleaned. `UV_INSTALL_DIR`, `UV_TOOL_DIR`, and `UV_TOOL_BIN_DIR` retain their location controls. +| Failure | Result and recovery | +| --- | --- | +| A prerequisite cannot be installed | Stop and report the failed download; retain prerequisites already installed | +| Package resolution fails, including an unavailable exact version | Preserve the installed tool and report the uv error | +| The installed launcher fails verification | Report that the package was installed but verification failed; allow an explicit version retry | +| Host setup fails | Preserve the verified Runtime, return nonzero, and identify the matching release ref for a setup retry | -## Persistence and compatibility +Verification can fail after replacing the previous executable. The installer does not roll package files back or undo completed host operations. It cleans only its temporary downloads. Source and PATH changes remain local to the installation process; PowerShell restores temporary source variables in `finally`. -Ordinary package installation does not read or rewrite `.env`, credentials, data directories, or database schemas, and does not stop or restart a service. Users configure and register services separately through the existing CLI, and follow the existing upgrade and migration instructions when starting an updated Server. An exact-version retry may reuse cached packages; `latest` intentionally allows upgrades. +## Persistence and compatibility -`UV_OFFLINE=1` permits cached reinstallation only when uv, a compatible Python, and all dependencies are present. It disables installer source probes as well as uv networking. It does not promise a complete offline distribution. Prerequisites remain available after later failures. A resolution failure for an unavailable exact version preserves the existing tool. Once uv accepts a package, verification can fail after replacing the old executable: report "package installed, verification failed" and allow an explicit version retry, without claiming rollback. A later host setup failure preserves the verified Runtime. There is no cross-component rollback or repair of unrelated host state. +Package installation leaves `.env`, credentials, data directories, and database schemas unchanged. It does not stop or restart a service. Users follow the existing migration and service instructions when starting an updated Server. Runtime APIs and the existing configuration, diagnostic, and service commands retain their contracts. -Windows retains its experimental product status. Native acceptance runs on Linux, macOS, and Windows; host-specific support still comes from each integration's capability contract. +`UV_OFFLINE=1` disables installer probes and uv networking. Cached reinstallation requires an existing uv, compatible Python, and all required packages. Missing prerequisites or artifacts fail explicitly. ## Executable contract and acceptance -`tests/fixtures/installation/*.json` defines shared public inputs and expected outcomes for both shell adapters. `tests/test_installation_contract.py` executes those cases with real uv and small offline fixture wheels: host opt-in, profiles, exact-version verification, unavailable commands, and partial host failure. Cases observe installed capabilities, selected hosts, release refs and exit results. They do not fix internal function boundaries or call order. The same catalog runs on each native operating system; fixture host execution is not native service or Agent acceptance. Host cases cover passing an existing connection file. Configuration and personal-service lifecycle acceptance remain separate from installer conformance. +Both shell adapters must pass the shared behavior cases in `tests/fixtures/installation/*.json`. These cases check public arguments, installed capabilities, matching host refs, and exit results through real uv and small fixture wheels. -`tests/native/test_installation.py` runs the actual shell installer, uv, installed PowerContext CLI, and HTTP Server. A wheel built from the tested commit uses release-shaped metadata and a direct local file constraint. That constraint selects the tested artifact; it does not establish an independently enforced checksum. Qualification includes: +Acceptance covers: -- missing uv/Python under global and China source selection, existing tools/configuration, and spaces/Unicode paths; -- real stable/prerelease wheel resolution through a controlled package index, default upgrade, exact selection, and preserving the installed version when a requested release is unavailable; -- Client-only installation and profile changes, with no local Server state created by installation; -- `.env` generation and validation, readiness, Memory remember/search, cached offline reinstall, and restart readback; -- explicit index failure, conflicting host choices, unattended and piped Bash installation, and the downloaded PowerShell scriptblock entry point. +- Bootstrap with missing uv or Python, reuse of existing tools and configuration, and paths containing spaces or Unicode. +- Latest stable and exact releases, explicit prereleases, and preservation of an installed tool when resolution fails. +- Client/local profile replacement, host opt-in and partial failure, and passing an existing connection file. +- Unattended and piped Bash execution, plus PowerShell file and downloaded-scriptblock entry points. +- Installation of the actual wheel, configuration validation, HTTP readiness, Memory persistence, cached offline reinstall, and restart readback. -A separate CI matrix executes the suite on all three operating systems. Release reference checks preserve script `latest` defaults while updating explicit version examples. Website validation confirms documentation links and the static build. Syntax checks alone do not establish Windows or macOS installation support, and these tests do not claim that a real Agent host completes its capture/recall workflow. +The native CI matrix runs installation acceptance on Linux, macOS, and Windows. Controlled-index source tests verify mirror selection and failure handling on Linux. Platform qualification requires native execution; syntax checks and fixture hosts do not establish service-manager or Agent workflow support. Release-reference checks keep explicit version examples current while preserving the `latest` default. # Drawbacks -Bash and PowerShell duplicate some sequencing and source-selection policy. Automatic mirrors add operational availability dependencies. `latest` deliberately changes over time and is unsuitable for reproducible deployment without an exact version. Matching tags coordinate releases but do not independently prove host compatibility. +Bash and PowerShell require separate implementations of prerequisite and source selection. Automatic mirrors add availability dependencies. `latest` changes as the selected index publishes releases, so repeatable deployments need an exact version. A matching integration tag coordinates versions; each host still needs its own compatibility checks. # Rationale and alternatives -A uv-only command is retained for users who already manage Python and uv; it cannot bootstrap a clean machine. Source installation is useful for development but is not the default release channel. Pinning the public script to a PowerContext version would leave new users on stale releases until the website is republished. +Direct uv commands remain suitable for users who already manage Python and uv. The scripts cover machines without those prerequisites. Source installation remains available for development. The script defaults to `latest` so its package version does not become stale between website releases. -A Python engine still needs a bootstrap on machines without Python. Generating the shell adapters would still require native behavioral tests and would add generator/versioning machinery. Shared conformance cases provide executable parity without another runtime asset. Introduce an engine or generator only when a concrete policy needs it. +A Python installer engine would still need a shell bootstrap. Generated shell adapters would add a generator while retaining the need for native tests. Two small adapters with shared behavior cases keep the initial implementation simpler. -Immutable manifests and a second installation ledger are useful when distributing independently versioned raw artifacts. Here uv owns wheel resolution, tool environments and its records. The installer observes the actual launcher and delegates repair to an explicit uv-backed retry. This boundary avoids two authorities for the same installation. +uv already maintains the package environment and installation records. A second installation ledger would duplicate that state. Separate manifests and records would be needed if PowerContext later distributed independently versioned raw artifacts. # Prior art -[RFC 1408](https://github.com/oceanbase/powercontext/pull/1408) defines the separation of installation, configuration, and diagnostics; independent Runtime profiles and host selection; component-level recovery; and explicit service registration. [RFC 1299](1299_local_server_availability_and_service_installation.md) defines personal service lifecycle. +[RFC 1408](https://github.com/oceanbase/powercontext/pull/1408) separates installation, configuration, and diagnostics, and defines independent profiles, host selection, and component recovery. [RFC 1299](1299_local_server_availability_and_service_installation.md) defines the personal-service lifecycle used here. -[Magpie's installer](https://github.com/yetone/magpie/blob/023f5aaad2ecd41ae04390166b9cac9a0b300d81/site/public/install.sh) separates an authoritative release feed from artifact mirrors and verifies a binary checksum before replacement. Its raw binary ownership justifies that verification; its manifest is not a replacement for Python package standards. +[Magpie's installer](https://github.com/yetone/magpie/blob/023f5aaad2ecd41ae04390166b9cac9a0b300d81/site/public/install.sh) separates release sources from artifact mirrors and checks binary checksums before replacement. Its manifest serves direct binary distribution; this design delegates package distribution to uv. -[Lody's daemon installation contract](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/specs/daemon-upgrade-installation.md) resolves and executes the actual npm installation destination and checks replacement readiness. Its separate [managed Agent runtime](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/apps/cli/src/agent/managed-agent-runtime.ts) owns raw archives and therefore owns manifests and completion records. Apply the actual-executable check here; retain service readiness in the service boundary. The public tag-release workflow does not publish installers. +[Lody's daemon installer](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/specs/daemon-upgrade-installation.md) resolves the actual npm installation destination and verifies the installed executable. That check also applies here. Its [managed Agent runtime](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/apps/cli/src/agent/managed-agent-runtime.ts) separately manages raw archives and their records. -[Bub](https://github.com/bubbuild/bub/tree/b4a61bf1326729a024161d22ba20019b8500f907/website/public) bootstraps uv before running its Python preset resolver. Its terminal and macOS Bash fixes motivate explicit host setup and native tests. [uv tools](https://docs.astral.sh/uv/concepts/tools/) and [configuration](https://docs.astral.sh/uv/concepts/configuration-files/) define the reused environment and source semantics; the [Python Simple API](https://packaging.python.org/en/latest/specifications/simple-repository-api/) defines package index interpretation. A shell regex cannot implement those contracts. +[Bub](https://github.com/bubbuild/bub/tree/b4a61bf1326729a024161d22ba20019b8500f907/website/public) bootstraps uv before a Python preset resolver. This proposal also uses a shell bootstrap and tests both shell adapters on their native platforms. [uv tools](https://docs.astral.sh/uv/concepts/tools/), [uv configuration](https://docs.astral.sh/uv/concepts/configuration-files/), and the [Python Simple API](https://packaging.python.org/en/latest/specifications/simple-repository-api/) define the environment and package-source behavior delegated to uv. # Unresolved questions -No further cross-subsystem decision is required for this contract. Complete offline bundles, cryptographic installer manifests, independently packaged host artifacts, and changes to configuration storage need separate designs. The platform matrix must remain green before promotion; Windows product support remains experimental. +Complete offline bundles, cryptographic installation manifests, independently packaged host artifacts, and changes to configuration storage need separate designs. This RFC retains Windows' experimental status. # Future possibilities -The scripts can bootstrap a distribution-owned installer engine if installation plans later need immutable component manifests, independent integration versions, or durable per-component repair records. An offline bundle can include all required artifacts explicitly. Neither extension should make Server startup or service registration implicit. - -The personal-service onboarding work tracks [Issue #1900](https://github.com/oceanbase/powercontext/issues/1900). +An installer engine may become useful for independent component versions, immutable manifests, or persistent repair records. An offline bundle could carry the required artifacts. Such extensions would retain explicit Server startup and service registration. diff --git a/docs/zh/docs/get-started/install-and-run.md b/docs/zh/docs/get-started/install-and-run.md index b80e903d25..f4ae7297ee 100644 --- a/docs/zh/docs/get-started/install-and-run.md +++ b/docs/zh/docs/get-started/install-and-run.md @@ -5,9 +5,7 @@ description: 安装 PowerContext 1.4.1,并运行本地 Server。 # 安装和运行 -跨机器连接 Agent 时,请阅读[连接远程 Server](../operate/connect-remote-server.md),了解地址引导确认、非交互安装及绑定地址的明文 HTTP 同意设置。 - -首次使用请从 [Quick Start](quickstart.md)开始。本页说明版本选择、平台要求、安装角色、启动、诊断和更新。 +本页说明如何安装 PowerContext 并运行个人 Server。首次连接 Agent 时,按[快速开始](quickstart.md)操作;连接已有 Server 时,使用[远程连接指南](../operate/connect-remote-server.md)。 ## 平台支持 @@ -16,13 +14,13 @@ description: 安装 PowerContext 1.4.1,并运行本地 Server。 | macOS、Linux | 支持 | | Windows | `experimental` | -Windows 的 CLI、Server 和个人服务支持为试验性;各 Agent Host 仍需满足自身的平台要求。使用 Bash 语法的示例需要 Bash 环境,不可直接粘贴到 PowerShell。嵌入式 seekDB 不支持 Windows。 +Windows 的 CLI、Server 和个人服务支持为试验性;各 Agent 宿主仍需满足自身要求。嵌入式 seekDB 不支持 Windows。下文 Bash 示例需要在 Bash 中执行。 ## 使用推荐安装脚本 -脚本在独立的 uv tool 环境中安装 CLI 和本地 Server。已有兼容的 uv/Python 时会直接复用;缺少时在用户目录安装 uv 和 Python 3.12,无需管理员权限。 +脚本在独立的 uv tool 环境中安装 CLI 和本地 Server,复用兼容的 Python 和 uv,缺少时在用户目录补齐,无需管理员权限。 -macOS 或 Linux 使用 Bash,需要 `curl` 或 `wget`;Windows 需要 PowerShell 5.1 或更新版本。 +macOS 或 Linux 需要 Bash 和 `curl` 或 `wget`;Windows 需要 PowerShell 5.1 或更新版本。 ```bash tab="macOS / Linux" curl -fsSL https://powercontext.oceanbase.io/install.sh | bash @@ -32,21 +30,23 @@ curl -fsSL https://powercontext.oceanbase.io/install.sh | bash powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" ``` -先执行安装完成时打印的 PATH 命令,再在终端运行 `powercontext`。安装器不会修改 shell 启动文件或 Windows 的持久 PATH。`UV_INSTALL_DIR` 指定缺少 uv 时的安装目录,`UV_TOOL_BIN_DIR` 指定 PowerContext 可执行文件目录。 +使用 `wget` 时,执行 `wget -qO- https://powercontext.oceanbase.io/install.sh | bash`。 -默认安装可以无人值守执行。使用 `--host codex` 安装选定的 Agent 集成;多个宿主可重复传入 `--host`。只有集成安装需要 Git 及对应宿主的前置条件。`--no-hosts` 可以显式表达默认行为,不能与 `--host` 同时使用。宿主支持范围见[能力矩阵](../integrations/capabilities.md)。 +先执行安装完成时打印的 PATH 命令,再在终端使用 `powercontext`。脚本会提示如何让终端找到已安装命令;shell 启动文件和 Windows 的持久 PATH 保持不变。 -连接已有远程 Server 时,加上 `--profile client`,只安装 CLI 和 Client 依赖。默认 `--profile local` 包含本地 Server。两种模式都不会启动 Server、注册服务或覆盖配置与数据。本地安装使用 `powercontext config init`,继续阅读 [快速开始](quickstart.md);Client-only 安装按[远程连接指南](../operate/connect-remote-server.md)设置地址和认证。 +默认安装最新稳定版及本地 Server 依赖。完成后继续配置个人服务。连接远程 Server 时,使用 `--profile client`,再按[远程连接指南](../operate/connect-remote-server.md)设置。 ## 配置并安装个人服务 -个人 macOS/Linux 安装完成后,先配置本地 Server: +在 macOS 或 Linux 上配置本地 Server: ```bash powercontext config init --output .env ``` -保存 `.env` 后再继续;如果取消向导,到这里停止。随后验证已保存的文件,并显式注册原生当前用户服务: +在向导中选择本机使用和 loopback 绑定,保存 `.env` 后再继续;如果取消,到这里停止。Linux 需要可用的 `systemd --user` 管理器。Windows 的服务安装及登录启动按[部署 Server](../operate/deploy-server.md)操作。 + +验证已保存的文件并注册服务: ```bash powercontext config validate --env-file .env @@ -54,9 +54,7 @@ powercontext service install --env-file .env powercontext service status ``` -个人服务选择 loopback 绑定;Linux 需要可用的 `systemd --user` 管理器。Windows 个人服务仍为试验性支持,文件保护和登录自启动说明见[部署 Server](../operate/deploy-server.md)。 - -加载生成的 Client 配置,检查运行中的 Server: +服务会在关闭终端后继续运行 Server。加载客户端配置,检查连接: ```bash set -a @@ -66,62 +64,85 @@ powercontext ready powercontext capabilities ``` -修改文件(包括写入返回的 Scope ID)后,使用原文件重新执行 `service install --env-file`。如果把配置保存在其他位置,始终使用同一路径。服务运行不代表 Agent 工作流已完成;继续按[快速开始](quickstart.md)完成 Scope 创建和 Agent 验收。 +修改 `.env`(包括写入 Scope ID)后,重新执行 `powercontext service install --env-file .env`,并重新加载客户端配置。文件保存在其他位置时,使用原路径。[快速开始](quickstart.md)说明 Scope 创建及 Agent 会话中的记忆验证步骤。 -安装器只安装软件及显式选择的集成。`--env-file PATH` 可以通过现有 `setup` 命令,为选定宿主提供已有连接文件;配置和服务注册仍由用户分步执行现有 CLI 命令。 - -## 选择脚本安装版本 +开发、排障或缺少原生用户服务管理器时,按[前台运行说明](#运行本地-server)启动 Server。 -默认 `--version latest` 安装或升级到所选包源中与当前 Python 兼容的最新稳定版,排除预发布版本。`--version` 也支持指定准确版本,包括 `1.3.0rc1` 这样的显式预发布版本;源中没有指定版本时会报错,不会换成其他版本。 +## 安装 Agent 集成 -例如,安装 PowerContext 1.4.1 及其 Codex 集成: +先安装 Agent 自身的 CLI,再安装它的 PowerContext 集成。以 Codex 为例: ```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.4.1 --host codex +powercontext setup codex --ref "powercontext-v$(powercontext --version)" ``` -Python 包版本为 `1.4.1`,对应 Git tag 为 `powercontext-v1.4.1`。使用 `latest` 时,安装器读取实际安装的 CLI 版本,并使用对应的 tag 安装集成,不会使用会移动的 `master` 分支。稍后添加集成时可以执行: +该命令使用与已安装 PowerContext 版本匹配的 tag。需要交互选择宿主时,运行 `powercontext setup --ref "powercontext-v$(powercontext --version)"`。然后按所选[集成指南](../integrations/index.md)检查连接并启用 Hook。 -```bash -powercontext setup codex --ref "powercontext-v$(powercontext --version)" +如需在安装包时同时安装集成,向脚本传入 `--host codex`;多个宿主可重复传入 `--host`,这时需要 Git 及各宿主的前置依赖。已有连接文件时,为所选宿主传入 `--env-file PATH`,脚本会把文件交给 `setup`。没有 `--host` 时跳过集成;`--no-hosts` 显式表达这一行为,不能与 `--host` 同时使用。支持范围见[能力矩阵](../integrations/capabilities.md)。 + +## 选择脚本安装版本 + +默认的 `--version latest` 安装或升级到所选索引中兼容的最新稳定版。安装 PowerContext 1.4.1 时,选择对应平台的命令: + +```bash tab="macOS / Linux" +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.4.1 ``` -需要交互选择宿主时,运行 `powercontext setup --ref "powercontext-v$(powercontext --version)"`。 +```powershell tab="Windows" +irm https://powercontext.oceanbase.io/install.ps1 -OutFile install.ps1 +powershell -ExecutionPolicy Bypass -File .\install.ps1 --version 1.4.1 +``` -PowerShell 也支持这里的双引号表达式。集成安装失败时,已安装的 Runtime 会保留,脚本返回错误并给出重试提示。安装成功不代表 Server 就绪或宿主工作流可用;仍需执行下文检查及集成文档中的验证。 +也可以指定预发布版,例如 `1.3.0rc1`。所选索引缺少该版本时,安装失败并保留原工具;`latest` 则排除预发布版。镜像同步可能滞后,其最新版本可能与 PyPI 不同。 -安装器在报告成功前验证版本和所选模式的命令。包安装成功后仍可能无法通过命令验证,此时旧可执行文件可能已被替换。请查看错误中指出的命令,再指定已知可用的 `--version` 重试。安装器不回滚包文件。 +## 下载失败时使用镜像 -## 使用镜像重试依赖下载 +使用 `--region cn` 选择中国区域镜像: -包索引、uv 二进制和 Python 发行版是三类独立下载。更换包索引不会改变 uv 或 Python 的下载位置。 +```bash tab="macOS / Linux" +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --region cn +``` + +```powershell tab="Windows" +irm https://powercontext.oceanbase.io/install.ps1 -OutFile install.ps1 +powershell -ExecutionPolicy Bypass -File .\install.ps1 --region cn +``` -| 下载内容 | 显式配置 | 中国区域自动源 | 全球默认源 | +`--region auto|cn|global` 优先于 `POWERCONTEXT_INSTALL_REGION`。未显式选择区域时,脚本依次参考命名时区、locale 地区,最后采用全球源。使用 `--region global` 选择全球默认源。 + +根据下载失败的组件设置对应来源。更换包索引不会改变 Python 或 uv 的下载位置: + +| 下载内容 | 显式设置 | 中国区域自动源 | 全球源 | | --- | --- | --- | --- | -| PowerContext 和 Python 包 | `--index-url URL`、uv 索引环境变量或 `uv.toml` | 清华 PyPI 镜像 | PyPI | +| PowerContext 和 Python 包 | `--index-url URL`、uv 索引变量或 `uv.toml` | 清华 PyPI 镜像 | PyPI | | uv 安装器 | `POWERCONTEXT_UV_INSTALLER_URL` | USTC uv release 镜像 | Astral 安装器 | | uv 二进制 | `UV_DOWNLOAD_URL` 或 `UV_INSTALLER_GITHUB_BASE_URL` | USTC uv release 镜像 | Astral release 渠道 | | Python 发行版 | `UV_PYTHON_INSTALL_MIRROR` | NJU python-build-standalone 镜像 | uv 默认渠道 | -```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --region cn -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --index-url https://pypi.org/simple -``` +已有 uv 设置优先于自动镜像。`--index-url` 更改默认包索引,额外 uv 索引保持原有优先级。私有索引凭据使用 uv 的认证配置。uv 不读取 `PIP_INDEX_URL` 或 `PIP_EXTRA_INDEX_URL`。已有 Python 镜像设置和 `uv.toml` 也会阻止自动选择 Python 镜像。支持 `UV_ASTRAL_MIRROR_URL` 的 uv 版本会接收该设置。 -`--region auto|cn|global` 优先于 `POWERCONTEXT_INSTALL_REGION`。自动选择依次参考本地命名时区、locale 地区,最后采用全球源,不请求网络定位服务。自动镜像不可用时可回退官方源;中国区域包镜像可达时,即使缺少请求版本,也保持选中。解析或文件下载失败由 uv 报告,不会再换源重试安装。需要换源时使用 `--region global` 或显式指定来源。显式配置的源不会自动回退。镜像可能有同步延迟:`latest` 指所选源中的最新兼容稳定版。 +自动镜像不可用时,可以回退官方源;可达包镜像缺少请求版本时,会返回安装错误。使用 `--region global` 或显式索引切换来源后重试。显式来源失败时仍保持选中。 -已有的 uv 索引环境变量和配置文件优先于自动包镜像。`--index-url` 只覆盖默认索引,额外 uv 索引仍保留其优先级。私有源凭据应使用 uv 认证配置,不放在脚本参数里。uv 不读取 `PIP_INDEX_URL` 或 `PIP_EXTRA_INDEX_URL`。已有 Python 镜像设置和 `uv.toml` 同样会禁用自动 Python 镜像选择。`UV_ASTRAL_MIRROR_URL` 会透传给支持它的 uv 版本。这些设置只作用于本次安装,不改写持久包管理配置;已有 uv 会直接复用,不会自动升级。 +## 安装失败后恢复 -需要检查脚本内容,或在 PowerShell 传入多个选项时,可以先保存脚本: +根据安装器报告的错误选择下一步: -```powershell -irm https://powercontext.oceanbase.io/install.ps1 -OutFile install.ps1 -powershell -ExecutionPolicy Bypass -File .\install.ps1 --region cn --version 1.4.1 -``` +| 失败情况 | 处理方式 | +| --- | --- | +| Python 或 uv 下载失败 | 检查上表中的下载设置,重新运行安装器 | +| 包解析或下载失败 | 检查所选索引和请求版本后重试 | +| 包已安装,但命令验证失败 | 检查报错命令,使用已知可用的 `--version` 重试;旧可执行文件可能已经被替换 | +| Agent 集成安装失败 | 保留已安装的 PowerContext,使用错误提示中的发行 tag 重试 `powercontext setup` | -已有 uv、兼容 Python 和完整依赖缓存时,可以使用 `UV_OFFLINE=1` 重装。这不等于离线发行包;缺少下载内容时会明确报错。 +只有在已有 uv、兼容 Python 和全部所需包时,才设置 `UV_OFFLINE=1` 进行缓存重装;离线模式下缺少下载内容会失败。 -## 选择版本 +需要更改安装位置时,使用 `UV_INSTALL_DIR` 指定缺少 uv 时的安装目录,使用 `UV_TOOL_BIN_DIR` 指定 PowerContext 命令目录。复用的 uv 版本及持久包管理器设置保持不变。 + +## 使用 uv 手动安装 + +已有 Python 和 uv 时,可以直接安装固定发行版。先选择版本,再安装所需的包 extras。 + +### 选择版本 本页使用 PowerContext 1.4.1。包与 Agent 集成保持版本一致:Python 包版本为 `1.4.1`,对应 Git tag 为 `powercontext-v1.4.1`。 @@ -132,7 +153,7 @@ powercontext setup codex --ref powercontext-v1.4.1 宿主支持范围和维护状态见[能力矩阵](../integrations/capabilities.md)。标为 `experimental` 的能力在此版本中仍属于试验性能力。 -## 安装应用 +### 安装应用 需要在 macOS、Linux 或 Windows 上准备 Python 3.11 或更新版本、Git 和 [`uv`](https://docs.astral.sh/uv/),然后从 PyPI 安装 PowerContext: @@ -154,6 +175,8 @@ Git 安装命令不会留下需要自行管理的仓库工作副本。Git 会沿 ## 运行本地 Server +开发、排障或临时使用时,可以采用前台运行方式。在同一地址启动另一个 Server 前,先停止已有个人服务。 + ```bash powercontext server run ``` diff --git a/docs/zh/docs/get-started/quickstart.md b/docs/zh/docs/get-started/quickstart.md index e27e5eaec4..6d78365a3f 100644 --- a/docs/zh/docs/get-started/quickstart.md +++ b/docs/zh/docs/get-started/quickstart.md @@ -39,7 +39,7 @@ powercontext config init --language zh --output .env ## 2. 启动 Server -在当前终端执行: +注册个人服务,让 Server 在关闭终端后继续运行。Linux 需要可用的 `systemd --user` 管理器。执行: ```bash powercontext config validate --env-file .env @@ -47,7 +47,7 @@ powercontext service install --env-file .env powercontext service status ``` -原生当前用户服务负责 Server 生命周期;Linux 需要可用的 `systemd --user` 管理器。在浏览器打开向导输出的 Dashboard 地址,端口以 `.env` 中保存的 `POWERCONTEXT_SERVER_HTTP_PORT` 为准。未启用认证时可直接进入页面;启用后使用 **Server Token** 登录,不是模型 API key。首次没有数据是正常现象。开发、排障、临时使用或没有原生用户服务管理器时,可使用 `powercontext server run --env-file .env`;如个人服务正在运行,先将其停止。服务管理详见[个人后台服务](../operate/deploy-server.md#运行持久个人-server)。 +在浏览器打开向导输出的 Dashboard 地址,端口以 `.env` 中保存的 `POWERCONTEXT_SERVER_HTTP_PORT` 为准。未启用认证时可直接进入页面;启用后使用 **Server Token** 登录,不是模型 API key。首次没有数据是正常现象。开发、排障、临时使用或没有原生用户服务管理器时,可使用 `powercontext server run --env-file .env`;如个人服务正在运行,先将其停止。服务管理详见[个人后台服务](../operate/deploy-server.md#运行持久个人-server)。 加载客户端连接配置并检查服务: @@ -72,7 +72,7 @@ POWERCONTEXT_CODEX_SCOPE_ID=替换为返回的scope_id `codex-xxxxxxxx` 是向导规划的标题,不是 ID。配置 Claude Code 时,将其创建请求返回的 ID 写入 `POWERCONTEXT_CLAUDE_SCOPE_ID`。不同 Agent 可以各自隔离,也可以显式绑定同一已有 Scope。切换目录本身不会创建隔离。验收期间,Dashboard 和 Agent 必须使用相同的 Scope。 -修改 `.env` 后,协调服务配置,重新加载客户端文件并安装匹配的插件: +修改 `.env` 后,更新服务设置,重新加载客户端配置并安装匹配的插件: ```bash powercontext service install --env-file .env diff --git a/docs/zh/rfcs/1892-script-installation.md b/docs/zh/rfcs/1892-script-installation.md index 2d4db5a26f..298b507b8a 100644 --- a/docs/zh/rfcs/1892-script-installation.md +++ b/docs/zh/rfcs/1892-script-installation.md @@ -1,164 +1,174 @@ --- title: 脚本安装 -description: 通过 Bash 和 PowerShell 安装最新或指定版本,独立管理包与运行时镜像,并显式选择 Agent。 +description: 引导安装 Python 和 uv,选择 PowerContext 版本,并安装匹配的 Agent 集成。 --- - Proposal Name: `script_installation` - Start Date: 2026-10-09 - RFC PR: [oceanbase/powercontext#1892](https://github.com/oceanbase/powercontext/pull/1892) +- Tracking Issue: [oceanbase/powercontext#1900](https://github.com/oceanbase/powercontext/issues/1900) - Related RFCs: [RFC 1733](1733-usability-and-agent-workflows.md)、[RFC 1299](1299_local_server_availability_and_service_installation.md) # Summary -提供由发行版维护的 Bash 和 PowerShell 入口,通过 uv 安装 PowerContext。默认版本是所选包索引中的最新稳定版,`--version` 可以指定准确版本。脚本补齐缺少的 uv 和 Python,安装所选 Runtime profile,并按需调用现有 Agent 集成适配器,使用与实际安装的 Runtime 对应的 tag。 +提供通过 uv 安装 PowerContext 的 Bash 和 PowerShell 脚本。脚本复用或安装 Python 和 uv,选择 Runtime profile,并按需安装版本匹配的 Agent 集成。默认包版本为所选索引中的最新稳定版,用户也可以指定准确版本。 -安装、配置、诊断和服务运行各自承担独立职责。安装软件不会配置推理服务、启动 Server 或注册持久服务;配置和服务运行仍分步复用现有 CLI。安装指南说明脚本用法;README 和快速开始保留固定版本的 uv 命令,并提供脚本指南入口。 +配置和个人服务注册仍由用户分步执行现有 CLI 命令。脚本完成后给出这些步骤的指引,具体操作见[安装指南](../docs/get-started/install-and-run.md)。 # Motivation -正常安装不应要求用户自行协调 Python 发现、uv 安装、包 extras、包索引及集成 Git ref。Python 包、uv 二进制和 Python 发行版也来自不同渠道:只更换 PyPI 索引无法解决 Python 下载不可达的问题。 +安装 PowerContext 需要兼容的 Python、uv、正确的包 extras,以及匹配的集成 ref。Python 包、uv 二进制和 Python 发行版还需要分别配置下载来源;仅更换包索引无法解决 Python 下载失败的问题。 -安装负责放置版本化组件并注册显式选择的集成;配置管理 Server URL、Scope、采集策略等可变值;诊断观察安装结果。分清这些职责,安装体验就可以独立于配置存储、统一 Hooks、运维工具或服务生命周期改进。 - -预期结果是每种 shell 一个入口、复用已有依赖、明确的版本选择、显式的宿主选择、可恢复的部分失败,以及通过实际安装产物进行验证。 +安装器负责前置依赖和发行版本选择,复用现有配置、集成和服务命令。用户可以先安装软件,再决定何时启动 Server、使用哪个 Agent 宿主。 # Guide-level explanation -## 安装 Runtime profile +## Runtime profile 与前置依赖 -macOS 或 Linux: +默认的 `local` profile 安装 CLI、Client 和本地 Server 依赖。`--profile client` 安装连接远程 Server 所需的 CLI 和 Client 依赖。Profile 与 Agent 选择相互独立。 -```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -``` +安装器复用已有 uv,以及兼容的系统 Python 或 uv 管理的 Python;缺少时在用户目录补齐。默认安装可以无人值守执行,无需管理员权限,完成后打印新终端所需的 PATH 命令。 + +## 发行版本与 Agent 集成 -Windows,使用 PowerShell 5.1 或更新版本: +`--version latest` 选择已配置索引中兼容的最新稳定版。指定准确版本可以固定包版本。例如: -```powershell -powershell -ExecutionPolicy Bypass -c "& ([scriptblock]::Create((irm https://powercontext.oceanbase.io/install.ps1)))" +```bash +curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.2.0 --host codex ``` -默认 `local` profile 安装 CLI、Client 和本地 Server 依赖。`--profile client` 只安装连接已有 Server 所需的 CLI 和 Client 依赖。Profile 与宿主选择相互独立:选择数据库角色不隐含选择 Agent,选择 Codex 也不会更改 Runtime profile。 +只有显式传入 `--host` 才运行集成安装。所选宿主需要 Git 和各自的前置依赖;可以选择多个宿主。`--no-hosts` 显式跳过集成,与 `--host` 冲突。 -安装器复用已有 uv,以及兼容的系统 Python 或 uv 管理的 Python;否则在用户目录补齐 uv 和 Python 3.12。完成后打印实际 Runtime 版本、profile、可执行文件目录、PATH 命令及下一步配置命令,不修改持久 PATH 设置。继续操作前,先执行打印的 PATH 命令。 +安装器读取已安装的 CLI 版本,把对应的 `powercontext-vVERSION` tag 传给 `powercontext setup`,全部宿主使用同一发行 ref。`--env-file PATH` 把已有连接文件传给宿主安装;安装器不解析或执行该文件。 -## 选择发行版和集成 +## 配置与个人服务 -默认值为 `--version latest`,重复执行会检查已配置索引上的更新稳定版。需要可重复的包版本选择时,指定准确版本: +安装完成后,用户通过现有 CLI 配置本地 Server、注册个人服务,由原生用户服务管理器在终端关闭后继续运行 Server。Client-only 安装按远程连接指南设置。[安装指南](../docs/get-started/install-and-run.md#配置并安装个人服务)说明配置、服务注册和连接检查的具体步骤。 -```bash -curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1.2.0 --host codex -``` +配置、Server 启动和服务生命周期由各自的现有命令负责。个人服务的 loopback 绑定、管理器支持等约束沿用 [RFC 1299](1299_local_server_availability_and_service_installation.md)。Windows 支持保持试验性。 -默认安装无需 Git 或宿主选择界面,可以无人值守执行。`--host` 可以重复,用于显式安装集成;这时需要 Git 和各宿主自己的前置条件。`--no-hosts` 显式表达默认行为,不能与 `--host` 同时使用。安装后需要交互选择宿主时,运行 `powercontext setup --ref "powercontext-v$(powercontext --version)"`。 +## 下载来源 -已安装的 CLI 提供准确版本,形成 `powercontext-vVERSION`。宿主安装不再次解析 `latest`,也不默认使用 `master`。宿主安装失败时,保留已安装的 Runtime,脚本返回非零状态,并提示按该 tag 重试 setup;各适配器提供其已有的宿主结果。 +包索引、uv 二进制和 Python 发行版分别配置来源。用户可以选择区域或分别指定来源。显式设置优先于自动镜像;自动镜像可以按下文规则回退到官方源。 -个人 macOS/Linux 配置受保护文件后,运行 `powercontext service install --env-file .env`、`powercontext service status` 再加载生成的 Client 配置,运行 `powercontext ready` 和 `powercontext capabilities`;由原生用户服务管理器负责生命周期。开发、调试、临时使用或缺少 manager 时,保留前台 `server run`。Client-only 模式按远程连接指南设置地址和认证。持久服务仍由用户显式注册。 +# Reference-level explanation -## 配置与个人服务 +## 职责与执行顺序 -配置和服务注册分步复用现有 CLI。先运行 `config init --output .env`,保存后再执行 `config validate --env-file .env`、`service install --env-file .env` 和 `service status`。运行 `ready` 和 `capabilities` 前,加载生成的 Client 配置。完整步骤见[安装指南](../docs/get-started/install-and-run.md#配置并安装个人服务)。 +入口为 `website/public/install.sh` 和 `website/public/install.ps1`,作为网站静态资源发布。两个脚本提供相同的公开选项和失败行为。 -安装器不运行配置向导,也不管理服务。取消向导后,不会由安装器继续注册服务。服务归属、管理器支持、文件保护、登录自启动和恢复规则仍由现有服务层负责。Windows 个人服务保持试验性支持。 +| 职责 | 负责方 | +| --- | --- | +| 校验安装参数、准备前置依赖、编排安装步骤 | Shell 脚本 | +| 解析包、选择兼容产物、维护工具环境 | uv | +| 安装选定的 Agent 集成 | 现有 `powercontext setup` 适配器 | +| 配置和运行 Server、诊断状态、管理个人服务 | 现有 CLI 和服务层 | -`--env-file PATH` 仅为显式选择的宿主安装指定已有连接文件,通过 `setup --env-file PATH HOST...` 及实际安装版本对应的 tag 传入;Shell 不解析或执行文件内容。修改服务配置(包括 Scope ID)后,按服务指南使用原文件重新注册。 +脚本按以下顺序执行: -## 选择下载来源 +1. 校验参数,包括宿主选项冲突及所指定连接文件是否存在。 +2. 复用或安装 uv 和 Python,再选择包索引。 +3. 在 uv tool 环境中安装所选包 requirement。 +4. 通过 `uv tool dir --bin` 找到已安装入口,检查版本和命令可用性。 +5. 打印已安装版本和 PATH 指引,再按该版本的 tag 安装选定宿主。 +6. 打印本地服务或远程连接指引;宿主安装失败时返回非零状态。 -`--region auto|cn|global` 优先于 `POWERCONTEXT_INSTALL_REGION`。自动模式依次参考本地命名时区、locale 地区,最后采用全球源,不调用网络定位服务。 +## 前置依赖复用 -| 组件 | 全球默认源 | 中国区域自动源 | 显式控制 | -| --- | --- | --- | --- | -| PowerContext 及依赖 | PyPI | 清华 PyPI 镜像 | `--index-url`、uv 索引设置及配置文件 | -| uv 安装器及二进制 | Astral 渠道 | USTC release 镜像 | `POWERCONTEXT_UV_INSTALLER_URL`、`UV_DOWNLOAD_URL`、uv 安装器镜像变量 | -| Python | uv 默认渠道 | NJU python-build-standalone 镜像 | `UV_PYTHON_INSTALL_MIRROR`、uv Python 下载配置 | +缺少 uv 时,安装器使用自己维护的固定版本安装,与 PowerContext 版本无关。已有 uv 直接复用,不执行升级。 -已有 uv 配置时不自动选择包镜像。显式 `--index-url` 只更改默认索引,额外索引沿用 uv 的优先级。认证和索引解析由 uv 负责。脚本 URL 参数必须使用 HTTPS,且不包含凭据;凭据通过 uv 配置管理。pip 的索引环境变量不属于 uv 配置。Python 和 uv 下载覆盖项与包索引彼此独立。 +Python 发现接受 3.11 及以上、低于 4.0 的版本,并排除虚拟环境。没有兼容解释器时,通过 uv 安装 Python 3.12。平台、架构和产物由 uv 选择。`UV_INSTALL_DIR`、`UV_TOOL_DIR` 和 `UV_TOOL_BIN_DIR` 继续控制安装位置。 -# Reference-level explanation +## 版本和 profile 语义 -## 安装职责 +| 选择 | 包 requirement 与行为 | +| --- | --- | +| `--profile local`(默认) | `powercontext[cli,server]` | +| `--profile client` | `powercontext[cli]`,包含 Client 依赖 | +| `--version latest`(默认) | 未固定版本的 profile requirement,使用 `uv tool install --upgrade --force --prerelease disallow` | +| 准确版本 | 在 profile requirement 后追加 `==VERSION` | -入口为 `website/public/install.sh` 和 `website/public/install.ps1`,作为网站静态资源发布。两者支持相同的选项和失败语义。脚本负责引导与编排;uv 负责环境创建和包解析;现有 `powercontext setup` 适配器负责集成安装。 +`latest` 指所选解释器及已配置索引兼容的最新稳定版。`--force` 让旧版 uv 重新解析工具环境,避免已有预发布版本绕过稳定版选择。解析失败时保留请求的 requirement 并报告错误。 -安装操作由以下输入确定: +准确版本采用 `X.Y.Z`,可以追加 `aN`、`bN` 或 `rcN`。该选项不接受 0.1.0 之前的版本、版本范围、源码 ref 或 URL。更换 profile 会替换工具的依赖集合;需要管理其他 extras 的用户使用手动安装路径。 -```text -包 requirement + Runtime profile + 显式宿主选择 = 安装操作 -``` +已安装入口必须报告发行版本,准确版本请求必须与其一致。所有 profile 都需要通过 CLI help 和 `capabilities --help`;`local` 还需要通过 `config init --help` 和 `server run --help`。这些检查验证导入和命令可用性;Server 就绪状态在用户完成配置并启动后检查。 -包管理器在替换工具环境前解析 requirement。安装完成后验证 CLI 的准确版本,再用于全部宿主安装。这样一次操作具有统一的发行版坐标,无需新增包解析器,也不依赖另一个发行清单系统。 +## 来源选择与回退 -保留 `powercontext setup`、`config`、`doctor` 和服务命令。不把领域行为搬到 shell,不引入独立 Python 安装引擎,也不修改 Runtime 或公共 HTTP API。 +区域选择依次使用 `--region`、`POWERCONTEXT_INSTALL_REGION`、本地命名时区、locale 地区,最后采用全球源。检测只读取本地设置,不调用网络定位服务。 -## 版本和 profile 语义 +| 组件 | 全球默认源 | 中国区域自动源 | 显式控制 | +| --- | --- | --- | --- | +| PowerContext 及依赖 | PyPI | 清华 PyPI 镜像 | `--index-url`、uv 索引设置及配置文件 | +| uv 安装器及二进制 | Astral 渠道 | USTC release 镜像 | `POWERCONTEXT_UV_INSTALLER_URL`、`UV_DOWNLOAD_URL`、uv 安装器镜像变量 | +| Python | uv 默认渠道 | NJU python-build-standalone 镜像 | `UV_PYTHON_INSTALL_MIRROR`、uv Python 下载配置 | -- `latest` 向 `uv tool install --upgrade --force --prerelease disallow` 传入未固定版本的 profile requirement。它表示所选解释器及已配置源兼容的最新稳定版,不一定是另一镜像上最近上传的版本。重新解析工具环境,避免旧版 uv 中已有预发布版本绕过稳定版选择。解析失败直接报告,不改换 requirement。 -- 准确版本使用 `==VERSION`,格式为 `X.Y.Z`,可追加 `aN`、`bN` 或 `rcN`。支持显式预发布版本;`--version` 不接受源码 ref、版本范围或 URL,也不支持 0.1.0 之前的版本。 -- `local` 使用 `powercontext[cli,server]`;`client` 使用 `powercontext[cli]`,后者已经包含 Client 依赖。使用不同 profile 重跑会替换该工具的依赖集合;自行添加的 extras 应通过文档中的手动安装路径维护。 -- `uv tool dir --bin` 中的可执行文件必须报告发行版本;显式请求必须与其一致。所有 profile 都需要通过 CLI help、`capabilities --help`;`local` 还需要通过 `config init --help` 和 `server run --help`。全部通过后才打印安装成功。这些检查验证导入和命令可用性,不代表 Server 就绪或 Agent 工作流可用。 +已有 uv 配置时不自动选择包镜像。`--index-url` 替换默认索引,额外索引沿用 uv 的优先级。认证和包索引协议由 uv 处理。脚本 URL 参数必须使用 HTTPS,且不含凭据;pip 索引变量不会配置 uv。 -uv 使用安装器维护的固定引导版本,与 PowerContext 发行版本无关。已有 uv 直接复用,不悄悄升级。Python 发现排除虚拟环境,避免项目 venv 意外成为安装前提。没有兼容的 Python 3.11+ 时,通过 uv 安装 Python 3.12。平台、架构和 wheel 兼容性错误由 uv 报告。 +显式 uv 和 Python 来源不做回退。自动选择时,uv 镜像安装器不可用则回退 Astral;uv 镜像产物可回退官方源。Python 构建 URL 由 uv 提供;脚本检查对应镜像产物,自动镜像失败时使用 uv 默认渠道重试。 -## 镜像优先级与恢复 +自动包镜像在工具安装前接受有超时限制的可用性请求。镜像不可达时回退 PyPI;可达时保持选中,即使缺少请求版本。解析和产物下载错误由 uv 报告,脚本不在工具安装尝试后切换索引,以免文件已改变时又使用另一来源重试替换。索引解析和兼容性检查仍由 uv 负责。 -保留用户显式指定的 uv/Python 来源,不做自动回退。中国区域自动选择时,uv 镜像安装器不可用则回退 Astral;uv 镜像文件下载失败时可以尝试官方源。Python 的准确构建 URL 由 uv 提供,安装前检查对应镜像文件;自动 Python 镜像安装失败后,使用 uv 默认渠道重试。 +## 失败与恢复 -中国区域自动包源选择对 PowerContext 索引页发起有超时限制的可用性请求;镜像不可达时,在工具安装前回退 PyPI。可达镜像即使缺少请求版本也保持选中,由 uv 报告解析失败。全球源和显式来源直接交给 uv。脚本不解释索引 HTML、wheel 文件名、编码 URL、包兼容性或认证,也不在一次可能已经改变安装文件的工具安装尝试后换源重试。 +后续步骤失败时,保留已经完成的组件: -Shell 环境变化局限于安装器进程;PowerShell 在 `finally` 恢复临时来源变量。不用 shell 解析或改写已有配置文件,只清理由安装器创建的临时下载目录。保留 `UV_INSTALL_DIR`、`UV_TOOL_DIR` 和 `UV_TOOL_BIN_DIR` 的位置控制。 +| 失败阶段 | 结果与恢复方式 | +| --- | --- | +| 前置依赖安装失败 | 停止并报告下载错误,保留已经安装的前置依赖 | +| 包解析失败,包括准确版本不存在 | 保留已安装工具,报告 uv 错误 | +| 已安装入口验证失败 | 报告包已安装但验证失败,允许指定准确版本重试 | +| 宿主安装失败 | 保留已验证的 Runtime,返回非零状态,并提示使用匹配的发行 ref 重试 setup | -## 持久化与兼容性 +入口验证失败时,旧可执行文件可能已经被替换。安装器不回滚包文件,也不撤销已经完成的宿主操作,只清理自己的临时下载。来源和 PATH 变化局限于安装进程;PowerShell 在 `finally` 恢复临时来源变量。 -普通包安装不读取或改写 `.env`、凭据、数据目录或数据库 schema,也不停止或重启服务。配置和服务注册分步复用现有 CLI。启动升级后的 Server 时,用户按现有升级和迁移指南操作。准确版本重试可以使用缓存;`latest` 则明确允许升级。 +## 持久化与兼容性 -`UV_OFFLINE=1` 只支持已有 uv、兼容 Python 和全部依赖时的缓存重装,同时禁用安装器源探测和 uv 网络访问,不承诺完整离线发行包。后续步骤失败会保留已安装的前置依赖。准确版本不存在导致的解析失败会保留原工具;但 uv 接受新包后,验证失败可能发生在旧可执行文件已被替换之后。此时明确报告“包已安装,验证失败”,允许通过准确版本重试,不声称已经回滚。后续宿主安装失败则保留已验证的 Runtime。不提供跨组件回滚,也不修复无关宿主状态。 +包安装保持 `.env`、凭据、数据目录和数据库 schema 不变,也不停止或重启服务。启动升级后的 Server 时,用户按现有迁移和服务指南操作。Runtime API 及现有配置、诊断、服务命令保持原有契约。 -Windows 保持产品的试验性支持状态。原生验收在 Linux、macOS 和 Windows 执行;具体宿主支持范围仍由各集成的能力契约决定。 +`UV_OFFLINE=1` 禁用安装器探测和 uv 网络访问。缓存重装需要已有 uv、兼容 Python 和全部所需包;缺少前置依赖或产物时明确失败。 ## 可执行契约与验收 -`tests/fixtures/installation/*.json` 定义两个 shell 适配器共享的公开输入与预期结果。`tests/test_installation_contract.py` 使用真实 uv 和小型离线 fixture wheel 执行宿主显式选择、profile、准确版本校验、命令缺失和宿主部分失败用例。用例观察安装后的能力、所选宿主、发行 ref 和退出结果,不固定内部函数边界或调用顺序。同一组用例在各操作系统原生执行;模拟宿主执行不等于真实原生服务或 Agent 验收。宿主用例覆盖传递已有连接文件;配置和个人服务生命周期验收与安装器契约分开。 +两个 shell 适配器都必须通过 `tests/fixtures/installation/*.json` 中的共享行为用例。用例通过真实 uv 和小型 fixture wheel,检查公开参数、安装后的能力、匹配的宿主 ref 和退出结果。 -`tests/native/test_installation.py` 执行真实 shell 安装器、uv、安装后的 PowerContext CLI 和 HTTP Server。从被测提交构建 wheel,使用发行版形式的元数据和直接本地文件约束。该约束选定被测产物,不构成独立执行的校验和验证。验收包括: +验收覆盖: -- 全球源及中国区域源下缺少 uv/Python 的安装、已有工具与配置、包含空格和中文的路径; -- 通过受控包索引解析真实稳定版和预发布版 wheel、默认升级、指定版本,以及版本不存在时保留原安装; -- Client-only 安装及 profile 切换,安装过程不创建本地 Server 状态; -- `.env` 生成与验证、就绪检查、Memory 保存与搜索、离线缓存重装及重启后的回读; -- 显式索引失败、宿主选项冲突、无人值守与 Bash 管道安装,以及 PowerShell 下载脚本后执行 scriptblock 的入口。 +- 缺少 uv 或 Python 时的引导、复用已有工具与配置,以及包含空格或非 ASCII 字符的路径。 +- 最新稳定版、准确版本、显式预发布版,以及解析失败时保留已安装工具。 +- Client/local profile 替换、显式宿主选择、宿主部分失败及已有连接文件传递。 +- 无人值守和 Bash 管道执行,以及 PowerShell 文件和下载后 scriptblock 入口。 +- 安装实际 wheel、配置验证、HTTP 就绪、Memory 持久化、离线缓存重装和重启回读。 -独立 CI 矩阵在三个操作系统执行上述套件。发布引用检查保持脚本的 `latest` 默认值,同时更新显式版本示例。网站验证检查文档链接和静态构建。语法检查本身不证明 Windows 或 macOS 安装可用;这些测试也不声称真实 Agent 宿主已完成采集与召回工作流。 +原生 CI 矩阵在 Linux、macOS 和 Windows 上运行安装验收;受控索引测试在 Linux 上验证镜像选择和失败处理。平台验收需要原生执行,语法检查和模拟宿主不代表服务管理器或 Agent 工作流可用。发布引用检查保持显式版本示例同步,同时保留 `latest` 默认值。 # Drawbacks -Bash 和 PowerShell 会重复部分编排和来源选择策略。自动镜像增加外部可用性依赖。`latest` 会随时间变化,可重复部署需要指定版本。匹配 tag 能协调发行版,但不能单独证明宿主兼容性。 +Bash 和 PowerShell 需要分别实现前置依赖与来源选择。自动镜像增加可用性依赖。`latest` 会随所选索引发布新版本而变化,可重复部署需要准确版本。匹配的集成 tag 用于协调版本,各宿主仍需单独验证兼容性。 # Rationale and alternatives -保留直接运行 uv 命令的路径,供已有 Python 和 uv 的用户使用;它无法引导干净机器。源码安装适合开发,不作为默认发行渠道。把公共脚本固定在某个 PowerContext 版本,会使新用户在网站重新发布前一直安装过期版本。 +已有 Python 和 uv 的用户可以直接使用 uv 命令;脚本负责补齐这些前置依赖。源码安装仍供开发使用。脚本默认使用 `latest`,避免两次网站发布之间固定的包版本逐渐过时。 -没有 Python 的机器仍需要为 Python 引擎提供引导。生成 shell 适配器仍需原生行为测试,还会增加生成器和版本管理。共享契约用例可以提供可执行的一致性约束,无需分发新的运行时资源。仅当具体策略确有需要时再引入引擎或生成器。 +Python 安装引擎仍需要 shell 引导。生成 shell 适配器会增加生成器,同时仍需原生测试。初期采用两个较小的适配器,并共享行为用例,可以减少维护环节。 -分发独立版本的原始产物时,不可变清单和额外安装账本有其用途。这里由 uv 管理 wheel 解析、工具环境及自身记录;安装器观察实际入口,通过显式的 uv 安装重试修复组件,避免同一安装状态出现两个权威来源。 +uv 已经维护包环境和安装记录,额外安装账本会重复保存这些状态。如果以后分发独立版本的原始产物,再引入对应的清单和记录。 # Prior art -[RFC 1408](https://github.com/oceanbase/powercontext/pull/1408) 定义了安装、配置和诊断的职责分离,独立的 Runtime profile 与宿主选择、组件级恢复和显式服务注册。[RFC 1299](1299_local_server_availability_and_service_installation.md) 定义个人服务生命周期。 +[RFC 1408](https://github.com/oceanbase/powercontext/pull/1408) 区分安装、配置和诊断,定义独立的 profile、宿主选择及组件恢复。[RFC 1299](1299_local_server_availability_and_service_installation.md) 定义本方案复用的个人服务生命周期。 -[Magpie 安装器](https://github.com/yetone/magpie/blob/023f5aaad2ecd41ae04390166b9cac9a0b300d81/site/public/install.sh) 将权威发行源与产物镜像分开,在替换二进制前检查校验和。它直接拥有原始二进制的分发职责,因此需要这层验证;其清单不能替代 Python 包标准。 +[Magpie 安装器](https://github.com/yetone/magpie/blob/023f5aaad2ecd41ae04390166b9cac9a0b300d81/site/public/install.sh) 区分发行源与产物镜像,在替换前验证二进制校验和。它的清单用于直接分发二进制,本方案则把包分发交给 uv。 -[Lody daemon 安装契约](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/specs/daemon-upgrade-installation.md) 解析并执行实际 npm 安装位置,检查替换后的就绪状态。它另有直接管理原始压缩包的 [Agent runtime](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/apps/cli/src/agent/managed-agent-runtime.ts),因此拥有清单和完成记录。本方案采用实际可执行文件检查,服务就绪仍归服务边界负责。其公开 tag-release 工作流不发布安装器。 +[Lody daemon 安装器](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/specs/daemon-upgrade-installation.md) 解析实际 npm 安装位置并验证安装后的可执行文件,本方案同样检查实际入口。它的 [Agent runtime](https://github.com/LodyAI/Lody/blob/811b573329716b23e1144e5d66211ea4ddfb0dfd/apps/cli/src/agent/managed-agent-runtime.ts) 另行管理原始归档及其记录。 -[Bub](https://github.com/bubbuild/bub/tree/b4a61bf1326729a024161d22ba20019b8500f907/website/public) 先引导 uv,再运行 Python preset 解析器。它对终端和 macOS Bash 的修复支持显式宿主安装与原生测试的必要性。[uv 工具环境](https://docs.astral.sh/uv/concepts/tools/)及 [配置规则](https://docs.astral.sh/uv/concepts/configuration-files/)定义复用的环境与来源语义;[Python Simple API](https://packaging.python.org/en/latest/specifications/simple-repository-api/)定义包索引解释规则。Shell 正则无法实现这些契约。 +[Bub](https://github.com/bubbuild/bub/tree/b4a61bf1326729a024161d22ba20019b8500f907/website/public) 先引导 uv,再运行 Python preset 解析器。本方案同样使用 shell 引导,并在各原生平台测试两个适配器。[uv 工具环境](https://docs.astral.sh/uv/concepts/tools/)、[uv 配置](https://docs.astral.sh/uv/concepts/configuration-files/)和 [Python Simple API](https://packaging.python.org/en/latest/specifications/simple-repository-api/)定义交给 uv 处理的环境与包来源规则。 # Unresolved questions -接受本契约无需额外的跨子系统决策。完整离线包、带密码学校验的安装清单、独立打包的宿主产物以及配置存储变化,需要各自设计。推广前必须保持平台矩阵通过,Windows 产品支持仍为试验性。 +完整离线包、密码学校验安装清单、独立打包的宿主产物,以及配置存储变化,需要单独设计。本 RFC 保持 Windows 的试验性支持状态。 # Future possibilities -当安装计划需要不可变组件清单、独立集成版本或持久组件修复记录时,脚本可以引导发行版维护的独立安装引擎。离线包也可以显式携带全部所需产物。这些扩展继续保持 Server 启动与服务注册的显式同意。 - -个人服务引导工作关联 [Issue #1900](https://github.com/oceanbase/powercontext/issues/1900)。 +当需要独立组件版本、不可变清单或持久修复记录时,可以考虑安装引擎。离线包可以携带全部所需产物。这些扩展仍保留显式的 Server 启动和服务注册。 diff --git a/tests/fixtures/installation/README.md b/tests/fixtures/installation/README.md index 82a6161d98..4e1c56b4db 100644 --- a/tests/fixtures/installation/README.md +++ b/tests/fixtures/installation/README.md @@ -1,14 +1,20 @@ # Installer behavior contract -The JSON catalogs describe public installer inputs and observable outcomes. Both native shell entry points use the same cases through `tests/test_installation_contract.py`. The catalogs are test inputs, not bootstrap downloads or an additional installation manifest. RFC 1892 defines the overall installation and recovery boundary. +Both shell installers run against the JSON catalogs in this directory through `tests/test_installation_contract.py`. Add a case here when changing a public argument or observable installer behavior. -Run the offline contract with an existing Python and uv: +## Run the contract tests + +With Python and uv available, run from the repository root: ```bash uv run pytest tests/test_installation_contract.py -q ``` -Each file has `schema_version: 1` and a `cases` array. Case IDs must be unique across catalogs. +The harness builds small fixture wheels, installs them with real uv in temporary directories without network access, and runs the installed launcher. Profile cases check the installed dependency set. Host cases use a fixture CLI to record the setup arguments and simulate success or failure. + +## Add a case + +Each catalog has `schema_version: 1` and a `cases` array. Give every case an ID unique across all catalogs. Describe the public arguments and the resulting installation state or error. Avoid assertions about private function names or call order. | Field | Meaning | | --- | --- | @@ -19,15 +25,23 @@ Each file has `schema_version: 1` and a `cases` array. Case IDs must be unique a | `reported_version` | Fixture CLI version; defaults to the wheel's `1.2.0` | | `environment_file` | Create a protected existing configuration file for the case | | `missing_commands` | Public command help paths omitted by the fixture | -| `expected.exit_code` | Installer completion or failure | +| `expected.exit_code` | Expected installer process exit code | | `expected.runtime_installed` | Whether uv installed the tool launcher, including verification failures | | `expected.server_extra` | Whether the selected profile contains the fixture Server dependency | | `expected.selected_hosts` | Hosts actually submitted to setup; defaults to none | | `expected.setup_ref` | Release tag actually submitted to setup | | `expected.setup_env_file` | Whether host setup received the selected configuration file | -| `expected.verification_failed` | Whether success must remain unannounced | +| `expected.verification_failed` | Whether the Runtime installed message must be absent | | `expected.error_contains` | Required diagnostic fragment | -The harness builds small standards-shaped wheels, installs them through real uv in private directories without network access, and executes the resulting native launcher. Profile cases inspect an optional installed dependency. Host cases observe the setup request and result through the fixture CLI. They do not start a real Agent, execute Git, or establish Server readiness. No case requires a particular shell function name, private call order or subprocess count. +## Choose the right test suite + +| Suite | Coverage | +| --- | --- | +| `tests/test_installation_contract.py` | Shared installer arguments, profiles, command verification, host selection, and partial host failures using fixture wheels | +| `tests/native/test_install_sources.py` | Source selection, fallback, and version resolution with real uv and a controlled loopback index on Linux | +| `tests/native/test_installation.py` | Actual PowerContext wheel, prerequisite bootstrap, HTTP readiness, Memory persistence, offline reinstall, profiles, and versions | + +The installation CI workflow builds the PowerContext wheel and runs the shared contract and native acceptance on Linux, macOS, and Windows. Set `POWERCONTEXT_INSTALL_WHEEL` to a built wheel to run actual-wheel acceptance locally; see `.github/workflows/installation.yml` for the build and invocation. -The installation CI matrix runs these cases on Linux, macOS and Windows. The same workflow also builds the actual PowerContext wheel and runs `tests/native/test_installation.py` for bootstrap, configuration, HTTP readiness, Memory persistence, profile replacement and version selection. `tests/native/test_install_sources.py` adds Linux loopback index fault tests using the real downloader and uv, with only distribution endpoints redirected to fixtures. Those source tests do not qualify public mirror availability or native Windows source handling. Personal-service lifecycle acceptance remains in the existing native service workflow, separate from installer conformance. +Fixture hosts do not exercise a real Agent or service manager. Controlled sources do not check public mirror availability. Personal-service acceptance runs separately in `.github/workflows/native-service.yml`. From f257d4a92d1b1892741c0d9febd44e9b681ece18 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Mon, 2 Nov 2026 14:03:59 +0800 Subject: [PATCH 19/24] fix(ci): route script installation through Main admission --- .github/ci-paths.toml | 4 +++- .github/workflows/installation.yml | 22 +++++++++++++++++----- .github/workflows/master.yml | 11 +++++++++++ tests/test_ci_changes.py | 6 ++++++ tests/test_ci_pipeline.py | 4 ++-- 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/.github/ci-paths.toml b/.github/ci-paths.toml index cabd12f66b..e9ba88b633 100644 --- a/.github/ci-paths.toml +++ b/.github/ci-paths.toml @@ -90,6 +90,8 @@ observability = [ "tests/e2e/*observability*", "tests/builtin/runtime/*worker*", "tests/builtin/runtime/*model_usage*", ] packaging = [ + "website/public/install.sh", "website/public/install.ps1", ".github/workflows/installation.yml", + "tests/test_installation_contract.py", "tests/native/test_installation.py", "tests/native/test_install_sources.py", "scripts/build_ops_bundle.py", "tests/test_installation.py", "tests/test_upgrade_publication.py", "scripts/ci_role_smoke.py", "scripts/ci_installation_roles.py", "tests/test_ci_installation_roles.py", "release/**", @@ -193,7 +195,7 @@ zcode = ["zcode-acceptance"] kimi = ["kimi-acceptance"] upgrade = ["seekdb-managed-startup", "upgrade-acceptance"] observability = ["worker-observability"] -packaging = ["isolated-installations"] +packaging = ["isolated-installations", "script-installation"] desktop = ["desktop-validation"] managed_resources = ["managed-resource-candidates"] candidate_replay = ["candidate-replay"] diff --git a/.github/workflows/installation.yml b/.github/workflows/installation.yml index 8d004cc417..f5deb4e392 100644 --- a/.github/workflows/installation.yml +++ b/.github/workflows/installation.yml @@ -1,20 +1,32 @@ name: Script installation on: - push: - branches: [master] - pull_request: - types: [opened, synchronize, reopened, ready_for_review] + workflow_call: + inputs: + suites: + description: Scope already selected by the Main orchestrator + required: true + type: string + workflow_dispatch: permissions: + actions: read contents: read concurrency: - group: installation-${{ github.event.pull_request.number || github.ref }} + group: installation-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name }} cancel-in-progress: true jobs: + changes: + if: ${{ inputs.suites == '' }} + uses: ./.github/workflows/ci-changes.yml + with: + baseline-workflow: installation.yml + installation: + needs: changes + if: ${{ !cancelled() && ((inputs.suites == '' && needs.changes.result != 'success') || fromJSON(inputs.suites || needs.changes.outputs.suites || '{}').packaging) }} strategy: fail-fast: false matrix: diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 6acc851d96..2e691ccfac 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -1143,6 +1143,16 @@ jobs: with: suites: ${{ needs.quality.outputs.suites }} + script-installation: + needs: [quality, tests] + if: >- + ${{ !cancelled() && needs.quality.result == 'success' && + contains(fromJSON('["success","skipped"]'), needs.tests.result) && + (fromJSON(needs.quality.outputs.suites || '{}').packaging) }} + uses: ./.github/workflows/installation.yml + with: + suites: ${{ needs.quality.outputs.suites }} + native-service: needs: [quality, tests] if: >- @@ -1243,6 +1253,7 @@ jobs: - desktop-validation - managed-resource-candidates - helm + - script-installation - native-service - kimi-acceptance - zcode-acceptance diff --git a/tests/test_ci_changes.py b/tests/test_ci_changes.py index d616600936..9729054488 100644 --- a/tests/test_ci_changes.py +++ b/tests/test_ci_changes.py @@ -36,6 +36,12 @@ (["docs/zh/docs/quickstart.md"], {"website", "docs_tests"}), (["docs/en/rfcs/0001_design.md", "README_CN.md"], {"website", "docs_tests"}), (["website/src/app/page.tsx"], {"website"}), + (["website/public/install.sh"], {"website", "packaging"}), + (["website/public/install.ps1"], {"website", "packaging"}), + ([".github/workflows/installation.yml"], {"packaging"}), + (["tests/test_installation_contract.py"], {"tests", "packaging"}), + (["tests/native/test_installation.py"], {"tests", "native", "packaging"}), + (["tests/native/test_install_sources.py"], {"tests", "native", "packaging"}), (["tests/e2e/test_blog_site.py"], {"website", "tests"}), (["AGENTS.md", "CONTRIBUTING.md"], set()), (["deploy/helm/powercontext/templates/deployments.yaml"], {"helm"}), diff --git a/tests/test_ci_pipeline.py b/tests/test_ci_pipeline.py index ea808a474c..1fe657ceef 100644 --- a/tests/test_ci_pipeline.py +++ b/tests/test_ci_pipeline.py @@ -169,7 +169,7 @@ def test_cancelled_or_draft_runs_cannot_start_terminal_runner_jobs(): assert "converted_to_draft" in workflow("master.yml")["on"]["pull_request"]["types"] -@pytest.mark.parametrize("selected_job", ["quality", "harness-validation"]) +@pytest.mark.parametrize("selected_job", ["quality", "harness-validation", "script-installation"]) @pytest.mark.parametrize( ("selected_result", "unselected_result", "passes"), [ @@ -185,7 +185,7 @@ def test_actual_ci_result_script_rejects_missing_selected_acceptance( ): script = workflow("master.yml")["jobs"]["ci-result"]["steps"][0]["run"] script = script.split("<<'PYTHON'\n", 1)[1].rsplit("PYTHON", 1)[0] - required = ["quality", "upgrade-release-gate", "harness-validation"] + required = ["quality", "upgrade-release-gate", "harness-validation", "script-installation"] results = {job: {"result": "success"} for job in required} results[selected_job]["result"] = selected_result results["desktop-validation"] = {"result": unselected_result} From 85483e2e88078ffdd9fa5b5b947e061c8c51654e Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Mon, 2 Nov 2026 14:37:20 +0800 Subject: [PATCH 20/24] fix(install): prevent HTTPS redirect downgrades --- tests/native/test_install_sources.py | 124 ++++++++++++++++++++++++++- website/public/install.sh | 12 ++- 2 files changed, 129 insertions(+), 7 deletions(-) diff --git a/tests/native/test_install_sources.py b/tests/native/test_install_sources.py index 2c7cde7eb6..1ab71a9e09 100644 --- a/tests/native/test_install_sources.py +++ b/tests/native/test_install_sources.py @@ -21,6 +21,7 @@ import io import os import shutil +import ssl import subprocess import sys import urllib.parse @@ -56,7 +57,40 @@ def build_wheel(version: str) -> bytes: @pytest.fixture -def installation(tmp_path): # noqa: C901 - one fixture owns the server and subprocess environment +def tls_certificate(tmp_path): + openssl = shutil.which("openssl") + if not openssl: + pytest.skip("HTTPS download acceptance requires openssl") + cert, key = tmp_path / "cert.pem", tmp_path / "key.pem" + subprocess.run( + [ + openssl, + "req", + "-x509", + "-newkey", + "rsa:2048", + "-nodes", + "-keyout", + str(key), + "-out", + str(cert), + "-days", + "1", + "-subj", + "/CN=localhost", + "-addext", + "subjectAltName=DNS:localhost", + "-addext", + "basicConstraints=critical,CA:FALSE", + ], + check=True, + capture_output=True, + ) + return cert, key + + +@pytest.fixture +def installation(tmp_path, tls_certificate): # noqa: C901 - one fixture owns the server and subprocess environment assert BASH bash_command: str = BASH wheels = {version: build_wheel(version) for version in ("1.0.0", "1.1.0", "1.2.0rc1")} @@ -105,9 +139,13 @@ def do_GET(self): self.wfile.write(payload) server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Index) + cert, key = tls_certificate + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain(cert, key) + server.socket = context.wrap_socket(server.socket, server_side=True) worker = Thread(target=server.serve_forever, daemon=True) worker.start() - base = f"http://127.0.0.1:{server.server_port}" + base = f"https://localhost:{server.server_port}" utilities = tmp_path / "utilities" utilities.mkdir() # Translate only distribution endpoints to fixtures; run the real downloader and uv. @@ -142,7 +180,10 @@ def do_GET(self): "UV_CACHE_DIR": str(tmp_path / "cache"), "UV_PYTHON_INSTALL_DIR": str(tmp_path / "python"), "UV_HTTP_RETRIES": "0", - "NO_PROXY": "127.0.0.1", + "NO_PROXY": "localhost,127.0.0.1", + "CURL_CA_BUNDLE": str(cert), + "SSL_CERT_FILE": str(cert), + "UV_NATIVE_TLS": "true", } Path(env["HOME"]).mkdir() @@ -238,3 +279,80 @@ def test_unrelated_config_preserved_and_region_does_not_override_index(installat assert "Using existing uv configuration" in output assert config.read_bytes() == original assert not any(path.startswith("/cn/") for path in index.requests) + + +@pytest.mark.parametrize("downloader", ["curl", "wget"]) +@pytest.mark.parametrize("route", ["direct", "https", "http"]) +def test_bootstrap_download_transport(tmp_path, tls_certificate, downloader, route): + executable = shutil.which(downloader) + if not executable: + pytest.skip(f"download acceptance requires {downloader}") + assert BASH + requests = [] + + class Installer(http.server.BaseHTTPRequestHandler): + def log_message(self, format: str, *args: object) -> None: # noqa: A002 + pass + + def do_GET(self): + scheme = "https" if isinstance(self.connection, ssl.SSLSocket) else "http" + requests.append((scheme, self.path)) + if self.path != "/direct": + target = self.path.removeprefix("/") + port = secure.server_port if target == "https" else plain.server_port + self.send_response(302) + self.send_header("Location", f"{target}://localhost:{port}/direct") + self.end_headers() + return + # Stop before real installation; the marker proves the bootstrap executed the download. + payload = b'#!/bin/sh\nprintf executed > "$HOME/download-executed"\nexit 1\n' + self.send_response(200) + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + cert, key = tls_certificate + plain = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Installer) + secure = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Installer) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain(cert, key) + secure.socket = context.wrap_socket(secure.socket, server_side=True) + workers = [Thread(target=server.serve_forever, daemon=True) for server in (plain, secure)] + utilities = tmp_path / "utilities" + utilities.mkdir() + for name in (downloader, "uname", "mktemp", "rm", "dirname", "cat", "sh", "readlink"): + tool = shutil.which(name) + assert tool + (utilities / name).symlink_to(tool) + wgetrc = tmp_path / "wgetrc" + wgetrc.write_text(f"ca_certificate = {cert}\n", encoding="utf-8") + for worker in workers: + worker.start() + try: + completed = subprocess.run( + [BASH, str(ROOT / "website/public/install.sh"), "--region", "global", "--no-hosts"], + env={ + "PATH": str(utilities), + "HOME": str(tmp_path), + "TMPDIR": str(tmp_path), + "UV_INSTALL_DIR": str(tmp_path / "bin"), + "CURL_CA_BUNDLE": str(cert), + "WGETRC": str(wgetrc), + "POWERCONTEXT_UV_INSTALLER_URL": f"https://localhost:{secure.server_port}/{route}", + }, + text=True, + capture_output=True, + timeout=30, + ) + output = completed.stdout + completed.stderr + assert completed.returncode != 0, output + expected_execution = route == "direct" or (route == "https" and downloader == "curl") + assert (tmp_path / "download-executed").exists() == expected_execution, output + assert all(scheme == "https" for scheme, _path in requests), requests + if downloader == "wget" and route != "direct": + assert "Install curl" in output + finally: + for server, worker in zip((plain, secure), workers, strict=True): + server.shutdown() + server.server_close() + worker.join() diff --git a/website/public/install.sh b/website/public/install.sh index 0c06d799d4..c8987c55ef 100644 --- a/website/public/install.sh +++ b/website/public/install.sh @@ -120,10 +120,14 @@ parse_args() { download() { local timeout=${3:-30} if command -v curl >/dev/null 2>&1; then - curl --fail --location --silent --show-error --connect-timeout 5 --max-time "$timeout" "$1" -o "$2" + curl --proto '=https' --proto-redir '=https' --fail --location --silent --show-error --connect-timeout 5 --max-time "$timeout" "$1" -o "$2" else timeout=${3:-15} - wget --quiet --timeout="$timeout" --tries=1 "$1" -O "$2" + # wget cannot restrict redirect protocols; reject redirects rather than allow a downgrade. + wget --quiet --max-redirect=0 --timeout="$timeout" --tries=1 "$1" -O "$2" || { + printf '%s\n' 'wget download failed. Install curl if the HTTPS source requires redirects.' >&2 + return 1 + } fi } @@ -181,9 +185,9 @@ has_uv_configuration() { url_available() { if command -v curl >/dev/null 2>&1; then - curl --fail --location --silent --head --connect-timeout 5 --max-time 10 "$1" >/dev/null + curl --proto '=https' --proto-redir '=https' --fail --location --silent --head --connect-timeout 5 --max-time 10 "$1" >/dev/null else - wget --quiet --spider --timeout=10 --tries=1 "$1" + wget --quiet --max-redirect=0 --spider --timeout=10 --tries=1 "$1" fi } From 876ccd741df105afbc6cb79b7ee1c6e8037c9940 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Tue, 3 Nov 2026 11:14:42 +0800 Subject: [PATCH 21/24] fix(install): secure PowerShell downloads and support release setup interfaces --- docs/en/rfcs/1892-script-installation.md | 6 +- docs/zh/rfcs/1892-script-installation.md | 6 +- tests/fixtures/installation/README.md | 2 + tests/fixtures/installation/hosts.json | 94 ++++++++++++++++ tests/native/test_installation.py | 134 ++++++++++++++++++++++- tests/test_installation_contract.py | 38 ++++++- website/public/install.ps1 | 53 ++++++++- website/public/install.sh | 18 ++- 8 files changed, 328 insertions(+), 23 deletions(-) diff --git a/docs/en/rfcs/1892-script-installation.md b/docs/en/rfcs/1892-script-installation.md index 8b918dfdcc..b092001444 100644 --- a/docs/en/rfcs/1892-script-installation.md +++ b/docs/en/rfcs/1892-script-installation.md @@ -39,7 +39,7 @@ curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1 Only explicit `--host` arguments run integration setup. Each selected host requires Git and its own prerequisites. Multiple hosts can be selected. `--no-hosts` explicitly skips integrations and conflicts with `--host`. -The installer reads the installed CLI version and passes its `powercontext-vVERSION` tag to `powercontext setup`. All hosts use that same release ref. `--env-file PATH` passes an existing connection file to host setup; the installer does not parse or source it. +The installer reads the installed CLI version and passes its `powercontext-vVERSION` tag to `powercontext setup`. All hosts use that same release ref. Setup runs once per selected host. `--env-file PATH` passes an existing connection file to host setup; the installer does not parse or source it. ## Configuration and personal services @@ -68,7 +68,7 @@ The scripts perform these steps in order: 1. Validate arguments, including conflicting host options and the existence of a supplied connection file. 2. Reuse or install uv and Python, then select the package index. -3. Install the selected package requirement in a uv tool environment. +3. For a client profile with selected hosts, check `setup --help` in an isolated environment with the selected package requirement. Reject unsupported combinations before replacing the installed tool; otherwise install the selected requirement in a uv tool environment. 4. Locate the installed launcher through `uv tool dir --bin` and verify its version and command availability. 5. Print the installed version and PATH instructions, then run any selected host setup at that version's tag. 6. Print local-service or remote-connection guidance. Return a nonzero status if host setup failed. @@ -104,7 +104,7 @@ Region selection follows `--region`, `POWERCONTEXT_INSTALL_REGION`, local named | uv installer and binaries | Astral channels | USTC release mirror | `POWERCONTEXT_UV_INSTALLER_URL`, `UV_DOWNLOAD_URL`, uv installer mirror variables | | Python | uv default channels | NJU python-build-standalone mirror | `UV_PYTHON_INSTALL_MIRROR`, uv Python download configuration | -Existing uv configuration suppresses automatic package mirrors. `--index-url` replaces the default index; additional indexes retain uv's priority rules. uv handles authentication and the package index protocol. Script URL arguments require HTTPS without embedded credentials. pip index variables do not configure uv. +Existing uv configuration suppresses automatic package mirrors. `--index-url` replaces the default index; additional indexes retain uv's priority rules. uv handles authentication and the package index protocol. Script URL arguments require HTTPS without embedded credentials. Download and availability-probe redirects must remain HTTPS. Windows captures native output as UTF-8 so Unicode paths remain usable, then restores the console encoding on exit. pip index variables do not configure uv. Explicit uv and Python sources are preserved without fallback. For automatic selection, an unavailable uv mirror installer falls back to Astral; mirrored uv artifacts can fall back to official sources. The Python build URL comes from uv. The script checks its mirrored artifact and retries with uv's default channels if the automatic mirror fails. diff --git a/docs/zh/rfcs/1892-script-installation.md b/docs/zh/rfcs/1892-script-installation.md index 298b507b8a..1501ef1112 100644 --- a/docs/zh/rfcs/1892-script-installation.md +++ b/docs/zh/rfcs/1892-script-installation.md @@ -39,7 +39,7 @@ curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1 只有显式传入 `--host` 才运行集成安装。所选宿主需要 Git 和各自的前置依赖;可以选择多个宿主。`--no-hosts` 显式跳过集成,与 `--host` 冲突。 -安装器读取已安装的 CLI 版本,把对应的 `powercontext-vVERSION` tag 传给 `powercontext setup`,全部宿主使用同一发行 ref。`--env-file PATH` 把已有连接文件传给宿主安装;安装器不解析或执行该文件。 +安装器读取已安装的 CLI 版本,把对应的 `powercontext-vVERSION` tag 传给 `powercontext setup`,全部宿主使用同一发行 ref。安装器逐个运行所选宿主的 setup。`--env-file PATH` 把已有连接文件传给宿主安装;安装器不解析或执行该文件。 ## 配置与个人服务 @@ -68,7 +68,7 @@ curl -fsSL https://powercontext.oceanbase.io/install.sh | bash -s -- --version 1 1. 校验参数,包括宿主选项冲突及所指定连接文件是否存在。 2. 复用或安装 uv 和 Python,再选择包索引。 -3. 在 uv tool 环境中安装所选包 requirement。 +3. client 模式选择宿主时,先在使用所选包 requirement 的独立环境中检查 `setup --help`;缺少 setup 时,在替换已安装工具前拒绝该组合。通过检查后,在 uv tool 环境中安装所选 requirement。 4. 通过 `uv tool dir --bin` 找到已安装入口,检查版本和命令可用性。 5. 打印已安装版本和 PATH 指引,再按该版本的 tag 安装选定宿主。 6. 打印本地服务或远程连接指引;宿主安装失败时返回非零状态。 @@ -104,7 +104,7 @@ Python 发现接受 3.11 及以上、低于 4.0 的版本,并排除虚拟环 | uv 安装器及二进制 | Astral 渠道 | USTC release 镜像 | `POWERCONTEXT_UV_INSTALLER_URL`、`UV_DOWNLOAD_URL`、uv 安装器镜像变量 | | Python | uv 默认渠道 | NJU python-build-standalone 镜像 | `UV_PYTHON_INSTALL_MIRROR`、uv Python 下载配置 | -已有 uv 配置时不自动选择包镜像。`--index-url` 替换默认索引,额外索引沿用 uv 的优先级。认证和包索引协议由 uv 处理。脚本 URL 参数必须使用 HTTPS,且不含凭据;pip 索引变量不会配置 uv。 +已有 uv 配置时不自动选择包镜像。`--index-url` 替换默认索引,额外索引沿用 uv 的优先级。认证和包索引协议由 uv 处理。脚本 URL 参数必须使用 HTTPS,且不含凭据。下载与可用性探测的重定向也必须保持 HTTPS。Windows 按 UTF-8 读取原生命令输出,避免破坏非 ASCII 路径,并在退出时恢复控制台编码。pip 索引变量不会配置 uv。 显式 uv 和 Python 来源不做回退。自动选择时,uv 镜像安装器不可用则回退 Astral;uv 镜像产物可回退官方源。Python 构建 URL 由 uv 提供;脚本检查对应镜像产物,自动镜像失败时使用 uv 默认渠道重试。 diff --git a/tests/fixtures/installation/README.md b/tests/fixtures/installation/README.md index 4e1c56b4db..7bed4672e5 100644 --- a/tests/fixtures/installation/README.md +++ b/tests/fixtures/installation/README.md @@ -21,6 +21,8 @@ Each catalog has `schema_version: 1` and a `cases` array. Give every case an ID | `id` | Stable scenario name shown in test results | | `args` | Arguments passed to the native installer | | `git` | Whether Git is discoverable; defaults to false | +| `setup_style` | `group` models published per-host subcommands and missing setup in client-only installs; defaults to the unified callback | +| `unicode_paths` | Use a non-ASCII tool directory; Windows also starts with OEM output decoding and checks encoding restoration | | `setup_exit` | Exit status of the fixture host operation; defaults to success | | `reported_version` | Fixture CLI version; defaults to the wheel's `1.2.0` | | `environment_file` | Create a protected existing configuration file for the case | diff --git a/tests/fixtures/installation/hosts.json b/tests/fixtures/installation/hosts.json index fc650a6fe0..fbd9782ef3 100644 --- a/tests/fixtures/installation/hosts.json +++ b/tests/fixtures/installation/hosts.json @@ -163,6 +163,100 @@ "runtime_installed": false, "error_contains": "Unknown option" } + }, + { + "id": "released-group-hosts", + "setup_style": "group", + "git": true, + "args": [ + "--host", + "codex", + "--host", + "claude" + ], + "expected": { + "exit_code": 0, + "runtime_installed": true, + "selected_hosts": [ + "codex", + "claude" + ], + "setup_ref": "powercontext-v1.2.0", + "server_extra": true + } + }, + { + "id": "released-group-env-file", + "setup_style": "group", + "git": true, + "environment_file": true, + "args": [ + "--host", + "codex", + "--env-file", + "{env_file}" + ], + "expected": { + "exit_code": 0, + "runtime_installed": true, + "selected_hosts": [ + "codex" + ], + "setup_ref": "powercontext-v1.2.0", + "setup_env_file": true, + "server_extra": true + } + }, + { + "id": "released-client-without-setup", + "setup_style": "group", + "git": true, + "args": [ + "--profile", + "client", + "--host", + "codex" + ], + "expected": { + "exit_code": 1, + "runtime_installed": false, + "selected_hosts": [], + "error_contains": "No such command 'setup'" + } + }, + { + "id": "client-with-setup", + "git": true, + "args": [ + "--profile", + "client", + "--host", + "codex" + ], + "expected": { + "exit_code": 0, + "runtime_installed": true, + "selected_hosts": [ + "codex" + ], + "setup_ref": "powercontext-v1.2.0", + "server_extra": false + } + }, + { + "id": "unicode-native-output", + "unicode_paths": true, + "git": false, + "args": [ + "--profile", + "client" + ], + "expected": { + "exit_code": 0, + "runtime_installed": true, + "selected_hosts": [], + "server_extra": false + } } ] } diff --git a/tests/native/test_installation.py b/tests/native/test_installation.py index ea78e9b39a..7459fac216 100644 --- a/tests/native/test_installation.py +++ b/tests/native/test_installation.py @@ -181,7 +181,7 @@ def test_install_configure_remember_and_reinstall(tmp_path: Path, environment: s # These subprocesses write UTF-8 log files, rather than a Windows console. PYTHONIOENCODING="utf-8", UV_TOOL_DIR=str(tmp_path / "tools"), - UV_TOOL_BIN_DIR=str(tmp_path / "bin"), + UV_TOOL_BIN_DIR=str(tmp_path / "bin with spaces naïve"), UV_CACHE_DIR=str(tmp_path / "cache"), UV_PYTHON_INSTALL_DIR=str(tmp_path / "python"), UV_NO_CONFIG="1", @@ -210,11 +210,11 @@ def test_install_configure_remember_and_reinstall(tmp_path: Path, environment: s f"& ([scriptblock]::Create([IO.File]::ReadAllText('{script_path}'))) @args\n", encoding="utf-8" ) installer[-1] = str(wrapper) - cli = str(tmp_path / "bin/powercontext.exe") + cli = str(tmp_path / "bin with spaces naïve/powercontext.exe") system_path = os.pathsep.join([str(Path(os.environ["SYSTEMROOT"]) / "System32"), os.environ["SYSTEMROOT"]]) else: installer = ["/bin/bash", str(ROOT / "website/public/install.sh")] - cli = str(tmp_path / "bin/powercontext") + cli = str(tmp_path / "bin with spaces naïve/powercontext") system_path = os.defpath installer += ["--no-hosts", "--version", str(version)] if environment.startswith("bootstrap"): @@ -380,3 +380,131 @@ def test_latest_exact_versions_and_client_profile(tmp_path: Path) -> None: server.shutdown() worker.join(timeout=5) server.server_close() + + +@pytest.fixture +def windows_https_certificate(tmp_path: Path): + """Trust one disposable certificate in the current user's store, then remove it.""" + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.serialization.pkcs12 import load_key_and_certificates + + shell = shutil.which("powershell.exe") + assert shell + pfx, der = tmp_path / "server.pfx", tmp_path / "server.cer" + script = tmp_path / "certificate.ps1" + script.write_text( + "$ErrorActionPreference = 'Stop'\n" + "$Cert = New-SelfSignedCertificate -DnsName localhost -CertStoreLocation Cert:\\CurrentUser\\My " + "-KeyExportPolicy Exportable -Type SSLServerAuthentication\n" + "$Password = ConvertTo-SecureString 'fixture-only' -AsPlainText -Force\n" + "Export-PfxCertificate -Cert $Cert -FilePath $args[0] -Password $Password | Out-Null\n" + "Export-Certificate -Cert $Cert -FilePath $args[1] | Out-Null\n" + "Import-Certificate -FilePath $args[1] -CertStoreLocation Cert:\\CurrentUser\\Root | Out-Null\n" + "$Cert.Thumbprint\n", + encoding="utf-8", + ) + env = {key: value for key, value in os.environ.items() if key.upper() != "PSMODULEPATH"} + thumbprint = subprocess.check_output( + [shell, "-NoProfile", "-File", str(script), str(pfx), str(der)], env=env, text=True, timeout=30 + ).strip() + try: + key, certificate, _chain = load_key_and_certificates(pfx.read_bytes(), b"fixture-only") + assert key is not None and certificate is not None + cert_file, key_file = tmp_path / "server.pem", tmp_path / "key.pem" + cert_file.write_bytes(certificate.public_bytes(serialization.Encoding.PEM)) + key_file.write_bytes( + key.private_bytes( + serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption() + ) + ) + yield cert_file, key_file + finally: + # Only remove this fixture's exact certificate, never another user's trust entries. + subprocess.run( + [ + shell, + "-NoProfile", + "-Command", + "foreach ($Store in 'My','Root') { Remove-Item -LiteralPath " + f"(Join-Path ('Cert:\\CurrentUser\\' + $Store) '{thumbprint}') -ErrorAction SilentlyContinue }}", + ], + env=env, + check=True, + timeout=30, + ) + + +@pytest.mark.skipif(sys.platform != "win32", reason="requires Windows PowerShell 5.1 and its certificate store") +@pytest.mark.parametrize("route", ["direct", "https", "http"]) +def test_windows_bootstrap_requires_https(tmp_path: Path, windows_https_certificate, route: str) -> None: + import ssl + from http.server import BaseHTTPRequestHandler + + requests = [] + + class Installer(BaseHTTPRequestHandler): + def log_message(self, format: str, *args: object) -> None: # noqa: A002 + pass + + def do_GET(self): + scheme = "https" if isinstance(self.connection, ssl.SSLSocket) else "http" + requests.append((scheme, self.path)) + if self.path != "/direct": + self.send_response(302) + location = "/direct" if self.path == "/https" else f"http://localhost:{plain.server_port}/direct" + self.send_header("Location", location) + self.end_headers() + return + # Stop bootstrap after proving the downloaded installer was executed. + payload = b"Set-Content -LiteralPath $env:CONTRACT_DOWNLOAD_MARKER -Value executed\nexit 1\n" + self.send_response(200) + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + cert, key = windows_https_certificate + plain = ThreadingHTTPServer(("127.0.0.1", 0), Installer) + secure = ThreadingHTTPServer(("127.0.0.1", 0), Installer) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_cert_chain(cert, key) + secure.socket = context.wrap_socket(secure.socket, server_side=True) + workers = [Thread(target=server.serve_forever, daemon=True) for server in (plain, secure)] + env = { + key: value + for key, value in os.environ.items() + if not key.upper().startswith(("UV_", "POWERCONTEXT_")) and key.upper() != "PSMODULEPATH" + } + env.update( + PATH=os.pathsep.join((str(Path(env["SYSTEMROOT"]) / "System32"), env["SYSTEMROOT"])), + UV_INSTALL_DIR=str(tmp_path / "missing-uv"), + POWERCONTEXT_UV_INSTALLER_URL=f"https://localhost:{secure.server_port}/{route}", + CONTRACT_DOWNLOAD_MARKER=str(tmp_path / "download-executed"), + ) + shell = str(Path(env["SYSTEMROOT"]) / "System32/WindowsPowerShell/v1.0/powershell.exe") + for worker in workers: + worker.start() + try: + output = run( + [ + shell, + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + str(ROOT / "website/public/install.ps1"), + "--region", + "global", + ], + tmp_path, + env, + "https-bootstrap", + success=False, + ) + assert ("https", f"/{route}") in requests, output + assert all(scheme == "https" for scheme, _path in requests), requests + assert Path(env["CONTRACT_DOWNLOAD_MARKER"]).exists() == (route != "http"), output + finally: + for server, worker in zip((plain, secure), workers, strict=True): + server.shutdown() + server.server_close() + worker.join() diff --git a/tests/test_installation_contract.py b/tests/test_installation_contract.py index 81b54e135b..e9f25db9ef 100644 --- a/tests/test_installation_contract.py +++ b/tests/test_installation_contract.py @@ -33,7 +33,8 @@ for path in sorted((ROOT / "tests/fixtures/installation").glob("*.json")) for case in json.loads(path.read_text())["cases"] ] -CLI = """import importlib.util +CLI = """import argparse +import importlib.util import json import os from pathlib import Path @@ -41,6 +42,10 @@ def main(): args = sys.argv[1:] + grouped = os.environ.get("CONTRACT_SETUP_STYLE") == "group" + if args and args[0] == "setup" and grouped and importlib.util.find_spec("contract_server") is None: + print("Error: No such command 'setup'.", file=sys.stderr) + return 2 if args == ["--version"]: print(os.environ.get("CONTRACT_REPORTED_VERSION", "1.2.0")) elif not args or args[-1] == "--help": @@ -60,8 +65,19 @@ def main(): else: hosts.append(args[index]) index += 1 + if grouped: + parser = argparse.ArgumentParser() + parser.add_argument("--env-file") + commands = parser.add_subparsers(dest="host", required=True) + for host in ("codex", "claude"): + command = commands.add_parser(host) + command.add_argument("--source", required=True) + command.add_argument("--ref", required=True) + parser.parse_args(args[1:]) ref = args[args.index("--ref") + 1] - selected = {"hosts": hosts, "ref": ref} + result_path = Path(os.environ["CONTRACT_SETUP_RESULT"]) + previous = json.loads(result_path.read_text()) if result_path.exists() else {"hosts": []} + selected = {"hosts": previous["hosts"] + hosts, "ref": ref} if "--env-file" in args: selected["env_file"] = args[args.index("--env-file") + 1] Path(os.environ["CONTRACT_SETUP_RESULT"]).write_text(json.dumps(selected)) @@ -107,7 +123,7 @@ def executable_path(directory: Path, uv: str, windows: bool, include_git: bool) if include_git and not git: pytest.skip("explicit-host contract requires Git discovery") if not windows: - for name in ("curl", "wget", "uname", "mktemp", "rm", "dirname", "cat", "sh", "readlink"): + for name in ("curl", "wget", "uname", "mktemp", "rm", "dirname", "cat", "sh", "readlink", "realpath"): executable = shutil.which(name) if executable: (tools / name).symlink_to(executable) @@ -159,7 +175,7 @@ def test_installation_contract(case: dict[str, Any], tmp_path: Path) -> None: "TMPDIR": str(tmp_path), "PATH": path, "UV_TOOL_DIR": str(tmp_path / "tool"), - "UV_TOOL_BIN_DIR": str(tmp_path / "bin"), + "UV_TOOL_BIN_DIR": str(tmp_path / ("bin with spaces naïve" if case.get("unicode_paths") else "bin")), "UV_CACHE_DIR": str(tmp_path / "cache"), "UV_NO_CONFIG": "1", "UV_NO_INDEX": "1", @@ -168,6 +184,7 @@ def test_installation_contract(case: dict[str, Any], tmp_path: Path) -> None: "UV_OFFLINE": "1", "CONTRACT_SETUP_RESULT": str(tmp_path / "setup.json"), "CONTRACT_SETUP_EXIT": str(case.get("setup_exit", 0)), + "CONTRACT_SETUP_STYLE": case.get("setup_style", "callback"), "CONTRACT_REPORTED_VERSION": case.get("reported_version", "1.2.0"), "CONTRACT_MISSING_COMMANDS": json.dumps(case.get("missing_commands", [])), }) @@ -180,6 +197,17 @@ def test_installation_contract(case: dict[str, Any], tmp_path: Path) -> None: command = ( [shell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(script)] if windows else [shell, str(script)] ) + if windows and case.get("unicode_paths"): + wrapper = tmp_path / "invoke-installer.ps1" + wrapper.write_text( + "[Console]::OutputEncoding = [Text.Encoding]::GetEncoding(437)\n" + "$Installer = $args[0]\n$InstallerArgs = @($args | Select-Object -Skip 1)\n" + "& $Installer @InstallerArgs\n$Result = $LASTEXITCODE\n" + "if ([Console]::OutputEncoding.CodePage -ne 437) { throw 'Console encoding was not restored' }\n" + "exit $Result\n", + encoding="utf-8-sig", + ) + command = [shell, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(wrapper), str(script)] result = subprocess.run( [*command, "--region", "global", *arguments], env=environment, capture_output=True, text=True, timeout=60 ) @@ -191,7 +219,7 @@ def assert_installation_outcome(case, tmp_path, result, environment) -> None: windows = sys.platform == "win32" env_file = tmp_path / "existing configuration.env" assert result.returncode == expected["exit_code"], result.stdout + result.stderr - executable = tmp_path / "bin" / ("powercontext.exe" if windows else "powercontext") + executable = Path(environment["UV_TOOL_BIN_DIR"]) / ("powercontext.exe" if windows else "powercontext") assert executable.exists() == expected["runtime_installed"] setup = tmp_path / "setup.json" observed = json.loads(setup.read_text()) if setup.exists() else {"hosts": []} diff --git a/website/public/install.ps1 b/website/public/install.ps1 index 9bedf0f6f9..25872de9a5 100644 --- a/website/public/install.ps1 +++ b/website/public/install.ps1 @@ -43,7 +43,6 @@ Usage: powershell -ExecutionPolicy Bypass -File install.ps1 [options] -h, --help Show this help. Agent integration setup runs only for explicitly selected --host values. -After installation, run powercontext setup --ref "powercontext-v$(powercontext --version)". Configure and register a personal service separately after installation. Windows personal services remain experimental. @@ -71,8 +70,35 @@ function Assert-HttpsUrl([string]$Value, [string]$Option) { } } -function Save-Download([string]$Url, [string]$Path, [int]$Timeout = 30) { - Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec $Timeout -OutFile $Path +function Save-Download([string]$Url, [string]$Path = '', [int]$Timeout = 30, [switch]$Head) { + $Uri = [Uri]$Url + for ($Redirects = 0; $Redirects -le 5; $Redirects++) { + if ($Uri.Scheme -ne 'https' -or -not $Uri.Host -or $Uri.UserInfo) { + throw 'Downloads and redirects must use HTTPS without embedded credentials.' + } + # Windows PowerShell 5.1 follows HTTPS-to-HTTP redirects by default. + $Request = [Net.HttpWebRequest]::CreateHttp($Uri) + $Request.AllowAutoRedirect = $false + $Request.Timeout = $Timeout * 1000 + $Request.ReadWriteTimeout = $Timeout * 1000 + if ($Head) { $Request.Method = 'HEAD' } + $Response = $Request.GetResponse() + try { + if ([int]$Response.StatusCode -in @(301, 302, 303, 307, 308)) { + $Location = $Response.Headers['Location'] + if (-not $Location -or $Redirects -eq 5) { throw 'Invalid or excessive download redirects.' } + $Uri = [Uri]::new($Uri, $Location) + continue + } + if (-not $Head) { + $Output = [IO.File]::Create($Path) + try { $Response.GetResponseStream().CopyTo($Output) } + finally { $Output.Dispose() } + } + return + } + finally { $Response.Dispose() } + } } function Select-Region { @@ -123,7 +149,7 @@ function Test-UvConfiguration { function Test-DownloadUrl([string]$Url) { try { - Invoke-WebRequest -Uri $Url -UseBasicParsing -Method Head -TimeoutSec 10 | Out-Null + Save-Download $Url -Timeout 10 -Head return $true } catch { return $false } @@ -212,11 +238,13 @@ function Install-UvIfMissing { return $Uv } +$SavedOutputEncoding = [Console]::OutputEncoding $SavedEnvironment = @{} foreach ($Name in @('UV_DEFAULT_INDEX', 'UV_INSTALL_DIR', 'UV_NO_MODIFY_PATH', 'UV_DOWNLOAD_URL')) { $SavedEnvironment[$Name] = [Environment]::GetEnvironmentVariable($Name) } try { + [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false) for ($Index = 0; $Index -lt $args.Count; $Index++) { $Option = $args[$Index] switch ($Option) { @@ -306,6 +334,16 @@ try { # Older uv retains installed prereleases unless it resolves a fresh tool environment. if ($Version -eq 'latest') { $InstallArgs += @('--upgrade', '--force', '--prerelease', 'disallow') } else { $Requirement += "==$Version" } + if ($Hosts.Count -and $RuntimeProfile -eq 'client') { + # Verify the selected client package supports setup before replacing the installed tool. + $ProbeArgs = @('tool', 'run', '--isolated', '--python', $Python.Trim(), '--no-python-downloads', '--from', $Requirement) + if ($Version -eq 'latest') { $ProbeArgs += @('--prerelease', 'disallow') } + if ($IndexUrl) { $ProbeArgs += @('--default-index', $IndexUrl) } + & $Uv @ProbeArgs powercontext setup --help | Out-Null + if ($LASTEXITCODE -ne 0) { + throw 'Client integration setup preflight failed. Check uv output, use --profile local, or omit --host.' + } + } $InstallArgs += $Requirement if ($IndexUrl) { $InstallArgs += @('--default-index', $IndexUrl) } & $Uv @InstallArgs @@ -337,8 +375,10 @@ try { if ($Hosts.Count) { $SetupArgs = @('setup') if ($EnvFile) { $SetupArgs += @('--env-file', $EnvFile) } - & $Cli @SetupArgs --source oceanbase/powercontext --ref "powercontext-v$Version" @Hosts - $SetupStatus = $LASTEXITCODE + foreach ($Agent in $Hosts) { + & $Cli @SetupArgs $Agent --source oceanbase/powercontext --ref "powercontext-v$Version" + if ($LASTEXITCODE -ne 0) { $SetupStatus = $LASTEXITCODE } + } } if ($RuntimeProfile -eq 'local') { Write-Host "`nConfigure a local Server: powercontext config init --output .env" @@ -361,6 +401,7 @@ catch { exit 1 } finally { + [Console]::OutputEncoding = $SavedOutputEncoding foreach ($Name in $SavedEnvironment.Keys) { [Environment]::SetEnvironmentVariable($Name, $SavedEnvironment[$Name]) } if ($TempDir) { Remove-Item -LiteralPath $TempDir -Recurse -Force -ErrorAction SilentlyContinue } } diff --git a/website/public/install.sh b/website/public/install.sh index c8987c55ef..db15a4308a 100644 --- a/website/public/install.sh +++ b/website/public/install.sh @@ -57,7 +57,7 @@ Usage: bash install.sh [--version VERSION] [--index-url URL] [--host HOST]... [- -h, --help Show this help. Agent integration setup runs only for explicitly selected --host values. -After installation, run powercontext setup --ref "powercontext-v$(powercontext --version)". +After installation, run powercontext setup HOST --ref "powercontext-v$(powercontext --version)" when supported. Configure and register a personal service separately after installation. Region priority: --region, POWERCONTEXT_INSTALL_REGION, named timezone, locale @@ -331,6 +331,14 @@ main() { else requirement="$requirement==$VERSION" fi + if [[ ${#HOSTS[@]} -gt 0 && "$PROFILE" == client ]]; then + # Some published client-only releases do not register setup at all. + local probe_args=(tool run --isolated --python "$PYTHON_BIN" --no-python-downloads --from "$requirement") + [[ "$VERSION" != latest ]] || probe_args+=(--prerelease disallow) + [[ -z "$INDEX_URL" ]] || probe_args+=(--default-index "$INDEX_URL") + "$UV_BIN" "${probe_args[@]}" powercontext setup --help >/dev/null || + fail "Client integration setup preflight failed. Check uv output; use --profile local for releases without setup or omit --host." + fi install_args+=("$requirement") [[ -z "$INDEX_URL" ]] || install_args+=(--default-index "$INDEX_URL") printf 'Installing PowerContext %s.\n' "$VERSION" @@ -364,8 +372,12 @@ main() { if [[ ${#HOSTS[@]} -gt 0 ]]; then local setup_args=(setup) [[ -z "$ENV_FILE" ]] || setup_args+=(--env-file "$ENV_FILE") - "$tool_bin/powercontext" "${setup_args[@]}" --source oceanbase/powercontext \ - --ref "powercontext-v$VERSION" ${HOSTS[@]+"${HOSTS[@]}"} || setup_status=$? + local agent + for agent in "${HOSTS[@]}"; do + # Host-first options work with both released subcommands and the unified callback. + "$tool_bin/powercontext" "${setup_args[@]}" "$agent" --source oceanbase/powercontext \ + --ref "powercontext-v$VERSION" || setup_status=$? + done fi if [[ "$PROFILE" == local ]]; then printf '\nConfigure a local Server: powercontext config init --output .env\n' From ff52d414405104b9bcbd4134634b17b9a96504be Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Tue, 3 Nov 2026 11:15:40 +0800 Subject: [PATCH 22/24] docs(install): keep script guidance focused on current usage --- website/public/install.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/website/public/install.sh b/website/public/install.sh index db15a4308a..e8886674ed 100644 --- a/website/public/install.sh +++ b/website/public/install.sh @@ -57,7 +57,6 @@ Usage: bash install.sh [--version VERSION] [--index-url URL] [--host HOST]... [- -h, --help Show this help. Agent integration setup runs only for explicitly selected --host values. -After installation, run powercontext setup HOST --ref "powercontext-v$(powercontext --version)" when supported. Configure and register a personal service separately after installation. Region priority: --region, POWERCONTEXT_INSTALL_REGION, named timezone, locale @@ -332,12 +331,12 @@ main() { requirement="$requirement==$VERSION" fi if [[ ${#HOSTS[@]} -gt 0 && "$PROFILE" == client ]]; then - # Some published client-only releases do not register setup at all. + # Verify the selected client package supports setup before replacing the installed tool. local probe_args=(tool run --isolated --python "$PYTHON_BIN" --no-python-downloads --from "$requirement") [[ "$VERSION" != latest ]] || probe_args+=(--prerelease disallow) [[ -z "$INDEX_URL" ]] || probe_args+=(--default-index "$INDEX_URL") "$UV_BIN" "${probe_args[@]}" powercontext setup --help >/dev/null || - fail "Client integration setup preflight failed. Check uv output; use --profile local for releases without setup or omit --host." + fail "Client integration setup preflight failed. Check uv output, use --profile local, or omit --host." fi install_args+=("$requirement") [[ -z "$INDEX_URL" ]] || install_args+=(--default-index "$INDEX_URL") @@ -374,7 +373,6 @@ main() { [[ -z "$ENV_FILE" ]] || setup_args+=(--env-file "$ENV_FILE") local agent for agent in "${HOSTS[@]}"; do - # Host-first options work with both released subcommands and the unified callback. "$tool_bin/powercontext" "${setup_args[@]}" "$agent" --source oceanbase/powercontext \ --ref "powercontext-v$VERSION" || setup_status=$? done From 5556a0aaeb5b0c282ceb4d142b03fad1be6046c3 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Tue, 3 Nov 2026 14:10:55 +0800 Subject: [PATCH 23/24] fix(ci): avoid certificate trust prompts in Windows installation tests --- tests/native/test_installation.py | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/tests/native/test_installation.py b/tests/native/test_installation.py index 7459fac216..fb454b0bd2 100644 --- a/tests/native/test_installation.py +++ b/tests/native/test_installation.py @@ -384,7 +384,7 @@ def test_latest_exact_versions_and_client_profile(tmp_path: Path) -> None: @pytest.fixture def windows_https_certificate(tmp_path: Path): - """Trust one disposable certificate in the current user's store, then remove it.""" + """Trust one disposable certificate on the elevated CI runner, then remove it.""" from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.serialization.pkcs12 import load_key_and_certificates @@ -394,18 +394,38 @@ def windows_https_certificate(tmp_path: Path): script = tmp_path / "certificate.ps1" script.write_text( "$ErrorActionPreference = 'Stop'\n" + "Add-Content -LiteralPath $args[2] -Value 'Creating fixture certificate.'\n" "$Cert = New-SelfSignedCertificate -DnsName localhost -CertStoreLocation Cert:\\CurrentUser\\My " "-KeyExportPolicy Exportable -Type SSLServerAuthentication\n" + "Add-Content -LiteralPath $args[2] -Value 'Exporting fixture certificate.'\n" "$Password = ConvertTo-SecureString 'fixture-only' -AsPlainText -Force\n" "Export-PfxCertificate -Cert $Cert -FilePath $args[0] -Password $Password | Out-Null\n" "Export-Certificate -Cert $Cert -FilePath $args[1] | Out-Null\n" - "Import-Certificate -FilePath $args[1] -CertStoreLocation Cert:\\CurrentUser\\Root | Out-Null\n" + # CurrentUser\Root can display a CryptoAPI confirmation dialog in unattended runs. + "Add-Content -LiteralPath $args[2] -Value 'Trusting fixture certificate in LocalMachine Root.'\n" + "Import-Certificate -FilePath $args[1] -CertStoreLocation Cert:\\LocalMachine\\Root | Out-Null\n" + "Add-Content -LiteralPath $args[2] -Value 'Fixture certificate trusted.'\n" "$Cert.Thumbprint\n", encoding="utf-8", ) env = {key: value for key, value in os.environ.items() if key.upper() != "PSMODULEPATH"} thumbprint = subprocess.check_output( - [shell, "-NoProfile", "-File", str(script), str(pfx), str(der)], env=env, text=True, timeout=30 + [ + shell, + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-File", + str(script), + str(pfx), + str(der), + str(tmp_path / "certificate.log"), + ], + env=env, + stdin=subprocess.DEVNULL, + text=True, + timeout=30, ).strip() try: key, certificate, _chain = load_key_and_certificates(pfx.read_bytes(), b"fixture-only") @@ -424,9 +444,10 @@ def windows_https_certificate(tmp_path: Path): [ shell, "-NoProfile", + "-NonInteractive", "-Command", - "foreach ($Store in 'My','Root') { Remove-Item -LiteralPath " - f"(Join-Path ('Cert:\\CurrentUser\\' + $Store) '{thumbprint}') -ErrorAction SilentlyContinue }}", + "foreach ($Store in 'Cert:\\CurrentUser\\My','Cert:\\LocalMachine\\Root') { Remove-Item -LiteralPath " + f"(Join-Path $Store '{thumbprint}') -ErrorAction SilentlyContinue }}", ], env=env, check=True, From bf1f308da49461c6e6457c2fb7617e92f12d1395 Mon Sep 17 00:00:00 2001 From: Chojan Shang Date: Wed, 4 Nov 2026 10:30:15 +0800 Subject: [PATCH 24/24] fix(test): isolate installer certificate configuration --- tests/native/test_install_sources.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/native/test_install_sources.py b/tests/native/test_install_sources.py index 1ab71a9e09..f452fd4332 100644 --- a/tests/native/test_install_sources.py +++ b/tests/native/test_install_sources.py @@ -62,6 +62,17 @@ def tls_certificate(tmp_path): if not openssl: pytest.skip("HTTPS download acceptance requires openssl") cert, key = tmp_path / "cert.pem", tmp_path / "key.pem" + config = tmp_path / "openssl.cnf" + config.write_text( + "[req]\n" + "distinguished_name = subject\n" + "x509_extensions = server\n" + "[subject]\n" + "[server]\n" + "subjectAltName = DNS:localhost\n" + "basicConstraints = critical,CA:FALSE\n", + encoding="utf-8", + ) subprocess.run( [ openssl, @@ -78,10 +89,8 @@ def tls_certificate(tmp_path): "1", "-subj", "/CN=localhost", - "-addext", - "subjectAltName=DNS:localhost", - "-addext", - "basicConstraints=critical,CA:FALSE", + "-config", + str(config), ], check=True, capture_output=True,