UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

297 lines (296 loc) 10.4 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { c as resolveUserPath } from "./home-dir-BPhrG-aM.js"; import { d as pathExists } from "./fs-safe-B6pvPGnf.js"; import "./utils-P__uGsPB.js"; import { n as resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir-DnyL0lW9.js"; import { s as resolveNpmJsonEntries } from "./npm-registry-spec-CM_p1_uq.js"; import { r as withTempWorkspace } from "./private-temp-workspace-Dh7teFP0.js"; import { r as runCommandWithTimeout } from "./exec-BIE-3oLG.js"; import { h as resolveArchiveKind } from "./archive-BfjFIxAJ.js"; import { t as applyNpmFreshnessBypassEnv } from "./npm-install-env-DKWpWmP2.js"; import path from "node:path"; import fs from "node:fs/promises"; import { gt, satisfies, validRange } from "semver"; //#region src/infra/install-source-utils.ts function formatNpmCommandFailureOutput(result) { const detail = result.stderr.trim() || result.stdout.trim(); if (detail) return detail; if (result.termination === "timeout" || result.termination === "no-output-timeout") return `termination ${result.termination} (no output from npm)`; if (result.termination === "exit" && result.code !== null) return `exit code ${result.code} (no output from npm)`; if (result.signal) return `signal ${result.signal} (no output from npm)`; return `termination ${result.termination} (no output from npm)`; } /** Converts npm resolution metadata into stable result field names. */ function buildNpmResolutionFields(resolution) { return { resolvedName: resolution?.name, resolvedVersion: resolution?.version, resolvedSpec: resolution?.resolvedSpec, integrity: resolution?.integrity, shasum: resolution?.shasum, resolvedAt: resolution?.resolvedAt }; } /** Creates a script-free npm environment for metadata and pack commands. */ function createNpmMetadataEnv(scope = {}) { const env = { COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", NPM_CONFIG_IGNORE_SCRIPTS: "true" }; applyNpmFreshnessBypassEnv(env, /* @__PURE__ */ new Date(), scope); return env; } function resolveNpmSpecVersionSelector(spec) { const separator = spec.lastIndexOf("@"); return separator > 0 ? normalizeOptionalString(spec.slice(separator + 1)) : void 0; } function selectNpmViewMetadataEntry(value, spec) { if (!Array.isArray(value)) return value; const entries = value.filter((entry) => isRecord(entry) && !Array.isArray(entry)); const selector = resolveNpmSpecVersionSelector(spec); const range = selector ? validRange(selector) : null; if (range) { let best; for (const entry of entries) { const version = normalizeOptionalString(entry.version); if (!version || !satisfies(version, range)) continue; if (!best || gt(version, best.version)) best = { entry, version }; } return best?.entry; } return entries.at(-1); } function normalizeNpmViewMetadata(value, spec) { const entry = selectNpmViewMetadataEntry(value, spec); if (!isRecord(entry) || Array.isArray(entry)) return null; const rec = entry; const name = normalizeOptionalString(rec.name); const version = normalizeOptionalString(rec.version); const resolvedSpec = name && version ? `${name}@${version}` : void 0; const dist = rec.dist && typeof rec.dist === "object" ? rec.dist : {}; return { name, version, resolvedSpec, integrity: normalizeOptionalString(rec["dist.integrity"]) ?? normalizeOptionalString(dist.integrity), shasum: normalizeOptionalString(rec["dist.shasum"]) ?? normalizeOptionalString(dist.shasum), ...isRecord(rec.openclaw) ? { packageOpenClaw: rec.openclaw } : {} }; } async function resolveNpmSpecMetadata(params) { const res = await runCommandWithTimeout([ "npm", "view", params.spec, "name", "version", "dist.integrity", "dist.shasum", "openclaw", "--json" ], { timeoutMs: Math.max(params.timeoutMs ?? 6e4, 6e4), signal: params.signal, killProcessTree: true, env: createNpmMetadataEnv() }); if (res.code !== 0) { const raw = formatNpmCommandFailureOutput(res); if (/E404|is not in this registry/i.test(raw)) return { ok: false, error: `Package not found on npm: ${params.spec}. See https://docs.openclaw.ai/tools/plugin for installable plugins.` }; return { ok: false, error: `npm view failed: ${raw}`, category: "metadata-env" }; } try { const metadata = normalizeNpmViewMetadata(JSON.parse(res.stdout.trim()), params.spec); if (!metadata?.name || !metadata.version) return { ok: false, error: `npm view produced incomplete package metadata (missing: ${[!metadata?.name ? "name" : null, !metadata?.version ? "version" : null].filter((field) => field !== null).join(", ")})`, category: "metadata-env" }; return { ok: true, metadata }; } catch (err) { return { ok: false, error: `npm view produced invalid JSON: ${String(err)}`, category: "metadata-env" }; } } /** Runs a callback in a private temp directory and removes it afterward. */ async function withInstallWorkspace(prefix, fn, options) { const rootDir = options?.rootDir ?? resolvePreferredOpenClawTmpDir(); return await withTempWorkspace({ rootDir, prefix }, async (tmp) => fn(tmp.dir)); } /** Resolves and validates a user-supplied archive path before extraction. */ async function resolveArchiveSourcePath(archivePath) { const resolved = resolveUserPath(archivePath); if (!await pathExists(resolved)) return { ok: false, error: `archive not found: ${resolved}` }; if (!resolveArchiveKind(resolved)) return { ok: false, error: `unsupported archive: ${resolved}` }; return { ok: true, path: resolved }; } function parseResolvedSpecFromId(id) { const at = id.lastIndexOf("@"); if (at <= 0 || at >= id.length - 1) return; const name = id.slice(0, at).trim(); const version = id.slice(at + 1).trim(); if (!name || !version) return; return `${name}@${version}`; } function normalizeNpmPackEntry(entry) { if (!entry || typeof entry !== "object") return null; const rec = entry; const name = normalizeOptionalString(rec.name); const version = normalizeOptionalString(rec.version); const id = normalizeOptionalString(rec.id); const resolvedSpec = (name && version ? `${name}@${version}` : void 0) ?? (id ? parseResolvedSpecFromId(id) : void 0); return { filename: normalizeOptionalString(rec.filename), metadata: { name, version, resolvedSpec, integrity: normalizeOptionalString(rec.integrity), shasum: normalizeOptionalString(rec.shasum) } }; } function parseNpmPackJsonOutput(raw) { const trimmed = raw.trim(); if (!trimmed) return null; const candidates = [trimmed]; const arrayStart = trimmed.indexOf("["); if (arrayStart > 0) candidates.push(trimmed.slice(arrayStart)); for (const candidate of candidates) { let parsed; try { parsed = JSON.parse(candidate); } catch { continue; } const entries = resolveNpmJsonEntries(parsed); let fallback = null; for (let i = entries.length - 1; i >= 0; i -= 1) { const normalized = normalizeNpmPackEntry(entries[i]); if (!normalized) continue; if (!fallback) fallback = normalized; if (normalized.filename) return normalized; } if (fallback) return fallback; } return null; } async function findPackedArchiveInDir(cwd) { const archives = (await fs.readdir(cwd, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isFile() && entry.name.endsWith(".tgz")); return archives.length === 1 ? archives[0]?.name : void 0; } /** Packs an npm spec into a tarball in `cwd` and returns archive metadata. */ async function packNpmSpecToArchive(params) { const res = await runCommandWithTimeout([ "npm", "pack", params.spec, "--ignore-scripts", "--json", "--dry-run=false", `--pack-destination=${params.cwd}` ], { timeoutMs: Math.max(params.timeoutMs, 3e5), signal: params.signal, killProcessTree: true, cwd: params.cwd, env: createNpmMetadataEnv({ npmConfigCwd: params.cwd }) }); if (res.code !== 0) { const raw = formatNpmCommandFailureOutput(res); if (/E404|is not in this registry/i.test(raw)) return { ok: false, error: `Package not found on npm: ${params.spec}. See https://docs.openclaw.ai/tools/plugin for installable plugins.` }; return { ok: false, error: `npm pack failed: ${raw}` }; } const parsedJson = parseNpmPackJsonOutput(res.stdout || ""); const packed = parsedJson?.filename ?? await findPackedArchiveInDir(params.cwd); if (!packed) return { ok: false, error: "npm pack produced no archive" }; const archivePath = path.isAbsolute(packed) ? packed : path.join(params.cwd, packed); if (!await pathExists(archivePath)) return { ok: false, error: "npm pack produced no archive" }; return { ok: true, archivePath, metadata: parsedJson?.metadata ?? {} }; } /** * Reads package metadata from an existing npm archive using `npm pack --dry-run`. * The archive path is validated first so callers get path errors before npm errors. */ async function resolveNpmPackArchiveMetadata(params) { const archivePathResult = await resolveArchiveSourcePath(params.archivePath); if (!archivePathResult.ok) return archivePathResult; const archivePath = archivePathResult.path; const archiveStat = await fs.stat(archivePath).catch(() => null); const archiveMetadataTimeoutMs = archiveStat && archiveStat.size > 104857600 ? 3e5 : 6e4; const res = await runCommandWithTimeout([ "npm", "pack", archivePath, "--ignore-scripts", "--dry-run", "--json" ], { timeoutMs: Math.max(params.timeoutMs ?? archiveMetadataTimeoutMs, archiveMetadataTimeoutMs), signal: params.signal, killProcessTree: true, env: createNpmMetadataEnv() }); if (res.code !== 0) return { ok: false, error: `npm pack metadata read failed: ${formatNpmCommandFailureOutput(res)}` }; const parsedJson = parseNpmPackJsonOutput(res.stdout || ""); if (!parsedJson?.metadata.name || !parsedJson.metadata.version) return { ok: false, error: "npm pack metadata read produced incomplete package metadata" }; return { ok: true, archivePath, tarballName: parsedJson.filename ?? path.basename(archivePath), metadata: parsedJson.metadata }; } //#endregion export { resolveArchiveSourcePath as a, withInstallWorkspace as c, packNpmSpecToArchive as i, createNpmMetadataEnv as n, resolveNpmPackArchiveMetadata as o, formatNpmCommandFailureOutput as r, resolveNpmSpecMetadata as s, buildNpmResolutionFields as t };