Files
jhonny-editor/code/uxp-plugin/workflows.cjs
T
João Henrique b541f502ba 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
2026-09-08 09:59:31 -04:00

1670 lines
107 KiB
JavaScript
Executable File

(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.PremiereMcpWorkflows = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
const MAX_SELECTION_ITEMS = 64;
const MAX_METADATA_CHARS = 350000;
const MAX_METADATA_RESULT_BYTES = 900000;
// A Project-panel schema is XML and can expand materially when represented
// in the JSON bridge request (quotes and backslashes are escaped). Keep each
// exact stale guard and replacement far below the protocol ceiling instead
// of accepting a large pair that cannot be safely serialized together.
const MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES = 12 * 1024;
function utf8ByteLength(value) {
let bytes = 0;
for (let i = 0; i < value.length; i += 1) {
const code = value.charCodeAt(i);
if (code < 0x80) bytes += 1;
else if (code < 0x800) bytes += 2;
else if (code >= 0xD800 && code <= 0xDBFF && i + 1 < value.length &&
value.charCodeAt(i + 1) >= 0xDC00 && value.charCodeAt(i + 1) <= 0xDFFF) {
bytes += 4;
i += 1;
} else bytes += 3;
}
return bytes;
}
function createWorkflowDefinitions(deps) {
const ppro = deps.ppro, Protocol = deps.Protocol, workspace = deps.workspace;
const projectPanelMetadataMutationTails = new Map();
const definitions = {
"effects.catalog": { readOnly: true, minHostVersion: "25.6.0", probe: canUseEffects, handler: effectCatalog },
"effects.chain.get": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canUseEffects, handler: effectChain },
"trackItem.identity.inspect": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canInspectTrackItemIdentity, handler: inspectTrackItemIdentity },
"effects.chain.add": { destructive: true, undoable: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canUseEffects, handler: addEffect },
"effects.chain.remove": { destructive: true, undoable: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canUseEffects, handler: removeEffect },
"selection.inspect": { readOnly: true, minHostVersion: "25.6.0", probe: canUseSelection, handler: inspectSelection },
"selection.fingerprints.inspect": { readOnly: true, minHostVersion: "25.6.0", probe: canUseSelection, handler: inspectSelectionFingerprints },
"selection.targets.inspect": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canUseSelection, handler: inspectSelectionTargets },
"selection.update": { idempotent: true, minHostVersion: "25.6.0", probe: canManageSelection, handler: updateSelection },
"effects.selection.add": { destructive: true, undoable: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canUseEffectsSelection, handler: addEffectToSelection },
"effects.selection.remove": { destructive: true, undoable: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canUseEffectsSelection, handler: removeEffectFromSelection },
"sceneEdit.detect": { destructive: true, undoable: false, minHostVersion: "26.3.0", probe: canDetectScenes, handler: detectScenes },
"proxy.inspect": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canUseClipItems, handler: inspectProxy },
"proxy.attach": { destructive: true, undoable: false, idempotent: true, targetCapabilityProbe: true, requiresWorkspace: true, minHostVersion: "25.6.0", probe: canAttachProxy, handler: attachProxy },
"ingest.get": { readOnly: true, minHostVersion: "25.6.0", probe: canUseIngest, handler: getIngest },
"ingest.configure": { destructive: true, undoable: true, idempotent: true, minHostVersion: "25.6.0", probe: canUseIngest, handler: configureIngest },
"media.relink": { destructive: true, undoable: false, idempotent: true, targetCapabilityProbe: true, requiresWorkspace: true, minHostVersion: "25.6.0", probe: canRelink, handler: relinkMedia },
"metadata.get": { readOnly: true, minHostVersion: "25.6.0", probe: canUseMetadata, handler: getMetadata },
"metadata.update": { destructive: true, undoable: true, idempotent: true, minHostVersion: "25.6.0", probe: canUseMetadata, handler: updateMetadata },
"metadata.columns.get": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canGetProjectColumnsMetadata, handler: getProjectColumnsMetadata },
"metadata.projectPanel.get": { readOnly: true, minHostVersion: "25.6.0", probe: canGetProjectPanelMetadata, handler: getProjectPanelMetadata },
"metadata.projectPanel.update": { destructive: true, undoable: false, idempotent: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canSetProjectPanelMetadata, handler: updateProjectPanelMetadata },
"metadata.projectSchema.inspect": { readOnly: true, minHostVersion: "25.6.0", probe: canCreateProjectMetadataSchema, handler: inspectProjectMetadataSchema },
"metadata.projectSchema.create": { destructive: true, undoable: false, idempotent: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canCreateProjectMetadataSchema, handler: createProjectMetadataField },
"color.preflight": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canInspectColor, handler: colorPreflight },
"environment.inspect": { readOnly: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canInspectEnvironment, handler: inspectEnvironment },
"footage.conform": { destructive: true, undoable: true, idempotent: true, targetCapabilityProbe: true, minHostVersion: "25.6.0", probe: canConformFootage, handler: conformFootage },
"sourceMonitor.state": { readOnly: true, minHostVersion: "25.6.0", probe: canInspectSourceMonitor, handler: sourceMonitorState },
"sourceMonitor.open": { idempotent: true, conditionalWorkspace: true, minHostVersion: "25.6.0", probe: canOpenSourceMonitor, handler: openSourceMonitor },
"sourceMonitor.play": { minHostVersion: "25.6.0", probe: canPlaySourceMonitor, handler: playSourceMonitor },
"sourceMonitor.close": { idempotent: true, minHostVersion: "25.6.0", probe: canCloseSourceMonitor, handler: closeSourceMonitor },
"storage.preflight": { readOnly: true, minHostVersion: "25.6.0", probe: canUseProjectSettings, handler: storagePreflight },
"scratch.configure": { destructive: true, undoable: true, idempotent: true, minHostVersion: "25.6.0", probe: canConfigureScratch, handler: configureScratch },
"workspace.status": { readOnly: true, minHostVersion: "25.6.0", probe: canReportWorkspace, handler: workspaceStatus }
};
async function activeProject(requireTransactions) {
const project = await ppro.Project.getActiveProject();
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
if (requireTransactions && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) {
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions");
}
return project;
}
async function activeContext(requireTransactions) {
const project = await activeProject(requireTransactions);
const sequence = await project.getActiveSequence();
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
return { project, sequence };
}
async function resolveClipProjectItem(project, input) {
const wantedId = input.projectItemId || "", wantedName = input.projectItemName || "";
if (!wantedId && !wantedName) return selectedClipProjectItem(project);
if (typeof project.getRootItem !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build cannot enumerate project items");
const root = await project.getRootItem(), queue = root ? [root] : [], nameMatches = [];
while (queue.length) {
const folder = queue.shift();
if (!folder || typeof folder.getItems !== "function") continue;
const children = Array.from(await folder.getItems() || []);
for (let index = 0; index < children.length; index += 1) {
const item = children[index], itemId = await projectItemIdentifier(item);
if (wantedId && itemId === wantedId) return castClipProjectItem(item);
if (!wantedId && wantedName && String(item.name || "") === wantedName) {
try { nameMatches.push(castClipProjectItem(item)); } catch (_) {}
}
if (isFolderItem(item)) queue.push(item);
}
}
if (wantedId) throw commandError("UXP_TARGET_NOT_FOUND", "projectItemId was not found or is not a media clip");
if (nameMatches.length > 1) throw commandError("UXP_AMBIGUOUS_TARGET", "projectItemName matched multiple media clips; use projectItemId");
if (nameMatches.length === 1) return nameMatches[0];
throw commandError("UXP_TARGET_NOT_FOUND", "projectItemName was not found or is not a media clip");
}
async function selectedClipProjectItem(project) {
if (!ppro.ProjectUtils || typeof ppro.ProjectUtils.getSelection !== "function") {
throw commandError("UXP_INVALID_ARGUMENT", "Pass projectItemId or projectItemName because Project panel selection is unavailable");
}
const selection = await ppro.ProjectUtils.getSelection(project), items = selection && await selection.getItems();
if (!items || items.length !== 1) throw commandError("UXP_INVALID_ARGUMENT", "Select exactly one media project item, or pass projectItemId/projectItemName");
return castClipProjectItem(items[0]);
}
function castClipProjectItem(item) {
if (!ppro.ClipProjectItem || typeof ppro.ClipProjectItem.cast !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build cannot cast project items to media clips");
}
try {
const clip = ppro.ClipProjectItem.cast(item);
if (clip) return clip;
} catch (_) {}
throw commandError("UXP_TARGET_NOT_FOUND", "Resolved project item is not a media clip");
}
function castProjectItem(item) {
if (!ppro.ProjectItem || typeof ppro.ProjectItem.cast !== "function") return item;
try { return ppro.ProjectItem.cast(item) || item; } catch (_) { return item; }
}
function isFolderItem(item) {
if (!ppro.FolderItem || typeof ppro.FolderItem.cast !== "function") return false;
try { return !!ppro.FolderItem.cast(item); } catch (_) { return false; }
}
async function projectItemIdentifier(item) {
const projectItem = castProjectItem(item);
if (!projectItem || typeof projectItem.getId !== "function") return "";
const id = await projectItem.getId();
return id == null ? "" : String(id);
}
async function clipTarget(args, allowedKeys) {
assertObject(args);
assertOnlyKeys(args, allowedKeys);
const target = validateProjectItemTarget(args);
const project = await activeProject(false);
return { project, clip: await resolveClipProjectItem(project, target), target };
}
async function trackItemAt(sequence, mediaType, trackIndex, clipIndex, trackItemsByTrack) {
const cacheKey = mediaType + ":" + trackIndex;
if (trackItemsByTrack && trackItemsByTrack.has(cacheKey)) {
const cachedItems = trackItemsByTrack.get(cacheKey);
if (!cachedItems[clipIndex]) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex " + clipIndex + " is out of range on " + mediaType + " track " + trackIndex);
return cachedItems[clipIndex];
}
const title = mediaType === "video" ? "Video" : "Audio";
const countMethod = "get" + title + "TrackCount", trackMethod = "get" + title + "Track";
if (typeof sequence[countMethod] !== "function" || typeof sequence[trackMethod] !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose " + mediaType + " track APIs");
}
const count = await sequence[countMethod]();
if (trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", mediaType + " trackIndex " + trackIndex + " is out of range");
const track = await sequence[trackMethod](trackIndex), itemType = ppro.Constants && ppro.Constants.TrackItemType;
if (!track || !itemType || itemType.CLIP == null || typeof track.getTrackItems !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere clip track-item APIs are unavailable");
}
const items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []);
if (trackItemsByTrack) trackItemsByTrack.set(cacheKey, items);
if (!items[clipIndex]) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex " + clipIndex + " is out of range on " + mediaType + " track " + trackIndex);
return items[clipIndex];
}
async function currentTrackItems(sequence, requireNonEmpty, enforceLimit) {
if (typeof sequence.getSelection !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build cannot inspect the timeline selection");
const selection = await sequence.getSelection();
if (!selection || typeof selection.getTrackItems !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return a track-item selection");
const items = Array.from(await selection.getTrackItems() || []);
if (requireNonEmpty && !items.length) throw commandError("UXP_EMPTY_SELECTION", "Select at least one clip in the active sequence");
if (enforceLimit !== false && items.length > MAX_SELECTION_ITEMS) {
throw commandError("UXP_SELECTION_TOO_LARGE", "Select at most " + MAX_SELECTION_ITEMS + " clips per compound operation");
}
return { selection, items };
}
async function selectedTrackItems(sequence) {
return currentTrackItems(sequence, true);
}
async function classifySelection(sequence, selectedItems) {
const cache = { video: new Map(), audio: new Map() }, classified = [];
async function clipsAt(mediaType, trackIndex) {
const values = cache[mediaType];
if (values.has(trackIndex)) return values.get(trackIndex);
const title = mediaType === "video" ? "Video" : "Audio", trackMethod = "get" + title + "Track";
const itemType = ppro.Constants && ppro.Constants.TrackItemType;
let items = [];
try {
const track = typeof sequence[trackMethod] === "function" ? await sequence[trackMethod](trackIndex) : null;
if (track && itemType && itemType.CLIP != null && typeof track.getTrackItems === "function") {
items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []);
}
} catch (_) {}
values.set(trackIndex, items);
return items;
}
for (let selectionIndex = 0; selectionIndex < selectedItems.length; selectionIndex += 1) {
const item = selectedItems[selectionIndex];
let trackIndex = null, mediaType = "unknown", clipIndex = null;
try { trackIndex = typeof item.getTrackIndex === "function" ? await item.getTrackIndex() : null; } catch (_) {}
if (Number.isInteger(trackIndex) && trackIndex >= 0) {
const video = await clipsAt("video", trackIndex), videoIndex = video.indexOf(item);
if (videoIndex >= 0) { mediaType = "video"; clipIndex = videoIndex; }
else {
const audio = await clipsAt("audio", trackIndex), audioIndex = audio.indexOf(item);
if (audioIndex >= 0) { mediaType = "audio"; clipIndex = audioIndex; }
}
}
classified.push({ item, selectionIndex, mediaType, trackIndex, clipIndex });
}
return classified;
}
async function componentChain(item) {
if (!item || typeof item.getComponentChain !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "The selected clip does not expose an effect component chain");
const chain = await item.getComponentChain();
if (!chain || typeof chain.getComponentCount !== "function" || typeof chain.getComponentAtIndex !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return a documented effect component chain");
}
return chain;
}
async function componentInfo(component, index) {
let matchName = "", displayName = "", parameterCount = null;
try { if (component && typeof component.getMatchName === "function") matchName = String(await component.getMatchName() || ""); } catch (_) {}
try { if (component && typeof component.getDisplayName === "function") displayName = String(await component.getDisplayName() || ""); } catch (_) {}
try { if (component && typeof component.getParamCount === "function") parameterCount = component.getParamCount(); } catch (_) {}
return { index, matchName, displayName, parameterCount };
}
async function chainSnapshot(chain) {
const count = chain.getComponentCount(), components = [];
for (let index = 0; index < count; index += 1) components.push(await componentInfo(chain.getComponentAtIndex(index), index));
return { count, components };
}
async function effectCatalog(args) {
assertObject(args); assertOnlyKeys(args, ["mediaType"]);
const mediaType = args.mediaType == null ? "all" : enumValue(args.mediaType, "mediaType", ["video", "audio", "all"]);
const result = {};
if (mediaType === "video" || mediaType === "all") {
if (!ppro.VideoFilterFactory || typeof ppro.VideoFilterFactory.getMatchNames !== "function" || typeof ppro.VideoFilterFactory.getDisplayNames !== "function") {
if (mediaType === "video") throw commandError("UXP_COMMAND_UNAVAILABLE", "Video effect catalog APIs are unavailable");
} else {
result.video = {
matchNames: Array.from(await ppro.VideoFilterFactory.getMatchNames() || []),
displayNames: Array.from(await ppro.VideoFilterFactory.getDisplayNames() || [])
};
}
}
if (mediaType === "audio" || mediaType === "all") {
if (!ppro.AudioFilterFactory || typeof ppro.AudioFilterFactory.getDisplayNames !== "function") {
if (mediaType === "audio") throw commandError("UXP_COMMAND_UNAVAILABLE", "Audio effect catalog APIs are unavailable");
} else result.audio = { displayNames: Array.from(await ppro.AudioFilterFactory.getDisplayNames() || []) };
}
return result;
}
async function effectChain(args) {
const input = validateTrackTarget(args, ["mediaType", "trackIndex", "clipIndex"]), context = await activeContext(false);
const item = await trackItemAt(context.sequence, input.mediaType, input.trackIndex, input.clipIndex);
return { ...input, ...(await chainSnapshot(await componentChain(item))) };
}
async function inspectTrackItemIdentity(args) {
const input = validateTrackItemIdentityArgs(args), context = await activeContext(false);
const sequenceGuid = guidString(context.sequence && context.sequence.guid);
if (!sequenceGuid) throw commandError("UXP_COMMAND_UNAVAILABLE", "The active sequence does not expose a stable GUID");
if (input.expectedSequenceGuid && input.expectedSequenceGuid !== sequenceGuid) {
throw commandError("UXP_STALE_SEQUENCE", "The active sequence differs from expectedSequenceGuid; inspect the track-item identity again");
}
const item = await trackItemAt(context.sequence, input.mediaType, input.trackIndex, input.clipIndex);
const identity = await trackItemIdentitySnapshot(item);
const activeAfter = await context.project.getActiveSequence(), activeAfterGuid = guidString(activeAfter && activeAfter.guid);
if (activeAfterGuid !== sequenceGuid) {
throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed while reading the track-item identity; retry the inspection");
}
return {
sequenceGuid, mediaType: input.mediaType, trackIndex: input.trackIndex, clipIndex: input.clipIndex,
...identity, verificationBoundary: "active_sequence_identity_readback"
};
}
async function trackItemIdentitySnapshot(item) {
const required = ["getMatchName", "getType", "getMediaType", "getTrackIndex", "getIsSelected"];
const missing = required.find((name) => !item || typeof item[name] !== "function");
if (missing) throw commandError("UXP_COMMAND_UNAVAILABLE", "This track item does not expose documented " + missing + " identity access");
const matchName = await item.getMatchName(), trackItemType = await item.getType(), mediaTypeGuid = guidString(await item.getMediaType());
const reportedTrackIndex = await item.getTrackIndex(), selected = await item.getIsSelected();
if (typeof matchName !== "string" || matchName.length > 512 || !Number.isInteger(trackItemType) ||
!mediaTypeGuid || !Number.isInteger(reportedTrackIndex) || reportedTrackIndex < 0 || typeof selected !== "boolean") {
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an incomplete track-item identity snapshot");
}
return { matchName, trackItemType, mediaTypeGuid, reportedTrackIndex, selected };
}
function guidString(value) {
if (value == null) return "";
try {
const result = String(typeof value.toString === "function" ? value.toString() : value);
return result.length <= 512 ? result : "";
} catch (_) { return ""; }
}
async function createEffectComponent(mediaType, effectId, item) {
if (mediaType === "video") {
const available = Array.from(await ppro.VideoFilterFactory.getMatchNames() || []);
if (!available.includes(effectId)) throw commandError("UXP_EFFECT_NOT_FOUND", "Unknown video effect matchName: " + effectId);
const component = await ppro.VideoFilterFactory.createComponent(effectId);
if (!component) throw commandError("UXP_EFFECT_NOT_FOUND", "Premiere could not create video effect: " + effectId);
return component;
}
const available = Array.from(await ppro.AudioFilterFactory.getDisplayNames() || []);
if (!available.includes(effectId)) throw commandError("UXP_EFFECT_NOT_FOUND", "Unknown audio effect display name: " + effectId);
const component = await ppro.AudioFilterFactory.createComponentByDisplayName(effectId, item);
if (!component) throw commandError("UXP_EFFECT_NOT_FOUND", "Premiere could not create audio effect: " + effectId);
return component;
}
async function addEffect(args) {
const input = validateEffectAdd(args, false), context = await activeContext(true);
const item = await trackItemAt(context.sequence, input.mediaType, input.trackIndex, input.clipIndex);
const chain = await componentChain(item), before = chain.getComponentCount();
if (input.insertionIndex != null && input.insertionIndex > before) throw commandError("UXP_INVALID_ARGUMENT", "insertionIndex exceeds the component count");
const component = await createEffectComponent(input.mediaType, input.effectId, item);
let committed = false;
context.project.lockedAccess(() => {
const action = input.insertionIndex == null
? chain.createAppendComponentAction(component)
: chain.createInsertComponentAction(component, input.insertionIndex);
committed = context.project.executeTransaction((compoundAction) => {
if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the effect action");
}, "Add " + input.mediaType + " effect");
});
assertCommitted(committed, "effect addition");
const after = await chainSnapshot(chain), verified = after.count === before + 1;
return mutationResult(verified, {
applied: true, mediaType: input.mediaType, trackIndex: input.trackIndex, clipIndex: input.clipIndex,
effectId: input.effectId, insertionIndex: input.insertionIndex, beforeCount: before, after
}, "effect_chain_count_readback", "Add " + input.mediaType + " effect");
}
async function removeEffect(args) {
const input = validateEffectRemove(args, false), context = await activeContext(true);
const item = await trackItemAt(context.sequence, input.mediaType, input.trackIndex, input.clipIndex);
const chain = await componentChain(item), before = chain.getComponentCount();
if (input.componentIndex >= before) throw commandError("UXP_TARGET_NOT_FOUND", "componentIndex is out of range");
const component = chain.getComponentAtIndex(input.componentIndex);
await assertExpectedComponent(component, input.expectedEffectId);
let committed = false;
context.project.lockedAccess(() => {
const action = chain.createRemoveComponentAction(component);
committed = context.project.executeTransaction((compoundAction) => {
if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the effect removal action");
}, "Remove " + input.mediaType + " effect");
});
assertCommitted(committed, "effect removal");
const after = await chainSnapshot(chain), verified = after.count === before - 1;
return mutationResult(verified, {
removed: true, mediaType: input.mediaType, trackIndex: input.trackIndex, clipIndex: input.clipIndex,
componentIndex: input.componentIndex, expectedEffectId: input.expectedEffectId, beforeCount: before, after
}, "effect_chain_count_readback", "Remove " + input.mediaType + " effect");
}
async function inspectSelection(args) {
assertObject(args); assertOnlyKeys(args, []);
const context = await activeContext(false), selected = await selectedTrackItems(context.sequence);
const classified = await classifySelection(context.sequence, selected.items), items = [];
for (const value of classified) items.push(await selectionItemSnapshot(value));
return { count: items.length, items };
}
async function inspectSelectionFingerprints(args) {
assertObject(args); assertOnlyKeys(args, []);
const context = await activeContext(false), selected = await currentTrackItems(context.sequence, false);
const classified = await classifySelection(context.sequence, selected.items), items = [];
assertClassifiedSelection(classified, "The current selection contains an item that cannot be addressed safely");
for (const value of classified) items.push(await selectionFingerprintSnapshot(value, true));
return { sequenceGuid: activeSequenceGuid(context.sequence), count: items.length, items };
}
async function inspectSelectionTargets(args) {
const inputs = validateSelectionTargetInspectionArgs(args), context = await activeContext(false), items = [];
const trackItemsByTrack = new Map();
for (let index = 0; index < inputs.length; index += 1) {
const input = inputs[index], item = await trackItemAt(context.sequence, input.mediaType, input.trackIndex, input.clipIndex, trackItemsByTrack);
const snapshot = await selectionFingerprintSnapshot({
item, selectionIndex: index, mediaType: input.mediaType,
trackIndex: input.trackIndex, clipIndex: input.clipIndex
});
items.push({
targetIndex: index, mediaType: snapshot.mediaType, trackIndex: snapshot.trackIndex,
clipIndex: snapshot.clipIndex, name: snapshot.name, startSeconds: snapshot.startSeconds,
endSeconds: snapshot.endSeconds, projectItem: snapshot.projectItem
});
}
return { sequenceGuid: activeSequenceGuid(context.sequence), count: items.length, items };
}
async function selectionFingerprintSnapshot(value, includeComponentCount) {
const item = value.item;
if (!item || typeof item.getProjectItem !== "function" ||
typeof item.getStartTime !== "function" || typeof item.getEndTime !== "function") {
throw commandError("UXP_SELECTION_FINGERPRINT_UNAVAILABLE", "Timeline item " + value.selectionIndex + " does not expose a complete mutation fingerprint");
}
let projectItem = null, projectItemId = "", startSeconds = null, endSeconds = null;
try {
projectItem = await item.getProjectItem();
projectItemId = await projectItemIdentifier(projectItem);
startSeconds = tickSeconds(await item.getStartTime());
endSeconds = tickSeconds(await item.getEndTime());
} catch (_) {
throw commandError("UXP_SELECTION_FINGERPRINT_UNAVAILABLE", "Timeline item " + value.selectionIndex + " could not provide its project item and native timeline times");
}
if (!projectItemId || startSeconds == null || endSeconds == null) {
throw commandError("UXP_SELECTION_FINGERPRINT_UNAVAILABLE", "Timeline item " + value.selectionIndex + " returned an incomplete mutation fingerprint");
}
let componentCount = null;
if (includeComponentCount) {
try { componentCount = (await componentChain(item)).getComponentCount(); } catch (_) {}
}
return {
selectionIndex: value.selectionIndex, mediaType: value.mediaType, trackIndex: value.trackIndex,
clipIndex: value.clipIndex, name: String(item.name || ""), startSeconds, endSeconds,
componentCount, projectItem: { id: projectItemId, name: String(projectItem && projectItem.name || "") }
};
}
async function selectionItemSnapshot(value) {
let projectItem = null, startSeconds = null, endSeconds = null, componentCount = null;
try {
if (typeof value.item.getProjectItem === "function") {
const item = await value.item.getProjectItem();
projectItem = { id: await projectItemIdentifier(item), name: String(item && item.name || "") };
}
} catch (_) {}
try { startSeconds = tickSeconds(await value.item.getStartTime()); } catch (_) {
try { startSeconds = tickSeconds(await value.item.getInPoint()); } catch (_) {}
}
try { endSeconds = tickSeconds(await value.item.getEndTime()); } catch (_) {
try { endSeconds = tickSeconds(await value.item.getOutPoint()); } catch (_) {}
}
try { componentCount = (await componentChain(value.item)).getComponentCount(); } catch (_) {}
return {
selectionIndex: value.selectionIndex, mediaType: value.mediaType, trackIndex: value.trackIndex,
clipIndex: value.clipIndex, name: String(value.item.name || ""), startSeconds, endSeconds, componentCount, projectItem
};
}
async function updateSelection(args) {
const input = validateSelectionUpdateArgs(args), context = await activeContext(false);
const sequenceGuid = activeSequenceGuid(context.sequence);
if (!sequenceGuid || input.expectedSequenceGuid !== sequenceGuid) {
throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed; inspect the timeline selection again before updating it");
}
await planSelectionUpdate(context.sequence, input);
const mutationContext = await activeContext(false), mutationSequenceGuid = activeSequenceGuid(mutationContext.sequence);
if (!mutationSequenceGuid || input.expectedSequenceGuid !== mutationSequenceGuid) {
throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed; inspect the timeline selection again before updating it");
}
await planSelectionUpdate(mutationContext.sequence, input);
const commitContext = await activeContext(false), commitSequenceGuid = activeSequenceGuid(commitContext.sequence);
if (!commitSequenceGuid || input.expectedSequenceGuid !== commitSequenceGuid) {
throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed; inspect the timeline selection again before updating it");
}
const commitPlan = await planSelectionUpdate(commitContext.sequence, input);
if (input.mode === "add" || input.mode === "remove") {
const currentBase = await selectionSnapshot(commitContext.sequence, false);
const plannedKeys = commitPlan.before ? selectionSnapshotKeys(commitPlan.before.items) : [];
if (!commitPlan.before || !sameStringArrays(plannedKeys, selectionSnapshotKeys(currentBase.items))) {
throw commandError("UXP_STALE_SELECTION", "The current timeline selection changed while the update was being prepared; inspect it again before updating it");
}
}
const finalContext = await activeContext(false), finalSequenceGuid = activeSequenceGuid(finalContext.sequence);
if (!finalSequenceGuid || input.expectedSequenceGuid !== finalSequenceGuid) {
throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed; inspect the timeline selection again before updating it");
}
const plan = await planSelectionUpdate(finalContext.sequence, input);
const beforeSelection = plan.beforeSelection, before = plan.before;
const desiredItems = plan.desiredItems, desired = plan.desired;
if (input.mode === "add" || input.mode === "remove") {
const commitKeys = commitPlan.before ? selectionSnapshotKeys(commitPlan.before.items) : [];
const finalKeys = before ? selectionSnapshotKeys(before.items) : [];
const currentBase = await selectionSnapshot(finalContext.sequence, false);
const currentKeys = selectionSnapshotKeys(currentBase.items);
if (!commitPlan.before || !before || !sameStringArrays(commitKeys, finalKeys) ||
!sameStringArrays(finalKeys, currentKeys)) {
throw commandError("UXP_STALE_SELECTION", "The current timeline selection changed while the update was being prepared; inspect it again before updating it");
}
}
const verifiedContext = await activeContext(false), verifiedSequenceGuid = activeSequenceGuid(verifiedContext.sequence);
if (!verifiedSequenceGuid || input.expectedSequenceGuid !== verifiedSequenceGuid) {
throw commandError("UXP_STALE_SEQUENCE", "The active sequence changed; inspect the timeline selection again before updating it");
}
if (desiredItems.length) {
const selection = createEmptyTrackItemSelection();
for (const item of desiredItems) {
if (selection.addItem(item, false) !== true) {
throw commandError("UXP_SELECTION_REJECTED", "Premiere rejected a clip while constructing the timeline selection");
}
}
const set = await hostBoolean(verifiedContext.sequence.setSelection(selection));
if (!set) throw commandError("UXP_SELECTION_REJECTED", "Premiere did not accept the requested timeline selection");
} else {
const cleared = await hostBoolean(verifiedContext.sequence.clearSelection());
if (!cleared) throw commandError("UXP_SELECTION_REJECTED", "Premiere did not clear the timeline selection");
}
const after = await selectionSnapshot(verifiedContext.sequence);
assertClassifiedSelection(after.classified, "Premiere returned an unclassified timeline item after the selection update");
const expectedKeys = selectionSnapshotKeys(desired.items), actualKeys = selectionSnapshotKeys(after.items);
if (!sameStringArrays(expectedKeys, actualKeys)) {
throw commandError("UXP_VERIFICATION_FAILED", "The timeline selection readback did not match the requested clips");
}
const beforeKeys = before ? selectionSnapshotKeys(before.items) : null;
const changed = beforeKeys ? !sameStringArrays(beforeKeys, actualKeys) :
input.mode === "clear" ? beforeSelection.items.length > 0 : true;
return {
updated: true, changed, mode: input.mode,
sequenceGuid, count: after.items.length, items: after.items,
outcome: "verified", verified: "timeline_selection_readback",
verificationBoundary: "timeline_selection_readback",
operation: operationSemantics({
mutatesProject: false, verificationStatus: "verified", verificationBoundary: "timeline_selection_readback",
verificationEvidence: [{ type: "timeline_selection", sequenceGuid, count: after.items.length }],
cancellationSupported: true
})
};
}
async function planSelectionUpdate(sequence, input) {
const beforeSelection = await currentTrackItems(sequence, false, false);
let before = null;
let desiredItems = [];
if (input.mode !== "clear") {
const resolved = await resolveSelectionTargets(sequence, input.items);
if (input.mode === "replace") {
desiredItems = resolved.map((value) => value.item);
if (beforeSelection.items.length <= MAX_SELECTION_ITEMS) {
before = await itemSelectionSnapshot(sequence, beforeSelection.items);
assertClassifiedSelection(before.classified, "The current selection contains an item that cannot be addressed safely");
}
} else {
if (input.mode === "add" && beforeSelection.items.length > MAX_SELECTION_ITEMS) {
throw commandError("UXP_SELECTION_TOO_LARGE", "The updated selection would exceed " + MAX_SELECTION_ITEMS + " clips");
}
if (input.mode === "remove" && beforeSelection.items.length - resolved.length > MAX_SELECTION_ITEMS) {
throw commandError("UXP_SELECTION_TOO_LARGE", "The updated selection would exceed " + MAX_SELECTION_ITEMS + " clips");
}
before = await itemSelectionSnapshot(sequence, beforeSelection.items);
assertClassifiedSelection(before.classified, "The current selection contains an item that cannot be addressed safely");
desiredItems = before.classified.map((value) => value.item);
if (input.mode === "add") {
const existing = new Set(before.classified.map(selectionCoordinateKey));
for (const value of resolved) {
const coordinate = selectionCoordinateKey(value.snapshot);
if (!existing.has(coordinate)) { desiredItems.push(value.item); existing.add(coordinate); }
}
if (desiredItems.length > MAX_SELECTION_ITEMS) {
throw commandError("UXP_SELECTION_TOO_LARGE", "The updated selection would exceed " + MAX_SELECTION_ITEMS + " clips");
}
} else {
const removed = new Set(resolved.map((value) => selectionCoordinateKey(value.snapshot)));
desiredItems = before.classified
.filter((value) => !removed.has(selectionCoordinateKey(value)))
.map((value) => value.item);
}
}
}
if (desiredItems.length > MAX_SELECTION_ITEMS) {
throw commandError("UXP_SELECTION_TOO_LARGE", "The updated selection would exceed " + MAX_SELECTION_ITEMS + " clips");
}
const desired = await itemSelectionSnapshot(sequence, desiredItems);
assertClassifiedSelection(desired.classified, "Premiere could not classify a requested timeline item");
return { beforeSelection, before, desiredItems, desired };
}
async function resolveSelectionTargets(sequence, inputs) {
const result = [], trackItemsByTrack = new Map();
for (let index = 0; index < inputs.length; index += 1) {
const input = inputs[index], item = await trackItemAt(sequence, input.mediaType, input.trackIndex, input.clipIndex, trackItemsByTrack);
const snapshot = await selectionFingerprintSnapshot({
item, selectionIndex: index, mediaType: input.mediaType,
trackIndex: input.trackIndex, clipIndex: input.clipIndex
});
const projectItemId = snapshot.projectItem && snapshot.projectItem.id;
if (projectItemId !== input.expectedProjectItemId ||
!valuesEqual(snapshot.startSeconds, input.expectedStartSeconds) ||
!valuesEqual(snapshot.endSeconds, input.expectedEndSeconds)) {
throw commandError("UXP_STALE_SELECTION_TARGET", "Timeline item " + index + " changed; inspect the selection again before updating it");
}
result.push({ item, snapshot });
}
return result;
}
async function selectionSnapshot(sequence, enforceLimit) {
const selected = await currentTrackItems(sequence, false, enforceLimit);
return itemSelectionSnapshot(sequence, selected.items);
}
async function itemSelectionSnapshot(sequence, selectedItems) {
const classified = await classifySelection(sequence, selectedItems), items = [];
for (const value of classified) items.push(await selectionFingerprintSnapshot(value));
return { classified, items };
}
function assertClassifiedSelection(classified, message) {
if (classified.some((value) => value.mediaType !== "video" && value.mediaType !== "audio" ||
!Number.isInteger(value.trackIndex) || !Number.isInteger(value.clipIndex))) {
throw commandError("UXP_UNCLASSIFIED_SELECTION", message);
}
}
function createEmptyTrackItemSelection() {
let selection = null;
const created = ppro.TrackItemSelection.createEmptySelection((value) => { selection = value; });
if (created !== true || !selection || typeof selection.addItem !== "function") {
throw commandError("UXP_SELECTION_REJECTED", "Premiere could not create an empty timeline selection");
}
return selection;
}
async function hostBoolean(value) {
const resolved = value && typeof value.then === "function" ? await value : value;
return resolved === true;
}
function activeSequenceGuid(sequence) {
return sequence && sequence.guid != null ? String(sequence.guid) : "";
}
function selectionSnapshotKeys(items) {
return items.map((item) => JSON.stringify([
item.mediaType, item.trackIndex, item.clipIndex,
item.projectItem && item.projectItem.id || "", item.startSeconds, item.endSeconds
])).sort();
}
function selectionCoordinateKey(item) {
return item.mediaType + ":" + item.trackIndex + ":" + item.clipIndex;
}
function sameStringArrays(left, right) {
return left.length === right.length && left.every((value, index) => value === right[index]);
}
async function selectedItemsForEffect(context, mediaType) {
const selected = await selectedTrackItems(context.sequence), classified = await classifySelection(context.sequence, selected.items);
const invalid = classified.filter((value) => value.mediaType !== mediaType);
if (invalid.length) throw commandError("UXP_SELECTION_TYPE_MISMATCH", "Every selected item must be a " + mediaType + " clip for this compound operation");
return classified;
}
async function addEffectToSelection(args) {
const input = validateEffectAdd(args, true), context = await activeContext(true);
const targets = await selectedItemsForEffect(context, input.mediaType), prepared = [];
for (const target of targets) {
const chain = await componentChain(target.item), before = chain.getComponentCount();
if (input.insertionIndex != null && input.insertionIndex > before) throw commandError("UXP_INVALID_ARGUMENT", "insertionIndex exceeds a selected clip's component count");
prepared.push({ target, chain, before, component: await createEffectComponent(input.mediaType, input.effectId, target.item) });
}
let committed = false;
context.project.lockedAccess(() => {
committed = context.project.executeTransaction((compoundAction) => {
for (const value of prepared) {
const action = input.insertionIndex == null
? value.chain.createAppendComponentAction(value.component)
: value.chain.createInsertComponentAction(value.component, input.insertionIndex);
if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected a selected effect action");
}
}, "Add effect to selected clips");
});
assertCommitted(committed, "selected effect addition");
const evidence = [], verified = await verifyChainDeltas(prepared, 1, evidence);
return mutationResult(verified, {
applied: prepared.length, mediaType: input.mediaType, effectId: input.effectId,
insertionIndex: input.insertionIndex, evidence
}, "selected_effect_chain_count_readback", "Add effect to selected clips");
}
async function removeEffectFromSelection(args) {
const input = validateEffectRemove(args, true), context = await activeContext(true);
const targets = await selectedItemsForEffect(context, input.mediaType), prepared = [];
for (const target of targets) {
const chain = await componentChain(target.item), before = chain.getComponentCount();
if (input.componentIndex >= before) throw commandError("UXP_TARGET_NOT_FOUND", "componentIndex is out of range on a selected clip");
const component = chain.getComponentAtIndex(input.componentIndex);
await assertExpectedComponent(component, input.expectedEffectId);
prepared.push({ target, chain, before, component });
}
let committed = false;
context.project.lockedAccess(() => {
committed = context.project.executeTransaction((compoundAction) => {
for (const value of prepared) {
const action = value.chain.createRemoveComponentAction(value.component);
if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected a selected effect removal action");
}
}, "Remove effect from selected clips");
});
assertCommitted(committed, "selected effect removal");
const evidence = [], verified = await verifyChainDeltas(prepared, -1, evidence);
return mutationResult(verified, {
removed: prepared.length, mediaType: input.mediaType, componentIndex: input.componentIndex,
expectedEffectId: input.expectedEffectId, evidence
}, "selected_effect_chain_count_readback", "Remove effect from selected clips");
}
async function assertExpectedComponent(component, expectedEffectId) {
const snapshot = await componentInfo(component, null);
if (snapshot.matchName !== expectedEffectId && snapshot.displayName !== expectedEffectId) {
throw commandError("UXP_STALE_EFFECT_CHAIN", "The component at componentIndex no longer matches expectedEffectId");
}
}
async function verifyChainDeltas(prepared, delta, evidence) {
let verified = true;
for (const value of prepared) {
const afterCount = value.chain.getComponentCount(), expected = value.before + delta;
if (afterCount !== expected) verified = false;
evidence.push({ selectionIndex: value.target.selectionIndex, beforeCount: value.before, afterCount, expected });
}
return verified;
}
async function detectScenes(args) {
assertObject(args); assertOnlyKeys(args, ["mode", "operationId"]);
const mode = enumValue(args.mode, "mode", ["applyCuts", "createMarkers", "createSubclips"]);
const context = await activeContext(false), selected = await selectedTrackItems(context.sequence);
const markerSnapshot = async () => {
if (!ppro.Markers || typeof ppro.Markers.getMarkers !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere marker readback is required before scene-marker detection can run");
const ownerIds = new Set(), markerIds = new Set();
for (const trackItem of selected.items) {
if (!trackItem || typeof trackItem.getProjectItem !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Selected timeline items must expose project-item marker owners for scene-marker readback");
const owner = await trackItem.getProjectItem(), ownerId = await projectItemIdentifier(owner);
if (!ownerId || ownerIds.has(ownerId)) continue;
ownerIds.add(ownerId);
const collection = await ppro.Markers.getMarkers(castProjectItem(owner));
if (!collection || typeof collection.getMarkers !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not expose a marker collection for a selected scene-edit item");
for (const marker of Array.from(await collection.getMarkers() || [])) {
let markerId = "";
try { markerId = marker && marker.guid != null ? String(marker.guid) : ""; } catch (_) {}
if (markerId) markerIds.add(ownerId + ":" + markerId);
}
}
return { ownerIds: Array.from(ownerIds), markerIds: Array.from(markerIds) };
};
let markersBefore = null;
if (mode === "createMarkers") {
markersBefore = await markerSnapshot();
}
const utils = ppro.SequenceUtils, names = {
applyCuts: "SEQUENCE_OPERATION_APPLYCUT",
createMarkers: "SEQUENCE_OPERATION_CREATEMARKER",
createSubclips: "SEQUENCE_OPERATION_CREATESUBCLIP"
};
const operation = utils && utils[names[mode]];
if (typeof operation !== "string" || !operation) throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere scene-edit operation constants are unavailable");
const detected = await utils.performSceneEditDetectionOnSelection(operation, selected.selection);
if (!detected) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm scene-edit detection");
if (mode === "createMarkers") {
const markersAfter = await markerSnapshot(), addedMarkerIds = markersAfter.markerIds.filter((id) => markersBefore.markerIds.indexOf(id) < 0);
if (!addedMarkerIds.length) throw commandError("UXP_VERIFICATION_FAILED", "Premiere reported scene-marker detection but did not add observable selected-item markers");
return {
detected: true, verified: true, mode, selectedItemCount: selected.items.length,
markerOwnerCount: markersAfter.ownerIds.length, addedMarkerCount: addedMarkerIds.length,
outcome: "verified", verificationBoundary: "selected_project_item_marker_guid_readback",
operation: operationSemantics({
mutatesProject: true, verificationStatus: "verified", verificationBoundary: "selected_project_item_marker_guid_readback",
verificationEvidence: [{ type: "selected_project_item_marker_guid_delta", addedMarkerCount: addedMarkerIds.length }], undoSupported: false, cancellationSupported: true
})
};
}
return {
detected: true, outcome: "committed_unverified", mode, selectedItemCount: selected.items.length,
verificationBoundary: "sequence_utils_host_return",
operation: operationSemantics({
mutatesProject: true, verificationStatus: "not_verified", verificationBoundary: "sequence_utils_host_return",
verificationEvidence: [{ type: "host_return", value: true }], undoSupported: false, cancellationSupported: true
})
};
}
async function proxySnapshot(clip) {
const projectItem = castProjectItem(clip);
const result = {
projectItemId: await projectItemIdentifier(clip), name: String(clip.name || ""),
canProxy: null, hasProxy: null, proxyPath: "", offline: null, mediaPath: ""
};
if (typeof clip.canProxy === "function") result.canProxy = !!await clip.canProxy();
if (typeof clip.hasProxy === "function") result.hasProxy = !!await clip.hasProxy();
if (typeof clip.getProxyPath === "function") result.proxyPath = String(await clip.getProxyPath() || "");
if (typeof clip.isOffline === "function") result.offline = !!await clip.isOffline();
if (projectItem && typeof projectItem.getMediaFilePath === "function") result.mediaPath = String(await projectItem.getMediaFilePath() || "");
return result;
}
async function inspectProxy(args) {
const context = await clipTarget(args, ["projectItemId", "projectItemName"]);
return proxySnapshot(context.clip);
}
async function attachProxy(args) {
assertObject(args);
assertOnlyKeys(args, ["projectItemId", "projectItemName", "mediaPath", "isHiRes", "makeAlternateLinkInTeamProjects", "replaceExistingProxy", "confirmNonUndoable", "operationId"]);
const target = validateProjectItemTarget(args);
requireConfirmation(args.confirmNonUndoable, "Attaching proxy or high-resolution media is not undoable");
const mediaPath = await allowedPath(args.mediaPath, "proxy mediaPath", "file");
const isHiRes = optionalBoolean(args.isHiRes, false, "isHiRes");
const alternate = optionalBoolean(args.makeAlternateLinkInTeamProjects, false, "makeAlternateLinkInTeamProjects");
const replaceExistingProxy = optionalBoolean(args.replaceExistingProxy, false, "replaceExistingProxy");
const project = await activeProject(false), clip = await resolveClipProjectItem(project, target), before = await proxySnapshot(clip);
if (typeof clip.canProxy !== "function" || !await clip.canProxy()) throw commandError("UXP_TARGET_UNSUPPORTED", "The resolved clip cannot accept proxy media");
if (!isHiRes && before.hasProxy && pathEqual(before.proxyPath, mediaPath)) {
return { attached: false, unchanged: true, outcome: "verified", before, after: before, mediaPath, isHiRes };
}
if (!isHiRes && before.hasProxy && !replaceExistingProxy) {
throw commandError("UXP_PROXY_ALREADY_ATTACHED", "A different proxy is already attached; inspect it and pass replaceExistingProxy=true to replace it");
}
const attached = await clip.attachProxy(mediaPath, isHiRes, alternate);
if (!attached) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm proxy attachment");
const after = await proxySnapshot(clip);
const verified = isHiRes ? false : after.hasProxy === true && pathEqual(after.proxyPath, mediaPath);
return {
attached: true, outcome: verified ? "verified" : "committed_unverified", before, after, mediaPath, isHiRes, replaceExistingProxy,
verificationBoundary: verified ? "proxy_path_readback" : "attach_proxy_host_return",
operation: operationSemantics({
mutatesProject: true, verificationStatus: verified ? "verified" : "not_verified",
verificationBoundary: verified ? "proxy_path_readback" : "attach_proxy_host_return",
verificationEvidence: verified ? [{ type: "proxy_path", pathMatched: true }] : [{ type: "host_return", value: true }],
undoSupported: false, cancellationSupported: true
})
};
}
async function ingestSnapshot(project) {
const settings = await ppro.ProjectSettings.getIngestSettings(project);
if (!settings || typeof settings.getIsIngestEnabled !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return ingest settings");
return { settings, enabled: !!await settings.getIsIngestEnabled() };
}
async function getIngest(args) {
assertObject(args); assertOnlyKeys(args, []);
const project = await activeProject(false), snapshot = await ingestSnapshot(project);
return { enabled: snapshot.enabled };
}
async function configureIngest(args) {
assertObject(args); assertOnlyKeys(args, ["enabled", "operationId"]);
const enabled = requiredBoolean(args.enabled, "enabled"), project = await activeProject(true);
const before = await ingestSnapshot(project);
if (before.enabled === enabled) return { configured: false, unchanged: true, outcome: "verified", enabled };
if (typeof before.settings.setIngestEnabled !== "function" || !await before.settings.setIngestEnabled(enabled)) {
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the ingest setting value");
}
let committed = false;
project.lockedAccess(() => {
const action = ppro.ProjectSettings.createSetIngestSettingsAction(project, before.settings);
committed = project.executeTransaction((compoundAction) => {
if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the ingest settings action");
}, "Configure ingest");
});
assertCommitted(committed, "ingest settings update");
const after = await ingestSnapshot(project), verified = after.enabled === enabled;
return mutationResult(verified, { configured: true, before: before.enabled, enabled: after.enabled }, "ingest_settings_readback", "Configure ingest");
}
async function relinkMedia(args) {
assertObject(args);
assertOnlyKeys(args, ["projectItemId", "projectItemName", "newPath", "expectedCurrentPath", "overrideCompatibilityCheck", "requireOffline", "confirmNonUndoable", "operationId"]);
const target = validateProjectItemTarget(args);
requireConfirmation(args.confirmNonUndoable, "Changing a clip's media path is not undoable");
const newPath = await allowedPath(args.newPath, "newPath", "file");
const expectedCurrentPath = args.expectedCurrentPath == null ? null : boundedString(args.expectedCurrentPath, "expectedCurrentPath", 4096);
const overrideCompatibilityCheck = optionalBoolean(args.overrideCompatibilityCheck, false, "overrideCompatibilityCheck");
const requireOffline = optionalBoolean(args.requireOffline, true, "requireOffline");
const project = await activeProject(false), clip = await resolveClipProjectItem(project, target), projectItem = castProjectItem(clip);
if (typeof clip.canChangeMediaPath !== "function" || !await clip.canChangeMediaPath()) throw commandError("UXP_TARGET_UNSUPPORTED", "Premiere reports that this clip's media path cannot be changed");
const before = await proxySnapshot(clip);
if (expectedCurrentPath != null && !pathEqual(before.mediaPath, expectedCurrentPath)) {
throw commandError("UXP_STALE_MEDIA_PATH", "The clip's current media path no longer matches expectedCurrentPath");
}
if (requireOffline && before.offline !== true) throw commandError("UXP_MEDIA_NOT_OFFLINE", "Safe relink defaults to offline clips; set requireOffline=false only after inspecting the target");
if (pathEqual(before.mediaPath, newPath) && before.offline === false) {
return { relinked: false, unchanged: true, outcome: "verified", before, after: before, newPath };
}
const changed = await clip.changeMediaFilePath(newPath, overrideCompatibilityCheck);
if (!changed) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm the media relink");
if (typeof clip.refreshMedia === "function") await clip.refreshMedia();
const after = await proxySnapshot(clip);
if (!after.mediaPath && projectItem && typeof projectItem.getMediaFilePath === "function") after.mediaPath = String(await projectItem.getMediaFilePath() || "");
const verified = pathEqual(after.mediaPath, newPath) && after.offline === false;
return {
relinked: true, outcome: verified ? "verified" : "committed_unverified", before, after, newPath,
overrideCompatibilityCheck, verificationBoundary: verified ? "media_path_and_online_readback" : "change_media_file_path_host_return",
operation: operationSemantics({
mutatesProject: true, verificationStatus: verified ? "verified" : "not_verified",
verificationBoundary: verified ? "media_path_and_online_readback" : "change_media_file_path_host_return",
verificationEvidence: [{ type: "media_path", pathMatched: pathEqual(after.mediaPath, newPath), online: after.offline === false }],
undoSupported: false, cancellationSupported: true
})
};
}
async function metadataSnapshot(clip) {
const projectItem = castProjectItem(clip);
const projectMetadata = String(await ppro.Metadata.getProjectMetadata(projectItem) || "");
const xmpMetadata = String(await ppro.Metadata.getXMPMetadata(projectItem) || "");
const result = { projectItemId: await projectItemIdentifier(projectItem), name: String(clip.name || ""), projectMetadata, xmpMetadata };
if (projectMetadata.length > MAX_METADATA_CHARS || xmpMetadata.length > MAX_METADATA_CHARS ||
utf8ByteLength(JSON.stringify(result)) > MAX_METADATA_RESULT_BYTES) {
throw commandError("UXP_RESULT_TOO_LARGE", "Metadata exceeds the bridge's bounded result size");
}
return Protocol && typeof Protocol.assertResultSize === "function" ? Protocol.assertResultSize(result) : result;
}
async function getMetadata(args) {
const context = await clipTarget(args, ["projectItemId", "projectItemName"]);
return metadataSnapshot(context.clip);
}
function boundedMetadataResult(value, field, result) {
const metadata = boundedStringAllowEmpty(value, field, MAX_METADATA_CHARS);
if (utf8ByteLength(JSON.stringify(result)) > MAX_METADATA_RESULT_BYTES) {
throw commandError("UXP_RESULT_TOO_LARGE", field + " exceeds the bridge's bounded result size");
}
return Protocol && typeof Protocol.assertResultSize === "function" ? Protocol.assertResultSize(result) : result;
}
async function getProjectColumnsMetadata(args) {
const context = await clipTarget(args, ["projectItemId", "projectItemName"]);
const projectItem = castProjectItem(context.clip);
const projectColumnsMetadata = await ppro.Metadata.getProjectColumnsMetadata(projectItem);
const result = {
projectItemId: await projectItemIdentifier(projectItem),
name: String(context.clip.name || ""),
projectColumnsMetadata
};
return boundedMetadataResult(result.projectColumnsMetadata, "projectColumnsMetadata", result);
}
async function getProjectPanelMetadata(args) {
assertObject(args); assertOnlyKeys(args, []);
const project = await activeProject(false);
const projectPanelMetadata = await ppro.Metadata.getProjectPanelMetadata();
const result = {
projectGuid: String(project.guid || ""),
projectName: String(project.name || ""),
projectPanelMetadata
};
if (!result.projectGuid) throw commandError("UXP_INVALID_HOST_STATE", "Premiere did not return a project GUID for Project-panel metadata");
return boundedMetadataResult(result.projectPanelMetadata, "projectPanelMetadata", result);
}
async function projectPanelMetadataSnapshot(maximumBytes) {
const project = await activeProject(false);
const projectGuid = String(project.guid || "");
if (!projectGuid) throw commandError("UXP_INVALID_HOST_STATE", "Premiere did not return a project GUID for Project-panel metadata");
const projectPanelMetadata = boundedUtf8StringAllowEmpty(
await ppro.Metadata.getProjectPanelMetadata(), "projectPanelMetadata", maximumBytes
);
return { project, projectGuid, projectName: String(project.name || ""), projectPanelMetadata };
}
async function updateProjectPanelMetadata(args) {
assertObject(args);
assertOnlyKeys(args, ["expectedProjectGuid", "expectedProjectPanelMetadata", "projectPanelMetadata", "confirmUpdate", "operationId"]);
const input = {
expectedProjectGuid: boundedString(args.expectedProjectGuid, "expectedProjectGuid", 512),
expectedProjectPanelMetadata: boundedUtf8StringAllowEmpty(args.expectedProjectPanelMetadata, "expectedProjectPanelMetadata", MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES),
projectPanelMetadata: boundedUtf8StringAllowEmpty(args.projectPanelMetadata, "projectPanelMetadata", MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES),
operationId: requiredOperationId(args.operationId, "operationId")
};
if (args.confirmUpdate !== true) {
throw commandError("UXP_CONFIRMATION_REQUIRED", "Replacing Project-panel metadata is not undoable; pass confirmUpdate=true after review");
}
return withProjectPanelMetadataMutationLock(input.expectedProjectGuid, async () => {
// This queue serializes this bridge's competing requests. Premiere does
// not expose an atomic compare-and-set for this direct setter, so the
// snapshot is deliberately the final asynchronous preflight before the
// setter is started; human UI or another extension can still race it.
const before = await projectPanelMetadataSnapshot(MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES);
if (before.projectGuid !== input.expectedProjectGuid) {
throw commandError("UXP_STALE_PROJECT_PANEL_METADATA", "The active project changed before Project-panel metadata was updated; inspect and retry");
}
if (before.projectPanelMetadata !== input.expectedProjectPanelMetadata) {
throw commandError("UXP_STALE_PROJECT_PANEL_METADATA", "Project-panel metadata changed before it was updated; inspect and retry");
}
if (before.projectPanelMetadata === input.projectPanelMetadata) {
return projectPanelMetadataUpdateResult(true, {
updated: false, unchanged: true, projectGuid: before.projectGuid,
projectName: before.projectName, projectPanelMetadata: before.projectPanelMetadata
}, "project_panel_metadata_exact_readback");
}
let request;
before.project.lockedAccess(() => {
request = ppro.Metadata.setProjectPanelMetadata(input.projectPanelMetadata);
});
if (await request !== true) throw commandError("UXP_ACTION_REJECTED", "Premiere did not accept Project-panel metadata replacement");
const after = await projectPanelMetadataSnapshot(MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES);
const verified = after.projectGuid === before.projectGuid && after.projectPanelMetadata === input.projectPanelMetadata;
return projectPanelMetadataUpdateResult(verified, {
updated: true, projectGuid: before.projectGuid, projectName: after.projectName,
projectPanelMetadata: after.projectPanelMetadata,
requestedProjectPanelMetadata: input.projectPanelMetadata,
readbackProjectGuid: after.projectGuid
}, verified ? "project_panel_metadata_exact_readback" : "project_panel_metadata_active_project_readback");
});
}
function withProjectPanelMetadataMutationLock(projectGuid, operation) {
const previous = projectPanelMetadataMutationTails.get(projectGuid) || Promise.resolve();
let release;
const gate = new Promise((resolve) => { release = resolve; });
const tail = previous.catch(() => undefined).then(() => gate);
projectPanelMetadataMutationTails.set(projectGuid, tail);
return previous.catch(() => undefined).then(operation).finally(() => {
release();
if (projectPanelMetadataMutationTails.get(projectGuid) === tail) projectPanelMetadataMutationTails.delete(projectGuid);
});
}
function projectPanelMetadataUpdateResult(verified, values, boundary) {
return {
...values, outcome: verified ? "verified" : "committed_unverified", verified,
verificationBoundary: boundary,
operation: operationSemantics({
mutatesProject: true, verificationStatus: verified ? "verified" : "not_verified", verificationBoundary: boundary,
verificationEvidence: [{ type: boundary, verified }], undoSupported: false,
transactionActionGroup: false, cancellationSupported: false
})
};
}
async function inspectProjectMetadataSchema(args) {
assertObject(args); assertOnlyKeys(args, []);
const snapshot = await projectPanelMetadataSnapshot(MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES);
return {
projectGuid: snapshot.projectGuid, projectName: snapshot.projectName,
projectPanelMetadata: snapshot.projectPanelMetadata,
verificationBoundary: "bounded_project_panel_metadata_readback"
};
}
async function createProjectMetadataField(args) {
assertObject(args);
assertOnlyKeys(args, ["expectedProjectGuid", "expectedProjectPanelMetadata", "fieldName", "fieldLabel", "fieldType", "confirmCreate", "operationId"]);
const input = {
expectedProjectGuid: boundedString(args.expectedProjectGuid, "expectedProjectGuid", 512),
expectedProjectPanelMetadata: boundedUtf8StringAllowEmpty(args.expectedProjectPanelMetadata, "expectedProjectPanelMetadata", MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES),
fieldName: metadataSchemaFieldName(args.fieldName),
fieldLabel: boundedString(args.fieldLabel, "fieldLabel", 255),
fieldType: enumValue(args.fieldType, "fieldType", ["integer", "real", "text", "boolean"]),
operationId: requiredOperationId(args.operationId, "operationId")
};
if (args.confirmCreate !== true) {
throw commandError("UXP_CONFIRMATION_REQUIRED", "Creating a Project metadata schema field is non-undoable; pass confirmCreate=true after review");
}
// Resolve every synchronous host value before the queued stale snapshot so
// no awaited conversion can open a gap between validation and the direct
// schema call under lockedAccess().
const metadataType = projectMetadataType(input.fieldType);
return withProjectPanelMetadataMutationLock(input.expectedProjectGuid, async () => {
// This shared queue also excludes this bridge's direct Project-panel XML
// replacement requests. Adobe provides no atomic compare-and-set or
// schema-field getter, so UI and other-extension races remain outside
// this protocol's proof boundary.
const before = await projectPanelMetadataSnapshot(MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES);
if (before.projectGuid !== input.expectedProjectGuid) {
throw commandError("UXP_STALE_PROJECT_METADATA_SCHEMA", "The active project changed before the metadata schema field was created; inspect and retry");
}
if (before.projectPanelMetadata !== input.expectedProjectPanelMetadata) {
throw commandError("UXP_STALE_PROJECT_METADATA_SCHEMA", "Project-panel metadata changed before the metadata schema field was created; inspect and retry");
}
let request;
before.project.lockedAccess(() => {
request = ppro.Metadata.addPropertyToProjectMetadataSchema(input.fieldName, input.fieldLabel, metadataType);
});
if (await request !== true) throw commandError("UXP_ACTION_REJECTED", "Premiere did not accept Project metadata schema field creation");
const after = await projectPanelMetadataSnapshot(MAX_PROJECT_PANEL_METADATA_UPDATE_BYTES);
const activeProjectRetained = after.projectGuid === before.projectGuid;
const panelMetadataChanged = activeProjectRetained && after.projectPanelMetadata !== before.projectPanelMetadata;
return {
creationRequested: true,
hostAccepted: true,
projectGuid: before.projectGuid,
projectName: after.projectName,
field: { name: input.fieldName, label: input.fieldLabel, type: input.fieldType },
readbackProjectGuid: after.projectGuid,
panelMetadataChanged,
outcome: "committed_unverified",
verified: false,
verificationBoundary: panelMetadataChanged ? "project_panel_metadata_change_readback" : "metadata_schema_add_host_return",
operation: operationSemantics({
mutatesProject: true, verificationStatus: "not_verified",
verificationBoundary: panelMetadataChanged ? "project_panel_metadata_change_readback" : "metadata_schema_add_host_return",
verificationEvidence: [{ type: "metadata_schema_host_return", value: true }, { type: "project_panel_metadata_changed", value: panelMetadataChanged }],
undoSupported: false, transactionActionGroup: false, cancellationSupported: false
})
};
});
}
function metadataSchemaFieldName(value) {
if (typeof value !== "string" || !/^[A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(value)) {
throw commandError("UXP_INVALID_ARGUMENT", "fieldName must start with a letter and contain at most 128 letters, digits, periods, underscores, or hyphens");
}
return value;
}
function projectMetadataType(fieldType) {
const typeNames = {
integer: "METADATA_TYPE_INTEGER", real: "METADATA_TYPE_REAL",
text: "METADATA_TYPE_TEXT", boolean: "METADATA_TYPE_BOOLEAN"
};
const value = ppro.Metadata && ppro.Metadata[typeNames[fieldType]];
if (value == null) throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose the " + fieldType + " Project metadata field type");
return value;
}
async function updateMetadata(args) {
assertObject(args);
assertOnlyKeys(args, ["projectItemId", "projectItemName", "projectMetadata", "xmpMetadata", "updatedFields", "operationId"]);
const target = validateProjectItemTarget(args), hasProject = args.projectMetadata != null, hasXmp = args.xmpMetadata != null;
if (!hasProject && !hasXmp) throw commandError("UXP_INVALID_ARGUMENT", "At least one of projectMetadata or xmpMetadata is required");
const projectMetadata = hasProject ? boundedStringAllowEmpty(args.projectMetadata, "projectMetadata", MAX_METADATA_CHARS) : null;
const xmpMetadata = hasXmp ? boundedStringAllowEmpty(args.xmpMetadata, "xmpMetadata", MAX_METADATA_CHARS) : null;
const updatedFields = validateUpdatedFields(args.updatedFields, hasProject);
const project = await activeProject(true), clip = await resolveClipProjectItem(project, target), projectItem = castProjectItem(clip);
const before = await metadataSnapshot(clip);
if ((!hasProject || before.projectMetadata === projectMetadata) && (!hasXmp || before.xmpMetadata === xmpMetadata)) {
return { updated: false, unchanged: true, outcome: "verified", metadata: before };
}
let committed = false;
project.lockedAccess(() => {
committed = project.executeTransaction((compoundAction) => {
if (hasProject && before.projectMetadata !== projectMetadata) {
const projectAction = ppro.Metadata.createSetProjectMetadataAction(projectItem, projectMetadata, updatedFields);
if (!projectAction || compoundAction.addAction(projectAction) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the project metadata action");
}
if (hasXmp && before.xmpMetadata !== xmpMetadata) {
const xmpAction = ppro.Metadata.createSetXMPMetadataAction(projectItem, xmpMetadata);
if (!xmpAction || compoundAction.addAction(xmpAction) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the XMP metadata action");
}
}, "Update clip metadata");
});
assertCommitted(committed, "metadata update");
const after = await metadataSnapshot(clip);
const verified = (!hasProject || after.projectMetadata === projectMetadata) && (!hasXmp || after.xmpMetadata === xmpMetadata);
return mutationResult(verified, { updated: true, updatedFields, metadata: after }, "metadata_readback", "Update clip metadata");
}
async function footageSnapshot(clip) {
if (typeof clip.getFootageInterpretation !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Footage interpretation APIs are unavailable for this clip");
const interpretation = await clip.getFootageInterpretation();
if (!interpretation) throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return footage interpretation data");
const values = {};
const getters = {
frameRate: "getFrameRate", pixelAspectRatio: "getPixelAspectRatio", fieldType: "getFieldType",
removePullDown: "getRemovePullDown", alphaUsage: "getAlphaUsage", ignoreAlpha: "getIgnoreAlpha",
invertAlpha: "getInvertAlpha", vrConform: "getVrConform", vrLayout: "getVrLayout",
vrHorzView: "getVrHorzView", vrVertView: "getVrVertView", footageInputLutId: "getInputLUTID"
};
for (const key of Object.keys(getters)) {
const method = getters[key];
values[key] = typeof interpretation[method] === "function" ? interpretation[method]() : null;
}
values.inputLutId = typeof clip.getInputLUTID === "function" ? String(await clip.getInputLUTID() || "") : null;
values.embeddedLutId = typeof clip.getEmbeddedLUTID === "function" ? String(await clip.getEmbeddedLUTID() || "") : null;
return { interpretation, values };
}
async function colorPreflight(args) {
const context = await clipTarget(args, ["projectItemId", "projectItemName"]);
if (typeof context.project.getColorSettings !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Project color settings are unavailable");
const settings = await context.project.getColorSettings();
if (!settings) throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return project color settings");
const footage = await footageSnapshot(context.clip);
return {
project: {
graphicsWhiteLuminance: await settings.getGraphicsWhiteLuminance(),
supportedGraphicsWhiteLuminances: Array.from(await settings.getSupportedGraphicsWhiteLuminances() || [])
},
clip: { projectItemId: await projectItemIdentifier(context.clip), name: String(context.clip.name || ""), ...footage.values }
};
}
async function inspectEnvironment(args) {
assertObject(args); assertOnlyKeys(args, []);
const project = await activeProject(false);
if (!ppro.Utils || typeof ppro.Utils.isAEInstalled !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "After Effects installation detection is unavailable");
}
if (typeof project.getColorSettings !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Project color settings are unavailable");
}
const settings = await project.getColorSettings();
if (!settings || typeof settings.getGraphicsWhiteLuminance !== "function" ||
typeof settings.getSupportedGraphicsWhiteLuminances !== "function") {
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return complete project color settings");
}
return {
afterEffectsInstalled: !!await ppro.Utils.isAEInstalled(),
projectColor: {
graphicsWhiteLuminance: await settings.getGraphicsWhiteLuminance(),
supportedGraphicsWhiteLuminances: Array.from(await settings.getSupportedGraphicsWhiteLuminances() || [])
}
};
}
async function conformFootage(args) {
const input = validateConformanceArgs(args), project = await activeProject(true);
const clip = await resolveClipProjectItem(project, input), before = await footageSnapshot(clip);
const setters = {
frameRate: "setFrameRate", pixelAspectRatio: "setPixelAspectRatio", fieldType: "setFieldType",
removePullDown: "setRemovePullDown", alphaUsage: "setAlphaUsage", ignoreAlpha: "setIgnoreAlpha",
invertAlpha: "setInvertAlpha", vrConform: "setVrConform", vrLayout: "setVrLayout",
vrHorzView: "setVrHorzView", vrVertView: "setVrVertView"
};
const interpretationKeys = Object.keys(setters).filter((key) => input[key] != null);
for (const key of interpretationKeys) {
const method = setters[key];
if (typeof before.interpretation[method] !== "function" || before.interpretation[method](input[key]) !== true) {
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected footage interpretation field " + key);
}
}
let committed = false;
project.lockedAccess(() => {
committed = project.executeTransaction((compoundAction) => {
if (interpretationKeys.length) {
const interpretationAction = clip.createSetFootageInterpretationAction(before.interpretation);
if (!interpretationAction || compoundAction.addAction(interpretationAction) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the footage interpretation action");
}
if (input.inputLutId != null) {
const lutAction = clip.createSetInputLUTIDAction(input.inputLutId);
if (!lutAction || compoundAction.addAction(lutAction) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the input LUT action");
}
}, "Conform source footage");
});
assertCommitted(committed, "footage conformance update");
const after = await footageSnapshot(clip), requested = { ...input };
delete requested.projectItemId; delete requested.projectItemName; delete requested.operationId;
const verified = Object.keys(requested).every((key) => valuesEqual(after.values[key], requested[key]));
return mutationResult(verified, {
conformed: true, projectItemId: await projectItemIdentifier(clip), requested, before: before.values, after: after.values
}, "footage_interpretation_readback", "Conform source footage");
}
async function sourceMonitorState(args) {
assertObject(args); assertOnlyKeys(args, []);
const source = ppro.SourceMonitor, position = await source.getPosition();
let projectItem = null;
try {
const item = await source.getProjectItem();
if (item) projectItem = { id: await projectItemIdentifier(item), name: String(item.name || "") };
} catch (_) {}
return { open: !!projectItem, positionSeconds: tickSeconds(position), projectItem };
}
async function openSourceMonitor(args) {
assertObject(args); assertOnlyKeys(args, ["projectItemId", "projectItemName", "filePath", "operationId"]);
const hasFile = args.filePath != null;
if (hasFile && (args.projectItemId != null || args.projectItemName != null)) throw commandError("UXP_INVALID_ARGUMENT", "filePath cannot be combined with a project-item selector");
if (hasFile) {
const filePath = await allowedPath(args.filePath, "Source Monitor filePath", "file");
const opened = await ppro.SourceMonitor.openFilePath(filePath);
if (!opened) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm opening the file in Source Monitor");
return {
opened: true, source: "file", filePath, outcome: "committed_unverified",
verificationBoundary: "source_monitor_open_file_host_return",
operation: operationSemantics({ mutatesProject: false, verificationStatus: "not_verified", verificationBoundary: "source_monitor_open_file_host_return", verificationEvidence: [{ type: "host_return", value: true }] })
};
}
const target = validateProjectItemTarget(args), project = await activeProject(false);
const clip = await resolveClipProjectItem(project, target), projectItem = castProjectItem(clip), expectedId = await projectItemIdentifier(projectItem);
const opened = await ppro.SourceMonitor.openProjectItem(projectItem);
if (!opened) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm opening the project item in Source Monitor");
const readback = await ppro.SourceMonitor.getProjectItem(), readbackId = await projectItemIdentifier(readback);
const verified = !!expectedId && readbackId === expectedId;
return {
opened: true, source: "projectItem", projectItemId: expectedId, name: String(clip.name || ""),
outcome: verified ? "verified" : "committed_unverified", verificationBoundary: "source_monitor_project_item_readback",
operation: operationSemantics({
mutatesProject: false, verificationStatus: verified ? "verified" : "not_verified",
verificationBoundary: "source_monitor_project_item_readback", verificationEvidence: [{ type: "project_item", expectedId, readbackId }]
})
};
}
async function playSourceMonitor(args) {
assertObject(args); assertOnlyKeys(args, ["speed", "operationId"]);
const speed = args.speed == null ? 1 : finiteNumber(args.speed, "speed", -16, 16);
const played = await ppro.SourceMonitor.play(speed);
if (!played) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm Source Monitor playback");
return { playing: true, speed, outcome: "committed_unverified", verificationBoundary: "source_monitor_play_host_return" };
}
async function closeSourceMonitor(args) {
assertObject(args); assertOnlyKeys(args, ["all", "operationId"]);
const all = optionalBoolean(args.all, false, "all");
const closed = all ? await ppro.SourceMonitor.closeAllClips() : await ppro.SourceMonitor.closeClip();
if (!closed) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not confirm closing Source Monitor media");
let state = null;
try { state = await sourceMonitorState({}); } catch (_) {}
const verified = !!state && state.open === false;
return {
closed: true, all, outcome: verified ? "verified" : "committed_unverified", state,
verificationBoundary: verified ? "source_monitor_state_readback" : "source_monitor_close_host_return"
};
}
function scratchTypeTable() {
const constants = ppro.Constants && ppro.Constants.ScratchDiskFolderType || {};
return {
capture: constants.CAPTURE, audioPreview: constants.AUDIO_PREVIEW, videoPreview: constants.VIDEO_PREVIEW,
autoSave: constants.AUTO_SAVE, ccLibraries: constants.CCL_LIBRARIES, capsuleMedia: constants.CAPSULE_MEDIA
};
}
function scratchSnapshot(settings) {
const result = {}, table = scratchTypeTable();
for (const key of Object.keys(table)) {
if (table[key] == null) continue;
try { result[key] = settings.getScratchDiskPath(table[key]); } catch (_) { result[key] = null; }
}
return result;
}
async function storagePreflight(args) {
assertObject(args); assertOnlyKeys(args, []);
const project = await activeProject(false), scratch = await ppro.ProjectSettings.getScratchDiskSettings(project);
const ingest = await ingestSnapshot(project);
let production = { apiAvailable: false, active: false, scratchDisks: null };
if (ppro.PRProduction && typeof ppro.PRProduction.getActiveProduction === "function") {
production.apiAvailable = true;
try {
const active = ppro.PRProduction.getActiveProduction();
if (active) production = { apiAvailable: true, active: true, scratchDisks: scratchSnapshot(await active.getScratchDiskSettings()) };
} catch (_) {}
}
return { project: { scratchDisks: scratchSnapshot(scratch), ingestEnabled: ingest.enabled }, production };
}
async function configureScratch(args) {
assertObject(args); assertOnlyKeys(args, ["folderTypes", "destination", "operationId"]);
const table = scratchTypeTable(), folderTypes = boundedEnumArray(args.folderTypes, "folderTypes", Object.keys(table), 6);
const destination = enumValue(args.destination, "destination", ["sameAsProject", "myDocuments"]);
const destinations = ppro.Constants && ppro.Constants.ScratchDiskFolder || {};
const destinationValue = destination === "sameAsProject" ? destinations.SAME_AS_PROJECT : destinations.MY_DOCUMENTS;
if (destinationValue == null) throw commandError("UXP_COMMAND_UNAVAILABLE", "Scratch disk destination constants are unavailable");
const project = await activeProject(true), settings = await ppro.ProjectSettings.getScratchDiskSettings(project);
const before = scratchSnapshot(settings);
for (const key of folderTypes) {
if (table[key] == null || settings.setScratchDiskPath(table[key], destinationValue) !== true) {
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected scratch disk folder type " + key);
}
}
let committed = false;
project.lockedAccess(() => {
const action = ppro.ProjectSettings.createSetScratchDiskSettingsAction(project, settings);
committed = project.executeTransaction((compoundAction) => {
if (!action || compoundAction.addAction(action) === false) throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the scratch disk settings action");
}, "Configure scratch disks");
});
assertCommitted(committed, "scratch disk update");
const afterSettings = await ppro.ProjectSettings.getScratchDiskSettings(project), after = scratchSnapshot(afterSettings);
return {
configured: true, outcome: "committed_unverified", folderTypes, destination, before, after,
verificationBoundary: "scratch_disk_settings_readback",
operation: operationSemantics({
mutatesProject: true, verificationStatus: "not_verified", verificationBoundary: "scratch_disk_settings_readback",
verificationEvidence: [{ type: "scratch_disk_snapshot", folderTypes, destination }], undoSupported: true,
undoLabel: "Configure scratch disks", transactionActionGroup: true, cancellationSupported: true
})
};
}
async function workspaceStatus(args) {
assertObject(args); assertOnlyKeys(args, []);
if (!workspace || typeof workspace.status !== "function") return { configured: false, accessMode: "unavailable", rootName: null, persistent: false, pathDisclosure: "redacted" };
return workspace.status();
}
async function allowedPath(value, label, kind) {
const path = boundedString(value, label, 4096);
return workspace && typeof workspace.assertPathAllowed === "function"
? await workspace.assertPathAllowed(path, { label, kind })
: path;
}
function operationSemantics(options) {
return Protocol && typeof Protocol.operationSemantics === "function" ? Protocol.operationSemantics(options) : undefined;
}
function mutationResult(verified, values, boundary, undoLabel) {
return {
...values, outcome: verified ? "verified" : "committed_unverified", verified,
verificationBoundary: boundary,
operation: operationSemantics({
mutatesProject: true, verificationStatus: verified ? "verified" : "not_verified", verificationBoundary: boundary,
verificationEvidence: [{ type: boundary, verified }], undoSupported: true, undoLabel,
transactionActionGroup: true, cancellationSupported: true
})
};
}
function assertCommitted(committed, operation) {
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the " + operation + " transaction");
}
function canInspectProject() { return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function"); }
function canUseClipItems() { return canInspectProject() && !!(ppro.ClipProjectItem && typeof ppro.ClipProjectItem.cast === "function"); }
function canUseVideoEffects() { return !!(ppro.VideoFilterFactory && typeof ppro.VideoFilterFactory.createComponent === "function" && typeof ppro.VideoFilterFactory.getMatchNames === "function"); }
function canUseAudioEffects() { return !!(ppro.AudioFilterFactory && typeof ppro.AudioFilterFactory.createComponentByDisplayName === "function" && typeof ppro.AudioFilterFactory.getDisplayNames === "function"); }
function canUseEffects() { return canInspectProject() && (canUseVideoEffects() || canUseAudioEffects()); }
function canInspectTrackItemIdentity() { return canInspectProject() && !!(ppro.Constants && ppro.Constants.TrackItemType); }
function canUseSelection() { return canInspectProject() && !!(ppro.Constants && ppro.Constants.TrackItemType); }
async function canManageSelection() {
if (!canUseSelection() || !ppro.TrackItemSelection || typeof ppro.TrackItemSelection.createEmptySelection !== "function") return false;
try {
const project = await ppro.Project.getActiveProject();
if (!project) return true;
const sequence = await project.getActiveSequence();
return !sequence || typeof sequence.getSelection === "function" &&
typeof sequence.setSelection === "function" && typeof sequence.clearSelection === "function";
} catch (_) { return false; }
}
function canUseEffectsSelection() { return canUseEffects() && canUseSelection(); }
function canDetectScenes() { return canUseSelection() && !!(ppro.SequenceUtils && typeof ppro.SequenceUtils.performSceneEditDetectionOnSelection === "function"); }
function canAttachProxy() { return canUseClipItems(); }
function canRelink() { return canUseClipItems(); }
function canUseProjectSettings() { return canInspectProject() && !!(ppro.ProjectSettings && typeof ppro.ProjectSettings.getScratchDiskSettings === "function"); }
function canUseIngest() { return canInspectProject() && !!(ppro.ProjectSettings && typeof ppro.ProjectSettings.getIngestSettings === "function" && typeof ppro.ProjectSettings.createSetIngestSettingsAction === "function"); }
function canUseMetadata() { return canUseClipItems() && !!(ppro.Metadata && typeof ppro.Metadata.getProjectMetadata === "function" && typeof ppro.Metadata.getXMPMetadata === "function" && typeof ppro.Metadata.createSetProjectMetadataAction === "function" && typeof ppro.Metadata.createSetXMPMetadataAction === "function"); }
function canGetProjectColumnsMetadata() { return canUseClipItems() && !!(ppro.Metadata && typeof ppro.Metadata.getProjectColumnsMetadata === "function"); }
function canGetProjectPanelMetadata() { return canInspectProject() && !!(ppro.Metadata && typeof ppro.Metadata.getProjectPanelMetadata === "function"); }
function canCreateProjectMetadataSchema() {
return canGetProjectPanelMetadata() && !!(ppro.Metadata && typeof ppro.Metadata.addPropertyToProjectMetadataSchema === "function"
&& ppro.Metadata.METADATA_TYPE_INTEGER != null && ppro.Metadata.METADATA_TYPE_REAL != null
&& ppro.Metadata.METADATA_TYPE_TEXT != null && ppro.Metadata.METADATA_TYPE_BOOLEAN != null);
}
async function canSetProjectPanelMetadata() {
if (!canGetProjectPanelMetadata() || !ppro.Metadata || typeof ppro.Metadata.setProjectPanelMetadata !== "function") return false;
try {
const project = await ppro.Project.getActiveProject();
return !project || typeof project.lockedAccess === "function";
} catch (_) { return false; }
}
function canInspectColor() { return canUseClipItems(); }
async function canInspectEnvironment() {
if (!canInspectProject() || !ppro.Utils || typeof ppro.Utils.isAEInstalled !== "function") return false;
try {
const project = await ppro.Project.getActiveProject();
if (!project) return true;
if (typeof project.getColorSettings !== "function") return false;
const settings = await project.getColorSettings();
return !!settings && typeof settings.getGraphicsWhiteLuminance === "function" &&
typeof settings.getSupportedGraphicsWhiteLuminances === "function";
} catch (_) { return false; }
}
function canConformFootage() { return canUseClipItems(); }
function canInspectSourceMonitor() {
return !!(ppro.SourceMonitor && typeof ppro.SourceMonitor.getPosition === "function" && typeof ppro.SourceMonitor.getProjectItem === "function");
}
function canOpenSourceMonitor() {
return !!(ppro.SourceMonitor && typeof ppro.SourceMonitor.getProjectItem === "function" &&
typeof ppro.SourceMonitor.openProjectItem === "function" && typeof ppro.SourceMonitor.openFilePath === "function");
}
function canPlaySourceMonitor() { return !!(ppro.SourceMonitor && typeof ppro.SourceMonitor.play === "function"); }
function canCloseSourceMonitor() {
return !!(ppro.SourceMonitor && typeof ppro.SourceMonitor.closeClip === "function" && typeof ppro.SourceMonitor.closeAllClips === "function");
}
function canConfigureScratch() { return canUseProjectSettings() && typeof ppro.ProjectSettings.createSetScratchDiskSettingsAction === "function"; }
function canReportWorkspace() { return !!(workspace && typeof workspace.status === "function"); }
return definitions;
}
function validateTrackTarget(args, allowedKeys) {
assertObject(args); assertOnlyKeys(args, allowedKeys.concat(["operationId"]));
return {
mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]),
trackIndex: nonNegativeInt(args.trackIndex, "trackIndex"), clipIndex: nonNegativeInt(args.clipIndex, "clipIndex")
};
}
function validateTrackItemIdentityArgs(args) {
assertObject(args); assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex", "expectedSequenceGuid"]);
const result = {
mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]),
trackIndex: nonNegativeInt(args.trackIndex, "trackIndex"),
clipIndex: nonNegativeInt(args.clipIndex, "clipIndex")
};
if (args.expectedSequenceGuid != null) result.expectedSequenceGuid = boundedString(args.expectedSequenceGuid, "expectedSequenceGuid", 512);
return result;
}
function validateEffectAdd(args, selection) {
const keys = selection ? ["mediaType", "effectId", "insertionIndex", "operationId"] : ["mediaType", "trackIndex", "clipIndex", "effectId", "insertionIndex", "operationId"];
const target = selection ? (assertObject(args), assertOnlyKeys(args, keys), { mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]) }) : validateTrackTarget(args, keys.filter((key) => key !== "operationId"));
target.effectId = boundedString(args.effectId, "effectId", 256);
target.insertionIndex = args.insertionIndex == null ? null : nonNegativeInt(args.insertionIndex, "insertionIndex");
return target;
}
function validateEffectRemove(args, selection) {
const keys = selection ? ["mediaType", "componentIndex", "expectedEffectId", "operationId"] : ["mediaType", "trackIndex", "clipIndex", "componentIndex", "expectedEffectId", "operationId"];
const target = selection ? (assertObject(args), assertOnlyKeys(args, keys), { mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]) }) : validateTrackTarget(args, keys.filter((key) => key !== "operationId"));
target.componentIndex = nonNegativeInt(args.componentIndex, "componentIndex");
target.expectedEffectId = boundedString(args.expectedEffectId, "expectedEffectId", 256);
return target;
}
function validateSelectionUpdateArgs(args) {
assertObject(args);
assertOnlyKeys(args, ["mode", "expectedSequenceGuid", "items", "operationId"]);
const mode = enumValue(args.mode, "mode", ["replace", "add", "remove", "clear"]);
const expectedSequenceGuid = boundedString(args.expectedSequenceGuid, "expectedSequenceGuid", 512);
if (mode === "clear") {
if (Object.prototype.hasOwnProperty.call(args, "items")) throw commandError("UXP_INVALID_ARGUMENT", "items must be omitted when mode is clear");
return { mode, expectedSequenceGuid, items: [] };
}
if (!Array.isArray(args.items) || !args.items.length || args.items.length > MAX_SELECTION_ITEMS) {
throw commandError("UXP_INVALID_ARGUMENT", "items must contain 1-" + MAX_SELECTION_ITEMS + " timeline targets");
}
const coordinates = new Set(), items = args.items.map((raw, index) => {
assertObject(raw);
assertOnlyKeys(raw, [
"mediaType", "trackIndex", "clipIndex", "expectedProjectItemId",
"expectedStartSeconds", "expectedEndSeconds"
]);
const item = {
mediaType: enumValue(raw.mediaType, "items[" + index + "].mediaType", ["video", "audio"]),
trackIndex: nonNegativeInt(raw.trackIndex, "items[" + index + "].trackIndex"),
clipIndex: nonNegativeInt(raw.clipIndex, "items[" + index + "].clipIndex"),
expectedProjectItemId: boundedString(raw.expectedProjectItemId, "items[" + index + "].expectedProjectItemId", 512),
expectedStartSeconds: finiteNumber(raw.expectedStartSeconds, "items[" + index + "].expectedStartSeconds", 0, Number.MAX_SAFE_INTEGER),
expectedEndSeconds: finiteNumber(raw.expectedEndSeconds, "items[" + index + "].expectedEndSeconds", 0, Number.MAX_SAFE_INTEGER)
};
if (item.expectedEndSeconds < item.expectedStartSeconds) {
throw commandError("UXP_INVALID_ARGUMENT", "items[" + index + "].expectedEndSeconds must not be before expectedStartSeconds");
}
const coordinate = item.mediaType + ":" + item.trackIndex + ":" + item.clipIndex;
if (coordinates.has(coordinate)) throw commandError("UXP_INVALID_ARGUMENT", "items must not contain duplicate timeline coordinates");
coordinates.add(coordinate);
return item;
});
return { mode, expectedSequenceGuid, items };
}
function validateSelectionTargetInspectionArgs(args) {
assertObject(args);
assertOnlyKeys(args, ["items"]);
if (!Array.isArray(args.items) || !args.items.length || args.items.length > MAX_SELECTION_ITEMS) {
throw commandError("UXP_INVALID_ARGUMENT", "items must contain 1-" + MAX_SELECTION_ITEMS + " timeline targets");
}
const coordinates = new Set();
return args.items.map((raw, index) => {
assertObject(raw);
assertOnlyKeys(raw, ["mediaType", "trackIndex", "clipIndex"]);
const item = {
mediaType: enumValue(raw.mediaType, "items[" + index + "].mediaType", ["video", "audio"]),
trackIndex: nonNegativeInt(raw.trackIndex, "items[" + index + "].trackIndex"),
clipIndex: nonNegativeInt(raw.clipIndex, "items[" + index + "].clipIndex")
};
const coordinate = item.mediaType + ":" + item.trackIndex + ":" + item.clipIndex;
if (coordinates.has(coordinate)) throw commandError("UXP_INVALID_ARGUMENT", "items must not contain duplicate timeline coordinates");
coordinates.add(coordinate);
return item;
});
}
function validateProjectItemTarget(args) {
const hasId = args.projectItemId != null, hasName = args.projectItemName != null;
if (hasId && hasName) throw commandError("UXP_INVALID_ARGUMENT", "Pass either projectItemId or projectItemName, not both");
const result = {};
if (hasId) result.projectItemId = boundedString(args.projectItemId, "projectItemId", 512);
if (hasName) result.projectItemName = boundedString(args.projectItemName, "projectItemName", 255);
return result;
}
function validateUpdatedFields(value, required) {
if (!required && value == null) return [];
if (!Array.isArray(value) || !value.length || value.length > 128) throw commandError("UXP_INVALID_ARGUMENT", "updatedFields must contain 1-128 metadata field names");
const fields = value.map((item, index) => boundedString(item, "updatedFields[" + index + "]", 512));
if (new Set(fields).size !== fields.length) throw commandError("UXP_INVALID_ARGUMENT", "updatedFields must not contain duplicates");
return fields;
}
function validateConformanceArgs(args) {
assertObject(args);
const keys = [
"projectItemId", "projectItemName", "frameRate", "pixelAspectRatio", "fieldType", "removePullDown",
"alphaUsage", "ignoreAlpha", "invertAlpha", "vrConform", "vrLayout", "vrHorzView", "vrVertView", "inputLutId", "operationId"
];
assertOnlyKeys(args, keys);
const result = validateProjectItemTarget(args), numericEnums = ["fieldType", "alphaUsage", "vrConform", "vrLayout"];
if (args.frameRate != null) result.frameRate = finiteNumber(args.frameRate, "frameRate", 1, 240);
if (args.pixelAspectRatio != null) result.pixelAspectRatio = finiteNumber(args.pixelAspectRatio, "pixelAspectRatio", 0.01, 100);
for (const key of numericEnums) if (args[key] != null) result[key] = boundedInt(args[key], key, 0, 64);
for (const key of ["removePullDown", "ignoreAlpha", "invertAlpha"]) if (args[key] != null) result[key] = requiredBoolean(args[key], key);
if (args.vrHorzView != null) result.vrHorzView = finiteNumber(args.vrHorzView, "vrHorzView", 1, 360);
if (args.vrVertView != null) result.vrVertView = finiteNumber(args.vrVertView, "vrVertView", 1, 180);
if (args.inputLutId != null) result.inputLutId = boundedStringAllowEmpty(args.inputLutId, "inputLutId", 512);
if (!Object.keys(result).some((key) => key !== "projectItemId" && key !== "projectItemName")) throw commandError("UXP_INVALID_ARGUMENT", "At least one conformance field is required");
return result;
}
function assertObject(value) { if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", "args must be an object"); }
function assertOnlyKeys(value, allowed) { const unknown = Object.keys(value).filter((key) => !allowed.includes(key)); if (unknown.length) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown[0]); }
function boundedString(value, name, maximum) { if (typeof value !== "string" || !value.trim() || value.length > maximum) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a non-empty string of at most " + maximum + " characters"); return value; }
function boundedStringAllowEmpty(value, name, maximum) { if (typeof value !== "string" || value.length > maximum) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a string of at most " + maximum + " characters"); return value; }
function boundedUtf8StringAllowEmpty(value, name, maximumBytes) {
if (typeof value !== "string" || utf8ByteLength(value) > maximumBytes) {
throw commandError("UXP_INVALID_ARGUMENT", name + " must be a UTF-8 string of at most " + maximumBytes + " bytes");
}
return value;
}
function requiredOperationId(value, name) {
if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) {
throw commandError("UXP_INVALID_ARGUMENT", name + " must be a 1-128 character token for safe replay");
}
return value;
}
function nonNegativeInt(value, name) { if (!Number.isInteger(value) || value < 0) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a non-negative integer"); return value; }
function boundedInt(value, name, minimum, maximum) { if (!Number.isInteger(value) || value < minimum || value > maximum) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from " + minimum + " to " + maximum); return value; }
function finiteNumber(value, name, minimum, maximum) { const number = Number(value); if (!Number.isFinite(number) || number < minimum || number > maximum) throw commandError("UXP_INVALID_ARGUMENT", name + " must be from " + minimum + " to " + maximum); return number; }
function requiredBoolean(value, name) { if (typeof value !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a boolean"); return value; }
function optionalBoolean(value, fallback, name) { return value == null ? fallback : requiredBoolean(value, name); }
function enumValue(value, name, allowed) { if (!allowed.includes(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", ")); return value; }
function boundedEnumArray(value, name, allowed, maximum) {
if (!Array.isArray(value) || !value.length || value.length > maximum) throw commandError("UXP_INVALID_ARGUMENT", name + " must contain 1-" + maximum + " values");
const result = value.map((item, index) => enumValue(item, name + "[" + index + "]", allowed));
if (new Set(result).size !== result.length) throw commandError("UXP_INVALID_ARGUMENT", name + " must not contain duplicates");
return result;
}
function requireConfirmation(value, message) { if (value !== true) throw commandError("UXP_CONFIRMATION_REQUIRED", message + "; pass confirmNonUndoable=true after review"); }
function tickSeconds(value) { const seconds = value && Number(value.seconds); return Number.isFinite(seconds) ? seconds : null; }
function pathEqual(left, right) {
if (typeof left !== "string" || typeof right !== "string") return false;
const normalizedLeft = left.replace(/\\/g, "/").replace(/\/$/, "");
const normalizedRight = right.replace(/\\/g, "/").replace(/\/$/, "");
const windowsPaths = /^(?:[A-Za-z]:\/|\/\/)/.test(normalizedLeft) && /^(?:[A-Za-z]:\/|\/\/)/.test(normalizedRight);
return windowsPaths ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight;
}
function valuesEqual(left, right) { return typeof right === "number" ? typeof left === "number" && Math.abs(left - right) < 0.000001 : left === right; }
function commandError(code, message) { const error = new Error(message); error.code = code; return error; }
return { createWorkflowDefinitions, commandError };
});