feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
This commit is contained in:
+210
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a macOS DMG for the Premiere Pro AI Chat plugin.
|
||||
# The DMG contains the plugin folder + an installer script.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
PLUGIN_SRC="$PROJECT_ROOT/chat-plugin"
|
||||
BUILD_DIR="$PROJECT_ROOT/build"
|
||||
DMG_NAME="Premiere-Pro-AI-Chat"
|
||||
DMG_VOLUME="Premiere Pro AI Chat"
|
||||
VERSION="1.0.0"
|
||||
|
||||
echo "========================================"
|
||||
echo " Building DMG: $DMG_NAME v$VERSION"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Clean previous DMG staging (preserve other build artifacts)
|
||||
rm -rf "$BUILD_DIR/dmg-staging"
|
||||
mkdir -p "$BUILD_DIR/dmg-staging"
|
||||
|
||||
STAGING="$BUILD_DIR/dmg-staging"
|
||||
|
||||
# ---- Copy plugin files ----
|
||||
echo "Copying plugin files..."
|
||||
mkdir -p "$STAGING/Premiere Pro AI Chat.cep"
|
||||
cp -R "$PLUGIN_SRC/"* "$STAGING/Premiere Pro AI Chat.cep/"
|
||||
echo "✓ Plugin files copied"
|
||||
|
||||
# ---- Create installer script inside DMG ----
|
||||
cat > "$STAGING/Install Plugin.command" << 'INSTALLER_EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Premiere Pro AI Chat — One-click Installer
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_NAME="com.ppro.ai.chat"
|
||||
PLUGIN_SRC="$SCRIPT_DIR/Premiere Pro AI Chat.cep"
|
||||
CEP_DIR="$HOME/Library/Application Support/Adobe/CEP/extensions"
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════╗"
|
||||
echo "║ Premiere Pro AI Chat — Installer ║"
|
||||
echo "╚══════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Verify source
|
||||
if [ ! -d "$PLUGIN_SRC" ]; then
|
||||
echo "✗ Error: Plugin files not found."
|
||||
echo " Make sure you're running this from the DMG."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create CEP directory
|
||||
mkdir -p "$CEP_DIR"
|
||||
|
||||
# Remove old installation
|
||||
if [ -d "$CEP_DIR/$PLUGIN_NAME" ] || [ -L "$CEP_DIR/$PLUGIN_NAME" ]; then
|
||||
echo "Removing previous installation..."
|
||||
rm -rf "$CEP_DIR/$PLUGIN_NAME"
|
||||
fi
|
||||
|
||||
# Copy plugin
|
||||
echo "Installing plugin..."
|
||||
cp -R "$PLUGIN_SRC" "$CEP_DIR/$PLUGIN_NAME"
|
||||
echo "✓ Plugin installed to: $CEP_DIR/$PLUGIN_NAME"
|
||||
|
||||
# Enable unsigned extensions
|
||||
echo "Enabling unsigned CEP extensions..."
|
||||
for ver in 9 10 11 12; do
|
||||
defaults write com.adobe.CSXS.$ver PlayerDebugMode 1 2>/dev/null || true
|
||||
done
|
||||
echo "✓ Debug mode enabled"
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════"
|
||||
echo " ✓ Installation complete!"
|
||||
echo ""
|
||||
echo " Next steps:"
|
||||
echo " 1. Restart Premiere Pro (if running)"
|
||||
echo " 2. Go to: Window → Extensions → AI Chat"
|
||||
echo " 3. Enter your Claude or Gemini API key"
|
||||
echo " 4. Start chatting!"
|
||||
echo "════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "Press any key to close..."
|
||||
read -n 1 -s
|
||||
INSTALLER_EOF
|
||||
|
||||
chmod +x "$STAGING/Install Plugin.command"
|
||||
echo "✓ Installer script created"
|
||||
|
||||
# ---- Create README ----
|
||||
cat > "$STAGING/README.txt" << 'README_EOF'
|
||||
Premiere Pro AI Chat Plugin
|
||||
============================
|
||||
|
||||
An embedded AI chat panel for Adobe Premiere Pro.
|
||||
Control your edits with natural language using Claude or Gemini.
|
||||
|
||||
INSTALLATION
|
||||
============
|
||||
|
||||
Option 1: Double-click "Install Plugin.command"
|
||||
- This will copy the plugin to your Adobe CEP extensions folder
|
||||
- It will enable unsigned extensions automatically
|
||||
|
||||
Option 2: Manual Install
|
||||
- Copy the "Premiere Pro AI Chat.cep" folder to:
|
||||
~/Library/Application Support/Adobe/CEP/extensions/com.ppro.ai.chat
|
||||
- Enable unsigned extensions by running in Terminal:
|
||||
defaults write com.adobe.CSXS.11 PlayerDebugMode 1
|
||||
|
||||
USAGE
|
||||
=====
|
||||
|
||||
1. Open Premiere Pro
|
||||
2. Go to: Window → Extensions → AI Chat
|
||||
3. Choose Claude or Gemini as your AI provider
|
||||
4. Enter your API key
|
||||
5. Start chatting! The AI can:
|
||||
- Query your project (clips, sequences, tracks)
|
||||
- Edit your timeline (add clips, effects, transitions)
|
||||
- Export sequences
|
||||
- And much more
|
||||
|
||||
REQUIREMENTS
|
||||
============
|
||||
|
||||
- macOS 10.14+
|
||||
- Adobe Premiere Pro 2020 (v14.0) or later
|
||||
- Claude API key (console.anthropic.com) or
|
||||
Gemini API key (aistudio.google.com)
|
||||
|
||||
SUPPORT
|
||||
=======
|
||||
|
||||
GitHub: https://github.com/leancoderkavy/premiere-pro-mcp
|
||||
Issues: https://github.com/leancoderkavy/premiere-pro-mcp/issues
|
||||
README_EOF
|
||||
|
||||
echo "✓ README created"
|
||||
|
||||
# ---- Create Uninstaller ----
|
||||
cat > "$STAGING/Uninstall Plugin.command" << 'UNINSTALL_EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
PLUGIN_NAME="com.ppro.ai.chat"
|
||||
CEP_DIR="$HOME/Library/Application Support/Adobe/CEP/extensions"
|
||||
|
||||
echo ""
|
||||
echo "Uninstalling Premiere Pro AI Chat..."
|
||||
|
||||
if [ -d "$CEP_DIR/$PLUGIN_NAME" ] || [ -L "$CEP_DIR/$PLUGIN_NAME" ]; then
|
||||
rm -rf "$CEP_DIR/$PLUGIN_NAME"
|
||||
echo "✓ Plugin removed."
|
||||
else
|
||||
echo "Plugin not found — nothing to remove."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Press any key to close..."
|
||||
read -n 1 -s
|
||||
UNINSTALL_EOF
|
||||
|
||||
chmod +x "$STAGING/Uninstall Plugin.command"
|
||||
echo "✓ Uninstaller created"
|
||||
|
||||
# ---- Build DMG ----
|
||||
echo ""
|
||||
echo "Building DMG..."
|
||||
|
||||
DMG_PATH="$BUILD_DIR/$DMG_NAME-v$VERSION.dmg"
|
||||
TEMP_DMG="$BUILD_DIR/temp.dmg"
|
||||
|
||||
# Create a temporary DMG
|
||||
hdiutil create -srcfolder "$STAGING" \
|
||||
-volname "$DMG_VOLUME" \
|
||||
-format UDRW \
|
||||
-fs HFS+ \
|
||||
-size 20m \
|
||||
"$TEMP_DMG" \
|
||||
-quiet
|
||||
|
||||
# Convert to compressed read-only DMG
|
||||
hdiutil convert "$TEMP_DMG" \
|
||||
-format UDZO \
|
||||
-imagekey zlib-level=9 \
|
||||
-o "$DMG_PATH" \
|
||||
-quiet
|
||||
|
||||
rm -f "$TEMP_DMG"
|
||||
|
||||
echo "✓ DMG built: $DMG_PATH"
|
||||
|
||||
# Get file size
|
||||
DMG_SIZE=$(du -h "$DMG_PATH" | cut -f1 | xargs)
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " ✓ Build complete!"
|
||||
echo ""
|
||||
echo " File: $DMG_PATH"
|
||||
echo " Size: $DMG_SIZE"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a cross-platform ZIP for the Premiere Pro AI Chat plugin.
|
||||
# The ZIP contains the plugin folder + installers for macOS and Windows.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
PLUGIN_SRC="$PROJECT_ROOT/chat-plugin"
|
||||
BUILD_DIR="$PROJECT_ROOT/build"
|
||||
ZIP_NAME="Premiere-Pro-AI-Chat"
|
||||
VERSION="1.0.0"
|
||||
|
||||
echo "========================================"
|
||||
echo " Building ZIP: $ZIP_NAME v$VERSION"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Clean previous build
|
||||
rm -rf "$BUILD_DIR/zip-staging"
|
||||
mkdir -p "$BUILD_DIR/zip-staging"
|
||||
|
||||
STAGING="$BUILD_DIR/zip-staging/$ZIP_NAME-v$VERSION"
|
||||
mkdir -p "$STAGING"
|
||||
|
||||
# ---- Copy plugin files ----
|
||||
echo "Copying plugin files..."
|
||||
mkdir -p "$STAGING/com.ppro.ai.chat"
|
||||
cp -R "$PLUGIN_SRC/"* "$STAGING/com.ppro.ai.chat/"
|
||||
echo "✓ Plugin files copied"
|
||||
|
||||
# ---- Create Windows installer (.bat) ----
|
||||
cat > "$STAGING/Install-Windows.bat" << 'BAT_EOF'
|
||||
@echo off
|
||||
REM Premiere Pro AI Chat — Windows Installer
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
echo.
|
||||
echo ==========================================
|
||||
echo Premiere Pro AI Chat - Windows Installer
|
||||
echo ==========================================
|
||||
echo.
|
||||
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "PLUGIN_SRC=%SCRIPT_DIR%com.ppro.ai.chat"
|
||||
set "PLUGIN_NAME=com.ppro.ai.chat"
|
||||
set "CEP_DIR=%APPDATA%\Adobe\CEP\extensions"
|
||||
|
||||
if not exist "%PLUGIN_SRC%" (
|
||||
echo [ERROR] Plugin files not found.
|
||||
echo Make sure you extracted the ZIP first.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Create CEP directory
|
||||
if not exist "%CEP_DIR%" mkdir "%CEP_DIR%"
|
||||
|
||||
REM Remove old installation
|
||||
if exist "%CEP_DIR%\%PLUGIN_NAME%" (
|
||||
echo Removing previous installation...
|
||||
rmdir /s /q "%CEP_DIR%\%PLUGIN_NAME%"
|
||||
)
|
||||
|
||||
REM Copy plugin
|
||||
echo Installing plugin...
|
||||
xcopy /s /e /i /q "%PLUGIN_SRC%" "%CEP_DIR%\%PLUGIN_NAME%"
|
||||
echo [OK] Plugin installed to: %CEP_DIR%\%PLUGIN_NAME%
|
||||
|
||||
REM Enable unsigned extensions (CSXS 9-12)
|
||||
echo Enabling unsigned CEP extensions...
|
||||
for %%v in (9 10 11 12) do (
|
||||
reg add "HKCU\SOFTWARE\Adobe\CSXS.%%v" /v PlayerDebugMode /t REG_SZ /d 1 /f >nul 2>&1
|
||||
)
|
||||
echo [OK] Debug mode enabled
|
||||
|
||||
echo.
|
||||
echo ==========================================
|
||||
echo [OK] Installation complete!
|
||||
echo.
|
||||
echo Next steps:
|
||||
echo 1. Restart Premiere Pro (if running)
|
||||
echo 2. Go to: Window ^> Extensions ^> AI Chat
|
||||
echo 3. Enter your Claude or Gemini API key
|
||||
echo 4. Start chatting!
|
||||
echo ==========================================
|
||||
echo.
|
||||
pause
|
||||
BAT_EOF
|
||||
|
||||
echo "✓ Windows installer created"
|
||||
|
||||
# ---- Create Windows uninstaller (.bat) ----
|
||||
cat > "$STAGING/Uninstall-Windows.bat" << 'UNBAT_EOF'
|
||||
@echo off
|
||||
REM Premiere Pro AI Chat — Windows Uninstaller
|
||||
|
||||
set "PLUGIN_NAME=com.ppro.ai.chat"
|
||||
set "CEP_DIR=%APPDATA%\Adobe\CEP\extensions"
|
||||
|
||||
echo.
|
||||
echo Uninstalling Premiere Pro AI Chat...
|
||||
|
||||
if exist "%CEP_DIR%\%PLUGIN_NAME%" (
|
||||
rmdir /s /q "%CEP_DIR%\%PLUGIN_NAME%"
|
||||
echo [OK] Plugin removed.
|
||||
) else (
|
||||
echo Plugin not found - nothing to remove.
|
||||
)
|
||||
|
||||
echo.
|
||||
pause
|
||||
UNBAT_EOF
|
||||
|
||||
echo "✓ Windows uninstaller created"
|
||||
|
||||
# ---- Create macOS installer (.command) ----
|
||||
cat > "$STAGING/Install-macOS.command" << 'MAC_EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Premiere Pro AI Chat — macOS Installer
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_NAME="com.ppro.ai.chat"
|
||||
PLUGIN_SRC="$SCRIPT_DIR/com.ppro.ai.chat"
|
||||
CEP_DIR="$HOME/Library/Application Support/Adobe/CEP/extensions"
|
||||
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════╗"
|
||||
echo "║ Premiere Pro AI Chat — Installer ║"
|
||||
echo "╚══════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
if [ ! -d "$PLUGIN_SRC" ]; then
|
||||
echo "✗ Error: Plugin files not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$CEP_DIR"
|
||||
|
||||
if [ -d "$CEP_DIR/$PLUGIN_NAME" ] || [ -L "$CEP_DIR/$PLUGIN_NAME" ]; then
|
||||
echo "Removing previous installation..."
|
||||
rm -rf "$CEP_DIR/$PLUGIN_NAME"
|
||||
fi
|
||||
|
||||
echo "Installing plugin..."
|
||||
cp -R "$PLUGIN_SRC" "$CEP_DIR/$PLUGIN_NAME"
|
||||
echo "✓ Plugin installed to: $CEP_DIR/$PLUGIN_NAME"
|
||||
|
||||
echo "Enabling unsigned CEP extensions..."
|
||||
for ver in 9 10 11 12; do
|
||||
defaults write com.adobe.CSXS.$ver PlayerDebugMode 1 2>/dev/null || true
|
||||
done
|
||||
echo "✓ Debug mode enabled"
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════"
|
||||
echo " ✓ Installation complete!"
|
||||
echo ""
|
||||
echo " Next steps:"
|
||||
echo " 1. Restart Premiere Pro (if running)"
|
||||
echo " 2. Go to: Window → Extensions → AI Chat"
|
||||
echo " 3. Enter your Claude or Gemini API key"
|
||||
echo " 4. Start chatting!"
|
||||
echo "════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "Press any key to close..."
|
||||
read -n 1 -s
|
||||
MAC_EOF
|
||||
|
||||
chmod +x "$STAGING/Install-macOS.command"
|
||||
echo "✓ macOS installer created"
|
||||
|
||||
# ---- Create macOS uninstaller ----
|
||||
cat > "$STAGING/Uninstall-macOS.command" << 'UNMAC_EOF'
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
PLUGIN_NAME="com.ppro.ai.chat"
|
||||
CEP_DIR="$HOME/Library/Application Support/Adobe/CEP/extensions"
|
||||
|
||||
echo ""
|
||||
echo "Uninstalling Premiere Pro AI Chat..."
|
||||
|
||||
if [ -d "$CEP_DIR/$PLUGIN_NAME" ] || [ -L "$CEP_DIR/$PLUGIN_NAME" ]; then
|
||||
rm -rf "$CEP_DIR/$PLUGIN_NAME"
|
||||
echo "✓ Plugin removed."
|
||||
else
|
||||
echo "Plugin not found — nothing to remove."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Press any key to close..."
|
||||
read -n 1 -s
|
||||
UNMAC_EOF
|
||||
|
||||
chmod +x "$STAGING/Uninstall-macOS.command"
|
||||
echo "✓ macOS uninstaller created"
|
||||
|
||||
# ---- Create README ----
|
||||
cat > "$STAGING/README.txt" << 'README_EOF'
|
||||
Premiere Pro AI Chat Plugin
|
||||
============================
|
||||
|
||||
An embedded AI chat panel for Adobe Premiere Pro.
|
||||
Control your edits with natural language using Claude or Gemini.
|
||||
|
||||
INSTALLATION
|
||||
============
|
||||
|
||||
Windows:
|
||||
1. Extract this ZIP file
|
||||
2. Right-click "Install-Windows.bat" → Run as administrator
|
||||
3. Restart Premiere Pro
|
||||
4. Go to: Window → Extensions → AI Chat
|
||||
|
||||
macOS:
|
||||
1. Extract this ZIP file
|
||||
2. Double-click "Install-macOS.command"
|
||||
3. Restart Premiere Pro
|
||||
4. Go to: Window → Extensions → AI Chat
|
||||
|
||||
Manual Install (any OS):
|
||||
- Copy the "com.ppro.ai.chat" folder to your CEP extensions folder:
|
||||
Windows: %APPDATA%\Adobe\CEP\extensions\
|
||||
macOS: ~/Library/Application Support/Adobe/CEP/extensions/
|
||||
- Enable unsigned extensions:
|
||||
Windows: Set registry key HKCU\SOFTWARE\Adobe\CSXS.11 → PlayerDebugMode = "1"
|
||||
macOS: Run: defaults write com.adobe.CSXS.11 PlayerDebugMode 1
|
||||
|
||||
UNINSTALL
|
||||
=========
|
||||
|
||||
Windows: Run "Uninstall-Windows.bat"
|
||||
macOS: Double-click "Uninstall-macOS.command"
|
||||
|
||||
USAGE
|
||||
=====
|
||||
|
||||
1. Open Premiere Pro
|
||||
2. Go to: Window → Extensions → AI Chat
|
||||
3. Choose Claude or Gemini as your AI provider
|
||||
4. Enter your API key
|
||||
5. Start chatting! The AI can:
|
||||
- Query your project (clips, sequences, tracks)
|
||||
- Edit your timeline (add clips, effects, transitions)
|
||||
- Export sequences
|
||||
- And much more
|
||||
|
||||
REQUIREMENTS
|
||||
============
|
||||
|
||||
- Windows 10+ or macOS 10.14+
|
||||
- Adobe Premiere Pro 2020 (v14.0) or later
|
||||
- Claude API key (console.anthropic.com) or
|
||||
Gemini API key (aistudio.google.com)
|
||||
|
||||
SUPPORT
|
||||
=======
|
||||
|
||||
GitHub: https://github.com/leancoderkavy/premiere-pro-mcp
|
||||
Issues: https://github.com/leancoderkavy/premiere-pro-mcp/issues
|
||||
README_EOF
|
||||
|
||||
echo "✓ README created"
|
||||
|
||||
# ---- Build ZIP ----
|
||||
echo ""
|
||||
echo "Building ZIP..."
|
||||
|
||||
ZIP_PATH="$BUILD_DIR/$ZIP_NAME-v$VERSION.zip"
|
||||
rm -f "$ZIP_PATH"
|
||||
|
||||
cd "$BUILD_DIR/zip-staging"
|
||||
zip -r -q "$ZIP_PATH" "$ZIP_NAME-v$VERSION/"
|
||||
|
||||
ZIP_SIZE=$(du -h "$ZIP_PATH" | cut -f1 | xargs)
|
||||
|
||||
echo "✓ ZIP built: $ZIP_PATH"
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " ✓ Build complete!"
|
||||
echo ""
|
||||
echo " File: $ZIP_PATH"
|
||||
echo " Size: $ZIP_SIZE"
|
||||
echo " Works on: macOS + Windows"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { cp, copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { validateClaudeSource, validateClaudeStage } from "./validate-distribution.mjs";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stage = path.join(root, "build", "claude-desktop");
|
||||
const artifacts = path.join(root, "artifacts");
|
||||
const manifestSource = path.join(root, "claude-desktop", "manifest.json");
|
||||
const packageSource = path.join(root, "package.json");
|
||||
const lockSource = path.join(root, "package-lock.json");
|
||||
|
||||
const run = (command, args, cwd) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, { cwd, stdio: "inherit", shell: false });
|
||||
child.on("error", reject);
|
||||
child.on("exit", (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`${command} exited with code ${code}`));
|
||||
});
|
||||
});
|
||||
|
||||
function runNpm(args, cwd) {
|
||||
// On Windows, spawn("npm", ...) can fail because npm is exposed as npm.cmd
|
||||
// rather than an executable. Running npm's bundled CLI through this Node
|
||||
// process works on every supported platform and keeps shell execution off.
|
||||
const npmCli = path.join(
|
||||
path.dirname(process.execPath),
|
||||
"node_modules",
|
||||
"npm",
|
||||
"bin",
|
||||
"npm-cli.js",
|
||||
);
|
||||
return existsSync(npmCli)
|
||||
? run(process.execPath, [npmCli, ...args], cwd)
|
||||
: run("npm", args, cwd);
|
||||
}
|
||||
|
||||
function runMcpb(args) {
|
||||
return runNpm(
|
||||
["exec", "--yes", "@anthropic-ai/mcpb@2.1.2", "--", ...args],
|
||||
root,
|
||||
);
|
||||
}
|
||||
|
||||
await validateClaudeSource();
|
||||
await rm(stage, { recursive: true, force: true });
|
||||
await mkdir(path.join(stage, "server"), { recursive: true });
|
||||
await mkdir(artifacts, { recursive: true });
|
||||
|
||||
await copyFile(manifestSource, path.join(stage, "manifest.json"));
|
||||
await cp(path.join(root, "dist"), path.join(stage, "server", "dist"), {
|
||||
recursive: true,
|
||||
});
|
||||
await copyFile(lockSource, path.join(stage, "package-lock.json"));
|
||||
|
||||
const packageJson = JSON.parse(await readFile(packageSource, "utf8"));
|
||||
await writeFile(
|
||||
path.join(stage, "package.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
// Keep this aligned with package-lock.json and manifest.json so npm ci and
|
||||
// MCPB validation both describe the same bundled server identity.
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
private: true,
|
||||
type: packageJson.type,
|
||||
engines: packageJson.engines,
|
||||
dependencies: packageJson.dependencies,
|
||||
overrides: packageJson.overrides,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
|
||||
await runNpm(["ci", "--omit=dev", "--ignore-scripts"], stage);
|
||||
await validateClaudeStage(stage);
|
||||
await runMcpb(["validate", path.join(stage, "manifest.json")]);
|
||||
|
||||
const mcpbPath = path.join(
|
||||
artifacts,
|
||||
`premiere-pro-mcp-${packageJson.version}.mcpb`,
|
||||
);
|
||||
await runMcpb(["pack", stage, mcpbPath]);
|
||||
await runMcpb(["info", mcpbPath]);
|
||||
|
||||
console.log(`Built ${path.relative(root, mcpbPath)}`);
|
||||
@@ -0,0 +1,50 @@
|
||||
param(
|
||||
[string]$ConnectorPackage = "",
|
||||
[string]$OutputDirectory = "",
|
||||
[switch]$RequireSigning,
|
||||
[string]$SigningCertificatePath = "",
|
||||
[string]$SigningCertificatePassword = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$projectDir = Split-Path -Parent $PSScriptRoot
|
||||
if (-not $ConnectorPackage) { $ConnectorPackage = Join-Path $projectDir "artifacts\MCPBridgeCEP.zxp" }
|
||||
if (-not $OutputDirectory) { $OutputDirectory = Join-Path $projectDir "artifacts\connector-installers" }
|
||||
$ConnectorPackage = [IO.Path]::GetFullPath($ConnectorPackage)
|
||||
$OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ConnectorPackage)) { throw "Verified connector package not found: $ConnectorPackage" }
|
||||
$package = Get-Content (Join-Path $projectDir "package.json") -Raw | ConvertFrom-Json
|
||||
$publishDir = Join-Path $OutputDirectory "windows-publish"
|
||||
New-Item -ItemType Directory -Force -Path $publishDir | Out-Null
|
||||
|
||||
dotnet publish (Join-Path $projectDir "installer\windows\PremiereConnectorInstaller.csproj") `
|
||||
--configuration Release --runtime win-x64 --self-contained true `
|
||||
--output $publishDir `
|
||||
-p:ConnectorPackage=$ConnectorPackage `
|
||||
-p:Version=$($package.version) `
|
||||
-p:PublishSingleFile=true `
|
||||
-p:DebugType=None `
|
||||
-p:DebugSymbols=false
|
||||
if ($LASTEXITCODE -ne 0) { throw "Windows connector installer build failed." }
|
||||
|
||||
$built = Join-Path $publishDir "PremiereConnectorInstaller.exe"
|
||||
$output = Join-Path $OutputDirectory "Premiere-Connector-Setup-$($package.version)-windows-x64.exe"
|
||||
Copy-Item -LiteralPath $built -Destination $output -Force
|
||||
|
||||
if ($SigningCertificatePath) {
|
||||
$signTool = Get-Command signtool.exe -ErrorAction SilentlyContinue
|
||||
if (-not $signTool) { throw "signtool.exe is required when a signing certificate is supplied." }
|
||||
& $signTool.Source sign /fd SHA256 /td SHA256 /tr http://timestamp.digicert.com /f $SigningCertificatePath /p $SigningCertificatePassword $output
|
||||
if ($LASTEXITCODE -ne 0) { throw "Authenticode signing failed." }
|
||||
& $signTool.Source verify /pa $output
|
||||
if ($LASTEXITCODE -ne 0) { throw "Authenticode verification failed." }
|
||||
}
|
||||
elseif ($RequireSigning) {
|
||||
throw "Production Windows installer signing was required, but no certificate was supplied."
|
||||
}
|
||||
|
||||
$hash = (Get-FileHash -LiteralPath $output -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
Write-Host "Built $output"
|
||||
Write-Host "SHA-256 $hash"
|
||||
if (-not $SigningCertificatePath) { Write-Warning "Preview artifact is not Authenticode-signed and must not be published as a production installer." }
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CONNECTOR_PACKAGE="${CONNECTOR_PACKAGE:-$PROJECT_DIR/artifacts/MCPBridgeCEP.zxp}"
|
||||
OUTPUT_DIRECTORY="${OUTPUT_DIRECTORY:-$PROJECT_DIR/artifacts/connector-installers}"
|
||||
REQUIRE_SIGNING="${REQUIRE_SIGNING:-false}"
|
||||
VERSION="$(node -p "require('$PROJECT_DIR/package.json').version")"
|
||||
|
||||
if [[ ! -f "$CONNECTOR_PACKAGE" ]]; then
|
||||
echo "Verified connector package not found: $CONNECTOR_PACKAGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TEMP_ROOT="$(mktemp -d)"
|
||||
trap 'rm -rf "$TEMP_ROOT"' EXIT
|
||||
PAYLOAD_ROOT="$TEMP_ROOT/payload"
|
||||
INSTALL_ROOT="$PAYLOAD_ROOT/Library/Application Support/Adobe/CEP/extensions/MCPBridgeCEP"
|
||||
mkdir -p "$INSTALL_ROOT" "$OUTPUT_DIRECTORY"
|
||||
ditto -x -k "$CONNECTOR_PACKAGE" "$INSTALL_ROOT"
|
||||
|
||||
if [[ ! -f "$INSTALL_ROOT/CSXS/manifest.xml" ]]; then
|
||||
echo "Connector package is missing CSXS/manifest.xml" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OUTPUT="$OUTPUT_DIRECTORY/Premiere-Connector-Setup-$VERSION-macos-universal.pkg"
|
||||
UNINSTALLER="$OUTPUT_DIRECTORY/Premiere-Connector-Uninstall-$VERSION-macos.command"
|
||||
ARGS=(--root "$PAYLOAD_ROOT" --identifier com.premieremcp.connector --version "$VERSION" --install-location /)
|
||||
if [[ -n "${MAC_INSTALLER_IDENTITY:-}" ]]; then
|
||||
ARGS+=(--sign "$MAC_INSTALLER_IDENTITY")
|
||||
elif [[ "$REQUIRE_SIGNING" == "true" ]]; then
|
||||
echo "Production macOS installer signing was required, but MAC_INSTALLER_IDENTITY is not configured." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pkgbuild "${ARGS[@]}" "$OUTPUT"
|
||||
cp "$PROJECT_DIR/scripts/uninstall-cep.sh" "$UNINSTALLER"
|
||||
chmod +x "$UNINSTALLER"
|
||||
pkgutil --check-signature "$OUTPUT" || {
|
||||
if [[ "$REQUIRE_SIGNING" == "true" ]]; then exit 1; fi
|
||||
echo "Preview artifact is unsigned and must not be published as a production installer." >&2
|
||||
}
|
||||
shasum -a 256 "$OUTPUT"
|
||||
shasum -a 256 "$UNINSTALLER"
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
param(
|
||||
[string]$OutputPath = "",
|
||||
[string]$ZxpSignCmdPath = "",
|
||||
[string]$CertificatePath = "",
|
||||
[string]$CertificatePassword = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$projectDir = Split-Path -Parent $PSScriptRoot
|
||||
$pluginSource = Join-Path $projectDir "cep-plugin"
|
||||
if (-not $OutputPath) {
|
||||
$OutputPath = Join-Path $projectDir "artifacts\MCPBridgeCEP.zxp"
|
||||
}
|
||||
$OutputPath = [System.IO.Path]::GetFullPath($OutputPath)
|
||||
$outputDir = Split-Path -Parent $OutputPath
|
||||
New-Item -ItemType Directory -Force -Path $outputDir | Out-Null
|
||||
|
||||
$temporaryRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("premiere-pro-mcp-zxp-" + [guid]::NewGuid().ToString("N"))
|
||||
New-Item -ItemType Directory -Path $temporaryRoot | Out-Null
|
||||
|
||||
try {
|
||||
if (-not $ZxpSignCmdPath) {
|
||||
$ZxpSignCmdPath = Join-Path $temporaryRoot "ZXPSignCmd.exe"
|
||||
$downloadUrl = "https://raw.githubusercontent.com/Adobe-CEP/CEP-Resources/ab5e4e3e53a42fad08e1225a22a991bb1ffe73f6/ZXPSignCMD/4.1.103/win64/ZXPSignCmd.exe"
|
||||
Invoke-WebRequest -Uri $downloadUrl -OutFile $ZxpSignCmdPath
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ZxpSignCmdPath)) {
|
||||
throw "ZXPSignCmd was not found at $ZxpSignCmdPath"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $pluginSource "CSXS\manifest.xml"))) {
|
||||
throw "CEP plugin manifest not found at $pluginSource"
|
||||
}
|
||||
|
||||
if (-not $CertificatePassword) {
|
||||
$CertificatePassword = [guid]::NewGuid().ToString("N")
|
||||
}
|
||||
if (-not $CertificatePath) {
|
||||
$CertificatePath = Join-Path $temporaryRoot "premiere-pro-mcp-release.p12"
|
||||
& $ZxpSignCmdPath -selfSignedCert US California "MCP for Adobe Premiere Pro" "MCP for Adobe Premiere Pro" $CertificatePassword $CertificatePath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ZXPSignCmd failed to create the release certificate."
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $OutputPath) {
|
||||
Remove-Item -LiteralPath $OutputPath -Force
|
||||
}
|
||||
& $ZxpSignCmdPath -sign $pluginSource $OutputPath $CertificatePath $CertificatePassword
|
||||
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $OutputPath)) {
|
||||
throw "ZXPSignCmd failed to create $OutputPath"
|
||||
}
|
||||
|
||||
& $ZxpSignCmdPath -verify $OutputPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ZXPSignCmd could not verify $OutputPath"
|
||||
}
|
||||
|
||||
Write-Host "Signed CEP package verified: $OutputPath"
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $temporaryRoot) {
|
||||
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force
|
||||
}
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, lstat, readdir, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
validateUxpManifest,
|
||||
validateUxpSource,
|
||||
} from "./validate-distribution.mjs";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const artifacts = path.join(root, "artifacts");
|
||||
const documentationFiles = new Set(["README.md", "DISTRIBUTION.md"]);
|
||||
|
||||
const crcTable = Uint32Array.from({ length: 256 }, (_, index) => {
|
||||
let value = index;
|
||||
for (let bit = 0; bit < 8; bit += 1) {
|
||||
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
||||
}
|
||||
return value >>> 0;
|
||||
});
|
||||
|
||||
function crc32(data) {
|
||||
let value = 0xffffffff;
|
||||
for (const byte of data) value = crcTable[(value ^ byte) & 0xff] ^ (value >>> 8);
|
||||
return (value ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(`UXP CCX build failed: ${message}`);
|
||||
}
|
||||
|
||||
function u16(value) {
|
||||
assert(value >= 0 && value <= 0xffff, "ZIP field exceeds 16-bit limit");
|
||||
return value;
|
||||
}
|
||||
|
||||
function u32(value) {
|
||||
assert(value >= 0 && value <= 0xffffffff, "ZIP field exceeds 32-bit limit");
|
||||
return value;
|
||||
}
|
||||
|
||||
async function collectFiles(directory, relative = "") {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
const files = [];
|
||||
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
||||
const source = path.join(directory, entry.name);
|
||||
const target = relative ? `${relative}/${entry.name}` : entry.name;
|
||||
const stat = await lstat(source);
|
||||
assert(!stat.isSymbolicLink(), `refusing to package symbolic link ${target}`);
|
||||
if (stat.isDirectory()) {
|
||||
files.push(...(await collectFiles(source, target)));
|
||||
continue;
|
||||
}
|
||||
assert(stat.isFile(), `refusing to package non-file entry ${target}`);
|
||||
if (documentationFiles.has(entry.name)) continue;
|
||||
files.push({ name: target, data: await readFile(source) });
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function buildStoredZip(files) {
|
||||
const localRecords = [];
|
||||
const centralRecords = [];
|
||||
let offset = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const name = Buffer.from(file.name, "utf8");
|
||||
const checksum = crc32(file.data);
|
||||
const local = Buffer.alloc(30);
|
||||
local.writeUInt32LE(0x04034b50, 0);
|
||||
local.writeUInt16LE(20, 4);
|
||||
local.writeUInt16LE(0x0800, 6);
|
||||
local.writeUInt16LE(0, 8);
|
||||
local.writeUInt16LE(0, 10);
|
||||
local.writeUInt16LE(0x0021, 12);
|
||||
local.writeUInt32LE(u32(checksum), 14);
|
||||
local.writeUInt32LE(u32(file.data.length), 18);
|
||||
local.writeUInt32LE(u32(file.data.length), 22);
|
||||
local.writeUInt16LE(u16(name.length), 26);
|
||||
local.writeUInt16LE(0, 28);
|
||||
|
||||
const central = Buffer.alloc(46);
|
||||
central.writeUInt32LE(0x02014b50, 0);
|
||||
central.writeUInt16LE(0x0314, 4);
|
||||
central.writeUInt16LE(20, 6);
|
||||
central.writeUInt16LE(0x0800, 8);
|
||||
central.writeUInt16LE(0, 10);
|
||||
central.writeUInt16LE(0, 12);
|
||||
central.writeUInt16LE(0x0021, 14);
|
||||
central.writeUInt32LE(u32(checksum), 16);
|
||||
central.writeUInt32LE(u32(file.data.length), 20);
|
||||
central.writeUInt32LE(u32(file.data.length), 24);
|
||||
central.writeUInt16LE(u16(name.length), 28);
|
||||
central.writeUInt16LE(0, 30);
|
||||
central.writeUInt16LE(0, 32);
|
||||
central.writeUInt16LE(0, 34);
|
||||
central.writeUInt16LE(0, 36);
|
||||
central.writeUInt32LE(0, 38);
|
||||
central.writeUInt32LE(u32(offset), 42);
|
||||
|
||||
localRecords.push(local, name, file.data);
|
||||
centralRecords.push(central, name);
|
||||
offset += local.length + name.length + file.data.length;
|
||||
}
|
||||
|
||||
const centralDirectory = Buffer.concat(centralRecords);
|
||||
const end = Buffer.alloc(22);
|
||||
end.writeUInt32LE(0x06054b50, 0);
|
||||
end.writeUInt16LE(0, 4);
|
||||
end.writeUInt16LE(0, 6);
|
||||
end.writeUInt16LE(u16(files.length), 8);
|
||||
end.writeUInt16LE(u16(files.length), 10);
|
||||
end.writeUInt32LE(u32(centralDirectory.length), 12);
|
||||
end.writeUInt32LE(u32(offset), 16);
|
||||
end.writeUInt16LE(0, 20);
|
||||
return Buffer.concat([...localRecords, centralDirectory, end]);
|
||||
}
|
||||
|
||||
function verifyStoredZip(archive, expectedNames) {
|
||||
const endOffset = archive.length - 22;
|
||||
assert(endOffset >= 0 && archive.readUInt32LE(endOffset) === 0x06054b50, "archive is missing a ZIP end record");
|
||||
assert(archive.readUInt16LE(endOffset + 20) === 0, "archive must not contain a ZIP comment");
|
||||
const entryCount = archive.readUInt16LE(endOffset + 10);
|
||||
const centralSize = archive.readUInt32LE(endOffset + 12);
|
||||
let cursor = archive.readUInt32LE(endOffset + 16);
|
||||
assert(cursor + centralSize === endOffset, "archive central directory has an unexpected length");
|
||||
assert(entryCount === expectedNames.length, "archive entry count does not match the staged plugin files");
|
||||
const names = [];
|
||||
|
||||
for (let index = 0; index < entryCount; index += 1) {
|
||||
assert(archive.readUInt32LE(cursor) === 0x02014b50, "archive central directory entry is invalid");
|
||||
assert(archive.readUInt16LE(cursor + 10) === 0, "archive must use stored ZIP entries");
|
||||
const checksum = archive.readUInt32LE(cursor + 16);
|
||||
const compressedSize = archive.readUInt32LE(cursor + 20);
|
||||
const uncompressedSize = archive.readUInt32LE(cursor + 24);
|
||||
const nameLength = archive.readUInt16LE(cursor + 28);
|
||||
const extraLength = archive.readUInt16LE(cursor + 30);
|
||||
const commentLength = archive.readUInt16LE(cursor + 32);
|
||||
const localOffset = archive.readUInt32LE(cursor + 42);
|
||||
const name = archive.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8");
|
||||
assert(!names.includes(name), `archive contains duplicate entry ${name}`);
|
||||
names.push(name);
|
||||
assert(archive.readUInt32LE(localOffset) === 0x04034b50, `archive local entry is invalid for ${name}`);
|
||||
const localNameLength = archive.readUInt16LE(localOffset + 26);
|
||||
const localExtraLength = archive.readUInt16LE(localOffset + 28);
|
||||
const localName = archive
|
||||
.subarray(localOffset + 30, localOffset + 30 + localNameLength)
|
||||
.toString("utf8");
|
||||
assert(localName === name, `archive local entry name does not match ${name}`);
|
||||
assert(compressedSize === uncompressedSize, `archive unexpectedly compresses ${name}`);
|
||||
const dataStart = localOffset + 30 + localNameLength + localExtraLength;
|
||||
const data = archive.subarray(dataStart, dataStart + uncompressedSize);
|
||||
assert(data.length === uncompressedSize, `archive data is truncated for ${name}`);
|
||||
assert(crc32(data) === checksum, `archive checksum is invalid for ${name}`);
|
||||
cursor += 46 + nameLength + extraLength + commentLength;
|
||||
}
|
||||
|
||||
assert(
|
||||
JSON.stringify(names.sort()) === JSON.stringify([...expectedNames].sort()),
|
||||
"archive contents do not match the staged plugin files",
|
||||
);
|
||||
}
|
||||
|
||||
function channelConfiguration(sourceManifest) {
|
||||
const channel = process.env.UXP_DISTRIBUTION_CHANNEL ?? "direct";
|
||||
assert(["direct", "marketplace"].includes(channel), "UXP_DISTRIBUTION_CHANNEL must be direct or marketplace");
|
||||
if (channel === "direct") {
|
||||
return { channel, pluginId: sourceManifest.id };
|
||||
}
|
||||
|
||||
const pluginId = process.env.UXP_MARKETPLACE_PLUGIN_ID?.trim();
|
||||
assert(
|
||||
pluginId,
|
||||
"marketplace builds require UXP_MARKETPLACE_PLUGIN_ID from Adobe Developer Distribution",
|
||||
);
|
||||
assert(pluginId !== sourceManifest.id, "Marketplace builds must use a distinct Adobe-assigned plugin id");
|
||||
return { channel, pluginId };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { manifest: sourceManifest, packageJson, pluginRoot } = await validateUxpSource();
|
||||
const { channel, pluginId } = channelConfiguration(sourceManifest);
|
||||
const manifest = { ...sourceManifest, id: pluginId };
|
||||
validateUxpManifest(manifest, packageJson, { expectedId: pluginId });
|
||||
|
||||
const files = await collectFiles(pluginRoot);
|
||||
const manifestIndex = files.findIndex((file) => file.name === "manifest.json");
|
||||
assert(manifestIndex >= 0, "UXP package is missing manifest.json");
|
||||
files[manifestIndex] = {
|
||||
name: "manifest.json",
|
||||
data: Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"),
|
||||
};
|
||||
files.sort((left, right) => left.name.localeCompare(right.name));
|
||||
|
||||
const archive = buildStoredZip(files);
|
||||
verifyStoredZip(
|
||||
archive,
|
||||
files.map((file) => file.name),
|
||||
);
|
||||
await mkdir(artifacts, { recursive: true });
|
||||
const output = path.join(
|
||||
artifacts,
|
||||
`premiere-pro-mcp-uxp-${packageJson.version}-${channel}.ccx`,
|
||||
);
|
||||
await writeFile(output, archive);
|
||||
console.log(`Built ${path.relative(root, output)}`);
|
||||
console.log(`SHA-256 ${createHash("sha256").update(archive).digest("hex")}`);
|
||||
console.log(`Channel ${channel}; package structure validated, not live Premiere installation validation.`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(scriptDir, "..");
|
||||
const quickstartDir = path.join(root, "docs", "quickstart");
|
||||
const localeManifest = JSON.parse(fs.readFileSync(path.join(quickstartDir, "locales.json"), "utf8"));
|
||||
|
||||
function sectionIds(file) {
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
const ids = [...source.matchAll(/<!-- quickstart:section=([a-z0-9-]+) -->/g)].map((match) => match[1]);
|
||||
if (ids.length === 0) throw new Error(`${path.basename(file)} has no quick-start section markers`);
|
||||
if (new Set(ids).size !== ids.length) throw new Error(`${path.basename(file)} has duplicate quick-start section markers`);
|
||||
return ids;
|
||||
}
|
||||
|
||||
if (localeManifest.schemaVersion !== "premiere-pro-mcp.quickstart-locales.v1") {
|
||||
throw new Error("Unsupported quick-start locale manifest schema");
|
||||
}
|
||||
const sourceIds = sectionIds(path.join(quickstartDir, localeManifest.source));
|
||||
for (const locale of localeManifest.locales ?? []) {
|
||||
if (!/^[a-z]{2,3}(?:-[A-Z]{2})?$/.test(locale.code ?? "")) throw new Error("Locale codes must be stable BCP 47-style identifiers");
|
||||
if (typeof locale.file !== "string" || !/^[a-z-]+\.md$/.test(locale.file)) throw new Error(`Locale ${locale.code} has an unsafe file name`);
|
||||
if (typeof locale.reviewStatus !== "string" || !locale.reviewStatus.includes("machine-assisted")) {
|
||||
throw new Error(`Locale ${locale.code} must disclose its machine-assisted review status`);
|
||||
}
|
||||
const translatedIds = sectionIds(path.join(quickstartDir, locale.file));
|
||||
if (JSON.stringify(translatedIds) !== JSON.stringify(sourceIds)) {
|
||||
throw new Error(`${locale.file} does not match the English source section structure`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Quick-start locales verified: ${localeManifest.locales.length} translations match ${localeManifest.source}`);
|
||||
@@ -0,0 +1,36 @@
|
||||
import { copyFile, mkdir } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDirectory = dirname(fileURLToPath(import.meta.url));
|
||||
const resources = [
|
||||
"adobe-uxp-coverage.json",
|
||||
"adobe-api-inventory.json",
|
||||
"adobe-beta-aaf-export-options-drift.json",
|
||||
"adobe-beta-project-options-drift.json",
|
||||
"adobe-beta-transition-options-drift.json",
|
||||
"adobe-beta-rectf-drift.json",
|
||||
"adobe-beta-color-drift.json",
|
||||
"adobe-beta-pointf-drift.json",
|
||||
"adobe-beta-guid-drift.json",
|
||||
"adobe-beta-frame-rate-drift.json",
|
||||
"adobe-beta-tick-time-drift.json",
|
||||
"adobe-beta-c2pa-drift.json",
|
||||
"adobe-beta-media-drift.json",
|
||||
"adobe-beta-media-manager-drift.json",
|
||||
"adobe-beta-transcript-drift.json",
|
||||
"adobe-beta-work-area-drift.json",
|
||||
"uxp-js-coverage.json",
|
||||
"uxp-js-api-inventory.json",
|
||||
"premiere-doc-inventory.json",
|
||||
"cep-reference-inventory.json",
|
||||
"extendscript-api-inventory.json",
|
||||
"premiere-surface-registry.json",
|
||||
];
|
||||
const targetDirectory = resolve(scriptDirectory, "../dist/resources");
|
||||
|
||||
await mkdir(targetDirectory, { recursive: true });
|
||||
await Promise.all(resources.map((resource) => copyFile(
|
||||
resolve(scriptDirectory, `../src/resources/${resource}`),
|
||||
resolve(targetDirectory, resource),
|
||||
)));
|
||||
@@ -0,0 +1,75 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(scriptDir, "..");
|
||||
const matrix = JSON.parse(fs.readFileSync(path.join(root, "docs", "licensed-host-sweep.matrix.json"), "utf8"));
|
||||
|
||||
const options = new Map();
|
||||
for (let index = 2; index < process.argv.length; index += 1) {
|
||||
const argument = process.argv[index];
|
||||
if (!argument.startsWith("--")) throw new Error(`Unexpected argument: ${argument}`);
|
||||
if (argument === "--help") {
|
||||
console.log(`Usage: node scripts/create-licensed-host-sweep.mjs --host-os <Windows|macOS> --premiere-version <version> --panel-build <hash> --fixture-revision <safe-id> --fixture-sha256 <sha256> [--source-commit <sha>] [--case <id>]... [--output <path>]\n\nThis creates a redacted, not-run report skeleton. It does not start Premiere, call MCP tools, or inspect a project.`);
|
||||
process.exit(0);
|
||||
}
|
||||
const value = process.argv[index + 1];
|
||||
if (!value || value.startsWith("--")) throw new Error(`Missing value for ${argument}`);
|
||||
index += 1;
|
||||
const values = options.get(argument) ?? [];
|
||||
values.push(value);
|
||||
options.set(argument, values);
|
||||
}
|
||||
|
||||
function one(name) {
|
||||
const values = options.get(name) ?? [];
|
||||
if (values.length !== 1) throw new Error(`${name} must be supplied exactly once`);
|
||||
return values[0];
|
||||
}
|
||||
|
||||
function matches(name, value, expression) {
|
||||
if (!expression.test(value)) throw new Error(`${name} has an unsafe or invalid format`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function currentCommit() {
|
||||
return execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
|
||||
}
|
||||
|
||||
const hostOs = one("--host-os");
|
||||
if (!new Set(["Windows", "macOS"]).has(hostOs)) throw new Error("--host-os must be Windows or macOS");
|
||||
const premiereVersion = matches("--premiere-version", one("--premiere-version"), /^[0-9][0-9A-Za-z._-]{0,63}$/);
|
||||
const panelBuild = matches("--panel-build", one("--panel-build"), /^[0-9a-f]{7,64}$/i);
|
||||
const fixtureRevision = matches("--fixture-revision", one("--fixture-revision"), /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/);
|
||||
const fixtureSha = matches("--fixture-sha256", one("--fixture-sha256"), /^[0-9a-f]{64}$/i);
|
||||
const sourceCommit = matches("--source-commit", (options.get("--source-commit") ?? [currentCommit()])[0], /^[0-9a-f]{40}$/i);
|
||||
const requestedCases = options.get("--case") ?? matrix.cases.map((entry) => entry.id);
|
||||
const casesById = new Map(matrix.cases.map((entry) => [entry.id, entry]));
|
||||
const unknown = requestedCases.filter((id) => !casesById.has(id));
|
||||
if (unknown.length > 0) throw new Error(`Unknown sweep case: ${unknown.join(", ")}`);
|
||||
|
||||
const report = {
|
||||
schemaVersion: "premiere-pro-mcp.licensed-host-sweep.v1",
|
||||
sourceCommit: sourceCommit.toLowerCase(),
|
||||
host: { os: hostOs, premiereVersion, panelBuild: panelBuild.toLowerCase() },
|
||||
fixture: { revision: fixtureRevision, sha256: fixtureSha.toLowerCase() },
|
||||
sweep: { matrixId: matrix.id, matrixVersion: "1" },
|
||||
cases: requestedCases.map((id) => ({
|
||||
id,
|
||||
operationClass: casesById.get(id).operationClass,
|
||||
status: "not_run",
|
||||
evidence: [],
|
||||
undoEvidence: false,
|
||||
})),
|
||||
};
|
||||
|
||||
const output = options.get("--output");
|
||||
if (output) {
|
||||
const outputPath = path.resolve(one("--output"));
|
||||
fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
console.log(JSON.stringify({ schemaVersion: report.schemaVersion, sourceCommit: report.sourceCommit, cases: report.cases.map((entry) => entry.id) }, null, 2));
|
||||
} else {
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const packagePath = resolve(root, "node_modules/@adobe/premierepro/package.json");
|
||||
const declarationsPath = process.env.PREMIERE_API_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_API_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const coveragePath = resolve(root, "src/resources/adobe-uxp-coverage.json");
|
||||
const outputPath = resolve(root, "src/resources/adobe-api-inventory.json");
|
||||
const check = process.argv.includes("--check");
|
||||
const validateOnly = process.argv.includes("--validate-only");
|
||||
|
||||
const [packageText, declarationsText, coverageText] = await Promise.all([
|
||||
readFile(packagePath, "utf8"),
|
||||
readFile(declarationsPath, "utf8"),
|
||||
readFile(coveragePath, "utf8"),
|
||||
]);
|
||||
const packageMetadata = JSON.parse(packageText);
|
||||
const coverage = JSON.parse(coverageText);
|
||||
const coveredApis = new Set(coverage.entries.flatMap((entry) => entry.adobeApi));
|
||||
const source = ts.createSourceFile(declarationsPath, declarationsText, ts.ScriptTarget.Latest, true);
|
||||
if (source.parseDiagnostics.length > 0) {
|
||||
const details = source.parseDiagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, " ")).join("; ");
|
||||
throw new Error(`TypeScript declaration parse failed: ${details}`);
|
||||
}
|
||||
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const normalizedDeclarations = declarationsText.replaceAll("\r\n", "\n");
|
||||
const declarationsSha256 = createHash("sha256").update(normalizedDeclarations).digest("hex");
|
||||
const canonicalOwners = new Map();
|
||||
const rootDeclaration = source.statements.find((statement) => (
|
||||
ts.isTypeAliasDeclaration(statement) && statement.name.text === "premierepro"
|
||||
));
|
||||
if (!rootDeclaration || !ts.isTypeLiteralNode(rootDeclaration.type)) {
|
||||
throw new Error("Adobe declarations must expose a premierepro root type literal.");
|
||||
}
|
||||
for (const member of rootDeclaration.type.members) {
|
||||
if (!ts.isPropertySignature(member) || !member.name || !member.type || !ts.isTypeReferenceNode(member.type)) continue;
|
||||
canonicalOwners.set(member.type.typeName.getText(source), member.name.getText(source).replaceAll('"', ""));
|
||||
}
|
||||
|
||||
function memberName(member) {
|
||||
if (ts.isConstructSignatureDeclaration(member)) return "[[construct]]";
|
||||
if (ts.isCallSignatureDeclaration(member)) return "[[call]]";
|
||||
if (ts.isIndexSignatureDeclaration(member)) return "[[index]]";
|
||||
if (!member.name) throw new Error(`Unsupported anonymous type member: ${ts.SyntaxKind[member.kind]}`);
|
||||
if (ts.isIdentifier(member.name) || ts.isStringLiteral(member.name) || ts.isNumericLiteral(member.name)) {
|
||||
return member.name.text;
|
||||
}
|
||||
return member.name.getText(source);
|
||||
}
|
||||
|
||||
function memberKind(member) {
|
||||
if (ts.isMethodSignature(member)) return "method";
|
||||
if (ts.isPropertySignature(member)) return "property";
|
||||
if (ts.isConstructSignatureDeclaration(member)) return "constructor";
|
||||
if (ts.isCallSignatureDeclaration(member)) return "call";
|
||||
if (ts.isIndexSignatureDeclaration(member)) return "index";
|
||||
throw new Error(`Unsupported type member: ${ts.SyntaxKind[member.kind]}`);
|
||||
}
|
||||
|
||||
const symbols = [];
|
||||
function addMember(owner, name, kind) {
|
||||
const canonicalOwner = canonicalOwners.get(owner) ?? owner;
|
||||
const symbol = `${canonicalOwner}.${name}`;
|
||||
const declarationSymbol = `${owner}.${name}`;
|
||||
symbols.push({ symbol, kind, ...(symbol === declarationSymbol ? {} : { declarationSymbol }) });
|
||||
}
|
||||
|
||||
function collectTypeMembers(owner, node) {
|
||||
if (ts.isTypeLiteralNode(node)) {
|
||||
for (const member of node.members) {
|
||||
addMember(owner, memberName(member), memberKind(member));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ts.isIntersectionTypeNode(node) || ts.isUnionTypeNode(node)) {
|
||||
for (const type of node.types) collectTypeMembers(owner, type);
|
||||
return;
|
||||
}
|
||||
if (ts.isParenthesizedTypeNode(node)) collectTypeMembers(owner, node.type);
|
||||
else throw new Error(`Unsupported type expression for ${owner}: ${ts.SyntaxKind[node.kind]}`);
|
||||
}
|
||||
|
||||
function collectModule(owner, moduleDeclaration) {
|
||||
symbols.push({ symbol: owner, kind: "namespace" });
|
||||
if (!moduleDeclaration.body) throw new Error(`Namespace ${owner} has no body.`);
|
||||
if (ts.isModuleDeclaration(moduleDeclaration.body)) {
|
||||
collectModule(`${owner}.${moduleDeclaration.body.name.getText(source).replaceAll('"', "")}`, moduleDeclaration.body);
|
||||
return;
|
||||
}
|
||||
if (!ts.isModuleBlock(moduleDeclaration.body)) {
|
||||
throw new Error(`Unsupported namespace body for ${owner}: ${ts.SyntaxKind[moduleDeclaration.body.kind]}`);
|
||||
}
|
||||
for (const child of moduleDeclaration.body.statements) {
|
||||
if (ts.isEnumDeclaration(child)) {
|
||||
const enumName = `${owner}.${child.name.text}`;
|
||||
symbols.push({ symbol: enumName, kind: "enum" });
|
||||
for (const member of child.members) {
|
||||
symbols.push({ symbol: `${enumName}.${member.name.getText(source).replaceAll('"', "")}`, kind: "enumMember" });
|
||||
}
|
||||
} else if (ts.isModuleDeclaration(child)) {
|
||||
collectModule(`${owner}.${child.name.getText(source).replaceAll('"', "")}`, child);
|
||||
} else {
|
||||
throw new Error(`Unsupported declaration in namespace ${owner}: ${ts.SyntaxKind[child.kind]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const statement of source.statements) {
|
||||
if (ts.isTypeAliasDeclaration(statement)) {
|
||||
const owner = statement.name.text;
|
||||
symbols.push({ symbol: owner, kind: "type" });
|
||||
collectTypeMembers(owner, statement.type);
|
||||
} else if (ts.isModuleDeclaration(statement)) {
|
||||
collectModule(statement.name.getText(source).replaceAll('"', ""), statement);
|
||||
} else if (ts.isExportAssignment(statement) && statement.expression.getText(source) === "premierepro") {
|
||||
// The package's `export = premierepro` binds the root declaration object.
|
||||
} else {
|
||||
throw new Error(`Unsupported top-level Adobe declaration: ${ts.SyntaxKind[statement.kind]}`);
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueSymbols = [...new Map(symbols.map((entry) => [entry.symbol, entry])).values()]
|
||||
.sort((left, right) => compareText(left.symbol, right.symbol));
|
||||
const entries = uniqueSymbols.map((entry) => ({
|
||||
...entry,
|
||||
coverage: coveredApis.has(entry.symbol) ? "mapped" : "unmapped",
|
||||
}));
|
||||
const mapped = entries.filter((entry) => entry.coverage === "mapped").length;
|
||||
const declaredSymbols = new Set(entries.map((entry) => entry.symbol));
|
||||
const manifestOnly = [...coveredApis].filter((symbol) => !declaredSymbols.has(symbol)).sort(compareText);
|
||||
const inventory = {
|
||||
schemaVersion: 1,
|
||||
source: {
|
||||
package: "@adobe/premierepro",
|
||||
version: packageMetadata.version,
|
||||
declarations: "node_modules/@adobe/premierepro/src/premierepro.d.ts",
|
||||
declarationsSha256,
|
||||
coverageManifest: "src/resources/adobe-uxp-coverage.json",
|
||||
},
|
||||
semantics: {
|
||||
mapped: "The exact declaration symbol is referenced by at least one coverage-manifest entry; this alone is not live-host verification.",
|
||||
unmapped: "No coverage-manifest entry references the exact declaration symbol; this is a review queue, not proof that a standalone MCP tool is appropriate.",
|
||||
},
|
||||
stats: {
|
||||
total: entries.length,
|
||||
mapped,
|
||||
unmapped: entries.length - mapped,
|
||||
manifestOnly: manifestOnly.length,
|
||||
},
|
||||
manifestOnly,
|
||||
entries,
|
||||
};
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
|
||||
if (validateOnly) {
|
||||
console.log(`Validated ${entries.length} Adobe API symbols from ${packageMetadata.version}.`);
|
||||
} else if (check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
if (current.replaceAll("\r\n", "\n") !== rendered) {
|
||||
console.error("Adobe API inventory is stale. Run npm run adobe:api-inventory.");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`Adobe API inventory is current: ${entries.length} symbols from ${packageMetadata.version}.`);
|
||||
}
|
||||
} else {
|
||||
await writeFile(outputPath, rendered);
|
||||
console.log(`Wrote ${entries.length} Adobe API symbols (${mapped} mapped, ${entries.length - mapped} unmapped).`);
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stablePackagePath = resolve(root, "node_modules/@adobe/premierepro/package.json");
|
||||
const betaPackagePath = resolve(root, "node_modules/@adobe/premierepro-beta/package.json");
|
||||
const stableDeclarationsPath = process.env.PREMIERE_BETA_AAF_EXPORT_OPTIONS_STABLE_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_AAF_EXPORT_OPTIONS_STABLE_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const betaDeclarationsPath = process.env.PREMIERE_BETA_AAF_EXPORT_OPTIONS_BETA_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_AAF_EXPORT_OPTIONS_BETA_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro-beta/src/premierepro.d.ts");
|
||||
const outputPath = process.env.PREMIERE_BETA_AAF_EXPORT_OPTIONS_DRIFT_OUTPUT_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_AAF_EXPORT_OPTIONS_DRIFT_OUTPUT_PATH)
|
||||
: resolve(root, "src/resources/adobe-beta-aaf-export-options-drift.json");
|
||||
const check = process.argv.includes("--check");
|
||||
const validateOnly = process.argv.includes("--validate-only");
|
||||
|
||||
const [stablePackageText, betaPackageText, stableDeclarations, betaDeclarations] = await Promise.all([
|
||||
readFile(stablePackagePath, "utf8"),
|
||||
readFile(betaPackagePath, "utf8"),
|
||||
readFile(stableDeclarationsPath, "utf8"),
|
||||
readFile(betaDeclarationsPath, "utf8"),
|
||||
]);
|
||||
|
||||
const stablePackage = JSON.parse(stablePackageText);
|
||||
const betaPackage = JSON.parse(betaPackageText);
|
||||
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const normalizedText = (text) => text.replaceAll("\r\n", "\n").replace(/\s+/g, " ").trim();
|
||||
|
||||
function sourceFile(declarations, declarationPath) {
|
||||
const source = ts.createSourceFile(declarationPath, declarations, ts.ScriptTarget.Latest, true);
|
||||
if (source.parseDiagnostics.length > 0) {
|
||||
const details = source.parseDiagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, " ")).join("; ");
|
||||
throw new Error(`Adobe AAFExportOptions declaration parse failed: ${details}`);
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
function typeLiteral(source, name) {
|
||||
const declaration = source.statements.find((statement) => (
|
||||
ts.isTypeAliasDeclaration(statement) && statement.name.text === name
|
||||
));
|
||||
if (!declaration || !ts.isTypeLiteralNode(declaration.type)) {
|
||||
throw new Error(`Adobe declarations must expose ${name} as a type literal.`);
|
||||
}
|
||||
return declaration;
|
||||
}
|
||||
|
||||
function propertyEntry(type, source, owner, name) {
|
||||
const member = type.members.find((candidate) => (
|
||||
ts.isPropertySignature(candidate) && candidate.name && ts.isIdentifier(candidate.name) && candidate.name.text === name
|
||||
));
|
||||
if (!member || !member.type) {
|
||||
throw new Error(`Adobe declarations must expose ${owner}.${name} as a typed property.`);
|
||||
}
|
||||
return {
|
||||
symbol: `${owner}.${name}`,
|
||||
kind: "property",
|
||||
signature: normalizedText(member.type.getText(source)),
|
||||
};
|
||||
}
|
||||
|
||||
function factoryEntries(type, source, owner) {
|
||||
const entries = type.members.filter((member) => (
|
||||
ts.isConstructSignatureDeclaration(member) || ts.isCallSignatureDeclaration(member)
|
||||
)).map((member) => {
|
||||
if (!member.type) throw new Error(`Adobe ${owner} factory signature is missing a return type.`);
|
||||
const signature = `(${member.parameters.map((parameter) => normalizedText(parameter.getText(source))).join(", ")}) => ${normalizedText(member.type.getText(source))}`;
|
||||
if (ts.isConstructSignatureDeclaration(member)) {
|
||||
return { symbol: `${owner}.new`, kind: "construct_signature", signature };
|
||||
}
|
||||
return { symbol: `${owner}.call`, kind: "call_signature", signature };
|
||||
}).sort((left, right) => compareText(left.symbol, right.symbol));
|
||||
if (entries.length !== 2 || entries[0].kind !== "call_signature" || entries[1].kind !== "construct_signature") {
|
||||
throw new Error(`Adobe ${owner} must expose exactly one call and one construct factory signature.`);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function hasFactorySignatures(type) {
|
||||
return type.members.some((member) => (
|
||||
ts.isConstructSignatureDeclaration(member) || ts.isCallSignatureDeclaration(member)
|
||||
));
|
||||
}
|
||||
|
||||
function nonFactoryMembers(type, source, owner) {
|
||||
const members = type.members.filter((member) => (
|
||||
!ts.isConstructSignatureDeclaration(member) && !ts.isCallSignatureDeclaration(member)
|
||||
)).map((member) => normalizedText(member.getText(source))).sort(compareText);
|
||||
if (members.length === 0) throw new Error(`Adobe ${owner} must retain non-factory option members.`);
|
||||
return members;
|
||||
}
|
||||
|
||||
function hasTypeAlias(source, name) {
|
||||
return source.statements.some((statement) => ts.isTypeAliasDeclaration(statement) && statement.name.text === name);
|
||||
}
|
||||
|
||||
function declarationHash(declaration, source) {
|
||||
return createHash("sha256").update(normalizedText(declaration.getText(source))).digest("hex");
|
||||
}
|
||||
|
||||
const stableSource = sourceFile(stableDeclarations, stableDeclarationsPath);
|
||||
const betaSource = sourceFile(betaDeclarations, betaDeclarationsPath);
|
||||
const stableRoot = typeLiteral(stableSource, "premierepro");
|
||||
const betaRoot = typeLiteral(betaSource, "premierepro");
|
||||
const stableOptions = typeLiteral(stableSource, "AAFExportOptions");
|
||||
const betaOptions = typeLiteral(betaSource, "AAFExportOptions");
|
||||
const stableBinding = propertyEntry(stableRoot.type, stableSource, "premierepro", "AAFExportOptions");
|
||||
const betaBinding = propertyEntry(betaRoot.type, betaSource, "premierepro", "AAFExportOptions");
|
||||
if (stableBinding.signature !== "AAFExportOptions") {
|
||||
throw new Error("Pinned stable declarations must expose premierepro.AAFExportOptions as AAFExportOptions.");
|
||||
}
|
||||
if (betaBinding.signature !== "AAFExportOptionsStatic") {
|
||||
throw new Error("Adobe beta declarations must expose premierepro.AAFExportOptions as AAFExportOptionsStatic.");
|
||||
}
|
||||
if (hasTypeAlias(stableSource, "AAFExportOptionsStatic")) {
|
||||
throw new Error("Pinned stable declarations unexpectedly expose AAFExportOptionsStatic.");
|
||||
}
|
||||
const betaStatic = typeLiteral(betaSource, "AAFExportOptionsStatic");
|
||||
const stableFactories = factoryEntries(stableOptions.type, stableSource, "AAFExportOptions");
|
||||
const betaFactories = factoryEntries(betaStatic.type, betaSource, "AAFExportOptionsStatic");
|
||||
const stableFactoryShapes = stableFactories.map(({ kind, signature }) => ({ kind, signature }));
|
||||
const betaFactoryShapes = betaFactories.map(({ kind, signature }) => ({ kind, signature }));
|
||||
if (JSON.stringify(stableFactoryShapes) !== JSON.stringify(betaFactoryShapes)) {
|
||||
throw new Error("Adobe beta AAFExportOptionsStatic factory signatures must match stable AAFExportOptions factory signatures.");
|
||||
}
|
||||
if (JSON.stringify(nonFactoryMembers(stableOptions.type, stableSource, "AAFExportOptions")) !==
|
||||
JSON.stringify(nonFactoryMembers(betaOptions.type, betaSource, "AAFExportOptions"))) {
|
||||
throw new Error("Adobe beta AAFExportOptions non-factory option members must match the pinned stable declaration.");
|
||||
}
|
||||
if (hasFactorySignatures(betaOptions.type)) {
|
||||
throw new Error("Adobe beta AAFExportOptions must not retain factory signatures after the static-type migration.");
|
||||
}
|
||||
|
||||
const betaOnly = [
|
||||
{
|
||||
symbol: "AAFExportOptionsStatic",
|
||||
kind: "type",
|
||||
signature: normalizedText(betaStatic.type.getText(betaSource)),
|
||||
},
|
||||
...betaFactories,
|
||||
].sort((left, right) => compareText(left.symbol, right.symbol));
|
||||
|
||||
const inventory = {
|
||||
schemaVersion: 1,
|
||||
scope: {
|
||||
declarations: ["premierepro.AAFExportOptions", "AAFExportOptions", "AAFExportOptionsStatic"],
|
||||
semantics: "This records the beta AAFExportOptions factory-type migration against the pinned stable package. It is static declaration-drift accounting, not beta API support or a complete package diff.",
|
||||
mutationBoundary: "AAFExportOptions configures AAF export behavior. This receipt intentionally does not construct options, create an export action, or expose an MCP AAF-export operation.",
|
||||
doesNotEstablish: "It does not prove a beta host exposes the static factory, that an AAF export can be configured or completed, that export paths or effect settings are accepted, or that any MCP action is supported or licensed-host validated.",
|
||||
},
|
||||
sources: {
|
||||
stable: {
|
||||
package: "@adobe/premierepro",
|
||||
version: stablePackage.version,
|
||||
declarations: relative(root, stableDeclarationsPath).replaceAll("\\", "/"),
|
||||
rootDeclarationSha256: declarationHash(stableRoot, stableSource),
|
||||
optionsDeclarationSha256: declarationHash(stableOptions, stableSource),
|
||||
staticFactoryPresent: false,
|
||||
},
|
||||
beta: {
|
||||
package: "@adobe/premierepro-beta",
|
||||
version: betaPackage.version,
|
||||
declarations: relative(root, betaDeclarationsPath).replaceAll("\\", "/"),
|
||||
rootDeclarationSha256: declarationHash(betaRoot, betaSource),
|
||||
optionsDeclarationSha256: declarationHash(betaOptions, betaSource),
|
||||
staticDeclarationSha256: declarationHash(betaStatic, betaSource),
|
||||
staticFactoryPresent: true,
|
||||
},
|
||||
},
|
||||
diff: {
|
||||
betaOnly,
|
||||
stableOnly: [],
|
||||
changed: [
|
||||
{
|
||||
symbol: "premierepro.AAFExportOptions",
|
||||
stable: stableBinding,
|
||||
beta: betaBinding,
|
||||
},
|
||||
{
|
||||
symbol: "AAFExportOptions.factorySignatures",
|
||||
stable: { owner: "AAFExportOptions", entries: stableFactoryShapes },
|
||||
beta: { owner: "AAFExportOptionsStatic", entries: betaFactoryShapes },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
|
||||
if (validateOnly) {
|
||||
console.log(`Validated Adobe beta AAFExportOptions declaration drift: ${betaOnly.length} beta-only and 2 changed symbols.`);
|
||||
} else if (check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
if (current.replaceAll("\r\n", "\n") !== rendered) {
|
||||
console.error("Adobe beta AAFExportOptions declaration drift inventory is stale. Run npm run adobe:beta-aaf-export-options-drift.");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`Adobe beta AAFExportOptions declaration drift inventory is current: ${betaOnly.length} beta-only and 2 changed symbols.`);
|
||||
}
|
||||
} else {
|
||||
await writeFile(outputPath, rendered);
|
||||
console.log(`Wrote Adobe beta AAFExportOptions declaration drift inventory: ${betaOnly.length} beta-only and 2 changed symbols.`);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stablePackagePath = resolve(root, "node_modules/@adobe/premierepro/package.json");
|
||||
const betaPackagePath = resolve(root, "node_modules/@adobe/premierepro-beta/package.json");
|
||||
const stableDeclarationsPath = process.env.PREMIERE_BETA_C2PA_STABLE_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_C2PA_STABLE_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const betaDeclarationsPath = process.env.PREMIERE_BETA_C2PA_BETA_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_C2PA_BETA_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro-beta/src/premierepro.d.ts");
|
||||
const outputPath = process.env.PREMIERE_BETA_C2PA_DRIFT_OUTPUT_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_C2PA_DRIFT_OUTPUT_PATH)
|
||||
: resolve(root, "src/resources/adobe-beta-c2pa-drift.json");
|
||||
const check = process.argv.includes("--check");
|
||||
const validateOnly = process.argv.includes("--validate-only");
|
||||
|
||||
const [stablePackageText, betaPackageText, stableDeclarations, betaDeclarations] = await Promise.all([
|
||||
readFile(stablePackagePath, "utf8"),
|
||||
readFile(betaPackagePath, "utf8"),
|
||||
readFile(stableDeclarationsPath, "utf8"),
|
||||
readFile(betaDeclarationsPath, "utf8"),
|
||||
]);
|
||||
|
||||
const stablePackage = JSON.parse(stablePackageText);
|
||||
const betaPackage = JSON.parse(betaPackageText);
|
||||
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const normalizedText = (text) => text.replaceAll("\r\n", "\n").replace(/\s+/g, " ").trim();
|
||||
|
||||
function sourceFile(declarations, declarationPath) {
|
||||
const source = ts.createSourceFile(declarationPath, declarations, ts.ScriptTarget.Latest, true);
|
||||
if (source.parseDiagnostics.length > 0) {
|
||||
const details = source.parseDiagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, " ")).join("; ");
|
||||
throw new Error(`Adobe C2PA declaration parse failed: ${details}`);
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
function nameOf(member, source, owner) {
|
||||
if (!member.name || !(
|
||||
ts.isIdentifier(member.name) || ts.isStringLiteral(member.name) || ts.isNumericLiteral(member.name)
|
||||
)) {
|
||||
throw new Error(`Adobe ${owner} declaration has an unsupported member name in ${source.fileName}`);
|
||||
}
|
||||
return member.name.text;
|
||||
}
|
||||
|
||||
function typeLiteral(source, name) {
|
||||
const declaration = source.statements.find((statement) => (
|
||||
ts.isTypeAliasDeclaration(statement) && statement.name.text === name
|
||||
));
|
||||
if (!declaration || !ts.isTypeLiteralNode(declaration.type)) {
|
||||
throw new Error(`Adobe declarations must expose ${name} as a type literal.`);
|
||||
}
|
||||
return declaration;
|
||||
}
|
||||
|
||||
function memberEntries(type, source, owner) {
|
||||
const entries = type.members.map((member) => {
|
||||
const name = nameOf(member, source, owner);
|
||||
if (ts.isMethodSignature(member)) {
|
||||
if (!member.type) throw new Error(`Adobe ${owner}.${name} is missing a return type.`);
|
||||
return {
|
||||
symbol: `${owner}.${name}`,
|
||||
kind: "method",
|
||||
signature: `(${member.parameters.map((parameter) => normalizedText(parameter.getText(source))).join(", ")}) => ${normalizedText(member.type.getText(source))}`,
|
||||
};
|
||||
}
|
||||
if (ts.isPropertySignature(member)) {
|
||||
if (!member.type) throw new Error(`Adobe ${owner}.${name} is missing a property type.`);
|
||||
const readonly = Boolean(ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Readonly);
|
||||
return {
|
||||
symbol: `${owner}.${name}`,
|
||||
kind: "property",
|
||||
...(readonly ? { readonly: true } : {}),
|
||||
signature: normalizedText(member.type.getText(source)),
|
||||
};
|
||||
}
|
||||
throw new Error(`Adobe ${owner}.${name} has an unsupported declaration kind.`);
|
||||
}).sort((left, right) => compareText(left.symbol, right.symbol));
|
||||
if (new Set(entries.map((entry) => entry.symbol)).size !== entries.length) {
|
||||
throw new Error(`Adobe ${owner} declarations must not contain duplicate symbols.`);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function constantsNamespace(source) {
|
||||
const declaration = source.statements.find((statement) => (
|
||||
ts.isModuleDeclaration(statement) && ts.isIdentifier(statement.name) && statement.name.text === "Constants"
|
||||
));
|
||||
if (!declaration || !declaration.body || !ts.isModuleBlock(declaration.body)) {
|
||||
throw new Error("Adobe declarations must expose Constants as a module block.");
|
||||
}
|
||||
return declaration.body;
|
||||
}
|
||||
|
||||
function enumEntries(source, name) {
|
||||
const declaration = constantsNamespace(source).statements.find((statement) => (
|
||||
ts.isEnumDeclaration(statement) && statement.name.text === name
|
||||
));
|
||||
if (!declaration) throw new Error(`Adobe declarations must expose Constants.${name} as an enum.`);
|
||||
const entries = declaration.members.map((member, ordinal) => {
|
||||
if (!ts.isIdentifier(member.name) && !ts.isStringLiteral(member.name)) {
|
||||
throw new Error(`Adobe Constants.${name} has an unsupported enum member name.`);
|
||||
}
|
||||
if (member.initializer) {
|
||||
throw new Error(`Adobe Constants.${name}.${member.name.text} must retain an implicit enum initializer.`);
|
||||
}
|
||||
return {
|
||||
symbol: `Constants.${name}.${member.name.text}`,
|
||||
kind: "enum_member",
|
||||
declarationOrder: ordinal,
|
||||
initializer: "implicit",
|
||||
};
|
||||
});
|
||||
if (new Set(entries.map((entry) => entry.symbol)).size !== entries.length) {
|
||||
throw new Error(`Adobe Constants.${name} declarations must not contain duplicate enum members.`);
|
||||
}
|
||||
return { declaration, entries };
|
||||
}
|
||||
|
||||
function hasTypeAlias(source, name) {
|
||||
return source.statements.some((statement) => ts.isTypeAliasDeclaration(statement) && statement.name.text === name);
|
||||
}
|
||||
|
||||
function hasConstantsEnum(source, name) {
|
||||
const constants = source.statements.find((statement) => (
|
||||
ts.isModuleDeclaration(statement) && ts.isIdentifier(statement.name) && statement.name.text === "Constants"
|
||||
));
|
||||
return Boolean(constants?.body && ts.isModuleBlock(constants.body) && constants.body.statements.some((statement) => (
|
||||
ts.isEnumDeclaration(statement) && statement.name.text === name
|
||||
)));
|
||||
}
|
||||
|
||||
function declarationHash(declaration, source) {
|
||||
return createHash("sha256").update(normalizedText(declaration.getText(source))).digest("hex");
|
||||
}
|
||||
|
||||
const stableSource = sourceFile(stableDeclarations, stableDeclarationsPath);
|
||||
const betaSource = sourceFile(betaDeclarations, betaDeclarationsPath);
|
||||
const stableRoot = typeLiteral(stableSource, "premierepro");
|
||||
const stableRootEntries = memberEntries(stableRoot.type, stableSource, "premierepro");
|
||||
if (stableRootEntries.some((entry) => entry.symbol === "premierepro.C2PAService") ||
|
||||
hasTypeAlias(stableSource, "C2PAServiceStatic") ||
|
||||
hasTypeAlias(stableSource, "C2PAService") ||
|
||||
hasConstantsEnum(stableSource, "C2PAManifestLocation")) {
|
||||
throw new Error("Pinned stable declarations unexpectedly expose a C2PA surface.");
|
||||
}
|
||||
|
||||
const betaRoot = typeLiteral(betaSource, "premierepro");
|
||||
const rootBinding = memberEntries(betaRoot.type, betaSource, "premierepro")
|
||||
.find((entry) => entry.symbol === "premierepro.C2PAService");
|
||||
if (!rootBinding || rootBinding.kind !== "property" || rootBinding.signature !== "C2PAServiceStatic") {
|
||||
throw new Error("Adobe beta declarations must expose premierepro.C2PAService as C2PAServiceStatic.");
|
||||
}
|
||||
const serviceStatic = typeLiteral(betaSource, "C2PAServiceStatic");
|
||||
const serviceStaticEntries = memberEntries(serviceStatic.type, betaSource, "C2PAServiceStatic");
|
||||
const serviceInstance = typeLiteral(betaSource, "C2PAService");
|
||||
if (serviceInstance.type.members.length !== 0) {
|
||||
throw new Error("Adobe beta C2PAService instance declaration must remain empty for this receipt.");
|
||||
}
|
||||
const manifestLocations = enumEntries(betaSource, "C2PAManifestLocation");
|
||||
const betaOnly = [
|
||||
rootBinding,
|
||||
{
|
||||
symbol: "C2PAService",
|
||||
kind: "type",
|
||||
signature: normalizedText(serviceInstance.type.getText(betaSource)),
|
||||
},
|
||||
...serviceStaticEntries,
|
||||
...manifestLocations.entries,
|
||||
].sort((left, right) => compareText(left.symbol, right.symbol));
|
||||
|
||||
const inventory = {
|
||||
schemaVersion: 1,
|
||||
scope: {
|
||||
declarations: [
|
||||
"premierepro.C2PAService",
|
||||
"C2PAServiceStatic",
|
||||
"C2PAService",
|
||||
"Constants.C2PAManifestLocation",
|
||||
],
|
||||
semantics: "This records the C2PA declaration surface that is absent from the pinned stable package and present in the pinned beta package. It is static declaration-drift accounting, not beta API support or a complete package diff.",
|
||||
enumValueBoundary: "C2PAManifestLocation members have implicit enum initializers. declarationOrder records source order only; this receipt does not establish runtime numeric flag values or manifest-location semantics.",
|
||||
doesNotEstablish: "It does not prove a beta host exposes C2PAService, that a stable host accepts a beta call, that a manifest can be read or validated, or that an MCP action is supported or licensed-host validated.",
|
||||
},
|
||||
sources: {
|
||||
stable: {
|
||||
package: "@adobe/premierepro",
|
||||
version: stablePackage.version,
|
||||
declarations: relative(root, stableDeclarationsPath).replaceAll("\\", "/"),
|
||||
c2paSurfacePresent: false,
|
||||
},
|
||||
beta: {
|
||||
package: "@adobe/premierepro-beta",
|
||||
version: betaPackage.version,
|
||||
declarations: relative(root, betaDeclarationsPath).replaceAll("\\", "/"),
|
||||
c2paSurfacePresent: true,
|
||||
rootDeclarationSha256: declarationHash(betaRoot, betaSource),
|
||||
serviceStaticDeclarationSha256: declarationHash(serviceStatic, betaSource),
|
||||
serviceDeclarationSha256: declarationHash(serviceInstance, betaSource),
|
||||
manifestLocationDeclarationSha256: declarationHash(manifestLocations.declaration, betaSource),
|
||||
},
|
||||
},
|
||||
diff: {
|
||||
betaOnly,
|
||||
stableOnly: [],
|
||||
changed: [],
|
||||
},
|
||||
};
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
|
||||
if (validateOnly) {
|
||||
console.log(`Validated Adobe beta C2PA declaration drift: ${betaOnly.length} beta-only symbols.`);
|
||||
} else if (check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
if (current.replaceAll("\r\n", "\n") !== rendered) {
|
||||
console.error("Adobe beta C2PA declaration drift inventory is stale. Run npm run adobe:beta-c2pa-drift.");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`Adobe beta C2PA declaration drift inventory is current: ${betaOnly.length} beta-only symbols.`);
|
||||
}
|
||||
} else {
|
||||
await writeFile(outputPath, rendered);
|
||||
console.log(`Wrote Adobe beta C2PA declaration drift inventory: ${betaOnly.length} beta-only symbols.`);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stablePath = process.env.PREMIERE_BETA_COLOR_STABLE_DECLARATIONS_PATH ? resolve(process.env.PREMIERE_BETA_COLOR_STABLE_DECLARATIONS_PATH) : resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const betaPath = process.env.PREMIERE_BETA_COLOR_BETA_DECLARATIONS_PATH ? resolve(process.env.PREMIERE_BETA_COLOR_BETA_DECLARATIONS_PATH) : resolve(root, "node_modules/@adobe/premierepro-beta/src/premierepro.d.ts");
|
||||
const outputPath = resolve(root, "src/resources/adobe-beta-color-drift.json");
|
||||
const normalize = (text) => text.replaceAll("\r\n", "\n").replace(/\s+/g, " ").trim();
|
||||
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const parse = (text, path) => { const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true); if (source.parseDiagnostics.length) throw new Error(`Adobe Color declaration parse failed: ${source.parseDiagnostics.map((item) => ts.flattenDiagnosticMessageText(item.messageText, " ")).join("; ")}`); return source; };
|
||||
const literal = (source, name) => { const declaration = source.statements.find((item) => ts.isTypeAliasDeclaration(item) && item.name.text === name); if (!declaration || !ts.isTypeLiteralNode(declaration.type)) throw new Error(`Adobe declarations must expose ${name} as a type literal.`); return declaration; };
|
||||
const binding = (rootDeclaration, source) => { const member = rootDeclaration.type.members.find((item) => ts.isPropertySignature(item) && item.name && ts.isIdentifier(item.name) && item.name.text === "Color"); if (!member?.type) throw new Error("Adobe declarations must expose premierepro.Color as a typed property."); return { symbol: "premierepro.Color", kind: "property", signature: normalize(member.type.getText(source)) }; };
|
||||
const factories = (declaration, source, owner) => declaration.type.members.filter((item) => ts.isConstructSignatureDeclaration(item) || ts.isCallSignatureDeclaration(item)).map((item) => { if (!item.type) throw new Error(`Adobe ${owner} factory signature is missing a return type.`); return { symbol: `${owner}.${ts.isConstructSignatureDeclaration(item) ? "new" : "call"}`, kind: ts.isConstructSignatureDeclaration(item) ? "construct_signature" : "call_signature", signature: `(${item.parameters.map((parameter) => normalize(parameter.getText(source))).join(", ")}) => ${normalize(item.type.getText(source))}` }; }).sort((left, right) => compare(left.symbol, right.symbol));
|
||||
const nonFactories = (declaration, source) => declaration.type.members.filter((item) => !ts.isConstructSignatureDeclaration(item) && !ts.isCallSignatureDeclaration(item)).map((item) => normalize(item.getText(source))).sort(compare);
|
||||
const [stablePackageText, betaPackageText, stableText, betaText] = await Promise.all([readFile(resolve(root, "node_modules/@adobe/premierepro/package.json"), "utf8"), readFile(resolve(root, "node_modules/@adobe/premierepro-beta/package.json"), "utf8"), readFile(stablePath, "utf8"), readFile(betaPath, "utf8")]);
|
||||
const stable = parse(stableText, stablePath), beta = parse(betaText, betaPath), stableRoot = literal(stable, "premierepro"), betaRoot = literal(beta, "premierepro"), stableColor = literal(stable, "Color"), betaColor = literal(beta, "Color"), betaStatic = literal(beta, "ColorStatic"), stableBinding = binding(stableRoot, stable), betaBinding = binding(betaRoot, beta), stableFactories = factories(stableColor, stable, "Color"), betaFactories = factories(betaStatic, beta, "ColorStatic");
|
||||
if (stable.statements.some((item) => ts.isTypeAliasDeclaration(item) && item.name.text === "ColorStatic")) throw new Error("Pinned stable declarations unexpectedly expose ColorStatic.");
|
||||
if (stableBinding.signature !== "Color" || betaBinding.signature !== "ColorStatic") throw new Error("Adobe beta declarations must move premierepro.Color to ColorStatic.");
|
||||
const stableShapes = stableFactories.map(({ kind, signature }) => ({ kind, signature })), betaShapes = betaFactories.map(({ kind, signature }) => ({ kind, signature }));
|
||||
if (stableFactories.length !== 2 || betaFactories.length !== 2 || JSON.stringify(stableShapes) !== JSON.stringify(betaShapes)) throw new Error("Adobe beta ColorStatic factory signatures must match stable Color.");
|
||||
if (JSON.stringify(nonFactories(stableColor, stable)) !== JSON.stringify(nonFactories(betaColor, beta))) throw new Error("Adobe beta Color non-factory members must match stable Color.");
|
||||
if (factories(betaColor, beta, "Color").length) throw new Error("Adobe beta Color must not retain factory signatures after the static-type migration.");
|
||||
const hash = (declaration, source) => createHash("sha256").update(normalize(declaration.getText(source))).digest("hex");
|
||||
const inventory = { schemaVersion: 1, scope: { declarations: ["premierepro.Color", "Color", "ColorStatic"], semantics: "This records the beta Color factory-type migration against the pinned stable package. It is static declaration-drift accounting, not beta API support or a complete package diff.", doesNotEstablish: "It does not prove a beta host exposes ColorStatic, that a Color can be constructed or accepted by another API, that RGBA behavior is preserved, or that any MCP action is supported or licensed-host validated." }, sources: { stable: { package: "@adobe/premierepro", version: JSON.parse(stablePackageText).version, declarations: relative(root, stablePath).replaceAll("\\", "/"), colorDeclarationSha256: hash(stableColor, stable), staticFactoryPresent: false }, beta: { package: "@adobe/premierepro-beta", version: JSON.parse(betaPackageText).version, declarations: relative(root, betaPath).replaceAll("\\", "/"), colorDeclarationSha256: hash(betaColor, beta), staticDeclarationSha256: hash(betaStatic, beta), staticFactoryPresent: true } }, diff: { betaOnly: [{ symbol: "ColorStatic", kind: "type", signature: normalize(betaStatic.type.getText(beta)) }, ...betaFactories].sort((left, right) => compare(left.symbol, right.symbol)), stableOnly: [], changed: [{ symbol: "premierepro.Color", stable: stableBinding, beta: betaBinding }, { symbol: "Color.factorySignatures", stable: { owner: "Color", entries: stableShapes }, beta: { owner: "ColorStatic", entries: betaShapes } }] } };
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
if (process.argv.includes("--validate-only")) console.log("Validated Adobe beta Color declaration drift.");
|
||||
else if (process.argv.includes("--check")) { let current = ""; try { current = await readFile(outputPath, "utf8"); } catch {} if (current.replaceAll("\r\n", "\n") !== rendered) { console.error("Adobe beta Color declaration drift inventory is stale. Run npm run adobe:beta-color-drift."); process.exitCode = 1; } else console.log("Adobe beta Color declaration drift inventory is current: 3 beta-only and 2 changed symbols."); }
|
||||
else { await writeFile(outputPath, rendered); console.log("Wrote Adobe beta Color declaration drift inventory: 3 beta-only and 2 changed symbols."); }
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stablePath = process.env.PREMIERE_BETA_FRAME_RATE_STABLE_DECLARATIONS_PATH ? resolve(process.env.PREMIERE_BETA_FRAME_RATE_STABLE_DECLARATIONS_PATH) : resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const betaPath = process.env.PREMIERE_BETA_FRAME_RATE_BETA_DECLARATIONS_PATH ? resolve(process.env.PREMIERE_BETA_FRAME_RATE_BETA_DECLARATIONS_PATH) : resolve(root, "node_modules/@adobe/premierepro-beta/src/premierepro.d.ts");
|
||||
const outputPath = resolve(root, "src/resources/adobe-beta-frame-rate-drift.json");
|
||||
const normalize = (text) => text.replaceAll("\r\n", "\n").replace(/\s+/g, " ").trim();
|
||||
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const parse = (text, path) => { const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true); if (source.parseDiagnostics.length) throw new Error(`Adobe FrameRate declaration parse failed: ${source.parseDiagnostics.map((item) => ts.flattenDiagnosticMessageText(item.messageText, " ")).join("; ")}`); return source; };
|
||||
const literal = (source, name) => { const declaration = source.statements.find((item) => ts.isTypeAliasDeclaration(item) && item.name.text === name); if (!declaration || !ts.isTypeLiteralNode(declaration.type)) throw new Error(`Adobe declarations must expose ${name} as a type literal.`); return declaration; };
|
||||
const binding = (rootDeclaration, source) => { const member = rootDeclaration.type.members.find((item) => ts.isPropertySignature(item) && item.name && ts.isIdentifier(item.name) && item.name.text === "FrameRate"); if (!member?.type) throw new Error("Adobe declarations must expose premierepro.FrameRate as a typed property."); return { symbol: "premierepro.FrameRate", kind: "property", signature: normalize(member.type.getText(source)) }; };
|
||||
const factories = (declaration, source, owner) => declaration.type.members.filter((item) => ts.isConstructSignatureDeclaration(item) || ts.isCallSignatureDeclaration(item)).map((item) => { if (!item.type) throw new Error(`Adobe ${owner} factory signature is missing a return type.`); return { symbol: `${owner}.${ts.isConstructSignatureDeclaration(item) ? "new" : "call"}`, kind: ts.isConstructSignatureDeclaration(item) ? "construct_signature" : "call_signature", signature: `(${item.parameters.map((parameter) => normalize(parameter.getText(source))).join(", ")}) => ${normalize(item.type.getText(source))}` }; }).sort((left, right) => compare(left.symbol, right.symbol));
|
||||
const nonFactories = (declaration, source) => declaration.type.members.filter((item) => !ts.isConstructSignatureDeclaration(item) && !ts.isCallSignatureDeclaration(item)).map((item) => normalize(item.getText(source))).sort(compare);
|
||||
const [stablePackageText, betaPackageText, stableText, betaText] = await Promise.all([readFile(resolve(root, "node_modules/@adobe/premierepro/package.json"), "utf8"), readFile(resolve(root, "node_modules/@adobe/premierepro-beta/package.json"), "utf8"), readFile(stablePath, "utf8"), readFile(betaPath, "utf8")]);
|
||||
const stable = parse(stableText, stablePath), beta = parse(betaText, betaPath), stableRoot = literal(stable, "premierepro"), betaRoot = literal(beta, "premierepro"), stableFrameRate = literal(stable, "FrameRate"), betaFrameRate = literal(beta, "FrameRate"), stableStatic = literal(stable, "FrameRateStatic"), betaStatic = literal(beta, "FrameRateStatic"), stableBinding = binding(stableRoot, stable), betaBinding = binding(betaRoot, beta), stableFactories = factories(stableFrameRate, stable, "FrameRate"), betaFactories = factories(betaStatic, beta, "FrameRateStatic");
|
||||
if (stableBinding.signature !== "FrameRateStatic" || betaBinding.signature !== "FrameRateStatic") throw new Error("Adobe beta declarations must retain premierepro.FrameRate as FrameRateStatic.");
|
||||
if (factories(stableStatic, stable, "FrameRateStatic").length || factories(betaFrameRate, beta, "FrameRate").length) throw new Error("Adobe beta FrameRate factory signatures must move only from stable FrameRate to beta FrameRateStatic.");
|
||||
const stableShapes = stableFactories.map(({ kind, signature }) => ({ kind, signature })), betaShapes = betaFactories.map(({ kind, signature }) => ({ kind, signature }));
|
||||
if (stableFactories.length !== 2 || betaFactories.length !== 2 || JSON.stringify(stableShapes) !== JSON.stringify(betaShapes)) throw new Error("Adobe beta FrameRateStatic factory signatures must match stable FrameRate.");
|
||||
if (JSON.stringify(nonFactories(stableFrameRate, stable)) !== JSON.stringify(nonFactories(betaFrameRate, beta))) throw new Error("Adobe beta FrameRate non-factory members must match stable FrameRate.");
|
||||
if (JSON.stringify(nonFactories(stableStatic, stable)) !== JSON.stringify(nonFactories(betaStatic, beta))) throw new Error("Adobe beta FrameRateStatic non-factory members must match stable FrameRateStatic.");
|
||||
const hash = (declaration, source) => createHash("sha256").update(normalize(declaration.getText(source))).digest("hex");
|
||||
const inventory = { schemaVersion: 1, scope: { declarations: ["premierepro.FrameRate", "FrameRate", "FrameRateStatic"], semantics: "This records the beta FrameRate factory-placement migration against the pinned stable package. It is static declaration-drift accounting, not beta API support or a complete package diff.", doesNotEstablish: "It does not prove a beta host exposes FrameRateStatic factory calls, that a FrameRate can be constructed or accepted by another API, that frame alignment or time conversion behavior is preserved, or that any MCP action is supported or licensed-host validated." }, sources: { stable: { package: "@adobe/premierepro", version: JSON.parse(stablePackageText).version, declarations: relative(root, stablePath).replaceAll("\\", "/"), frameRateDeclarationSha256: hash(stableFrameRate, stable), frameRateStaticDeclarationSha256: hash(stableStatic, stable), rootBinding: stableBinding.signature }, beta: { package: "@adobe/premierepro-beta", version: JSON.parse(betaPackageText).version, declarations: relative(root, betaPath).replaceAll("\\", "/"), frameRateDeclarationSha256: hash(betaFrameRate, beta), frameRateStaticDeclarationSha256: hash(betaStatic, beta), rootBinding: betaBinding.signature } }, diff: { betaOnly: betaFactories, stableOnly: stableFactories, changed: [{ symbol: "FrameRate.factorySignaturePlacement", stable: { owner: "FrameRate", entries: stableShapes }, beta: { owner: "FrameRateStatic", entries: betaShapes } }] } };
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
if (process.argv.includes("--validate-only")) console.log("Validated Adobe beta FrameRate declaration drift.");
|
||||
else if (process.argv.includes("--check")) { let current = ""; try { current = await readFile(outputPath, "utf8"); } catch {} if (current.replaceAll("\r\n", "\n") !== rendered) { console.error("Adobe beta FrameRate declaration drift inventory is stale. Run npm run adobe:beta-frame-rate-drift."); process.exitCode = 1; } else console.log("Adobe beta FrameRate declaration drift inventory is current: 2 beta-only, 2 stable-only, and 1 changed symbol."); }
|
||||
else { await writeFile(outputPath, rendered); console.log("Wrote Adobe beta FrameRate declaration drift inventory: 2 beta-only, 2 stable-only, and 1 changed symbol."); }
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stablePath = process.env.PREMIERE_BETA_GUID_STABLE_DECLARATIONS_PATH ? resolve(process.env.PREMIERE_BETA_GUID_STABLE_DECLARATIONS_PATH) : resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const betaPath = process.env.PREMIERE_BETA_GUID_BETA_DECLARATIONS_PATH ? resolve(process.env.PREMIERE_BETA_GUID_BETA_DECLARATIONS_PATH) : resolve(root, "node_modules/@adobe/premierepro-beta/src/premierepro.d.ts");
|
||||
const outputPath = resolve(root, "src/resources/adobe-beta-guid-drift.json");
|
||||
const normalize = (text) => text.replaceAll("\r\n", "\n").replace(/\s+/g, " ").trim();
|
||||
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const parse = (text, path) => { const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true); if (source.parseDiagnostics.length) throw new Error(`Adobe Guid declaration parse failed: ${source.parseDiagnostics.map((item) => ts.flattenDiagnosticMessageText(item.messageText, " ")).join("; ")}`); return source; };
|
||||
const literal = (source, name) => { const declaration = source.statements.find((item) => ts.isTypeAliasDeclaration(item) && item.name.text === name); if (!declaration || !ts.isTypeLiteralNode(declaration.type)) throw new Error(`Adobe declarations must expose ${name} as a type literal.`); return declaration; };
|
||||
const binding = (rootDeclaration, source) => { const member = rootDeclaration.type.members.find((item) => ts.isPropertySignature(item) && item.name && ts.isIdentifier(item.name) && item.name.text === "Guid"); if (!member?.type) throw new Error("Adobe declarations must expose premierepro.Guid as a typed property."); return { symbol: "premierepro.Guid", kind: "property", signature: normalize(member.type.getText(source)) }; };
|
||||
const factories = (declaration, source, owner) => declaration.type.members.filter((item) => ts.isConstructSignatureDeclaration(item) || ts.isCallSignatureDeclaration(item)).map((item) => { if (!item.type) throw new Error(`Adobe ${owner} factory signature is missing a return type.`); return { symbol: `${owner}.${ts.isConstructSignatureDeclaration(item) ? "new" : "call"}`, kind: ts.isConstructSignatureDeclaration(item) ? "construct_signature" : "call_signature", signature: `(${item.parameters.map((parameter) => normalize(parameter.getText(source))).join(", ")}) => ${normalize(item.type.getText(source))}` }; }).sort((left, right) => compare(left.symbol, right.symbol));
|
||||
const nonFactories = (declaration, source) => declaration.type.members.filter((item) => !ts.isConstructSignatureDeclaration(item) && !ts.isCallSignatureDeclaration(item)).map((item) => normalize(item.getText(source))).sort(compare);
|
||||
const [stablePackageText, betaPackageText, stableText, betaText] = await Promise.all([readFile(resolve(root, "node_modules/@adobe/premierepro/package.json"), "utf8"), readFile(resolve(root, "node_modules/@adobe/premierepro-beta/package.json"), "utf8"), readFile(stablePath, "utf8"), readFile(betaPath, "utf8")]);
|
||||
const stable = parse(stableText, stablePath), beta = parse(betaText, betaPath), stableRoot = literal(stable, "premierepro"), betaRoot = literal(beta, "premierepro"), stableGuid = literal(stable, "Guid"), betaGuid = literal(beta, "Guid"), stableStatic = literal(stable, "GuidStatic"), betaStatic = literal(beta, "GuidStatic"), stableBinding = binding(stableRoot, stable), betaBinding = binding(betaRoot, beta), stableFactories = factories(stableGuid, stable, "Guid"), betaFactories = factories(betaStatic, beta, "GuidStatic");
|
||||
if (stableBinding.signature !== "GuidStatic" || betaBinding.signature !== "GuidStatic") throw new Error("Adobe beta declarations must retain premierepro.Guid as GuidStatic.");
|
||||
if (factories(stableStatic, stable, "GuidStatic").length || factories(betaGuid, beta, "Guid").length) throw new Error("Adobe beta Guid factory signatures must move only from stable Guid to beta GuidStatic.");
|
||||
const stableShapes = stableFactories.map(({ kind, signature }) => ({ kind, signature })), betaShapes = betaFactories.map(({ kind, signature }) => ({ kind, signature }));
|
||||
if (stableFactories.length !== 2 || betaFactories.length !== 2 || JSON.stringify(stableShapes) !== JSON.stringify(betaShapes)) throw new Error("Adobe beta GuidStatic factory signatures must match stable Guid.");
|
||||
if (JSON.stringify(nonFactories(stableGuid, stable)) !== JSON.stringify(nonFactories(betaGuid, beta))) throw new Error("Adobe beta Guid non-factory members must match stable Guid.");
|
||||
if (JSON.stringify(nonFactories(stableStatic, stable)) !== JSON.stringify(nonFactories(betaStatic, beta))) throw new Error("Adobe beta GuidStatic non-factory members must match stable GuidStatic.");
|
||||
const hash = (declaration, source) => createHash("sha256").update(normalize(declaration.getText(source))).digest("hex");
|
||||
const inventory = { schemaVersion: 1, scope: { declarations: ["premierepro.Guid", "Guid", "GuidStatic"], semantics: "This records the beta Guid factory-placement migration against the pinned stable package. It is static declaration-drift accounting, not beta API support or a complete package diff.", doesNotEstablish: "It does not prove a beta host exposes GuidStatic factory calls, that a Guid can be constructed or parsed or accepted by another API, that GUID identity is stable, or that any MCP action is supported or licensed-host validated." }, sources: { stable: { package: "@adobe/premierepro", version: JSON.parse(stablePackageText).version, declarations: relative(root, stablePath).replaceAll("\\", "/"), guidDeclarationSha256: hash(stableGuid, stable), guidStaticDeclarationSha256: hash(stableStatic, stable), rootBinding: stableBinding.signature }, beta: { package: "@adobe/premierepro-beta", version: JSON.parse(betaPackageText).version, declarations: relative(root, betaPath).replaceAll("\\", "/"), guidDeclarationSha256: hash(betaGuid, beta), guidStaticDeclarationSha256: hash(betaStatic, beta), rootBinding: betaBinding.signature } }, diff: { betaOnly: betaFactories, stableOnly: stableFactories, changed: [{ symbol: "Guid.factorySignaturePlacement", stable: { owner: "Guid", entries: stableShapes }, beta: { owner: "GuidStatic", entries: betaShapes } }] } };
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
if (process.argv.includes("--validate-only")) console.log("Validated Adobe beta Guid declaration drift.");
|
||||
else if (process.argv.includes("--check")) { let current = ""; try { current = await readFile(outputPath, "utf8"); } catch {} if (current.replaceAll("\r\n", "\n") !== rendered) { console.error("Adobe beta Guid declaration drift inventory is stale. Run npm run adobe:beta-guid-drift."); process.exitCode = 1; } else console.log("Adobe beta Guid declaration drift inventory is current: 2 beta-only, 2 stable-only, and 1 changed symbol."); }
|
||||
else { await writeFile(outputPath, rendered); console.log("Wrote Adobe beta Guid declaration drift inventory: 2 beta-only, 2 stable-only, and 1 changed symbol."); }
|
||||
@@ -0,0 +1,150 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stablePackagePath = resolve(root, "node_modules/@adobe/premierepro/package.json");
|
||||
const betaPackagePath = resolve(root, "node_modules/@adobe/premierepro-beta/package.json");
|
||||
const stableDeclarationsPath = process.env.PREMIERE_BETA_MEDIA_STABLE_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_MEDIA_STABLE_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const betaDeclarationsPath = process.env.PREMIERE_BETA_MEDIA_BETA_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_MEDIA_BETA_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro-beta/src/premierepro.d.ts");
|
||||
const outputPath = process.env.PREMIERE_BETA_MEDIA_DRIFT_OUTPUT_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_MEDIA_DRIFT_OUTPUT_PATH)
|
||||
: resolve(root, "src/resources/adobe-beta-media-drift.json");
|
||||
const check = process.argv.includes("--check");
|
||||
const validateOnly = process.argv.includes("--validate-only");
|
||||
|
||||
const [stablePackageText, betaPackageText, stableDeclarations, betaDeclarations] = await Promise.all([
|
||||
readFile(stablePackagePath, "utf8"),
|
||||
readFile(betaPackagePath, "utf8"),
|
||||
readFile(stableDeclarationsPath, "utf8"),
|
||||
readFile(betaDeclarationsPath, "utf8"),
|
||||
]);
|
||||
|
||||
const stablePackage = JSON.parse(stablePackageText);
|
||||
const betaPackage = JSON.parse(betaPackageText);
|
||||
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const normalizedText = (text) => text.replaceAll("\r\n", "\n").replace(/\s+/g, " ").trim();
|
||||
|
||||
function memberName(member, source) {
|
||||
if (!member.name || !(
|
||||
ts.isIdentifier(member.name) || ts.isStringLiteral(member.name) || ts.isNumericLiteral(member.name)
|
||||
)) {
|
||||
throw new Error(`Adobe Media declaration has an unsupported member name in ${source.fileName}`);
|
||||
}
|
||||
return member.name.text;
|
||||
}
|
||||
|
||||
function mediaDeclaration(declarations, declarationPath) {
|
||||
const source = ts.createSourceFile(declarationPath, declarations, ts.ScriptTarget.Latest, true);
|
||||
if (source.parseDiagnostics.length > 0) {
|
||||
const details = source.parseDiagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, " ")).join("; ");
|
||||
throw new Error(`Adobe Media declaration parse failed: ${details}`);
|
||||
}
|
||||
const declaration = source.statements.find((statement) => (
|
||||
ts.isTypeAliasDeclaration(statement) && statement.name.text === "Media"
|
||||
));
|
||||
if (!declaration || !ts.isTypeLiteralNode(declaration.type)) {
|
||||
throw new Error("Adobe declarations must expose Media as a type literal.");
|
||||
}
|
||||
const members = declaration.type.members.map((member) => {
|
||||
const name = memberName(member, source);
|
||||
if (ts.isMethodSignature(member)) {
|
||||
if (!member.type) throw new Error(`Adobe Media.${name} is missing a return type.`);
|
||||
return {
|
||||
symbol: `Media.${name}`,
|
||||
kind: "method",
|
||||
signature: `(${member.parameters.map((parameter) => normalizedText(parameter.getText(source))).join(", ")}) => ${normalizedText(member.type.getText(source))}`,
|
||||
};
|
||||
}
|
||||
if (ts.isPropertySignature(member)) {
|
||||
if (!member.type) throw new Error(`Adobe Media.${name} is missing a property type.`);
|
||||
const readonly = Boolean(ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Readonly);
|
||||
return {
|
||||
symbol: `Media.${name}`,
|
||||
kind: "property",
|
||||
...(readonly ? { readonly: true } : {}),
|
||||
signature: normalizedText(member.type.getText(source)),
|
||||
};
|
||||
}
|
||||
throw new Error(`Adobe Media.${name} has an unsupported declaration kind.`);
|
||||
}).sort((left, right) => compareText(left.symbol, right.symbol));
|
||||
if (new Set(members.map((member) => member.symbol)).size !== members.length) {
|
||||
throw new Error("Adobe Media declarations must not contain duplicate symbols.");
|
||||
}
|
||||
return {
|
||||
members,
|
||||
declarationSha256: createHash("sha256").update(normalizedText(declaration.getText(source))).digest("hex"),
|
||||
};
|
||||
}
|
||||
|
||||
const stable = mediaDeclaration(stableDeclarations, stableDeclarationsPath);
|
||||
const beta = mediaDeclaration(betaDeclarations, betaDeclarationsPath);
|
||||
const stableBySymbol = new Map(stable.members.map((member) => [member.symbol, member]));
|
||||
const betaBySymbol = new Map(beta.members.map((member) => [member.symbol, member]));
|
||||
const betaOnly = beta.members.filter((member) => !stableBySymbol.has(member.symbol));
|
||||
const stableOnly = stable.members.filter((member) => !betaBySymbol.has(member.symbol));
|
||||
const changed = stable.members.flatMap((member) => {
|
||||
const betaMember = betaBySymbol.get(member.symbol);
|
||||
if (!betaMember || JSON.stringify(member) === JSON.stringify(betaMember)) return [];
|
||||
return [{ symbol: member.symbol, stable: member, beta: betaMember }];
|
||||
});
|
||||
const unchanged = stable.members.filter((member) => {
|
||||
const betaMember = betaBySymbol.get(member.symbol);
|
||||
return betaMember && JSON.stringify(member) === JSON.stringify(betaMember);
|
||||
}).map((member) => member.symbol);
|
||||
|
||||
const inventory = {
|
||||
schemaVersion: 1,
|
||||
scope: {
|
||||
declaration: "Media",
|
||||
semantics: "This compares only the public Media declaration in the pinned stable and beta packages. It is a declaration-drift audit, not beta API support or a complete package diff.",
|
||||
doesNotEstablish: "It does not prove a beta host exposes these members, that a stable host accepts a beta call, or that any MCP action is supported or licensed-host validated.",
|
||||
},
|
||||
sources: {
|
||||
stable: {
|
||||
package: "@adobe/premierepro",
|
||||
version: stablePackage.version,
|
||||
declarations: relative(root, stableDeclarationsPath).replaceAll("\\", "/"),
|
||||
mediaDeclarationSha256: stable.declarationSha256,
|
||||
},
|
||||
beta: {
|
||||
package: "@adobe/premierepro-beta",
|
||||
version: betaPackage.version,
|
||||
declarations: relative(root, betaDeclarationsPath).replaceAll("\\", "/"),
|
||||
mediaDeclarationSha256: beta.declarationSha256,
|
||||
},
|
||||
},
|
||||
members: {
|
||||
stable: stable.members,
|
||||
beta: beta.members,
|
||||
},
|
||||
diff: {
|
||||
betaOnly,
|
||||
stableOnly,
|
||||
changed,
|
||||
unchanged,
|
||||
},
|
||||
};
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
|
||||
if (validateOnly) {
|
||||
console.log(`Validated Adobe Media declaration drift: ${betaOnly.length} beta-only and ${changed.length} changed symbols.`);
|
||||
} else if (check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
if (current.replaceAll("\r\n", "\n") !== rendered) {
|
||||
console.error("Adobe beta Media declaration drift inventory is stale. Run npm run adobe:beta-media-drift.");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`Adobe beta Media declaration drift inventory is current: ${betaOnly.length} beta-only and ${changed.length} changed symbols.`);
|
||||
}
|
||||
} else {
|
||||
await writeFile(outputPath, rendered);
|
||||
console.log(`Wrote Adobe beta Media declaration drift inventory: ${betaOnly.length} beta-only and ${changed.length} changed symbols.`);
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stablePackagePath = resolve(root, "node_modules/@adobe/premierepro/package.json");
|
||||
const betaPackagePath = resolve(root, "node_modules/@adobe/premierepro-beta/package.json");
|
||||
const stableDeclarationsPath = process.env.PREMIERE_BETA_MEDIA_MANAGER_STABLE_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_MEDIA_MANAGER_STABLE_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const betaDeclarationsPath = process.env.PREMIERE_BETA_MEDIA_MANAGER_BETA_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_MEDIA_MANAGER_BETA_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro-beta/src/premierepro.d.ts");
|
||||
const outputPath = process.env.PREMIERE_BETA_MEDIA_MANAGER_DRIFT_OUTPUT_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_MEDIA_MANAGER_DRIFT_OUTPUT_PATH)
|
||||
: resolve(root, "src/resources/adobe-beta-media-manager-drift.json");
|
||||
const check = process.argv.includes("--check");
|
||||
const validateOnly = process.argv.includes("--validate-only");
|
||||
|
||||
const [stablePackageText, betaPackageText, stableDeclarations, betaDeclarations] = await Promise.all([
|
||||
readFile(stablePackagePath, "utf8"),
|
||||
readFile(betaPackagePath, "utf8"),
|
||||
readFile(stableDeclarationsPath, "utf8"),
|
||||
readFile(betaDeclarationsPath, "utf8"),
|
||||
]);
|
||||
|
||||
const stablePackage = JSON.parse(stablePackageText);
|
||||
const betaPackage = JSON.parse(betaPackageText);
|
||||
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const normalizedText = (text) => text.replaceAll("\r\n", "\n").replace(/\s+/g, " ").trim();
|
||||
|
||||
function sourceFile(declarations, declarationPath) {
|
||||
const source = ts.createSourceFile(declarationPath, declarations, ts.ScriptTarget.Latest, true);
|
||||
if (source.parseDiagnostics.length > 0) {
|
||||
const details = source.parseDiagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, " ")).join("; ");
|
||||
throw new Error(`Adobe MediaManager declaration parse failed: ${details}`);
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
function typeLiteral(source, name) {
|
||||
const declaration = source.statements.find((statement) => (
|
||||
ts.isTypeAliasDeclaration(statement) && statement.name.text === name
|
||||
));
|
||||
if (!declaration || !ts.isTypeLiteralNode(declaration.type)) {
|
||||
throw new Error(`Adobe declarations must expose ${name} as a type literal.`);
|
||||
}
|
||||
return declaration;
|
||||
}
|
||||
|
||||
function nameOf(member, source, owner) {
|
||||
if (!member.name || !(
|
||||
ts.isIdentifier(member.name) || ts.isStringLiteral(member.name) || ts.isNumericLiteral(member.name)
|
||||
)) {
|
||||
throw new Error(`Adobe ${owner} declaration has an unsupported member name in ${source.fileName}`);
|
||||
}
|
||||
return member.name.text;
|
||||
}
|
||||
|
||||
function memberEntries(type, source, owner) {
|
||||
const entries = type.members.map((member) => {
|
||||
const name = nameOf(member, source, owner);
|
||||
if (ts.isMethodSignature(member)) {
|
||||
if (!member.type) throw new Error(`Adobe ${owner}.${name} is missing a return type.`);
|
||||
return {
|
||||
symbol: `${owner}.${name}`,
|
||||
kind: "method",
|
||||
signature: `(${member.parameters.map((parameter) => normalizedText(parameter.getText(source))).join(", ")}) => ${normalizedText(member.type.getText(source))}`,
|
||||
};
|
||||
}
|
||||
if (ts.isPropertySignature(member)) {
|
||||
if (!member.type) throw new Error(`Adobe ${owner}.${name} is missing a property type.`);
|
||||
const readonly = Boolean(ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Readonly);
|
||||
return {
|
||||
symbol: `${owner}.${name}`,
|
||||
kind: "property",
|
||||
...(readonly ? { readonly: true } : {}),
|
||||
signature: normalizedText(member.type.getText(source)),
|
||||
};
|
||||
}
|
||||
throw new Error(`Adobe ${owner}.${name} has an unsupported declaration kind.`);
|
||||
}).sort((left, right) => compareText(left.symbol, right.symbol));
|
||||
if (new Set(entries.map((entry) => entry.symbol)).size !== entries.length) {
|
||||
throw new Error(`Adobe ${owner} declarations must not contain duplicate symbols.`);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function hasTypeAlias(source, name) {
|
||||
return source.statements.some((statement) => ts.isTypeAliasDeclaration(statement) && statement.name.text === name);
|
||||
}
|
||||
|
||||
function declarationHash(declaration, source) {
|
||||
return createHash("sha256").update(normalizedText(declaration.getText(source))).digest("hex");
|
||||
}
|
||||
|
||||
const stableSource = sourceFile(stableDeclarations, stableDeclarationsPath);
|
||||
const betaSource = sourceFile(betaDeclarations, betaDeclarationsPath);
|
||||
const stableRoot = typeLiteral(stableSource, "premierepro");
|
||||
const stableRootEntries = memberEntries(stableRoot.type, stableSource, "premierepro");
|
||||
if (stableRootEntries.some((entry) => entry.symbol === "premierepro.MediaManager") ||
|
||||
hasTypeAlias(stableSource, "MediaManagerStatic") ||
|
||||
hasTypeAlias(stableSource, "MediaManager")) {
|
||||
throw new Error("Pinned stable declarations unexpectedly expose MediaManager.");
|
||||
}
|
||||
|
||||
const betaRoot = typeLiteral(betaSource, "premierepro");
|
||||
const rootBinding = memberEntries(betaRoot.type, betaSource, "premierepro")
|
||||
.find((entry) => entry.symbol === "premierepro.MediaManager");
|
||||
if (!rootBinding || rootBinding.kind !== "property" || rootBinding.signature !== "MediaManagerStatic") {
|
||||
throw new Error("Adobe beta declarations must expose premierepro.MediaManager as MediaManagerStatic.");
|
||||
}
|
||||
const mediaManagerStatic = typeLiteral(betaSource, "MediaManagerStatic");
|
||||
const mediaManagerEntries = memberEntries(mediaManagerStatic.type, betaSource, "MediaManagerStatic");
|
||||
const mediaManagerInstance = typeLiteral(betaSource, "MediaManager");
|
||||
if (mediaManagerInstance.type.members.length !== 0) {
|
||||
throw new Error("Adobe beta MediaManager instance declaration must remain empty for this receipt.");
|
||||
}
|
||||
const betaOnly = [
|
||||
rootBinding,
|
||||
{
|
||||
symbol: "MediaManager",
|
||||
kind: "type",
|
||||
signature: normalizedText(mediaManagerInstance.type.getText(betaSource)),
|
||||
},
|
||||
...mediaManagerEntries,
|
||||
].sort((left, right) => compareText(left.symbol, right.symbol));
|
||||
|
||||
const inventory = {
|
||||
schemaVersion: 1,
|
||||
scope: {
|
||||
declarations: ["premierepro.MediaManager", "MediaManagerStatic", "MediaManager"],
|
||||
semantics: "This records the MediaManager declaration surface that is absent from the pinned stable package and present in the pinned beta package. It is static declaration-drift accounting, not beta API support or a complete package diff.",
|
||||
doesNotEstablish: "It does not prove a beta host exposes MediaManager, that a stable host accepts a beta call, that purging changes a cache, or that an MCP action is supported or licensed-host validated.",
|
||||
mutationBoundary: "purgeMediaCache is declared as a mutating cache operation. This receipt intentionally has no production call or user-facing cache-purge action.",
|
||||
},
|
||||
sources: {
|
||||
stable: {
|
||||
package: "@adobe/premierepro",
|
||||
version: stablePackage.version,
|
||||
declarations: relative(root, stableDeclarationsPath).replaceAll("\\", "/"),
|
||||
mediaManagerSurfacePresent: false,
|
||||
},
|
||||
beta: {
|
||||
package: "@adobe/premierepro-beta",
|
||||
version: betaPackage.version,
|
||||
declarations: relative(root, betaDeclarationsPath).replaceAll("\\", "/"),
|
||||
mediaManagerSurfacePresent: true,
|
||||
rootDeclarationSha256: declarationHash(betaRoot, betaSource),
|
||||
mediaManagerStaticDeclarationSha256: declarationHash(mediaManagerStatic, betaSource),
|
||||
mediaManagerDeclarationSha256: declarationHash(mediaManagerInstance, betaSource),
|
||||
},
|
||||
},
|
||||
diff: {
|
||||
betaOnly,
|
||||
stableOnly: [],
|
||||
changed: [],
|
||||
},
|
||||
};
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
|
||||
if (validateOnly) {
|
||||
console.log(`Validated Adobe beta MediaManager declaration drift: ${betaOnly.length} beta-only symbols.`);
|
||||
} else if (check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
if (current.replaceAll("\r\n", "\n") !== rendered) {
|
||||
console.error("Adobe beta MediaManager declaration drift inventory is stale. Run npm run adobe:beta-media-manager-drift.");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`Adobe beta MediaManager declaration drift inventory is current: ${betaOnly.length} beta-only symbols.`);
|
||||
}
|
||||
} else {
|
||||
await writeFile(outputPath, rendered);
|
||||
console.log(`Wrote Adobe beta MediaManager declaration drift inventory: ${betaOnly.length} beta-only symbols.`);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stablePath = process.env.PREMIERE_BETA_POINTF_STABLE_DECLARATIONS_PATH ? resolve(process.env.PREMIERE_BETA_POINTF_STABLE_DECLARATIONS_PATH) : resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const betaPath = process.env.PREMIERE_BETA_POINTF_BETA_DECLARATIONS_PATH ? resolve(process.env.PREMIERE_BETA_POINTF_BETA_DECLARATIONS_PATH) : resolve(root, "node_modules/@adobe/premierepro-beta/src/premierepro.d.ts");
|
||||
const outputPath = resolve(root, "src/resources/adobe-beta-pointf-drift.json");
|
||||
const normalize = (text) => text.replaceAll("\r\n", "\n").replace(/\s+/g, " ").trim();
|
||||
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const parse = (text, path) => { const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true); if (source.parseDiagnostics.length) throw new Error(`Adobe PointF declaration parse failed: ${source.parseDiagnostics.map((item) => ts.flattenDiagnosticMessageText(item.messageText, " ")).join("; ")}`); return source; };
|
||||
const literal = (source, name) => { const declaration = source.statements.find((item) => ts.isTypeAliasDeclaration(item) && item.name.text === name); if (!declaration || !ts.isTypeLiteralNode(declaration.type)) throw new Error(`Adobe declarations must expose ${name} as a type literal.`); return declaration; };
|
||||
const binding = (rootDeclaration, source) => { const member = rootDeclaration.type.members.find((item) => ts.isPropertySignature(item) && item.name && ts.isIdentifier(item.name) && item.name.text === "PointF"); if (!member?.type) throw new Error("Adobe declarations must expose premierepro.PointF as a typed property."); return { symbol: "premierepro.PointF", kind: "property", signature: normalize(member.type.getText(source)) }; };
|
||||
const factories = (declaration, source, owner) => declaration.type.members.filter((item) => ts.isConstructSignatureDeclaration(item) || ts.isCallSignatureDeclaration(item)).map((item) => { if (!item.type) throw new Error(`Adobe ${owner} factory signature is missing a return type.`); return { symbol: `${owner}.${ts.isConstructSignatureDeclaration(item) ? "new" : "call"}`, kind: ts.isConstructSignatureDeclaration(item) ? "construct_signature" : "call_signature", signature: `(${item.parameters.map((parameter) => normalize(parameter.getText(source))).join(", ")}) => ${normalize(item.type.getText(source))}` }; }).sort((left, right) => compare(left.symbol, right.symbol));
|
||||
const nonFactories = (declaration, source) => declaration.type.members.filter((item) => !ts.isConstructSignatureDeclaration(item) && !ts.isCallSignatureDeclaration(item)).map((item) => normalize(item.getText(source))).sort(compare);
|
||||
const [stablePackageText, betaPackageText, stableText, betaText] = await Promise.all([readFile(resolve(root, "node_modules/@adobe/premierepro/package.json"), "utf8"), readFile(resolve(root, "node_modules/@adobe/premierepro-beta/package.json"), "utf8"), readFile(stablePath, "utf8"), readFile(betaPath, "utf8")]);
|
||||
const stable = parse(stableText, stablePath), beta = parse(betaText, betaPath), stableRoot = literal(stable, "premierepro"), betaRoot = literal(beta, "premierepro"), stablePoint = literal(stable, "PointF"), betaPoint = literal(beta, "PointF"), betaStatic = literal(beta, "PointFStatic"), stableBinding = binding(stableRoot, stable), betaBinding = binding(betaRoot, beta), stableFactories = factories(stablePoint, stable, "PointF"), betaFactories = factories(betaStatic, beta, "PointFStatic");
|
||||
if (stable.statements.some((item) => ts.isTypeAliasDeclaration(item) && item.name.text === "PointFStatic")) throw new Error("Pinned stable declarations unexpectedly expose PointFStatic.");
|
||||
if (stableBinding.signature !== "PointF" || betaBinding.signature !== "PointFStatic") throw new Error("Adobe beta declarations must move premierepro.PointF to PointFStatic.");
|
||||
const stableShapes = stableFactories.map(({ kind, signature }) => ({ kind, signature })), betaShapes = betaFactories.map(({ kind, signature }) => ({ kind, signature }));
|
||||
if (stableFactories.length !== 2 || betaFactories.length !== 2 || JSON.stringify(stableShapes) !== JSON.stringify(betaShapes)) throw new Error("Adobe beta PointFStatic factory signatures must match stable PointF.");
|
||||
if (JSON.stringify(nonFactories(stablePoint, stable)) !== JSON.stringify(nonFactories(betaPoint, beta))) throw new Error("Adobe beta PointF non-factory members must match stable PointF.");
|
||||
if (factories(betaPoint, beta, "PointF").length) throw new Error("Adobe beta PointF must not retain factory signatures after the static-type migration.");
|
||||
const hash = (declaration, source) => createHash("sha256").update(normalize(declaration.getText(source))).digest("hex");
|
||||
const inventory = { schemaVersion: 1, scope: { declarations: ["premierepro.PointF", "PointF", "PointFStatic"], semantics: "This records the beta PointF factory-type migration against the pinned stable package. It is static declaration-drift accounting, not beta API support or a complete package diff.", doesNotEstablish: "It does not prove a beta host exposes PointFStatic, that a PointF can be constructed or accepted by another API, that point arithmetic or component-parameter behavior is preserved, or that any MCP action is supported or licensed-host validated." }, sources: { stable: { package: "@adobe/premierepro", version: JSON.parse(stablePackageText).version, declarations: relative(root, stablePath).replaceAll("\\", "/"), pointDeclarationSha256: hash(stablePoint, stable), staticFactoryPresent: false }, beta: { package: "@adobe/premierepro-beta", version: JSON.parse(betaPackageText).version, declarations: relative(root, betaPath).replaceAll("\\", "/"), pointDeclarationSha256: hash(betaPoint, beta), staticDeclarationSha256: hash(betaStatic, beta), staticFactoryPresent: true } }, diff: { betaOnly: [{ symbol: "PointFStatic", kind: "type", signature: normalize(betaStatic.type.getText(beta)) }, ...betaFactories].sort((left, right) => compare(left.symbol, right.symbol)), stableOnly: [], changed: [{ symbol: "premierepro.PointF", stable: stableBinding, beta: betaBinding }, { symbol: "PointF.factorySignatures", stable: { owner: "PointF", entries: stableShapes }, beta: { owner: "PointFStatic", entries: betaShapes } }] } };
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
if (process.argv.includes("--validate-only")) console.log("Validated Adobe beta PointF declaration drift.");
|
||||
else if (process.argv.includes("--check")) { let current = ""; try { current = await readFile(outputPath, "utf8"); } catch {} if (current.replaceAll("\r\n", "\n") !== rendered) { console.error("Adobe beta PointF declaration drift inventory is stale. Run npm run adobe:beta-pointf-drift."); process.exitCode = 1; } else console.log("Adobe beta PointF declaration drift inventory is current: 3 beta-only and 2 changed symbols."); }
|
||||
else { await writeFile(outputPath, rendered); console.log("Wrote Adobe beta PointF declaration drift inventory: 3 beta-only and 2 changed symbols."); }
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stableDeclarationsPath = process.env.PREMIERE_BETA_PROJECT_OPTIONS_STABLE_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_PROJECT_OPTIONS_STABLE_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const betaDeclarationsPath = process.env.PREMIERE_BETA_PROJECT_OPTIONS_BETA_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_PROJECT_OPTIONS_BETA_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro-beta/src/premierepro.d.ts");
|
||||
const outputPath = process.env.PREMIERE_BETA_PROJECT_OPTIONS_DRIFT_OUTPUT_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_PROJECT_OPTIONS_DRIFT_OUTPUT_PATH)
|
||||
: resolve(root, "src/resources/adobe-beta-project-options-drift.json");
|
||||
const check = process.argv.includes("--check");
|
||||
const validateOnly = process.argv.includes("--validate-only");
|
||||
const [stablePackageText, betaPackageText, stableText, betaText] = await Promise.all([
|
||||
readFile(resolve(root, "node_modules/@adobe/premierepro/package.json"), "utf8"),
|
||||
readFile(resolve(root, "node_modules/@adobe/premierepro-beta/package.json"), "utf8"),
|
||||
readFile(stableDeclarationsPath, "utf8"),
|
||||
readFile(betaDeclarationsPath, "utf8"),
|
||||
]);
|
||||
const normalize = (value) => value.replaceAll("\r\n", "\n").replace(/\s+/g, " ").trim();
|
||||
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const hash = (node, source) => createHash("sha256").update(normalize(node.getText(source))).digest("hex");
|
||||
|
||||
function sourceFile(text, path) {
|
||||
const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
|
||||
if (source.parseDiagnostics.length) throw new Error(`Adobe project-options declaration parse failed: ${source.parseDiagnostics.map((item) => ts.flattenDiagnosticMessageText(item.messageText, " ")).join("; ")}`);
|
||||
return source;
|
||||
}
|
||||
function type(source, name) {
|
||||
const declaration = source.statements.find((item) => ts.isTypeAliasDeclaration(item) && item.name.text === name);
|
||||
if (!declaration || !ts.isTypeLiteralNode(declaration.type)) throw new Error(`Adobe declarations must expose ${name} as a type literal.`);
|
||||
return declaration;
|
||||
}
|
||||
function binding(rootDeclaration, source, name) {
|
||||
const member = rootDeclaration.type.members.find((item) => ts.isPropertySignature(item) && item.name && ts.isIdentifier(item.name) && item.name.text === name);
|
||||
if (!member?.type) throw new Error(`Adobe declarations must expose premierepro.${name} as a typed property.`);
|
||||
return { symbol: `premierepro.${name}`, kind: "property", signature: normalize(member.type.getText(source)) };
|
||||
}
|
||||
function factories(declaration, source, owner) {
|
||||
const values = declaration.type.members.filter((item) => ts.isConstructSignatureDeclaration(item) || ts.isCallSignatureDeclaration(item)).map((item) => {
|
||||
if (!item.type) throw new Error(`Adobe ${owner} factory signature is missing a return type.`);
|
||||
return {
|
||||
symbol: `${owner}.${ts.isConstructSignatureDeclaration(item) ? "new" : "call"}`,
|
||||
kind: ts.isConstructSignatureDeclaration(item) ? "construct_signature" : "call_signature",
|
||||
signature: `(${item.parameters.map((parameter) => normalize(parameter.getText(source))).join(", ")}) => ${normalize(item.type.getText(source))}`,
|
||||
};
|
||||
}).sort((left, right) => compare(left.symbol, right.symbol));
|
||||
if (values.length !== 2 || values[0].kind !== "call_signature" || values[1].kind !== "construct_signature") throw new Error(`Adobe ${owner} must expose exactly one call and one construct factory signature.`);
|
||||
return values;
|
||||
}
|
||||
function nonFactories(declaration, source, owner) {
|
||||
const values = declaration.type.members.filter((item) => !ts.isConstructSignatureDeclaration(item) && !ts.isCallSignatureDeclaration(item)).map((item) => normalize(item.getText(source))).sort(compare);
|
||||
if (!values.length) throw new Error(`Adobe ${owner} must retain non-factory option members.`);
|
||||
return values;
|
||||
}
|
||||
function hasFactories(declaration) {
|
||||
return declaration.type.members.some((item) => ts.isConstructSignatureDeclaration(item) || ts.isCallSignatureDeclaration(item));
|
||||
}
|
||||
function hasType(source, name) {
|
||||
return source.statements.some((item) => ts.isTypeAliasDeclaration(item) && item.name.text === name);
|
||||
}
|
||||
|
||||
const stableSource = sourceFile(stableText, stableDeclarationsPath);
|
||||
const betaSource = sourceFile(betaText, betaDeclarationsPath);
|
||||
const stableRoot = type(stableSource, "premierepro");
|
||||
const betaRoot = type(betaSource, "premierepro");
|
||||
const names = ["OpenProjectOptions", "CloseProjectOptions"];
|
||||
const records = names.map((name) => {
|
||||
const staticName = `${name}Static`;
|
||||
const stableOptions = type(stableSource, name);
|
||||
const betaOptions = type(betaSource, name);
|
||||
if (hasType(stableSource, staticName)) throw new Error(`Pinned stable declarations unexpectedly expose ${staticName}.`);
|
||||
const betaStatic = type(betaSource, staticName);
|
||||
const stableBinding = binding(stableRoot, stableSource, name);
|
||||
const betaBinding = binding(betaRoot, betaSource, name);
|
||||
if (stableBinding.signature !== name) throw new Error(`Pinned stable declarations must expose premierepro.${name} as ${name}.`);
|
||||
if (betaBinding.signature !== staticName) throw new Error(`Adobe beta declarations must expose premierepro.${name} as ${staticName}.`);
|
||||
const stableFactories = factories(stableOptions, stableSource, name);
|
||||
const betaFactories = factories(betaStatic, betaSource, staticName);
|
||||
const stableShapes = stableFactories.map(({ kind, signature }) => ({ kind, signature }));
|
||||
const betaShapes = betaFactories.map(({ kind, signature }) => ({ kind, signature }));
|
||||
if (JSON.stringify(stableShapes) !== JSON.stringify(betaShapes)) throw new Error(`Adobe beta ${staticName} factory signatures must match stable ${name}.`);
|
||||
if (JSON.stringify(nonFactories(stableOptions, stableSource, name)) !== JSON.stringify(nonFactories(betaOptions, betaSource, name))) throw new Error(`Adobe beta ${name} option members must match the pinned stable declaration.`);
|
||||
if (hasFactories(betaOptions)) throw new Error(`Adobe beta ${name} must not retain factory signatures after the static-type migration.`);
|
||||
return { name, staticName, stableBinding, betaBinding, stableShapes, betaFactories, stableOptions, betaOptions, betaStatic };
|
||||
});
|
||||
const betaOnly = records.flatMap((record) => [
|
||||
{ symbol: record.staticName, kind: "type", signature: normalize(record.betaStatic.type.getText(betaSource)) },
|
||||
...record.betaFactories,
|
||||
]).sort((left, right) => compare(left.symbol, right.symbol));
|
||||
const changed = records.flatMap((record) => [
|
||||
{ symbol: `premierepro.${record.name}`, stable: record.stableBinding, beta: record.betaBinding },
|
||||
{ symbol: `${record.name}.factorySignatures`, stable: { owner: record.name, entries: record.stableShapes }, beta: { owner: record.staticName, entries: record.stableShapes } },
|
||||
]);
|
||||
const inventory = {
|
||||
schemaVersion: 1,
|
||||
scope: {
|
||||
declarations: records.flatMap(({ name, staticName }) => [`premierepro.${name}`, name, staticName]),
|
||||
semantics: "This records the beta OpenProjectOptions and CloseProjectOptions factory-type migrations against the pinned stable package. It is static declaration-drift accounting, not beta API support or a complete package diff.",
|
||||
mutationBoundary: "These options configure project open and close behavior, including dialog, dirty-project, workspace, and quit controls. This receipt intentionally does not construct options or expose an MCP open/close project operation.",
|
||||
doesNotEstablish: "It does not prove a beta host exposes either static factory, that dialogs or dirty-project behavior can be safely controlled, that a project opens or closes, or that any MCP action is supported or licensed-host validated.",
|
||||
},
|
||||
sources: {
|
||||
stable: { package: "@adobe/premierepro", version: JSON.parse(stablePackageText).version, declarations: relative(root, stableDeclarationsPath).replaceAll("\\", "/"), rootDeclarationSha256: hash(stableRoot, stableSource), staticFactoriesPresent: false, options: Object.fromEntries(records.map((record) => [record.name, hash(record.stableOptions, stableSource)])) },
|
||||
beta: { package: "@adobe/premierepro-beta", version: JSON.parse(betaPackageText).version, declarations: relative(root, betaDeclarationsPath).replaceAll("\\", "/"), rootDeclarationSha256: hash(betaRoot, betaSource), staticFactoriesPresent: true, options: Object.fromEntries(records.map((record) => [record.name, { optionsDeclarationSha256: hash(record.betaOptions, betaSource), staticDeclarationSha256: hash(record.betaStatic, betaSource) }])) },
|
||||
},
|
||||
diff: { betaOnly, stableOnly: [], changed },
|
||||
};
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
if (validateOnly) console.log(`Validated Adobe beta project-options declaration drift: ${betaOnly.length} beta-only and ${changed.length} changed symbols.`);
|
||||
else if (check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
if (current.replaceAll("\r\n", "\n") !== rendered) { console.error("Adobe beta project-options declaration drift inventory is stale. Run npm run adobe:beta-project-options-drift."); process.exitCode = 1; }
|
||||
else console.log(`Adobe beta project-options declaration drift inventory is current: ${betaOnly.length} beta-only and ${changed.length} changed symbols.`);
|
||||
} else { await writeFile(outputPath, rendered); console.log(`Wrote Adobe beta project-options declaration drift inventory: ${betaOnly.length} beta-only and ${changed.length} changed symbols.`); }
|
||||
@@ -0,0 +1,29 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stablePath = process.env.PREMIERE_BETA_RECTF_STABLE_DECLARATIONS_PATH ? resolve(process.env.PREMIERE_BETA_RECTF_STABLE_DECLARATIONS_PATH) : resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const betaPath = process.env.PREMIERE_BETA_RECTF_BETA_DECLARATIONS_PATH ? resolve(process.env.PREMIERE_BETA_RECTF_BETA_DECLARATIONS_PATH) : resolve(root, "node_modules/@adobe/premierepro-beta/src/premierepro.d.ts");
|
||||
const outputPath = resolve(root, "src/resources/adobe-beta-rectf-drift.json");
|
||||
const normalize = (text) => text.replaceAll("\r\n", "\n").replace(/\s+/g, " ").trim();
|
||||
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const parse = (text, path) => { const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true); if (source.parseDiagnostics.length) throw new Error(`Adobe RectF declaration parse failed: ${source.parseDiagnostics.map((item) => ts.flattenDiagnosticMessageText(item.messageText, " ")).join("; ")}`); return source; };
|
||||
const literal = (source, name) => { const declaration = source.statements.find((item) => ts.isTypeAliasDeclaration(item) && item.name.text === name); if (!declaration || !ts.isTypeLiteralNode(declaration.type)) throw new Error(`Adobe declarations must expose ${name} as a type literal.`); return declaration; };
|
||||
const binding = (rootDeclaration, source) => { const member = rootDeclaration.type.members.find((item) => ts.isPropertySignature(item) && item.name && ts.isIdentifier(item.name) && item.name.text === "RectF"); if (!member?.type) throw new Error("Adobe declarations must expose premierepro.RectF as a typed property."); return { symbol: "premierepro.RectF", kind: "property", signature: normalize(member.type.getText(source)) }; };
|
||||
const factories = (declaration, source, owner) => declaration.type.members.filter((item) => ts.isConstructSignatureDeclaration(item) || ts.isCallSignatureDeclaration(item)).map((item) => ({ symbol: `${owner}.${ts.isConstructSignatureDeclaration(item) ? "new" : "call"}`, kind: ts.isConstructSignatureDeclaration(item) ? "construct_signature" : "call_signature", signature: `() => ${normalize(item.type.getText(source))}` })).sort((a, b) => compare(a.symbol, b.symbol));
|
||||
const fields = (declaration, source) => declaration.type.members.filter((item) => ts.isPropertySignature(item)).map((item) => normalize(item.getText(source))).sort(compare);
|
||||
const [stablePackageText, betaPackageText, stableText, betaText] = await Promise.all([readFile(resolve(root, "node_modules/@adobe/premierepro/package.json"), "utf8"), readFile(resolve(root, "node_modules/@adobe/premierepro-beta/package.json"), "utf8"), readFile(stablePath, "utf8"), readFile(betaPath, "utf8")]);
|
||||
const stable = parse(stableText, stablePath), beta = parse(betaText, betaPath), stableRoot = literal(stable, "premierepro"), betaRoot = literal(beta, "premierepro"), stableRect = literal(stable, "RectF"), betaRect = literal(beta, "RectF"), betaStatic = literal(beta, "RectFStatic"), stableBinding = binding(stableRoot, stable), betaBinding = binding(betaRoot, beta), stableFactories = factories(stableRect, stable, "RectF"), betaFactories = factories(betaStatic, beta, "RectFStatic");
|
||||
if (stable.statements.some((item) => ts.isTypeAliasDeclaration(item) && item.name.text === "RectFStatic")) throw new Error("Pinned stable declarations unexpectedly expose RectFStatic.");
|
||||
if (stableBinding.signature !== "RectF" || betaBinding.signature !== "RectFStatic") throw new Error("Adobe beta declarations must move premierepro.RectF to RectFStatic.");
|
||||
if (JSON.stringify(stableFactories.map(({ kind, signature }) => ({ kind, signature }))) !== JSON.stringify(betaFactories.map(({ kind, signature }) => ({ kind, signature }))) || stableFactories.length !== 2) throw new Error("Adobe beta RectFStatic factory signatures must match stable RectF.");
|
||||
if (JSON.stringify(fields(stableRect, stable)) !== JSON.stringify(fields(betaRect, beta)) || JSON.stringify(fields(betaRect, beta)) !== JSON.stringify(["height: number;", "width: number;"])) throw new Error("Adobe beta RectF fields must retain width and height.");
|
||||
if (factories(betaRect, beta, "RectF").length) throw new Error("Adobe beta RectF must not retain factory signatures after the static-type migration.");
|
||||
const hash = (declaration, source) => createHash("sha256").update(normalize(declaration.getText(source))).digest("hex");
|
||||
const inventory = { schemaVersion: 1, scope: { declarations: ["premierepro.RectF", "RectF", "RectFStatic"], semantics: "This records the beta RectF factory-type migration against the pinned stable package. It is static declaration-drift accounting, not beta API support or a complete package diff.", doesNotEstablish: "It does not prove a beta host exposes RectFStatic, that a RectF can be constructed or used by another API, or that any MCP action is supported or licensed-host validated." }, sources: { stable: { package: "@adobe/premierepro", version: JSON.parse(stablePackageText).version, declarations: relative(root, stablePath).replaceAll("\\", "/"), rectDeclarationSha256: hash(stableRect, stable), staticFactoryPresent: false }, beta: { package: "@adobe/premierepro-beta", version: JSON.parse(betaPackageText).version, declarations: relative(root, betaPath).replaceAll("\\", "/"), rectDeclarationSha256: hash(betaRect, beta), staticDeclarationSha256: hash(betaStatic, beta), staticFactoryPresent: true } }, diff: { betaOnly: [{ symbol: "RectFStatic", kind: "type", signature: normalize(betaStatic.type.getText(beta)) }, ...betaFactories].sort((a, b) => compare(a.symbol, b.symbol)), stableOnly: [], changed: [{ symbol: "premierepro.RectF", stable: stableBinding, beta: betaBinding }, { symbol: "RectF.factorySignatures", stable: { owner: "RectF", entries: stableFactories.map(({ kind, signature }) => ({ kind, signature })) }, beta: { owner: "RectFStatic", entries: betaFactories.map(({ kind, signature }) => ({ kind, signature })) } }] } };
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
if (process.argv.includes("--validate-only")) console.log("Validated Adobe beta RectF declaration drift.");
|
||||
else if (process.argv.includes("--check")) { let current = ""; try { current = await readFile(outputPath, "utf8"); } catch {} if (current.replaceAll("\r\n", "\n") !== rendered) { console.error("Adobe beta RectF declaration drift inventory is stale. Run npm run adobe:beta-rectf-drift."); process.exitCode = 1; } else console.log("Adobe beta RectF declaration drift inventory is current: 3 beta-only and 2 changed symbols."); } else { await writeFile(outputPath, rendered); console.log("Wrote Adobe beta RectF declaration drift inventory: 3 beta-only and 2 changed symbols."); }
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stablePath = process.env.PREMIERE_BETA_TICK_TIME_STABLE_DECLARATIONS_PATH ? resolve(process.env.PREMIERE_BETA_TICK_TIME_STABLE_DECLARATIONS_PATH) : resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const betaPath = process.env.PREMIERE_BETA_TICK_TIME_BETA_DECLARATIONS_PATH ? resolve(process.env.PREMIERE_BETA_TICK_TIME_BETA_DECLARATIONS_PATH) : resolve(root, "node_modules/@adobe/premierepro-beta/src/premierepro.d.ts");
|
||||
const outputPath = resolve(root, "src/resources/adobe-beta-tick-time-drift.json");
|
||||
const normalize = (text) => text.replaceAll("\r\n", "\n").replace(/\s+/g, " ").trim();
|
||||
const compare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const parse = (text, path) => { const source = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true); if (source.parseDiagnostics.length) throw new Error(`Adobe TickTime declaration parse failed: ${source.parseDiagnostics.map((item) => ts.flattenDiagnosticMessageText(item.messageText, " ")).join("; ")}`); return source; };
|
||||
const literal = (source, name) => { const declaration = source.statements.find((item) => ts.isTypeAliasDeclaration(item) && item.name.text === name); if (!declaration || !ts.isTypeLiteralNode(declaration.type)) throw new Error(`Adobe declarations must expose ${name} as a type literal.`); return declaration; };
|
||||
const binding = (rootDeclaration, source) => { const member = rootDeclaration.type.members.find((item) => ts.isPropertySignature(item) && item.name && ts.isIdentifier(item.name) && item.name.text === "TickTime"); if (!member?.type) throw new Error("Adobe declarations must expose premierepro.TickTime as a typed property."); return { symbol: "premierepro.TickTime", kind: "property", signature: normalize(member.type.getText(source)) }; };
|
||||
const factories = (declaration, source, owner) => declaration.type.members.filter((item) => ts.isConstructSignatureDeclaration(item) || ts.isCallSignatureDeclaration(item)).map((item) => { if (!item.type) throw new Error(`Adobe ${owner} factory signature is missing a return type.`); return { symbol: `${owner}.${ts.isConstructSignatureDeclaration(item) ? "new" : "call"}`, kind: ts.isConstructSignatureDeclaration(item) ? "construct_signature" : "call_signature", signature: `(${item.parameters.map((parameter) => normalize(parameter.getText(source))).join(", ")}) => ${normalize(item.type.getText(source))}` }; }).sort((left, right) => compare(left.symbol, right.symbol));
|
||||
const nonFactories = (declaration, source) => declaration.type.members.filter((item) => !ts.isConstructSignatureDeclaration(item) && !ts.isCallSignatureDeclaration(item)).map((item) => normalize(item.getText(source))).sort(compare);
|
||||
const [stablePackageText, betaPackageText, stableText, betaText] = await Promise.all([readFile(resolve(root, "node_modules/@adobe/premierepro/package.json"), "utf8"), readFile(resolve(root, "node_modules/@adobe/premierepro-beta/package.json"), "utf8"), readFile(stablePath, "utf8"), readFile(betaPath, "utf8")]);
|
||||
const stable = parse(stableText, stablePath), beta = parse(betaText, betaPath), stableRoot = literal(stable, "premierepro"), betaRoot = literal(beta, "premierepro"), stableTickTime = literal(stable, "TickTime"), betaTickTime = literal(beta, "TickTime"), stableStatic = literal(stable, "TickTimeStatic"), betaStatic = literal(beta, "TickTimeStatic"), stableBinding = binding(stableRoot, stable), betaBinding = binding(betaRoot, beta), stableFactories = factories(stableTickTime, stable, "TickTime"), betaFactories = factories(betaStatic, beta, "TickTimeStatic");
|
||||
if (stableBinding.signature !== "TickTimeStatic" || betaBinding.signature !== "TickTimeStatic") throw new Error("Adobe beta declarations must retain premierepro.TickTime as TickTimeStatic.");
|
||||
if (factories(stableStatic, stable, "TickTimeStatic").length || factories(betaTickTime, beta, "TickTime").length) throw new Error("Adobe beta TickTime factory signatures must move only from stable TickTime to beta TickTimeStatic.");
|
||||
const stableShapes = stableFactories.map(({ kind, signature }) => ({ kind, signature })), betaShapes = betaFactories.map(({ kind, signature }) => ({ kind, signature }));
|
||||
if (stableFactories.length !== 2 || betaFactories.length !== 2 || JSON.stringify(stableShapes) !== JSON.stringify(betaShapes)) throw new Error("Adobe beta TickTimeStatic factory signatures must match stable TickTime.");
|
||||
if (JSON.stringify(nonFactories(stableTickTime, stable)) !== JSON.stringify(nonFactories(betaTickTime, beta))) throw new Error("Adobe beta TickTime non-factory members must match stable TickTime.");
|
||||
if (JSON.stringify(nonFactories(stableStatic, stable)) !== JSON.stringify(nonFactories(betaStatic, beta))) throw new Error("Adobe beta TickTimeStatic non-factory members must match stable TickTimeStatic.");
|
||||
const hash = (declaration, source) => createHash("sha256").update(normalize(declaration.getText(source))).digest("hex");
|
||||
const inventory = { schemaVersion: 1, scope: { declarations: ["premierepro.TickTime", "TickTime", "TickTimeStatic"], semantics: "This records the beta TickTime factory-placement migration against the pinned stable package. It is static declaration-drift accounting, not beta API support or a complete package diff.", doesNotEstablish: "It does not prove a beta host exposes TickTimeStatic factory calls, that a TickTime can be constructed or accepted by another API, that TickTime arithmetic or frame alignment behavior is preserved, or that any MCP action is supported or licensed-host validated." }, sources: { stable: { package: "@adobe/premierepro", version: JSON.parse(stablePackageText).version, declarations: relative(root, stablePath).replaceAll("\\", "/"), tickTimeDeclarationSha256: hash(stableTickTime, stable), tickTimeStaticDeclarationSha256: hash(stableStatic, stable), rootBinding: stableBinding.signature }, beta: { package: "@adobe/premierepro-beta", version: JSON.parse(betaPackageText).version, declarations: relative(root, betaPath).replaceAll("\\", "/"), tickTimeDeclarationSha256: hash(betaTickTime, beta), tickTimeStaticDeclarationSha256: hash(betaStatic, beta), rootBinding: betaBinding.signature } }, diff: { betaOnly: betaFactories, stableOnly: stableFactories, changed: [{ symbol: "TickTime.factorySignaturePlacement", stable: { owner: "TickTime", entries: stableShapes }, beta: { owner: "TickTimeStatic", entries: betaShapes } }] } };
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
if (process.argv.includes("--validate-only")) console.log("Validated Adobe beta TickTime declaration drift.");
|
||||
else if (process.argv.includes("--check")) { let current = ""; try { current = await readFile(outputPath, "utf8"); } catch {} if (current.replaceAll("\r\n", "\n") !== rendered) { console.error("Adobe beta TickTime declaration drift inventory is stale. Run npm run adobe:beta-tick-time-drift."); process.exitCode = 1; } else console.log("Adobe beta TickTime declaration drift inventory is current: 2 beta-only, 2 stable-only, and 1 changed symbol."); }
|
||||
else { await writeFile(outputPath, rendered); console.log("Wrote Adobe beta TickTime declaration drift inventory: 2 beta-only, 2 stable-only, and 1 changed symbol."); }
|
||||
@@ -0,0 +1,143 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stablePackagePath = resolve(root, "node_modules/@adobe/premierepro/package.json");
|
||||
const betaPackagePath = resolve(root, "node_modules/@adobe/premierepro-beta/package.json");
|
||||
const stableDeclarationsPath = process.env.PREMIERE_BETA_TRANSCRIPT_STABLE_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_TRANSCRIPT_STABLE_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const betaDeclarationsPath = process.env.PREMIERE_BETA_TRANSCRIPT_BETA_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_TRANSCRIPT_BETA_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro-beta/src/premierepro.d.ts");
|
||||
const outputPath = process.env.PREMIERE_BETA_TRANSCRIPT_DRIFT_OUTPUT_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_TRANSCRIPT_DRIFT_OUTPUT_PATH)
|
||||
: resolve(root, "src/resources/adobe-beta-transcript-drift.json");
|
||||
const check = process.argv.includes("--check");
|
||||
const validateOnly = process.argv.includes("--validate-only");
|
||||
|
||||
const [stablePackageText, betaPackageText, stableDeclarations, betaDeclarations] = await Promise.all([
|
||||
readFile(stablePackagePath, "utf8"),
|
||||
readFile(betaPackagePath, "utf8"),
|
||||
readFile(stableDeclarationsPath, "utf8"),
|
||||
readFile(betaDeclarationsPath, "utf8"),
|
||||
]);
|
||||
|
||||
const stablePackage = JSON.parse(stablePackageText);
|
||||
const betaPackage = JSON.parse(betaPackageText);
|
||||
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const normalizedText = (text) => text.replaceAll("\r\n", "\n").replace(/\s+/g, " ").trim();
|
||||
|
||||
function sourceFile(declarations, declarationPath) {
|
||||
const source = ts.createSourceFile(declarationPath, declarations, ts.ScriptTarget.Latest, true);
|
||||
if (source.parseDiagnostics.length > 0) {
|
||||
const details = source.parseDiagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, " ")).join("; ");
|
||||
throw new Error(`Adobe TranscriptStatic declaration parse failed: ${details}`);
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
function transcriptDeclaration(source) {
|
||||
const declaration = source.statements.find((statement) => (
|
||||
ts.isTypeAliasDeclaration(statement) && statement.name.text === "TranscriptStatic"
|
||||
));
|
||||
if (!declaration || !ts.isTypeLiteralNode(declaration.type)) {
|
||||
throw new Error("Adobe declarations must expose TranscriptStatic as a type literal.");
|
||||
}
|
||||
return declaration;
|
||||
}
|
||||
|
||||
function members(declaration, source) {
|
||||
const entries = declaration.type.members.map((member) => {
|
||||
if (!member.name || !(
|
||||
ts.isIdentifier(member.name) || ts.isStringLiteral(member.name) || ts.isNumericLiteral(member.name)
|
||||
)) {
|
||||
throw new Error(`Adobe TranscriptStatic declaration has an unsupported member name in ${source.fileName}`);
|
||||
}
|
||||
if (!ts.isMethodSignature(member) || !member.type) {
|
||||
throw new Error(`Adobe TranscriptStatic.${member.name.text} must be a typed method signature.`);
|
||||
}
|
||||
return {
|
||||
symbol: `TranscriptStatic.${member.name.text}`,
|
||||
kind: "method",
|
||||
signature: `(${member.parameters.map((parameter) => normalizedText(parameter.getText(source))).join(", ")}) => ${normalizedText(member.type.getText(source))}`,
|
||||
};
|
||||
}).sort((left, right) => compareText(left.symbol, right.symbol));
|
||||
if (new Set(entries.map((entry) => entry.symbol)).size !== entries.length) {
|
||||
throw new Error("Adobe TranscriptStatic declarations must not contain duplicate symbols.");
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function declarationHash(declaration, source) {
|
||||
return createHash("sha256").update(normalizedText(declaration.getText(source))).digest("hex");
|
||||
}
|
||||
|
||||
const stableSource = sourceFile(stableDeclarations, stableDeclarationsPath);
|
||||
const betaSource = sourceFile(betaDeclarations, betaDeclarationsPath);
|
||||
const stableDeclaration = transcriptDeclaration(stableSource);
|
||||
const betaDeclaration = transcriptDeclaration(betaSource);
|
||||
const stableMembers = members(stableDeclaration, stableSource);
|
||||
const betaMembers = members(betaDeclaration, betaSource);
|
||||
const stableBySymbol = new Map(stableMembers.map((member) => [member.symbol, member]));
|
||||
const betaBySymbol = new Map(betaMembers.map((member) => [member.symbol, member]));
|
||||
const betaOnly = betaMembers.filter((member) => !stableBySymbol.has(member.symbol));
|
||||
const stableOnly = stableMembers.filter((member) => !betaBySymbol.has(member.symbol));
|
||||
const changed = stableMembers.flatMap((member) => {
|
||||
const betaMember = betaBySymbol.get(member.symbol);
|
||||
if (!betaMember || JSON.stringify(member) === JSON.stringify(betaMember)) return [];
|
||||
return [{ symbol: member.symbol, stable: member, beta: betaMember }];
|
||||
});
|
||||
|
||||
const inventory = {
|
||||
schemaVersion: 1,
|
||||
scope: {
|
||||
declaration: "TranscriptStatic",
|
||||
semantics: "This compares only the public TranscriptStatic declaration in the pinned stable and beta packages. It is static declaration-drift accounting, not beta API support or a complete package diff.",
|
||||
mutationBoundary: "transcribeClipProjectItem is a beta transcription-start operation. This receipt intentionally has no production call or user-facing transcription action.",
|
||||
doesNotEstablish: "It does not prove a beta host exposes either added member, that a language pack is installed or usable, that transcription starts or completes, that transcript content is safe to handle, or that any MCP action is supported or licensed-host validated.",
|
||||
},
|
||||
sources: {
|
||||
stable: {
|
||||
package: "@adobe/premierepro",
|
||||
version: stablePackage.version,
|
||||
declarations: relative(root, stableDeclarationsPath).replaceAll("\\", "/"),
|
||||
transcriptDeclarationSha256: declarationHash(stableDeclaration, stableSource),
|
||||
},
|
||||
beta: {
|
||||
package: "@adobe/premierepro-beta",
|
||||
version: betaPackage.version,
|
||||
declarations: relative(root, betaDeclarationsPath).replaceAll("\\", "/"),
|
||||
transcriptDeclarationSha256: declarationHash(betaDeclaration, betaSource),
|
||||
},
|
||||
},
|
||||
members: {
|
||||
stable: stableMembers,
|
||||
beta: betaMembers,
|
||||
},
|
||||
diff: {
|
||||
betaOnly,
|
||||
stableOnly,
|
||||
changed,
|
||||
},
|
||||
};
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
|
||||
if (validateOnly) {
|
||||
console.log(`Validated Adobe beta TranscriptStatic declaration drift: ${betaOnly.length} beta-only and ${changed.length} changed symbols.`);
|
||||
} else if (check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
if (current.replaceAll("\r\n", "\n") !== rendered) {
|
||||
console.error("Adobe beta TranscriptStatic declaration drift inventory is stale. Run npm run adobe:beta-transcript-drift.");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`Adobe beta TranscriptStatic declaration drift inventory is current: ${betaOnly.length} beta-only and ${changed.length} changed symbols.`);
|
||||
}
|
||||
} else {
|
||||
await writeFile(outputPath, rendered);
|
||||
console.log(`Wrote Adobe beta TranscriptStatic declaration drift inventory: ${betaOnly.length} beta-only and ${changed.length} changed symbols.`);
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stablePath = process.env.PREMIERE_BETA_TRANSITION_OPTIONS_STABLE_DECLARATIONS_PATH ? resolve(process.env.PREMIERE_BETA_TRANSITION_OPTIONS_STABLE_DECLARATIONS_PATH) : resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const betaPath = process.env.PREMIERE_BETA_TRANSITION_OPTIONS_BETA_DECLARATIONS_PATH ? resolve(process.env.PREMIERE_BETA_TRANSITION_OPTIONS_BETA_DECLARATIONS_PATH) : resolve(root, "node_modules/@adobe/premierepro-beta/src/premierepro.d.ts");
|
||||
const outputPath = resolve(root, "src/resources/adobe-beta-transition-options-drift.json");
|
||||
const normalize = (text) => text.replaceAll("\r\n", "\n").replace(/\s+/g, " ").trim();
|
||||
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
||||
const source = (text, path) => { const value = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true); if (value.parseDiagnostics.length) throw new Error(`Adobe AddTransitionOptions declaration parse failed: ${value.parseDiagnostics.map((item) => ts.flattenDiagnosticMessageText(item.messageText, " ")).join("; ")}`); return value; };
|
||||
const type = (file, name) => { const value = file.statements.find((item) => ts.isTypeAliasDeclaration(item) && item.name.text === name); if (!value || !ts.isTypeLiteralNode(value.type)) throw new Error(`Adobe declarations must expose ${name} as a type literal.`); return value; };
|
||||
const factories = (value, file, owner) => value.type.members.filter((item) => ts.isConstructSignatureDeclaration(item) || ts.isCallSignatureDeclaration(item)).map((item) => { if (!item.type) throw new Error(`Adobe ${owner} factory signature is missing a return type.`); return { symbol: `${owner}.${ts.isConstructSignatureDeclaration(item) ? "new" : "call"}`, kind: ts.isConstructSignatureDeclaration(item) ? "construct_signature" : "call_signature", signature: `(${item.parameters.map((parameter) => normalize(parameter.getText(file))).join(", ")}) => ${normalize(item.type.getText(file))}` }; }).sort((a, b) => compare(a.symbol, b.symbol));
|
||||
const nonFactories = (value, file) => value.type.members.filter((item) => !ts.isConstructSignatureDeclaration(item) && !ts.isCallSignatureDeclaration(item)).map((item) => normalize(item.getText(file))).sort(compare);
|
||||
const binding = (rootType, file) => { const member = rootType.type.members.find((item) => ts.isPropertySignature(item) && item.name && ts.isIdentifier(item.name) && item.name.text === "AddTransitionOptions"); if (!member?.type) throw new Error("Adobe declarations must expose premierepro.AddTransitionOptions as a typed property."); return { symbol: "premierepro.AddTransitionOptions", kind: "property", signature: normalize(member.type.getText(file)) }; };
|
||||
const [stablePackageText, betaPackageText, stableText, betaText] = await Promise.all([readFile(resolve(root, "node_modules/@adobe/premierepro/package.json"), "utf8"), readFile(resolve(root, "node_modules/@adobe/premierepro-beta/package.json"), "utf8"), readFile(stablePath, "utf8"), readFile(betaPath, "utf8")]);
|
||||
const stable = source(stableText, stablePath), beta = source(betaText, betaPath), stableRoot = type(stable, "premierepro"), betaRoot = type(beta, "premierepro"), stableOptions = type(stable, "AddTransitionOptions"), betaOptions = type(beta, "AddTransitionOptions"), betaStatic = type(beta, "AddTransitionOptionsStatic");
|
||||
if (stable.statements.some((item) => ts.isTypeAliasDeclaration(item) && item.name.text === "AddTransitionOptionsStatic")) throw new Error("Pinned stable declarations unexpectedly expose AddTransitionOptionsStatic.");
|
||||
const stableBinding = binding(stableRoot, stable), betaBinding = binding(betaRoot, beta);
|
||||
if (stableBinding.signature !== "AddTransitionOptions") throw new Error("Pinned stable declarations must expose premierepro.AddTransitionOptions as AddTransitionOptions.");
|
||||
if (betaBinding.signature !== "AddTransitionOptionsStatic") throw new Error("Adobe beta declarations must expose premierepro.AddTransitionOptions as AddTransitionOptionsStatic.");
|
||||
const stableFactories = factories(stableOptions, stable, "AddTransitionOptions"), betaFactories = factories(betaStatic, beta, "AddTransitionOptionsStatic"), stableShapes = stableFactories.map(({ kind, signature }) => ({ kind, signature })), betaShapes = betaFactories.map(({ kind, signature }) => ({ kind, signature }));
|
||||
if (stableFactories.length !== 2 || betaFactories.length !== 2 || JSON.stringify(stableShapes) !== JSON.stringify(betaShapes)) throw new Error("Adobe beta AddTransitionOptionsStatic factory signatures must match the pinned stable declaration.");
|
||||
if (JSON.stringify(nonFactories(stableOptions, stable)) !== JSON.stringify(nonFactories(betaOptions, beta))) throw new Error("Adobe beta AddTransitionOptions non-factory members must match the pinned stable declaration.");
|
||||
if (factories(betaOptions, beta, "AddTransitionOptions").length) throw new Error("Adobe beta AddTransitionOptions must not retain factory signatures after the static-type migration.");
|
||||
const hash = (value, file) => createHash("sha256").update(normalize(value.getText(file))).digest("hex");
|
||||
const inventory = { schemaVersion: 1, scope: { declarations: ["premierepro.AddTransitionOptions", "AddTransitionOptions", "AddTransitionOptionsStatic"], semantics: "This records the beta AddTransitionOptions factory-type migration against the pinned stable package. It is static declaration-drift accounting, not beta API support or a complete package diff.", mutationBoundary: "AddTransitionOptions configures transition application. This receipt intentionally does not construct options, create a transition action, or expose an MCP transition operation.", doesNotEstablish: "It does not prove a beta host exposes the static factory, that transition timing or alignment is accepted, that a transition is applied, or that any MCP action is supported or licensed-host validated." }, sources: { stable: { package: "@adobe/premierepro", version: JSON.parse(stablePackageText).version, declarations: relative(root, stablePath).replaceAll("\\", "/"), optionsDeclarationSha256: hash(stableOptions, stable), staticFactoryPresent: false }, beta: { package: "@adobe/premierepro-beta", version: JSON.parse(betaPackageText).version, declarations: relative(root, betaPath).replaceAll("\\", "/"), optionsDeclarationSha256: hash(betaOptions, beta), staticDeclarationSha256: hash(betaStatic, beta), staticFactoryPresent: true } }, diff: { betaOnly: [{ symbol: "AddTransitionOptionsStatic", kind: "type", signature: normalize(betaStatic.type.getText(beta)) }, ...betaFactories].sort((a, b) => compare(a.symbol, b.symbol)), stableOnly: [], changed: [{ symbol: "premierepro.AddTransitionOptions", stable: stableBinding, beta: betaBinding }, { symbol: "AddTransitionOptions.factorySignatures", stable: { owner: "AddTransitionOptions", entries: stableShapes }, beta: { owner: "AddTransitionOptionsStatic", entries: betaShapes } }] } };
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
if (process.argv.includes("--validate-only")) console.log("Validated Adobe beta AddTransitionOptions declaration drift.");
|
||||
else if (process.argv.includes("--check")) { let current = ""; try { current = await readFile(outputPath, "utf8"); } catch {} if (current.replaceAll("\r\n", "\n") !== rendered) { console.error("Adobe beta AddTransitionOptions declaration drift inventory is stale. Run npm run adobe:beta-transition-options-drift."); process.exitCode = 1; } else console.log("Adobe beta AddTransitionOptions declaration drift inventory is current: 3 beta-only and 2 changed symbols."); }
|
||||
else { await writeFile(outputPath, rendered); console.log("Wrote Adobe beta AddTransitionOptions declaration drift inventory: 3 beta-only and 2 changed symbols."); }
|
||||
@@ -0,0 +1,178 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stablePackagePath = resolve(root, "node_modules/@adobe/premierepro/package.json");
|
||||
const betaPackagePath = resolve(root, "node_modules/@adobe/premierepro-beta/package.json");
|
||||
const stableDeclarationsPath = process.env.PREMIERE_BETA_WORK_AREA_STABLE_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_WORK_AREA_STABLE_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro/src/premierepro.d.ts");
|
||||
const betaDeclarationsPath = process.env.PREMIERE_BETA_WORK_AREA_BETA_DECLARATIONS_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_WORK_AREA_BETA_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/premierepro-beta/src/premierepro.d.ts");
|
||||
const outputPath = process.env.PREMIERE_BETA_WORK_AREA_DRIFT_OUTPUT_PATH
|
||||
? resolve(process.env.PREMIERE_BETA_WORK_AREA_DRIFT_OUTPUT_PATH)
|
||||
: resolve(root, "src/resources/adobe-beta-work-area-drift.json");
|
||||
const check = process.argv.includes("--check");
|
||||
const validateOnly = process.argv.includes("--validate-only");
|
||||
|
||||
const [stablePackageText, betaPackageText, stableDeclarations, betaDeclarations] = await Promise.all([
|
||||
readFile(stablePackagePath, "utf8"),
|
||||
readFile(betaPackagePath, "utf8"),
|
||||
readFile(stableDeclarationsPath, "utf8"),
|
||||
readFile(betaDeclarationsPath, "utf8"),
|
||||
]);
|
||||
|
||||
const stablePackage = JSON.parse(stablePackageText);
|
||||
const betaPackage = JSON.parse(betaPackageText);
|
||||
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const normalizedText = (text) => text.replaceAll("\r\n", "\n").replace(/\s+/g, " ").trim();
|
||||
|
||||
function sourceFile(declarations, declarationPath) {
|
||||
const source = ts.createSourceFile(declarationPath, declarations, ts.ScriptTarget.Latest, true);
|
||||
if (source.parseDiagnostics.length > 0) {
|
||||
const details = source.parseDiagnostics.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, " ")).join("; ");
|
||||
throw new Error(`Adobe WorkAreaUtils declaration parse failed: ${details}`);
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
function typeLiteral(source, name) {
|
||||
const declaration = source.statements.find((statement) => (
|
||||
ts.isTypeAliasDeclaration(statement) && statement.name.text === name
|
||||
));
|
||||
if (!declaration || !ts.isTypeLiteralNode(declaration.type)) {
|
||||
throw new Error(`Adobe declarations must expose ${name} as a type literal.`);
|
||||
}
|
||||
return declaration;
|
||||
}
|
||||
|
||||
function nameOf(member, source, owner) {
|
||||
if (!member.name || !(
|
||||
ts.isIdentifier(member.name) || ts.isStringLiteral(member.name) || ts.isNumericLiteral(member.name)
|
||||
)) {
|
||||
throw new Error(`Adobe ${owner} declaration has an unsupported member name in ${source.fileName}`);
|
||||
}
|
||||
return member.name.text;
|
||||
}
|
||||
|
||||
function memberEntries(type, source, owner) {
|
||||
const entries = type.members.map((member) => {
|
||||
const name = nameOf(member, source, owner);
|
||||
if (ts.isMethodSignature(member)) {
|
||||
if (!member.type) throw new Error(`Adobe ${owner}.${name} is missing a return type.`);
|
||||
return {
|
||||
symbol: `${owner}.${name}`,
|
||||
kind: "method",
|
||||
signature: `(${member.parameters.map((parameter) => normalizedText(parameter.getText(source))).join(", ")}) => ${normalizedText(member.type.getText(source))}`,
|
||||
};
|
||||
}
|
||||
if (ts.isPropertySignature(member)) {
|
||||
if (!member.type) throw new Error(`Adobe ${owner}.${name} is missing a property type.`);
|
||||
const readonly = Boolean(ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Readonly);
|
||||
return {
|
||||
symbol: `${owner}.${name}`,
|
||||
kind: "property",
|
||||
...(readonly ? { readonly: true } : {}),
|
||||
signature: normalizedText(member.type.getText(source)),
|
||||
};
|
||||
}
|
||||
throw new Error(`Adobe ${owner}.${name} has an unsupported declaration kind.`);
|
||||
}).sort((left, right) => compareText(left.symbol, right.symbol));
|
||||
if (new Set(entries.map((entry) => entry.symbol)).size !== entries.length) {
|
||||
throw new Error(`Adobe ${owner} declarations must not contain duplicate symbols.`);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function hasTypeAlias(source, name) {
|
||||
return source.statements.some((statement) => ts.isTypeAliasDeclaration(statement) && statement.name.text === name);
|
||||
}
|
||||
|
||||
function declarationHash(declaration, source) {
|
||||
return createHash("sha256").update(normalizedText(declaration.getText(source))).digest("hex");
|
||||
}
|
||||
|
||||
const stableSource = sourceFile(stableDeclarations, stableDeclarationsPath);
|
||||
const betaSource = sourceFile(betaDeclarations, betaDeclarationsPath);
|
||||
const stableRoot = typeLiteral(stableSource, "premierepro");
|
||||
const stableRootEntries = memberEntries(stableRoot.type, stableSource, "premierepro");
|
||||
if (stableRootEntries.some((entry) => entry.symbol === "premierepro.WorkAreaUtils") ||
|
||||
hasTypeAlias(stableSource, "WorkAreaUtilsStatic") ||
|
||||
hasTypeAlias(stableSource, "WorkAreaUtils")) {
|
||||
throw new Error("Pinned stable declarations unexpectedly expose WorkAreaUtils.");
|
||||
}
|
||||
|
||||
const betaRoot = typeLiteral(betaSource, "premierepro");
|
||||
const rootBinding = memberEntries(betaRoot.type, betaSource, "premierepro")
|
||||
.find((entry) => entry.symbol === "premierepro.WorkAreaUtils");
|
||||
if (!rootBinding || rootBinding.kind !== "property" || rootBinding.signature !== "WorkAreaUtilsStatic") {
|
||||
throw new Error("Adobe beta declarations must expose premierepro.WorkAreaUtils as WorkAreaUtilsStatic.");
|
||||
}
|
||||
const workAreaStatic = typeLiteral(betaSource, "WorkAreaUtilsStatic");
|
||||
const workAreaEntries = memberEntries(workAreaStatic.type, betaSource, "WorkAreaUtilsStatic");
|
||||
const workAreaInstance = typeLiteral(betaSource, "WorkAreaUtils");
|
||||
if (workAreaInstance.type.members.length !== 0) {
|
||||
throw new Error("Adobe beta WorkAreaUtils instance declaration must remain empty for this receipt.");
|
||||
}
|
||||
const betaOnly = [
|
||||
rootBinding,
|
||||
{
|
||||
symbol: "WorkAreaUtils",
|
||||
kind: "type",
|
||||
signature: normalizedText(workAreaInstance.type.getText(betaSource)),
|
||||
},
|
||||
...workAreaEntries,
|
||||
].sort((left, right) => compareText(left.symbol, right.symbol));
|
||||
|
||||
const inventory = {
|
||||
schemaVersion: 1,
|
||||
scope: {
|
||||
declarations: ["premierepro.WorkAreaUtils", "WorkAreaUtilsStatic", "WorkAreaUtils"],
|
||||
semantics: "This records the WorkAreaUtils declaration surface that is absent from the pinned stable package and present in the pinned beta package. It is static declaration-drift accounting, not beta API support or a complete package diff.",
|
||||
doesNotEstablish: "It does not prove a beta host exposes WorkAreaUtils, that a stable host accepts a beta call, that any work-area mutation changes a sequence, or that an MCP action is supported or licensed-host validated.",
|
||||
existingToolBoundary: "Existing MCP work-area tools use established legacy host paths. This receipt neither changes those implementations nor establishes that they are equivalent to beta WorkAreaUtils behavior.",
|
||||
},
|
||||
sources: {
|
||||
stable: {
|
||||
package: "@adobe/premierepro",
|
||||
version: stablePackage.version,
|
||||
declarations: relative(root, stableDeclarationsPath).replaceAll("\\", "/"),
|
||||
workAreaSurfacePresent: false,
|
||||
},
|
||||
beta: {
|
||||
package: "@adobe/premierepro-beta",
|
||||
version: betaPackage.version,
|
||||
declarations: relative(root, betaDeclarationsPath).replaceAll("\\", "/"),
|
||||
workAreaSurfacePresent: true,
|
||||
rootDeclarationSha256: declarationHash(betaRoot, betaSource),
|
||||
workAreaStaticDeclarationSha256: declarationHash(workAreaStatic, betaSource),
|
||||
workAreaDeclarationSha256: declarationHash(workAreaInstance, betaSource),
|
||||
},
|
||||
},
|
||||
diff: {
|
||||
betaOnly,
|
||||
stableOnly: [],
|
||||
changed: [],
|
||||
},
|
||||
};
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
|
||||
if (validateOnly) {
|
||||
console.log(`Validated Adobe beta WorkAreaUtils declaration drift: ${betaOnly.length} beta-only symbols.`);
|
||||
} else if (check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
if (current.replaceAll("\r\n", "\n") !== rendered) {
|
||||
console.error("Adobe beta WorkAreaUtils declaration drift inventory is stale. Run npm run adobe:beta-work-area-drift.");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`Adobe beta WorkAreaUtils declaration drift inventory is current: ${betaOnly.length} beta-only symbols.`);
|
||||
}
|
||||
} else {
|
||||
await writeFile(outputPath, rendered);
|
||||
console.log(`Wrote Adobe beta WorkAreaUtils declaration drift inventory: ${betaOnly.length} beta-only symbols.`);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const sources = [
|
||||
{
|
||||
repository: "Adobe-CEP/CEP-Resources",
|
||||
commit: "ab5e4e3e53a42fad08e1225a22a991bb1ffe73f6",
|
||||
prefix: "",
|
||||
authority: "adobe",
|
||||
scope: "cep-platform",
|
||||
},
|
||||
{
|
||||
repository: "Adobe-CEP/Samples",
|
||||
commit: "e4946b73ac1e566dced8e95dba10811c31036927",
|
||||
prefix: "PProPanel/",
|
||||
authority: "adobe",
|
||||
scope: "premiere-extendscript-sample",
|
||||
},
|
||||
{
|
||||
repository: "docsforadobe/premiere-scripting-guide",
|
||||
commit: "4253cea094e84d43590b77012b33bd1c140f72ea",
|
||||
prefix: "",
|
||||
authority: "community",
|
||||
scope: "premiere-extendscript-guide",
|
||||
},
|
||||
];
|
||||
const outputPath = resolve(process.env.CEP_INVENTORY_OUTPUT_PATH ?? "src/resources/cep-reference-inventory.json");
|
||||
const check = process.argv.includes("--check");
|
||||
|
||||
function category(source, path) {
|
||||
const lower = path.toLowerCase();
|
||||
if (source.scope === "premiere-extendscript-sample") return "sample";
|
||||
if (source.scope === "premiere-extendscript-guide") {
|
||||
if (lower.endsWith(".md")) return "documentation";
|
||||
if (lower.includes("mkdocs") || lower.endsWith(".py") || lower.endsWith(".yml")) return "documentation-tooling";
|
||||
return "site-asset";
|
||||
}
|
||||
if (lower.includes("zxpsigncmd")) return "signing-tool";
|
||||
if (lower.includes("csinterface")) return "cep-runtime-library";
|
||||
if (lower.includes("documentation") || lower.endsWith(".md") || lower.endsWith(".pdf")) return "documentation";
|
||||
if (lower.includes("sample") || lower.includes("demo")) return "sample";
|
||||
if (/\.(js|jsx|ts|tsx|c|cc|cpp|cxx|h|hpp|java|cs)$/.test(lower)) return "source";
|
||||
if (/\.(json|xml|plist|yml|yaml|toml|ini|conf)$/.test(lower)) return "configuration";
|
||||
return "asset";
|
||||
}
|
||||
|
||||
async function repositoryTree(source) {
|
||||
const fixtureDirectory = process.env.CEP_INVENTORY_FIXTURE_DIRECTORY;
|
||||
if (fixtureDirectory) {
|
||||
const name = source.repository.replaceAll("/", "__");
|
||||
return JSON.parse(await readFile(resolve(fixtureDirectory, `${name}.json`), "utf8"));
|
||||
}
|
||||
const headers = { Accept: "application/vnd.github+json", "User-Agent": "premiere-pro-mcp-inventory" };
|
||||
if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
const response = await fetch(
|
||||
`https://api.github.com/repos/${source.repository}/git/trees/${source.commit}?recursive=1`,
|
||||
{ headers, signal: AbortSignal.timeout(30_000) },
|
||||
);
|
||||
if (!response.ok) throw new Error(`GitHub tree request failed for ${source.repository}: HTTP ${response.status}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const entries = [];
|
||||
for (const source of sources) {
|
||||
const tree = await repositoryTree(source);
|
||||
if (tree.truncated) throw new Error(`GitHub returned a truncated tree for ${source.repository}`);
|
||||
if (!Array.isArray(tree.tree)) throw new Error(`GitHub returned no tree for ${source.repository}`);
|
||||
const files = tree.tree.filter((entry) => entry.type === "blob" && entry.path.startsWith(source.prefix));
|
||||
if (files.length === 0) throw new Error(`No files matched ${source.repository}:${source.prefix}`);
|
||||
for (const file of files) {
|
||||
if (!/^[0-9a-f]{40}$/.test(file.sha) || !Number.isSafeInteger(file.size) || file.size < 0) {
|
||||
throw new Error(`Invalid Git blob metadata for ${source.repository}:${file.path}`);
|
||||
}
|
||||
entries.push({
|
||||
repository: source.repository,
|
||||
commit: source.commit,
|
||||
authority: source.authority,
|
||||
scope: source.scope,
|
||||
path: file.path,
|
||||
blobSha: file.sha,
|
||||
size: file.size,
|
||||
category: category(source, file.path),
|
||||
});
|
||||
}
|
||||
}
|
||||
entries.sort((left, right) => `${left.repository}/${left.path}`.localeCompare(`${right.repository}/${right.path}`));
|
||||
const keys = entries.map((entry) => `${entry.repository}:${entry.path}`);
|
||||
if (new Set(keys).size !== keys.length) throw new Error("CEP reference inventory contains duplicate repository paths");
|
||||
const counts = Object.fromEntries(sources.map((source) => [
|
||||
source.repository,
|
||||
entries.filter((entry) => entry.repository === source.repository).length,
|
||||
]));
|
||||
const inventory = {
|
||||
schemaVersion: 1,
|
||||
generatedFrom: "Pinned recursive Git trees; Adobe authority and community reference remain distinct.",
|
||||
sources: sources.map(({ prefix, ...source }) => ({ ...source, pathPrefix: prefix })),
|
||||
stats: { total: entries.length, byRepository: counts },
|
||||
entries,
|
||||
};
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
if (check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
if (current.replaceAll("\r\n", "\n") !== rendered) {
|
||||
console.error("CEP reference inventory is stale. Run npm run cep:reference-inventory.");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`CEP reference inventory is current: ${entries.length} files.`);
|
||||
}
|
||||
} else {
|
||||
await writeFile(outputPath, rendered);
|
||||
console.log(`Wrote ${entries.length} CEP and Premiere scripting reference files.`);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const repository = "docsforadobe/premiere-scripting-guide";
|
||||
const commit = "4253cea094e84d43590b77012b33bd1c140f72ea";
|
||||
const referencePath = resolve(process.env.EXTENDSCRIPT_REFERENCE_PATH ?? "src/resources/cep-reference-inventory.json");
|
||||
const outputPath = resolve(process.env.EXTENDSCRIPT_INVENTORY_OUTPUT_PATH ?? "src/resources/extendscript-api-inventory.json");
|
||||
const fixtureDirectory = process.env.EXTENDSCRIPT_INVENTORY_FIXTURE_DIRECTORY;
|
||||
const check = process.argv.includes("--check");
|
||||
|
||||
async function markdown(path) {
|
||||
if (fixtureDirectory) return readFile(resolve(fixtureDirectory, path), "utf8");
|
||||
const response = await fetch(`https://raw.githubusercontent.com/${repository}/${commit}/${path}`, {
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
if (!response.ok) throw new Error(`Guide request failed for ${path}: HTTP ${response.status}`);
|
||||
return response.text();
|
||||
}
|
||||
|
||||
function parsePage(path, source) {
|
||||
const lines = source.replaceAll("\r\n", "\n").split("\n");
|
||||
const title = lines.find((line) => line.startsWith("# "))?.slice(2).trim();
|
||||
if (!title || !/ object$/i.test(title)) return [];
|
||||
const objectName = title.replace(/ object$/i, "");
|
||||
let section = null;
|
||||
let sectionHasHeadings = false;
|
||||
const symbols = [];
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const line = lines[index];
|
||||
if (line === "## Attributes") { section = "attribute"; sectionHasHeadings = false; }
|
||||
else if (line === "## Methods") { section = "method"; sectionHasHeadings = false; }
|
||||
else if (line.startsWith("## ")) section = null;
|
||||
if (section && !sectionHasHeadings) {
|
||||
const tableMember = line.match(/^\|\s*`([^`]+)`\s*\|/);
|
||||
if (tableMember) {
|
||||
const member = tableMember[1];
|
||||
symbols.push({
|
||||
object: objectName,
|
||||
name: `${objectName}.${member}`,
|
||||
kind: section,
|
||||
signature: member,
|
||||
sourcePath: path,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!section || !line.startsWith("### ")) continue;
|
||||
sectionHasHeadings = true;
|
||||
const name = line.slice(4).trim();
|
||||
const signatureLine = lines.slice(index + 1).find((candidate) => candidate.trim() !== "");
|
||||
const match = signatureLine?.match(/^`([^`]+)`$/);
|
||||
if (!match) throw new Error(`Missing inline signature for ${path}:${name}`);
|
||||
symbols.push({ object: objectName, name, kind: section, signature: match[1], sourcePath: path });
|
||||
}
|
||||
return symbols;
|
||||
}
|
||||
|
||||
const reference = JSON.parse(await readFile(referencePath, "utf8"));
|
||||
const guideEntries = reference.entries.filter((entry) => entry.repository === repository);
|
||||
if (guideEntries.some((entry) => entry.commit !== commit || entry.scope !== "premiere-extendscript-guide")) {
|
||||
throw new Error("Scripting guide reference entries do not match the pinned commit and scope");
|
||||
}
|
||||
const paths = reference.entries
|
||||
.filter((entry) => entry.repository === repository
|
||||
&& entry.commit === commit
|
||||
&& entry.scope === "premiere-extendscript-guide"
|
||||
&& entry.path.startsWith("docs/")
|
||||
&& entry.path.endsWith(".md"))
|
||||
.map((entry) => entry.path)
|
||||
.sort();
|
||||
if (paths.length === 0) throw new Error("Pinned CEP reference inventory contains no scripting guide Markdown files");
|
||||
const symbols = [];
|
||||
for (const path of paths) symbols.push(...parsePage(path, await markdown(path)));
|
||||
symbols.sort((left, right) => `${left.object}:${left.kind}:${left.name}`.localeCompare(`${right.object}:${right.kind}:${right.name}`));
|
||||
if (symbols.length === 0) throw new Error("No ExtendScript symbols were parsed from the guide");
|
||||
const keys = symbols.map((symbol) => `${symbol.object}:${symbol.kind}:${symbol.name}`);
|
||||
if (new Set(keys).size !== keys.length) throw new Error("ExtendScript inventory contains duplicate object members");
|
||||
const objects = [...new Set(symbols.map((symbol) => symbol.object))].sort();
|
||||
const inventory = {
|
||||
schemaVersion: 1,
|
||||
source: { repository, commit, authority: "community", authorityNote: "Community-maintained guide; not Adobe API authority." },
|
||||
stats: {
|
||||
total: symbols.length,
|
||||
objects: objects.length,
|
||||
attributes: symbols.filter((symbol) => symbol.kind === "attribute").length,
|
||||
methods: symbols.filter((symbol) => symbol.kind === "method").length,
|
||||
},
|
||||
objects,
|
||||
symbols,
|
||||
};
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
if (check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
if (current.replaceAll("\r\n", "\n") !== rendered) {
|
||||
console.error("ExtendScript API inventory is stale. Run npm run extendscript:api-inventory.");
|
||||
process.exitCode = 1;
|
||||
} else console.log(`ExtendScript API inventory is current: ${symbols.length} symbols.`);
|
||||
} else {
|
||||
await writeFile(outputPath, rendered);
|
||||
console.log(`Wrote ${symbols.length} ExtendScript symbols across ${objects.length} objects.`);
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, readdir, realpath, stat, writeFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { relative, resolve, sep } from "node:path";
|
||||
import {
|
||||
compareNativeSdkPaths,
|
||||
hasNativeSdkFamily,
|
||||
NATIVE_SDK_FAMILIES,
|
||||
NATIVE_SDK_HEADER_INVENTORY_SCHEMA_VERSION,
|
||||
NATIVE_SDK_HEADER_INVENTORY_SEMANTICS,
|
||||
} from "./native-sdk-header-inventory-contract.mjs";
|
||||
|
||||
function inventoryError(message) {
|
||||
const error = new Error(message);
|
||||
error.code = "NATIVE_SDK_INVENTORY_INVALID";
|
||||
return error;
|
||||
}
|
||||
|
||||
function sha256(value) {
|
||||
return createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
function normalRelativePath(value, label) {
|
||||
if (typeof value !== "string" || !value || value.includes("\0")) {
|
||||
throw inventoryError(`${label} must be a non-empty relative path`);
|
||||
}
|
||||
const normalized = value.replaceAll("\\", "/");
|
||||
if (normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized) || normalized.split("/").includes("..")) {
|
||||
throw inventoryError(`${label} must stay relative to the SDK root`);
|
||||
}
|
||||
const segments = normalized.split("/");
|
||||
if (normalized !== "." && segments.some((segment) => segment === "" || segment === ".")) {
|
||||
throw inventoryError(`${label} must use a canonical relative path`);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function stringOption(value, label, maximum = 512) {
|
||||
if (typeof value !== "string" || !value.trim() || value.length > maximum || value.includes("\0")) {
|
||||
throw inventoryError(`${label} must be a non-empty string of at most ${maximum} characters`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function assertInside(root, candidate, label) {
|
||||
const value = resolve(candidate);
|
||||
if (value !== root && !value.startsWith(`${root}${sep}`)) {
|
||||
throw inventoryError(`${label} must stay inside the SDK root`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function requiredDirectory(path, label) {
|
||||
let value;
|
||||
try { value = await stat(path); } catch { throw inventoryError(`${label} does not exist`); }
|
||||
if (!value.isDirectory()) throw inventoryError(`${label} must be a directory`);
|
||||
}
|
||||
|
||||
async function requiredFile(path, label) {
|
||||
let value;
|
||||
try { value = await stat(path); } catch { throw inventoryError(`${label} does not exist`); }
|
||||
if (!value.isFile()) throw inventoryError(`${label} must be a file`);
|
||||
}
|
||||
|
||||
async function resolvedPathInside(root, candidate, label) {
|
||||
let resolvedPath;
|
||||
try { resolvedPath = await realpath(candidate); } catch { throw inventoryError(`${label} does not exist`); }
|
||||
return assertInside(root, resolvedPath, label);
|
||||
}
|
||||
|
||||
async function resolvedDirectoryInside(root, candidate, label) {
|
||||
const path = await resolvedPathInside(root, candidate, label);
|
||||
await requiredDirectory(path, label);
|
||||
return path;
|
||||
}
|
||||
|
||||
async function listHeaders(root, includeDirectories) {
|
||||
const headers = [];
|
||||
const visit = async (directory) => {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
entries.sort((left, right) => compareNativeSdkPaths(left.name, right.name));
|
||||
for (const entry of entries) {
|
||||
const candidate = resolve(directory, entry.name);
|
||||
if (entry.isSymbolicLink()) throw inventoryError(`SDK header inventory refuses symbolic link: ${relative(root, candidate)}`);
|
||||
const path = await resolvedPathInside(root, candidate, `SDK entry ${relative(root, candidate)}`);
|
||||
if (entry.isDirectory()) await visit(path);
|
||||
else if (entry.isFile() && /\.(h|hpp)$/i.test(entry.name)) {
|
||||
const content = await readFile(path);
|
||||
headers.push({
|
||||
path: relative(root, path).split(sep).join("/"),
|
||||
bytes: content.length,
|
||||
sha256: sha256(content),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const includeDirectory of includeDirectories) {
|
||||
const path = await resolvedDirectoryInside(root, resolve(root, includeDirectory), `include directory ${includeDirectory}`);
|
||||
await visit(path);
|
||||
}
|
||||
headers.sort((left, right) => compareNativeSdkPaths(left.path, right.path));
|
||||
if (headers.length === 0) throw inventoryError("No C/C++ headers were found in the selected SDK include directories");
|
||||
if (new Set(headers.map((header) => header.path)).size !== headers.length) {
|
||||
throw inventoryError("SDK include directories overlap and produced duplicate headers");
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export async function generateNativeSdkHeaderInventory(options) {
|
||||
const sdk = options?.sdk;
|
||||
const family = hasNativeSdkFamily(sdk) ? NATIVE_SDK_FAMILIES[sdk] : undefined;
|
||||
if (!family) throw inventoryError("sdk must be uxp-hybrid or premiere-prsdk");
|
||||
const sdkVersion = stringOption(options?.sdkVersion, "sdkVersion", 128);
|
||||
const suppliedSdkRoot = resolve(stringOption(options?.sdkRoot, "sdkRoot", 4096));
|
||||
const archivePath = resolve(stringOption(options?.archivePath, "archivePath", 4096));
|
||||
await requiredDirectory(suppliedSdkRoot, "sdkRoot");
|
||||
const sdkRoot = await realpath(suppliedSdkRoot);
|
||||
await requiredFile(archivePath, "archivePath");
|
||||
|
||||
const suppliedDirectories = options?.includeDirectories ?? [];
|
||||
if (!Array.isArray(suppliedDirectories) || suppliedDirectories.some((value) => typeof value !== "string")) {
|
||||
throw inventoryError("includeDirectories must be an array of relative paths");
|
||||
}
|
||||
const includeDirectories = family.includeDirectories ?? suppliedDirectories.map((value) => normalRelativePath(value, "include directory"));
|
||||
if (family.includeDirectories && suppliedDirectories.length > 0) {
|
||||
throw inventoryError(`${sdk} has fixed documented include directories; do not pass includeDirectories`);
|
||||
}
|
||||
if (includeDirectories.length === 0) {
|
||||
throw inventoryError("premiere-prsdk requires one or more explicit includeDirectories from the licensed SDK documentation");
|
||||
}
|
||||
if (new Set(includeDirectories).size !== includeDirectories.length) {
|
||||
throw inventoryError("includeDirectories must not contain duplicates");
|
||||
}
|
||||
const headers = await listHeaders(sdkRoot, includeDirectories);
|
||||
for (const requiredHeader of family.requiredHeaders) {
|
||||
if (!headers.some((header) => header.path === requiredHeader)) {
|
||||
throw inventoryError(`Missing required UXP Hybrid SDK header: ${requiredHeader}`);
|
||||
}
|
||||
}
|
||||
const archive = await readFile(archivePath);
|
||||
return {
|
||||
schemaVersion: NATIVE_SDK_HEADER_INVENTORY_SCHEMA_VERSION,
|
||||
source: {
|
||||
sdk,
|
||||
sdkVersion,
|
||||
authorityUrl: family.authorityUrl,
|
||||
archiveSha256: sha256(archive),
|
||||
inventoryScope: "header_files_only",
|
||||
includeDirectories,
|
||||
},
|
||||
semantics: NATIVE_SDK_HEADER_INVENTORY_SEMANTICS,
|
||||
stats: {
|
||||
headers: headers.length,
|
||||
bytes: headers.reduce((total, header) => total + header.bytes, 0),
|
||||
},
|
||||
headers,
|
||||
};
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const options = { includeDirectories: [] };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index];
|
||||
if (argument === "--check") options.check = true;
|
||||
else if (argument === "--validate-only") options.validateOnly = true;
|
||||
else if (["--sdk", "--sdk-version", "--sdk-root", "--archive", "--output", "--include-dir"].includes(argument)) {
|
||||
const value = argv[index + 1];
|
||||
if (value == null || value.startsWith("--")) throw inventoryError(`${argument} requires a value`);
|
||||
index += 1;
|
||||
if (argument === "--sdk") options.sdk = value;
|
||||
else if (argument === "--sdk-version") options.sdkVersion = value;
|
||||
else if (argument === "--sdk-root") options.sdkRoot = value;
|
||||
else if (argument === "--archive") options.archivePath = value;
|
||||
else if (argument === "--output") options.outputPath = value;
|
||||
else options.includeDirectories.push(value);
|
||||
} else {
|
||||
throw inventoryError(`Unknown argument: ${argument}`);
|
||||
}
|
||||
}
|
||||
if (options.check && options.validateOnly) throw inventoryError("--check and --validate-only cannot be combined");
|
||||
if (!options.validateOnly && !options.outputPath) throw inventoryError("--output is required unless --validate-only is used");
|
||||
return options;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
const inventory = await generateNativeSdkHeaderInventory(options);
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
if (options.validateOnly) {
|
||||
process.stdout.write(`Validated ${inventory.stats.headers} ${inventory.source.sdk} header files.\n`);
|
||||
return;
|
||||
}
|
||||
const outputPath = resolve(options.outputPath);
|
||||
if (options.check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
if (current.replaceAll("\r\n", "\n") !== rendered) {
|
||||
throw inventoryError("Native SDK header inventory is stale; rerun without --check after reviewing the licensed SDK artifact");
|
||||
}
|
||||
process.stdout.write(`Native SDK header inventory is current: ${inventory.stats.headers} ${inventory.source.sdk} header files.\n`);
|
||||
return;
|
||||
}
|
||||
await writeFile(outputPath, rendered);
|
||||
process.stdout.write(`Wrote ${inventory.stats.headers} ${inventory.source.sdk} header files.\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error.message}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const sitemapUrl = "https://developer.adobe.com/sitemap.xml";
|
||||
const outputPath = process.env.PREMIERE_DOC_INVENTORY_OUTPUT_PATH
|
||||
? resolve(process.env.PREMIERE_DOC_INVENTORY_OUTPUT_PATH)
|
||||
: resolve("src/resources/premiere-doc-inventory.json");
|
||||
const check = process.argv.includes("--check");
|
||||
const validateOnly = process.argv.includes("--validate-only");
|
||||
|
||||
async function loadSitemap() {
|
||||
if (process.env.PREMIERE_DOC_SITEMAP_PATH) {
|
||||
return readFile(resolve(process.env.PREMIERE_DOC_SITEMAP_PATH), "utf8");
|
||||
}
|
||||
const response = await fetch(sitemapUrl, { signal: AbortSignal.timeout(30_000) });
|
||||
if (!response.ok) throw new Error(`Adobe sitemap request failed: HTTP ${response.status}`);
|
||||
return response.text();
|
||||
}
|
||||
|
||||
function decodeXml(value) {
|
||||
const entities = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
||||
return value.replace(/&(amp|lt|gt|quot|apos);/g, (_match, entity) => entities[entity]);
|
||||
}
|
||||
|
||||
function classify(url) {
|
||||
const path = new URL(url).pathname;
|
||||
if (path.includes("/ppro-reference/")) return "premiere-dom";
|
||||
if (path.includes("/uxp-api/reference-js/")) return "uxp-javascript";
|
||||
if (path.includes("/uxp-api/reference-html/")) return "uxp-html";
|
||||
if (path.includes("/uxp-api/reference-css/")) return "uxp-css";
|
||||
if (path.includes("/uxp-api/reference-spectrum/")) return "spectrum-web-components";
|
||||
if (path.includes("/plugins/")) return "uxp-plugin-guides";
|
||||
return "premiere-uxp-supporting-docs";
|
||||
}
|
||||
|
||||
function isCalendarDate(value) {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
||||
const parsed = new Date(`${value}T00:00:00.000Z`);
|
||||
return !Number.isNaN(parsed.valueOf()) && parsed.toISOString().slice(0, 10) === value;
|
||||
}
|
||||
|
||||
function validateCommittedInventory(inventory) {
|
||||
if (inventory?.schemaVersion !== 1 || inventory?.source?.sitemapUrl !== sitemapUrl) {
|
||||
throw new Error("Premiere documentation inventory has an unsupported schema or source.");
|
||||
}
|
||||
if (!Array.isArray(inventory.pages) || inventory.pages.length === 0) {
|
||||
throw new Error("Premiere documentation inventory contains no pages.");
|
||||
}
|
||||
const counts = {};
|
||||
let previousUrl = "";
|
||||
for (const page of inventory.pages) {
|
||||
if (typeof page?.url !== "string" || !page.url.startsWith("https://developer.adobe.com/premiere-pro/uxp/")) {
|
||||
throw new Error("Premiere documentation inventory contains an invalid page URL.");
|
||||
}
|
||||
if (page.url <= previousUrl || typeof page.surface !== "string" || classify(page.url) !== page.surface) {
|
||||
throw new Error("Premiere documentation inventory pages are not uniquely sorted or classified.");
|
||||
}
|
||||
if (page.lastModified !== null && !isCalendarDate(page.lastModified)) {
|
||||
throw new Error(`Invalid Adobe sitemap lastmod for ${page.url}: ${page.lastModified}`);
|
||||
}
|
||||
previousUrl = page.url;
|
||||
counts[page.surface] = (counts[page.surface] ?? 0) + 1;
|
||||
}
|
||||
const orderedCounts = Object.fromEntries(Object.entries(counts).sort(([left], [right]) => left.localeCompare(right)));
|
||||
if (inventory.stats?.total !== inventory.pages.length || JSON.stringify(inventory.stats?.bySurface) !== JSON.stringify(orderedCounts)) {
|
||||
throw new Error("Premiere documentation inventory statistics are stale.");
|
||||
}
|
||||
}
|
||||
|
||||
if (check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
let inventory;
|
||||
let validationFailed = false;
|
||||
try {
|
||||
inventory = JSON.parse(current);
|
||||
validateCommittedInventory(inventory);
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
validationFailed = true;
|
||||
}
|
||||
const rendered = inventory ? `${JSON.stringify(inventory, null, 2)}\n` : "";
|
||||
if (validationFailed) {
|
||||
// The validation error above is the actionable result.
|
||||
} else if (current.replace(/\r\n?/g, "\n") !== rendered) {
|
||||
console.error("Premiere documentation inventory is stale. Run npm run premiere:docs-inventory.");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`Premiere documentation inventory is current: ${inventory.stats.total} pages.`);
|
||||
}
|
||||
} else {
|
||||
const sitemap = await loadSitemap();
|
||||
const urlBlocks = [...sitemap.matchAll(/<url>\s*([\s\S]*?)\s*<\/url>/g)].map((match) => match[1]);
|
||||
if (urlBlocks.length === 0) throw new Error("Adobe sitemap contained no URL entries");
|
||||
const pages = [];
|
||||
for (const block of urlBlocks) {
|
||||
const location = block.match(/<loc>([\s\S]*?)<\/loc>/)?.[1]?.trim();
|
||||
if (!location) throw new Error("Adobe sitemap URL entry has no location");
|
||||
const url = decodeXml(location);
|
||||
if (!url.startsWith("https://developer.adobe.com/premiere-pro/uxp/")) continue;
|
||||
const lastModified = block.match(/<lastmod>([\s\S]*?)<\/lastmod>/)?.[1]?.trim() ?? null;
|
||||
if (lastModified !== null && !isCalendarDate(lastModified)) {
|
||||
throw new Error(`Invalid Adobe sitemap lastmod for ${url}: ${lastModified}`);
|
||||
}
|
||||
pages.push({ url, surface: classify(url), lastModified });
|
||||
}
|
||||
pages.sort((left, right) => left.url.localeCompare(right.url));
|
||||
if (pages.length === 0) throw new Error("Adobe sitemap contained no Premiere UXP pages");
|
||||
if (new Set(pages.map((page) => page.url)).size !== pages.length) {
|
||||
throw new Error("Adobe sitemap contains duplicate Premiere UXP URLs");
|
||||
}
|
||||
const counts = Object.fromEntries(
|
||||
[...new Set(pages.map((page) => page.surface))].sort().map((surface) => [
|
||||
surface,
|
||||
pages.filter((page) => page.surface === surface).length,
|
||||
]),
|
||||
);
|
||||
const inventory = {
|
||||
schemaVersion: 1,
|
||||
source: { sitemapUrl },
|
||||
semantics: {
|
||||
listed: "The page appears in Adobe's live developer sitemap; this inventories documentation and does not prove API implementation or host behavior.",
|
||||
},
|
||||
stats: { total: pages.length, bySurface: counts },
|
||||
pages,
|
||||
};
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
|
||||
if (validateOnly) {
|
||||
console.log(`Validated ${pages.length} Adobe Premiere UXP documentation pages.`);
|
||||
} else {
|
||||
await writeFile(outputPath, rendered);
|
||||
console.log(`Wrote ${pages.length} Adobe Premiere UXP documentation pages.`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const OUTPUT_PATH = resolve("public-product-manifest.json");
|
||||
|
||||
async function readJson(relativePath) {
|
||||
return JSON.parse(await readFile(resolve(relativePath), "utf8"));
|
||||
}
|
||||
|
||||
export async function buildPublicProductManifest() {
|
||||
const [release, packageJson, registry] = await Promise.all([
|
||||
readJson("release-metadata.json"),
|
||||
readJson("package.json"),
|
||||
readJson("registry/server.json"),
|
||||
]);
|
||||
|
||||
return {
|
||||
schemaVersion: "premiere-pro-mcp.public-product.v1",
|
||||
generatedFrom: {
|
||||
releaseMetadata: "release-metadata.json",
|
||||
packageMetadata: "package.json",
|
||||
registryMetadata: "registry/server.json",
|
||||
},
|
||||
product: {
|
||||
name: "MCP for Adobe Premiere Pro",
|
||||
mcpName: packageJson.mcpName,
|
||||
npmPackage: packageJson.name,
|
||||
version: release.version,
|
||||
repository: "https://github.com/leancoderkavy/premiere-pro-mcp",
|
||||
homepage: packageJson.homepage,
|
||||
license: packageJson.license,
|
||||
localFirst: true,
|
||||
transport: registry.packages?.[0]?.transport?.type ?? "stdio",
|
||||
},
|
||||
compatibility: {
|
||||
node: packageJson.engines?.node,
|
||||
premiere: release.premiereVersions,
|
||||
uxpMinimumVersion: release.uxpMinimumVersion,
|
||||
operatingSystems: ["Windows", "macOS"],
|
||||
},
|
||||
capabilitySurface: {
|
||||
registeredCoreTools: release.coreTools,
|
||||
defaultProfileTools: release.defaultProfileTools,
|
||||
authenticatedUxpAdditions: release.uxpAdditionalTools,
|
||||
defaultProfileWithUxp: release.defaultProfileWithUxpTools,
|
||||
toolModules: release.toolModules,
|
||||
guidedWorkflows: release.guidedWorkflows,
|
||||
},
|
||||
workflows: [
|
||||
{
|
||||
id: "safe-project-intake",
|
||||
title: "Inspect and organize a project safely",
|
||||
documentation: "docs/ai-editorial-workflows.md",
|
||||
firstTools: ["get_project_info", "manage_project_context", "create_editorial_plan", "preview_editorial_plan"],
|
||||
mutationBoundary: "Planning is local and review-only; any later Premiere mutation has its own capability, confirmation, and readback contract.",
|
||||
},
|
||||
{
|
||||
id: "transcript-backed-rough-cut",
|
||||
title: "Build a transcript-backed rough-cut proposal",
|
||||
documentation: "docs/ai-editorial-workflows.md",
|
||||
firstTools: ["manage_project_context", "create_editorial_context_pack", "create_editorial_plan", "preview_editorial_plan"],
|
||||
mutationBoundary: "Context packs and plans never transcribe, call an AI provider, or remove timeline media.",
|
||||
},
|
||||
{
|
||||
id: "caption-review",
|
||||
title: "Import and structurally review captions",
|
||||
documentation: "docs/ai-editorial-workflows.md",
|
||||
firstTools: ["create_caption_track", "get_sequence_structure"],
|
||||
mutationBoundary: "Caption-track readback is structural acceptance only; playback, readability, and render verification remain separate.",
|
||||
},
|
||||
{
|
||||
id: "verified-delivery",
|
||||
title: "Prepare and verify a delivery",
|
||||
documentation: "docs/supported-actions.md",
|
||||
firstTools: ["export_sequence", "verify_export", "analyze_video_qc"],
|
||||
mutationBoundary: "An export request, file check, and quality review establish different evidence levels and do not publish a delivery.",
|
||||
},
|
||||
],
|
||||
verification: {
|
||||
automated: "Tests and generated catalogs prove package behavior, schemas, routing, bounds, and documented readback contracts.",
|
||||
host: "A real supported Premiere host is required to establish host behavior; this manifest contains no licensed-host claim.",
|
||||
playbackAndRender: "Structural readback does not establish playback, visual quality, audio quality, caption readability, or final render quality.",
|
||||
details: "docs/editorial-workflow-host-validation.md",
|
||||
},
|
||||
proofKit: {
|
||||
status: "runbook_and_redacted_template_only",
|
||||
runbook: "docs/workflow-proof-runbook.md",
|
||||
receiptTemplate: "docs/workflow-proof-receipt.template.json",
|
||||
video: null,
|
||||
note: "No walkthrough video or licensed-host receipt is claimed until a fixture-only run is recorded and reviewed.",
|
||||
},
|
||||
communityCoverage: {
|
||||
status: "independent_historical_reports",
|
||||
documentation: "docs/community-coverage.md",
|
||||
note: "External reports are historical user experiences, not current compatibility or support claims.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function render(manifest) {
|
||||
return `${JSON.stringify(manifest, null, 2)}\n`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const arguments_ = process.argv.slice(2);
|
||||
const checkOnly = arguments_.includes("--check");
|
||||
const unknown = arguments_.filter((argument) => argument !== "--check");
|
||||
if (unknown.length) throw new Error(`Unknown argument: ${unknown[0]}`);
|
||||
|
||||
const expected = render(await buildPublicProductManifest());
|
||||
if (checkOnly) {
|
||||
const current = await readFile(OUTPUT_PATH, "utf8").catch(() => "");
|
||||
// Git may materialize text files with CRLF on Windows while generators
|
||||
// deliberately emit LF. Compare content, not the checkout's line-ending
|
||||
// convention, so the release gate remains portable.
|
||||
if (current.replace(/\r\n?/g, "\n") !== expected) {
|
||||
throw new Error("public-product-manifest.json is stale. Run npm run product-manifest.");
|
||||
}
|
||||
console.log("Public product manifest is current.");
|
||||
return;
|
||||
}
|
||||
|
||||
await writeFile(OUTPUT_PATH, expected, "utf8");
|
||||
console.log(`Wrote ${OUTPUT_PATH}`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
||||
await main();
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { createServer } from "../dist/server.js";
|
||||
|
||||
const DEFAULT_CAPABILITIES = "inspect,edit,export,filesystem";
|
||||
const FULL_CAPABILITIES = `${DEFAULT_CAPABILITIES},unsafe-script`;
|
||||
const OUTPUT_PATH = resolve("docs/supported-actions.md");
|
||||
|
||||
const disabledTelemetry = {
|
||||
enabled: false,
|
||||
capture() {},
|
||||
async shutdown() {},
|
||||
};
|
||||
|
||||
const mockUxpBridge = {
|
||||
async request() {
|
||||
return {};
|
||||
},
|
||||
getState() {
|
||||
return { status: "connected", connected: true };
|
||||
},
|
||||
};
|
||||
|
||||
async function collectRegisteredTools(capabilities, includeUxp) {
|
||||
const previousCapabilities = process.env.PREMIERE_MCP_CAPABILITIES;
|
||||
process.env.PREMIERE_MCP_CAPABILITIES = capabilities;
|
||||
const server = createServer({}, {
|
||||
telemetry: disabledTelemetry,
|
||||
...(includeUxp ? { uxpBridge: mockUxpBridge } : {}),
|
||||
});
|
||||
const client = new Client({ name: "supported-actions-generator", version: "1.0.0" });
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
|
||||
try {
|
||||
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
|
||||
const tools = [];
|
||||
let cursor;
|
||||
do {
|
||||
const response = await client.listTools(cursor ? { cursor } : undefined);
|
||||
tools.push(...response.tools);
|
||||
cursor = response.nextCursor;
|
||||
} while (cursor);
|
||||
return tools.sort((left, right) => left.name.localeCompare(right.name));
|
||||
} finally {
|
||||
await client.close();
|
||||
await server.close();
|
||||
if (previousCapabilities === undefined) delete process.env.PREMIERE_MCP_CAPABILITIES;
|
||||
else process.env.PREMIERE_MCP_CAPABILITIES = previousCapabilities;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeCell(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.replaceAll("|", "\\|");
|
||||
}
|
||||
|
||||
function code(value) {
|
||||
return `\`${String(value).replaceAll("`", "\\`")}\``;
|
||||
}
|
||||
|
||||
function renderModes(tool) {
|
||||
const properties = tool.inputSchema?.properties ?? {};
|
||||
const actionValues = properties.action?.enum;
|
||||
if (Array.isArray(actionValues) && actionValues.length > 0) {
|
||||
return actionValues.map(code).join(", ");
|
||||
}
|
||||
|
||||
const modeGroups = Object.entries(properties)
|
||||
.filter(([, property]) => Array.isArray(property?.enum) && property.enum.length > 0)
|
||||
.map(([name, property]) => `${code(name)}: ${property.enum.map(code).join(", ")}`);
|
||||
return modeGroups.length > 0 ? modeGroups.join("; ") : "Single operation";
|
||||
}
|
||||
|
||||
function renderToolRows(tools, availability) {
|
||||
return tools.map((tool) => (
|
||||
`| ${code(tool.name)} | ${availability} | ${renderModes(tool)} | ${escapeCell(tool.description)} |`
|
||||
)).join("\n");
|
||||
}
|
||||
|
||||
export async function generateSupportedActionsMarkdown() {
|
||||
const defaultCore = await collectRegisteredTools(DEFAULT_CAPABILITIES, false);
|
||||
const fullCore = await collectRegisteredTools(FULL_CAPABILITIES, false);
|
||||
const connectedDefault = await collectRegisteredTools(DEFAULT_CAPABILITIES, true);
|
||||
const defaultNames = new Set(defaultCore.map((tool) => tool.name));
|
||||
const fullNames = new Set(fullCore.map((tool) => tool.name));
|
||||
const restrictedCore = fullCore.filter((tool) => !defaultNames.has(tool.name));
|
||||
const uxpTools = connectedDefault.filter((tool) => !defaultNames.has(tool.name));
|
||||
|
||||
if (uxpTools.some((tool) => fullNames.has(tool.name))) {
|
||||
throw new Error("The UXP additions overlap the registered core tool names.");
|
||||
}
|
||||
|
||||
return `# Supported actions catalog
|
||||
|
||||
<!-- Generated by \`npm run docs:supported-actions\`. Do not edit the tables manually. -->
|
||||
|
||||
This is the complete source-derived public action catalog for the current repository.
|
||||
The generator reads the same MCP registration surface used by clients, so tool names,
|
||||
descriptions, action enums, authority visibility, and counts stay aligned with the code.
|
||||
Release metadata and distributed-artifact claims remain versioned separately; this
|
||||
source catalog may include unreleased actions.
|
||||
|
||||
| Surface | Count | Availability |
|
||||
| --- | ---: | --- |
|
||||
| Registered core actions | ${fullCore.length} | CEP/local server catalog; host and authority checks still apply |
|
||||
| Default-profile core actions | ${defaultCore.length} | Advertised with \`${DEFAULT_CAPABILITIES}\` |
|
||||
| Restricted core actions | ${restrictedCore.length} | Require explicit \`unsafe-script\` authority |
|
||||
| Authenticated UXP additions | ${uxpTools.length} | Advertised only while a compatible authenticated UXP panel is connected |
|
||||
| Default profile with UXP | ${connectedDefault.length} | ${defaultCore.length} core plus ${uxpTools.length} UXP tools |
|
||||
|
||||
## How to read support
|
||||
|
||||
- \`tools/list\` is authoritative for what the current MCP session may call.
|
||||
- \`get_capabilities\` reports the full registered catalog, authority decisions, backend
|
||||
eligibility, and any live-host verification still required.
|
||||
- A listed tool is not proof that a particular Premiere installation supports every host
|
||||
API. The authenticated UXP capability handshake and per-call preflight remain authoritative.
|
||||
- CEP remains the compatibility backend. A failed UXP mutation is never automatically
|
||||
replayed through CEP or the undocumented QE DOM.
|
||||
- Automated tests establish schemas, routing, bounds, transactions, and readback contracts;
|
||||
they do not replace validation in a real Premiere host.
|
||||
|
||||
## Core actions
|
||||
|
||||
Each core tool is one callable MCP action. “Actions or modes” records a top-level
|
||||
\`action\` enum when present, otherwise other top-level enum selectors, or “Single
|
||||
operation” when the tool has no enum-based mode.
|
||||
|
||||
| MCP tool | Availability | Actions or modes | Description |
|
||||
| --- | --- | --- | --- |
|
||||
${renderToolRows(defaultCore, "Default profile")}
|
||||
${renderToolRows(restrictedCore, `Requires ${code("unsafe-script")}`)}
|
||||
|
||||
## Authenticated UXP actions
|
||||
|
||||
These tools are additive. They appear only while the local loopback UXP bridge is
|
||||
authenticated and the connected host advertises the required command capabilities.
|
||||
|
||||
| MCP tool | Availability | Actions or modes | Description |
|
||||
| --- | --- | --- | --- |
|
||||
${renderToolRows(uxpTools, "Connected UXP")}
|
||||
|
||||
## Maintenance
|
||||
|
||||
Run \`npm run docs:supported-actions\` after changing tool registration or action enums.
|
||||
\`npm run check\` fails when this generated page no longer matches the registered surface.
|
||||
`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const checkOnly = process.argv.slice(2).includes("--check");
|
||||
const unknown = process.argv.slice(2).filter((argument) => argument !== "--check");
|
||||
if (unknown.length > 0) throw new Error(`Unknown argument: ${unknown[0]}`);
|
||||
const markdown = await generateSupportedActionsMarkdown();
|
||||
if (checkOnly) {
|
||||
const current = await readFile(OUTPUT_PATH, "utf8").catch(() => "");
|
||||
if (current.replaceAll("\r\n", "\n") !== markdown) {
|
||||
throw new Error("docs/supported-actions.md is stale. Run npm run docs:supported-actions.");
|
||||
}
|
||||
process.stdout.write("Supported actions catalog is current.\n");
|
||||
return;
|
||||
}
|
||||
await writeFile(OUTPUT_PATH, markdown, "utf8");
|
||||
process.stdout.write(`Wrote ${OUTPUT_PATH}\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { lstat, readFile, realpath, writeFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { relative, resolve, sep } from "node:path";
|
||||
import {
|
||||
UXP_HYBRID_ADDON_AUTHORITY_URL,
|
||||
UXP_HYBRID_ADDON_ENTRYPOINT_PATH,
|
||||
UXP_HYBRID_ADDON_RECEIPT_SCHEMA_VERSION,
|
||||
UXP_HYBRID_ADDON_RECEIPT_SEMANTICS,
|
||||
UXP_HYBRID_ADDON_TARGETS,
|
||||
} from "./uxp-hybrid-addon-receipt-contract.mjs";
|
||||
import {
|
||||
canonicalNativeSdkHeaderInventorySha256,
|
||||
verifyNativeSdkHeaderInventory,
|
||||
} from "./verify-native-sdk-header-inventory.mjs";
|
||||
import { verifyUxpHybridAddonReceipt } from "./verify-uxp-hybrid-addon-receipt.mjs";
|
||||
|
||||
const MAX_ARTIFACT_BYTES = 2 ** 31;
|
||||
const MAX_ENTRYPOINT_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
function receiptError(message) {
|
||||
const error = new Error(message);
|
||||
error.code = "UXP_HYBRID_ADDON_RECEIPT_INVALID";
|
||||
return error;
|
||||
}
|
||||
|
||||
function stringOption(value, label, maximum = 4096) {
|
||||
if (typeof value !== "string" || !value.trim() || value.length > maximum || value.includes("\0")) {
|
||||
throw receiptError(`${label} must be a non-empty string of at most ${maximum} characters`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function assertInside(root, candidate, label) {
|
||||
const value = resolve(candidate);
|
||||
if (value !== root && !value.startsWith(`${root}${sep}`)) throw receiptError(`${label} must stay inside the plugin root`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function addonName(value) {
|
||||
if (typeof value !== "string" || !/^[A-Za-z0-9._-]+\.uxpaddon$/.test(value)) {
|
||||
throw receiptError("manifest addon.name must be a simple .uxpaddon filename");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function requiredPluginFile(root, candidate, label) {
|
||||
const requestedPath = assertInside(root, candidate, label);
|
||||
let requestedStat;
|
||||
try { requestedStat = await lstat(requestedPath); } catch { throw receiptError(`${label} does not exist`); }
|
||||
if (requestedStat.isSymbolicLink()) throw receiptError(`${label} must not be a symbolic link`);
|
||||
if (!requestedStat.isFile()) throw receiptError(`${label} must be a file`);
|
||||
let canonicalPath;
|
||||
try { canonicalPath = await realpath(requestedPath); } catch { throw receiptError(`${label} does not exist`); }
|
||||
assertInside(root, canonicalPath, label);
|
||||
return { path: canonicalPath, size: requestedStat.size };
|
||||
}
|
||||
|
||||
async function sha256File(path) {
|
||||
const hash = createHash("sha256");
|
||||
await new Promise((resolveHash, rejectHash) => {
|
||||
const stream = createReadStream(path);
|
||||
stream.on("data", (chunk) => hash.update(chunk));
|
||||
stream.on("error", rejectHash);
|
||||
stream.on("end", resolveHash);
|
||||
});
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
async function readDevelopmentManifest(root) {
|
||||
const file = await requiredPluginFile(root, resolve(root, "manifest.json"), "manifest.json");
|
||||
if (file.size > 1024 * 1024) throw receiptError("manifest.json is too large");
|
||||
let manifest;
|
||||
try { manifest = JSON.parse(await readFile(file.path, "utf8")); } catch { throw receiptError("manifest.json must be readable JSON"); }
|
||||
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) throw receiptError("manifest.json must be an object");
|
||||
if (!Number.isInteger(manifest.manifestVersion) || manifest.manifestVersion < 6) {
|
||||
throw receiptError("manifest.json must declare manifestVersion 6 or newer");
|
||||
}
|
||||
if (manifest.host?.app !== "premierepro") throw receiptError("manifest.json host.app must be premierepro");
|
||||
if (!/^\d+\.\d+\.\d+$/.test(String(manifest.host?.minVersion || ""))) {
|
||||
throw receiptError("manifest.json host.minVersion must be a semantic version");
|
||||
}
|
||||
const name = addonName(manifest.addon?.name);
|
||||
if (manifest.requiredPermissions?.enableAddon !== true) throw receiptError("manifest.json requiredPermissions.enableAddon must be true");
|
||||
return {
|
||||
manifestVersion: manifest.manifestVersion,
|
||||
hostApp: manifest.host.app,
|
||||
hostMinVersion: manifest.host.minVersion,
|
||||
addonName: name,
|
||||
enableAddon: true,
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateUxpHybridAddonReceipt(options) {
|
||||
const suppliedPluginRoot = resolve(stringOption(options?.pluginRoot, "pluginRoot"));
|
||||
let pluginRoot;
|
||||
try { pluginRoot = await realpath(suppliedPluginRoot); } catch { throw receiptError("pluginRoot does not exist"); }
|
||||
let rootStat;
|
||||
try { rootStat = await lstat(pluginRoot); } catch { throw receiptError("pluginRoot does not exist"); }
|
||||
if (!rootStat.isDirectory()) throw receiptError("pluginRoot must be a directory");
|
||||
|
||||
if (!options?.sdkHeaderReceipt || typeof options.sdkHeaderReceipt !== "object") {
|
||||
throw receiptError("sdkHeaderReceipt is required");
|
||||
}
|
||||
const sdkSummary = verifyNativeSdkHeaderInventory(options.sdkHeaderReceipt);
|
||||
if (sdkSummary.sdk !== "uxp-hybrid") throw receiptError("sdkHeaderReceipt must identify uxp-hybrid");
|
||||
|
||||
const manifest = await readDevelopmentManifest(pluginRoot);
|
||||
const entrypointFile = await requiredPluginFile(pluginRoot, resolve(pluginRoot, UXP_HYBRID_ADDON_ENTRYPOINT_PATH), UXP_HYBRID_ADDON_ENTRYPOINT_PATH);
|
||||
if (!Number.isSafeInteger(entrypointFile.size) || entrypointFile.size <= 0 || entrypointFile.size > MAX_ENTRYPOINT_BYTES) {
|
||||
throw receiptError(`${UXP_HYBRID_ADDON_ENTRYPOINT_PATH} must be a non-empty file no larger than ${MAX_ENTRYPOINT_BYTES} bytes`);
|
||||
}
|
||||
const entrypoint = {
|
||||
path: UXP_HYBRID_ADDON_ENTRYPOINT_PATH,
|
||||
bytes: entrypointFile.size,
|
||||
sha256: await sha256File(entrypointFile.path),
|
||||
};
|
||||
const artifacts = [];
|
||||
for (const target of UXP_HYBRID_ADDON_TARGETS) {
|
||||
const relativePath = `${target.pathPrefix}/${manifest.addonName}`;
|
||||
const file = await requiredPluginFile(pluginRoot, resolve(pluginRoot, relativePath), `addon artifact ${target.target}`);
|
||||
if (!Number.isSafeInteger(file.size) || file.size <= 0 || file.size > MAX_ARTIFACT_BYTES) {
|
||||
throw receiptError(`addon artifact ${target.target} must be a non-empty file no larger than ${MAX_ARTIFACT_BYTES} bytes`);
|
||||
}
|
||||
artifacts.push({ target: target.target, path: relativePath, bytes: file.size, sha256: await sha256File(file.path) });
|
||||
}
|
||||
|
||||
const receipt = {
|
||||
schemaVersion: UXP_HYBRID_ADDON_RECEIPT_SCHEMA_VERSION,
|
||||
source: {
|
||||
sdk: "uxp-hybrid",
|
||||
sdkVersion: options.sdkHeaderReceipt.source.sdkVersion,
|
||||
sdkHeaderReceiptSha256: canonicalNativeSdkHeaderInventorySha256(options.sdkHeaderReceipt),
|
||||
authorityUrl: UXP_HYBRID_ADDON_AUTHORITY_URL,
|
||||
},
|
||||
manifest,
|
||||
entrypoint,
|
||||
semantics: UXP_HYBRID_ADDON_RECEIPT_SEMANTICS,
|
||||
stats: {
|
||||
artifacts: artifacts.length,
|
||||
addonBytes: artifacts.reduce((total, artifact) => total + artifact.bytes, 0),
|
||||
entrypoints: 1,
|
||||
entrypointBytes: entrypoint.bytes,
|
||||
},
|
||||
artifacts,
|
||||
};
|
||||
verifyUxpHybridAddonReceipt(receipt, { sdkHeaderReceipt: options.sdkHeaderReceipt });
|
||||
return receipt;
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const options = {};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index];
|
||||
if (["--plugin-root", "--sdk-header-receipt", "--output"].includes(argument)) {
|
||||
const value = argv[++index];
|
||||
if (!value || value.startsWith("--")) throw receiptError(`${argument} requires a value`);
|
||||
if (argument === "--plugin-root") options.pluginRoot = value;
|
||||
else if (argument === "--sdk-header-receipt") options.sdkHeaderReceiptPath = value;
|
||||
else options.outputPath = value;
|
||||
} else if (argument === "--check") options.check = true;
|
||||
else if (argument === "--validate-only") options.validateOnly = true;
|
||||
else throw receiptError(`Unknown argument: ${argument}`);
|
||||
}
|
||||
if (options.check && options.validateOnly) throw receiptError("--check and --validate-only cannot be combined");
|
||||
if (!options.pluginRoot || !options.sdkHeaderReceiptPath) throw receiptError("--plugin-root and --sdk-header-receipt are required");
|
||||
if (!options.validateOnly && !options.outputPath) throw receiptError("--output is required unless --validate-only is used");
|
||||
return options;
|
||||
}
|
||||
|
||||
async function readSdkHeaderReceipt(path) {
|
||||
try { return JSON.parse(await readFile(resolve(path), "utf8")); } catch { throw receiptError("sdkHeaderReceipt must be a readable JSON receipt"); }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
const receipt = await generateUxpHybridAddonReceipt({
|
||||
pluginRoot: options.pluginRoot,
|
||||
sdkHeaderReceipt: await readSdkHeaderReceipt(options.sdkHeaderReceiptPath),
|
||||
});
|
||||
const rendered = `${JSON.stringify(receipt, null, 2)}\n`;
|
||||
if (options.validateOnly) {
|
||||
process.stdout.write(`Validated ${receipt.stats.artifacts} UXP Hybrid addon artifacts and ${receipt.stats.entrypoints} entrypoint.\n`);
|
||||
return;
|
||||
}
|
||||
const outputPath = resolve(options.outputPath);
|
||||
if (options.check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
if (current.replaceAll("\r\n", "\n") !== rendered) {
|
||||
throw receiptError("UXP Hybrid addon receipt is stale; rerun without --check after reviewing the local development bundle");
|
||||
}
|
||||
process.stdout.write(`UXP Hybrid addon receipt is current: ${receipt.stats.artifacts} artifacts and ${receipt.stats.entrypoints} entrypoint.\n`);
|
||||
return;
|
||||
}
|
||||
await writeFile(outputPath, rendered);
|
||||
process.stdout.write(`Wrote ${receipt.stats.artifacts} UXP Hybrid addon artifacts and ${receipt.stats.entrypoints} entrypoint.\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
import { buildUxpHybridCcxReceipt } from "./uxp-hybrid-ccx-receipt-core.mjs";
|
||||
|
||||
function receiptError(message) {
|
||||
const error = new Error(message);
|
||||
error.code = "UXP_HYBRID_CCX_RECEIPT_INVALID";
|
||||
return error;
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const options = {};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index];
|
||||
if (["--ccx", "--addon-receipt", "--sdk-header-receipt", "--output"].includes(argument)) {
|
||||
const value = argv[++index];
|
||||
if (!value || value.startsWith("--")) throw receiptError(`${argument} requires a value`);
|
||||
if (argument === "--ccx") options.ccxPath = value;
|
||||
else if (argument === "--addon-receipt") options.addonReceiptPath = value;
|
||||
else if (argument === "--sdk-header-receipt") options.sdkHeaderReceiptPath = value;
|
||||
else options.outputPath = value;
|
||||
} else if (argument === "--check") options.check = true;
|
||||
else if (argument === "--validate-only") options.validateOnly = true;
|
||||
else throw receiptError(`Unknown argument: ${argument}`);
|
||||
}
|
||||
if (options.check && options.validateOnly) throw receiptError("--check and --validate-only cannot be combined");
|
||||
if (!options.ccxPath || !options.addonReceiptPath || !options.sdkHeaderReceiptPath) {
|
||||
throw receiptError("--ccx, --addon-receipt, and --sdk-header-receipt are required");
|
||||
}
|
||||
if (!options.validateOnly && !options.outputPath) throw receiptError("--output is required unless --validate-only is used");
|
||||
return options;
|
||||
}
|
||||
|
||||
async function readJson(path, label) {
|
||||
try { return JSON.parse(await readFile(resolve(path), "utf8")); } catch { throw receiptError(`${label} must be a readable JSON receipt`); }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
const [addonReceipt, sdkHeaderReceipt] = await Promise.all([
|
||||
readJson(options.addonReceiptPath, "addonReceipt"),
|
||||
readJson(options.sdkHeaderReceiptPath, "sdkHeaderReceipt"),
|
||||
]);
|
||||
const receipt = await buildUxpHybridCcxReceipt({ ccxPath: resolve(options.ccxPath), addonReceipt, sdkHeaderReceipt });
|
||||
const rendered = `${JSON.stringify(receipt, null, 2)}\n`;
|
||||
if (options.validateOnly) {
|
||||
process.stdout.write(`Validated CCX archive with ${receipt.stats.artifacts} addon artifacts and ${receipt.stats.entrypoints} entrypoint.\n`);
|
||||
return;
|
||||
}
|
||||
const outputPath = resolve(options.outputPath);
|
||||
if (options.check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
if (current.replaceAll("\r\n", "\n") !== rendered) {
|
||||
throw receiptError("UXP Hybrid CCX receipt is stale; rerun without --check after reviewing the local archive");
|
||||
}
|
||||
process.stdout.write(`UXP Hybrid CCX receipt is current: ${receipt.stats.artifacts} artifacts and ${receipt.stats.entrypoints} entrypoint.\n`);
|
||||
return;
|
||||
}
|
||||
await writeFile(outputPath, rendered);
|
||||
process.stdout.write(`Wrote UXP Hybrid CCX receipt: ${receipt.stats.artifacts} artifacts and ${receipt.stats.entrypoints} entrypoint.\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const packagePath = resolve(root, "node_modules/@adobe/cc-ext-uxp-types/package.json");
|
||||
const declarationsPath = process.env.UXP_JS_DECLARATIONS_PATH
|
||||
? resolve(process.env.UXP_JS_DECLARATIONS_PATH)
|
||||
: resolve(root, "node_modules/@adobe/cc-ext-uxp-types/uxp/index.d.ts");
|
||||
const coveragePath = resolve(root, "src/resources/uxp-js-coverage.json");
|
||||
const outputPath = resolve(root, "src/resources/uxp-js-api-inventory.json");
|
||||
const check = process.argv.includes("--check");
|
||||
const validateOnly = process.argv.includes("--validate-only");
|
||||
|
||||
const [packageText, declarationsText, coverageText] = await Promise.all([
|
||||
readFile(packagePath, "utf8"),
|
||||
readFile(declarationsPath, "utf8"),
|
||||
readFile(coveragePath, "utf8"),
|
||||
]);
|
||||
const packageMetadata = JSON.parse(packageText);
|
||||
const coverage = JSON.parse(coverageText);
|
||||
const mappedSymbols = new Set(coverage.entries.flatMap((entry) => entry.uxpApi));
|
||||
const source = ts.createSourceFile(declarationsPath, declarationsText, ts.ScriptTarget.Latest, true);
|
||||
if (source.parseDiagnostics.length > 0) {
|
||||
const details = source.parseDiagnostics
|
||||
.map((diagnostic) => ts.flattenDiagnosticMessageText(diagnostic.messageText, " "))
|
||||
.join("; ");
|
||||
throw new Error(`TypeScript declaration parse failed: ${details}`);
|
||||
}
|
||||
|
||||
const normalizedDeclarations = declarationsText.replaceAll("\r\n", "\n");
|
||||
const declarationsSha256 = createHash("sha256").update(normalizedDeclarations).digest("hex");
|
||||
const symbols = [];
|
||||
const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
||||
const cleanName = (node) => node.getText(source).replaceAll('"', "").replaceAll("'", "");
|
||||
const joinName = (owner, name) => owner ? `${owner}.${name}` : name;
|
||||
|
||||
function add(symbol, kind) {
|
||||
symbols.push({ symbol, kind });
|
||||
}
|
||||
|
||||
function memberName(member) {
|
||||
if (ts.isConstructorDeclaration(member) || ts.isConstructSignatureDeclaration(member)) return "[[construct]]";
|
||||
if (ts.isCallSignatureDeclaration(member)) return "[[call]]";
|
||||
if (ts.isIndexSignatureDeclaration(member)) return "[[index]]";
|
||||
if (!member.name) throw new Error(`Unsupported anonymous UXP member: ${ts.SyntaxKind[member.kind]}`);
|
||||
return cleanName(member.name);
|
||||
}
|
||||
|
||||
function memberKind(member) {
|
||||
if (ts.isConstructorDeclaration(member) || ts.isConstructSignatureDeclaration(member)) return "constructor";
|
||||
if (ts.isMethodDeclaration(member) || ts.isMethodSignature(member)) return "method";
|
||||
if (ts.isPropertyDeclaration(member) || ts.isPropertySignature(member)) return "property";
|
||||
if (ts.isGetAccessorDeclaration(member)) return "getter";
|
||||
if (ts.isSetAccessorDeclaration(member)) return "setter";
|
||||
if (ts.isCallSignatureDeclaration(member)) return "call";
|
||||
if (ts.isIndexSignatureDeclaration(member)) return "index";
|
||||
throw new Error(`Unsupported UXP type member: ${ts.SyntaxKind[member.kind]}`);
|
||||
}
|
||||
|
||||
function collectMembers(owner, members) {
|
||||
for (const member of members) add(joinName(owner, memberName(member)), memberKind(member));
|
||||
}
|
||||
|
||||
function collectTypeNode(owner, node) {
|
||||
if (ts.isTypeLiteralNode(node)) {
|
||||
collectMembers(owner, node.members);
|
||||
} else if (ts.isIntersectionTypeNode(node) || ts.isUnionTypeNode(node)) {
|
||||
for (const type of node.types) collectTypeNode(owner, type);
|
||||
} else if (ts.isParenthesizedTypeNode(node)) {
|
||||
collectTypeNode(owner, node.type);
|
||||
}
|
||||
}
|
||||
|
||||
function collectStatements(statements, owner = "globalThis") {
|
||||
for (const statement of statements) {
|
||||
if (ts.isModuleDeclaration(statement)) {
|
||||
const moduleOwner = joinName(owner === "globalThis" ? "" : owner, cleanName(statement.name));
|
||||
add(moduleOwner, "module");
|
||||
collectModuleBody(statement.body, moduleOwner);
|
||||
} else if (ts.isClassDeclaration(statement) || ts.isInterfaceDeclaration(statement)) {
|
||||
if (!statement.name) throw new Error(`Anonymous UXP declaration: ${ts.SyntaxKind[statement.kind]}`);
|
||||
const symbol = joinName(owner, statement.name.text);
|
||||
add(symbol, ts.isClassDeclaration(statement) ? "class" : "interface");
|
||||
collectMembers(symbol, statement.members);
|
||||
} else if (ts.isTypeAliasDeclaration(statement)) {
|
||||
const symbol = joinName(owner, statement.name.text);
|
||||
add(symbol, "type");
|
||||
collectTypeNode(symbol, statement.type);
|
||||
} else if (ts.isEnumDeclaration(statement)) {
|
||||
const symbol = joinName(owner, statement.name.text);
|
||||
add(symbol, "enum");
|
||||
for (const member of statement.members) add(joinName(symbol, cleanName(member.name)), "enumMember");
|
||||
} else if (ts.isFunctionDeclaration(statement)) {
|
||||
if (!statement.name) throw new Error("Anonymous UXP function declaration");
|
||||
add(joinName(owner, statement.name.text), "function");
|
||||
} else if (ts.isVariableStatement(statement)) {
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
if (!ts.isIdentifier(declaration.name)) throw new Error("Unsupported destructured UXP variable declaration");
|
||||
add(joinName(owner, declaration.name.text), "variable");
|
||||
}
|
||||
} else if (ts.isExportAssignment(statement) || ts.isExportDeclaration(statement) || ts.isImportDeclaration(statement)) {
|
||||
// Module wiring does not add a callable or inspectable API symbol.
|
||||
} else {
|
||||
throw new Error(`Unsupported top-level UXP declaration: ${ts.SyntaxKind[statement.kind]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectModuleBody(body, owner) {
|
||||
if (!body) throw new Error(`UXP module ${owner} has no body`);
|
||||
if (ts.isModuleDeclaration(body)) {
|
||||
const nestedOwner = joinName(owner, cleanName(body.name));
|
||||
add(nestedOwner, "module");
|
||||
collectModuleBody(body.body, nestedOwner);
|
||||
} else if (ts.isModuleBlock(body)) {
|
||||
collectStatements(body.statements, owner);
|
||||
} else {
|
||||
throw new Error(`Unsupported UXP module body: ${ts.SyntaxKind[body.kind]}`);
|
||||
}
|
||||
}
|
||||
|
||||
collectStatements(source.statements);
|
||||
const uniqueSymbols = [...new Map(symbols.map((entry) => [entry.symbol, entry])).values()]
|
||||
.sort((left, right) => compareText(left.symbol, right.symbol));
|
||||
const declaredSymbols = new Set(uniqueSymbols.map((entry) => entry.symbol));
|
||||
const entries = uniqueSymbols.map((entry) => ({
|
||||
...entry,
|
||||
coverage: mappedSymbols.has(entry.symbol) ? "mapped" : "unmapped",
|
||||
}));
|
||||
const mapped = entries.filter((entry) => entry.coverage === "mapped").length;
|
||||
const manifestOnly = [...mappedSymbols].filter((symbol) => !declaredSymbols.has(symbol)).sort(compareText);
|
||||
const inventory = {
|
||||
schemaVersion: 1,
|
||||
source: {
|
||||
package: "@adobe/cc-ext-uxp-types",
|
||||
version: packageMetadata.version,
|
||||
declarations: "node_modules/@adobe/cc-ext-uxp-types/uxp/index.d.ts",
|
||||
declarationsSha256,
|
||||
coverageManifest: "src/resources/uxp-js-coverage.json",
|
||||
},
|
||||
semantics: {
|
||||
mapped: "The exact declaration symbol is referenced by panel coverage; this alone is not licensed-host verification.",
|
||||
unmapped: "No panel coverage entry references the exact declaration symbol; this is a review queue, not a requirement for a standalone MCP tool.",
|
||||
},
|
||||
stats: { total: entries.length, mapped, unmapped: entries.length - mapped, manifestOnly: manifestOnly.length },
|
||||
manifestOnly,
|
||||
entries,
|
||||
};
|
||||
const rendered = `${JSON.stringify(inventory, null, 2)}\n`;
|
||||
|
||||
if (validateOnly) {
|
||||
console.log(`Validated ${entries.length} UXP JavaScript API symbols from ${packageMetadata.version}.`);
|
||||
} else if (check) {
|
||||
let current = "";
|
||||
try { current = await readFile(outputPath, "utf8"); } catch {}
|
||||
if (current.replaceAll("\r\n", "\n") !== rendered) {
|
||||
console.error("UXP JavaScript API inventory is stale. Run npm run uxp:js-api-inventory.");
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`UXP JavaScript API inventory is current: ${entries.length} symbols from ${packageMetadata.version}.`);
|
||||
}
|
||||
} else {
|
||||
await writeFile(outputPath, rendered);
|
||||
console.log(`Wrote ${entries.length} UXP JavaScript API symbols (${mapped} mapped, ${entries.length - mapped} unmapped).`);
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
param(
|
||||
[switch]$Diagnose,
|
||||
[ValidateSet("Premiere", "AfterEffects")]
|
||||
[string]$ConnectorHost = "Premiere"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$projectDir = Split-Path -Parent $PSScriptRoot
|
||||
$isAfterEffects = $ConnectorHost -eq "AfterEffects"
|
||||
$pluginSource = Join-Path $projectDir $(if ($isAfterEffects) { "after-effects-cep-plugin" } else { "cep-plugin" })
|
||||
$signedPackage = if ($isAfterEffects) { $null } else { Join-Path $projectDir "artifacts\MCPBridgeCEP.zxp" }
|
||||
$packageMetadata = Get-Content -LiteralPath (Join-Path $projectDir "package.json") -Raw | ConvertFrom-Json
|
||||
$expectedVersion = [string]$packageMetadata.version
|
||||
$cepRoot = Join-Path $env:APPDATA "Adobe\CEP\extensions"
|
||||
$pluginDestination = Join-Path $cepRoot $(if ($isAfterEffects) { "MCPAfterEffectsBridgeCEP" } else { "MCPBridgeCEP" })
|
||||
$resolvedCepRoot = [System.IO.Path]::GetFullPath($cepRoot).TrimEnd([System.IO.Path]::DirectorySeparatorChar)
|
||||
$resolvedDestination = [System.IO.Path]::GetFullPath($pluginDestination)
|
||||
|
||||
if (-not $resolvedDestination.StartsWith($resolvedCepRoot + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Refusing to install outside the CEP extensions directory: $resolvedDestination"
|
||||
}
|
||||
|
||||
Write-Host "=== $(if ($isAfterEffects) { 'After Effects' } else { 'Premiere' }) MCP Connector ==="
|
||||
Write-Host "Source: $pluginSource"
|
||||
Write-Host "Destination: $pluginDestination"
|
||||
if ($Diagnose) {
|
||||
Write-Host "Mode: Check only (no files or settings will be changed)"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $pluginSource "CSXS\manifest.xml"))) {
|
||||
throw "CEP plugin manifest not found at $pluginSource"
|
||||
}
|
||||
|
||||
$signedPackageMatchesRelease = $false
|
||||
if ($signedPackage -and (Test-Path -LiteralPath $signedPackage)) {
|
||||
try {
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$archive = [System.IO.Compression.ZipFile]::OpenRead($signedPackage)
|
||||
try {
|
||||
$manifestEntry = $archive.Entries | Where-Object { $_.FullName -eq "CSXS/manifest.xml" } | Select-Object -First 1
|
||||
if (-not $manifestEntry) {
|
||||
throw "Signed package does not contain CSXS/manifest.xml"
|
||||
}
|
||||
$reader = [System.IO.StreamReader]::new($manifestEntry.Open())
|
||||
try {
|
||||
$signedManifest = $reader.ReadToEnd()
|
||||
}
|
||||
finally {
|
||||
$reader.Dispose()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$archive.Dispose()
|
||||
}
|
||||
|
||||
$signedPackageMatchesRelease = $signedManifest -match ('ExtensionBundleVersion="' + [regex]::Escape($expectedVersion) + '"')
|
||||
if (-not $signedPackageMatchesRelease) {
|
||||
Write-Warning "Ignoring artifacts\MCPBridgeCEP.zxp because its embedded connector version does not match package version $expectedVersion."
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Ignoring artifacts\MCPBridgeCEP.zxp because its embedded manifest could not be verified: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $Diagnose) {
|
||||
New-Item -ItemType Directory -Force -Path $cepRoot | Out-Null
|
||||
|
||||
if (Test-Path -LiteralPath $pluginDestination) {
|
||||
Remove-Item -LiteralPath $resolvedDestination -Recurse -Force
|
||||
}
|
||||
if ($signedPackageMatchesRelease) {
|
||||
$temporaryZip = Join-Path ([System.IO.Path]::GetTempPath()) ("MCPBridgeCEP-" + [guid]::NewGuid().ToString("N") + ".zip")
|
||||
try {
|
||||
Copy-Item -LiteralPath $signedPackage -Destination $temporaryZip
|
||||
Expand-Archive -LiteralPath $temporaryZip -DestinationPath $pluginDestination
|
||||
Write-Host "Installed signed CEP package: $signedPackage"
|
||||
}
|
||||
finally {
|
||||
if (Test-Path -LiteralPath $temporaryZip) {
|
||||
Remove-Item -LiteralPath $temporaryZip -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Copy-Item -LiteralPath $pluginSource -Destination $pluginDestination -Recurse
|
||||
Write-Warning "No signed CEP package is present; installed the development bundle and enabled PlayerDebugMode."
|
||||
}
|
||||
|
||||
# Adobe requires PlayerDebugMode to be a String value. A DWORD that happens
|
||||
# to contain 1 is ignored by CEP and the unsigned extension is not discovered.
|
||||
foreach ($version in 9..14) {
|
||||
$key = "HKCU:\SOFTWARE\Adobe\CSXS.$version"
|
||||
New-Item -Path $key -Force | Out-Null
|
||||
New-ItemProperty -Path $key -Name "PlayerDebugMode" -PropertyType String -Value "1" -Force | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
$problems = @()
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $pluginDestination "CSXS\manifest.xml"))) {
|
||||
$problems += "Plugin manifest is missing from $pluginDestination"
|
||||
}
|
||||
|
||||
foreach ($version in 9..14) {
|
||||
$key = "HKCU:\SOFTWARE\Adobe\CSXS.$version"
|
||||
$value = Get-ItemProperty -Path $key -Name "PlayerDebugMode" -ErrorAction SilentlyContinue
|
||||
if ($null -eq $value -or [string]$value.PlayerDebugMode -ne "1") {
|
||||
$problems += "CSXS.$version PlayerDebugMode is missing or not set to 1"
|
||||
continue
|
||||
}
|
||||
|
||||
$kind = (Get-Item -Path $key).GetValueKind("PlayerDebugMode")
|
||||
if ($kind -ne [Microsoft.Win32.RegistryValueKind]::String) {
|
||||
$problems += "CSXS.$version PlayerDebugMode is $kind; Adobe requires REG_SZ"
|
||||
}
|
||||
}
|
||||
|
||||
$signatureFailures = Get-ChildItem -Path $env:TEMP -Filter "CEP*-PPRO.log" -File -ErrorAction SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -First 5 |
|
||||
Select-String -Pattern "Signature verification failed for extension com\.mcp\.premiere\.bridge" -ErrorAction SilentlyContinue
|
||||
if (!$isAfterEffects -and $signatureFailures) {
|
||||
$latestFailure = $signatureFailures | Select-Object -First 1
|
||||
$problems += "Premiere logged a signature failure in $($latestFailure.Path). Reinstall from a release containing artifacts\MCPBridgeCEP.zxp, fully quit Premiere, and relaunch it."
|
||||
}
|
||||
|
||||
if ($problems.Count -gt 0) {
|
||||
Write-Error ("The $(if ($isAfterEffects) { 'After Effects' } else { 'Premiere' }) Connector needs attention:`n" + ($problems -join [Environment]::NewLine))
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:"
|
||||
Write-Host " 1. Fully quit $(if ($isAfterEffects) { 'After Effects' } else { 'Premiere Pro' })."
|
||||
Write-Host " 2. Run the Connector installer again."
|
||||
Write-Host " 3. Reopen $(if ($isAfterEffects) { 'After Effects, then choose Window > Extensions > MCP for Adobe After Effects.' } else { 'Premiere Pro, then choose Window > Extensions > MCP for Adobe Premiere Pro.' })"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
if ($Diagnose) {
|
||||
Write-Host "Connector installation looks ready."
|
||||
Write-Host "This check cannot confirm that $(if ($isAfterEffects) { 'After Effects' } else { 'Premiere Pro' }) is currently open or connected."
|
||||
Write-Host "Next: Open $(if ($isAfterEffects) { 'After Effects and run Verify After Effects connection.' } else { 'Premiere Pro and run Verify Premiere connection.' })"
|
||||
}
|
||||
else {
|
||||
Write-Host "Connector installed. Fully restart $(if ($isAfterEffects) { 'After Effects, then open Window > Extensions > MCP for Adobe After Effects.' } else { 'Premiere Pro, then open Window > Extensions > MCP for Adobe Premiere Pro.' })"
|
||||
Write-Host "After that, ask your AI assistant to run '$(if ($isAfterEffects) { 'Verify After Effects connection' } else { 'Verify Premiere connection' })' before editing."
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
# Install the MCP for Adobe Premiere Pro CEP plugin
|
||||
# This script creates a symlink from the CEP extensions directory to this project's cep-plugin folder.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
HOST="Premiere"
|
||||
MODE=""
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--after-effects) HOST="AfterEffects" ;;
|
||||
--diagnose|--copy) MODE="$arg" ;;
|
||||
*) echo "Unknown option: $arg" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
if [ "$HOST" = "AfterEffects" ]; then
|
||||
PLUGIN_SRC="$PROJECT_DIR/after-effects-cep-plugin"
|
||||
PLUGIN_NAME="MCPAfterEffectsBridgeCEP"
|
||||
HOST_LABEL="After Effects"
|
||||
HOST_MENU="MCP for Adobe After Effects"
|
||||
else
|
||||
PLUGIN_SRC="$PROJECT_DIR/cep-plugin"
|
||||
PLUGIN_NAME="MCPBridgeCEP"
|
||||
HOST_LABEL="Premiere Pro"
|
||||
HOST_MENU="MCP for Adobe Premiere Pro"
|
||||
fi
|
||||
|
||||
# Detect OS
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
CEP_DIR="$HOME/Library/Application Support/Adobe/CEP/extensions"
|
||||
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
|
||||
CEP_DIR="$APPDATA/Adobe/CEP/extensions"
|
||||
else
|
||||
echo "Unsupported OS: $OSTYPE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== $HOST_LABEL MCP Connector ==="
|
||||
echo ""
|
||||
echo "Source: $PLUGIN_SRC"
|
||||
echo "Destination: $CEP_DIR/$PLUGIN_NAME"
|
||||
echo ""
|
||||
|
||||
if [ ! -f "$PLUGIN_SRC/CSXS/manifest.xml" ]; then
|
||||
echo "CEP plugin manifest not found at $PLUGIN_SRC" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$MODE" = "--diagnose" ]; then
|
||||
problems=0
|
||||
if [ ! -f "$CEP_DIR/$PLUGIN_NAME/CSXS/manifest.xml" ]; then
|
||||
echo "Plugin manifest is missing from $CEP_DIR/$PLUGIN_NAME" >&2
|
||||
problems=1
|
||||
fi
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
for version in 9 10 11 12 13 14; do
|
||||
if [ "$(defaults read com.adobe.CSXS.$version PlayerDebugMode 2>/dev/null || true)" != "1" ]; then
|
||||
echo "CSXS.$version PlayerDebugMode is missing or not set to 1" >&2
|
||||
problems=1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [ "$problems" -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Next steps: fully quit $HOST_LABEL, run the Connector installer again, then reopen it and choose Window > Extensions > $HOST_MENU." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Installation verified: Connector files are present."
|
||||
echo "This check cannot confirm that Premiere Pro is currently open or connected."
|
||||
echo "Next: Open $HOST_LABEL and ask your AI assistant to run 'Verify After Effects connection' for AE or 'Verify Premiere connection' for Premiere."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create CEP extensions directory if needed
|
||||
mkdir -p "$CEP_DIR"
|
||||
|
||||
# Remove existing installation
|
||||
if [ -e "$CEP_DIR/$PLUGIN_NAME" ] || [ -L "$CEP_DIR/$PLUGIN_NAME" ]; then
|
||||
echo "Removing existing installation..."
|
||||
rm -rf "$CEP_DIR/$PLUGIN_NAME"
|
||||
fi
|
||||
|
||||
# Create symlink (for development) or copy (for production)
|
||||
if [ "$MODE" = "--copy" ]; then
|
||||
echo "Copying plugin files..."
|
||||
cp -r "$PLUGIN_SRC" "$CEP_DIR/$PLUGIN_NAME"
|
||||
else
|
||||
echo "Creating symlink (development mode)..."
|
||||
ln -s "$PLUGIN_SRC" "$CEP_DIR/$PLUGIN_NAME"
|
||||
fi
|
||||
|
||||
if [ ! -f "$CEP_DIR/$PLUGIN_NAME/CSXS/manifest.xml" ]; then
|
||||
echo "Installation failed: plugin manifest was not installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Enable CEP debug mode (allows unsigned extensions)
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
echo "Enabling CEP debug mode..."
|
||||
for version in 8 9 10 11 12 13 14; do
|
||||
defaults write com.adobe.CSXS.$version PlayerDebugMode 1 2>/dev/null || true
|
||||
done
|
||||
echo "CEP debug mode enabled for CSXS 8-14"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✓ Connector installed"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Fully restart $HOST_LABEL"
|
||||
echo " 2. Go to Window > Extensions > $HOST_MENU"
|
||||
echo " 3. Check that the Connector says it is running"
|
||||
echo " 4. Ask your AI assistant to run the corresponding host connection check"
|
||||
echo ""
|
||||
echo "Advanced configuration (only if your AI assistant did not install it for you):"
|
||||
echo " node $PROJECT_DIR/dist/index.js"
|
||||
echo ""
|
||||
@@ -0,0 +1,67 @@
|
||||
@echo off
|
||||
REM Premiere Pro AI Chat — Windows Installer
|
||||
REM Copies the plugin to the Adobe CEP extensions folder
|
||||
REM and enables unsigned extensions via registry.
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
echo ========================================
|
||||
echo Premiere Pro AI Chat — Plugin Installer
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM Resolve paths
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "PROJECT_ROOT=%SCRIPT_DIR%.."
|
||||
set "PLUGIN_SRC=%PROJECT_ROOT%\chat-plugin"
|
||||
set "PLUGIN_NAME=com.ppro.ai.chat"
|
||||
set "CEP_DIR=%APPDATA%\Adobe\CEP\extensions"
|
||||
|
||||
echo Plugin source: %PLUGIN_SRC%
|
||||
echo CEP target: %CEP_DIR%\%PLUGIN_NAME%
|
||||
echo.
|
||||
|
||||
REM Verify source exists
|
||||
if not exist "%PLUGIN_SRC%" (
|
||||
echo [ERROR] Plugin source not found at %PLUGIN_SRC%
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Create CEP extensions directory if needed
|
||||
if not exist "%CEP_DIR%" (
|
||||
echo Creating CEP extensions directory...
|
||||
mkdir "%CEP_DIR%"
|
||||
)
|
||||
|
||||
REM Remove existing installation
|
||||
if exist "%CEP_DIR%\%PLUGIN_NAME%" (
|
||||
echo Removing existing installation...
|
||||
rmdir /s /q "%CEP_DIR%\%PLUGIN_NAME%"
|
||||
)
|
||||
|
||||
REM Copy plugin
|
||||
echo Installing plugin...
|
||||
xcopy /s /e /i /q "%PLUGIN_SRC%" "%CEP_DIR%\%PLUGIN_NAME%"
|
||||
echo [OK] Plugin installed
|
||||
|
||||
REM Enable unsigned extensions (CSXS 9-12)
|
||||
echo.
|
||||
echo Enabling unsigned CEP extensions...
|
||||
for %%v in (9 10 11 12) do (
|
||||
reg add "HKCU\SOFTWARE\Adobe\CSXS.%%v" /v PlayerDebugMode /t REG_SZ /d 1 /f >nul 2>&1
|
||||
)
|
||||
echo [OK] Debug mode enabled (CSXS 9-12)
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo [OK] Installation complete!
|
||||
echo ========================================
|
||||
echo.
|
||||
echo Next steps:
|
||||
echo 1. Restart Premiere Pro (if running)
|
||||
echo 2. Open: Window ^> Extensions ^> AI Chat
|
||||
echo 3. Enter your Claude or Gemini API key
|
||||
echo 4. Start chatting to control Premiere Pro!
|
||||
echo.
|
||||
pause
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install the Premiere Pro AI Chat CEP plugin
|
||||
# Creates a symlink (or copies) from the chat-plugin/ directory
|
||||
# into the Adobe CEP extensions folder.
|
||||
|
||||
set -e
|
||||
|
||||
PLUGIN_NAME="com.ppro.ai.chat"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
PLUGIN_SRC="$PROJECT_ROOT/chat-plugin"
|
||||
|
||||
echo "========================================"
|
||||
echo " Premiere Pro AI Chat — Plugin Installer"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Detect OS
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
CEP_DIR="$HOME/Library/Application Support/Adobe/CEP/extensions"
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
CEP_DIR="$HOME/.config/Adobe/CEP/extensions"
|
||||
else
|
||||
echo "⚠ Windows detected. Please manually copy:"
|
||||
echo " From: $PLUGIN_SRC"
|
||||
echo " To: %APPDATA%\\Adobe\\CEP\\extensions\\$PLUGIN_NAME"
|
||||
echo ""
|
||||
echo "Then set registry key to enable unsigned extensions:"
|
||||
echo ' HKEY_CURRENT_USER\SOFTWARE\Adobe\CSXS.11'
|
||||
echo ' PlayerDebugMode = "1" (String)'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Plugin source: $PLUGIN_SRC"
|
||||
echo "CEP target: $CEP_DIR/$PLUGIN_NAME"
|
||||
echo ""
|
||||
|
||||
# Verify source exists
|
||||
if [ ! -d "$PLUGIN_SRC" ]; then
|
||||
echo "✗ Error: Plugin source not found at $PLUGIN_SRC"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create CEP extensions directory if needed
|
||||
if [ ! -d "$CEP_DIR" ]; then
|
||||
echo "Creating CEP extensions directory..."
|
||||
mkdir -p "$CEP_DIR"
|
||||
fi
|
||||
|
||||
# Remove existing installation
|
||||
if [ -L "$CEP_DIR/$PLUGIN_NAME" ] || [ -d "$CEP_DIR/$PLUGIN_NAME" ]; then
|
||||
echo "Removing existing installation..."
|
||||
rm -rf "$CEP_DIR/$PLUGIN_NAME"
|
||||
fi
|
||||
|
||||
# Create symlink
|
||||
echo "Creating symlink..."
|
||||
ln -s "$PLUGIN_SRC" "$CEP_DIR/$PLUGIN_NAME"
|
||||
echo "✓ Symlink created"
|
||||
|
||||
# Enable unsigned extensions (macOS)
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
echo ""
|
||||
echo "Enabling unsigned CEP extensions..."
|
||||
# Support CSXS versions 9-12 for various Premiere Pro versions
|
||||
for ver in 9 10 11 12; do
|
||||
defaults write com.adobe.CSXS.$ver PlayerDebugMode 1 2>/dev/null || true
|
||||
done
|
||||
echo "✓ Debug mode enabled (CSXS 9-12)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " ✓ Installation complete!"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Restart Premiere Pro (if running)"
|
||||
echo " 2. Open: Window → Extensions → AI Chat"
|
||||
echo " 3. Enter your Claude or Gemini API key"
|
||||
echo " 4. Start chatting to control Premiere Pro!"
|
||||
echo ""
|
||||
echo "Note: The AI Chat panel and MCP for Adobe Premiere Pro panel"
|
||||
echo "can run side by side if you have both installed."
|
||||
echo ""
|
||||
@@ -0,0 +1,31 @@
|
||||
export const NATIVE_SDK_HEADER_INVENTORY_SCHEMA_VERSION = 1;
|
||||
|
||||
export const NATIVE_SDK_HEADER_INVENTORY_SEMANTICS = Object.freeze({
|
||||
listed: "Each listed item is a header file observed in a locally supplied Adobe SDK extraction. Paths are relative to that extraction and contents are not copied into this inventory.",
|
||||
doesNotEstablish: "This receipt does not establish Adobe entitlement, complete C/C++ declaration coverage, a compiled addon, a loaded plugin, MCP exposure, or licensed-host behavior.",
|
||||
});
|
||||
|
||||
export const NATIVE_SDK_FAMILIES = Object.freeze({
|
||||
"uxp-hybrid": Object.freeze({
|
||||
authorityUrl: "https://developer.adobe.com/premiere-pro/uxp/plugins/hybrid-plugins/",
|
||||
includeDirectories: Object.freeze(["src/api", "src/utilities"]),
|
||||
requiredHeaders: Object.freeze([
|
||||
"src/api/UxpAddonShared.h",
|
||||
"src/api/UxpAddonTypes.h",
|
||||
"src/utilities/UxpAddon.h",
|
||||
]),
|
||||
}),
|
||||
"premiere-prsdk": Object.freeze({
|
||||
authorityUrl: "https://developer.adobe.com/premiere-pro/",
|
||||
includeDirectories: null,
|
||||
requiredHeaders: Object.freeze([]),
|
||||
}),
|
||||
});
|
||||
|
||||
export function compareNativeSdkPaths(left, right) {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
|
||||
export function hasNativeSdkFamily(value) {
|
||||
return Object.prototype.hasOwnProperty.call(NATIVE_SDK_FAMILIES, value);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = join(fileURLToPath(new URL("..", import.meta.url)));
|
||||
const readJson = (relativePath) =>
|
||||
JSON.parse(readFileSync(join(root, relativePath), "utf8"));
|
||||
|
||||
const packageJson = readJson("package.json");
|
||||
const manifest = readJson("registry/server.json");
|
||||
const npmPackage = manifest.packages?.[0];
|
||||
|
||||
function fail(message) {
|
||||
console.error(`MCP Registry submission preflight failed: ${message}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
if (!npmPackage || manifest.name !== packageJson.mcpName) {
|
||||
fail("local registry metadata does not match package.json; run validate:mcp-registry-metadata first.");
|
||||
} else if (
|
||||
manifest.version !== packageJson.version ||
|
||||
npmPackage.identifier !== packageJson.name ||
|
||||
npmPackage.version !== packageJson.version ||
|
||||
npmPackage.transport?.type !== "stdio"
|
||||
) {
|
||||
fail("local registry package, version, or transport metadata is not publishable.");
|
||||
} else {
|
||||
const npmUrl = `https://registry.npmjs.org/${encodeURIComponent(packageJson.name)}/${encodeURIComponent(packageJson.version)}`;
|
||||
const registryUrl = `https://registry.modelcontextprotocol.io/v0.1/servers?search=${encodeURIComponent(manifest.name)}`;
|
||||
|
||||
try {
|
||||
const [npmResponse, registryResponse] = await Promise.all([
|
||||
fetch(npmUrl, {
|
||||
headers: { accept: "application/json" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
}),
|
||||
fetch(registryUrl, {
|
||||
headers: { accept: "application/json" },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!npmResponse.ok) {
|
||||
fail(`published npm artifact was not found (${npmResponse.status}).`);
|
||||
} else if (!registryResponse.ok) {
|
||||
fail(`official registry search failed (${registryResponse.status}).`);
|
||||
} else {
|
||||
const [publishedPackage, registrySearch] = await Promise.all([
|
||||
npmResponse.json(),
|
||||
registryResponse.json(),
|
||||
]);
|
||||
|
||||
if (
|
||||
publishedPackage.version !== packageJson.version ||
|
||||
publishedPackage.mcpName !== packageJson.mcpName
|
||||
) {
|
||||
fail("published npm artifact does not expose the expected version and mcpName metadata.");
|
||||
} else {
|
||||
const servers = Array.isArray(registrySearch.servers)
|
||||
? registrySearch.servers
|
||||
: [];
|
||||
const matches = servers.filter((server) =>
|
||||
[server.name, server.server?.name].includes(manifest.name),
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Published npm artifact verified: ${packageJson.name}@${packageJson.version} (${packageJson.mcpName}).`,
|
||||
);
|
||||
console.log(
|
||||
matches.length
|
||||
? `Official MCP Registry already has ${matches.length} matching record(s) for ${manifest.name}; inspect them before publishing another immutable version.`
|
||||
: `Official MCP Registry has no matching record for ${manifest.name}; metadata is ready for the owner-authorized publisher login.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
fail(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { stdin as input, stdout as output } from "node:process";
|
||||
import process from "node:process";
|
||||
import packageJson from "../package.json" with { type: "json" };
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const dryRun = args.has("--dry-run");
|
||||
const skipTests = args.has("--skip-tests");
|
||||
const yes = args.has("--yes") || process.env.CI === "true";
|
||||
const version = packageJson.version;
|
||||
const packageName = packageJson.name;
|
||||
|
||||
function npmCommand(commandArgs) {
|
||||
const npmCli = join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
||||
if (existsSync(npmCli)) {
|
||||
return {
|
||||
command: process.execPath,
|
||||
args: [npmCli, ...commandArgs],
|
||||
rendered: ["npm", ...commandArgs].join(" "),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
command: "npm",
|
||||
args: commandArgs,
|
||||
rendered: ["npm", ...commandArgs].join(" "),
|
||||
};
|
||||
}
|
||||
|
||||
function run(command, commandArgs, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const invocation = command === "npm"
|
||||
? npmCommand(commandArgs)
|
||||
: { command, args: commandArgs, rendered: [command, ...commandArgs].join(" ") };
|
||||
const child = spawn(invocation.command, invocation.args, {
|
||||
shell: false,
|
||||
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
||||
env: { ...process.env, ...options.env },
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
if (options.capture) {
|
||||
child.stdout?.on("data", (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr?.on("data", (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
}
|
||||
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ stdout: stdout.trim(), stderr: stderr.trim() });
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error(`${invocation.rendered} failed with exit code ${code}\n${stderr.trim()}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function npmViewVersion() {
|
||||
try {
|
||||
const { stdout } = await run("npm", ["view", `${packageName}@${version}`, "version"], {
|
||||
capture: true,
|
||||
});
|
||||
return stdout || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function npmWhoami() {
|
||||
const token = process.env.NODE_AUTH_TOKEN || process.env.NPM_TOKEN;
|
||||
if (token) {
|
||||
return "token auth";
|
||||
}
|
||||
|
||||
try {
|
||||
const { stdout } = await run("npm", ["whoami"], { capture: true });
|
||||
return stdout;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForOtp() {
|
||||
if (process.env.NPM_OTP) {
|
||||
return process.env.NPM_OTP;
|
||||
}
|
||||
|
||||
if (yes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rl = createInterface({ input, output });
|
||||
try {
|
||||
const otp = await rl.question(
|
||||
"npm 2FA code, if your account requires one. Press Enter to try without it: ",
|
||||
);
|
||||
return otp.trim() || null;
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmPublish() {
|
||||
if (yes || dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rl = createInterface({ input, output });
|
||||
try {
|
||||
const answer = await rl.question(`Publish ${packageName}@${version} to npm latest? Type yes: `);
|
||||
if (answer.trim().toLowerCase() !== "yes") {
|
||||
throw new Error("Publish cancelled.");
|
||||
}
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`Preparing ${packageName}@${version} for npm ${dryRun ? "dry run" : "publish"}...`);
|
||||
|
||||
const existingVersion = await npmViewVersion();
|
||||
if (existingVersion === version && !dryRun) {
|
||||
throw new Error(`${packageName}@${version} is already published on npm.`);
|
||||
}
|
||||
|
||||
const identity = await npmWhoami();
|
||||
if (!identity && !dryRun) {
|
||||
throw new Error(
|
||||
"npm is not authenticated. Run npm login --auth-type=web, or set NPM_TOKEN/NODE_AUTH_TOKEN.",
|
||||
);
|
||||
}
|
||||
|
||||
if (identity) {
|
||||
console.log(`npm auth: ${identity}`);
|
||||
}
|
||||
|
||||
await run("npm", ["run", "build"]);
|
||||
|
||||
if (!skipTests) {
|
||||
await run("npm", ["test"]);
|
||||
}
|
||||
|
||||
await run("npm", ["pack", "--dry-run"]);
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`Dry run complete for ${packageName}@${version}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmPublish();
|
||||
|
||||
const otp = await promptForOtp();
|
||||
const publishArgs = ["publish", "--access", "public"];
|
||||
if (otp) {
|
||||
publishArgs.push(`--otp=${otp}`);
|
||||
}
|
||||
|
||||
await run("npm", publishArgs, {
|
||||
env: process.env.NPM_TOKEN ? { NODE_AUTH_TOKEN: process.env.NPM_TOKEN } : {},
|
||||
});
|
||||
|
||||
const { stdout } = await run("npm", ["view", packageName, "version"], { capture: true });
|
||||
console.log(`${packageName} latest is now ${stdout}.`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
+373
@@ -0,0 +1,373 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { createReadStream } from "node:fs";
|
||||
import { open, stat } from "node:fs/promises";
|
||||
import { createInflateRaw } from "node:zlib";
|
||||
|
||||
const END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
|
||||
const CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
|
||||
const LOCAL_FILE_SIGNATURE = 0x04034b50;
|
||||
const DATA_DESCRIPTOR_SIGNATURE = 0x08074b50;
|
||||
const ZIP64_EXTRA_FIELD_ID = 0x0001;
|
||||
const MAX_ZIP32_BYTES = 0xffff_ffff;
|
||||
const MAX_CENTRAL_DIRECTORY_BYTES = 64 * 1024 * 1024;
|
||||
const MAX_ENTRIES = 100_000;
|
||||
const ZIP_FLAG_ENCRYPTED = 0x0001;
|
||||
const ZIP_FLAG_MAXIMUM_COMPRESSION = 0x0002;
|
||||
const ZIP_FLAG_FAST_COMPRESSION = 0x0004;
|
||||
const ZIP_FLAG_DATA_DESCRIPTOR = 0x0008;
|
||||
const ZIP_FLAG_STRONG_ENCRYPTION = 0x0040;
|
||||
const ZIP_FLAG_UTF8 = 0x0800;
|
||||
const ZIP_FLAG_CENTRAL_DIRECTORY_ENCRYPTION = 0x2000;
|
||||
const ENCRYPTED_ENTRY_FLAGS = ZIP_FLAG_ENCRYPTED | ZIP_FLAG_STRONG_ENCRYPTION | ZIP_FLAG_CENTRAL_DIRECTORY_ENCRYPTION;
|
||||
const ZIP_FLAG_COMPRESSION_OPTIONS = ZIP_FLAG_MAXIMUM_COMPRESSION | ZIP_FLAG_FAST_COMPRESSION;
|
||||
const SUPPORTED_GENERAL_PURPOSE_FLAGS = ZIP_FLAG_MAXIMUM_COMPRESSION | ZIP_FLAG_FAST_COMPRESSION | ZIP_FLAG_DATA_DESCRIPTOR | ZIP_FLAG_UTF8;
|
||||
const ZIP_HOST_UNIX = 3;
|
||||
const UNIX_FILE_TYPE_MASK = 0o170000;
|
||||
const UNIX_FILE_TYPE_DIRECTORY = 0o040000;
|
||||
const UNIX_FILE_TYPE_REGULAR = 0o100000;
|
||||
const CRC32_TABLE = Uint32Array.from({ length: 256 }, (_, index) => {
|
||||
let value = index;
|
||||
for (let bit = 0; bit < 8; bit += 1) value = (value >>> 1) ^ (value & 1 ? 0xedb8_8320 : 0);
|
||||
return value >>> 0;
|
||||
});
|
||||
|
||||
function archiveError(message) {
|
||||
const error = new Error(message);
|
||||
error.code = "UXP_HYBRID_CCX_ARCHIVE_INVALID";
|
||||
return error;
|
||||
}
|
||||
|
||||
function updateCrc32(value, buffer) {
|
||||
let crc32 = value;
|
||||
for (const byte of buffer) crc32 = (CRC32_TABLE[(crc32 ^ byte) & 0xff] ^ (crc32 >>> 8)) >>> 0;
|
||||
return crc32;
|
||||
}
|
||||
|
||||
async function readExactly(handle, bytes, position, label) {
|
||||
const buffer = Buffer.alloc(bytes);
|
||||
const { bytesRead } = await handle.read(buffer, 0, bytes, position);
|
||||
if (bytesRead !== bytes) throw archiveError(`${label} is truncated`);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
function findEndOfCentralDirectory(tail, archiveBytes) {
|
||||
for (let offset = tail.length - 22; offset >= 0; offset -= 1) {
|
||||
if (tail.readUInt32LE(offset) !== END_OF_CENTRAL_DIRECTORY_SIGNATURE) continue;
|
||||
const commentBytes = tail.readUInt16LE(offset + 20);
|
||||
if (offset + 22 + commentBytes !== tail.length) continue;
|
||||
const disk = tail.readUInt16LE(offset + 4);
|
||||
const directoryDisk = tail.readUInt16LE(offset + 6);
|
||||
const entriesOnDisk = tail.readUInt16LE(offset + 8);
|
||||
const entries = tail.readUInt16LE(offset + 10);
|
||||
const directoryBytes = tail.readUInt32LE(offset + 12);
|
||||
const directoryOffset = tail.readUInt32LE(offset + 16);
|
||||
if (disk !== 0 || directoryDisk !== 0 || entriesOnDisk !== entries) {
|
||||
throw archiveError("CCX archive must be a single-disk ZIP");
|
||||
}
|
||||
if (entries === 0xffff || directoryBytes === 0xffff_ffff || directoryOffset === 0xffff_ffff) {
|
||||
throw archiveError("CCX archive ZIP64 metadata is not supported by this bounded verifier");
|
||||
}
|
||||
if (entries > MAX_ENTRIES || directoryBytes > MAX_CENTRAL_DIRECTORY_BYTES) {
|
||||
throw archiveError("CCX archive central directory exceeds the verifier bounds");
|
||||
}
|
||||
const directoryEnd = directoryOffset + directoryBytes;
|
||||
const eocdPosition = archiveBytes - tail.length + offset;
|
||||
if (!Number.isSafeInteger(directoryEnd) || directoryEnd > eocdPosition) {
|
||||
throw archiveError("CCX archive central directory is outside the ZIP bounds");
|
||||
}
|
||||
if (directoryEnd !== eocdPosition) {
|
||||
throw archiveError("CCX archive has unaccounted bytes between the central directory and ZIP end record");
|
||||
}
|
||||
return { entries, directoryBytes, directoryOffset };
|
||||
}
|
||||
throw archiveError("CCX archive must contain a conventional ZIP end record");
|
||||
}
|
||||
|
||||
function validateZipExtraFields(extraFields) {
|
||||
for (let offset = 0; offset < extraFields.length;) {
|
||||
if (offset + 4 > extraFields.length) throw archiveError("CCX archive ZIP extra fields are malformed");
|
||||
const id = extraFields.readUInt16LE(offset);
|
||||
const bytes = extraFields.readUInt16LE(offset + 2);
|
||||
const next = offset + 4 + bytes;
|
||||
if (next > extraFields.length) throw archiveError("CCX archive ZIP extra fields are malformed");
|
||||
if (id === ZIP64_EXTRA_FIELD_ID) throw archiveError("CCX archive ZIP64 extra fields are not supported by this bounded verifier");
|
||||
offset = next;
|
||||
}
|
||||
}
|
||||
|
||||
function readCentralDirectory(buffer, expectedEntries) {
|
||||
const entries = [];
|
||||
let offset = 0;
|
||||
while (offset < buffer.length) {
|
||||
if (offset + 46 > buffer.length || buffer.readUInt32LE(offset) !== CENTRAL_DIRECTORY_SIGNATURE) {
|
||||
throw archiveError("CCX archive central directory entry is invalid");
|
||||
}
|
||||
const versionMadeBy = buffer.readUInt16LE(offset + 4);
|
||||
const versionNeeded = buffer.readUInt16LE(offset + 6);
|
||||
const flags = buffer.readUInt16LE(offset + 8);
|
||||
const method = buffer.readUInt16LE(offset + 10);
|
||||
const crc32 = buffer.readUInt32LE(offset + 16);
|
||||
const compressedBytes = buffer.readUInt32LE(offset + 20);
|
||||
const uncompressedBytes = buffer.readUInt32LE(offset + 24);
|
||||
const nameBytes = buffer.readUInt16LE(offset + 28);
|
||||
const extraBytes = buffer.readUInt16LE(offset + 30);
|
||||
const commentBytes = buffer.readUInt16LE(offset + 32);
|
||||
const disk = buffer.readUInt16LE(offset + 34);
|
||||
const externalAttributes = buffer.readUInt32LE(offset + 38);
|
||||
const localOffset = buffer.readUInt32LE(offset + 42);
|
||||
const next = offset + 46 + nameBytes + extraBytes + commentBytes;
|
||||
if (next > buffer.length || disk !== 0) throw archiveError("CCX archive central directory entry is invalid");
|
||||
if (compressedBytes === MAX_ZIP32_BYTES || uncompressedBytes === MAX_ZIP32_BYTES || localOffset === MAX_ZIP32_BYTES) {
|
||||
throw archiveError("CCX archive ZIP64 entry metadata is not supported by this bounded verifier");
|
||||
}
|
||||
const rawName = buffer.subarray(offset + 46, offset + 46 + nameBytes);
|
||||
const rawExtraFields = buffer.subarray(offset + 46 + nameBytes, offset + 46 + nameBytes + extraBytes);
|
||||
const rawComment = buffer.subarray(offset + 46 + nameBytes + extraBytes, next);
|
||||
validateZipExtraFields(rawExtraFields);
|
||||
if (!(flags & ZIP_FLAG_UTF8) && rawName.some((byte) => byte > 0x7f)) {
|
||||
throw archiveError("CCX archive non-ASCII ZIP entry names must declare UTF-8");
|
||||
}
|
||||
const name = rawName.toString("utf8");
|
||||
if (!name || !rawName.equals(Buffer.from(name, "utf8"))) {
|
||||
throw archiveError("CCX archive contains an invalid ZIP entry name");
|
||||
}
|
||||
if (flags & ZIP_FLAG_UTF8) {
|
||||
const comment = rawComment.toString("utf8");
|
||||
if (!rawComment.equals(Buffer.from(comment, "utf8"))) {
|
||||
throw archiveError("CCX archive UTF-8 ZIP entry comment is invalid");
|
||||
}
|
||||
}
|
||||
entries.push(Object.freeze({ name, versionMadeBy, versionNeeded, flags, method, crc32, compressedBytes, uncompressedBytes, externalAttributes, localOffset }));
|
||||
offset = next;
|
||||
}
|
||||
if (entries.length !== expectedEntries) throw archiveError("CCX archive central directory count is inconsistent");
|
||||
return entries;
|
||||
}
|
||||
|
||||
function validateArchiveEntries(entries) {
|
||||
const names = new Set();
|
||||
let directories = 0;
|
||||
for (const entry of entries) {
|
||||
if (entry.flags & ENCRYPTED_ENTRY_FLAGS) {
|
||||
throw archiveError("CCX archive entries must not be encrypted");
|
||||
}
|
||||
if (entry.flags & ~SUPPORTED_GENERAL_PURPOSE_FLAGS) {
|
||||
throw archiveError("CCX archive entries use unsupported ZIP flags");
|
||||
}
|
||||
if (entry.method !== 0 && entry.method !== 8) {
|
||||
throw archiveError("CCX archive entries must use stored or deflate compression");
|
||||
}
|
||||
if (entry.method !== 8 && entry.flags & ZIP_FLAG_COMPRESSION_OPTIONS) {
|
||||
throw archiveError("CCX archive compression option flags require deflate");
|
||||
}
|
||||
const isDirectory = entry.name.endsWith("/");
|
||||
const minimumVersionNeeded = isDirectory || entry.method === 8 ? 20 : 10;
|
||||
if (entry.versionNeeded < minimumVersionNeeded) {
|
||||
throw archiveError("CCX archive entry version-needed does not support its ZIP features");
|
||||
}
|
||||
const createdOnUnix = entry.versionMadeBy >>> 8 === ZIP_HOST_UNIX;
|
||||
const unixFileType = (entry.externalAttributes >>> 16) & UNIX_FILE_TYPE_MASK;
|
||||
if (createdOnUnix && unixFileType !== 0 && unixFileType !== UNIX_FILE_TYPE_REGULAR && unixFileType !== UNIX_FILE_TYPE_DIRECTORY) {
|
||||
throw archiveError("CCX archive Unix entries must be regular files or directories");
|
||||
}
|
||||
const { name } = entry;
|
||||
if (name.length > 1024 || /[\0-\x1f\x7f]/.test(name) || name.includes("\\") || name.startsWith("/") || /^[A-Za-z]:/.test(name)) {
|
||||
throw archiveError("CCX archive contains an unsafe ZIP entry name");
|
||||
}
|
||||
if (isDirectory && (entry.compressedBytes !== 0 || entry.uncompressedBytes !== 0)) {
|
||||
throw archiveError("CCX archive directory entries must not contain file data");
|
||||
}
|
||||
if (isDirectory && entry.crc32 !== 0) {
|
||||
throw archiveError("CCX archive directory entries must use a zero CRC-32");
|
||||
}
|
||||
const segments = name.split("/");
|
||||
if (isDirectory) segments.pop();
|
||||
if (segments.length === 0 || segments.some((segment) => !segment || segment === "." || segment === ".." || /^[A-Za-z]:$/.test(segment))) {
|
||||
throw archiveError("CCX archive contains an unsafe ZIP entry name");
|
||||
}
|
||||
if (names.has(name)) throw archiveError("CCX archive contains duplicate ZIP entry names");
|
||||
names.add(name);
|
||||
if (isDirectory) directories += 1;
|
||||
}
|
||||
return Object.freeze({
|
||||
entries: entries.length,
|
||||
files: entries.length - directories,
|
||||
directories,
|
||||
pathSetSha256: createHash("sha256").update(JSON.stringify(Array.from(names).sort())).digest("hex"),
|
||||
});
|
||||
}
|
||||
|
||||
function pathPrefix(name, expectedPath) {
|
||||
if (name === expectedPath) return "";
|
||||
if (!name.endsWith(`/${expectedPath}`)) return null;
|
||||
const prefix = name.slice(0, name.length - expectedPath.length);
|
||||
const segments = prefix.slice(0, -1).split("/");
|
||||
if (!segments.length || segments.some((segment) => !segment || segment === "." || segment === ".." || /^[A-Za-z]:$/.test(segment))) return null;
|
||||
return prefix;
|
||||
}
|
||||
|
||||
function selectRequiredEntries(entries, expectedPaths) {
|
||||
const selected = new Map();
|
||||
let commonPrefix;
|
||||
for (const expectedPath of expectedPaths) {
|
||||
const matches = entries.map((entry) => ({ entry, prefix: pathPrefix(entry.name, expectedPath) }))
|
||||
.filter((candidate) => candidate.prefix !== null);
|
||||
if (matches.length !== 1) throw archiveError(`CCX archive must contain exactly one ${expectedPath} entry`);
|
||||
const [{ entry, prefix }] = matches;
|
||||
if (commonPrefix === undefined) commonPrefix = prefix;
|
||||
else if (commonPrefix !== prefix) throw archiveError("CCX archive required entries must share one bundle root");
|
||||
selected.set(expectedPath, entry);
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
async function localDataRangeFromHandle(handle, entry, maximumOffset) {
|
||||
if (entry.localOffset + 30 > maximumOffset) throw archiveError("CCX archive local entry is outside the ZIP bounds");
|
||||
const local = await readExactly(handle, 30, entry.localOffset, "CCX archive local entry");
|
||||
if (local.readUInt32LE(0) !== LOCAL_FILE_SIGNATURE) throw archiveError("CCX archive local entry is invalid");
|
||||
const versionNeeded = local.readUInt16LE(4);
|
||||
const flags = local.readUInt16LE(6);
|
||||
const method = local.readUInt16LE(8);
|
||||
if (versionNeeded !== entry.versionNeeded || flags !== entry.flags || method !== entry.method) {
|
||||
throw archiveError("CCX archive local entry metadata is inconsistent");
|
||||
}
|
||||
const localCrc32 = local.readUInt32LE(14);
|
||||
const localCompressedBytes = local.readUInt32LE(18);
|
||||
const localUncompressedBytes = local.readUInt32LE(22);
|
||||
const hasDataDescriptor = Boolean(entry.flags & ZIP_FLAG_DATA_DESCRIPTOR);
|
||||
const hasInconsistentDeclaredMetadata = hasDataDescriptor
|
||||
? (localCrc32 !== 0 && localCrc32 !== entry.crc32) ||
|
||||
(localCompressedBytes !== 0 && localCompressedBytes !== entry.compressedBytes) ||
|
||||
(localUncompressedBytes !== 0 && localUncompressedBytes !== entry.uncompressedBytes)
|
||||
: localCrc32 !== entry.crc32 || localCompressedBytes !== entry.compressedBytes || localUncompressedBytes !== entry.uncompressedBytes;
|
||||
if (hasInconsistentDeclaredMetadata) throw archiveError("CCX archive local entry metadata is inconsistent");
|
||||
const localNameBytes = local.readUInt16LE(26);
|
||||
const localExtraBytes = local.readUInt16LE(28);
|
||||
const localName = await readExactly(handle, localNameBytes, entry.localOffset + 30, "CCX archive local entry name");
|
||||
if (!localName.equals(Buffer.from(entry.name, "utf8"))) throw archiveError("CCX archive local entry name is inconsistent");
|
||||
const localExtraFields = await readExactly(handle, localExtraBytes, entry.localOffset + 30 + localNameBytes, "CCX archive local entry extra fields");
|
||||
validateZipExtraFields(localExtraFields);
|
||||
const dataStart = entry.localOffset + 30 + localNameBytes + localExtraBytes;
|
||||
const dataEnd = dataStart + entry.compressedBytes;
|
||||
if (!Number.isSafeInteger(dataEnd) || dataEnd > maximumOffset) throw archiveError("CCX archive local entry is outside the ZIP bounds");
|
||||
return { dataStart, dataEnd, hasDataDescriptor };
|
||||
}
|
||||
|
||||
function dataDescriptorMatches(buffer, offset, entry) {
|
||||
return buffer.readUInt32LE(offset) === entry.crc32 &&
|
||||
buffer.readUInt32LE(offset + 4) === entry.compressedBytes &&
|
||||
buffer.readUInt32LE(offset + 8) === entry.uncompressedBytes;
|
||||
}
|
||||
|
||||
async function validateDataDescriptor(handle, range, nextOffset) {
|
||||
if (!range.hasDataDescriptor) return range.dataEnd;
|
||||
const availableBytes = nextOffset - range.dataEnd;
|
||||
if (availableBytes < 12) throw archiveError("CCX archive data descriptor is missing or truncated");
|
||||
const descriptor = await readExactly(handle, Math.min(availableBytes, 16), range.dataEnd, "CCX archive data descriptor");
|
||||
const unsignedMatches = dataDescriptorMatches(descriptor, 0, range.entry);
|
||||
const signedMatches = descriptor.length === 16 && descriptor.readUInt32LE(0) === DATA_DESCRIPTOR_SIGNATURE &&
|
||||
dataDescriptorMatches(descriptor, 4, range.entry);
|
||||
if (!unsignedMatches && !signedMatches) throw archiveError("CCX archive data descriptor is inconsistent");
|
||||
return range.dataEnd + (signedMatches ? 16 : 12);
|
||||
}
|
||||
|
||||
async function validateLocalEntries(handle, entries, directoryOffset) {
|
||||
const ranges = [];
|
||||
for (const entry of entries) {
|
||||
ranges.push({ entry, ...(await localDataRangeFromHandle(handle, entry, directoryOffset)) });
|
||||
}
|
||||
ranges.sort((left, right) => left.entry.localOffset - right.entry.localOffset);
|
||||
if (ranges[0]?.entry.localOffset !== 0) {
|
||||
throw archiveError("CCX archive local records have unaccounted bytes");
|
||||
}
|
||||
for (let index = 0; index < ranges.length; index += 1) {
|
||||
const nextOffset = index + 1 < ranges.length ? ranges[index + 1].entry.localOffset : directoryOffset;
|
||||
const recordEnd = await validateDataDescriptor(handle, ranges[index], nextOffset);
|
||||
if (recordEnd > nextOffset) {
|
||||
throw archiveError("CCX archive local entries overlap");
|
||||
}
|
||||
if (recordEnd < nextOffset) {
|
||||
throw archiveError("CCX archive local records have unaccounted bytes");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function localDataRange(archivePath, entry, archiveBytes) {
|
||||
const handle = await open(archivePath, "r");
|
||||
try {
|
||||
return await localDataRangeFromHandle(handle, entry, archiveBytes);
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function consumeZipEntry(archivePath, archiveBytes, entry, maximumBytes, collect) {
|
||||
const { dataStart, dataEnd } = await localDataRange(archivePath, entry, archiveBytes);
|
||||
const input = createReadStream(archivePath, { start: dataStart, end: dataEnd - 1 });
|
||||
const inflator = entry.method === 8 ? createInflateRaw() : undefined;
|
||||
const stream = inflator ? input.pipe(inflator) : input;
|
||||
const digest = createHash("sha256");
|
||||
const chunks = [];
|
||||
let bytes = 0;
|
||||
let crc32 = 0xffff_ffff;
|
||||
try {
|
||||
for await (const chunk of stream) {
|
||||
bytes += chunk.length;
|
||||
if (!Number.isSafeInteger(bytes) || bytes > maximumBytes) throw archiveError("CCX archive required entry exceeds the verifier bounds");
|
||||
digest.update(chunk);
|
||||
crc32 = updateCrc32(crc32, chunk);
|
||||
if (collect) chunks.push(chunk);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.code === "UXP_HYBRID_CCX_ARCHIVE_INVALID") throw error;
|
||||
throw archiveError("CCX archive required entry cannot be decompressed");
|
||||
}
|
||||
if (bytes !== entry.uncompressedBytes) throw archiveError("CCX archive required entry size is inconsistent");
|
||||
if ((crc32 ^ 0xffff_ffff) >>> 0 !== entry.crc32) throw archiveError("CCX archive required entry checksum is inconsistent");
|
||||
if (inflator && inflator.bytesWritten !== entry.compressedBytes) {
|
||||
throw archiveError("CCX archive required entry has trailing compressed data");
|
||||
}
|
||||
return { bytes, sha256: digest.digest("hex"), buffer: collect ? Buffer.concat(chunks, bytes) : undefined };
|
||||
}
|
||||
|
||||
export async function inspectZipArchive(archivePath, expectedPaths) {
|
||||
let archive;
|
||||
try { archive = await stat(archivePath); } catch { throw archiveError("CCX archive must be a readable file"); }
|
||||
if (!archive.isFile() || !Number.isSafeInteger(archive.size) || archive.size < 22 || archive.size > MAX_ZIP32_BYTES) {
|
||||
throw archiveError("CCX archive must be a bounded ZIP file");
|
||||
}
|
||||
const tailBytes = Math.min(archive.size, 22 + 0xffff);
|
||||
const handle = await open(archivePath, "r");
|
||||
try {
|
||||
const tail = await readExactly(handle, tailBytes, archive.size - tailBytes, "CCX archive end record");
|
||||
const end = findEndOfCentralDirectory(tail, archive.size);
|
||||
const directory = await readExactly(handle, end.directoryBytes, end.directoryOffset, "CCX archive central directory");
|
||||
const entries = readCentralDirectory(directory, end.entries);
|
||||
await validateLocalEntries(handle, entries, end.directoryOffset);
|
||||
return Object.freeze({
|
||||
bytes: archive.size,
|
||||
summary: validateArchiveEntries(entries),
|
||||
selected: selectRequiredEntries(entries, expectedPaths),
|
||||
});
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function hashZipEntry(archivePath, archiveBytes, entry, maximumBytes) {
|
||||
return consumeZipEntry(archivePath, archiveBytes, entry, maximumBytes, false);
|
||||
}
|
||||
|
||||
export async function readZipEntry(archivePath, archiveBytes, entry, maximumBytes) {
|
||||
return consumeZipEntry(archivePath, archiveBytes, entry, maximumBytes, true);
|
||||
}
|
||||
|
||||
export async function sha256File(archivePath) {
|
||||
const digest = createHash("sha256");
|
||||
try {
|
||||
for await (const chunk of createReadStream(archivePath)) digest.update(chunk);
|
||||
} catch {
|
||||
throw archiveError("CCX archive must be readable");
|
||||
}
|
||||
return digest.digest("hex");
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
param(
|
||||
[switch]$Quiet,
|
||||
[ValidateSet("Premiere", "AfterEffects")]
|
||||
[string]$ConnectorHost = "Premiere"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$cepRoot = Join-Path $env:APPDATA "Adobe\CEP\extensions"
|
||||
$isAfterEffects = $ConnectorHost -eq "AfterEffects"
|
||||
$pluginDestination = Join-Path $cepRoot $(if ($isAfterEffects) { "MCPAfterEffectsBridgeCEP" } else { "MCPBridgeCEP" })
|
||||
$resolvedCepRoot = [System.IO.Path]::GetFullPath($cepRoot).TrimEnd([System.IO.Path]::DirectorySeparatorChar)
|
||||
$resolvedDestination = [System.IO.Path]::GetFullPath($pluginDestination)
|
||||
|
||||
if (-not $resolvedDestination.StartsWith($resolvedCepRoot + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Refusing to uninstall outside the CEP extensions directory: $resolvedDestination"
|
||||
}
|
||||
|
||||
$hostProcess = Get-Process -Name $(if ($isAfterEffects) { "AfterFX" } else { "Adobe Premiere Pro" }) -ErrorAction SilentlyContinue
|
||||
if ($hostProcess) {
|
||||
throw "$(if ($isAfterEffects) { 'After Effects' } else { 'Premiere Pro' }) is running. Fully quit it before removing the Connector."
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $resolvedDestination) {
|
||||
Remove-Item -LiteralPath $resolvedDestination -Recurse -Force
|
||||
if (-not $Quiet) {
|
||||
Write-Host "Removed the $(if ($isAfterEffects) { 'After Effects' } else { 'Premiere' }) MCP Connector from $pluginDestination"
|
||||
}
|
||||
}
|
||||
elseif (-not $Quiet) {
|
||||
Write-Host "The $(if ($isAfterEffects) { 'After Effects' } else { 'Premiere' }) MCP Connector is not installed for this Windows user."
|
||||
}
|
||||
|
||||
if (-not $Quiet) {
|
||||
Write-Host "CEP PlayerDebugMode settings were left unchanged because they can be used by other CEP extensions."
|
||||
Write-Host "This removes only the selected connector. Remove the MCP server from your AI client's configuration separately if you no longer use it."
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# Remove only the MCP for Adobe Premiere Pro CEP connector. It deliberately
|
||||
# leaves Adobe's shared PlayerDebugMode setting alone.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MODE="--user"
|
||||
HOST="Premiere"
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--after-effects) HOST="AfterEffects" ;;
|
||||
--user|--uninstall|--system|--uninstall-system|--help|-h) MODE="$arg" ;;
|
||||
*) echo "Unknown option: $arg" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
if [ "$HOST" = "AfterEffects" ]; then
|
||||
PLUGIN_NAME="MCPAfterEffectsBridgeCEP"
|
||||
HOST_LABEL="After Effects"
|
||||
HOST_PROCESS="After Effects"
|
||||
else
|
||||
PLUGIN_NAME="MCPBridgeCEP"
|
||||
HOST_LABEL="Premiere Pro"
|
||||
HOST_PROCESS="Adobe Premiere Pro"
|
||||
fi
|
||||
|
||||
if [[ "$OSTYPE" != darwin* ]]; then
|
||||
echo "CEP uninstallation is supported only on macOS by this script." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if pgrep -if "$HOST_PROCESS" >/dev/null 2>&1; then
|
||||
echo "$HOST_LABEL is running. Fully quit it before removing the Connector." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$MODE" in
|
||||
--user|--uninstall)
|
||||
CEP_ROOT="$HOME/Library/Application Support/Adobe/CEP/extensions"
|
||||
;;
|
||||
--system|--uninstall-system)
|
||||
if [[ "$(id -u)" -ne 0 ]]; then
|
||||
echo "The system-wide connector requires administrator permission." >&2
|
||||
echo "Run: sudo \"$0\" --system" >&2
|
||||
exit 1
|
||||
fi
|
||||
CEP_ROOT="/Library/Application Support/Adobe/CEP/extensions"
|
||||
;;
|
||||
--help|-h)
|
||||
cat <<'EOF'
|
||||
Usage: uninstall-cep.sh [--user|--system]
|
||||
|
||||
--user Remove the connector installed for the current user (default).
|
||||
--system Remove the system-wide connector installed by the macOS .pkg.
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $MODE. Use --user or --system." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
DESTINATION="$CEP_ROOT/$PLUGIN_NAME"
|
||||
case "$DESTINATION" in
|
||||
"$CEP_ROOT/$PLUGIN_NAME") ;;
|
||||
*)
|
||||
echo "Refusing to uninstall outside the CEP extensions directory." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ -e "$DESTINATION" || -L "$DESTINATION" ]]; then
|
||||
rm -rf -- "$DESTINATION"
|
||||
echo "Removed the $HOST_LABEL MCP Connector from $DESTINATION"
|
||||
else
|
||||
echo "The $HOST_LABEL MCP Connector is not installed at this scope."
|
||||
fi
|
||||
|
||||
echo "Adobe's shared PlayerDebugMode setting was left unchanged."
|
||||
echo "Remove the MCP server from your AI client's configuration separately if you no longer use it."
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const requested = new Set(process.argv.slice(2));
|
||||
const allowed = new Set(["--check"]);
|
||||
const unknown = [...requested].filter((argument) => !allowed.has(argument));
|
||||
|
||||
if (unknown.length > 0) {
|
||||
console.error(`Unknown update option: ${unknown.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
return execFileSync(command, args, {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
...options,
|
||||
}).trim();
|
||||
}
|
||||
|
||||
function runInherited(command, args) {
|
||||
execFileSync(command, args, { cwd: root, stdio: "inherit" });
|
||||
}
|
||||
|
||||
function npmInvocation() {
|
||||
const fromNpm = process.env.npm_execpath;
|
||||
if (fromNpm && existsSync(fromNpm)) return [process.execPath, [fromNpm]];
|
||||
|
||||
const candidates = [
|
||||
join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
|
||||
join(dirname(dirname(process.execPath)), "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
||||
];
|
||||
const npmCli = candidates.find((candidate) => existsSync(candidate));
|
||||
return npmCli ? [process.execPath, [npmCli]] : [process.platform === "win32" ? "npm.cmd" : "npm", []];
|
||||
}
|
||||
|
||||
function runNpm(args) {
|
||||
const [command, prefix] = npmInvocation();
|
||||
runInherited(command, [...prefix, ...args]);
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
console.error(`Source update stopped: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let repositoryRoot;
|
||||
try {
|
||||
repositoryRoot = run("git", ["rev-parse", "--show-toplevel"]);
|
||||
} catch {
|
||||
fail("this command must run from a Git clone of premiere-pro-mcp.");
|
||||
}
|
||||
|
||||
if (resolve(repositoryRoot) !== root) {
|
||||
fail("run this command from the premiere-pro-mcp repository root.");
|
||||
}
|
||||
|
||||
const workingTreeDirty = Boolean(run("git", ["status", "--porcelain"]));
|
||||
|
||||
try {
|
||||
runInherited("git", ["fetch", "origin", "--tags"]);
|
||||
const counts = run("git", ["rev-list", "--left-right", "--count", "HEAD...@{upstream}"])
|
||||
.split(/\s+/)
|
||||
.map(Number);
|
||||
const [ahead, behind] = counts;
|
||||
if (!Number.isInteger(ahead) || !Number.isInteger(behind)) {
|
||||
fail("could not compare this checkout with its configured upstream.");
|
||||
}
|
||||
|
||||
if (requested.has("--check")) {
|
||||
const status = behind > 0
|
||||
? `Source update available: ${behind} commit${behind === 1 ? "" : "s"} behind upstream.${ahead > 0 ? ` This checkout also has ${ahead} local commit${ahead === 1 ? "" : "s"}.` : ""}`
|
||||
: "Source checkout is up to date with its upstream.";
|
||||
console.log(`${status}${workingTreeDirty ? " Local changes were detected, so automatic source update will refuse to run." : ""}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (workingTreeDirty) {
|
||||
fail("your checkout has uncommitted or untracked files. Commit, stash, or move them before updating.");
|
||||
}
|
||||
|
||||
if (behind === 0) {
|
||||
console.log("Source checkout is already up to date. To repair the connector, run npm run install-cep after fully quitting Premiere.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (ahead > 0) {
|
||||
fail("this checkout has local commits. Update or merge it manually so no work is overwritten.");
|
||||
}
|
||||
|
||||
console.log("Updating source, dependencies, build output, and the Premiere connector...");
|
||||
runInherited("git", ["merge", "--ff-only", "@{upstream}"]);
|
||||
runNpm(["ci"]);
|
||||
runNpm(["run", "build"]);
|
||||
runInherited(process.execPath, ["dist/index.js", "--install-cep"]);
|
||||
console.log("Update complete. Restart Premiere and your MCP client, then run verify_premiere_connection before editing.");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error && error.message ? error.message : "an update command failed.";
|
||||
fail(message);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export const UXP_HYBRID_ADDON_RECEIPT_SCHEMA_VERSION = 2;
|
||||
export const UXP_HYBRID_ADDON_RECEIPT_LEGACY_SCHEMA_VERSION = 1;
|
||||
export const UXP_HYBRID_ADDON_AUTHORITY_URL = "https://developer.adobe.com/premiere-pro/uxp/plugins/hybrid-plugins/build";
|
||||
export const UXP_HYBRID_ADDON_ENTRYPOINT_PATH = "main.js";
|
||||
export const UXP_HYBRID_ADDON_TARGETS = Object.freeze([
|
||||
Object.freeze({ target: "mac-x64", pathPrefix: "mac/x64" }),
|
||||
Object.freeze({ target: "mac-arm64", pathPrefix: "mac/arm64" }),
|
||||
Object.freeze({ target: "win-x64", pathPrefix: "win/x64" }),
|
||||
]);
|
||||
|
||||
export const UXP_HYBRID_ADDON_RECEIPT_LEGACY_SEMANTICS = Object.freeze({
|
||||
listed: "Each listed item is a required UXP Hybrid addon artifact observed in a locally supplied development plugin bundle. Paths are relative to that bundle and binary or manifest contents are not copied into this receipt.",
|
||||
doesNotEstablish: "This receipt does not establish Adobe entitlement, SDK compilation, binary architecture, code-signing or notarization validity, UDT loading, MCP exposure, or licensed-host behavior.",
|
||||
});
|
||||
|
||||
export const UXP_HYBRID_ADDON_RECEIPT_SEMANTICS = Object.freeze({
|
||||
listed: "Each listed addon artifact and root main.js entrypoint is observed in a locally supplied development plugin bundle. Paths are relative to that bundle and binary, manifest, or entrypoint contents are not copied or parsed by this receipt.",
|
||||
doesNotEstablish: UXP_HYBRID_ADDON_RECEIPT_LEGACY_SEMANTICS.doesNotEstablish,
|
||||
});
|
||||
|
||||
export function compareUxpHybridPaths(left, right) {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export const UXP_HYBRID_CCX_RECEIPT_LEGACY_SCHEMA_VERSION = 1;
|
||||
export const UXP_HYBRID_CCX_RECEIPT_SCHEMA_VERSION = 2;
|
||||
export const UXP_HYBRID_CCX_AUTHORITY_URL = "https://developer.adobe.com/premiere-pro/uxp/plugins/distribution/package/";
|
||||
|
||||
export const UXP_HYBRID_CCX_RECEIPT_SEMANTICS = Object.freeze({
|
||||
listed: "This receipt records the SHA-256 identity of a locally supplied CCX ZIP archive, a one-way digest of its complete safe ZIP entry-name set, and confirms that its manifest facts, root main.js entrypoint, and required UXP Hybrid addon artifacts exactly match a supplied schema-v2 addon-layout receipt. It does not copy archive, entry names, manifest, entrypoint, or binary contents into the receipt.",
|
||||
doesNotEstablish: "This receipt does not establish that UXP Developer Tool created the archive, that a manifest ID matches an Adobe Developer Distribution portal record, SDK compilation, binary architecture, code-signing or notarization validity, installation, UDT loading, MCP exposure, or licensed-host behavior.",
|
||||
});
|
||||
|
||||
export const UXP_HYBRID_CCX_RECEIPT_LEGACY_SEMANTICS = Object.freeze({
|
||||
listed: "This receipt records the SHA-256 identity of a locally supplied CCX ZIP archive and confirms that its manifest facts, root main.js entrypoint, and required UXP Hybrid addon artifacts exactly match a supplied schema-v2 addon-layout receipt. It does not copy archive, manifest, entrypoint, or binary contents into the receipt.",
|
||||
doesNotEstablish: UXP_HYBRID_CCX_RECEIPT_SEMANTICS.doesNotEstablish,
|
||||
});
|
||||
|
||||
export function compareUxpHybridCcxPaths(left, right) {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
hashZipEntry,
|
||||
inspectZipArchive,
|
||||
readZipEntry,
|
||||
sha256File,
|
||||
} from "./read-zip-archive.mjs";
|
||||
import {
|
||||
UXP_HYBRID_ADDON_ENTRYPOINT_PATH,
|
||||
UXP_HYBRID_ADDON_RECEIPT_SCHEMA_VERSION,
|
||||
UXP_HYBRID_ADDON_TARGETS,
|
||||
compareUxpHybridPaths,
|
||||
} from "./uxp-hybrid-addon-receipt-contract.mjs";
|
||||
import {
|
||||
UXP_HYBRID_CCX_AUTHORITY_URL,
|
||||
UXP_HYBRID_CCX_RECEIPT_LEGACY_SCHEMA_VERSION,
|
||||
UXP_HYBRID_CCX_RECEIPT_LEGACY_SEMANTICS,
|
||||
UXP_HYBRID_CCX_RECEIPT_SCHEMA_VERSION,
|
||||
UXP_HYBRID_CCX_RECEIPT_SEMANTICS,
|
||||
compareUxpHybridCcxPaths,
|
||||
} from "./uxp-hybrid-ccx-receipt-contract.mjs";
|
||||
import {
|
||||
canonicalNativeSdkHeaderInventorySha256,
|
||||
verifyNativeSdkHeaderInventory,
|
||||
} from "./verify-native-sdk-header-inventory.mjs";
|
||||
import {
|
||||
canonicalUxpHybridAddonReceiptSha256,
|
||||
verifyUxpHybridAddonReceipt,
|
||||
} from "./verify-uxp-hybrid-addon-receipt.mjs";
|
||||
|
||||
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
||||
const MAX_ARCHIVE_BYTES = 0xffff_ffff;
|
||||
const MAX_MANIFEST_BYTES = 1024 * 1024;
|
||||
const MAX_ENTRYPOINT_BYTES = 16 * 1024 * 1024;
|
||||
const MAX_ARTIFACT_BYTES = 2 ** 31;
|
||||
const MAX_MANIFEST_ID_LENGTH = 512;
|
||||
|
||||
function receiptError(message) {
|
||||
const error = new Error(message);
|
||||
error.code = "UXP_HYBRID_CCX_RECEIPT_INVALID";
|
||||
return error;
|
||||
}
|
||||
|
||||
function record(value, label, expectedKeys) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw receiptError(`${label} must be an object`);
|
||||
const keys = Object.keys(value).sort(compareUxpHybridCcxPaths);
|
||||
const expected = [...expectedKeys].sort(compareUxpHybridCcxPaths);
|
||||
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
|
||||
throw receiptError(`${label} must contain only the documented receipt fields`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function nonEmptyString(value, label, maximum = MAX_MANIFEST_ID_LENGTH) {
|
||||
if (typeof value !== "string" || !value.trim() || value.length > maximum || value.includes("\0")) {
|
||||
throw receiptError(`${label} must be a non-empty string of at most ${maximum} characters`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function sha256(value, label) {
|
||||
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
||||
throw receiptError(`${label} must be a lowercase SHA-256 hex digest`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function positiveInteger(value, label, maximum) {
|
||||
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) {
|
||||
throw receiptError(`${label} must be a positive safe integer no larger than ${maximum}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function nonNegativeInteger(value, label, maximum) {
|
||||
if (!Number.isSafeInteger(value) || value < 0 || value > maximum) {
|
||||
throw receiptError(`${label} must be a non-negative safe integer no larger than ${maximum}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function addonName(value, label = "manifest.addonName") {
|
||||
const name = nonEmptyString(value, label, 128);
|
||||
if (!/^[A-Za-z0-9._-]+\.uxpaddon$/.test(name)) throw receiptError(`${label} must be a simple .uxpaddon filename`);
|
||||
return name;
|
||||
}
|
||||
|
||||
function validateManifestFacts(value, label, includeArchiveIdentity) {
|
||||
const fields = includeArchiveIdentity
|
||||
? ["bytes", "sha256", "idSha256", "idLength", "manifestVersion", "hostApp", "hostMinVersion", "addonName", "enableAddon"]
|
||||
: ["manifestVersion", "hostApp", "hostMinVersion", "addonName", "enableAddon"];
|
||||
const manifest = record(value, label, fields);
|
||||
if (includeArchiveIdentity) {
|
||||
positiveInteger(manifest.bytes, `${label}.bytes`, MAX_MANIFEST_BYTES);
|
||||
sha256(manifest.sha256, `${label}.sha256`);
|
||||
sha256(manifest.idSha256, `${label}.idSha256`);
|
||||
positiveInteger(manifest.idLength, `${label}.idLength`, MAX_MANIFEST_ID_LENGTH);
|
||||
}
|
||||
if (!Number.isInteger(manifest.manifestVersion) || manifest.manifestVersion < 6) {
|
||||
throw receiptError(`${label}.manifestVersion must be 6 or newer`);
|
||||
}
|
||||
if (manifest.hostApp !== "premierepro") throw receiptError(`${label}.hostApp must be premierepro`);
|
||||
if (!/^\d+\.\d+\.\d+$/.test(String(manifest.hostMinVersion || ""))) {
|
||||
throw receiptError(`${label}.hostMinVersion must be a semantic version`);
|
||||
}
|
||||
const name = addonName(manifest.addonName, `${label}.addonName`);
|
||||
if (manifest.enableAddon !== true) throw receiptError(`${label}.enableAddon must be true`);
|
||||
return Object.freeze({
|
||||
manifestVersion: manifest.manifestVersion,
|
||||
hostApp: manifest.hostApp,
|
||||
hostMinVersion: manifest.hostMinVersion,
|
||||
addonName: name,
|
||||
enableAddon: true,
|
||||
});
|
||||
}
|
||||
|
||||
function sameManifestFacts(left, right, label) {
|
||||
for (const key of ["manifestVersion", "hostApp", "hostMinVersion", "addonName", "enableAddon"]) {
|
||||
if (left[key] !== right[key]) throw receiptError(`${label}.${key} must match the addon-layout receipt`);
|
||||
}
|
||||
}
|
||||
|
||||
function verifyCurrentAddonReceipt(addonReceipt, sdkHeaderReceipt) {
|
||||
if (!addonReceipt || addonReceipt.schemaVersion !== UXP_HYBRID_ADDON_RECEIPT_SCHEMA_VERSION) {
|
||||
throw receiptError(`addonReceipt must use schemaVersion ${UXP_HYBRID_ADDON_RECEIPT_SCHEMA_VERSION}`);
|
||||
}
|
||||
const addon = verifyUxpHybridAddonReceipt(addonReceipt, sdkHeaderReceipt === undefined ? {} : { sdkHeaderReceipt });
|
||||
if (!("addonBytes" in addon) || addon.entrypoints !== 1) {
|
||||
throw receiptError("addonReceipt must contain the current root main.js entrypoint accounting");
|
||||
}
|
||||
return addon;
|
||||
}
|
||||
|
||||
function canonicalize(value) {
|
||||
if (Array.isArray(value)) return value.map(canonicalize);
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(Object.keys(value).sort(compareUxpHybridCcxPaths).map((key) => [key, canonicalize(value[key])]));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function expectedArchivePaths(addonReceipt) {
|
||||
return ["manifest.json", UXP_HYBRID_ADDON_ENTRYPOINT_PATH, ...addonReceipt.artifacts.map((artifact) => artifact.path)];
|
||||
}
|
||||
|
||||
function archiveManifest(buffer) {
|
||||
let parsed;
|
||||
try { parsed = JSON.parse(buffer.toString("utf8")); } catch { throw receiptError("CCX archive manifest.json must be readable JSON"); }
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw receiptError("CCX archive manifest.json must be an object");
|
||||
const id = nonEmptyString(parsed.id, "CCX archive manifest.json id");
|
||||
if (id.trim() !== id) throw receiptError("CCX archive manifest.json id must not have surrounding whitespace");
|
||||
const facts = validateManifestFacts({
|
||||
bytes: buffer.length,
|
||||
sha256: createHash("sha256").update(buffer).digest("hex"),
|
||||
idSha256: createHash("sha256").update(id).digest("hex"),
|
||||
idLength: id.length,
|
||||
manifestVersion: parsed.manifestVersion,
|
||||
hostApp: parsed.host?.app,
|
||||
hostMinVersion: parsed.host?.minVersion,
|
||||
addonName: parsed.addon?.name,
|
||||
enableAddon: parsed.requiredPermissions?.enableAddon,
|
||||
}, "manifest", true);
|
||||
return Object.freeze({
|
||||
bytes: buffer.length,
|
||||
sha256: createHash("sha256").update(buffer).digest("hex"),
|
||||
idSha256: createHash("sha256").update(id).digest("hex"),
|
||||
idLength: id.length,
|
||||
...facts,
|
||||
});
|
||||
}
|
||||
|
||||
function verifyArchiveReceiptAgainstAddon(receipt, addonReceipt, sdkHeaderReceipt) {
|
||||
const addon = verifyCurrentAddonReceipt(addonReceipt, sdkHeaderReceipt);
|
||||
const manifest = validateManifestFacts(receipt.manifest, "manifest", true);
|
||||
sameManifestFacts(manifest, addonReceipt.manifest, "manifest");
|
||||
if (receipt.source.sdkVersion !== addonReceipt.source.sdkVersion) throw receiptError("source.sdkVersion must match the addon-layout receipt");
|
||||
if (receipt.source.sdkHeaderReceiptSha256 !== addonReceipt.source.sdkHeaderReceiptSha256) {
|
||||
throw receiptError("source.sdkHeaderReceiptSha256 must match the addon-layout receipt");
|
||||
}
|
||||
if (receipt.source.addonReceiptSha256 !== canonicalUxpHybridAddonReceiptSha256(addonReceipt)) {
|
||||
throw receiptError("source.addonReceiptSha256 must match the addon-layout receipt");
|
||||
}
|
||||
if (receipt.stats.artifacts !== addon.artifacts || receipt.stats.addonBytes !== addon.addonBytes ||
|
||||
receipt.stats.entrypoints !== addon.entrypoints || receipt.stats.entrypointBytes !== addon.entrypointBytes) {
|
||||
throw receiptError("stats must match the addon-layout receipt");
|
||||
}
|
||||
return addon;
|
||||
}
|
||||
|
||||
function validateArchiveContents(value) {
|
||||
const contents = record(value, "contents", ["entries", "files", "directories", "pathSetSha256"]);
|
||||
const entries = positiveInteger(contents.entries, "contents.entries", 100_000);
|
||||
const files = nonNegativeInteger(contents.files, "contents.files", entries);
|
||||
const directories = nonNegativeInteger(contents.directories, "contents.directories", entries);
|
||||
if (files + directories !== entries) throw receiptError("contents file and directory totals must match entries");
|
||||
sha256(contents.pathSetSha256, "contents.pathSetSha256");
|
||||
return Object.freeze({ entries, files, directories, pathSetSha256: contents.pathSetSha256 });
|
||||
}
|
||||
|
||||
export function verifyUxpHybridCcxReceipt(document, options = {}) {
|
||||
const legacy = document?.schemaVersion === UXP_HYBRID_CCX_RECEIPT_LEGACY_SCHEMA_VERSION;
|
||||
const receipt = record(document, "receipt", legacy
|
||||
? ["schemaVersion", "source", "archive", "manifest", "semantics", "stats"]
|
||||
: ["schemaVersion", "source", "archive", "contents", "manifest", "semantics", "stats"]);
|
||||
if (!legacy && receipt.schemaVersion !== UXP_HYBRID_CCX_RECEIPT_SCHEMA_VERSION) {
|
||||
throw receiptError(`schemaVersion must be ${UXP_HYBRID_CCX_RECEIPT_LEGACY_SCHEMA_VERSION} or ${UXP_HYBRID_CCX_RECEIPT_SCHEMA_VERSION}`);
|
||||
}
|
||||
const source = record(receipt.source, "source", ["sdk", "sdkVersion", "sdkHeaderReceiptSha256", "addonReceiptSha256", "authorityUrl"]);
|
||||
if (source.sdk !== "uxp-hybrid") throw receiptError("source.sdk must be uxp-hybrid");
|
||||
nonEmptyString(source.sdkVersion, "source.sdkVersion", 128);
|
||||
sha256(source.sdkHeaderReceiptSha256, "source.sdkHeaderReceiptSha256");
|
||||
sha256(source.addonReceiptSha256, "source.addonReceiptSha256");
|
||||
if (source.authorityUrl !== UXP_HYBRID_CCX_AUTHORITY_URL) {
|
||||
throw receiptError("source.authorityUrl must match the documented CCX packaging guide");
|
||||
}
|
||||
|
||||
const archive = record(receipt.archive, "archive", ["format", "bytes", "sha256"]);
|
||||
if (archive.format !== "zip") throw receiptError("archive.format must be zip");
|
||||
positiveInteger(archive.bytes, "archive.bytes", MAX_ARCHIVE_BYTES);
|
||||
sha256(archive.sha256, "archive.sha256");
|
||||
|
||||
validateManifestFacts(receipt.manifest, "manifest", true);
|
||||
|
||||
if (!legacy) validateArchiveContents(receipt.contents);
|
||||
|
||||
const semantics = record(receipt.semantics, "semantics", ["listed", "doesNotEstablish"]);
|
||||
const requiredSemantics = legacy ? UXP_HYBRID_CCX_RECEIPT_LEGACY_SEMANTICS : UXP_HYBRID_CCX_RECEIPT_SEMANTICS;
|
||||
if (semantics.listed !== requiredSemantics.listed || semantics.doesNotEstablish !== requiredSemantics.doesNotEstablish) {
|
||||
throw receiptError("semantics must retain the documented evidence boundary");
|
||||
}
|
||||
|
||||
const stats = record(receipt.stats, "stats", ["artifacts", "addonBytes", "entrypoints", "entrypointBytes"]);
|
||||
if (stats.artifacts !== UXP_HYBRID_ADDON_TARGETS.length) throw receiptError(`stats.artifacts must be ${UXP_HYBRID_ADDON_TARGETS.length}`);
|
||||
positiveInteger(stats.addonBytes, "stats.addonBytes", MAX_ARTIFACT_BYTES * UXP_HYBRID_ADDON_TARGETS.length);
|
||||
if (stats.entrypoints !== 1) throw receiptError("stats.entrypoints must be 1");
|
||||
positiveInteger(stats.entrypointBytes, "stats.entrypointBytes", MAX_ENTRYPOINT_BYTES);
|
||||
|
||||
if (options.addonReceipt !== undefined) verifyArchiveReceiptAgainstAddon(receipt, options.addonReceipt, options.sdkHeaderReceipt);
|
||||
return Object.freeze({ artifacts: stats.artifacts, addonBytes: stats.addonBytes, entrypoints: stats.entrypoints, entrypointBytes: stats.entrypointBytes });
|
||||
}
|
||||
|
||||
export function canonicalUxpHybridCcxReceiptSha256(document) {
|
||||
verifyUxpHybridCcxReceipt(document);
|
||||
return createHash("sha256").update(JSON.stringify(canonicalize(document))).digest("hex");
|
||||
}
|
||||
|
||||
export async function buildUxpHybridCcxReceipt(options) {
|
||||
if (!options?.addonReceipt || typeof options.addonReceipt !== "object") throw receiptError("addonReceipt is required");
|
||||
if (!options?.sdkHeaderReceipt || typeof options.sdkHeaderReceipt !== "object") throw receiptError("sdkHeaderReceipt is required");
|
||||
if (typeof options.ccxPath !== "string" || !options.ccxPath.trim() || options.ccxPath.includes("\0")) {
|
||||
throw receiptError("ccxPath must be a non-empty path");
|
||||
}
|
||||
const addon = verifyCurrentAddonReceipt(options.addonReceipt, options.sdkHeaderReceipt);
|
||||
const archive = await inspectZipArchive(options.ccxPath, expectedArchivePaths(options.addonReceipt));
|
||||
const manifestEntry = archive.selected.get("manifest.json");
|
||||
const entrypointEntry = archive.selected.get(UXP_HYBRID_ADDON_ENTRYPOINT_PATH);
|
||||
const manifestContents = await readZipEntry(options.ccxPath, archive.bytes, manifestEntry, MAX_MANIFEST_BYTES);
|
||||
const manifest = archiveManifest(manifestContents.buffer);
|
||||
sameManifestFacts(manifest, options.addonReceipt.manifest, "manifest");
|
||||
|
||||
const entrypoint = await hashZipEntry(options.ccxPath, archive.bytes, entrypointEntry, MAX_ENTRYPOINT_BYTES);
|
||||
if (entrypoint.bytes !== options.addonReceipt.entrypoint.bytes || entrypoint.sha256 !== options.addonReceipt.entrypoint.sha256) {
|
||||
throw receiptError("CCX archive main.js must match the addon-layout receipt");
|
||||
}
|
||||
for (const artifact of options.addonReceipt.artifacts) {
|
||||
const entry = archive.selected.get(artifact.path);
|
||||
const observed = await hashZipEntry(options.ccxPath, archive.bytes, entry, MAX_ARTIFACT_BYTES);
|
||||
if (observed.bytes !== artifact.bytes || observed.sha256 !== artifact.sha256) {
|
||||
throw receiptError(`CCX archive addon artifact ${artifact.target} must match the addon-layout receipt`);
|
||||
}
|
||||
}
|
||||
|
||||
const receipt = {
|
||||
schemaVersion: UXP_HYBRID_CCX_RECEIPT_SCHEMA_VERSION,
|
||||
source: {
|
||||
sdk: "uxp-hybrid",
|
||||
sdkVersion: options.addonReceipt.source.sdkVersion,
|
||||
sdkHeaderReceiptSha256: canonicalNativeSdkHeaderInventorySha256(options.sdkHeaderReceipt),
|
||||
addonReceiptSha256: canonicalUxpHybridAddonReceiptSha256(options.addonReceipt),
|
||||
authorityUrl: UXP_HYBRID_CCX_AUTHORITY_URL,
|
||||
},
|
||||
archive: { format: "zip", bytes: archive.bytes, sha256: await sha256File(options.ccxPath) },
|
||||
contents: archive.summary,
|
||||
manifest,
|
||||
semantics: UXP_HYBRID_CCX_RECEIPT_SEMANTICS,
|
||||
stats: {
|
||||
artifacts: addon.artifacts,
|
||||
addonBytes: addon.addonBytes,
|
||||
entrypoints: addon.entrypoints,
|
||||
entrypointBytes: addon.entrypointBytes,
|
||||
},
|
||||
};
|
||||
verifyUxpHybridCcxReceipt(receipt, { addonReceipt: options.addonReceipt, sdkHeaderReceipt: options.sdkHeaderReceipt });
|
||||
return receipt;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const displayName = "MCP for Adobe Premiere Pro";
|
||||
const retiredDisplayNames = ["Premiere Pro MCP", "Premiere Pro MCP Bridge", "MCP Bridge"];
|
||||
|
||||
const surfaces = [
|
||||
{
|
||||
file: "cep-plugin/CSXS/manifest.xml",
|
||||
required: [
|
||||
`ExtensionBundleName=\"${displayName}\"`,
|
||||
`<Menu>${displayName}</Menu>`,
|
||||
],
|
||||
},
|
||||
{
|
||||
file: "cep-plugin/index.html",
|
||||
required: [`<title>${displayName}</title>`, `<h1>${displayName}</h1>`],
|
||||
},
|
||||
{
|
||||
file: "uxp-plugin/manifest.json",
|
||||
required: [
|
||||
`\"name\": \"${displayName}\"`,
|
||||
`\"label\": { \"default\": \"${displayName}\" }`,
|
||||
],
|
||||
},
|
||||
{
|
||||
file: "uxp-plugin/index.html",
|
||||
required: [`<h3>${displayName}</h3>`],
|
||||
},
|
||||
{
|
||||
file: "claude-desktop/manifest.json",
|
||||
required: [`\"display_name\": \"${displayName}\"`],
|
||||
},
|
||||
{
|
||||
file: "landing/lib/product.ts",
|
||||
required: [`name: \"${displayName}\"`],
|
||||
},
|
||||
{
|
||||
file: "landing/app/manifest.ts",
|
||||
required: [`name: \"${displayName}\"`],
|
||||
},
|
||||
{
|
||||
file: "README.md",
|
||||
required: [`# ${displayName}`],
|
||||
},
|
||||
];
|
||||
|
||||
const errors = [];
|
||||
|
||||
for (const surface of surfaces) {
|
||||
const source = await readFile(path.join(root, surface.file), "utf8");
|
||||
for (const required of surface.required) {
|
||||
if (!source.includes(required)) {
|
||||
errors.push(`${surface.file} is missing required display name evidence: ${required}`);
|
||||
}
|
||||
}
|
||||
for (const retired of retiredDisplayNames) {
|
||||
if (source.includes(retired)) {
|
||||
errors.push(`${surface.file} still exposes retired display name: ${retired}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Adobe Marketplace branding validation failed:\n- ${errors.join("\n- ")}`);
|
||||
}
|
||||
|
||||
console.log(`Validated Marketplace display-name consistency across ${surfaces.length} source surfaces.`);
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { access, readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
const MCPB_SCHEMA =
|
||||
"https://raw.githubusercontent.com/modelcontextprotocol/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json";
|
||||
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
||||
const ADOBE_VERSION = /^\d+\.\d+\.\d+$/;
|
||||
|
||||
function fail(message) {
|
||||
throw new Error(`Distribution validation failed: ${message}`);
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) fail(message);
|
||||
}
|
||||
|
||||
async function readJson(file) {
|
||||
try {
|
||||
return JSON.parse(await readFile(file, "utf8"));
|
||||
} catch (error) {
|
||||
throw new Error(`Distribution validation failed: could not read ${file}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function assertFile(file, message) {
|
||||
try {
|
||||
await access(file);
|
||||
} catch {
|
||||
fail(message ?? `missing ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateClaudeManifest(manifest, packageJson) {
|
||||
assert(manifest.$schema === MCPB_SCHEMA, "Claude manifest must reference the MCPB v0.4 schema");
|
||||
assert(manifest.manifest_version === "0.4", "Claude manifest must use manifest_version 0.4");
|
||||
assert(!("dxt_version" in manifest), "Claude manifest must not use deprecated dxt_version");
|
||||
assert(manifest.name === packageJson.name, "Claude manifest name must match package.json");
|
||||
assert(
|
||||
manifest.version === packageJson.version,
|
||||
"Claude manifest version must match package.json",
|
||||
);
|
||||
assert(SEMVER.test(manifest.version), "Claude manifest version must be SemVer");
|
||||
assert(typeof manifest.description === "string" && manifest.description.length > 0, "Claude manifest needs a description");
|
||||
assert(
|
||||
manifest.author && typeof manifest.author.name === "string" && manifest.author.name.length > 0,
|
||||
"Claude manifest needs an author name",
|
||||
);
|
||||
assert(manifest.server?.type === "node", "Claude bundle must declare a Node server");
|
||||
assert(
|
||||
manifest.server?.entry_point === "server/dist/index.js",
|
||||
"Claude bundle entry point must be server/dist/index.js",
|
||||
);
|
||||
assert(
|
||||
manifest.server?.mcp_config?.command === "node",
|
||||
"Claude bundle must launch with Claude Desktop's Node runtime",
|
||||
);
|
||||
assert(
|
||||
JSON.stringify(manifest.server?.mcp_config?.args) ===
|
||||
JSON.stringify(["${__dirname}/server/dist/index.js"]),
|
||||
"Claude bundle command arguments must use a bundle-relative entry point",
|
||||
);
|
||||
const tokenConfig = manifest.user_config?.premiere_uxp_token;
|
||||
assert(
|
||||
tokenConfig?.type === "string" &&
|
||||
tokenConfig?.sensitive === true &&
|
||||
tokenConfig?.required === true,
|
||||
"Claude bundle must require a sensitive Premiere UXP token configuration",
|
||||
);
|
||||
assert(
|
||||
manifest.server?.mcp_config?.env?.PREMIERE_UXP_TOKEN ===
|
||||
"${user_config.premiere_uxp_token}",
|
||||
"Claude bundle must map the configured Premiere UXP token into the server environment",
|
||||
);
|
||||
const protocolModeConfig = manifest.user_config?.premiere_mcp_protocol_mode;
|
||||
assert(
|
||||
protocolModeConfig?.type === "string" && protocolModeConfig?.required === false,
|
||||
"Claude bundle must expose an optional MCP protocol-mode fallback configuration",
|
||||
);
|
||||
assert(
|
||||
manifest.server?.mcp_config?.env?.PREMIERE_MCP_PROTOCOL_MODE ===
|
||||
"${user_config.premiere_mcp_protocol_mode}",
|
||||
"Claude bundle must map the configured MCP protocol mode into the server environment",
|
||||
);
|
||||
assert(
|
||||
Array.isArray(manifest.compatibility?.platforms) &&
|
||||
manifest.compatibility.platforms.length === 2 &&
|
||||
manifest.compatibility.platforms.includes("darwin") &&
|
||||
manifest.compatibility.platforms.includes("win32"),
|
||||
"Claude bundle must target supported Premiere platforms only (darwin and win32)",
|
||||
);
|
||||
assert(
|
||||
manifest.compatibility?.runtimes?.node === packageJson.engines?.node,
|
||||
"Claude bundle Node compatibility must match package.json engines.node",
|
||||
);
|
||||
}
|
||||
|
||||
export async function validateClaudeSource({ projectRoot = root } = {}) {
|
||||
const packageJson = await readJson(path.join(projectRoot, "package.json"));
|
||||
const manifest = await readJson(path.join(projectRoot, "claude-desktop", "manifest.json"));
|
||||
validateClaudeManifest(manifest, packageJson);
|
||||
return { manifest, packageJson };
|
||||
}
|
||||
|
||||
export async function validateClaudeStage(stage) {
|
||||
const manifest = await readJson(path.join(stage, "manifest.json"));
|
||||
const packageJson = await readJson(path.join(stage, "package.json"));
|
||||
validateClaudeManifest(manifest, packageJson);
|
||||
await assertFile(
|
||||
path.join(stage, "server", "dist", "index.js"),
|
||||
"Claude bundle stage is missing server/dist/index.js; run the TypeScript build first",
|
||||
);
|
||||
for (const dependency of Object.keys(packageJson.dependencies ?? {})) {
|
||||
await assertFile(
|
||||
path.join(stage, "node_modules", dependency, "package.json"),
|
||||
`Claude bundle stage is missing production dependency ${dependency}`,
|
||||
);
|
||||
}
|
||||
return { manifest, packageJson };
|
||||
}
|
||||
|
||||
export function validateUxpManifest(manifest, packageJson, { expectedId } = {}) {
|
||||
assert(manifest.manifestVersion === 5, "UXP manifestVersion must be 5");
|
||||
assert(typeof manifest.id === "string" && manifest.id.trim().length > 0, "UXP manifest needs a plugin id");
|
||||
assert(!/\s/.test(manifest.id), "UXP plugin id must not contain whitespace");
|
||||
if (expectedId) {
|
||||
assert(manifest.id === expectedId, "staged UXP plugin id did not match the requested distribution channel");
|
||||
}
|
||||
assert(manifest.version === packageJson.version, "UXP manifest version must match package.json");
|
||||
assert(ADOBE_VERSION.test(manifest.version), "UXP manifest version must use Adobe's major.minor.patch format");
|
||||
assert(manifest.host?.app === "premierepro", "UXP package must target Premiere Pro");
|
||||
assert(manifest.host?.minVersion === "25.6.0", "UXP package must keep the documented 25.6.0 minimum host version");
|
||||
assert(manifest.main === "index.html", "UXP package main entry must be index.html");
|
||||
assert(
|
||||
Array.isArray(manifest.entrypoints) &&
|
||||
manifest.entrypoints.some(
|
||||
(entrypoint) => entrypoint.type === "panel" && entrypoint.id === "mcpBridgePanel",
|
||||
),
|
||||
"UXP package must include the MCP for Adobe Premiere Pro panel entry point",
|
||||
);
|
||||
assert(
|
||||
manifest.requiredPermissions?.localFileSystem === "request",
|
||||
"UXP package must use operator-requested local file access",
|
||||
);
|
||||
const domains = manifest.requiredPermissions?.network?.domains;
|
||||
assert(
|
||||
domains === "all",
|
||||
"UXP package must use Adobe's compatible network permission; runtime code remains the loopback-only authority",
|
||||
);
|
||||
}
|
||||
|
||||
export async function validateUxpSource({ projectRoot = root, expectedId } = {}) {
|
||||
const packageJson = await readJson(path.join(projectRoot, "package.json"));
|
||||
const pluginRoot = path.join(projectRoot, "uxp-plugin");
|
||||
const manifest = await readJson(path.join(pluginRoot, "manifest.json"));
|
||||
validateUxpManifest(manifest, packageJson, { expectedId });
|
||||
await assertFile(path.join(pluginRoot, manifest.main), "UXP package is missing its main entry file");
|
||||
return { manifest, packageJson, pluginRoot };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const requested = new Set(process.argv.slice(2));
|
||||
const valid = new Set(["--all", "--claude", "--uxp"]);
|
||||
for (const argument of requested) {
|
||||
assert(valid.has(argument), `unknown argument ${argument}; use --all, --claude, or --uxp`);
|
||||
}
|
||||
const validateAll = requested.size === 0 || requested.has("--all");
|
||||
if (validateAll || requested.has("--claude")) {
|
||||
await validateClaudeSource();
|
||||
console.log("Validated Claude Desktop MCPB source manifest.");
|
||||
}
|
||||
if (validateAll || requested.has("--uxp")) {
|
||||
await validateUxpSource();
|
||||
console.log("Validated UXP CCX source manifest.");
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
main().catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(scriptDir, "..");
|
||||
const matrix = JSON.parse(fs.readFileSync(path.join(root, "docs", "licensed-host-sweep.matrix.json"), "utf8"));
|
||||
const sweepCasesById = new Map(matrix.cases.map((entry) => [entry.id, entry]));
|
||||
const reportPath = process.argv[2];
|
||||
if (!reportPath) {
|
||||
throw new Error("Usage: node scripts/validate-licensed-host-report.mjs <redacted-report.json>");
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(reportPath);
|
||||
const parsedReport = JSON.parse(fs.readFileSync(resolvedPath, "utf8"));
|
||||
const report = parsedReport && typeof parsedReport === "object" && !Array.isArray(parsedReport) ? parsedReport : {};
|
||||
const errors = [];
|
||||
const allowedStatuses = new Set(["passed", "failed", "unsupported", "not_run"]);
|
||||
const allowedEvidenceKinds = new Set([
|
||||
"host_state",
|
||||
"panel_state",
|
||||
"before_state",
|
||||
"after_state",
|
||||
"structured_response",
|
||||
"undo",
|
||||
"artifact_check",
|
||||
]);
|
||||
const isSweep = report.schemaVersion === "premiere-pro-mcp.licensed-host-sweep.v1";
|
||||
const cases = Array.isArray(report.cases) ? report.cases : [];
|
||||
|
||||
function hasOnlyKeys(value, expectedKeys) {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
&& JSON.stringify(Object.keys(value).sort()) === JSON.stringify(expectedKeys);
|
||||
}
|
||||
|
||||
if (report.schemaVersion !== undefined && !isSweep) {
|
||||
errors.push("schemaVersion must be premiere-pro-mcp.licensed-host-sweep.v1 when supplied");
|
||||
}
|
||||
if (!/^[0-9a-f]{40}$/i.test(report.sourceCommit ?? "")) errors.push("sourceCommit must be a 40-character commit SHA");
|
||||
if (!new Set(["Windows", "macOS"]).has(report.host?.os)) errors.push("host.os must be Windows or macOS");
|
||||
if (typeof report.host?.premiereVersion !== "string" || !report.host.premiereVersion.trim()) errors.push("host.premiereVersion is required");
|
||||
if (!/^[0-9a-f]{7,64}$/i.test(report.host?.panelBuild ?? "")) errors.push("host.panelBuild must be a build hash");
|
||||
if (typeof report.fixture?.revision !== "string" || !report.fixture.revision.trim()) errors.push("fixture.revision is required");
|
||||
if (!/^[0-9a-f]{64}$/i.test(report.fixture?.sha256 ?? "")) errors.push("fixture.sha256 must be a SHA-256 hash");
|
||||
if (!Array.isArray(report.cases) || report.cases.length === 0) errors.push("cases must be a non-empty array");
|
||||
|
||||
if (isSweep) {
|
||||
const reportKeys = Object.keys(report).sort();
|
||||
const expectedKeys = ["cases", "fixture", "host", "schemaVersion", "sourceCommit", "sweep"];
|
||||
if (JSON.stringify(reportKeys) !== JSON.stringify(expectedKeys)) errors.push("sweep reports must not add unstructured fields");
|
||||
if (!hasOnlyKeys(report.host, ["os", "panelBuild", "premiereVersion"])) {
|
||||
errors.push("sweep host must contain only os, premiereVersion, and panelBuild");
|
||||
}
|
||||
if (!hasOnlyKeys(report.fixture, ["revision", "sha256"])) {
|
||||
errors.push("sweep fixture must contain only revision and sha256");
|
||||
}
|
||||
if (!hasOnlyKeys(report.sweep, ["matrixId", "matrixVersion"])) {
|
||||
errors.push("sweep must contain only matrixId and matrixVersion");
|
||||
}
|
||||
if (report.sweep?.matrixId !== matrix.id || report.sweep?.matrixVersion !== "1") {
|
||||
errors.push("sweep must identify the checked-in core-connection-and-edit-v1 matrix version 1");
|
||||
}
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(report.fixture?.revision ?? "")) {
|
||||
errors.push("sweep fixture.revision must be a non-sensitive identifier");
|
||||
}
|
||||
}
|
||||
|
||||
const seenCaseIds = new Set();
|
||||
for (const entry of cases) {
|
||||
if (typeof entry?.id !== "string" || !entry.id.trim()) errors.push("each case requires an id");
|
||||
else if (seenCaseIds.has(entry.id)) errors.push(`case id is duplicated: ${entry.id}`);
|
||||
else seenCaseIds.add(entry.id);
|
||||
|
||||
if (!allowedStatuses.has(entry?.status)) errors.push(`case ${entry?.id ?? "<unknown>"} has an invalid status`);
|
||||
if (!Array.isArray(entry?.evidence)) errors.push(`case ${entry?.id ?? "<unknown>"} requires an evidence array`);
|
||||
|
||||
if (isSweep) {
|
||||
const matrixCase = sweepCasesById.get(entry?.id);
|
||||
if (!matrixCase) errors.push(`case ${entry?.id ?? "<unknown>"} is not in the checked-in sweep matrix`);
|
||||
if (!new Set(["read_only", "mutation"]).has(entry?.operationClass)) {
|
||||
errors.push(`case ${entry?.id ?? "<unknown>"} must identify its operationClass`);
|
||||
} else if (matrixCase && entry.operationClass !== matrixCase.operationClass) {
|
||||
errors.push(`case ${entry.id} operationClass does not match the sweep matrix`);
|
||||
}
|
||||
const caseKeys = Object.keys(entry ?? {}).sort();
|
||||
const expectedCaseKeys = ["evidence", "id", "operationClass", "status", "undoEvidence"];
|
||||
if (JSON.stringify(caseKeys) !== JSON.stringify(expectedCaseKeys)) errors.push(`case ${entry?.id ?? "<unknown>"} must not add raw response or path fields`);
|
||||
for (const evidence of entry?.evidence ?? []) {
|
||||
if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) {
|
||||
errors.push(`case ${entry?.id ?? "<unknown>"} evidence must use { kind, ref } references`);
|
||||
continue;
|
||||
}
|
||||
if (JSON.stringify(Object.keys(evidence).sort()) !== JSON.stringify(["kind", "ref"])) {
|
||||
errors.push(`case ${entry?.id ?? "<unknown>"} evidence must contain only kind and ref`);
|
||||
}
|
||||
if (!allowedEvidenceKinds.has(evidence.kind)) errors.push(`case ${entry?.id ?? "<unknown>"} has invalid evidence kind`);
|
||||
if (typeof evidence.ref !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(evidence.ref)) {
|
||||
errors.push(`case ${entry?.id ?? "<unknown>"} evidence refs must be opaque non-sensitive IDs`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entry?.status === "passed") {
|
||||
if (isSweep && entry.operationClass === "read_only") {
|
||||
const kinds = new Set((entry.evidence ?? []).map((evidence) => evidence?.kind));
|
||||
for (const kind of sweepCasesById.get(entry.id)?.requiredEvidenceKinds ?? []) {
|
||||
if (!kinds.has(kind)) errors.push(`passed read-only case ${entry.id} requires ${kind} evidence`);
|
||||
}
|
||||
if (entry.undoEvidence !== false) errors.push(`passed read-only case ${entry.id} must not claim undo evidence`);
|
||||
} else {
|
||||
if (entry.evidence.length < 3) errors.push(`passed case ${entry.id} requires before, after, and structured-response evidence`);
|
||||
if (entry.undoEvidence !== true) errors.push(`passed case ${entry.id} requires undoEvidence: true`);
|
||||
if (isSweep) {
|
||||
const kinds = new Set((entry.evidence ?? []).map((evidence) => evidence?.kind));
|
||||
for (const kind of sweepCasesById.get(entry.id)?.requiredEvidenceKinds ?? []) {
|
||||
if (!kinds.has(kind)) errors.push(`passed mutation case ${entry.id} requires ${kind} evidence`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const serialized = JSON.stringify(report);
|
||||
if (/([A-Za-z]:\\|\/Users\/|\/home\/|\/private\/|authorization|bearer\s+[A-Za-z0-9._-]+|(?:token|password|secret|cookie)\s*[:=])/i.test(serialized)) {
|
||||
errors.push("report appears to include a local path or credential-like value; redact it before sharing");
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Licensed-host report is invalid:\n- ${errors.join("\n- ")}`);
|
||||
}
|
||||
|
||||
const summary = {
|
||||
report: path.basename(resolvedPath),
|
||||
schemaVersion: report.schemaVersion ?? "legacy-editorial-report",
|
||||
sourceCommit: report.sourceCommit,
|
||||
host: report.host,
|
||||
totals: Object.fromEntries([...allowedStatuses].map((status) => [
|
||||
status,
|
||||
cases.filter((entry) => entry.status === status).length,
|
||||
])),
|
||||
};
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
@@ -0,0 +1,36 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = join(fileURLToPath(new URL("..", import.meta.url)));
|
||||
const readJson = (relativePath) => JSON.parse(readFileSync(join(root, relativePath), "utf8"));
|
||||
const packageJson = readJson("package.json");
|
||||
const manifest = readJson("registry/server.json");
|
||||
const readme = readFileSync(join(root, "README.md"), "utf8");
|
||||
const failures = [];
|
||||
|
||||
function expect(condition, message) {
|
||||
if (!condition) failures.push(message);
|
||||
}
|
||||
|
||||
const mcpName = packageJson.mcpName;
|
||||
const npmPackage = manifest.packages?.[0];
|
||||
|
||||
expect(typeof mcpName === "string" && /^io\.github\.leancoderkavy\/[a-z0-9-]+$/.test(mcpName), "package.json mcpName must use the leancoderkavy GitHub namespace");
|
||||
expect(manifest.$schema === "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "registry/server.json must use the current official registry schema");
|
||||
expect(manifest.name === mcpName, "registry/server.json name must match package.json mcpName");
|
||||
expect(manifest.version === packageJson.version, "registry/server.json version must match package.json version");
|
||||
expect(typeof manifest.description === "string" && manifest.description.length <= 100, "registry/server.json description must stay within the official registry's 100-character limit");
|
||||
expect(manifest.repository?.url === "https://github.com/leancoderkavy/premiere-pro-mcp", "registry/server.json must reference the canonical GitHub repository");
|
||||
expect(npmPackage?.registryType === "npm", "registry/server.json must describe an npm package");
|
||||
expect(npmPackage?.identifier === packageJson.name, "registry npm identifier must match package.json name");
|
||||
expect(npmPackage?.version === packageJson.version, "registry npm package version must match package.json version");
|
||||
expect(npmPackage?.transport?.type === "stdio", "registry transport must stay local stdio");
|
||||
expect(readme.includes(`<!-- mcp-name: ${mcpName} -->`), "README must retain the package mcp-name verification marker");
|
||||
|
||||
if (failures.length) {
|
||||
console.error(`MCP Registry metadata validation failed:\n- ${failures.join("\n- ")}`);
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`MCP Registry metadata is internally aligned for ${mcpName}@${packageJson.version}.`);
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const SCHEMA_VERSION = "project-intake-host-report/v1";
|
||||
const NOT_RUN_SHA256 = "0".repeat(64);
|
||||
const NOT_RUN_COMMIT = "0".repeat(40);
|
||||
const ALLOWED_STATUSES = new Set(["passed", "failed", "unsupported", "not_run"]);
|
||||
const CASE_REQUIREMENTS = {
|
||||
"PIP-CONNECT-001": {
|
||||
tool: "verify_premiere_connection",
|
||||
evidence: ["structured_connection_response"],
|
||||
assertions: { overall: "ready" },
|
||||
},
|
||||
"PIP-PREVIEW-001": {
|
||||
tool: "preview_project_intake",
|
||||
evidence: ["structured_preview_response"],
|
||||
assertions: {
|
||||
applied: false,
|
||||
pathDisclosure: "redacted",
|
||||
organizationPlanApplied: false,
|
||||
},
|
||||
},
|
||||
"PIP-NO-MUTATION-001": {
|
||||
tool: "preview_project_intake",
|
||||
evidence: ["before_project_panel", "after_project_panel", "structured_preview_response"],
|
||||
assertions: {
|
||||
projectMutated: false,
|
||||
projectSaved: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
const SENSITIVE_CONTENT = /(?:(?:^|[^A-Za-z0-9])[A-Za-z]:[\\/]|\\\\|file:\/\/|\/(?:Users|home|private)\/|authorization|bearer\s+[A-Za-z0-9._-]+|api[_-]?key|password|secret|access[_-]?token|refresh[_-]?token|project(?:Name|Path)|media(?:Name|Path)|transcript|prompt)/i;
|
||||
|
||||
function isObject(value) {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function hasOnlyKeys(value, allowedKeys, label, errors) {
|
||||
if (!isObject(value)) {
|
||||
errors.push(`${label} must be an object`);
|
||||
return false;
|
||||
}
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!allowedKeys.has(key)) errors.push(`${label} contains unsupported field: ${key}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isNonEmptyString(value, maxLength = 128) {
|
||||
return typeof value === "string" && value.trim().length > 0 && value.length <= maxLength;
|
||||
}
|
||||
|
||||
function isSha256(value) {
|
||||
return /^[0-9a-f]{64}$/i.test(value ?? "");
|
||||
}
|
||||
|
||||
function isCommit(value) {
|
||||
return /^[0-9a-f]{40}$/i.test(value ?? "");
|
||||
}
|
||||
|
||||
function isRfc3339(value) {
|
||||
return typeof value === "string" && !Number.isNaN(Date.parse(value)) && /(?:Z|[+-]\d\d:\d\d)$/.test(value);
|
||||
}
|
||||
|
||||
function serializedValues(value) {
|
||||
if (typeof value === "string") return value;
|
||||
if (Array.isArray(value)) return value.map(serializedValues).join("\n");
|
||||
if (isObject(value)) return Object.values(value).map(serializedValues).join("\n");
|
||||
return "";
|
||||
}
|
||||
|
||||
function validateEvidence(evidence, label, errors) {
|
||||
if (!Array.isArray(evidence)) {
|
||||
errors.push(`${label}.evidence must be an array`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const kinds = [];
|
||||
for (const [index, item] of evidence.entries()) {
|
||||
const evidenceLabel = `${label}.evidence[${index}]`;
|
||||
hasOnlyKeys(item, new Set(["kind", "reference", "sha256"]), evidenceLabel, errors);
|
||||
if (!isNonEmptyString(item?.kind, 64)) errors.push(`${evidenceLabel}.kind is required`);
|
||||
else kinds.push(item.kind);
|
||||
if (typeof item?.reference !== "string" || !/^evidence:\/\/[a-z0-9][a-z0-9._-]{0,127}$/i.test(item.reference)) {
|
||||
errors.push(`${evidenceLabel}.reference must be an opaque evidence:// reference`);
|
||||
}
|
||||
if (!isSha256(item?.sha256)) errors.push(`${evidenceLabel}.sha256 must be a SHA-256 hash`);
|
||||
}
|
||||
if (new Set(kinds).size !== kinds.length) errors.push(`${label}.evidence must not repeat a kind`);
|
||||
return kinds;
|
||||
}
|
||||
|
||||
function validatePassedCase(entry, requirement, errors) {
|
||||
const label = `case ${entry.id}`;
|
||||
if (!isRfc3339(entry.executedAt)) errors.push(`${label}.executedAt must be an RFC 3339 timestamp`);
|
||||
hasOnlyKeys(entry.assertions, new Set(["tool", ...Object.keys(requirement.assertions)]), `${label}.assertions`, errors);
|
||||
if (entry.assertions?.tool !== requirement.tool) errors.push(`${label}.assertions.tool must be ${requirement.tool}`);
|
||||
for (const [key, expected] of Object.entries(requirement.assertions)) {
|
||||
if (entry.assertions?.[key] !== expected) errors.push(`${label}.assertions.${key} must be ${JSON.stringify(expected)}`);
|
||||
}
|
||||
|
||||
const evidenceKinds = validateEvidence(entry.evidence, label, errors);
|
||||
for (const requiredKind of requirement.evidence) {
|
||||
if (!evidenceKinds.includes(requiredKind)) errors.push(`${label} requires ${requiredKind} evidence`);
|
||||
}
|
||||
for (const kind of evidenceKinds) {
|
||||
if (!requirement.evidence.includes(kind)) errors.push(`${label} has unsupported evidence kind: ${kind}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateReport(report) {
|
||||
const errors = [];
|
||||
hasOnlyKeys(report, new Set(["schemaVersion", "sourceCommit", "host", "client", "fixture", "privacy", "cases"]), "report", errors);
|
||||
if (report?.schemaVersion !== SCHEMA_VERSION) errors.push(`schemaVersion must be ${SCHEMA_VERSION}`);
|
||||
if (!isCommit(report?.sourceCommit)) errors.push("sourceCommit must be a 40-character commit SHA");
|
||||
|
||||
hasOnlyKeys(report?.host, new Set(["os", "premiereVersion", "premiereBuild", "connector"]), "host", errors);
|
||||
if (!new Set(["Windows", "macOS"]).has(report?.host?.os)) errors.push("host.os must be Windows or macOS");
|
||||
if (!isNonEmptyString(report?.host?.premiereVersion, 64)) errors.push("host.premiereVersion is required");
|
||||
if (!isNonEmptyString(report?.host?.premiereBuild, 64)) errors.push("host.premiereBuild is required");
|
||||
hasOnlyKeys(report?.host?.connector, new Set(["type", "buildHash"]), "host.connector", errors);
|
||||
if (report?.host?.connector?.type !== "cep") errors.push("host.connector.type must be cep for v1.13.0 Project Intake validation");
|
||||
if (!/^[0-9a-f]{7,64}$/i.test(report?.host?.connector?.buildHash ?? "")) errors.push("host.connector.buildHash must be a build hash");
|
||||
|
||||
hasOnlyKeys(report?.client, new Set(["name", "version"]), "client", errors);
|
||||
if (!isNonEmptyString(report?.client?.name)) errors.push("client.name is required");
|
||||
if (!isNonEmptyString(report?.client?.version)) errors.push("client.version is required");
|
||||
|
||||
hasOnlyKeys(report?.fixture, new Set(["revision", "sha256"]), "fixture", errors);
|
||||
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(report?.fixture?.revision ?? "")) errors.push("fixture.revision must be a non-sensitive identifier");
|
||||
if (!isSha256(report?.fixture?.sha256)) errors.push("fixture.sha256 must be a SHA-256 hash");
|
||||
|
||||
const requiredPrivacyFlags = ["containsOnlyGeneratedFixtureData", "localPathsRemoved", "mediaNamesRemoved", "promptsRemoved", "transcriptsRemoved", "credentialsRemoved"];
|
||||
hasOnlyKeys(report?.privacy, new Set(requiredPrivacyFlags), "privacy", errors);
|
||||
for (const flag of requiredPrivacyFlags) {
|
||||
if (report?.privacy?.[flag] !== true) errors.push(`privacy.${flag} must be true`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(report?.cases) || report.cases.length !== Object.keys(CASE_REQUIREMENTS).length) {
|
||||
errors.push(`cases must contain exactly ${Object.keys(CASE_REQUIREMENTS).length} Project Intake cases`);
|
||||
}
|
||||
|
||||
const seenCaseIds = new Set();
|
||||
for (const entry of report?.cases ?? []) {
|
||||
const label = `case ${entry?.id ?? "<unknown>"}`;
|
||||
hasOnlyKeys(entry, new Set(["id", "status", "executedAt", "assertions", "evidence"]), label, errors);
|
||||
if (!Object.hasOwn(CASE_REQUIREMENTS, entry?.id)) errors.push(`${label} is not a supported Project Intake case`);
|
||||
if (seenCaseIds.has(entry?.id)) errors.push(`${label} is duplicated`);
|
||||
seenCaseIds.add(entry?.id);
|
||||
if (!ALLOWED_STATUSES.has(entry?.status)) errors.push(`${label} has an invalid status`);
|
||||
|
||||
if (entry?.status === "passed" && CASE_REQUIREMENTS[entry.id]) {
|
||||
validatePassedCase(entry, CASE_REQUIREMENTS[entry.id], errors);
|
||||
} else {
|
||||
if (entry?.executedAt !== undefined) errors.push(`${label}.executedAt is only recorded for passed cases`);
|
||||
if (entry?.assertions !== undefined) errors.push(`${label}.assertions are only accepted for passed cases`);
|
||||
const evidenceKinds = validateEvidence(entry?.evidence, label, errors);
|
||||
if (evidenceKinds.length > 0) errors.push(`${label} must not include evidence unless it passed; retain failure evidence outside this minimal shared report`);
|
||||
}
|
||||
}
|
||||
for (const id of Object.keys(CASE_REQUIREMENTS)) {
|
||||
if (!seenCaseIds.has(id)) errors.push(`cases must include ${id}`);
|
||||
}
|
||||
|
||||
const previewCase = report?.cases?.find((entry) => entry?.id === "PIP-PREVIEW-001");
|
||||
const noMutationCase = report?.cases?.find((entry) => entry?.id === "PIP-NO-MUTATION-001");
|
||||
if (previewCase?.status === "passed" && noMutationCase?.status !== "passed") {
|
||||
errors.push("PIP-PREVIEW-001 cannot pass unless PIP-NO-MUTATION-001 also passes");
|
||||
}
|
||||
if (report?.cases?.some((entry) => entry?.status === "passed")) {
|
||||
if (report.sourceCommit === NOT_RUN_COMMIT) errors.push("passed cases require a real sourceCommit, not the template sentinel");
|
||||
if (report.fixture?.sha256 === NOT_RUN_SHA256) errors.push("passed cases require a real fixture checksum, not the template sentinel");
|
||||
}
|
||||
|
||||
if (SENSITIVE_CONTENT.test(serializedValues(report))) {
|
||||
errors.push("report appears to contain project data, a local path, or credential-like content; redact it before sharing");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
const reportPath = process.argv[2];
|
||||
if (!reportPath) {
|
||||
throw new Error("Usage: node scripts/validate-project-intake-host-report.mjs <redacted-report.json>");
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(reportPath);
|
||||
let report;
|
||||
try {
|
||||
report = JSON.parse(fs.readFileSync(resolvedPath, "utf8"));
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to parse Project Intake host report: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
const errors = validateReport(report);
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`Project Intake host report is invalid:\n- ${errors.join("\n- ")}`);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
report: path.basename(resolvedPath),
|
||||
schemaVersion: SCHEMA_VERSION,
|
||||
sourceCommit: report.sourceCommit,
|
||||
host: report.host,
|
||||
totals: Object.fromEntries([...ALLOWED_STATUSES].map((status) => [
|
||||
status,
|
||||
report.cases.filter((entry) => entry.status === status).length,
|
||||
])),
|
||||
humanReviewRequired: true,
|
||||
licensedHostVerifiedByValidator: false,
|
||||
}, null, 2));
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
import {
|
||||
compareNativeSdkPaths,
|
||||
hasNativeSdkFamily,
|
||||
NATIVE_SDK_FAMILIES,
|
||||
NATIVE_SDK_HEADER_INVENTORY_SCHEMA_VERSION,
|
||||
NATIVE_SDK_HEADER_INVENTORY_SEMANTICS,
|
||||
} from "./native-sdk-header-inventory-contract.mjs";
|
||||
|
||||
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
||||
const HEADER_PATH_PATTERN = /\.(h|hpp)$/i;
|
||||
const MAX_HEADERS = 100_000;
|
||||
const MAX_PATH_LENGTH = 4_096;
|
||||
|
||||
function verificationError(message) {
|
||||
const error = new Error(message);
|
||||
error.code = "NATIVE_SDK_INVENTORY_INVALID";
|
||||
return error;
|
||||
}
|
||||
|
||||
function record(value, label, expectedKeys) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw verificationError(`${label} must be an object`);
|
||||
}
|
||||
const keys = Object.keys(value).sort(compareNativeSdkPaths);
|
||||
const expected = [...expectedKeys].sort(compareNativeSdkPaths);
|
||||
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
|
||||
throw verificationError(`${label} must contain only the documented receipt fields`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function string(value, label, maximum = MAX_PATH_LENGTH) {
|
||||
if (typeof value !== "string" || !value.trim() || value.length > maximum || value.includes("\0")) {
|
||||
throw verificationError(`${label} must be a non-empty string of at most ${maximum} characters`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function canonicalRelativePath(value, label) {
|
||||
const path = string(value, label).replaceAll("\\", "/");
|
||||
const segments = path.split("/");
|
||||
if (path !== value || path.startsWith("/") || /^[A-Za-z]:/.test(path) || path !== "." && segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
||||
throw verificationError(`${label} must be a canonical relative path`);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function sha256(value, label) {
|
||||
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
||||
throw verificationError(`${label} must be a lowercase SHA-256 hex digest`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function nonNegativeSafeInteger(value, label) {
|
||||
if (!Number.isSafeInteger(value) || value < 0) {
|
||||
throw verificationError(`${label} must be a non-negative safe integer`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function headerIsInsideIncludeDirectory(path, includeDirectory) {
|
||||
return includeDirectory === "." || path.startsWith(`${includeDirectory}/`);
|
||||
}
|
||||
|
||||
export function verifyNativeSdkHeaderInventory(inventory) {
|
||||
const receipt = record(inventory, "receipt", ["schemaVersion", "source", "semantics", "stats", "headers"]);
|
||||
if (receipt.schemaVersion !== NATIVE_SDK_HEADER_INVENTORY_SCHEMA_VERSION) {
|
||||
throw verificationError(`schemaVersion must be ${NATIVE_SDK_HEADER_INVENTORY_SCHEMA_VERSION}`);
|
||||
}
|
||||
|
||||
const source = record(receipt.source, "source", ["sdk", "sdkVersion", "authorityUrl", "archiveSha256", "inventoryScope", "includeDirectories"]);
|
||||
if (!hasNativeSdkFamily(source.sdk)) throw verificationError("source.sdk must be uxp-hybrid or premiere-prsdk");
|
||||
const family = NATIVE_SDK_FAMILIES[source.sdk];
|
||||
string(source.sdkVersion, "source.sdkVersion", 128);
|
||||
if (source.authorityUrl !== family.authorityUrl) throw verificationError("source.authorityUrl does not match the documented SDK family");
|
||||
sha256(source.archiveSha256, "source.archiveSha256");
|
||||
if (source.inventoryScope !== "header_files_only") throw verificationError("source.inventoryScope must be header_files_only");
|
||||
if (!Array.isArray(source.includeDirectories) || source.includeDirectories.length === 0 || source.includeDirectories.length > 64) {
|
||||
throw verificationError("source.includeDirectories must contain one to 64 documented relative directories");
|
||||
}
|
||||
const includeDirectories = source.includeDirectories.map((directory) => canonicalRelativePath(directory, "source.includeDirectories entry"));
|
||||
if (new Set(includeDirectories).size !== includeDirectories.length) {
|
||||
throw verificationError("source.includeDirectories must not contain duplicates");
|
||||
}
|
||||
if (family.includeDirectories && (includeDirectories.length !== family.includeDirectories.length || includeDirectories.some((directory, index) => directory !== family.includeDirectories[index]))) {
|
||||
throw verificationError(`${source.sdk} must use the documented fixed include directories`);
|
||||
}
|
||||
|
||||
const semantics = record(receipt.semantics, "semantics", ["listed", "doesNotEstablish"]);
|
||||
if (semantics.listed !== NATIVE_SDK_HEADER_INVENTORY_SEMANTICS.listed || semantics.doesNotEstablish !== NATIVE_SDK_HEADER_INVENTORY_SEMANTICS.doesNotEstablish) {
|
||||
throw verificationError("semantics must retain the documented evidence boundary");
|
||||
}
|
||||
|
||||
if (!Array.isArray(receipt.headers) || receipt.headers.length === 0 || receipt.headers.length > MAX_HEADERS) {
|
||||
throw verificationError(`headers must contain one to ${MAX_HEADERS} entries`);
|
||||
}
|
||||
const headers = receipt.headers.map((header, index) => {
|
||||
const value = record(header, `headers[${index}]`, ["path", "bytes", "sha256"]);
|
||||
const path = canonicalRelativePath(value.path, `headers[${index}].path`);
|
||||
if (!HEADER_PATH_PATTERN.test(path)) throw verificationError(`headers[${index}].path must name a .h or .hpp file`);
|
||||
if (!includeDirectories.some((directory) => headerIsInsideIncludeDirectory(path, directory))) {
|
||||
throw verificationError(`headers[${index}].path must stay within source.includeDirectories`);
|
||||
}
|
||||
return { path, bytes: nonNegativeSafeInteger(value.bytes, `headers[${index}].bytes`), sha256: sha256(value.sha256, `headers[${index}].sha256`) };
|
||||
});
|
||||
if (headers.some((header, index) => index > 0 && compareNativeSdkPaths(headers[index - 1].path, header.path) >= 0)) {
|
||||
throw verificationError("headers must be strictly sorted by canonical path without duplicates");
|
||||
}
|
||||
for (const requiredHeader of family.requiredHeaders) {
|
||||
if (!headers.some((header) => header.path === requiredHeader)) {
|
||||
throw verificationError(`Missing required UXP Hybrid SDK header: ${requiredHeader}`);
|
||||
}
|
||||
}
|
||||
|
||||
const stats = record(receipt.stats, "stats", ["headers", "bytes"]);
|
||||
if (stats.headers !== headers.length) throw verificationError("stats.headers does not match headers");
|
||||
const totalBytes = headers.reduce((total, header) => total + header.bytes, 0);
|
||||
if (!Number.isSafeInteger(totalBytes) || stats.bytes !== totalBytes) throw verificationError("stats.bytes does not match headers");
|
||||
|
||||
return Object.freeze({ sdk: source.sdk, headers: headers.length, bytes: totalBytes });
|
||||
}
|
||||
|
||||
function canonicalize(value) {
|
||||
if (Array.isArray(value)) return value.map(canonicalize);
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(Object.keys(value).sort(compareNativeSdkPaths).map((key) => [key, canonicalize(value[key])]));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function canonicalNativeSdkHeaderInventorySha256(inventory) {
|
||||
verifyNativeSdkHeaderInventory(inventory);
|
||||
return createHash("sha256").update(JSON.stringify(canonicalize(inventory))).digest("hex");
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const options = { input: null, printCanonicalSha256: false };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index];
|
||||
if (argument === "--print-canonical-sha256") options.printCanonicalSha256 = true;
|
||||
else if (argument === "--input") {
|
||||
const value = argv[++index];
|
||||
if (!value || value.startsWith("--")) throw verificationError("--input requires a receipt path");
|
||||
options.input = value;
|
||||
} else throw verificationError(`Unknown argument: ${argument}`);
|
||||
}
|
||||
if (!options.input) throw verificationError("Usage: node scripts/verify-native-sdk-header-inventory.mjs --input <receipt.json> [--print-canonical-sha256]");
|
||||
return { inputPath: resolve(options.input), printCanonicalSha256: options.printCanonicalSha256 };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
let inventory;
|
||||
try { inventory = JSON.parse(await readFile(options.inputPath, "utf8")); } catch { throw verificationError("input must be a readable JSON receipt"); }
|
||||
const summary = verifyNativeSdkHeaderInventory(inventory);
|
||||
process.stdout.write(`Native SDK header receipt is valid: ${summary.headers} ${summary.sdk} header files, ${summary.bytes} bytes.\n`);
|
||||
if (options.printCanonicalSha256) process.stdout.write(`Canonical receipt SHA-256: ${canonicalNativeSdkHeaderInventorySha256(inventory)}\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error.message}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { gunzipSync } from "node:zlib";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const root = process.cwd();
|
||||
const npmCli = [
|
||||
process.env.npm_execpath,
|
||||
join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
|
||||
join(dirname(dirname(process.execPath)), "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
||||
].find((candidate) => candidate && existsSync(candidate));
|
||||
const requiredFiles = [
|
||||
"package/package.json",
|
||||
"package/dist/index.js",
|
||||
"package/dist/index.d.ts",
|
||||
"package/cep-plugin/CSXS/manifest.xml",
|
||||
"package/after-effects-cep-plugin/CSXS/manifest.xml",
|
||||
"package/uxp-plugin/manifest.json",
|
||||
"package/docs/supported-actions.md",
|
||||
"package/docs/mogrt-authoring.md",
|
||||
"package/docs/premiere-surface-registry.md",
|
||||
"package/docs/adobe-beta-aaf-export-options-drift.md",
|
||||
"package/docs/adobe-beta-project-options-drift.md",
|
||||
"package/docs/adobe-beta-transition-options-drift.md",
|
||||
"package/docs/adobe-beta-rectf-drift.md",
|
||||
"package/docs/adobe-beta-color-drift.md",
|
||||
"package/docs/adobe-beta-pointf-drift.md",
|
||||
"package/docs/adobe-beta-guid-drift.md",
|
||||
"package/docs/adobe-beta-frame-rate-drift.md",
|
||||
"package/docs/adobe-beta-tick-time-drift.md",
|
||||
"package/docs/adobe-beta-c2pa-drift.md",
|
||||
"package/docs/adobe-beta-media-drift.md",
|
||||
"package/docs/adobe-beta-media-manager-drift.md",
|
||||
"package/docs/adobe-beta-transcript-drift.md",
|
||||
"package/docs/adobe-beta-work-area-drift.md",
|
||||
"package/docs/uxp-js-api-inventory.md",
|
||||
"package/docs/premiere-doc-inventory.md",
|
||||
"package/docs/native-sdk-header-inventory.md",
|
||||
"package/docs/uxp-hybrid-addon-receipt.md",
|
||||
"package/docs/uxp-hybrid-ccx-receipt.md",
|
||||
"package/docs/uxp-hybrid-benchmark.md",
|
||||
"package/docs/cep-reference-inventory.md",
|
||||
"package/docs/extendscript-api-inventory.md",
|
||||
"package/dist/resources/premiere-surface-registry.json",
|
||||
"package/dist/resources/adobe-beta-aaf-export-options-drift.json",
|
||||
"package/dist/resources/adobe-beta-project-options-drift.json",
|
||||
"package/dist/resources/adobe-beta-transition-options-drift.json",
|
||||
"package/dist/resources/adobe-beta-rectf-drift.json",
|
||||
"package/dist/resources/adobe-beta-color-drift.json",
|
||||
"package/dist/resources/adobe-beta-pointf-drift.json",
|
||||
"package/dist/resources/adobe-beta-guid-drift.json",
|
||||
"package/dist/resources/adobe-beta-frame-rate-drift.json",
|
||||
"package/dist/resources/adobe-beta-tick-time-drift.json",
|
||||
"package/dist/resources/adobe-beta-c2pa-drift.json",
|
||||
"package/dist/resources/adobe-beta-media-drift.json",
|
||||
"package/dist/resources/adobe-beta-media-manager-drift.json",
|
||||
"package/dist/resources/adobe-beta-transcript-drift.json",
|
||||
"package/dist/resources/adobe-beta-work-area-drift.json",
|
||||
"package/dist/resources/uxp-js-api-inventory.json",
|
||||
"package/dist/resources/premiere-doc-inventory.json",
|
||||
"package/dist/resources/cep-reference-inventory.json",
|
||||
"package/dist/resources/extendscript-api-inventory.json",
|
||||
"package/scripts/install-cep.ps1",
|
||||
"package/scripts/install-cep.sh",
|
||||
"package/scripts/uninstall-cep.ps1",
|
||||
"package/scripts/uninstall-cep.sh",
|
||||
];
|
||||
|
||||
function tarEntries(tarball) {
|
||||
const archive = gunzipSync(readFileSync(tarball));
|
||||
const entries = new Set();
|
||||
let offset = 0;
|
||||
|
||||
while (offset + 512 <= archive.length) {
|
||||
const header = archive.subarray(offset, offset + 512);
|
||||
if (header.every((byte) => byte === 0)) break;
|
||||
|
||||
const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
|
||||
const prefix = header.subarray(345, 500).toString("utf8").replace(/\0.*$/, "");
|
||||
const sizeText = header.subarray(124, 136).toString("utf8").replace(/\0.*$/, "").trim();
|
||||
const size = Number.parseInt(sizeText || "0", 8);
|
||||
if (!name || !Number.isSafeInteger(size) || size < 0) {
|
||||
throw new Error(`Invalid tar entry at byte ${offset} in ${tarball}`);
|
||||
}
|
||||
|
||||
entries.add(prefix ? `${prefix}/${name}` : name);
|
||||
offset += 512 + Math.ceil(size / 512) * 512;
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
return execFileSync(command, args, {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
function runNpm(args, options = {}) {
|
||||
if (!npmCli) {
|
||||
throw new Error("The bundled npm CLI is unavailable for package verification");
|
||||
}
|
||||
return run(process.execPath, [npmCli, ...args], options);
|
||||
}
|
||||
|
||||
let tarball;
|
||||
let installDir;
|
||||
let packDir;
|
||||
|
||||
try {
|
||||
packDir = mkdtempSync(join(tmpdir(), "premiere-pro-mcp-pack-output-"));
|
||||
const packed = JSON.parse(
|
||||
runNpm(["pack", "--json", "--ignore-scripts", "--pack-destination", packDir]),
|
||||
);
|
||||
const packageJson = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
const packEntries = Array.isArray(packed)
|
||||
? packed
|
||||
: packed && typeof packed === "object"
|
||||
? Object.values(packed)
|
||||
: [];
|
||||
const matchingPackages = packEntries.filter(
|
||||
(entry) =>
|
||||
entry?.name === packageJson.name &&
|
||||
entry?.version === packageJson.version &&
|
||||
typeof entry?.filename === "string",
|
||||
);
|
||||
if (matchingPackages.length !== 1) {
|
||||
throw new Error("npm pack did not return exactly one package matching package.json");
|
||||
}
|
||||
|
||||
tarball = resolve(packDir, matchingPackages[0].filename);
|
||||
const entries = tarEntries(tarball);
|
||||
const missing = requiredFiles.filter((path) => !entries.has(path));
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`npm package is missing required files: ${missing.join(", ")}`);
|
||||
}
|
||||
|
||||
installDir = mkdtempSync(join(tmpdir(), "premiere-pro-mcp-pack-"));
|
||||
runNpm(["install", "--ignore-scripts", "--no-audit", "--no-fund", tarball], {
|
||||
cwd: installDir,
|
||||
});
|
||||
|
||||
const installedCli = join(installDir, "node_modules", "premiere-pro-mcp", "dist", "index.js");
|
||||
if (!existsSync(installedCli)) {
|
||||
throw new Error("isolated package installation did not contain the CLI entrypoint");
|
||||
}
|
||||
const installedPackageRoot = join(installDir, "node_modules", "premiere-pro-mcp");
|
||||
const installedRegistry = JSON.parse(readFileSync(join(
|
||||
installedPackageRoot,
|
||||
"dist",
|
||||
"resources",
|
||||
"premiere-surface-registry.json",
|
||||
), "utf8"));
|
||||
if (installedRegistry.schemaVersion !== 1 || !Array.isArray(installedRegistry.integrationSurfaces)) {
|
||||
throw new Error("installed package did not contain a valid Premiere surface registry");
|
||||
}
|
||||
for (const surface of installedRegistry.integrationSurfaces) {
|
||||
if (surface.inventoryArtifact !== null &&
|
||||
(typeof surface.inventoryArtifact !== "string" || !existsSync(join(installedPackageRoot, surface.inventoryArtifact)))) {
|
||||
throw new Error(`installed package registry references a missing inventory artifact: ${surface.id}`);
|
||||
}
|
||||
}
|
||||
const help = run(process.execPath, [installedCli, "--help"], { cwd: installDir });
|
||||
if (!help.includes("Usage:")) {
|
||||
throw new Error("installed CLI --help did not return the expected usage text");
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Verified npm package contents and isolated CLI install: ${matchingPackages[0].filename}`,
|
||||
);
|
||||
} finally {
|
||||
if (installDir) rmSync(installDir, { recursive: true, force: true });
|
||||
if (tarball) rmSync(tarball, { force: true });
|
||||
if (packDir) rmSync(packDir, { recursive: true, force: true });
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const read = (path) => readFileSync(join(root, path), "utf8");
|
||||
const readJson = (path) => JSON.parse(read(path));
|
||||
const release = readJson("release-metadata.json");
|
||||
const tag = process.env.RELEASE_TAG;
|
||||
const expectedTag = `v${release.version}`;
|
||||
|
||||
if (tag !== expectedTag) {
|
||||
throw new Error(`Release tag must be exactly ${expectedTag}; received ${tag || "(missing)"}`);
|
||||
}
|
||||
|
||||
const versionedJson = [
|
||||
"package.json",
|
||||
"claude-desktop/manifest.json",
|
||||
"uxp-plugin/manifest.json",
|
||||
"plugins/premiere-pro/.codex-plugin/plugin.json",
|
||||
"claude-plugins/premiere-pro/.claude-plugin/plugin.json",
|
||||
];
|
||||
for (const path of versionedJson) {
|
||||
if (readJson(path).version !== release.version) {
|
||||
throw new Error(`${path} does not match release metadata version ${release.version}`);
|
||||
}
|
||||
}
|
||||
|
||||
const cepManifest = read("cep-plugin/CSXS/manifest.xml");
|
||||
if (!cepManifest.includes(`ExtensionBundleVersion="${release.version}"`)) {
|
||||
throw new Error("CEP bundle version does not match release metadata");
|
||||
}
|
||||
|
||||
console.log(`Release tag and distributable manifests verified for ${expectedTag}`);
|
||||
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
import {
|
||||
UXP_HYBRID_ADDON_AUTHORITY_URL,
|
||||
UXP_HYBRID_ADDON_ENTRYPOINT_PATH,
|
||||
UXP_HYBRID_ADDON_RECEIPT_LEGACY_SCHEMA_VERSION,
|
||||
UXP_HYBRID_ADDON_RECEIPT_LEGACY_SEMANTICS,
|
||||
UXP_HYBRID_ADDON_RECEIPT_SCHEMA_VERSION,
|
||||
UXP_HYBRID_ADDON_RECEIPT_SEMANTICS,
|
||||
UXP_HYBRID_ADDON_TARGETS,
|
||||
compareUxpHybridPaths,
|
||||
} from "./uxp-hybrid-addon-receipt-contract.mjs";
|
||||
import {
|
||||
canonicalNativeSdkHeaderInventorySha256,
|
||||
verifyNativeSdkHeaderInventory,
|
||||
} from "./verify-native-sdk-header-inventory.mjs";
|
||||
|
||||
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
||||
const MAX_PATH_LENGTH = 512;
|
||||
const MAX_ARTIFACT_BYTES = 2 ** 31;
|
||||
const MAX_ENTRYPOINT_BYTES = 16 * 1024 * 1024;
|
||||
|
||||
function receiptError(message) {
|
||||
const error = new Error(message);
|
||||
error.code = "UXP_HYBRID_ADDON_RECEIPT_INVALID";
|
||||
return error;
|
||||
}
|
||||
|
||||
function record(value, label, expectedKeys) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw receiptError(`${label} must be an object`);
|
||||
const keys = Object.keys(value).sort(compareUxpHybridPaths);
|
||||
const expected = [...expectedKeys].sort(compareUxpHybridPaths);
|
||||
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
|
||||
throw receiptError(`${label} must contain only the documented receipt fields`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function nonEmptyString(value, label, maximum = MAX_PATH_LENGTH) {
|
||||
if (typeof value !== "string" || !value.trim() || value.length > maximum || value.includes("\0")) {
|
||||
throw receiptError(`${label} must be a non-empty string of at most ${maximum} characters`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function sha256(value, label) {
|
||||
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
||||
throw receiptError(`${label} must be a lowercase SHA-256 hex digest`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function safePositiveInteger(value, label) {
|
||||
if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_ARTIFACT_BYTES) {
|
||||
throw receiptError(`${label} must be a positive safe integer no larger than ${MAX_ARTIFACT_BYTES}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function addonName(value) {
|
||||
const name = nonEmptyString(value, "manifest.addonName", 128);
|
||||
if (!/^[A-Za-z0-9._-]+\.uxpaddon$/.test(name)) {
|
||||
throw receiptError("manifest.addonName must be a simple .uxpaddon filename");
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function canonicalize(value) {
|
||||
if (Array.isArray(value)) return value.map(canonicalize);
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(Object.keys(value).sort(compareUxpHybridPaths).map((key) => [key, canonicalize(value[key])]));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function expectedArtifactPath(target, name) {
|
||||
return `${target.pathPrefix}/${name}`;
|
||||
}
|
||||
|
||||
export function verifyUxpHybridAddonReceipt(document, options = {}) {
|
||||
if (!document || typeof document !== "object" || Array.isArray(document)) throw receiptError("receipt must be an object");
|
||||
const legacy = document.schemaVersion === UXP_HYBRID_ADDON_RECEIPT_LEGACY_SCHEMA_VERSION;
|
||||
const receipt = record(document, "receipt", legacy
|
||||
? ["schemaVersion", "source", "manifest", "semantics", "stats", "artifacts"]
|
||||
: ["schemaVersion", "source", "manifest", "entrypoint", "semantics", "stats", "artifacts"]);
|
||||
if (!legacy && receipt.schemaVersion !== UXP_HYBRID_ADDON_RECEIPT_SCHEMA_VERSION) {
|
||||
throw receiptError(`schemaVersion must be ${UXP_HYBRID_ADDON_RECEIPT_LEGACY_SCHEMA_VERSION} or ${UXP_HYBRID_ADDON_RECEIPT_SCHEMA_VERSION}`);
|
||||
}
|
||||
|
||||
const source = record(receipt.source, "source", ["sdk", "sdkVersion", "sdkHeaderReceiptSha256", "authorityUrl"]);
|
||||
if (source.sdk !== "uxp-hybrid") throw receiptError("source.sdk must be uxp-hybrid");
|
||||
nonEmptyString(source.sdkVersion, "source.sdkVersion", 128);
|
||||
sha256(source.sdkHeaderReceiptSha256, "source.sdkHeaderReceiptSha256");
|
||||
if (source.authorityUrl !== UXP_HYBRID_ADDON_AUTHORITY_URL) {
|
||||
throw receiptError("source.authorityUrl must match the documented Hybrid addon build guide");
|
||||
}
|
||||
|
||||
const manifest = record(receipt.manifest, "manifest", ["manifestVersion", "hostApp", "hostMinVersion", "addonName", "enableAddon"]);
|
||||
if (!Number.isInteger(manifest.manifestVersion) || manifest.manifestVersion < 6) {
|
||||
throw receiptError("manifest.manifestVersion must be 6 or newer");
|
||||
}
|
||||
if (manifest.hostApp !== "premierepro") throw receiptError("manifest.hostApp must be premierepro");
|
||||
if (!/^\d+\.\d+\.\d+$/.test(String(manifest.hostMinVersion || ""))) {
|
||||
throw receiptError("manifest.hostMinVersion must be a semantic version");
|
||||
}
|
||||
const name = addonName(manifest.addonName);
|
||||
if (manifest.enableAddon !== true) throw receiptError("manifest.enableAddon must be true");
|
||||
|
||||
const semantics = record(receipt.semantics, "semantics", ["listed", "doesNotEstablish"]);
|
||||
const requiredSemantics = legacy ? UXP_HYBRID_ADDON_RECEIPT_LEGACY_SEMANTICS : UXP_HYBRID_ADDON_RECEIPT_SEMANTICS;
|
||||
if (semantics.listed !== requiredSemantics.listed || semantics.doesNotEstablish !== requiredSemantics.doesNotEstablish) {
|
||||
throw receiptError("semantics must retain the documented evidence boundary");
|
||||
}
|
||||
|
||||
let entrypoint;
|
||||
if (!legacy) {
|
||||
const value = record(receipt.entrypoint, "entrypoint", ["path", "bytes", "sha256"]);
|
||||
if (value.path !== UXP_HYBRID_ADDON_ENTRYPOINT_PATH) {
|
||||
throw receiptError(`entrypoint.path must be ${UXP_HYBRID_ADDON_ENTRYPOINT_PATH}`);
|
||||
}
|
||||
entrypoint = {
|
||||
path: value.path,
|
||||
bytes: safePositiveInteger(value.bytes, "entrypoint.bytes"),
|
||||
sha256: sha256(value.sha256, "entrypoint.sha256"),
|
||||
};
|
||||
if (entrypoint.bytes > MAX_ENTRYPOINT_BYTES) {
|
||||
throw receiptError(`entrypoint.bytes must be no larger than ${MAX_ENTRYPOINT_BYTES}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(receipt.artifacts) || receipt.artifacts.length !== UXP_HYBRID_ADDON_TARGETS.length) {
|
||||
throw receiptError(`artifacts must contain exactly ${UXP_HYBRID_ADDON_TARGETS.length} required target entries`);
|
||||
}
|
||||
const artifacts = receipt.artifacts.map((entry, index) => {
|
||||
const artifact = record(entry, `artifacts[${index}]`, ["target", "path", "bytes", "sha256"]);
|
||||
const expectedTarget = UXP_HYBRID_ADDON_TARGETS[index];
|
||||
if (artifact.target !== expectedTarget.target) throw receiptError(`artifacts[${index}].target must be ${expectedTarget.target}`);
|
||||
const path = nonEmptyString(artifact.path, `artifacts[${index}].path`, MAX_PATH_LENGTH);
|
||||
if (path !== expectedArtifactPath(expectedTarget, name)) throw receiptError(`artifacts[${index}].path must be the documented ${expectedTarget.target} addon path`);
|
||||
return { target: artifact.target, path, bytes: safePositiveInteger(artifact.bytes, `artifacts[${index}].bytes`), sha256: sha256(artifact.sha256, `artifacts[${index}].sha256`) };
|
||||
});
|
||||
|
||||
const stats = record(receipt.stats, "stats", legacy
|
||||
? ["artifacts", "bytes"]
|
||||
: ["artifacts", "addonBytes", "entrypoints", "entrypointBytes"]);
|
||||
if (stats.artifacts !== artifacts.length) throw receiptError("stats.artifacts does not match artifacts");
|
||||
const addonBytes = artifacts.reduce((total, artifact) => total + artifact.bytes, 0);
|
||||
if (!Number.isSafeInteger(addonBytes)) throw receiptError("addon artifact bytes exceed the supported total");
|
||||
if (legacy && stats.bytes !== addonBytes) throw receiptError("stats.bytes does not match artifacts");
|
||||
if (!legacy) {
|
||||
if (stats.addonBytes !== addonBytes) throw receiptError("stats.addonBytes does not match artifacts");
|
||||
if (stats.entrypoints !== 1) throw receiptError("stats.entrypoints must be 1");
|
||||
if (stats.entrypointBytes !== entrypoint.bytes) throw receiptError("stats.entrypointBytes does not match entrypoint");
|
||||
}
|
||||
|
||||
if (options.sdkHeaderReceipt !== undefined) {
|
||||
const summary = verifyNativeSdkHeaderInventory(options.sdkHeaderReceipt);
|
||||
if (summary.sdk !== "uxp-hybrid") throw receiptError("sdkHeaderReceipt must identify uxp-hybrid");
|
||||
if (options.sdkHeaderReceipt.source.sdkVersion !== source.sdkVersion) {
|
||||
throw receiptError("source.sdkVersion must match sdkHeaderReceipt.source.sdkVersion");
|
||||
}
|
||||
if (canonicalNativeSdkHeaderInventorySha256(options.sdkHeaderReceipt) !== source.sdkHeaderReceiptSha256) {
|
||||
throw receiptError("source.sdkHeaderReceiptSha256 does not match sdkHeaderReceipt");
|
||||
}
|
||||
}
|
||||
|
||||
return Object.freeze(legacy
|
||||
? { addonName: name, artifacts: artifacts.length, bytes: addonBytes }
|
||||
: { addonName: name, artifacts: artifacts.length, addonBytes, entrypoints: 1, entrypointBytes: entrypoint.bytes });
|
||||
}
|
||||
|
||||
export function canonicalUxpHybridAddonReceiptSha256(document) {
|
||||
verifyUxpHybridAddonReceipt(document);
|
||||
return createHash("sha256").update(JSON.stringify(canonicalize(document))).digest("hex");
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const options = { input: null, sdkHeaderReceipt: null, printCanonicalSha256: false };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index];
|
||||
if (argument === "--print-canonical-sha256") options.printCanonicalSha256 = true;
|
||||
else if (["--input", "--sdk-header-receipt"].includes(argument)) {
|
||||
const value = argv[++index];
|
||||
if (!value || value.startsWith("--")) throw receiptError(`${argument} requires a receipt path`);
|
||||
if (argument === "--input") options.input = value;
|
||||
else options.sdkHeaderReceipt = value;
|
||||
} else throw receiptError(`Unknown argument: ${argument}`);
|
||||
}
|
||||
if (!options.input || !options.sdkHeaderReceipt) {
|
||||
throw receiptError("Usage: node scripts/verify-uxp-hybrid-addon-receipt.mjs --input <receipt.json> --sdk-header-receipt <sdk-header-receipt.json> [--print-canonical-sha256]");
|
||||
}
|
||||
return { inputPath: resolve(options.input), sdkHeaderReceiptPath: resolve(options.sdkHeaderReceipt), printCanonicalSha256: options.printCanonicalSha256 };
|
||||
}
|
||||
|
||||
async function readJson(path, label) {
|
||||
try { return JSON.parse(await readFile(path, "utf8")); } catch { throw receiptError(`${label} must be a readable JSON receipt`); }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
const [receipt, sdkHeaderReceipt] = await Promise.all([
|
||||
readJson(options.inputPath, "input"),
|
||||
readJson(options.sdkHeaderReceiptPath, "sdkHeaderReceipt"),
|
||||
]);
|
||||
const summary = verifyUxpHybridAddonReceipt(receipt, { sdkHeaderReceipt });
|
||||
const entrypointText = "entrypoints" in summary ? ` and ${summary.entrypoints} entrypoint` : "";
|
||||
const bytes = "addonBytes" in summary ? summary.addonBytes + summary.entrypointBytes : summary.bytes;
|
||||
process.stdout.write(`UXP Hybrid addon receipt is valid: ${summary.artifacts} addon artifacts${entrypointText}, ${bytes} bytes.\n`);
|
||||
if (options.printCanonicalSha256) process.stdout.write(`Canonical receipt SHA-256: ${canonicalUxpHybridAddonReceiptSha256(receipt)}\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import {
|
||||
canonicalNativeSdkHeaderInventorySha256,
|
||||
verifyNativeSdkHeaderInventory,
|
||||
} from "./verify-native-sdk-header-inventory.mjs";
|
||||
import {
|
||||
canonicalUxpHybridAddonReceiptSha256,
|
||||
verifyUxpHybridAddonReceipt,
|
||||
} from "./verify-uxp-hybrid-addon-receipt.mjs";
|
||||
import {
|
||||
buildUxpHybridCcxReceipt,
|
||||
canonicalUxpHybridCcxReceiptSha256,
|
||||
verifyUxpHybridCcxReceipt,
|
||||
} from "./uxp-hybrid-ccx-receipt-core.mjs";
|
||||
|
||||
export const REQUIRED_TARGETS = ["win-x64", "mac-x64", "mac-arm64"];
|
||||
const LOCAL_CCX_ARCHIVE_REQUIRED = "A local CCX archive must be rechecked for schemaVersion 3.";
|
||||
|
||||
export function verifyHybridBenchmarkEvidence(document, options = {}) {
|
||||
const minimumSpeedupPercent = finiteOption(options.minimumSpeedupPercent, 30, "minimumSpeedupPercent");
|
||||
const maximumMemoryRegressionPercent = finiteOption(options.maximumMemoryRegressionPercent, 10, "maximumMemoryRegressionPercent");
|
||||
const errors = [];
|
||||
if (!document || typeof document !== "object" || Array.isArray(document)) {
|
||||
return verdict(errors.concat("Evidence must be a JSON object."), [], minimumSpeedupPercent, maximumMemoryRegressionPercent);
|
||||
}
|
||||
const receiptBound = document.schemaVersion === 2 || document.schemaVersion === 3;
|
||||
const packageBound = document.schemaVersion === 3;
|
||||
const expectedEvidenceKeys = packageBound
|
||||
? ["schemaVersion", "workloadId", "configuration", "memoryMeasurement", "sdkHeaderReceiptSha256", "addonReceiptSha256", "ccxReceiptSha256", "runs"]
|
||||
: receiptBound
|
||||
? ["schemaVersion", "workloadId", "configuration", "memoryMeasurement", "sdkHeaderReceiptSha256", "runs"]
|
||||
: ["schemaVersion", "workloadId", "configuration", "memoryMeasurement", "runs"];
|
||||
if (!sameKeys(document, expectedEvidenceKeys)) {
|
||||
errors.push("Evidence must contain only the documented benchmark receipt fields.");
|
||||
}
|
||||
if (document.schemaVersion !== 1 && document.schemaVersion !== 2 && document.schemaVersion !== 3) {
|
||||
errors.push("schemaVersion must be 1, 2, or 3.");
|
||||
}
|
||||
if (document.workloadId !== "weighted-energy-v1") errors.push("workloadId must be weighted-energy-v1.");
|
||||
if (receiptBound && !/^[a-f0-9]{64}$/.test(String(document.sdkHeaderReceiptSha256 || ""))) {
|
||||
errors.push("sdkHeaderReceiptSha256 must be a canonical SDK header receipt SHA-256 digest.");
|
||||
}
|
||||
if (packageBound && !/^[a-f0-9]{64}$/.test(String(document.addonReceiptSha256 || ""))) {
|
||||
errors.push("addonReceiptSha256 must be a canonical addon-layout receipt SHA-256 digest.");
|
||||
}
|
||||
if (packageBound && !/^[a-f0-9]{64}$/.test(String(document.ccxReceiptSha256 || ""))) {
|
||||
errors.push("ccxReceiptSha256 must be a canonical CCX receipt SHA-256 digest.");
|
||||
}
|
||||
const expectedConfiguration = { sampleCount: 30, warmupCount: 3, iterations: 4, inputLength: 131072, seed: 1337 };
|
||||
if (!sameConfiguration(document.configuration, expectedConfiguration)) {
|
||||
errors.push("configuration must match the versioned weighted-energy-v1 benchmark settings.");
|
||||
}
|
||||
if (typeof document.memoryMeasurement !== "string" || !document.memoryMeasurement.trim()) {
|
||||
errors.push("memoryMeasurement must identify the process peak-working-set collection method.");
|
||||
}
|
||||
if (!Array.isArray(document.runs)) errors.push("runs must be an array.");
|
||||
const runs = Array.isArray(document.runs) ? document.runs : [];
|
||||
const targets = new Map();
|
||||
const commits = new Set();
|
||||
const sdkVersions = new Set();
|
||||
for (let index = 0; index < runs.length; index += 1) {
|
||||
const run = runs[index], prefix = `runs[${index}]`;
|
||||
if (!run || typeof run !== "object" || Array.isArray(run)) {
|
||||
errors.push(`${prefix} must be an object.`);
|
||||
continue;
|
||||
}
|
||||
if (!sameKeys(run, ["platform", "arch", "hostVersion", "sdkVersion", "buildMode", "addonLoaded", "addonSha256", "sourceCommit", "checksumMatch", "codeSigned", "notarized", "javascript", "native"])) {
|
||||
errors.push(`${prefix} must contain only the documented benchmark fields.`);
|
||||
}
|
||||
const target = `${run.platform || ""}-${run.arch || ""}`;
|
||||
if (!REQUIRED_TARGETS.includes(target)) errors.push(`${prefix} has unsupported target ${target}.`);
|
||||
else if (targets.has(target)) errors.push(`${prefix} duplicates target ${target}.`);
|
||||
else targets.set(target, run);
|
||||
if (!versionAtLeast(String(run.hostVersion || ""), "26.2.0")) {
|
||||
errors.push(`${prefix}.hostVersion must be stable Premiere 26.2.0 or newer.`);
|
||||
}
|
||||
if (typeof run.sdkVersion !== "string" || !run.sdkVersion.trim()) errors.push(`${prefix}.sdkVersion is required.`);
|
||||
else sdkVersions.add(run.sdkVersion);
|
||||
if (run.buildMode !== "Release") errors.push(`${prefix}.buildMode must be Release.`);
|
||||
if (run.addonLoaded !== true) errors.push(`${prefix}.addonLoaded must be true.`);
|
||||
if (!/^[a-f0-9]{64}$/.test(String(run.addonSha256 || ""))) errors.push(`${prefix}.addonSha256 must be a lowercase SHA-256 digest.`);
|
||||
if (!/^[a-f0-9]{40}$/.test(String(run.sourceCommit || ""))) errors.push(`${prefix}.sourceCommit must be a full Git SHA.`);
|
||||
else commits.add(run.sourceCommit);
|
||||
if (run.checksumMatch !== true) errors.push(`${prefix}.checksumMatch must be true.`);
|
||||
if (target.startsWith("mac-") && (run.codeSigned !== true || run.notarized !== true)) {
|
||||
errors.push(`${prefix} must record signed and notarized macOS addon evidence.`);
|
||||
}
|
||||
validateMetrics(run.javascript, `${prefix}.javascript`, errors);
|
||||
validateMetrics(run.native, `${prefix}.native`, errors);
|
||||
if (validMetrics(run.javascript) && validMetrics(run.native)) {
|
||||
const p50Speedup = speedup(run.javascript.p50Ms, run.native.p50Ms);
|
||||
const p95Speedup = speedup(run.javascript.p95Ms, run.native.p95Ms);
|
||||
if (p50Speedup < minimumSpeedupPercent) errors.push(`${prefix} p50 speedup ${p50Speedup.toFixed(2)}% is below ${minimumSpeedupPercent}%.`);
|
||||
if (p95Speedup < minimumSpeedupPercent) errors.push(`${prefix} p95 speedup ${p95Speedup.toFixed(2)}% is below ${minimumSpeedupPercent}%.`);
|
||||
const jsMemory = run.javascript.peakWorkingSetBytes, nativeMemory = run.native.peakWorkingSetBytes;
|
||||
const memoryRegression = (nativeMemory - jsMemory) / jsMemory * 100;
|
||||
if (memoryRegression > maximumMemoryRegressionPercent) {
|
||||
errors.push(`${prefix} memory regression ${memoryRegression.toFixed(2)}% exceeds ${maximumMemoryRegressionPercent}%.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const target of REQUIRED_TARGETS) if (!targets.has(target)) errors.push(`Missing required ${target} evidence.`);
|
||||
if (commits.size > 1) errors.push("All runs must measure the same sourceCommit.");
|
||||
if (commits.size === 0) errors.push("A shared full sourceCommit is required.");
|
||||
if (sdkVersions.size > 1) errors.push("All runs must use the same UXP Hybrid SDK version.");
|
||||
if (packageBound) validatePackageReceipts(document, options, sdkVersions, targets, errors);
|
||||
else if (receiptBound) validateSdkReceipt(document, options.sdkHeaderReceipt, sdkVersions, errors);
|
||||
if (packageBound) errors.push(LOCAL_CCX_ARCHIVE_REQUIRED);
|
||||
return verdict(errors, Array.from(targets.keys()).sort(), minimumSpeedupPercent, maximumMemoryRegressionPercent, document.schemaVersion);
|
||||
}
|
||||
|
||||
function validatePackageReceipts(document, options, sdkVersions, targets, errors) {
|
||||
const { sdkHeaderReceipt, addonReceipt, ccxReceipt } = options;
|
||||
if (!sdkHeaderReceipt) errors.push("A verified UXP Hybrid SDK header receipt is required.");
|
||||
if (!addonReceipt) errors.push("A verified current UXP Hybrid addon-layout receipt is required.");
|
||||
if (!ccxReceipt) errors.push("A verified UXP Hybrid CCX receipt is required.");
|
||||
if (!sdkHeaderReceipt || !addonReceipt || !ccxReceipt) return;
|
||||
try {
|
||||
const sdk = verifyNativeSdkHeaderInventory(sdkHeaderReceipt);
|
||||
if (sdk.sdk !== "uxp-hybrid") errors.push("SDK header receipt must identify uxp-hybrid.");
|
||||
if (sdkVersions.size === 1 && sdkHeaderReceipt.source.sdkVersion !== Array.from(sdkVersions)[0]) {
|
||||
errors.push("SDK header receipt sdkVersion must match all benchmark runs.");
|
||||
}
|
||||
if (document.sdkHeaderReceiptSha256 !== canonicalNativeSdkHeaderInventorySha256(sdkHeaderReceipt)) {
|
||||
errors.push("sdkHeaderReceiptSha256 does not match the verified SDK header receipt.");
|
||||
}
|
||||
|
||||
verifyUxpHybridAddonReceipt(addonReceipt, { sdkHeaderReceipt });
|
||||
if (document.addonReceiptSha256 !== canonicalUxpHybridAddonReceiptSha256(addonReceipt)) {
|
||||
errors.push("addonReceiptSha256 does not match the verified addon-layout receipt.");
|
||||
}
|
||||
|
||||
verifyUxpHybridCcxReceipt(ccxReceipt, { addonReceipt, sdkHeaderReceipt });
|
||||
if (document.ccxReceiptSha256 !== canonicalUxpHybridCcxReceiptSha256(ccxReceipt)) {
|
||||
errors.push("ccxReceiptSha256 does not match the verified CCX receipt.");
|
||||
}
|
||||
|
||||
for (const [target, run] of targets) {
|
||||
const artifact = addonReceipt.artifacts.find((entry) => entry.target === target);
|
||||
if (!artifact || run.addonSha256 !== artifact.sha256) {
|
||||
errors.push(`runs for ${target} must match the corresponding addon-layout artifact SHA-256.`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`Hybrid package receipt is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyHybridBenchmarkEvidenceWithLocalCcx(document, options = {}) {
|
||||
const result = verifyHybridBenchmarkEvidence(document, options);
|
||||
if (document?.schemaVersion !== 3) return result;
|
||||
const structuralErrors = result.errors.filter((error) => error !== LOCAL_CCX_ARCHIVE_REQUIRED);
|
||||
if (structuralErrors.length > 0) return result;
|
||||
if (typeof options.ccxPath !== "string" || !options.ccxPath.trim() || options.ccxPath.includes("\0")) {
|
||||
return ineligible({ ...result, errors: structuralErrors }, "A local CCX archive path is required for schemaVersion 3.");
|
||||
}
|
||||
try {
|
||||
const current = await buildUxpHybridCcxReceipt({
|
||||
ccxPath: options.ccxPath,
|
||||
addonReceipt: options.addonReceipt,
|
||||
sdkHeaderReceipt: options.sdkHeaderReceipt,
|
||||
});
|
||||
if (canonicalUxpHybridCcxReceiptSha256(current) !== canonicalUxpHybridCcxReceiptSha256(options.ccxReceipt)) {
|
||||
return ineligible({ ...result, errors: structuralErrors }, "CCX receipt does not match the supplied local archive.");
|
||||
}
|
||||
return { ...result, promotionEligible: true, errors: structuralErrors };
|
||||
} catch (error) {
|
||||
return ineligible({ ...result, errors: structuralErrors }, `CCX archive is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ineligible(result, error) {
|
||||
return { ...result, promotionEligible: false, errors: [...result.errors, error] };
|
||||
}
|
||||
|
||||
function validateSdkReceipt(document, receipt, sdkVersions, errors) {
|
||||
if (!receipt) {
|
||||
errors.push("A verified UXP Hybrid SDK header receipt is required.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const summary = verifyNativeSdkHeaderInventory(receipt);
|
||||
if (summary.sdk !== "uxp-hybrid") errors.push("SDK header receipt must identify uxp-hybrid.");
|
||||
if (sdkVersions.size === 1 && receipt.source.sdkVersion !== Array.from(sdkVersions)[0]) {
|
||||
errors.push("SDK header receipt sdkVersion must match all benchmark runs.");
|
||||
}
|
||||
if (document.sdkHeaderReceiptSha256 !== canonicalNativeSdkHeaderInventorySha256(receipt)) {
|
||||
errors.push("sdkHeaderReceiptSha256 does not match the verified SDK header receipt.");
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`SDK header receipt is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateMetrics(value, label, errors) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
errors.push(`${label} metrics are required.`);
|
||||
return;
|
||||
}
|
||||
if (!sameKeys(value, ["sampleCount", "p50Ms", "p95Ms", "peakWorkingSetBytes"])) {
|
||||
errors.push(`${label} must contain only the documented metric fields.`);
|
||||
}
|
||||
if (!Number.isInteger(value.sampleCount) || value.sampleCount < 20) errors.push(`${label}.sampleCount must be at least 20.`);
|
||||
for (const key of ["p50Ms", "p95Ms", "peakWorkingSetBytes"]) {
|
||||
if (!Number.isFinite(value[key]) || value[key] <= 0) errors.push(`${label}.${key} must be a positive finite number.`);
|
||||
}
|
||||
if (Number.isFinite(value.p50Ms) && Number.isFinite(value.p95Ms) && value.p95Ms < value.p50Ms) {
|
||||
errors.push(`${label}.p95Ms cannot be lower than p50Ms.`);
|
||||
}
|
||||
}
|
||||
|
||||
function validMetrics(value) {
|
||||
return value && Number.isFinite(value.p50Ms) && value.p50Ms > 0 && Number.isFinite(value.p95Ms) && value.p95Ms > 0 &&
|
||||
Number.isFinite(value.peakWorkingSetBytes) && value.peakWorkingSetBytes > 0;
|
||||
}
|
||||
|
||||
function speedup(baseline, candidate) {
|
||||
return (baseline - candidate) / baseline * 100;
|
||||
}
|
||||
|
||||
function sameConfiguration(value, expected) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
const keys = Object.keys(expected);
|
||||
return Object.keys(value).length === keys.length && keys.every((key) => value[key] === expected[key]);
|
||||
}
|
||||
|
||||
function sameKeys(value, expected) {
|
||||
return value && typeof value === "object" && !Array.isArray(value) &&
|
||||
Object.keys(value).length === expected.length && expected.every((key) => Object.prototype.hasOwnProperty.call(value, key));
|
||||
}
|
||||
|
||||
function versionAtLeast(value, minimum) {
|
||||
if (!/^\d+\.\d+\.\d+$/.test(value)) return false;
|
||||
const left = value.split(".").map(Number), right = minimum.split(".").map(Number);
|
||||
for (let index = 0; index < 3; index += 1) {
|
||||
if (left[index] > right[index]) return true;
|
||||
if (left[index] < right[index]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function finiteOption(value, fallback, name) {
|
||||
const result = value == null ? fallback : Number(value);
|
||||
if (!Number.isFinite(result) || result < 0 || result > 1000) throw new Error(`${name} must be between 0 and 1000.`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function verdict(errors, targets, minimumSpeedupPercent, maximumMemoryRegressionPercent, schemaVersion) {
|
||||
const verificationBoundary = schemaVersion === 3
|
||||
? "submitted_cross_platform_release_build_and_verified_sdk_addon_ccx_receipt_evidence"
|
||||
: schemaVersion === 2
|
||||
? "submitted_cross_platform_release_build_and_verified_sdk_receipt_evidence"
|
||||
: "submitted_cross_platform_release_build_evidence";
|
||||
return {
|
||||
promotionEligible: errors.length === 0,
|
||||
errors,
|
||||
targets,
|
||||
thresholds: { minimumSpeedupPercent, maximumMemoryRegressionPercent },
|
||||
verificationBoundary,
|
||||
};
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const result = {
|
||||
input: null,
|
||||
sdkHeaderReceipt: null,
|
||||
addonReceipt: null,
|
||||
ccxReceipt: null,
|
||||
ccxPath: null,
|
||||
minimumSpeedupPercent: 30,
|
||||
maximumMemoryRegressionPercent: 10,
|
||||
};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const value = argv[index];
|
||||
if (value === "--input") result.input = argv[++index];
|
||||
else if (value === "--sdk-header-receipt") result.sdkHeaderReceipt = argv[++index];
|
||||
else if (value === "--addon-receipt") result.addonReceipt = argv[++index];
|
||||
else if (value === "--ccx-receipt") result.ccxReceipt = argv[++index];
|
||||
else if (value === "--ccx") result.ccxPath = argv[++index];
|
||||
else if (value === "--min-speedup-percent") result.minimumSpeedupPercent = Number(argv[++index]);
|
||||
else if (value === "--max-memory-regression-percent") result.maximumMemoryRegressionPercent = Number(argv[++index]);
|
||||
else throw new Error(`Unknown argument: ${value}`);
|
||||
}
|
||||
if (!result.input) throw new Error("--input <evidence.json> is required.");
|
||||
return result;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArguments(process.argv.slice(2));
|
||||
const [document, sdkHeaderReceipt, addonReceipt, ccxReceipt] = await Promise.all([
|
||||
readEvidenceJson(args.input, "benchmark evidence"),
|
||||
args.sdkHeaderReceipt ? readEvidenceJson(args.sdkHeaderReceipt, "SDK header receipt") : undefined,
|
||||
args.addonReceipt ? readEvidenceJson(args.addonReceipt, "addon-layout receipt") : undefined,
|
||||
args.ccxReceipt ? readEvidenceJson(args.ccxReceipt, "CCX receipt") : undefined,
|
||||
]);
|
||||
const result = await verifyHybridBenchmarkEvidenceWithLocalCcx(document, {
|
||||
...args,
|
||||
sdkHeaderReceipt,
|
||||
addonReceipt,
|
||||
ccxReceipt,
|
||||
});
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
if (!result.promotionEligible) process.exitCode = 1;
|
||||
}
|
||||
|
||||
async function readEvidenceJson(path, label) {
|
||||
try {
|
||||
return JSON.parse(await readFile(path, "utf8"));
|
||||
} catch {
|
||||
throw new Error(`${label} must be a readable JSON document.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
import {
|
||||
buildUxpHybridCcxReceipt,
|
||||
canonicalUxpHybridCcxReceiptSha256,
|
||||
verifyUxpHybridCcxReceipt,
|
||||
} from "./uxp-hybrid-ccx-receipt-core.mjs";
|
||||
import { UXP_HYBRID_CCX_RECEIPT_LEGACY_SEMANTICS } from "./uxp-hybrid-ccx-receipt-contract.mjs";
|
||||
|
||||
function receiptError(message) {
|
||||
const error = new Error(message);
|
||||
error.code = "UXP_HYBRID_CCX_RECEIPT_INVALID";
|
||||
return error;
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const options = { printCanonicalSha256: false };
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index];
|
||||
if (argument === "--print-canonical-sha256") options.printCanonicalSha256 = true;
|
||||
else if (["--input", "--ccx", "--addon-receipt", "--sdk-header-receipt"].includes(argument)) {
|
||||
const value = argv[++index];
|
||||
if (!value || value.startsWith("--")) throw receiptError(`${argument} requires a value`);
|
||||
if (argument === "--input") options.inputPath = value;
|
||||
else if (argument === "--ccx") options.ccxPath = value;
|
||||
else if (argument === "--addon-receipt") options.addonReceiptPath = value;
|
||||
else options.sdkHeaderReceiptPath = value;
|
||||
} else throw receiptError(`Unknown argument: ${argument}`);
|
||||
}
|
||||
if (!options.inputPath || !options.ccxPath || !options.addonReceiptPath || !options.sdkHeaderReceiptPath) {
|
||||
throw receiptError("Usage: node scripts/verify-uxp-hybrid-ccx-receipt.mjs --input <receipt.json> --ccx <archive.ccx> --addon-receipt <addon-receipt.json> --sdk-header-receipt <sdk-header-receipt.json> [--print-canonical-sha256]");
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async function readJson(path, label) {
|
||||
try { return JSON.parse(await readFile(resolve(path), "utf8")); } catch { throw receiptError(`${label} must be a readable JSON receipt`); }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArguments(process.argv.slice(2));
|
||||
const [receipt, addonReceipt, sdkHeaderReceipt] = await Promise.all([
|
||||
readJson(options.inputPath, "input"),
|
||||
readJson(options.addonReceiptPath, "addonReceipt"),
|
||||
readJson(options.sdkHeaderReceiptPath, "sdkHeaderReceipt"),
|
||||
]);
|
||||
verifyUxpHybridCcxReceipt(receipt, { addonReceipt, sdkHeaderReceipt });
|
||||
const current = await buildUxpHybridCcxReceipt({ ccxPath: resolve(options.ccxPath), addonReceipt, sdkHeaderReceipt });
|
||||
const expected = receipt.schemaVersion === 1
|
||||
? (() => {
|
||||
const { contents, ...legacy } = current;
|
||||
void contents;
|
||||
return { ...legacy, schemaVersion: 1, semantics: UXP_HYBRID_CCX_RECEIPT_LEGACY_SEMANTICS };
|
||||
})()
|
||||
: current;
|
||||
if (canonicalUxpHybridCcxReceiptSha256(receipt) !== canonicalUxpHybridCcxReceiptSha256(expected)) {
|
||||
throw receiptError("UXP Hybrid CCX receipt does not match the supplied local archive");
|
||||
}
|
||||
process.stdout.write(`UXP Hybrid CCX receipt is valid: ${receipt.stats.artifacts} addon artifacts and ${receipt.stats.entrypoints} entrypoint.\n`);
|
||||
if (options.printCanonicalSha256) process.stdout.write(`Canonical receipt SHA-256: ${canonicalUxpHybridCcxReceiptSha256(receipt)}\n`);
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user