feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
This commit is contained in:
+208
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { lstat, readFile, realpath, writeFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { relative, resolve, sep } from "node:path";
|
||||
import {
|
||||
UXP_HYBRID_ADDON_AUTHORITY_URL,
|
||||
UXP_HYBRID_ADDON_ENTRYPOINT_PATH,
|
||||
UXP_HYBRID_ADDON_RECEIPT_SCHEMA_VERSION,
|
||||
UXP_HYBRID_ADDON_RECEIPT_SEMANTICS,
|
||||
UXP_HYBRID_ADDON_TARGETS,
|
||||
} from "./uxp-hybrid-addon-receipt-contract.mjs";
|
||||
import {
|
||||
canonicalNativeSdkHeaderInventorySha256,
|
||||
verifyNativeSdkHeaderInventory,
|
||||
} from "./verify-native-sdk-header-inventory.mjs";
|
||||
import { verifyUxpHybridAddonReceipt } from "./verify-uxp-hybrid-addon-receipt.mjs";
|
||||
|
||||
const MAX_ARTIFACT_BYTES = 2 ** 31;
|
||||
const MAX_ENTRYPOINT_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
function receiptError(message) {
|
||||
const error = new Error(message);
|
||||
error.code = "UXP_HYBRID_ADDON_RECEIPT_INVALID";
|
||||
return error;
|
||||
}
|
||||
|
||||
function stringOption(value, label, maximum = 4096) {
|
||||
if (typeof value !== "string" || !value.trim() || value.length > maximum || value.includes("\0")) {
|
||||
throw receiptError(`${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 receiptError(`${label} must stay inside the plugin root`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function addonName(value) {
|
||||
if (typeof value !== "string" || !/^[A-Za-z0-9._-]+\.uxpaddon$/.test(value)) {
|
||||
throw receiptError("manifest addon.name must be a simple .uxpaddon filename");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function requiredPluginFile(root, candidate, label) {
|
||||
const requestedPath = assertInside(root, candidate, label);
|
||||
let requestedStat;
|
||||
try { requestedStat = await lstat(requestedPath); } catch { throw receiptError(`${label} does not exist`); }
|
||||
if (requestedStat.isSymbolicLink()) throw receiptError(`${label} must not be a symbolic link`);
|
||||
if (!requestedStat.isFile()) throw receiptError(`${label} must be a file`);
|
||||
let canonicalPath;
|
||||
try { canonicalPath = await realpath(requestedPath); } catch { throw receiptError(`${label} does not exist`); }
|
||||
assertInside(root, canonicalPath, label);
|
||||
return { path: canonicalPath, size: requestedStat.size };
|
||||
}
|
||||
|
||||
async function sha256File(path) {
|
||||
const hash = createHash("sha256");
|
||||
await new Promise((resolveHash, rejectHash) => {
|
||||
const stream = createReadStream(path);
|
||||
stream.on("data", (chunk) => hash.update(chunk));
|
||||
stream.on("error", rejectHash);
|
||||
stream.on("end", resolveHash);
|
||||
});
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
async function readDevelopmentManifest(root) {
|
||||
const file = await requiredPluginFile(root, resolve(root, "manifest.json"), "manifest.json");
|
||||
if (file.size > 1024 * 1024) throw receiptError("manifest.json is too large");
|
||||
let manifest;
|
||||
try { manifest = JSON.parse(await readFile(file.path, "utf8")); } catch { throw receiptError("manifest.json must be readable JSON"); }
|
||||
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) throw receiptError("manifest.json must be an object");
|
||||
if (!Number.isInteger(manifest.manifestVersion) || manifest.manifestVersion < 6) {
|
||||
throw receiptError("manifest.json must declare manifestVersion 6 or newer");
|
||||
}
|
||||
if (manifest.host?.app !== "premierepro") throw receiptError("manifest.json host.app must be premierepro");
|
||||
if (!/^\d+\.\d+\.\d+$/.test(String(manifest.host?.minVersion || ""))) {
|
||||
throw receiptError("manifest.json host.minVersion must be a semantic version");
|
||||
}
|
||||
const name = addonName(manifest.addon?.name);
|
||||
if (manifest.requiredPermissions?.enableAddon !== true) throw receiptError("manifest.json requiredPermissions.enableAddon must be true");
|
||||
return {
|
||||
manifestVersion: manifest.manifestVersion,
|
||||
hostApp: manifest.host.app,
|
||||
hostMinVersion: manifest.host.minVersion,
|
||||
addonName: name,
|
||||
enableAddon: true,
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateUxpHybridAddonReceipt(options) {
|
||||
const suppliedPluginRoot = resolve(stringOption(options?.pluginRoot, "pluginRoot"));
|
||||
let pluginRoot;
|
||||
try { pluginRoot = await realpath(suppliedPluginRoot); } catch { throw receiptError("pluginRoot does not exist"); }
|
||||
let rootStat;
|
||||
try { rootStat = await lstat(pluginRoot); } catch { throw receiptError("pluginRoot does not exist"); }
|
||||
if (!rootStat.isDirectory()) throw receiptError("pluginRoot must be a directory");
|
||||
|
||||
if (!options?.sdkHeaderReceipt || typeof options.sdkHeaderReceipt !== "object") {
|
||||
throw receiptError("sdkHeaderReceipt is required");
|
||||
}
|
||||
const sdkSummary = verifyNativeSdkHeaderInventory(options.sdkHeaderReceipt);
|
||||
if (sdkSummary.sdk !== "uxp-hybrid") throw receiptError("sdkHeaderReceipt must identify uxp-hybrid");
|
||||
|
||||
const manifest = await readDevelopmentManifest(pluginRoot);
|
||||
const entrypointFile = await requiredPluginFile(pluginRoot, resolve(pluginRoot, UXP_HYBRID_ADDON_ENTRYPOINT_PATH), UXP_HYBRID_ADDON_ENTRYPOINT_PATH);
|
||||
if (!Number.isSafeInteger(entrypointFile.size) || entrypointFile.size <= 0 || entrypointFile.size > MAX_ENTRYPOINT_BYTES) {
|
||||
throw receiptError(`${UXP_HYBRID_ADDON_ENTRYPOINT_PATH} must be a non-empty file no larger than ${MAX_ENTRYPOINT_BYTES} bytes`);
|
||||
}
|
||||
const entrypoint = {
|
||||
path: UXP_HYBRID_ADDON_ENTRYPOINT_PATH,
|
||||
bytes: entrypointFile.size,
|
||||
sha256: await sha256File(entrypointFile.path),
|
||||
};
|
||||
const artifacts = [];
|
||||
for (const target of UXP_HYBRID_ADDON_TARGETS) {
|
||||
const relativePath = `${target.pathPrefix}/${manifest.addonName}`;
|
||||
const file = await requiredPluginFile(pluginRoot, resolve(pluginRoot, relativePath), `addon artifact ${target.target}`);
|
||||
if (!Number.isSafeInteger(file.size) || file.size <= 0 || file.size > MAX_ARTIFACT_BYTES) {
|
||||
throw receiptError(`addon artifact ${target.target} must be a non-empty file no larger than ${MAX_ARTIFACT_BYTES} bytes`);
|
||||
}
|
||||
artifacts.push({ target: target.target, path: relativePath, bytes: file.size, sha256: await sha256File(file.path) });
|
||||
}
|
||||
|
||||
const receipt = {
|
||||
schemaVersion: UXP_HYBRID_ADDON_RECEIPT_SCHEMA_VERSION,
|
||||
source: {
|
||||
sdk: "uxp-hybrid",
|
||||
sdkVersion: options.sdkHeaderReceipt.source.sdkVersion,
|
||||
sdkHeaderReceiptSha256: canonicalNativeSdkHeaderInventorySha256(options.sdkHeaderReceipt),
|
||||
authorityUrl: UXP_HYBRID_ADDON_AUTHORITY_URL,
|
||||
},
|
||||
manifest,
|
||||
entrypoint,
|
||||
semantics: UXP_HYBRID_ADDON_RECEIPT_SEMANTICS,
|
||||
stats: {
|
||||
artifacts: artifacts.length,
|
||||
addonBytes: artifacts.reduce((total, artifact) => total + artifact.bytes, 0),
|
||||
entrypoints: 1,
|
||||
entrypointBytes: entrypoint.bytes,
|
||||
},
|
||||
artifacts,
|
||||
};
|
||||
verifyUxpHybridAddonReceipt(receipt, { sdkHeaderReceipt: options.sdkHeaderReceipt });
|
||||
return receipt;
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const options = {};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index];
|
||||
if (["--plugin-root", "--sdk-header-receipt", "--output"].includes(argument)) {
|
||||
const value = argv[++index];
|
||||
if (!value || value.startsWith("--")) throw receiptError(`${argument} requires a value`);
|
||||
if (argument === "--plugin-root") options.pluginRoot = value;
|
||||
else if (argument === "--sdk-header-receipt") options.sdkHeaderReceiptPath = value;
|
||||
else options.outputPath = value;
|
||||
} else if (argument === "--check") options.check = true;
|
||||
else if (argument === "--validate-only") options.validateOnly = true;
|
||||
else throw receiptError(`Unknown argument: ${argument}`);
|
||||
}
|
||||
if (options.check && options.validateOnly) throw receiptError("--check and --validate-only cannot be combined");
|
||||
if (!options.pluginRoot || !options.sdkHeaderReceiptPath) throw receiptError("--plugin-root and --sdk-header-receipt are required");
|
||||
if (!options.validateOnly && !options.outputPath) throw receiptError("--output is required unless --validate-only is used");
|
||||
return options;
|
||||
}
|
||||
|
||||
async function readSdkHeaderReceipt(path) {
|
||||
try { return JSON.parse(await readFile(resolve(path), "utf8")); } catch { throw receiptError("sdkHeaderReceipt must be a readable JSON receipt"); }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
const receipt = await generateUxpHybridAddonReceipt({
|
||||
pluginRoot: options.pluginRoot,
|
||||
sdkHeaderReceipt: await readSdkHeaderReceipt(options.sdkHeaderReceiptPath),
|
||||
});
|
||||
const rendered = `${JSON.stringify(receipt, null, 2)}\n`;
|
||||
if (options.validateOnly) {
|
||||
process.stdout.write(`Validated ${receipt.stats.artifacts} UXP Hybrid addon artifacts and ${receipt.stats.entrypoints} entrypoint.\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 receiptError("UXP Hybrid addon receipt is stale; rerun without --check after reviewing the local development bundle");
|
||||
}
|
||||
process.stdout.write(`UXP Hybrid addon receipt is current: ${receipt.stats.artifacts} artifacts and ${receipt.stats.entrypoints} entrypoint.\n`);
|
||||
return;
|
||||
}
|
||||
await writeFile(outputPath, rendered);
|
||||
process.stdout.write(`Wrote ${receipt.stats.artifacts} UXP Hybrid addon artifacts and ${receipt.stats.entrypoints} entrypoint.\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user