fix: make ai runtime npm installs idempotent
This commit is contained in:
parent
7936f65485
commit
5630df788a
@ -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`
|
||||
|
||||
@ -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
|
||||
|
||||
113
roles/ai_agent_runtime/files/manage_npm_global_package.sh
Normal file
113
roles/ai_agent_runtime/files/manage_npm_global_package.sh
Normal 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
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user