- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
215 lines
9.0 KiB
JavaScript
Executable File
215 lines
9.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import { createHash } from "node:crypto";
|
|
import { readFile, readdir, realpath, stat, writeFile } from "node:fs/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import { relative, resolve, sep } from "node:path";
|
|
import {
|
|
compareNativeSdkPaths,
|
|
hasNativeSdkFamily,
|
|
NATIVE_SDK_FAMILIES,
|
|
NATIVE_SDK_HEADER_INVENTORY_SCHEMA_VERSION,
|
|
NATIVE_SDK_HEADER_INVENTORY_SEMANTICS,
|
|
} from "./native-sdk-header-inventory-contract.mjs";
|
|
|
|
function inventoryError(message) {
|
|
const error = new Error(message);
|
|
error.code = "NATIVE_SDK_INVENTORY_INVALID";
|
|
return error;
|
|
}
|
|
|
|
function sha256(value) {
|
|
return createHash("sha256").update(value).digest("hex");
|
|
}
|
|
|
|
function normalRelativePath(value, label) {
|
|
if (typeof value !== "string" || !value || value.includes("\0")) {
|
|
throw inventoryError(`${label} must be a non-empty relative path`);
|
|
}
|
|
const normalized = value.replaceAll("\\", "/");
|
|
if (normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized) || normalized.split("/").includes("..")) {
|
|
throw inventoryError(`${label} must stay relative to the SDK root`);
|
|
}
|
|
const segments = normalized.split("/");
|
|
if (normalized !== "." && segments.some((segment) => segment === "" || segment === ".")) {
|
|
throw inventoryError(`${label} must use a canonical relative path`);
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function stringOption(value, label, maximum = 512) {
|
|
if (typeof value !== "string" || !value.trim() || value.length > maximum || value.includes("\0")) {
|
|
throw inventoryError(`${label} must be a non-empty string of at most ${maximum} characters`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function assertInside(root, candidate, label) {
|
|
const value = resolve(candidate);
|
|
if (value !== root && !value.startsWith(`${root}${sep}`)) {
|
|
throw inventoryError(`${label} must stay inside the SDK root`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
async function requiredDirectory(path, label) {
|
|
let value;
|
|
try { value = await stat(path); } catch { throw inventoryError(`${label} does not exist`); }
|
|
if (!value.isDirectory()) throw inventoryError(`${label} must be a directory`);
|
|
}
|
|
|
|
async function requiredFile(path, label) {
|
|
let value;
|
|
try { value = await stat(path); } catch { throw inventoryError(`${label} does not exist`); }
|
|
if (!value.isFile()) throw inventoryError(`${label} must be a file`);
|
|
}
|
|
|
|
async function resolvedPathInside(root, candidate, label) {
|
|
let resolvedPath;
|
|
try { resolvedPath = await realpath(candidate); } catch { throw inventoryError(`${label} does not exist`); }
|
|
return assertInside(root, resolvedPath, label);
|
|
}
|
|
|
|
async function resolvedDirectoryInside(root, candidate, label) {
|
|
const path = await resolvedPathInside(root, candidate, label);
|
|
await requiredDirectory(path, label);
|
|
return path;
|
|
}
|
|
|
|
async function listHeaders(root, includeDirectories) {
|
|
const headers = [];
|
|
const visit = async (directory) => {
|
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
entries.sort((left, right) => compareNativeSdkPaths(left.name, right.name));
|
|
for (const entry of entries) {
|
|
const candidate = resolve(directory, entry.name);
|
|
if (entry.isSymbolicLink()) throw inventoryError(`SDK header inventory refuses symbolic link: ${relative(root, candidate)}`);
|
|
const path = await resolvedPathInside(root, candidate, `SDK entry ${relative(root, candidate)}`);
|
|
if (entry.isDirectory()) await visit(path);
|
|
else if (entry.isFile() && /\.(h|hpp)$/i.test(entry.name)) {
|
|
const content = await readFile(path);
|
|
headers.push({
|
|
path: relative(root, path).split(sep).join("/"),
|
|
bytes: content.length,
|
|
sha256: sha256(content),
|
|
});
|
|
}
|
|
}
|
|
};
|
|
for (const includeDirectory of includeDirectories) {
|
|
const path = await resolvedDirectoryInside(root, resolve(root, includeDirectory), `include directory ${includeDirectory}`);
|
|
await visit(path);
|
|
}
|
|
headers.sort((left, right) => compareNativeSdkPaths(left.path, right.path));
|
|
if (headers.length === 0) throw inventoryError("No C/C++ headers were found in the selected SDK include directories");
|
|
if (new Set(headers.map((header) => header.path)).size !== headers.length) {
|
|
throw inventoryError("SDK include directories overlap and produced duplicate headers");
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
export async function generateNativeSdkHeaderInventory(options) {
|
|
const sdk = options?.sdk;
|
|
const family = hasNativeSdkFamily(sdk) ? NATIVE_SDK_FAMILIES[sdk] : undefined;
|
|
if (!family) throw inventoryError("sdk must be uxp-hybrid or premiere-prsdk");
|
|
const sdkVersion = stringOption(options?.sdkVersion, "sdkVersion", 128);
|
|
const suppliedSdkRoot = resolve(stringOption(options?.sdkRoot, "sdkRoot", 4096));
|
|
const archivePath = resolve(stringOption(options?.archivePath, "archivePath", 4096));
|
|
await requiredDirectory(suppliedSdkRoot, "sdkRoot");
|
|
const sdkRoot = await realpath(suppliedSdkRoot);
|
|
await requiredFile(archivePath, "archivePath");
|
|
|
|
const suppliedDirectories = options?.includeDirectories ?? [];
|
|
if (!Array.isArray(suppliedDirectories) || suppliedDirectories.some((value) => typeof value !== "string")) {
|
|
throw inventoryError("includeDirectories must be an array of relative paths");
|
|
}
|
|
const includeDirectories = family.includeDirectories ?? suppliedDirectories.map((value) => normalRelativePath(value, "include directory"));
|
|
if (family.includeDirectories && suppliedDirectories.length > 0) {
|
|
throw inventoryError(`${sdk} has fixed documented include directories; do not pass includeDirectories`);
|
|
}
|
|
if (includeDirectories.length === 0) {
|
|
throw inventoryError("premiere-prsdk requires one or more explicit includeDirectories from the licensed SDK documentation");
|
|
}
|
|
if (new Set(includeDirectories).size !== includeDirectories.length) {
|
|
throw inventoryError("includeDirectories must not contain duplicates");
|
|
}
|
|
const headers = await listHeaders(sdkRoot, includeDirectories);
|
|
for (const requiredHeader of family.requiredHeaders) {
|
|
if (!headers.some((header) => header.path === requiredHeader)) {
|
|
throw inventoryError(`Missing required UXP Hybrid SDK header: ${requiredHeader}`);
|
|
}
|
|
}
|
|
const archive = await readFile(archivePath);
|
|
return {
|
|
schemaVersion: NATIVE_SDK_HEADER_INVENTORY_SCHEMA_VERSION,
|
|
source: {
|
|
sdk,
|
|
sdkVersion,
|
|
authorityUrl: family.authorityUrl,
|
|
archiveSha256: sha256(archive),
|
|
inventoryScope: "header_files_only",
|
|
includeDirectories,
|
|
},
|
|
semantics: NATIVE_SDK_HEADER_INVENTORY_SEMANTICS,
|
|
stats: {
|
|
headers: headers.length,
|
|
bytes: headers.reduce((total, header) => total + header.bytes, 0),
|
|
},
|
|
headers,
|
|
};
|
|
}
|
|
|
|
function parseArguments(argv) {
|
|
const options = { includeDirectories: [] };
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const argument = argv[index];
|
|
if (argument === "--check") options.check = true;
|
|
else if (argument === "--validate-only") options.validateOnly = true;
|
|
else if (["--sdk", "--sdk-version", "--sdk-root", "--archive", "--output", "--include-dir"].includes(argument)) {
|
|
const value = argv[index + 1];
|
|
if (value == null || value.startsWith("--")) throw inventoryError(`${argument} requires a value`);
|
|
index += 1;
|
|
if (argument === "--sdk") options.sdk = value;
|
|
else if (argument === "--sdk-version") options.sdkVersion = value;
|
|
else if (argument === "--sdk-root") options.sdkRoot = value;
|
|
else if (argument === "--archive") options.archivePath = value;
|
|
else if (argument === "--output") options.outputPath = value;
|
|
else options.includeDirectories.push(value);
|
|
} else {
|
|
throw inventoryError(`Unknown argument: ${argument}`);
|
|
}
|
|
}
|
|
if (options.check && options.validateOnly) throw inventoryError("--check and --validate-only cannot be combined");
|
|
if (!options.validateOnly && !options.outputPath) throw inventoryError("--output is required unless --validate-only is used");
|
|
return options;
|
|
}
|
|
|
|
async function main() {
|
|
const options = parseArguments(process.argv.slice(2));
|
|
const inventory = await generateNativeSdkHeaderInventory(options);
|
|
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
|
if (options.validateOnly) {
|
|
process.stdout.write(`Validated ${inventory.stats.headers} ${inventory.source.sdk} header files.\n`);
|
|
return;
|
|
}
|
|
const outputPath = resolve(options.outputPath);
|
|
if (options.check) {
|
|
let current = "";
|
|
try { current = await readFile(outputPath, "utf8"); } catch {}
|
|
if (current.replaceAll("\r\n", "\n") !== rendered) {
|
|
throw inventoryError("Native SDK header inventory is stale; rerun without --check after reviewing the licensed SDK artifact");
|
|
}
|
|
process.stdout.write(`Native SDK header inventory is current: ${inventory.stats.headers} ${inventory.source.sdk} header files.\n`);
|
|
return;
|
|
}
|
|
await writeFile(outputPath, rendered);
|
|
process.stdout.write(`Wrote ${inventory.stats.headers} ${inventory.source.sdk} header files.\n`);
|
|
}
|
|
|
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
main().catch((error) => {
|
|
process.stderr.write(`${error.message}\n`);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|