





















@@ -0,0 +1,152 @@
1+import fs from "node:fs";
2+import path from "node:path";
3+import { readRuntimeDepsJsonObject, type JsonObject } from "./bundled-runtime-deps-json.js";
4+import {
5+collectPackageRuntimeDeps,
6+normalizeRuntimeDepSpecs,
7+parseInstallableRuntimeDep,
8+parseInstallableRuntimeDepSpec,
9+resolveDependencySentinelAbsolutePath,
10+} from "./bundled-runtime-deps-specs.js";
11+import { satisfies } from "./semver.runtime.js";
12+13+const LEGACY_RETAINED_RUNTIME_DEPS_MANIFEST = ".openclaw-runtime-deps.json";
14+15+export function readGeneratedInstallManifestSpecs(installRoot: string): string[] | null {
16+const parsed = readRuntimeDepsJsonObject(path.join(installRoot, "package.json"));
17+if (parsed?.name !== "openclaw-runtime-deps-install") {
18+return null;
19+}
20+const dependencies = parsed.dependencies;
21+if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) {
22+return [];
23+}
24+const specs: string[] = [];
25+for (const [name, version] of Object.entries(dependencies as Record<string, unknown>)) {
26+const dep = parseInstallableRuntimeDep(name, version);
27+if (dep) {
28+specs.push(`${dep.name}@${dep.version}`);
29+}
30+}
31+return normalizeRuntimeDepSpecs(specs);
32+}
33+34+function readPackageRuntimeDepSpecs(packageRoot: string): string[] | null {
35+const parsed = readRuntimeDepsJsonObject(path.join(packageRoot, "package.json"));
36+if (!parsed || parsed.name === "openclaw-runtime-deps-install") {
37+return null;
38+}
39+const specs = Object.entries(collectPackageRuntimeDeps(parsed))
40+.map(([name, rawVersion]) => parseInstallableRuntimeDep(name, rawVersion))
41+.filter((dep): dep is { name: string; version: string } => Boolean(dep))
42+.map((dep) => `${dep.name}@${dep.version}`);
43+return normalizeRuntimeDepSpecs(specs);
44+}
45+46+function sameRuntimeDepSpecs(left: readonly string[], right: readonly string[]): boolean {
47+const normalizedLeft = normalizeRuntimeDepSpecs(left);
48+const normalizedRight = normalizeRuntimeDepSpecs(right);
49+return (
50+normalizedLeft.length === normalizedRight.length &&
51+normalizedLeft.every((entry, index) => entry === normalizedRight[index])
52+);
53+}
54+55+function readInstalledRuntimeDepVersion(rootDir: string, depName: string): string | null {
56+try {
57+const parsed = JSON.parse(
58+fs.readFileSync(resolveDependencySentinelAbsolutePath(rootDir, depName), "utf8"),
59+) as unknown;
60+if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
61+return null;
62+}
63+const version = (parsed as JsonObject).version;
64+return typeof version === "string" && version.trim() ? version.trim() : null;
65+} catch {
66+return null;
67+}
68+}
69+70+export function isRuntimeDepSatisfied(
71+rootDir: string,
72+dep: { name: string; version: string },
73+): boolean {
74+const installedVersion = readInstalledRuntimeDepVersion(rootDir, dep.name);
75+return Boolean(installedVersion && satisfies(installedVersion, dep.version));
76+}
77+78+export function isRuntimeDepSatisfiedInAnyRoot(
79+dep: { name: string; version: string },
80+roots: readonly string[],
81+): boolean {
82+return roots.some((root) => isRuntimeDepSatisfied(root, dep));
83+}
84+85+function hasSatisfiedInstallSpecPackages(rootDir: string, specs: readonly string[]): boolean {
86+return specs
87+.map(parseInstallableRuntimeDepSpec)
88+.every((dep) => isRuntimeDepSatisfied(rootDir, dep));
89+}
90+91+export function isRuntimeDepsPlanMaterialized(
92+installRoot: string,
93+installSpecs: readonly string[],
94+): boolean {
95+const generatedManifestSpecs = readGeneratedInstallManifestSpecs(installRoot);
96+const packageManifestSpecs =
97+generatedManifestSpecs !== null ? null : readPackageRuntimeDepSpecs(installRoot);
98+return (
99+((generatedManifestSpecs !== null &&
100+sameRuntimeDepSpecs(generatedManifestSpecs, installSpecs)) ||
101+(packageManifestSpecs !== null && sameRuntimeDepSpecs(packageManifestSpecs, installSpecs))) &&
102+hasSatisfiedInstallSpecPackages(installRoot, installSpecs)
103+);
104+}
105+106+export function assertBundledRuntimeDepsInstalled(rootDir: string, specs: readonly string[]): void {
107+const missingSpecs = specs.filter((spec) => {
108+const dep = parseInstallableRuntimeDepSpec(spec);
109+return !isRuntimeDepSatisfied(rootDir, dep);
110+});
111+if (missingSpecs.length === 0) {
112+return;
113+}
114+throw new Error(
115+`package manager install did not place bundled runtime deps in ${rootDir}: ${missingSpecs.join(", ")}`,
116+);
117+}
118+119+export function removeLegacyRuntimeDepsManifest(installRoot: string): void {
120+fs.rmSync(path.join(installRoot, LEGACY_RETAINED_RUNTIME_DEPS_MANIFEST), {
121+force: true,
122+});
123+}
124+125+function createNpmInstallExecutionManifest(installSpecs: readonly string[]): JsonObject {
126+const dependencies: Record<string, string> = {};
127+for (const spec of installSpecs) {
128+const dep = parseInstallableRuntimeDepSpec(spec);
129+dependencies[dep.name] = dep.version;
130+}
131+const sortedDependencies = Object.fromEntries(
132+Object.entries(dependencies).toSorted(([left], [right]) => left.localeCompare(right)),
133+);
134+return {
135+name: "openclaw-runtime-deps-install",
136+private: true,
137+ ...(Object.keys(sortedDependencies).length > 0 ? { dependencies: sortedDependencies } : {}),
138+};
139+}
140+141+export function ensureNpmInstallExecutionManifest(
142+installExecutionRoot: string,
143+installSpecs: readonly string[] = [],
144+): void {
145+const manifestPath = path.join(installExecutionRoot, "package.json");
146+const manifest = createNpmInstallExecutionManifest(installSpecs);
147+const nextContents = `${JSON.stringify(manifest, null, 2)}\n`;
148+if (fs.existsSync(manifestPath) && fs.readFileSync(manifestPath, "utf8") === nextContents) {
149+return;
150+}
151+fs.writeFileSync(manifestPath, nextContents, "utf8");
152+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。