openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
304 lines (303 loc) • 11.7 kB
JavaScript
import { n as tryProcessCwd } from "./safe-cwd-DOxDm8mD.mjs";
import { y as uniqueStrings } from "./string-normalization-D6DKCpwE.mjs";
import { r as resolveSafeChildProcessInvocation } from "./windows-command-D3cGio1z.mjs";
import { createRequire } from "node:module";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { spawnSync } from "node:child_process";
import { safeStatSync } from "@openclaw/fs-safe/path";
//#region src/infra/update-network-budget.ts
const UPDATE_NETWORK_TIMEOUT_MS = 3e5;
//#endregion
//#region src/infra/npm-command.ts
const require = createRequire(import.meta.url);
var BundledNpmCliNotFoundError = class extends Error {
constructor(cliPath, cause) {
super(`Bundled npm CLI is missing: ${cliPath}. Reinstall OpenClaw to restore npm/bin/npm-cli.js.`, { cause });
this.code = "BUNDLED_NPM_CLI_NOT_FOUND";
this.name = "BundledNpmCliNotFoundError";
}
};
/** Runs the packaged npm CLI with the current JavaScript runtime. */
function resolveBundledNpmCommand(args) {
let cliPath = "npm/bin/npm-cli.js";
try {
cliPath = path.join(path.dirname(require.resolve("npm/package.json")), "bin", "npm-cli.js");
if (!fs.statSync(cliPath).isFile()) throw new Error("Not a regular file");
} catch (cause) {
throw new BundledNpmCliNotFoundError(cliPath, cause);
}
return [
process.execPath,
cliPath,
...args
];
}
/** Bun must run the packaged CLI directly; npm's executable requires Node. */
function resolveNpmCommand(args) {
return process.versions.bun ? resolveBundledNpmCommand(args) : ["npm", ...args];
}
//#endregion
//#region src/infra/npm-install-env.ts
const NPM_CONFIG_SCRIPT_SHELL_KEYS = ["NPM_CONFIG_SCRIPT_SHELL", "npm_config_script_shell"];
const NPM_CONFIG_KEYS_TO_RESET = /* @__PURE__ */ new Set([
"npm_config_cache",
"npm_config_dry_run",
"npm_config_global",
"npm_config_include_workspace_root",
"npm_config_ignore_scripts",
"npm_config_location",
"npm_config_legacy_peer_deps",
"npm_config_prefix",
"npm_config_strict_peer_deps",
"npm_config_workspace",
"npm_config_workspaces"
]);
const NPM_FRESHNESS_BYPASS_KEYS = [
"NPM_CONFIG_BEFORE",
"npm_config_before",
"NPM_CONFIG_MIN_RELEASE_AGE",
"npm_config_min_release_age",
"NPM_CONFIG_MIN-RELEASE-AGE",
"npm_config_min-release-age"
];
const NPM_CONFIG_PATH_PROBE_PARENT_ENV_KEYS = [
"PATH",
"Path",
"PATHEXT",
"SystemRoot",
"ComSpec"
];
const NPM_GLOBAL_CONFIG_PATH_CACHE = /* @__PURE__ */ new Map();
const NPM_GLOBAL_CONFIG_PATH_CACHE_ENV_KEYS = [
...NPM_CONFIG_PATH_PROBE_PARENT_ENV_KEYS,
"NPM_CONFIG_GLOBALCONFIG",
"npm_config_globalconfig",
"NPM_CONFIG_PREFIX",
"npm_config_prefix",
"NPM_CONFIG_USERCONFIG",
"npm_config_userconfig",
"HOME",
"PREFIX",
"USERPROFILE"
];
function resolveEnvPath(env, name) {
const raw = env[`NPM_CONFIG_${name}`]?.trim() || env[`npm_config_${name.toLowerCase()}`]?.trim();
return raw ? resolveNpmConfigPath(raw, env) : null;
}
function resolveHomeNpmrc(env) {
const home = env.HOME?.trim() || env.USERPROFILE?.trim() || os.homedir();
return path.join(home, ".npmrc");
}
function replaceNpmEnvRefs(value, env) {
return value.replace(/(?<!\\)(\\*)\$\{([^${}?]+)(\?)?\}/gu, (original, escapes, name, modifier) => {
const fallback = modifier === "?" ? "" : `\${${name}}`;
const resolved = env[name] !== void 0 ? env[name] : fallback;
if (escapes.length % 2) return original.slice((escapes.length + 1) / 2);
return `${escapes.slice(escapes.length / 2)}${resolved}`;
});
}
function resolveNpmConfigPath(rawPath, env) {
const expanded = replaceNpmEnvRefs(rawPath, env);
const home = env.HOME?.trim() || env.USERPROFILE?.trim() || os.homedir();
return (process.platform === "win32" ? /^~(\/|\\)/u : /^~\//u).test(expanded) && home ? path.resolve(home, expanded.slice(2)) : path.resolve(expanded);
}
function createNpmConfigPathProbeEnv(env, scope) {
const probeEnv = { ...env };
for (const key of NPM_FRESHNESS_BYPASS_KEYS) delete probeEnv[key];
for (const key of NPM_CONFIG_PATH_PROBE_PARENT_ENV_KEYS) if (probeEnv[key] == null && process.env[key] != null) probeEnv[key] = process.env[key];
if (scope.npmConfigPrefix) probeEnv.npm_config_prefix = scope.npmConfigPrefix;
return probeEnv;
}
function runNpmConfigProbe(params) {
const invocation = resolveSafeChildProcessInvocation({
argv: resolveNpmCommand(params.args),
cwd: params.cwd,
env: params.env
});
const result = spawnSync(invocation.command, invocation.args, {
cwd: params.cwd,
encoding: "utf-8",
env: params.env,
stdio: [
"ignore",
"pipe",
"ignore"
],
timeout: params.timeoutMs,
windowsHide: invocation.windowsHide,
windowsVerbatimArguments: invocation.windowsVerbatimArguments
});
if (result.error) throw result.error;
if (result.status !== 0) throw new Error(`npm config check exited with status ${result.status ?? "unknown"}`);
return result.stdout;
}
function readNpmGlobalConfigPath(env, scope) {
const scopedGlobalConfig = resolveScopedGlobalNpmrc(scope);
if (scopedGlobalConfig) return scopedGlobalConfig;
const configuredGlobalConfig = resolveEnvPath(env, "GLOBALCONFIG");
if (configuredGlobalConfig) return configuredGlobalConfig;
const configuredPrefix = resolveEnvPath(env, "PREFIX");
if (configuredPrefix) return path.join(configuredPrefix, "etc", "npmrc");
const cacheKey = buildNpmGlobalConfigPathCacheKey(env, scope);
if (NPM_GLOBAL_CONFIG_PATH_CACHE.has(cacheKey)) return NPM_GLOBAL_CONFIG_PATH_CACHE.get(cacheKey) ?? null;
try {
const raw = runNpmConfigProbe({
args: [
"config",
"get",
"globalconfig"
],
env: createNpmConfigPathProbeEnv(env, scope),
timeoutMs: 2e3
}).trim();
const resolved = raw && raw !== "null" && raw !== "undefined" ? raw : null;
NPM_GLOBAL_CONFIG_PATH_CACHE.set(cacheKey, resolved);
return resolved;
} catch {
NPM_GLOBAL_CONFIG_PATH_CACHE.set(cacheKey, null);
return null;
}
}
function buildNpmGlobalConfigPathCacheKey(env, scope) {
const configFiles = resolveNpmConfigFiles(env, scope);
return JSON.stringify({
cwd: scope.npmConfigCwd?.trim() || tryProcessCwd() || "",
prefix: scope.npmConfigPrefix?.trim() ?? "",
env: Object.fromEntries(NPM_GLOBAL_CONFIG_PATH_CACHE_ENV_KEYS.map((key) => [key, env[key] ?? process.env[key] ?? ""])),
configFiles: configFiles.map((filePath) => {
const stat = safeStatSync(filePath);
return {
path: filePath,
signature: stat ? `${stat.mtimeMs}:${stat.size}` : "missing"
};
})
});
}
function resolveScopedProjectNpmrc(scope) {
const cwd = scope.npmConfigCwd?.trim() || tryProcessCwd();
return cwd ? path.join(cwd, ".npmrc") : null;
}
function resolveScopedGlobalNpmrc(scope) {
const prefix = scope.npmConfigPrefix?.trim();
return prefix ? path.join(prefix, "etc", "npmrc") : null;
}
function resolveNpmConfigFiles(env, scope, includeProbedGlobal = false) {
const files = [
resolveScopedProjectNpmrc(scope),
resolveEnvPath(env, "USERCONFIG") ?? resolveHomeNpmrc(env),
resolveEnvPath(env, "GLOBALCONFIG"),
resolveScopedGlobalNpmrc(scope),
...includeProbedGlobal ? [readNpmGlobalConfigPath(env, scope)] : []
];
return uniqueStrings(files.filter((file) => Boolean(file)));
}
function hasNpmrcConfigKey(filePath, key) {
try {
const raw = fs.readFileSync(filePath, "utf-8");
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
return new RegExp(`^\\s*${escapedKey}\\s*=`, "imu").test(raw);
} catch {
return false;
}
}
function hasNpmEnvConfigKey(env, key) {
return Object.entries(env).some(([envKey, value]) => {
if (!/^npm_config_/iu.test(envKey) || !value?.trim()) return false;
return envKey.slice(11).replace(/(?!^)_/gu, "-").toLowerCase() === key;
});
}
/** Finds keys npm resolves from explicit config layers rather than built-in defaults. */
function findExplicitNpmConfigKeys(env, keys, scope = {}) {
const found = new Set(keys.filter((key) => hasNpmEnvConfigKey(env, key)));
const remaining = keys.filter((key) => !found.has(key));
if (remaining.length === 0) return found;
const cwd = scope.npmConfigCwd?.trim() || tryProcessCwd() || void 0;
const probeEnv = createNpmConfigPathProbeEnv(env, scope);
try {
const raw = runNpmConfigProbe({
args: [
"config",
"list",
"--location=project",
"--json=false",
"--long=false"
],
cwd,
env: probeEnv,
timeoutMs: 5e3
});
const installConfig = raw.split(/^; "publishConfig" from /mu, 1)[0] ?? raw;
for (const key of remaining) {
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
if (new RegExp(`^${escapedKey}\\s*=`, "mu").test(installConfig)) found.add(key);
}
} catch {
for (const key of remaining) found.add(key);
}
return found;
}
function resolveNpmFreshnessBypassMode(env, scope) {
if (process.platform === "win32") return "before";
const hasRawKey = (key) => resolveNpmConfigFiles(env, scope, true).some((file) => hasNpmrcConfigKey(file, key));
if (hasRawKey("min-release-age")) return "min-release-age";
return hasRawKey("before") ? "before" : "min-release-age";
}
/**
* Builds npm args that bypass host freshness policies for OpenClaw-managed installs.
* Existing npmrc policy decides whether `before` or `min-release-age` is safer.
*/
function createNpmFreshnessBypassArgs(env = process.env, now = /* @__PURE__ */ new Date(), scope = {}) {
if (resolveNpmFreshnessBypassMode(env, scope) === "min-release-age") return ["--min-release-age=0"];
return [`--before=${now.toISOString()}`];
}
function applyNpmFreshnessBypassEnv(env, now = /* @__PURE__ */ new Date(), scope = {}) {
const before = resolveNpmFreshnessBypassMode(env, scope) === "before" ? now.toISOString() : void 0;
for (const key of NPM_FRESHNESS_BYPASS_KEYS) {
if (process.platform === "win32" && key.includes("-")) {
delete env[key];
continue;
}
env[key] = "";
}
if (before !== void 0) env.npm_config_before = before;
else env.npm_config_min_release_age = "0";
}
/**
* Creates npm env for project-local installs, clearing global/workspace config
* and adding fetch, freshness, cache, and POSIX script-shell defaults.
*/
function createNpmProjectInstallEnv(env, options = {}, now = /* @__PURE__ */ new Date()) {
const nextEnv = { ...env };
for (const key of Object.keys(nextEnv)) if (NPM_CONFIG_KEYS_TO_RESET.has(key.toLowerCase())) delete nextEnv[key];
for (const [key, fallback] of Object.entries({
npm_config_fetch_retries: "5",
npm_config_fetch_retry_maxtimeout: "120000",
npm_config_fetch_retry_mintimeout: "10000",
npm_config_fetch_timeout: String(UPDATE_NETWORK_TIMEOUT_MS)
})) if (!Object.entries(nextEnv).some(([name, value]) => name.toLowerCase() === key && value !== void 0)) nextEnv[key] = fallback;
const installEnv = {
...nextEnv,
npm_config_dry_run: "false",
npm_config_global: "false",
npm_config_location: "project",
npm_config_package_lock: "false",
npm_config_save: "false",
...options.cacheDir ? { npm_config_cache: options.cacheDir } : {}
};
applyNpmFreshnessBypassEnv(installEnv, now, options);
applyPosixNpmScriptShellEnv(installEnv);
return installEnv;
}
/** Sets npm's script-shell env only when the caller has not configured one. */
function applyPosixNpmScriptShellEnv(env) {
if (NPM_CONFIG_SCRIPT_SHELL_KEYS.some((key) => Boolean(env[key]?.trim())) || process.platform === "win32") return;
if (fs.existsSync("/bin/sh")) env.NPM_CONFIG_SCRIPT_SHELL = "/bin/sh";
else {
const shell = env.SHELL?.trim();
if (shell && path.isAbsolute(shell) && fs.existsSync(shell)) env.NPM_CONFIG_SCRIPT_SHELL = shell;
}
}
//#endregion
export { findExplicitNpmConfigKeys as a, createNpmProjectInstallEnv as i, applyPosixNpmScriptShellEnv as n, resolveNpmCommand as o, createNpmFreshnessBypassArgs as r, UPDATE_NETWORK_TIMEOUT_MS as s, applyNpmFreshnessBypassEnv as t };