























@@ -1,3 +1,4 @@
1+import type { Dirent } from "node:fs";
12import fs from "node:fs/promises";
23import path from "node:path";
34import { runCommandWithTimeout } from "../process/exec.js";
@@ -24,6 +25,7 @@ type HostPackageManifest = {
24252526type ManagedNpmRootOpenClawMetadata = {
2627managedOverrides?: string[];
28+managedPeerDependencies?: string[];
2729[key: string]: unknown;
2830};
2931@@ -86,9 +88,17 @@ function readManagedOverrideKeys(value: unknown): string[] {
8688return value.managedOverrides.filter((key): key is string => typeof key === "string");
8789}
889091+function readManagedPeerDependencyKeys(value: unknown): string[] {
92+if (!isRecord(value) || !Array.isArray(value.managedPeerDependencies)) {
93+return [];
94+}
95+return value.managedPeerDependencies.filter((key): key is string => typeof key === "string");
96+}
97+8998function buildManagedOpenClawMetadata(params: {
9099current: unknown;
91100managedOverrideKeys: string[];
101+managedPeerDependencyKeys?: string[];
92102}): ManagedNpmRootOpenClawMetadata | undefined {
93103const metadata: ManagedNpmRootOpenClawMetadata = isRecord(params.current)
94104 ? { ...params.current }
@@ -98,6 +108,12 @@ function buildManagedOpenClawMetadata(params: {
98108} else {
99109delete metadata.managedOverrides;
100110}
111+const managedPeerDependencyKeys = params.managedPeerDependencyKeys;
112+if (managedPeerDependencyKeys && managedPeerDependencyKeys.length > 0) {
113+metadata.managedPeerDependencies = managedPeerDependencyKeys;
114+} else if (managedPeerDependencyKeys) {
115+delete metadata.managedPeerDependencies;
116+}
101117return Object.keys(metadata).length > 0 ? metadata : undefined;
102118}
103119@@ -243,6 +259,174 @@ export async function upsertManagedNpmRootDependency(params: {
243259await writeJson(manifestPath, next, { trailingNewline: true });
244260}
245261262+async function readPackageJsonIfExists(
263+packageDir: string,
264+): Promise<Record<string, unknown> | null> {
265+try {
266+const parsed = JSON.parse(await fs.readFile(path.join(packageDir, "package.json"), "utf8"));
267+return isRecord(parsed) ? parsed : null;
268+} catch (err) {
269+if ((err as NodeJS.ErrnoException).code === "ENOENT") {
270+return null;
271+}
272+throw err;
273+}
274+}
275+276+async function readPackageVersion(packageDir: string): Promise<string | undefined> {
277+const parsed = await readPackageJsonIfExists(packageDir);
278+return parsed ? readOptionalString(parsed.version) : undefined;
279+}
280+281+function isOptionalPeerDependency(manifest: Record<string, unknown>, peerName: string): boolean {
282+if (!isRecord(manifest.peerDependenciesMeta)) {
283+return false;
284+}
285+const peerMetadata = manifest.peerDependenciesMeta[peerName];
286+return isRecord(peerMetadata) && peerMetadata.optional === true;
287+}
288+289+async function listNodeModulesPackageDirs(nodeModulesDir: string): Promise<string[]> {
290+let entries: Dirent[];
291+try {
292+entries = await fs.readdir(nodeModulesDir, { withFileTypes: true });
293+} catch (err) {
294+if (
295+(err as NodeJS.ErrnoException).code === "ENOENT" ||
296+(err as NodeJS.ErrnoException).code === "ENOTDIR"
297+) {
298+return [];
299+}
300+throw err;
301+}
302+const packageDirs: string[] = [];
303+for (const entry of entries.toSorted((left, right) => left.name.localeCompare(right.name))) {
304+if (entry.name === ".bin" || entry.name.startsWith(".")) {
305+continue;
306+}
307+const entryPath = path.join(nodeModulesDir, entry.name);
308+if (entry.name.startsWith("@") && entry.isDirectory()) {
309+let scopedEntries: Dirent[];
310+try {
311+scopedEntries = await fs.readdir(entryPath, { withFileTypes: true });
312+} catch (err) {
313+if (
314+(err as NodeJS.ErrnoException).code === "ENOENT" ||
315+(err as NodeJS.ErrnoException).code === "ENOTDIR"
316+) {
317+continue;
318+}
319+throw err;
320+}
321+for (const scopedEntry of scopedEntries.toSorted((left, right) =>
322+left.name.localeCompare(right.name),
323+)) {
324+if (scopedEntry.isDirectory() || scopedEntry.isSymbolicLink()) {
325+packageDirs.push(path.join(entryPath, scopedEntry.name));
326+}
327+}
328+continue;
329+}
330+if (entry.isDirectory() || entry.isSymbolicLink()) {
331+packageDirs.push(entryPath);
332+}
333+}
334+return packageDirs;
335+}
336+337+async function collectManagedNpmRootPeerDependencyPins(params: {
338+npmRoot: string;
339+}): Promise<Record<string, string>> {
340+const pins = new Map<string, string>();
341+const queue = await listNodeModulesPackageDirs(path.join(params.npmRoot, "node_modules"));
342+for (let index = 0; index < queue.length; index += 1) {
343+const packageDir = queue[index];
344+if (!packageDir) {
345+continue;
346+}
347+const manifest = await readPackageJsonIfExists(packageDir);
348+if (manifest) {
349+if (readOptionalString(manifest.name) === "openclaw") {
350+continue;
351+}
352+const peerDependencies = readDependencyRecord(manifest.peerDependencies);
353+for (const [peerName, peerRange] of Object.entries(peerDependencies)) {
354+if (peerName === "openclaw" || pins.has(peerName)) {
355+continue;
356+}
357+const installedVersion = await readPackageVersion(
358+path.join(params.npmRoot, "node_modules", ...peerName.split("/")),
359+);
360+if (!installedVersion && isOptionalPeerDependency(manifest, peerName)) {
361+continue;
362+}
363+pins.set(peerName, installedVersion ?? peerRange);
364+}
365+}
366+queue.push(...(await listNodeModulesPackageDirs(path.join(packageDir, "node_modules"))));
367+}
368+return Object.fromEntries(
369+[...pins.entries()].toSorted(([left], [right]) => left.localeCompare(right)),
370+);
371+}
372+373+export async function syncManagedNpmRootPeerDependencies(params: {
374+npmRoot: string;
375+managedOverrides?: Record<string, unknown>;
376+omitUnsupportedManagedOverrides?: boolean;
377+}): Promise<boolean> {
378+const manifestPath = path.join(params.npmRoot, "package.json");
379+const manifest = await readManagedNpmRootManifest(manifestPath);
380+const dependencies = readDependencyRecord(manifest.dependencies);
381+const previousManagedPeerDependencies = readManagedPeerDependencyKeys(manifest.openclaw);
382+const peerPins = await collectManagedNpmRootPeerDependencyPins({ npmRoot: params.npmRoot });
383+const nextDependencies = { ...dependencies };
384+for (const packageName of previousManagedPeerDependencies) {
385+if (!Object.hasOwn(peerPins, packageName)) {
386+delete nextDependencies[packageName];
387+}
388+}
389+for (const [packageName, dependencySpec] of Object.entries(peerPins)) {
390+nextDependencies[packageName] = dependencies[packageName] ?? dependencySpec;
391+}
392+393+const managedOverrides = params.omitUnsupportedManagedOverrides
394+ ? filterUnsupportedManagedNpmRootOverrides(params.managedOverrides)
395+ : readOverrideRecord(params.managedOverrides);
396+const managedOverrideKeys = Object.keys(managedOverrides).toSorted();
397+const overrides = readOverrideRecord(manifest.overrides);
398+for (const key of readManagedOverrideKeys(manifest.openclaw)) {
399+delete overrides[key];
400+}
401+Object.assign(overrides, managedOverrides);
402+const managedPeerDependencyKeys = Object.keys(peerPins).toSorted();
403+const openclawMetadata = buildManagedOpenClawMetadata({
404+current: manifest.openclaw,
405+ managedOverrideKeys,
406+ managedPeerDependencyKeys,
407+});
408+const next: ManagedNpmRootManifest = {
409+ ...manifest,
410+private: true,
411+dependencies: nextDependencies,
412+};
413+if (Object.keys(overrides).length > 0) {
414+next.overrides = overrides;
415+} else {
416+delete next.overrides;
417+}
418+if (openclawMetadata) {
419+next.openclaw = openclawMetadata;
420+} else {
421+delete next.openclaw;
422+}
423+const changed = JSON.stringify(next) !== JSON.stringify(manifest);
424+if (changed) {
425+await writeJson(manifestPath, next, { trailingNewline: true });
426+}
427+return changed;
428+}
429+246430export async function repairManagedNpmRootOpenClawPeer(params: {
247431npmRoot: string;
248432timeoutMs?: number;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。