app
Premiere Pro Bridge
Public Made by Adomby adom
Let the AI edit video in Adobe Premiere Pro on your desktop (import, cut, trim, razor, effects, audio, previews, export) with no mouse or keyboard. Check adom-bridge bridge_list for 'premiere'; install with bridge_install + adom-wiki pkg install adom/premiere-bridge, then start with premiere_status.
main
anastasia
Premiere Pro Bridge 0.1.0: Rust bridge, UXP plugin, CEP helper, skills, macOS manifest
32fb583
10d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
//! Installing the two in-app halves. Both are compiled INTO this binary (include_str!),
//! so the Release zip stays a single executable + bridge.json and a bridge update always
//! carries the matching plugin.
//!
//! * UXP plugin → packaged as a .ccx and installed with Adobe's own
//! UnifiedPluginInstallerAgent (UPIA) — no dialogs, no Developer Tool.
//! * CEP helper → copied into the per-user CEP extensions folder; unsigned CEP needs
//! PlayerDebugMode=1 for the CSXS versions Premiere uses.
use crate::Shared;
use serde_json::{json, Value};
use std::{
io::Write,
path::{Path, PathBuf},
};
use tokio::process::Command;
pub const BUNDLED_VERSION: &str = "0.1.0";
const UXP_ID: &str = "inc.adom.premiere.bridge";
const UXP_NAME: &str = "Adom Bridge";
const CEP_ID: &str = "inc.adom.premiere.cep";
const UXP_FILES: &[(&str, &str)] = &[
("manifest.json", include_str!("../plugin/uxp/manifest.json")),
("index.html", include_str!("../plugin/uxp/index.html")),
("index.js", include_str!("../plugin/uxp/index.js")),
];
const UXP_ICONS: &[(&str, &[u8])] = &[
("icons/[email protected]", include_bytes!("../plugin/uxp/icons/[email protected]")),
("icons/[email protected]", include_bytes!("../plugin/uxp/icons/[email protected]")),
("icons/[email protected]", include_bytes!("../plugin/uxp/icons/[email protected]")),
("icons/[email protected]", include_bytes!("../plugin/uxp/icons/[email protected]")),
];
const CEP_FILES: &[(&str, &str)] = &[
("CSXS/manifest.xml", include_str!("../plugin/cep/CSXS/manifest.xml")),
("index.html", include_str!("../plugin/cep/index.html")),
("main.js", include_str!("../plugin/cep/main.js")),
("host.jsx", include_str!("../plugin/cep/host.jsx")),
];
fn record_path(st: &Shared) -> PathBuf {
st.data_dir.join("plugin-installed.json")
}
pub fn installed_version() -> Option<String> {
let p = crate::host::data_dir().join("plugin-installed.json");
let v: Value = serde_json::from_slice(&std::fs::read(p).ok()?).ok()?;
v["version"].as_str().map(str::to_string)
}
fn upia_path() -> Option<PathBuf> {
let p = if cfg!(target_os = "macos") {
PathBuf::from("/Library/Application Support/Adobe/Adobe Desktop Common/RemoteComponents/UPI/UnifiedPluginInstallerAgent/UnifiedPluginInstallerAgent.app/Contents/MacOS/UnifiedPluginInstallerAgent")
} else {
let cpf = std::env::var("CommonProgramFiles").unwrap_or_else(|_| r"C:\Program Files\Common Files".into());
PathBuf::from(cpf).join(r"Adobe\Adobe Desktop Common\RemoteComponents\UPI\UnifiedPluginInstallerAgent\UnifiedPluginInstallerAgent.exe")
};
p.exists().then_some(p)
}
fn cep_dir() -> PathBuf {
if cfg!(windows) {
let appdata = std::env::var("APPDATA").map(PathBuf::from).unwrap_or_else(|_| crate::host::home_dir().join("AppData/Roaming"));
appdata.join(r"Adobe\CEP\extensions").join(CEP_ID)
} else {
crate::host::home_dir().join("Library/Application Support/Adobe/CEP/extensions").join(CEP_ID)
}
}
/// Build the .ccx (a zip with manifest.json at the root). A copy of the connection file
/// rides inside so the plugin can find us even if it can't read ~/.adom.
fn build_ccx(st: &Shared) -> std::io::Result<PathBuf> {
let out = st.data_dir.join(format!("{UXP_ID}.ccx"));
let f = std::fs::File::create(&out)?;
let mut z = zip::ZipWriter::new(f);
let opts: zip::write::SimpleFileOptions = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
for (name, body) in UXP_FILES {
z.start_file(*name, opts).map_err(std::io::Error::other)?;
z.write_all(body.as_bytes())?;
}
for (name, body) in UXP_ICONS {
z.start_file(*name, opts).map_err(std::io::Error::other)?;
z.write_all(body)?;
}
let conn = std::fs::read(st.data_dir.join("connection.json")).unwrap_or_default();
z.start_file("connection.json", opts).map_err(std::io::Error::other)?;
z.write_all(&conn)?;
z.finish().map_err(std::io::Error::other)?;
Ok(out)
}
async fn run(cmd: &Path, args: &[&str]) -> (bool, String) {
match Command::new(cmd).args(args).output().await {
Ok(o) => {
let mut s = String::from_utf8_lossy(&o.stdout).into_owned();
s.push_str(&String::from_utf8_lossy(&o.stderr));
(o.status.success(), s.trim().to_string())
}
Err(e) => (false, e.to_string()),
}
}
async fn install_uxp(st: &Shared, force: bool) -> Value {
let Some(upia) = upia_path() else {
return json!({"ok": false, "errorCode": "upia_missing",
"error": "Adobe's UnifiedPluginInstallerAgent wasn't found (it ships with the Creative Cloud desktop app)."});
};
let ccx = match build_ccx(st) {
Ok(p) => p,
Err(e) => return json!({"ok": false, "errorCode": "ccx_build_failed", "error": e.to_string()}),
};
let flag = |mac: &'static str, win: &'static str| if cfg!(windows) { win } else { mac };
let mut log = Vec::new();
if force {
let (_, out) = run(&upia, &[flag("--remove", "/remove"), UXP_NAME]).await;
log.push(json!({"step": "remove", "output": out}));
}
let ccx_s = ccx.to_string_lossy().into_owned();
let (ok, out) = run(&upia, &[flag("--install", "/install"), &ccx_s]).await;
log.push(json!({"step": "install", "ok": ok, "output": out}));
// UPIA's exit code is unreliable across versions; its text is the real signal.
let lower = out.to_lowercase();
let success = ok && !lower.contains("fail") && !lower.contains("error") || lower.contains("successfully installed") || lower.contains("installation successful");
json!({"ok": success, "ccx": ccx, "upia": upia, "log": log})
}
async fn install_cep() -> Value {
let dir = cep_dir();
for (name, body) in CEP_FILES {
let p = dir.join(name);
if let Some(parent) = p.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
return json!({"ok": false, "errorCode": "cep_copy_failed", "error": e.to_string()});
}
}
if let Err(e) = std::fs::write(&p, body) {
return json!({"ok": false, "errorCode": "cep_copy_failed", "error": e.to_string()});
}
}
// Unsigned CEP extensions only load with PlayerDebugMode=1. Set it for every CSXS
// generation Premiere 2022–2027 might use; it's per-user and harmless for others.
let mut debug = Vec::new();
for n in 9..=13 {
let ok = if cfg!(windows) {
let key = format!(r"HKCU\Software\Adobe\CSXS.{n}");
Command::new("reg").args(["add", &key, "/v", "PlayerDebugMode", "/t", "REG_SZ", "/d", "1", "/f"]).output().await.map(|o| o.status.success()).unwrap_or(false)
} else {
Command::new("defaults").args(["write", &format!("com.adobe.CSXS.{n}"), "PlayerDebugMode", "1"]).output().await.map(|o| o.status.success()).unwrap_or(false)
};
debug.push(json!({"csxs": n, "ok": ok}));
}
json!({"ok": true, "dir": dir, "playerDebugMode": debug})
}
pub async fn install(st: &Shared, args: &Value) -> Value {
let force = args["force"].as_bool().unwrap_or(false);
let uxp = install_uxp(st, force).await;
let cep = if args["cep"].as_bool().unwrap_or(true) { install_cep().await } else { json!({"ok": false, "skipped": true}) };
let uxp_ok = uxp["ok"].as_bool().unwrap_or(false);
if uxp_ok {
let _ = std::fs::write(
record_path(st),
serde_json::to_vec_pretty(&json!({"version": BUNDLED_VERSION, "uxp": uxp["ccx"], "cep": cep["dir"]})).unwrap_or_default(),
);
}
let running = crate::host::premiere_running().await;
let mut out = json!({
"success": uxp_ok,
"version": BUNDLED_VERSION,
"uxp": uxp,
"cep": cep,
"premiereRunning": running,
});
if uxp_ok {
out["_hint"] = json!(if running {
"Installed. Premiere is running, so it hasn't loaded the plugin yet: premiere_quit {save:true} then premiere_launch. After launch, if pluginConnected stays false, the Adom Bridge panel needs opening once (Window > UXP Plugins > Adom Bridge) — the CEP helper connects on its own meanwhile."
} else {
"Installed. premiere_launch next; the plugin connects on startup."
});
out["_next"] = json!(if running { json!(["premiere_quit", "premiere_launch"]) } else { json!(["premiere_launch"]) });
} else {
out["errorCode"] = uxp["errorCode"].clone();
out["error"] = json!("UXP plugin install failed — see uxp.log for the installer's output.");
out["_hint"] = json!(format!(
"If UPIA refused the unsigned .ccx, the package is at {} — opening it with the Creative Cloud app installs it (one confirmation click). Retry with force:true if an older copy is in the way.",
uxp["ccx"].as_str().unwrap_or("?")
));
}
out
}
pub async fn info(st: &Shared) -> Value {
json!({
"success": true,
"bundledVersion": BUNDLED_VERSION,
"installedVersion": installed_version(),
"uxpPluginId": UXP_ID,
"upia": upia_path(),
"cepDir": cep_dir(),
"cepInstalled": cep_dir().join("CSXS/manifest.xml").exists(),
"ccx": st.data_dir.join(format!("{UXP_ID}.ccx")),
})
}