mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix: handle npm min-release-age in installers
Replays #84749 because the contributor fork branch became conflicted and was no longer maintainer-writable. Co-authored-by: TeodoroRodrigo <rodrigoteodoro.90@gmail.com>
This commit is contained in:
committed by
GitHub
parent
6704d0ab27
commit
316d97c938
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Build: include `ui:build` in the `full` and `ciArtifacts` profiles of `scripts/build-all.mjs` so `pnpm build` always rebuilds `dist/control-ui` after `tsdown` cleans `dist`, removing the second-command requirement and the missing-asset failure mode for source/runtime installs and CI artifact uploads. (#85206)
|
||||
### Fixes
|
||||
|
||||
- Install/update: bypass npm `min-release-age` policies with `--min-release-age=0` instead of `--before` so hosted installers keep working on npm versions that reject the combined config. (#84749) Thanks @TeodoroRodrigo.
|
||||
- WebChat: keep message-tool replies visible in the chat while still summarizing internal tool results for the model. Fixes #86347. Thanks @shakkernerd.
|
||||
- Agents/commitments: serialize commitment store load-modify-save writes so concurrent heartbeat and CLI updates no longer lose dismissal, sent, or attempt state. (#81153) Thanks @ai-hpc.
|
||||
- CLI: suppress benign self-update version-skew warnings during package post-update finalization.
|
||||
|
||||
+48
-41
@@ -857,25 +857,27 @@ fix_npm_prefix_if_needed() {
|
||||
log "Configured npm prefix to ${target}"
|
||||
}
|
||||
|
||||
expand_npm_config_path() {
|
||||
local path="$1"
|
||||
if [[ -z "$path" ]]; then
|
||||
resolve_npm_config_path() {
|
||||
local raw="$1"
|
||||
if [[ -z "$raw" || "$raw" == "null" || "$raw" == "undefined" ]]; then
|
||||
return 1
|
||||
fi
|
||||
case "$path" in
|
||||
"\${HOME}/"*) path="${HOME:-}/${path#\$\{HOME\}/}" ;;
|
||||
"\$HOME/"*) path="${HOME:-}/${path#\$HOME/}" ;;
|
||||
[~]/*) path="${HOME:-}/${path#\~/}" ;;
|
||||
esac
|
||||
printf '%s\n' "$path"
|
||||
if [[ "$raw" == \~/* && -n "${HOME:-}" ]]; then
|
||||
printf '%s\n' "${HOME}/${raw#"~/"}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$raw" == "\${HOME}/"* && -n "${HOME:-}" ]]; then
|
||||
printf '%s\n' "${HOME}/${raw#"\${HOME}/"}"
|
||||
return 0
|
||||
fi
|
||||
printf '%s\n' "$raw"
|
||||
}
|
||||
|
||||
npm_config_file_has_key() {
|
||||
local file
|
||||
file="$(expand_npm_config_path "$1")" || return 1
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
[[ -f "$file" ]] || return 1
|
||||
grep -E "^[[:space:]]*${key}[[:space:]]*=" "$file" >/dev/null 2>&1
|
||||
grep -Eiq "^[[:space:]]*${key}[[:space:]]*=" "$file"
|
||||
}
|
||||
|
||||
npm_command_path() {
|
||||
@@ -899,36 +901,39 @@ npm_builtin_config_path() {
|
||||
printf '%s\n' "${npm_root}/npmrc"
|
||||
}
|
||||
|
||||
npm_raw_config_has_key() {
|
||||
local key="$1"
|
||||
local npm_cmd="${2:-npm}"
|
||||
local user_config="${NPM_CONFIG_USERCONFIG:-${npm_config_userconfig:-}}"
|
||||
local global_config="${NPM_CONFIG_GLOBALCONFIG:-${npm_config_globalconfig:-}}"
|
||||
local prefix="${NPM_CONFIG_PREFIX:-${npm_config_prefix:-}}"
|
||||
npm_config_has_raw_key() {
|
||||
local npm_cmd="$1"
|
||||
local key="$2"
|
||||
local raw=""
|
||||
local file=""
|
||||
local -a files=()
|
||||
|
||||
npm_config_file_has_key ".npmrc" "$key" && return 0
|
||||
if [[ -n "$user_config" ]]; then
|
||||
npm_config_file_has_key "$user_config" "$key" && return 0
|
||||
raw="${NPM_CONFIG_USERCONFIG:-${npm_config_userconfig:-}}"
|
||||
if [[ -n "$raw" ]]; then
|
||||
file="$(resolve_npm_config_path "$raw" 2>/dev/null || true)"
|
||||
[[ -n "$file" ]] && files+=("$file")
|
||||
elif [[ -n "${HOME:-}" ]]; then
|
||||
npm_config_file_has_key "${HOME}/.npmrc" "$key" && return 0
|
||||
files+=("${HOME}/.npmrc")
|
||||
fi
|
||||
if [[ -n "$global_config" ]]; then
|
||||
npm_config_file_has_key "$global_config" "$key" && return 0
|
||||
else
|
||||
local resolved_global_config=""
|
||||
resolved_global_config="$(env -u NPM_CONFIG_BEFORE -u npm_config_before "$npm_cmd" config get globalconfig 2>/dev/null || true)"
|
||||
if [[ -n "$resolved_global_config" && "$resolved_global_config" != "null" && "$resolved_global_config" != "undefined" ]]; then
|
||||
npm_config_file_has_key "$resolved_global_config" "$key" && return 0
|
||||
|
||||
raw="${NPM_CONFIG_GLOBALCONFIG:-${npm_config_globalconfig:-}}"
|
||||
if [[ -n "$raw" ]]; then
|
||||
file="$(resolve_npm_config_path "$raw" 2>/dev/null || true)"
|
||||
[[ -n "$file" ]] && files+=("$file")
|
||||
fi
|
||||
|
||||
raw="$(env -u NPM_CONFIG_BEFORE -u npm_config_before -u NPM_CONFIG_MIN_RELEASE_AGE -u npm_config_min_release_age -u npm_config_min-release-age "$npm_cmd" config get globalconfig --global 2>/dev/null || true)"
|
||||
file="$(resolve_npm_config_path "$raw" 2>/dev/null || true)"
|
||||
[[ -n "$file" ]] && files+=("$file")
|
||||
|
||||
file="$(npm_builtin_config_path "$npm_cmd" 2>/dev/null || true)"
|
||||
[[ -n "$file" ]] && files+=("$file")
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if npm_config_file_has_key "$file" "$key"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$prefix" ]]; then
|
||||
npm_config_file_has_key "${prefix}/etc/npmrc" "$key" && return 0
|
||||
fi
|
||||
local builtin_config=""
|
||||
builtin_config="$(npm_builtin_config_path "$npm_cmd" 2>/dev/null || true)"
|
||||
if [[ -n "$builtin_config" ]]; then
|
||||
npm_config_file_has_key "$builtin_config" "$key" && return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -939,10 +944,12 @@ install_openclaw() {
|
||||
fi
|
||||
local freshness_flag="--min-release-age=0"
|
||||
local min_release_age=""
|
||||
min_release_age="$(env -u NPM_CONFIG_BEFORE -u npm_config_before "$(npm_bin)" config get min-release-age 2>/dev/null || true)"
|
||||
if ! npm_raw_config_has_key "min-release-age" "$(npm_bin)" && [[ -z "$min_release_age" || "$min_release_age" == "null" || "$min_release_age" == "undefined" ]]; then
|
||||
min_release_age="$(env -u NPM_CONFIG_BEFORE -u npm_config_before "$(npm_bin)" config get min-release-age --global 2>/dev/null || true)"
|
||||
if npm_config_has_raw_key "$(npm_bin)" "min-release-age"; then
|
||||
freshness_flag="--min-release-age=0"
|
||||
elif [[ -z "$min_release_age" || "$min_release_age" == "null" || "$min_release_age" == "undefined" ]]; then
|
||||
local before_value=""
|
||||
before_value="$(env -u NPM_CONFIG_MIN_RELEASE_AGE -u npm_config_min_release_age -u npm_config_min-release-age "$(npm_bin)" config get before 2>/dev/null || true)"
|
||||
before_value="$(env -u NPM_CONFIG_MIN_RELEASE_AGE -u npm_config_min_release_age -u npm_config_min-release-age "$(npm_bin)" config get before --global 2>/dev/null || true)"
|
||||
if [[ -n "$before_value" && "$before_value" != "null" && "$before_value" != "undefined" ]]; then
|
||||
freshness_flag="--before=$(date -u '+%Y-%m-%dT%H:%M:%S.000Z')"
|
||||
fi
|
||||
|
||||
+63
-3
@@ -930,6 +930,63 @@ function Test-OpenClawSourcePackageInstallSpec {
|
||||
)
|
||||
}
|
||||
|
||||
function Resolve-NpmConfigPath {
|
||||
param([string]$RawPath)
|
||||
if ([string]::IsNullOrWhiteSpace($RawPath) -or $RawPath -eq "null" -or $RawPath -eq "undefined") {
|
||||
return $null
|
||||
}
|
||||
if (($RawPath.StartsWith("~/") -or $RawPath.StartsWith("~\")) -and -not [string]::IsNullOrWhiteSpace($HOME)) {
|
||||
return (Join-Path $HOME $RawPath.Substring(2))
|
||||
}
|
||||
if (($RawPath.StartsWith('${HOME}/') -or $RawPath.StartsWith('${HOME}\')) -and -not [string]::IsNullOrWhiteSpace($HOME)) {
|
||||
return (Join-Path $HOME $RawPath.Substring(8))
|
||||
}
|
||||
return $RawPath
|
||||
}
|
||||
|
||||
function Test-NpmConfigFileKey {
|
||||
param(
|
||||
[string]$Path,
|
||||
[string]$Key
|
||||
)
|
||||
if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
return $false
|
||||
}
|
||||
$escapedKey = [regex]::Escape($Key)
|
||||
return [bool](Select-String -LiteralPath $Path -Pattern "^\s*$escapedKey\s*=" -Quiet)
|
||||
}
|
||||
|
||||
function Test-NpmConfigRawKey {
|
||||
param([string]$Key)
|
||||
$files = New-Object System.Collections.Generic.List[string]
|
||||
$userConfig = if ($env:NPM_CONFIG_USERCONFIG) { $env:NPM_CONFIG_USERCONFIG } else { $env:npm_config_userconfig }
|
||||
if ($userConfig) {
|
||||
$resolvedUserConfig = Resolve-NpmConfigPath $userConfig
|
||||
if ($resolvedUserConfig) { $files.Add($resolvedUserConfig) }
|
||||
} elseif (-not [string]::IsNullOrWhiteSpace($HOME)) {
|
||||
$files.Add((Join-Path $HOME ".npmrc"))
|
||||
}
|
||||
|
||||
$globalConfig = if ($env:NPM_CONFIG_GLOBALCONFIG) { $env:NPM_CONFIG_GLOBALCONFIG } else { $env:npm_config_globalconfig }
|
||||
if ($globalConfig) {
|
||||
$resolvedGlobalConfig = Resolve-NpmConfigPath $globalConfig
|
||||
if ($resolvedGlobalConfig) { $files.Add($resolvedGlobalConfig) }
|
||||
}
|
||||
|
||||
$detectedGlobalConfig = (Invoke-NpmCommand -Arguments @("config", "get", "globalconfig", "--global") 2>$null)
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$resolvedDetectedGlobalConfig = Resolve-NpmConfigPath $detectedGlobalConfig
|
||||
if ($resolvedDetectedGlobalConfig) { $files.Add($resolvedDetectedGlobalConfig) }
|
||||
}
|
||||
|
||||
foreach ($file in ($files | Select-Object -Unique)) {
|
||||
if (Test-NpmConfigFileKey -Path $file -Key $Key) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Install-OpenClaw {
|
||||
if ([string]::IsNullOrWhiteSpace($Tag)) {
|
||||
$Tag = "latest"
|
||||
@@ -951,9 +1008,12 @@ function Install-OpenClaw {
|
||||
$installSpec = Resolve-NpmOpenClawInstallSpec -PackageName $packageName -RequestedTag $Tag
|
||||
Write-Host "[*] Installing OpenClaw ($installSpec)..." -ForegroundColor Yellow
|
||||
$freshnessArgs = @("--min-release-age=0")
|
||||
$minReleaseAge = (Invoke-NpmCommand -Arguments @("config", "get", "min-release-age") 2>$null)
|
||||
if ($LASTEXITCODE -ne 0 -or -not $minReleaseAge -or $minReleaseAge.Trim() -eq "null" -or $minReleaseAge.Trim() -eq "undefined") {
|
||||
$beforeValue = (Invoke-NpmCommand -Arguments @("config", "get", "before") 2>$null)
|
||||
$minReleaseAge = (Invoke-NpmCommand -Arguments @("config", "get", "min-release-age", "--global") 2>$null)
|
||||
$minReleaseAgeStatus = $LASTEXITCODE
|
||||
if (Test-NpmConfigRawKey -Key "min-release-age") {
|
||||
$freshnessArgs = @("--min-release-age=0")
|
||||
} elseif ($minReleaseAgeStatus -ne 0 -or -not $minReleaseAge -or $minReleaseAge.Trim() -eq "null" -or $minReleaseAge.Trim() -eq "undefined") {
|
||||
$beforeValue = (Invoke-NpmCommand -Arguments @("config", "get", "before", "--global") 2>$null)
|
||||
if ($LASTEXITCODE -eq 0 -and $beforeValue -and $beforeValue.Trim() -ne "null" -and $beforeValue.Trim() -ne "undefined") {
|
||||
$freshnessArgs = @("--before=$((Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"))")
|
||||
}
|
||||
|
||||
+48
-41
@@ -744,25 +744,27 @@ auto_install_build_tools_for_npm_failure() {
|
||||
return 0
|
||||
}
|
||||
|
||||
expand_npm_config_path() {
|
||||
local path="$1"
|
||||
if [[ -z "$path" ]]; then
|
||||
resolve_npm_config_path() {
|
||||
local raw="$1"
|
||||
if [[ -z "$raw" || "$raw" == "null" || "$raw" == "undefined" ]]; then
|
||||
return 1
|
||||
fi
|
||||
case "$path" in
|
||||
"\${HOME}/"*) path="${HOME:-}/${path#\$\{HOME\}/}" ;;
|
||||
"\$HOME/"*) path="${HOME:-}/${path#\$HOME/}" ;;
|
||||
[~]/*) path="${HOME:-}/${path#\~/}" ;;
|
||||
esac
|
||||
printf '%s\n' "$path"
|
||||
if [[ "$raw" == \~/* && -n "${HOME:-}" ]]; then
|
||||
printf '%s\n' "${HOME}/${raw#"~/"}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$raw" == "\${HOME}/"* && -n "${HOME:-}" ]]; then
|
||||
printf '%s\n' "${HOME}/${raw#"\${HOME}/"}"
|
||||
return 0
|
||||
fi
|
||||
printf '%s\n' "$raw"
|
||||
}
|
||||
|
||||
npm_config_file_has_key() {
|
||||
local file
|
||||
file="$(expand_npm_config_path "$1")" || return 1
|
||||
local file="$1"
|
||||
local key="$2"
|
||||
[[ -f "$file" ]] || return 1
|
||||
grep -E "^[[:space:]]*${key}[[:space:]]*=" "$file" >/dev/null 2>&1
|
||||
grep -Eiq "^[[:space:]]*${key}[[:space:]]*=" "$file"
|
||||
}
|
||||
|
||||
npm_command_path() {
|
||||
@@ -786,36 +788,39 @@ npm_builtin_config_path() {
|
||||
printf '%s\n' "${npm_root}/npmrc"
|
||||
}
|
||||
|
||||
npm_raw_config_has_key() {
|
||||
local key="$1"
|
||||
local npm_cmd="${2:-npm}"
|
||||
local user_config="${NPM_CONFIG_USERCONFIG:-${npm_config_userconfig:-}}"
|
||||
local global_config="${NPM_CONFIG_GLOBALCONFIG:-${npm_config_globalconfig:-}}"
|
||||
local prefix="${NPM_CONFIG_PREFIX:-${npm_config_prefix:-}}"
|
||||
npm_config_has_raw_key() {
|
||||
local npm_cmd="$1"
|
||||
local key="$2"
|
||||
local raw=""
|
||||
local file=""
|
||||
local -a files=()
|
||||
|
||||
npm_config_file_has_key ".npmrc" "$key" && return 0
|
||||
if [[ -n "$user_config" ]]; then
|
||||
npm_config_file_has_key "$user_config" "$key" && return 0
|
||||
raw="${NPM_CONFIG_USERCONFIG:-${npm_config_userconfig:-}}"
|
||||
if [[ -n "$raw" ]]; then
|
||||
file="$(resolve_npm_config_path "$raw" 2>/dev/null || true)"
|
||||
[[ -n "$file" ]] && files+=("$file")
|
||||
elif [[ -n "${HOME:-}" ]]; then
|
||||
npm_config_file_has_key "${HOME}/.npmrc" "$key" && return 0
|
||||
files+=("${HOME}/.npmrc")
|
||||
fi
|
||||
if [[ -n "$global_config" ]]; then
|
||||
npm_config_file_has_key "$global_config" "$key" && return 0
|
||||
else
|
||||
local resolved_global_config=""
|
||||
resolved_global_config="$(env -u NPM_CONFIG_BEFORE -u npm_config_before "$npm_cmd" config get globalconfig 2>/dev/null || true)"
|
||||
if [[ -n "$resolved_global_config" && "$resolved_global_config" != "null" && "$resolved_global_config" != "undefined" ]]; then
|
||||
npm_config_file_has_key "$resolved_global_config" "$key" && return 0
|
||||
|
||||
raw="${NPM_CONFIG_GLOBALCONFIG:-${npm_config_globalconfig:-}}"
|
||||
if [[ -n "$raw" ]]; then
|
||||
file="$(resolve_npm_config_path "$raw" 2>/dev/null || true)"
|
||||
[[ -n "$file" ]] && files+=("$file")
|
||||
fi
|
||||
|
||||
raw="$(env -u NPM_CONFIG_BEFORE -u npm_config_before -u NPM_CONFIG_MIN_RELEASE_AGE -u npm_config_min_release_age -u npm_config_min-release-age "$npm_cmd" config get globalconfig --global 2>/dev/null || true)"
|
||||
file="$(resolve_npm_config_path "$raw" 2>/dev/null || true)"
|
||||
[[ -n "$file" ]] && files+=("$file")
|
||||
|
||||
file="$(npm_builtin_config_path "$npm_cmd" 2>/dev/null || true)"
|
||||
[[ -n "$file" ]] && files+=("$file")
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if npm_config_file_has_key "$file" "$key"; then
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
if [[ -n "$prefix" ]]; then
|
||||
npm_config_file_has_key "${prefix}/etc/npmrc" "$key" && return 0
|
||||
fi
|
||||
local builtin_config=""
|
||||
builtin_config="$(npm_builtin_config_path "$npm_cmd" 2>/dev/null || true)"
|
||||
if [[ -n "$builtin_config" ]]; then
|
||||
npm_config_file_has_key "$builtin_config" "$key" && return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
@@ -825,10 +830,12 @@ run_npm_global_install() {
|
||||
|
||||
local freshness_flag="--min-release-age=0"
|
||||
local min_release_age=""
|
||||
min_release_age="$(env -u NPM_CONFIG_BEFORE -u npm_config_before npm config get min-release-age 2>/dev/null || true)"
|
||||
if ! npm_raw_config_has_key "min-release-age" "npm" && [[ -z "$min_release_age" || "$min_release_age" == "null" || "$min_release_age" == "undefined" ]]; then
|
||||
min_release_age="$(env -u NPM_CONFIG_BEFORE -u npm_config_before npm config get min-release-age --global 2>/dev/null || true)"
|
||||
if npm_config_has_raw_key npm "min-release-age"; then
|
||||
freshness_flag="--min-release-age=0"
|
||||
elif [[ -z "$min_release_age" || "$min_release_age" == "null" || "$min_release_age" == "undefined" ]]; then
|
||||
local before_value=""
|
||||
before_value="$(env -u NPM_CONFIG_MIN_RELEASE_AGE -u npm_config_min_release_age -u npm_config_min-release-age npm config get before 2>/dev/null || true)"
|
||||
before_value="$(env -u NPM_CONFIG_MIN_RELEASE_AGE -u npm_config_min_release_age -u npm_config_min-release-age npm config get before --global 2>/dev/null || true)"
|
||||
if [[ -n "$before_value" && "$before_value" != "null" && "$before_value" != "undefined" ]]; then
|
||||
freshness_flag="--before=$(date -u '+%Y-%m-%dT%H:%M:%S.000Z')"
|
||||
fi
|
||||
|
||||
@@ -41,6 +41,17 @@ function expectUnsetOrZeroNpmJsonConfig(value: unknown): void {
|
||||
expect(value == null || value === false || value === 0 || value === "0").toBe(true);
|
||||
}
|
||||
|
||||
function createIsolatedNpmConfigEnv(dir: string): NodeJS.ProcessEnv {
|
||||
const home = path.join(dir, "home");
|
||||
const globalconfig = path.join(dir, "global-npmrc");
|
||||
fsSync.mkdirSync(home, { recursive: true });
|
||||
fsSync.writeFileSync(globalconfig, "", "utf-8");
|
||||
return {
|
||||
HOME: home,
|
||||
NPM_CONFIG_GLOBALCONFIG: globalconfig,
|
||||
};
|
||||
}
|
||||
|
||||
describe("npm project install env", () => {
|
||||
it("uses an absolute POSIX script shell for npm lifecycle scripts", () => {
|
||||
withMockedPlatform("linux", () => {
|
||||
@@ -163,10 +174,12 @@ describe("npm project install env", () => {
|
||||
it("uses a current before override for explicit npm before policy", () => {
|
||||
const dir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-npmrc-"));
|
||||
try {
|
||||
const baseEnv = createIsolatedNpmConfigEnv(dir);
|
||||
const npmrc = path.join(dir, "npmrc");
|
||||
fsSync.writeFileSync(npmrc, "before=2026-01-01T00:00:00.000Z\n", "utf-8");
|
||||
const env = createNpmProjectInstallEnv(
|
||||
{
|
||||
...baseEnv,
|
||||
NPM_CONFIG_USERCONFIG: npmrc,
|
||||
},
|
||||
{},
|
||||
@@ -180,6 +193,7 @@ describe("npm project install env", () => {
|
||||
|
||||
const envWithParentAge = createNpmProjectInstallEnv(
|
||||
{
|
||||
...baseEnv,
|
||||
NPM_CONFIG_USERCONFIG: npmrc,
|
||||
NPM_CONFIG_MIN_RELEASE_AGE: "7",
|
||||
},
|
||||
@@ -200,12 +214,14 @@ describe("npm project install env", () => {
|
||||
it("uses before args for stale npm before policies", () => {
|
||||
const dir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-npmrc-"));
|
||||
try {
|
||||
const baseEnv = createIsolatedNpmConfigEnv(dir);
|
||||
const npmrc = path.join(dir, "npmrc");
|
||||
fsSync.writeFileSync(npmrc, "before=2026-01-01T00:00:00.000Z\n", "utf-8");
|
||||
|
||||
expect(
|
||||
createNpmFreshnessBypassArgs(
|
||||
{
|
||||
...baseEnv,
|
||||
NPM_CONFIG_USERCONFIG: npmrc,
|
||||
},
|
||||
FROZEN_NOW,
|
||||
@@ -219,11 +235,13 @@ describe("npm project install env", () => {
|
||||
it("uses before args for expanded npm userconfig paths", () => {
|
||||
const dir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-home-npmrc-"));
|
||||
try {
|
||||
const baseEnv = createIsolatedNpmConfigEnv(dir);
|
||||
fsSync.writeFileSync(path.join(dir, ".npmrc"), "before=2026-01-01T00:00:00.000Z\n", "utf-8");
|
||||
|
||||
expect(
|
||||
createNpmFreshnessBypassArgs(
|
||||
{
|
||||
...baseEnv,
|
||||
HOME: dir,
|
||||
NPM_CONFIG_USERCONFIG: "~/.npmrc",
|
||||
},
|
||||
@@ -233,6 +251,7 @@ describe("npm project install env", () => {
|
||||
expect(
|
||||
createNpmFreshnessBypassArgs(
|
||||
{
|
||||
...baseEnv,
|
||||
HOME: dir,
|
||||
NPM_CONFIG_USERCONFIG: "${HOME}/.npmrc",
|
||||
},
|
||||
@@ -247,7 +266,9 @@ describe("npm project install env", () => {
|
||||
it("uses before args for npm default globalconfig before policies", () => {
|
||||
const dir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-npm-prefix-"));
|
||||
try {
|
||||
const home = path.join(dir, "home");
|
||||
const npmrcDir = path.join(dir, "etc");
|
||||
fsSync.mkdirSync(home, { recursive: true });
|
||||
fsSync.mkdirSync(npmrcDir, { recursive: true });
|
||||
fsSync.writeFileSync(
|
||||
path.join(npmrcDir, "npmrc"),
|
||||
@@ -258,6 +279,7 @@ describe("npm project install env", () => {
|
||||
expect(
|
||||
createNpmFreshnessBypassArgs(
|
||||
{
|
||||
HOME: home,
|
||||
NPM_CONFIG_PREFIX: dir,
|
||||
},
|
||||
FROZEN_NOW,
|
||||
@@ -271,13 +293,14 @@ describe("npm project install env", () => {
|
||||
it("uses before args for command project npmrc before policies", () => {
|
||||
const dir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-project-npmrc-"));
|
||||
try {
|
||||
const baseEnv = createIsolatedNpmConfigEnv(dir);
|
||||
fsSync.writeFileSync(path.join(dir, ".npmrc"), "before=2026-01-01T00:00:00.000Z\n", "utf-8");
|
||||
|
||||
expect(createNpmFreshnessBypassArgs({}, FROZEN_NOW, { npmConfigCwd: dir })).toEqual([
|
||||
expect(createNpmFreshnessBypassArgs(baseEnv, FROZEN_NOW, { npmConfigCwd: dir })).toEqual([
|
||||
`--before=${FROZEN_NOW.toISOString()}`,
|
||||
]);
|
||||
|
||||
const env = createNpmProjectInstallEnv({}, { npmConfigCwd: dir }, FROZEN_NOW);
|
||||
const env = createNpmProjectInstallEnv(baseEnv, { npmConfigCwd: dir }, FROZEN_NOW);
|
||||
expect(env.npm_config_min_release_age).toBe("");
|
||||
expect(env.npm_config_before).toBe(FROZEN_NOW.toISOString());
|
||||
} finally {
|
||||
@@ -288,10 +311,11 @@ describe("npm project install env", () => {
|
||||
it("uses before args for the current project npmrc by default", () => {
|
||||
const dir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-current-npmrc-"));
|
||||
try {
|
||||
const baseEnv = createIsolatedNpmConfigEnv(dir);
|
||||
fsSync.writeFileSync(path.join(dir, ".npmrc"), "before=2026-01-01T00:00:00.000Z\n", "utf-8");
|
||||
const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(dir);
|
||||
withRestoredMocks([cwdSpy], () => {
|
||||
expect(createNpmFreshnessBypassArgs({}, FROZEN_NOW)).toEqual([
|
||||
expect(createNpmFreshnessBypassArgs(baseEnv, FROZEN_NOW)).toEqual([
|
||||
`--before=${FROZEN_NOW.toISOString()}`,
|
||||
]);
|
||||
});
|
||||
@@ -303,6 +327,7 @@ describe("npm project install env", () => {
|
||||
it("uses before args for scoped npm prefix before policies", () => {
|
||||
const dir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-prefix-npmrc-"));
|
||||
try {
|
||||
const baseEnv = createIsolatedNpmConfigEnv(dir);
|
||||
const npmrcDir = path.join(dir, "etc");
|
||||
fsSync.mkdirSync(npmrcDir, { recursive: true });
|
||||
fsSync.writeFileSync(
|
||||
@@ -311,7 +336,7 @@ describe("npm project install env", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
expect(createNpmFreshnessBypassArgs({}, FROZEN_NOW, { npmConfigPrefix: dir })).toEqual([
|
||||
expect(createNpmFreshnessBypassArgs(baseEnv, FROZEN_NOW, { npmConfigPrefix: dir })).toEqual([
|
||||
`--before=${FROZEN_NOW.toISOString()}`,
|
||||
]);
|
||||
} finally {
|
||||
@@ -322,10 +347,12 @@ describe("npm project install env", () => {
|
||||
it("overrides stale npmrc before config without emitting release-age config", () => {
|
||||
const dir = fsSync.mkdtempSync(path.join(os.tmpdir(), "openclaw-npmrc-"));
|
||||
try {
|
||||
const baseEnv = createIsolatedNpmConfigEnv(dir);
|
||||
const npmrc = path.join(dir, "npmrc");
|
||||
fsSync.writeFileSync(npmrc, "before=2026-01-01T00:00:00.000Z\n", "utf-8");
|
||||
const env = createNpmProjectInstallEnv(
|
||||
{
|
||||
...baseEnv,
|
||||
NPM_CONFIG_USERCONFIG: npmrc,
|
||||
},
|
||||
{},
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import {
|
||||
chmodSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
readlinkSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
@@ -23,6 +33,74 @@ function linkRequiredShellTools(bin: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function writeNpmFreshnessConflictFixture(path: string, argsLog: string) {
|
||||
writeFileSync(
|
||||
path,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
`printf '%s\\n' "$*" >> ${JSON.stringify(argsLog)}`,
|
||||
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "min-release-age" ]]; then',
|
||||
" printf 'null\\n'",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "before" ]]; then',
|
||||
" printf 'Wed May 13 2026 21:25:20 GMT-0300 (Brasilia Standard Time)\\n'",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'for arg in "$@"; do',
|
||||
' if [[ "$arg" == --before=* ]]; then',
|
||||
" printf '%s\\n' 'Exit prior to config file resolving' >&2",
|
||||
" printf '%s\\n' 'cause' >&2",
|
||||
" printf '%s\\n' '--min-release-age cannot be provided when using --before' >&2",
|
||||
" exit 64",
|
||||
" fi",
|
||||
"done",
|
||||
'for arg in "$@"; do',
|
||||
' if [[ "$arg" == "--min-release-age=0" ]]; then',
|
||||
" exit 0",
|
||||
" fi",
|
||||
"done",
|
||||
"exit 65",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
chmodSync(path, 0o755);
|
||||
}
|
||||
|
||||
function writeNpmBeforePolicyFixture(path: string, argsLog: string) {
|
||||
writeFileSync(
|
||||
path,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
`printf '%s\\n' "$*" >> ${JSON.stringify(argsLog)}`,
|
||||
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "min-release-age" ]]; then',
|
||||
" printf 'null\\n'",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "before" ]]; then',
|
||||
" printf 'Wed May 13 2026 21:25:20 GMT-0300 (Brasilia Standard Time)\\n'",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'for arg in "$@"; do',
|
||||
' if [[ "$arg" == "--min-release-age=0" ]]; then',
|
||||
" printf '%s\\n' 'min-release-age should not be selected for project-only npmrc' >&2",
|
||||
" exit 64",
|
||||
" fi",
|
||||
"done",
|
||||
'for arg in "$@"; do',
|
||||
' if [[ "$arg" == --before=* ]]; then',
|
||||
" exit 0",
|
||||
" fi",
|
||||
"done",
|
||||
"exit 65",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
chmodSync(path, 0o755);
|
||||
}
|
||||
|
||||
describe("install-cli.sh", () => {
|
||||
const script = readFileSync(SCRIPT_PATH, "utf8");
|
||||
|
||||
@@ -144,12 +222,7 @@ describe("install-cli.sh", () => {
|
||||
linkRequiredShellTools(bin);
|
||||
writeFileSync(
|
||||
fakeApk,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
'printf "%s\\n" "$*" >> "$APK_LOG"',
|
||||
"exit 99",
|
||||
"",
|
||||
].join("\n"),
|
||||
["#!/bin/bash", 'printf "%s\\n" "$*" >> "$APK_LOG"', "exit 99", ""].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
fakeNode,
|
||||
@@ -166,14 +239,7 @@ describe("install-cli.sh", () => {
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
fakeNpm,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(fakeNpm, ["#!/bin/bash", "exit 0", ""].join("\n"));
|
||||
chmodSync(fakeApk, 0o755);
|
||||
chmodSync(fakeNode, 0o755);
|
||||
chmodSync(fakeNpm, 0o755);
|
||||
@@ -233,12 +299,7 @@ describe("install-cli.sh", () => {
|
||||
mkdirSync(nodePrefixBin, { recursive: true });
|
||||
writeFileSync(
|
||||
fakeApk,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
'printf "%s\\n" "$*" >> "$APK_LOG"',
|
||||
"exit 99",
|
||||
"",
|
||||
].join("\n"),
|
||||
["#!/bin/bash", 'printf "%s\\n" "$*" >> "$APK_LOG"', "exit 99", ""].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
staleNode,
|
||||
@@ -285,22 +346,8 @@ describe("install-cli.sh", () => {
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
oldNpm,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
fakeNpm,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(oldNpm, ["#!/bin/bash", "exit 0", ""].join("\n"));
|
||||
writeFileSync(fakeNpm, ["#!/bin/bash", "exit 0", ""].join("\n"));
|
||||
chmodSync(fakeApk, 0o755);
|
||||
chmodSync(staleNode, 0o755);
|
||||
chmodSync(oldNode, 0o755);
|
||||
@@ -384,14 +431,7 @@ describe("install-cli.sh", () => {
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
fakeNpm,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(fakeNpm, ["#!/bin/bash", "exit 0", ""].join("\n"));
|
||||
chmodSync(fakeApk, 0o755);
|
||||
chmodSync(fakeNode, 0o755);
|
||||
chmodSync(fakeNpm, 0o755);
|
||||
@@ -446,12 +486,7 @@ describe("install-cli.sh", () => {
|
||||
linkRequiredShellTools(bin);
|
||||
writeFileSync(
|
||||
fakeApk,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
'printf "%s\\n" "$*" >> "$APK_LOG"',
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
["#!/bin/bash", 'printf "%s\\n" "$*" >> "$APK_LOG"', "exit 0", ""].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
fakeNode,
|
||||
@@ -468,14 +503,7 @@ describe("install-cli.sh", () => {
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(
|
||||
fakeNpm,
|
||||
[
|
||||
"#!/bin/bash",
|
||||
"exit 0",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
writeFileSync(fakeNpm, ["#!/bin/bash", "exit 0", ""].join("\n"));
|
||||
chmodSync(fakeApk, 0o755);
|
||||
chmodSync(fakeNode, 0o755);
|
||||
chmodSync(fakeNpm, 0o755);
|
||||
@@ -504,7 +532,9 @@ describe("install-cli.sh", () => {
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(readFileSync(apkLog, "utf8")).toContain("add --no-cache nodejs npm");
|
||||
expect(result.stdout).toContain("Alpine Node package must provide Node >= 22.22.0 with node:sqlite");
|
||||
expect(result.stdout).toContain(
|
||||
"Alpine Node package must provide Node >= 22.22.0 with node:sqlite",
|
||||
);
|
||||
expect(result.stdout).toContain("found v22.18.0");
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
@@ -513,7 +543,7 @@ describe("install-cli.sh", () => {
|
||||
|
||||
it("clears npm freshness filters for package installs", () => {
|
||||
expect(script).toContain('freshness_flag="--min-release-age=0"');
|
||||
expect(script).toContain('npm_raw_config_has_key "min-release-age"');
|
||||
expect(script).toContain('npm_config_has_raw_key "$(npm_bin)" "min-release-age"');
|
||||
expect(script).toContain('freshness_flag="--before=$(date -u');
|
||||
expect(script).toContain("env -u NPM_CONFIG_BEFORE -u npm_config_before");
|
||||
});
|
||||
@@ -748,4 +778,78 @@ describe("install-cli.sh", () => {
|
||||
expect(result.stdout).toContain("npm installs do not support OpenClaw GitHub source targets");
|
||||
expect(result.stdout).toContain("--install-method git --version main");
|
||||
});
|
||||
|
||||
it("does not emit before args when npmrc min-release-age computes a before cutoff", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-freshness-"));
|
||||
const prefix = join(tmp, "prefix");
|
||||
const home = join(tmp, "home");
|
||||
const nodeBin = join(prefix, "tools/node-v22.22.0/bin");
|
||||
const argsLog = join(tmp, "npm-args.log");
|
||||
mkdirSync(nodeBin, { recursive: true });
|
||||
mkdirSync(home, { recursive: true });
|
||||
writeFileSync(join(home, ".npmrc"), "min-release-age=7\n");
|
||||
writeNpmFreshnessConflictFixture(join(nodeBin, "npm"), argsLog);
|
||||
|
||||
let result: ReturnType<typeof runInstallCliShell> | undefined;
|
||||
let argsOutput = "";
|
||||
try {
|
||||
result = runInstallCliShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`HOME=${JSON.stringify(home)}`,
|
||||
`OPENCLAW_PREFIX=${JSON.stringify(prefix)}`,
|
||||
"OPENCLAW_VERSION=2026.5.19",
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
"ensure_git() { return 0; }",
|
||||
"install_openclaw",
|
||||
].join("\n"),
|
||||
);
|
||||
argsOutput = readFileSync(argsLog, "utf8");
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
|
||||
expect(result?.status).toBe(0);
|
||||
expect(argsOutput).toContain("--min-release-age=0");
|
||||
expect(argsOutput).not.toContain("--before=");
|
||||
});
|
||||
|
||||
it("ignores project npmrc when choosing global install freshness args", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-global-freshness-"));
|
||||
const prefix = join(tmp, "prefix");
|
||||
const home = join(tmp, "home");
|
||||
const project = join(tmp, "project");
|
||||
const nodeBin = join(prefix, "tools/node-v22.22.0/bin");
|
||||
const argsLog = join(tmp, "npm-args.log");
|
||||
mkdirSync(nodeBin, { recursive: true });
|
||||
mkdirSync(home, { recursive: true });
|
||||
mkdirSync(project, { recursive: true });
|
||||
writeFileSync(join(home, ".npmrc"), "before=2026-01-01T00:00:00.000Z\n");
|
||||
writeFileSync(join(project, ".npmrc"), "min-release-age=7\n");
|
||||
writeNpmBeforePolicyFixture(join(nodeBin, "npm"), argsLog);
|
||||
|
||||
let result: ReturnType<typeof runInstallCliShell> | undefined;
|
||||
let argsOutput = "";
|
||||
try {
|
||||
result = runInstallCliShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`cd ${JSON.stringify(project)}`,
|
||||
`HOME=${JSON.stringify(home)}`,
|
||||
`OPENCLAW_PREFIX=${JSON.stringify(prefix)}`,
|
||||
"OPENCLAW_VERSION=2026.5.19",
|
||||
`source ${JSON.stringify(process.cwd() + "/" + SCRIPT_PATH)}`,
|
||||
"ensure_git() { return 0; }",
|
||||
"install_openclaw",
|
||||
].join("\n"),
|
||||
);
|
||||
argsOutput = readFileSync(argsLog, "utf8");
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
|
||||
expect(result?.status).toBe(0);
|
||||
expect(argsOutput).toContain("--before=");
|
||||
expect(argsOutput).not.toContain("--min-release-age=0");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,6 +152,30 @@ describe("install.ps1 failure handling", () => {
|
||||
expect(npmInstallBody).toContain("-InstallMethod git -Tag main");
|
||||
});
|
||||
|
||||
it("does not read project npmrc when choosing global install freshness args", () => {
|
||||
const rawKeyBody = extractFunctionBody(source, "Test-NpmConfigRawKey");
|
||||
expect(rawKeyBody).not.toContain("Get-Location");
|
||||
expect(rawKeyBody).not.toContain('Join-Path (Get-Location) ".npmrc"');
|
||||
});
|
||||
|
||||
it("preserves the min-release-age probe status before raw npmrc detection", () => {
|
||||
const npmInstallBody = extractFunctionBody(source, "Install-OpenClaw");
|
||||
const probeStatusCapture = npmInstallBody.indexOf("$minReleaseAgeStatus = $LASTEXITCODE");
|
||||
const rawKeyProbe = npmInstallBody.indexOf("Test-NpmConfigRawKey -Key");
|
||||
expect(probeStatusCapture).toBeGreaterThan(-1);
|
||||
expect(rawKeyProbe).toBeGreaterThan(-1);
|
||||
expect(probeStatusCapture).toBeLessThan(rawKeyProbe);
|
||||
expect(npmInstallBody).toContain(
|
||||
"} elseif ($minReleaseAgeStatus -ne 0 -or -not $minReleaseAge",
|
||||
);
|
||||
expect(npmInstallBody).toContain(
|
||||
'Invoke-NpmCommand -Arguments @("config", "get", "min-release-age", "--global")',
|
||||
);
|
||||
expect(npmInstallBody).toContain(
|
||||
'Invoke-NpmCommand -Arguments @("config", "get", "before", "--global")',
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves caller-relative local tarball install specs before safe-cwd npm calls", () => {
|
||||
const resolveSpecBody = extractFunctionBody(source, "Resolve-NpmOpenClawInstallSpec");
|
||||
const localSpecBody = extractFunctionBody(source, "Resolve-LocalNpmPackageInstallSpec");
|
||||
|
||||
@@ -17,6 +17,74 @@ function runInstallShell(script: string, env: NodeJS.ProcessEnv = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function writeNpmFreshnessConflictFixture(path: string, argsLog: string) {
|
||||
writeFileSync(
|
||||
path,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
`printf '%s\\n' "$*" >> ${JSON.stringify(argsLog)}`,
|
||||
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "min-release-age" ]]; then',
|
||||
" printf 'null\\n'",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "before" ]]; then',
|
||||
" printf 'Wed May 13 2026 21:25:20 GMT-0300 (Brasilia Standard Time)\\n'",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'for arg in "$@"; do',
|
||||
' if [[ "$arg" == --before=* ]]; then',
|
||||
" printf '%s\\n' 'Exit prior to config file resolving' >&2",
|
||||
" printf '%s\\n' 'cause' >&2",
|
||||
" printf '%s\\n' '--min-release-age cannot be provided when using --before' >&2",
|
||||
" exit 64",
|
||||
" fi",
|
||||
"done",
|
||||
'for arg in "$@"; do',
|
||||
' if [[ "$arg" == "--min-release-age=0" ]]; then',
|
||||
" exit 0",
|
||||
" fi",
|
||||
"done",
|
||||
"exit 65",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
chmodSync(path, 0o755);
|
||||
}
|
||||
|
||||
function writeNpmBeforePolicyFixture(path: string, argsLog: string) {
|
||||
writeFileSync(
|
||||
path,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
`printf '%s\\n' "$*" >> ${JSON.stringify(argsLog)}`,
|
||||
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "min-release-age" ]]; then',
|
||||
" printf 'null\\n'",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "before" ]]; then',
|
||||
" printf 'Wed May 13 2026 21:25:20 GMT-0300 (Brasilia Standard Time)\\n'",
|
||||
" exit 0",
|
||||
"fi",
|
||||
'for arg in "$@"; do',
|
||||
' if [[ "$arg" == "--min-release-age=0" ]]; then',
|
||||
" printf '%s\\n' 'min-release-age should not be selected for project-only npmrc' >&2",
|
||||
" exit 64",
|
||||
" fi",
|
||||
"done",
|
||||
'for arg in "$@"; do',
|
||||
' if [[ "$arg" == --before=* ]]; then',
|
||||
" exit 0",
|
||||
" fi",
|
||||
"done",
|
||||
"exit 65",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
chmodSync(path, 0o755);
|
||||
}
|
||||
|
||||
describe("install.sh", () => {
|
||||
const script = readFileSync(SCRIPT_PATH, "utf8");
|
||||
|
||||
@@ -42,15 +110,13 @@ describe("install.sh", () => {
|
||||
it("installs Node.js with apk on Alpine before falling back to NodeSource", () => {
|
||||
expect(script).toContain("finish_linux_node_install()");
|
||||
expect(script).toContain('ui_info "Installing Node.js via apk (Alpine Linux detected)"');
|
||||
expect(script).toContain(
|
||||
'run_quiet_step "Installing Node.js" apk add --no-cache nodejs npm',
|
||||
);
|
||||
expect(script).toContain('run_quiet_step "Installing Node.js" apk add --no-cache nodejs npm');
|
||||
expect(script).toContain(
|
||||
'run_quiet_step "Installing Node.js" sudo apk add --no-cache nodejs npm',
|
||||
);
|
||||
expect(script).toContain('if ! node_is_at_least_required; then');
|
||||
expect(script).toContain("if ! node_is_at_least_required; then");
|
||||
|
||||
const apkIndex = script.indexOf('if command -v apk &> /dev/null; then');
|
||||
const apkIndex = script.indexOf("if command -v apk &> /dev/null; then");
|
||||
const nodeSourceIndex = script.indexOf('ui_info "Installing Node.js via NodeSource"');
|
||||
expect(apkIndex).toBeGreaterThan(-1);
|
||||
expect(nodeSourceIndex).toBeGreaterThan(apkIndex);
|
||||
@@ -82,7 +148,7 @@ describe("install.sh", () => {
|
||||
it("clears npm freshness filters for package installs", () => {
|
||||
expect(script).toContain("env -u NPM_CONFIG_BEFORE -u npm_config_before");
|
||||
expect(script).toContain('freshness_flag="--min-release-age=0"');
|
||||
expect(script).toContain('npm_raw_config_has_key "min-release-age"');
|
||||
expect(script).toContain('npm_config_has_raw_key npm "min-release-age"');
|
||||
expect(script).toContain('freshness_flag="--before=$(date -u');
|
||||
expect(script).toContain('cmd+=(--no-fund --no-audit "$freshness_flag" install -g "$spec")');
|
||||
});
|
||||
@@ -386,6 +452,80 @@ describe("install.sh", () => {
|
||||
expect(result.stdout).toContain("--install-method git --version main");
|
||||
});
|
||||
|
||||
it("does not emit before args when npmrc min-release-age computes a before cutoff", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-npm-freshness-"));
|
||||
const bin = join(tmp, "bin");
|
||||
const home = join(tmp, "home");
|
||||
const argsLog = join(tmp, "npm-args.log");
|
||||
mkdirSync(bin, { recursive: true });
|
||||
mkdirSync(home, { recursive: true });
|
||||
writeFileSync(join(home, ".npmrc"), "min-release-age=7\n");
|
||||
writeNpmFreshnessConflictFixture(join(bin, "npm"), argsLog);
|
||||
|
||||
let result: ReturnType<typeof runInstallShell> | undefined;
|
||||
let argsOutput = "";
|
||||
try {
|
||||
result = runInstallShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`source ${JSON.stringify(SCRIPT_PATH)}`,
|
||||
`HOME=${JSON.stringify(home)}`,
|
||||
`PATH=${JSON.stringify(`${bin}:/usr/bin:/bin`)}`,
|
||||
"NPM_LOGLEVEL=error",
|
||||
"NPM_SILENT_FLAG=",
|
||||
"SHARP_IGNORE_GLOBAL_LIBVIPS=1",
|
||||
`run_npm_global_install openclaw@latest ${JSON.stringify(join(tmp, "install.log"))}`,
|
||||
].join("\n"),
|
||||
);
|
||||
argsOutput = readFileSync(argsLog, "utf8");
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
|
||||
expect(result?.status).toBe(0);
|
||||
expect(argsOutput).toContain("--min-release-age=0");
|
||||
expect(argsOutput).not.toContain("--before=");
|
||||
});
|
||||
|
||||
it("ignores project npmrc when choosing global install freshness args", () => {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-global-freshness-"));
|
||||
const bin = join(tmp, "bin");
|
||||
const home = join(tmp, "home");
|
||||
const project = join(tmp, "project");
|
||||
const argsLog = join(tmp, "npm-args.log");
|
||||
mkdirSync(bin, { recursive: true });
|
||||
mkdirSync(home, { recursive: true });
|
||||
mkdirSync(project, { recursive: true });
|
||||
writeFileSync(join(home, ".npmrc"), "before=2026-01-01T00:00:00.000Z\n");
|
||||
writeFileSync(join(project, ".npmrc"), "min-release-age=7\n");
|
||||
writeNpmBeforePolicyFixture(join(bin, "npm"), argsLog);
|
||||
|
||||
let result: ReturnType<typeof runInstallShell> | undefined;
|
||||
let argsOutput = "";
|
||||
try {
|
||||
result = runInstallShell(
|
||||
[
|
||||
"set -euo pipefail",
|
||||
`cd ${JSON.stringify(project)}`,
|
||||
`source ${JSON.stringify(process.cwd() + "/" + SCRIPT_PATH)}`,
|
||||
`HOME=${JSON.stringify(home)}`,
|
||||
`PATH=${JSON.stringify(`${bin}:/usr/bin:/bin`)}`,
|
||||
"NPM_LOGLEVEL=error",
|
||||
"NPM_SILENT_FLAG=",
|
||||
"SHARP_IGNORE_GLOBAL_LIBVIPS=1",
|
||||
`run_npm_global_install openclaw@latest ${JSON.stringify(join(tmp, "install.log"))}`,
|
||||
].join("\n"),
|
||||
);
|
||||
argsOutput = readFileSync(argsLog, "utf8");
|
||||
} finally {
|
||||
rmSync(tmp, { force: true, recursive: true });
|
||||
}
|
||||
|
||||
expect(result?.status).toBe(0);
|
||||
expect(argsOutput).toContain("--before=");
|
||||
expect(argsOutput).not.toContain("--min-release-age=0");
|
||||
});
|
||||
|
||||
it("exports noninteractive apt env during Linux startup", () => {
|
||||
expect(script).toMatch(
|
||||
/detect_os_or_die\s+if \[\[ "\$OS" == "linux" \]\]; then\s+export DEBIAN_FRONTEND="\$\{DEBIAN_FRONTEND:-noninteractive\}"\s+export NEEDRESTART_MODE="\$\{NEEDRESTART_MODE:-a\}"\s+fi/m,
|
||||
|
||||
Reference in New Issue
Block a user