fix: make ai runtime npm installs idempotent

This commit is contained in:
Haitao Pan 2026-06-16 15:04:14 +08:00
parent 7936f65485
commit 5630df788a
4 changed files with 140 additions and 2 deletions

View File

@ -16,9 +16,20 @@ role entrypoint. The role installs:
Design constraints:
- system packages are the primary source of truth
- global npm packages are managed through
`/usr/local/sbin/ai-workspace-manage-npm-global-package` so repeated installs
are idempotent and stale global bin links can be overwritten safely
- Playwright uses the resolved system browser instead of downloading browsers
- Chinese PDF rendering is treated as a runtime requirement, not an optional add-on
Global npm package actions:
- `install` is the default and only changes the host when a package is missing
or an exact pinned version differs
- `reinstall` forces the configured package set back into place
- `upgrade`, `backup`, `restore`, and `migrate` are reserved action entrypoints
for future runtime lifecycle workflows
Default Playwright environment:
- `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1`

View File

@ -17,6 +17,7 @@ ai_agent_runtime_npm_global_packages:
- "@google/gemini-cli"
- "@openai/codex"
- "@anthropic-ai/claude-code"
ai_agent_runtime_npm_global_package_action: install
ai_agent_runtime_agent_cli_commands:
- opencode
- gemini

View File

@ -0,0 +1,113 @@
#!/usr/bin/env bash
set -euo pipefail
action="${1:-install}"
package_spec="${2:-}"
if [ -z "${package_spec}" ]; then
echo "Usage: $0 <install|reinstall|upgrade|backup|restore|migrate> <npm-package-spec>" >&2
exit 2
fi
package_name() {
local spec="$1"
if [[ "${spec}" == @* ]]; then
local rest="${spec#@}"
local scope="${rest%%/*}"
local after_scope="${rest#*/}"
local name="${after_scope%%@*}"
printf '@%s/%s\n' "${scope}" "${name}"
else
printf '%s\n' "${spec%%@*}"
fi
}
desired_version() {
local spec="$1"
if [[ "${spec}" == @* ]]; then
local rest="${spec#@}"
local after_scope="${rest#*/}"
if [[ "${after_scope}" == *"@"* ]]; then
printf '%s\n' "${after_scope#*@}"
fi
elif [[ "${spec}" == *"@"* ]]; then
printf '%s\n' "${spec#*@}"
fi
}
installed_version() {
local name="$1"
local npm_root
npm_root="$(npm root -g)"
node -e '
const fs = require("fs");
const path = require("path");
const pkg = process.argv[1];
const root = process.argv[2];
const packageJson = path.join(root, ...pkg.split("/"), "package.json");
if (!fs.existsSync(packageJson)) process.exit(1);
const parsed = JSON.parse(fs.readFileSync(packageJson, "utf8"));
process.stdout.write(parsed.version || "");
' "${name}" "${npm_root}"
}
is_installed() {
local name="$1"
local want="${2:-}"
local have
have="$(installed_version "${name}" 2>/dev/null || true)"
[ -n "${have}" ] || return 1
[ -z "${want}" ] || [ "${have}" = "${want}" ]
}
install_package() {
local spec="$1"
local name want
name="$(package_name "${spec}")"
want="$(desired_version "${spec}")"
if is_installed "${name}" "${want}"; then
echo "changed=0 action=install package=${spec}"
return
fi
npm install -g --force "${spec}"
echo "changed=1 action=install package=${spec}"
}
reinstall_package() {
local spec="$1"
npm install -g --force "${spec}"
echo "changed=1 action=reinstall package=${spec}"
}
upgrade_package() {
local spec="$1"
npm install -g --force "${spec}"
echo "changed=1 action=upgrade package=${spec}"
}
backup_package() {
echo "changed=0 action=backup package=${1} status=reserved"
}
restore_package() {
echo "changed=0 action=restore package=${1} status=reserved"
}
migrate_package() {
echo "changed=0 action=migrate package=${1} status=reserved"
}
case "${action}" in
install) install_package "${package_spec}" ;;
reinstall) reinstall_package "${package_spec}" ;;
upgrade) upgrade_package "${package_spec}" ;;
backup) backup_package "${package_spec}" ;;
restore) restore_package "${package_spec}" ;;
migrate) migrate_package "${package_spec}" ;;
*)
echo "Unsupported npm package action: ${action}" >&2
exit 2
;;
esac

View File

@ -7,15 +7,28 @@
install_yarn: "{{ ai_agent_runtime_install_yarn }}"
yarn_version: "{{ ai_agent_runtime_yarn_version }}"
- name: Install npm global package manager helper
ansible.builtin.copy:
src: manage_npm_global_package.sh
dest: /usr/local/sbin/ai-workspace-manage-npm-global-package
owner: root
group: root
mode: "0755"
become: true
- name: Install global npm packages for AI runtime
ansible.builtin.command:
cmd: "npm install -g {{ item }}"
cmd: "/usr/local/sbin/ai-workspace-manage-npm-global-package {{ ai_agent_runtime_npm_global_package_action }} {{ item }}"
loop: "{{ ai_agent_runtime_npm_global_packages }}"
register: ai_agent_runtime_npm_global_install
changed_when: "'changed=1' in ai_agent_runtime_npm_global_install.stdout"
when: ai_agent_runtime_npm_global_packages | length > 0
- name: Install pinned Playwright package for AI runtime
ansible.builtin.command:
cmd: "npm install -g playwright@{{ ai_agent_runtime_playwright_version }}"
cmd: "/usr/local/sbin/ai-workspace-manage-npm-global-package {{ ai_agent_runtime_npm_global_package_action }} playwright@{{ ai_agent_runtime_playwright_version }}"
register: ai_agent_runtime_playwright_install
changed_when: "'changed=1' in ai_agent_runtime_playwright_install.stdout"
when:
- ai_agent_runtime_playwright_enabled | bool
- ai_agent_runtime_playwright_version | length > 0