← Files Cecil-IA Labs FFmpegARCHIVED FILE
dist/environment/readiness.js
10.8 KB · Sep 30, 2026 · 23:17 UTC
import os from "node:os";
import path from "node:path";
import { runCommand } from "../core/command-result.js";
import { resolveBinary } from "../core/binary-resolver.js";
import { isToolkitRuntimeError } from "../core/errors.js";
import { inspectWritablePath, resolveSafePath } from "../skill-runtime/paths.js";
import { getExecutionContext, contextGuidance } from "../skill-runtime/context.js";
import { inspectEnvironmentCapabilities } from "./capabilities.js";
import { resolveExecutable } from "./executable.js";
import { inspectEnvironmentVersions } from "./version.js";
function versionFrom(execution) {
return [execution.stdout, execution.stderr]
.flatMap((value) => value?.split(/\r?\n/) ?? [])
.map((line) => line.trim())
.find((line) => line.length > 0);
}
function nodeIsSupported(version) {
const major = Number(/^v?(\d+)/.exec(version)?.[1] ?? "0");
return Number.isInteger(major) && major >= 22;
}
function warning(warnings, code, message, details) {
warnings.push({ code, message, ...(details !== undefined ? { details } : {}) });
}
function missingReason(kind, error) {
if (isToolkitRuntimeError(error))
return error.message;
return `${kind} could not be resolved.`;
}
function plannedCommands(options) {
const ffmpeg = options.ffmpegPath ?? "ffmpeg";
const ffprobe = options.ffprobePath ?? "ffprobe";
return [
"node --version",
"npm --version",
`${ffmpeg} -version`,
`${ffprobe} -version`,
`${ffmpeg} -hide_banner -codecs`,
`${ffmpeg} -hide_banner -encoders`,
`${ffmpeg} -hide_banner -decoders`,
`${ffmpeg} -hide_banner -filters`,
`${ffmpeg} -hide_banner -hwaccels`,
];
}
function statusFor(context, versions, capabilities, warnings, planned) {
if (planned || !context.canExecuteScripts)
return "planned";
if (!versions?.ffmpeg.version || !versions.ffprobe.version)
return "blocked";
if (versions.compatible === false || capabilities === undefined)
return "warning";
return warnings.length > 0 ? "warning" : "ready";
}
function summarizeVersions(report) {
return {
planned: report.planned,
minimumSupportedFFmpeg: report.minimumSupportedFFmpeg,
ffmpeg: {
path: report.ffmpeg.path,
...(report.ffmpeg.version !== undefined ? { version: report.ffmpeg.version.version } : {}),
},
ffprobe: {
path: report.ffprobe.path,
...(report.ffprobe.version !== undefined ? { version: report.ffprobe.version.version } : {}),
},
...(report.compatible !== undefined ? { compatible: report.compatible } : {}),
warnings: report.warnings,
};
}
function summarizeCapabilities(report) {
return {
planned: report.planned,
ffmpegPath: report.ffmpegPath,
codecs: report.codecs.map((item) => item.name),
encoders: report.encoders.map((item) => item.name),
decoders: report.decoders.map((item) => item.name),
filters: report.filters.map((item) => item.name),
hardwareAcceleration: report.hardwareAcceleration,
plannedCommands: [...report.plannedCommands],
};
}
/**
* Inspect the host without installing packages or changing shell state.
* Missing FFmpeg-family binaries are reported as readiness facts, not thrown
* away as an opaque command failure.
*/
export async function inspectEnvironmentReadiness(options = {}) {
const environment = options.environment ?? process.env;
const cwd = resolveSafePath(options.cwd ?? process.cwd(), { allowAbsolute: true });
const context = getExecutionContext(options.context, environment);
const warnings = [];
const next = contextGuidance(context);
const planned = options.dryRun === true || !context.canExecuteScripts;
const nodeCompatible = nodeIsSupported(process.version);
const report = {
status: planned ? "planned" : "blocked",
context,
observed: {
platform: process.platform,
architecture: process.arch,
hostname: os.hostname(),
cwd,
canInspectFiles: context.canInspectFiles,
canExecuteScripts: context.canExecuteScripts,
canInstallDependencies: context.canInstallDependencies,
},
runtime: {
node: {
available: true,
path: process.execPath,
version: process.version,
minimumSupported: "22.0.0",
compatible: nodeCompatible,
},
npm: { available: false },
},
toolkit: { available: false },
ffmpeg: { available: false, minimumSupported: "6.1" },
ffprobe: { available: false },
plannedCommands: planned ? plannedCommands(options) : [],
warnings,
next,
};
if (!nodeCompatible) {
warning(warnings, "W_ENV_UNSUPPORTED_NODE", `Node.js ${process.version} is below the supported minimum 22.0.0.`);
report.next.push("Use Node.js 22 or newer, then repeat the environment check.");
}
if (planned) {
warning(warnings, "W_CONTEXT_NOT_EXECUTED", "Environment commands were not executed in this context.");
if (context.name === "chatgpt-regular") {
report.next.push("npm exec --yes --package=@cecilialabs/ffmpeg -- cecilia-ffmpeg environment check --json");
}
return report;
}
const npmPath = await resolveExecutable("npm", { cwd, env: environment });
if (npmPath !== undefined) {
const npmExecution = await runCommand({ binary: npmPath, args: ["--version"], cwd, env: environment }, {
...(options.signal !== undefined ? { signal: options.signal } : {}),
...(options.verbose !== undefined ? { verbose: options.verbose } : {}),
});
const npmVersion = versionFrom(npmExecution);
report.runtime.npm = {
available: npmExecution.exitCode === 0,
path: npmPath,
...(npmVersion !== undefined ? { version: npmVersion } : {}),
};
}
else {
warning(warnings, "W_ENV_NPM_NOT_FOUND", "npm is not available on PATH; package installation cannot be performed.");
report.next.push("Install Node.js 22+ with npm, then repeat the environment check.");
}
const toolkitPath = await resolveExecutable("cecilia-ffmpeg", {
cwd,
env: environment,
...(options.toolkitPath !== undefined ? { explicitPath: options.toolkitPath } : {}),
});
if (toolkitPath !== undefined)
report.toolkit = { available: true, path: toolkitPath };
else
report.next.push("Install @cecilialabs/ffmpeg globally, locally, or use the explicit npm exec fallback.");
let ffmpegPath;
try {
ffmpegPath = await resolveBinary({
kind: "ffmpeg",
...(options.ffmpegPath !== undefined ? { explicitPath: options.ffmpegPath } : {}),
cwd,
env: environment,
});
report.ffmpeg = { available: true, path: ffmpegPath, minimumSupported: "6.1" };
}
catch (error) {
report.ffmpeg = {
available: false,
minimumSupported: "6.1",
error: missingReason("ffmpeg", error),
};
warning(warnings, "W_ENV_FFMPEG_NOT_FOUND", report.ffmpeg.error ?? "FFmpeg is unavailable.");
report.next.push("Install FFmpeg 6.1+ or provide an explicit --ffmpeg-path.");
}
let ffprobePath;
try {
ffprobePath = await resolveBinary({
kind: "ffprobe",
...(options.ffprobePath !== undefined ? { explicitPath: options.ffprobePath } : {}),
cwd,
env: environment,
});
report.ffprobe = { available: true, path: ffprobePath };
}
catch (error) {
report.ffprobe = { available: false, error: missingReason("ffprobe", error) };
warning(warnings, "W_ENV_FFPROBE_NOT_FOUND", report.ffprobe.error ?? "FFprobe is unavailable.");
report.next.push("Install FFprobe alongside FFmpeg or provide an explicit --ffprobe-path.");
}
let versions;
let capabilities;
if (ffmpegPath !== undefined && ffprobePath !== undefined) {
try {
versions = await inspectEnvironmentVersions({
ffmpegPath,
ffprobePath,
...(options.signal !== undefined ? { signal: options.signal } : {}),
...(options.verbose !== undefined ? { verbose: options.verbose } : {}),
});
report.versions = summarizeVersions(versions);
report.ffmpeg = {
...report.ffmpeg,
...(versions.ffmpeg.version !== undefined
? { version: versions.ffmpeg.version.version }
: {}),
...(versions.compatible !== undefined ? { compatible: versions.compatible } : {}),
};
report.ffprobe = {
...report.ffprobe,
...(versions.ffprobe.version !== undefined
? { version: versions.ffprobe.version.version }
: {}),
};
for (const item of versions.warnings)
warning(warnings, item.code, item.message, item.details);
if (versions.compatible === false) {
report.next.push("Use FFmpeg 6.1 or newer for the supported toolkit runtime.");
}
}
catch (error) {
warning(warnings, "W_ENV_VERSION_CHECK_FAILED", "FFmpeg/FFprobe version inspection failed.", {
message: error instanceof Error ? error.message : String(error),
});
}
try {
capabilities = await inspectEnvironmentCapabilities({
ffmpegPath,
...(options.signal !== undefined ? { signal: options.signal } : {}),
...(options.verbose !== undefined ? { verbose: options.verbose } : {}),
});
report.capabilities = summarizeCapabilities(capabilities);
}
catch (error) {
warning(warnings, "W_ENV_CAPABILITY_CHECK_FAILED", "FFmpeg capability inspection failed.", {
message: error instanceof Error ? error.message : String(error),
});
}
}
if (options.outputPath !== undefined) {
try {
report.output = await inspectWritablePath(options.outputPath, { cwd });
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
warning(warnings, "W_OUTPUT_PATH_UNWRITABLE", message, {
path: path.resolve(cwd, options.outputPath),
});
report.next.push("Choose an output path whose parent directory exists and is writable.");
}
}
report.status = statusFor(context, versions, capabilities, warnings, false);
return report;
}
//# sourceMappingURL=readiness.js.mapSHA-256: c93323e794405cfeb78f1a723ed71360152aaf2fdb61a00cbd8924c3063a2ae3