660 lines
20 KiB
TypeScript
660 lines
20 KiB
TypeScript
import { spawn } from 'node:child_process';
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
renameSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import path from 'node:path';
|
|
import type { GatewayLaunchContext } from './config-sync';
|
|
import { logger } from '../utils/logger';
|
|
|
|
const DEFAULT_RUNTIME_DEPS_PREFLIGHT_TIMEOUT_MS = 10 * 60_000;
|
|
const MAX_CAPTURED_OUTPUT_LENGTH = 16_000;
|
|
const OPENCLAW_BUNDLED_RUNTIME_DEPENDENCY_NAMES = [
|
|
'@agentclientprotocol/claude-agent-acp',
|
|
'@agentclientprotocol/sdk',
|
|
'@anthropic-ai/sdk',
|
|
'@anthropic-ai/vertex-sdk',
|
|
'@aws-sdk/client-bedrock',
|
|
'@aws-sdk/client-bedrock-runtime',
|
|
'@aws-sdk/client-s3',
|
|
'@aws-sdk/credential-provider-node',
|
|
'@aws-sdk/s3-request-presigner',
|
|
'@aws/bedrock-token-generator',
|
|
'@azure/identity',
|
|
'@clack/prompts',
|
|
'@clawdbot/lobster',
|
|
'@discordjs/voice',
|
|
'@google/genai',
|
|
'@grammyjs/runner',
|
|
'@grammyjs/transformer-throttler',
|
|
'@homebridge/ciao',
|
|
'@lancedb/lancedb',
|
|
'@larksuiteoapi/node-sdk',
|
|
'@line/bot-sdk',
|
|
'@lydell/node-pty',
|
|
'@mariozechner/pi-agent-core',
|
|
'@mariozechner/pi-ai',
|
|
'@mariozechner/pi-coding-agent',
|
|
'@matrix-org/matrix-sdk-crypto-nodejs',
|
|
'@matrix-org/matrix-sdk-crypto-wasm',
|
|
'@microsoft/teams.api',
|
|
'@microsoft/teams.apps',
|
|
'@modelcontextprotocol/sdk',
|
|
'@mozilla/readability',
|
|
'@openai/codex',
|
|
'@opentelemetry/api',
|
|
'@opentelemetry/api-logs',
|
|
'@opentelemetry/exporter-logs-otlp-proto',
|
|
'@opentelemetry/exporter-metrics-otlp-proto',
|
|
'@opentelemetry/exporter-trace-otlp-proto',
|
|
'@opentelemetry/resources',
|
|
'@opentelemetry/sdk-logs',
|
|
'@opentelemetry/sdk-metrics',
|
|
'@opentelemetry/sdk-node',
|
|
'@opentelemetry/sdk-trace-base',
|
|
'@opentelemetry/semantic-conventions',
|
|
'@pierre/diffs',
|
|
'@pierre/theme',
|
|
'@slack/bolt',
|
|
'@slack/web-api',
|
|
'@tencent-connect/qqbot-connector',
|
|
'@tloncorp/tlon-skill',
|
|
'@twurple/api',
|
|
'@twurple/auth',
|
|
'@twurple/chat',
|
|
'@urbit/aura',
|
|
'@whiskeysockets/baileys',
|
|
'@zed-industries/codex-acp',
|
|
'acpx',
|
|
'ajv',
|
|
'chokidar',
|
|
'commander',
|
|
'croner',
|
|
'discord-api-types',
|
|
'dotenv',
|
|
'express',
|
|
'fake-indexeddb',
|
|
'gaxios',
|
|
'global-agent',
|
|
'google-auth-library',
|
|
'grammy',
|
|
'https-proxy-agent',
|
|
'jimp',
|
|
'jiti',
|
|
'json5',
|
|
'jsonwebtoken',
|
|
'jszip',
|
|
'jwks-rsa',
|
|
'linkedom',
|
|
'markdown-it',
|
|
'matrix-js-sdk',
|
|
'minimatch',
|
|
'mpg123-decoder',
|
|
'music-metadata',
|
|
'node-edge-tts',
|
|
'nostr-tools',
|
|
'openai',
|
|
'openshell',
|
|
'opusscript',
|
|
'pdfjs-dist',
|
|
'playwright-core',
|
|
'semver',
|
|
'sharp',
|
|
'silk-wasm',
|
|
'sqlite-vec',
|
|
'tar',
|
|
'tokenjuice',
|
|
'tslog',
|
|
'typebox',
|
|
'undici',
|
|
'web-push',
|
|
'ws',
|
|
'yaml',
|
|
'zca-js',
|
|
'zod',
|
|
] as const;
|
|
|
|
const preflightPromises = new Map<string, Promise<void>>();
|
|
|
|
interface OpenClawRuntimeDepsReport {
|
|
installRoot?: string;
|
|
deps?: Array<{ name?: unknown; version?: unknown }>;
|
|
missing?: unknown[];
|
|
repairedSpecs?: unknown[];
|
|
}
|
|
|
|
function appendOutput(current: string, chunk: Buffer): string {
|
|
const next = current + chunk.toString('utf8');
|
|
if (next.length <= MAX_CAPTURED_OUTPUT_LENGTH) return next;
|
|
return next.slice(next.length - MAX_CAPTURED_OUTPUT_LENGTH);
|
|
}
|
|
|
|
function resolveNodeRunner(launchContext: GatewayLaunchContext): {
|
|
command: string;
|
|
env: Record<string, string | undefined>;
|
|
} {
|
|
const bundledNodePath = launchContext.forkEnv.YINIAN_NODE_EXEC_PATH;
|
|
if (bundledNodePath && existsSync(bundledNodePath)) {
|
|
return {
|
|
command: bundledNodePath,
|
|
env: { ...launchContext.forkEnv },
|
|
};
|
|
}
|
|
|
|
return {
|
|
command: process.execPath,
|
|
env: {
|
|
...launchContext.forkEnv,
|
|
ELECTRON_RUN_AS_NODE: '1',
|
|
},
|
|
};
|
|
}
|
|
|
|
async function runRuntimeDepsRepair(
|
|
launchContext: GatewayLaunchContext,
|
|
timeoutMs = DEFAULT_RUNTIME_DEPS_PREFLIGHT_TIMEOUT_MS,
|
|
): Promise<void> {
|
|
const inspected = await runPluginsDepsCommand(launchContext, [
|
|
'plugins',
|
|
'deps',
|
|
'--json',
|
|
'--package-root',
|
|
launchContext.openclawDir,
|
|
], timeoutMs);
|
|
|
|
const removedStaleRoot = maybeRemoveStaleInstallRoot(launchContext, inspected.report);
|
|
const missingBefore = Array.isArray(inspected.report.missing) ? inspected.report.missing.length : 0;
|
|
const shouldRepair = removedStaleRoot || missingBefore > 0;
|
|
|
|
let finalReport = inspected.report;
|
|
let localMaterialized = false;
|
|
if (shouldRepair) {
|
|
localMaterialized = tryMaterializeRuntimeDepsFromLocalPackages(launchContext, inspected.report);
|
|
finalReport = (await runPluginsDepsCommand(launchContext, [
|
|
'plugins',
|
|
'deps',
|
|
'--json',
|
|
'--package-root',
|
|
launchContext.openclawDir,
|
|
], timeoutMs)).report;
|
|
|
|
const missingAfterLocal = Array.isArray(finalReport.missing) ? finalReport.missing.length : 0;
|
|
if (missingAfterLocal > 0 || !localMaterialized) {
|
|
finalReport = (await runPluginsDepsCommand(launchContext, [
|
|
'plugins',
|
|
'deps',
|
|
'--repair',
|
|
'--json',
|
|
'--package-root',
|
|
launchContext.openclawDir,
|
|
], timeoutMs)).report;
|
|
}
|
|
}
|
|
|
|
const missingAfter = Array.isArray(finalReport.missing) ? finalReport.missing.length : 0;
|
|
const repairedCount = Array.isArray(finalReport.repairedSpecs) ? finalReport.repairedSpecs.length : 0;
|
|
if (missingAfter > 0) {
|
|
throw new Error(`OpenClaw runtime dependency preflight still has ${missingAfter} missing dependency/dependencies after repair`);
|
|
}
|
|
|
|
logger.info(`[plugins] OpenClaw runtime dependency preflight complete (missing=${missingAfter}, repaired=${repairedCount}, reset=${removedStaleRoot ? 'yes' : 'no'}, local=${localMaterialized ? 'yes' : 'no'})`);
|
|
}
|
|
|
|
function runPluginsDepsCommand(
|
|
launchContext: GatewayLaunchContext,
|
|
args: string[],
|
|
timeoutMs: number,
|
|
): Promise<{ report: OpenClawRuntimeDepsReport; stdout: string; stderr: string }> {
|
|
const runner = resolveNodeRunner(launchContext);
|
|
const commandArgs = [launchContext.entryScript, ...args];
|
|
|
|
logger.info(`[plugins] Checking OpenClaw runtime dependencies before Gateway start (packageRoot=${launchContext.openclawDir})`);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(runner.command, commandArgs, {
|
|
cwd: launchContext.openclawDir,
|
|
env: {
|
|
...runner.env,
|
|
OPENCLAW_NO_RESPAWN: '1',
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
windowsHide: true,
|
|
});
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
let settled = false;
|
|
|
|
const timeout = setTimeout(() => {
|
|
if (settled) return;
|
|
settled = true;
|
|
child.kill('SIGTERM');
|
|
reject(new Error(`OpenClaw runtime dependency preflight timed out after ${timeoutMs}ms`));
|
|
}, timeoutMs);
|
|
timeout.unref?.();
|
|
|
|
child.stdout?.on('data', (chunk: Buffer) => {
|
|
stdout = appendOutput(stdout, chunk);
|
|
});
|
|
child.stderr?.on('data', (chunk: Buffer) => {
|
|
stderr = appendOutput(stderr, chunk);
|
|
});
|
|
|
|
child.on('error', (error) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timeout);
|
|
reject(error);
|
|
});
|
|
|
|
child.on('close', (code, signal) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timeout);
|
|
|
|
if (code === 0 && !signal) {
|
|
try {
|
|
const parsed = JSON.parse(stdout) as OpenClawRuntimeDepsReport;
|
|
resolve({ report: parsed, stdout, stderr });
|
|
} catch (error) {
|
|
reject(new Error([
|
|
'OpenClaw runtime dependency preflight returned invalid JSON',
|
|
error instanceof Error ? error.message : String(error),
|
|
stderr.trim(),
|
|
stdout.trim(),
|
|
].filter(Boolean).join('\n')));
|
|
}
|
|
return;
|
|
}
|
|
|
|
reject(new Error([
|
|
`OpenClaw runtime dependency preflight failed (code=${code ?? 'null'}, signal=${signal ?? 'none'})`,
|
|
stderr.trim(),
|
|
stdout.trim(),
|
|
].filter(Boolean).join('\n')));
|
|
});
|
|
});
|
|
}
|
|
|
|
function maybeRemoveStaleInstallRoot(
|
|
launchContext: GatewayLaunchContext,
|
|
report: OpenClawRuntimeDepsReport,
|
|
): boolean {
|
|
const installRoot = typeof report.installRoot === 'string' ? report.installRoot : '';
|
|
if (!installRoot) return false;
|
|
|
|
const resolvedInstallRoot = path.resolve(installRoot);
|
|
const resolvedOpenClawDir = path.resolve(launchContext.openclawDir);
|
|
if (resolvedInstallRoot === resolvedOpenClawDir) return false;
|
|
|
|
const desiredDeps = createDesiredDependencyMap(report, launchContext);
|
|
if (desiredDeps.size === 0) return false;
|
|
|
|
const manifestPath = path.join(installRoot, 'package.json');
|
|
if (!existsSync(manifestPath)) return false;
|
|
|
|
let currentDeps: Record<string, string>;
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
|
|
name?: unknown;
|
|
dependencies?: unknown;
|
|
};
|
|
if (parsed.name !== 'openclaw-runtime-deps-install') {
|
|
logger.warn(`[plugins] Removing stale OpenClaw runtime dependency root with unexpected manifest: ${installRoot}`);
|
|
rmSync(installRoot, { recursive: true, force: true });
|
|
return true;
|
|
}
|
|
currentDeps = isPlainStringRecord(parsed.dependencies) ? parsed.dependencies : {};
|
|
} catch (error) {
|
|
logger.warn(`[plugins] Removing unreadable OpenClaw runtime dependency root: ${installRoot}`, error);
|
|
rmSync(installRoot, { recursive: true, force: true });
|
|
return true;
|
|
}
|
|
|
|
if (dependencyMapContainsAll(currentDeps, desiredDeps)) return false;
|
|
|
|
logger.warn(`[plugins] Resetting stale OpenClaw runtime dependency root (expected=${desiredDeps.size}, current=${Object.keys(currentDeps).length}): ${installRoot}`);
|
|
rmSync(installRoot, { recursive: true, force: true });
|
|
return true;
|
|
}
|
|
|
|
function tryMaterializeRuntimeDepsFromLocalPackages(
|
|
launchContext: GatewayLaunchContext,
|
|
report: OpenClawRuntimeDepsReport,
|
|
): boolean {
|
|
const installRoot = typeof report.installRoot === 'string' ? report.installRoot : '';
|
|
if (!installRoot) return false;
|
|
|
|
const resolvedInstallRoot = path.resolve(installRoot);
|
|
const resolvedOpenClawDir = path.resolve(launchContext.openclawDir);
|
|
if (resolvedInstallRoot === resolvedOpenClawDir) return false;
|
|
|
|
const desiredDeps = createDesiredDependencyMap(report, launchContext);
|
|
if (desiredDeps.size === 0) return false;
|
|
|
|
const tempRoot = `${installRoot}.tmp-${process.pid}-${Date.now()}`;
|
|
try {
|
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
mkdirSync(path.join(tempRoot, 'node_modules'), { recursive: true });
|
|
|
|
const localPackages = collectLocalRuntimePackageDirs(
|
|
launchContext,
|
|
Array.from(desiredDeps.keys()),
|
|
[
|
|
...readLocalPackageDependencyNames(launchContext.openclawDir).map((name) => ({
|
|
name,
|
|
parentDir: launchContext.openclawDir,
|
|
})),
|
|
...readLocalPackageDependencyNames(process.cwd()).map((name) => ({
|
|
name,
|
|
parentDir: process.cwd(),
|
|
})),
|
|
...listNodeModulesPackageNames(path.join(launchContext.openclawDir, 'node_modules')).map((name) => ({
|
|
name,
|
|
})),
|
|
],
|
|
);
|
|
if (localPackages.missingRootName) {
|
|
logger.warn(`[plugins] Local OpenClaw runtime dependency not found, falling back to npm repair: ${localPackages.missingRootName}`);
|
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
return false;
|
|
}
|
|
|
|
for (const [depName, packageDir] of localPackages.packageDirs) {
|
|
const destination = path.join(tempRoot, 'node_modules', ...depName.split('/'));
|
|
mkdirSync(path.dirname(destination), { recursive: true });
|
|
symlinkSync(packageDir, destination, process.platform === 'win32' ? 'junction' : 'dir');
|
|
}
|
|
|
|
writeFileSync(
|
|
path.join(tempRoot, 'package.json'),
|
|
`${JSON.stringify({
|
|
name: 'openclaw-runtime-deps-install',
|
|
private: true,
|
|
dependencies: Object.fromEntries(
|
|
Array.from(desiredDeps.entries()).sort(([left], [right]) => left.localeCompare(right)),
|
|
),
|
|
}, null, 2)}\n`,
|
|
'utf8',
|
|
);
|
|
|
|
rmSync(installRoot, { recursive: true, force: true });
|
|
mkdirSync(path.dirname(installRoot), { recursive: true });
|
|
renameSync(tempRoot, installRoot);
|
|
logger.info(`[plugins] Materialized OpenClaw runtime dependencies from bundled/local packages (${desiredDeps.size} direct deps, ${localPackages.packageDirs.size} packages): ${installRoot}`);
|
|
return true;
|
|
} catch (error) {
|
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
logger.warn('[plugins] Failed to materialize OpenClaw runtime dependencies from local packages; falling back to npm repair:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function collectLocalRuntimePackageDirs(
|
|
launchContext: GatewayLaunchContext,
|
|
rootNames: string[],
|
|
preferredItems: Array<{ name: string; parentDir?: string }> = [],
|
|
): {
|
|
packageDirs: Map<string, string>;
|
|
missingRootName: string | null;
|
|
} {
|
|
const packageDirs = new Map<string, string>();
|
|
const seen = new Set<string>();
|
|
const rootSet = new Set(rootNames);
|
|
const queue: Array<{ name: string; parentDir?: string }> = [
|
|
...rootNames.map((name) => ({ name })),
|
|
...preferredItems.filter((item) => !rootSet.has(item.name)),
|
|
];
|
|
|
|
for (let index = 0; index < queue.length; index += 1) {
|
|
const item = queue[index];
|
|
if (seen.has(item.name) || isOptionalNativeClipboardPackage(item.name)) continue;
|
|
|
|
const packageDir = item.parentDir
|
|
? resolvePackageDirFromPackage(item.parentDir, item.name) ?? resolveLocalPackageDir(launchContext, item.name)
|
|
: resolveLocalPackageDir(launchContext, item.name);
|
|
|
|
if (!packageDir) {
|
|
if (rootSet.has(item.name)) return { packageDirs, missingRootName: item.name };
|
|
continue;
|
|
}
|
|
|
|
seen.add(item.name);
|
|
packageDirs.set(item.name, packageDir);
|
|
|
|
for (const depName of readLocalPackageDependencyNames(packageDir)) {
|
|
if (!seen.has(depName) && !isOptionalNativeClipboardPackage(depName)) {
|
|
queue.push({ name: depName, parentDir: packageDir });
|
|
}
|
|
}
|
|
}
|
|
|
|
return { packageDirs, missingRootName: null };
|
|
}
|
|
|
|
function listNodeModulesPackageNames(nodeModulesDir: string): string[] {
|
|
if (!existsSync(nodeModulesDir)) return [];
|
|
|
|
const names: string[] = [];
|
|
try {
|
|
for (const entry of readdirSafe(nodeModulesDir)) {
|
|
if (entry === '.bin') continue;
|
|
const entryPath = path.join(nodeModulesDir, entry);
|
|
if (entry.startsWith('@')) {
|
|
for (const scopedEntry of readdirSafe(entryPath)) {
|
|
names.push(`${entry}/${scopedEntry}`);
|
|
}
|
|
} else {
|
|
names.push(entry);
|
|
}
|
|
}
|
|
} catch {
|
|
return [];
|
|
}
|
|
return Array.from(new Set(names)).sort((left, right) => left.localeCompare(right));
|
|
}
|
|
|
|
function readdirSafe(dir: string): string[] {
|
|
try {
|
|
return existsSync(dir) ? readdirSync(dir) : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function isOptionalNativeClipboardPackage(name: string): boolean {
|
|
return name === '@mariozechner/clipboard' || name.startsWith('@mariozechner/clipboard-');
|
|
}
|
|
|
|
function readLocalPackageDependencyNames(packageDir: string): string[] {
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as {
|
|
dependencies?: unknown;
|
|
optionalDependencies?: unknown;
|
|
};
|
|
return Array.from(new Set([
|
|
...Object.keys(isPlainStringRecord(parsed.dependencies) ? parsed.dependencies : {}),
|
|
...Object.keys(isPlainStringRecord(parsed.optionalDependencies) ? parsed.optionalDependencies : {}),
|
|
])).sort((left, right) => left.localeCompare(right));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function resolvePackageDirFromPackage(parentPackageDir: string, packageName: string): string | null {
|
|
try {
|
|
const req = createRequire(path.join(parentPackageDir, 'package.json'));
|
|
try {
|
|
return path.dirname(req.resolve(`${packageName}/package.json`));
|
|
} catch {
|
|
const entryPath = req.resolve(packageName);
|
|
return findPackageRootForEntry(entryPath, packageName);
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function resolveLocalPackageDir(
|
|
launchContext: GatewayLaunchContext,
|
|
packageName: string,
|
|
): string | null {
|
|
const directCandidates = [
|
|
path.join(launchContext.openclawDir, 'node_modules', ...packageName.split('/')),
|
|
path.join(process.cwd(), 'node_modules', ...packageName.split('/')),
|
|
];
|
|
for (const candidate of directCandidates) {
|
|
if (existsSync(path.join(candidate, 'package.json'))) return candidate;
|
|
}
|
|
|
|
const requireRoots = [
|
|
path.join(launchContext.openclawDir, 'package.json'),
|
|
path.join(process.cwd(), 'package.json'),
|
|
];
|
|
|
|
for (const root of requireRoots) {
|
|
try {
|
|
const req = createRequire(root);
|
|
try {
|
|
return path.dirname(req.resolve(`${packageName}/package.json`));
|
|
} catch {
|
|
const entryPath = req.resolve(packageName);
|
|
const packageDir = findPackageRootForEntry(entryPath, packageName);
|
|
if (packageDir) return packageDir;
|
|
}
|
|
} catch {
|
|
// Try the next require root.
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function findPackageRootForEntry(entryPath: string, packageName: string): string | null {
|
|
let current = path.dirname(entryPath);
|
|
while (current && current !== path.dirname(current)) {
|
|
const manifestPath = path.join(current, 'package.json');
|
|
if (existsSync(manifestPath)) {
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(manifestPath, 'utf8')) as { name?: unknown };
|
|
if (parsed.name === packageName) return current;
|
|
} catch {
|
|
// Keep walking.
|
|
}
|
|
}
|
|
current = path.dirname(current);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function createDesiredDependencyMap(
|
|
report: OpenClawRuntimeDepsReport,
|
|
launchContext: GatewayLaunchContext,
|
|
): Map<string, string> {
|
|
const desired = new Map<string, string>();
|
|
|
|
if (Array.isArray(report.deps)) {
|
|
for (const dep of report.deps) {
|
|
if (!dep || typeof dep.name !== 'string' || typeof dep.version !== 'string') continue;
|
|
desired.set(dep.name, dep.version);
|
|
}
|
|
}
|
|
|
|
for (const depName of OPENCLAW_BUNDLED_RUNTIME_DEPENDENCY_NAMES) {
|
|
if (desired.has(depName)) continue;
|
|
const spec = readLocalDependencySpec(launchContext, depName) ?? readLocalPackageVersionSpec(launchContext, depName);
|
|
if (spec) {
|
|
desired.set(depName, spec);
|
|
}
|
|
}
|
|
return desired;
|
|
}
|
|
|
|
function readLocalDependencySpec(
|
|
launchContext: GatewayLaunchContext,
|
|
packageName: string,
|
|
): string | null {
|
|
const roots = [process.cwd(), launchContext.openclawDir];
|
|
for (const root of roots) {
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')) as {
|
|
dependencies?: unknown;
|
|
devDependencies?: unknown;
|
|
optionalDependencies?: unknown;
|
|
};
|
|
for (const entries of [parsed.dependencies, parsed.devDependencies, parsed.optionalDependencies]) {
|
|
if (isPlainStringRecord(entries) && entries[packageName]) {
|
|
return entries[packageName];
|
|
}
|
|
}
|
|
} catch {
|
|
// Try the next manifest.
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function readLocalPackageVersionSpec(
|
|
launchContext: GatewayLaunchContext,
|
|
packageName: string,
|
|
): string | null {
|
|
const packageDir = resolveLocalPackageDir(launchContext, packageName);
|
|
if (!packageDir) return null;
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as {
|
|
version?: unknown;
|
|
};
|
|
return typeof parsed.version === 'string' && parsed.version.trim() ? parsed.version.trim() : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function isPlainStringRecord(value: unknown): value is Record<string, string> {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
return Object.values(value).every((entry) => typeof entry === 'string');
|
|
}
|
|
|
|
function dependencyMapContainsAll(current: Record<string, string>, desired: Map<string, string>): boolean {
|
|
for (const [key, version] of desired) {
|
|
if (current[key] !== version) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export async function ensureOpenClawRuntimeDepsReady(
|
|
launchContext: GatewayLaunchContext,
|
|
): Promise<void> {
|
|
if (process.env.YINIAN_SKIP_OPENCLAW_RUNTIME_DEPS_PREFLIGHT === '1') {
|
|
logger.warn('[plugins] Skipping OpenClaw runtime dependency preflight via YINIAN_SKIP_OPENCLAW_RUNTIME_DEPS_PREFLIGHT=1');
|
|
return;
|
|
}
|
|
|
|
const key = `${launchContext.entryScript}::${launchContext.openclawDir}`;
|
|
const existing = preflightPromises.get(key);
|
|
if (existing) {
|
|
await existing;
|
|
return;
|
|
}
|
|
|
|
const promise = runRuntimeDepsRepair(launchContext);
|
|
preflightPromises.set(key, promise);
|
|
try {
|
|
await promise;
|
|
} catch (error) {
|
|
preflightPromises.delete(key);
|
|
throw error;
|
|
} finally {
|
|
preflightPromises.delete(key);
|
|
}
|
|
}
|