@engine-room/after-effects-mcp 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -185
- package/bin/server.js +12555 -2477
- package/package.json +8 -8
- package/panel/CSXS/manifest.xml +44 -4
- package/panel/META-INF/signatures.xml +182 -0
- package/panel/client/csinterface.js +32 -0
- package/panel/client/main.js +522 -632
- package/panel/client/panelhash.js +79 -0
- package/panel/client/status.html +27 -0
- package/panel/client/status.js +112 -0
- package/panel/jsx/bundle.jsx +8957 -2514
- package/panel/mimetype +1 -0
- package/panel/node_modules/ws/LICENSE +20 -0
- package/panel/node_modules/ws/README.md +548 -0
- package/panel/node_modules/ws/browser.js +8 -0
- package/panel/node_modules/ws/index.js +22 -0
- package/panel/node_modules/ws/lib/buffer-util.js +131 -0
- package/panel/node_modules/ws/lib/constants.js +19 -0
- package/panel/node_modules/ws/lib/event-target.js +292 -0
- package/panel/node_modules/ws/lib/extension.js +203 -0
- package/panel/node_modules/ws/lib/limiter.js +55 -0
- package/panel/node_modules/ws/lib/permessage-deflate.js +530 -0
- package/panel/node_modules/ws/lib/receiver.js +743 -0
- package/panel/node_modules/ws/lib/sender.js +607 -0
- package/panel/node_modules/ws/lib/stream.js +161 -0
- package/panel/node_modules/ws/lib/subprotocol.js +62 -0
- package/panel/node_modules/ws/lib/validation.js +152 -0
- package/panel/node_modules/ws/lib/websocket-server.js +562 -0
- package/panel/node_modules/ws/lib/websocket.js +1421 -0
- package/panel/node_modules/ws/package.json +74 -0
- package/panel/node_modules/ws/wrapper.mjs +21 -0
- package/panel/package.json +2 -2
- package/panel/client/contactsheet.js +0 -347
- package/panel/client/framecache.js +0 -98
- package/panel/client/framereader.js +0 -248
- package/panel/client/mogrt.js +0 -328
- package/panel/client/pngcodec.js +0 -524
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// panelhash.js — the hash that names the panel's own code (issue #243).
|
|
2
|
+
//
|
|
3
|
+
// `bundleHash` on /health names the ExtendScript the panel evaluated. It says
|
|
4
|
+
// nothing about the files beside it — this one, main.js, index.html, the
|
|
5
|
+
// manifest — and a panel with a current bundle and old client code used to
|
|
6
|
+
// pass the version gate. `clientHash` is the other half: every shipped file of
|
|
7
|
+
// the panel except bundle.jsx, which has its own hash because /reload-jsx can
|
|
8
|
+
// replace it without a restart.
|
|
9
|
+
//
|
|
10
|
+
// The server computes the same value over the panel it ships
|
|
11
|
+
// (packages/mcp-server/src/setup/panelHash.ts) and over the installed copy;
|
|
12
|
+
// tests/unit/panel-hash.mjs holds the two implementations to each other.
|
|
13
|
+
// Change both together.
|
|
14
|
+
//
|
|
15
|
+
// Left out, and why:
|
|
16
|
+
// jsx/bundle.jsx its own hash (bundleHash), which follows reloads
|
|
17
|
+
// node_modules/ (any) `ws` is copied in beside the panel separately
|
|
18
|
+
// .DS_Store, Thumbs.db, Finder and Explorer litter
|
|
19
|
+
// desktop.ini (any)
|
|
20
|
+
// META-INF/, mimetype what a self-signed install adds (issues #91, #182);
|
|
21
|
+
// a signed install is a current install
|
|
22
|
+
// package.json (root) a version stamp; nothing at runtime reads it
|
|
23
|
+
// and in CSXS/manifest.xml the two version attributes are blanked, so a
|
|
24
|
+
// release that changes nothing in the panel does not ask every user to
|
|
25
|
+
// reinstall it and restart After Effects.
|
|
26
|
+
//
|
|
27
|
+
// A pure function of the files: sorted paths, '/' separators, no timestamps.
|
|
28
|
+
|
|
29
|
+
"use strict";
|
|
30
|
+
|
|
31
|
+
var fs = require("fs");
|
|
32
|
+
var path = require("path");
|
|
33
|
+
var crypto = require("crypto");
|
|
34
|
+
|
|
35
|
+
var SKIP_ANY_DEPTH = { "node_modules": true, ".DS_Store": true, "Thumbs.db": true, "desktop.ini": true };
|
|
36
|
+
var SKIP_AT_ROOT = { "META-INF": true, "mimetype": true, "package.json": true };
|
|
37
|
+
var SKIP_PATHS = { "jsx/bundle.jsx": true };
|
|
38
|
+
var MANIFEST = "CSXS/manifest.xml";
|
|
39
|
+
|
|
40
|
+
function listFiles(dir, prefix, out) {
|
|
41
|
+
var names = fs.readdirSync(dir);
|
|
42
|
+
for (var i = 0; i < names.length; i++) {
|
|
43
|
+
var name = names[i];
|
|
44
|
+
if (SKIP_ANY_DEPTH[name]) continue;
|
|
45
|
+
if (!prefix && SKIP_AT_ROOT[name]) continue;
|
|
46
|
+
var rel = prefix ? prefix + "/" + name : name;
|
|
47
|
+
if (SKIP_PATHS[rel]) continue;
|
|
48
|
+
var full = path.join(dir, name);
|
|
49
|
+
// stat, not lstat: a `--symlink` dev install links into the source tree.
|
|
50
|
+
if (fs.statSync(full).isDirectory()) listFiles(full, rel, out);
|
|
51
|
+
else out.push(rel);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalise(rel, bytes) {
|
|
57
|
+
if (rel !== MANIFEST) return bytes;
|
|
58
|
+
var text = bytes.toString("utf8")
|
|
59
|
+
.replace(/(ExtensionBundleVersion=")[^"]*(")/g, "$1$2")
|
|
60
|
+
.replace(/(<Extension\s+Id="[^"]*"\s+Version=")[^"]*(")/g, "$1$2");
|
|
61
|
+
return Buffer.from(text, "utf8");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** sha256 over every panel file but the ones above, or null when the folder cannot be read. */
|
|
65
|
+
function panelClientHash(dir) {
|
|
66
|
+
try {
|
|
67
|
+
var files = listFiles(dir, "", []).sort(function (a, b) { return a < b ? -1 : a > b ? 1 : 0; });
|
|
68
|
+
var h = crypto.createHash("sha256");
|
|
69
|
+
for (var i = 0; i < files.length; i++) {
|
|
70
|
+
var bytes = normalise(files[i], fs.readFileSync(path.join(dir, files[i])));
|
|
71
|
+
h.update(files[i] + "\u0000" + crypto.createHash("sha256").update(bytes).digest("hex") + "\n");
|
|
72
|
+
}
|
|
73
|
+
return h.digest("hex");
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { panelClientHash: panelClientHash };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>AE MCP Bridge</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: -apple-system, "Segoe UI", sans-serif; background: #2a2a2a; color: #ddd; padding: 12px; margin: 0; font-size: 12px; }
|
|
8
|
+
h1 { font-size: 13px; margin: 0 0 8px 0; color: #fff; }
|
|
9
|
+
.row { margin: 4px 0; }
|
|
10
|
+
.label { color: #888; }
|
|
11
|
+
.ok { color: #6f6; } .err { color: #f66; } .warn { color: #fc6; }
|
|
12
|
+
#log { margin-top: 8px; font-family: ui-monospace, Menlo, monospace; font-size: 10px; background: #1a1a1a; padding: 6px; border-radius: 4px; max-height: 220px; overflow: auto; }
|
|
13
|
+
#log div { white-space: pre-wrap; }
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<h1>AE MCP Bridge</h1>
|
|
18
|
+
<div class="row"><span class="label">Status:</span> <span id="status">asking the bridge…</span></div>
|
|
19
|
+
<div class="row"><span class="label">Port:</span> <span id="port">—</span></div>
|
|
20
|
+
<div class="row"><span class="label">AE:</span> <span id="ae">—</span></div>
|
|
21
|
+
<div class="row"><span class="label">JSX:</span> <span id="jsx">—</span></div>
|
|
22
|
+
<div class="row"><span class="label">Requests:</span> <span id="reqs">0</span></div>
|
|
23
|
+
<div id="log"></div>
|
|
24
|
+
<script src="./csinterface.js"></script>
|
|
25
|
+
<script src="./status.js"></script>
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// status.js — the visible status panel (Window > Extensions > AE MCP Bridge).
|
|
2
|
+
//
|
|
3
|
+
// Issue #283. The bridge itself runs in a hidden extension of this bundle
|
|
4
|
+
// (games.engine-room.ae-mcp.bridge, client/index.html + main.js) that starts
|
|
5
|
+
// with After Effects and is never shown. This page is a separate extension
|
|
6
|
+
// with no Node: it runs no server, binds no port, writes no file. It only
|
|
7
|
+
// shows what the bridge reports over a CEP event, so opening it — or opening
|
|
8
|
+
// it twice — can never start a second bridge.
|
|
9
|
+
//
|
|
10
|
+
// It keeps the id the old visible bridge had (games.engine-room.ae-mcp.panel),
|
|
11
|
+
// so a workspace saved with that panel docked restores this window into the
|
|
12
|
+
// same frame rather than pointing a Panel slot at a hidden extension.
|
|
13
|
+
//
|
|
14
|
+
// Silence is not death. While After Effects runs a long script the bridge's
|
|
15
|
+
// answers can stop for as long as the script runs, so a quiet bridge is shown
|
|
16
|
+
// as "no answer for Ns (After Effects may be busy)" and the window goes back
|
|
17
|
+
// to the bridge's own status the moment an answer arrives. The one active
|
|
18
|
+
// thing this page does is for a bridge it has never heard from: ask CEP, once,
|
|
19
|
+
// to open the bridge extension — a no-op for an extension already running,
|
|
20
|
+
// and the recovery for a host that did not start it at launch.
|
|
21
|
+
|
|
22
|
+
(function () {
|
|
23
|
+
"use strict";
|
|
24
|
+
|
|
25
|
+
var BRIDGE_ID = "games.engine-room.ae-mcp.bridge";
|
|
26
|
+
// Event types are their own namespace, never an extension id.
|
|
27
|
+
var STATUS_EVENT = "games.engine-room.ae-mcp.event.status";
|
|
28
|
+
var STATUS_REQUEST = "games.engine-room.ae-mcp.event.status-request";
|
|
29
|
+
var ASK_EVERY_MS = 2000;
|
|
30
|
+
var SILENT_AFTER_MS = 6000;
|
|
31
|
+
// A window restored by a saved workspace opens at launch, while the bridge
|
|
32
|
+
// takes 7–11 s to answer (measured on AE 26.5). Asking CEP to open it before
|
|
33
|
+
// then would be asked on nearly every launch, mid-startup.
|
|
34
|
+
var OPEN_AFTER_MS = 30000;
|
|
35
|
+
// Past this, a bridge that once answered is more likely gone than busy.
|
|
36
|
+
var STOPPED_AFTER_MS = 120000;
|
|
37
|
+
|
|
38
|
+
function $(id) { return document.getElementById(id); }
|
|
39
|
+
function set(id, text, klass) {
|
|
40
|
+
var el = $(id);
|
|
41
|
+
if (!el) return;
|
|
42
|
+
el.textContent = text;
|
|
43
|
+
el.className = klass || "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
var cs;
|
|
47
|
+
try { cs = new CSInterface(); }
|
|
48
|
+
catch (e) { set("status", "cannot ask the bridge — CSInterface is unavailable", "err"); return; }
|
|
49
|
+
|
|
50
|
+
var lastHeard = 0;
|
|
51
|
+
var openedBridge = false;
|
|
52
|
+
var startedAt = new Date().getTime();
|
|
53
|
+
|
|
54
|
+
function render(state) {
|
|
55
|
+
set("status", state.status || "—", state.statusClass);
|
|
56
|
+
set("port", state.port || "—", state.portClass);
|
|
57
|
+
set("ae", state.ae || "—");
|
|
58
|
+
set("jsx", state.jsx || "—", state.jsxClass);
|
|
59
|
+
set("reqs", String(state.reqs || 0));
|
|
60
|
+
var $log = $("log");
|
|
61
|
+
if (!$log) return;
|
|
62
|
+
while ($log.firstChild) $log.removeChild($log.firstChild);
|
|
63
|
+
var lines = state.log || [];
|
|
64
|
+
for (var i = 0; i < lines.length; i++) {
|
|
65
|
+
var d = document.createElement("div");
|
|
66
|
+
d.className = lines[i].level || "";
|
|
67
|
+
d.textContent = lines[i].text;
|
|
68
|
+
$log.appendChild(d);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
cs.addEventListener(STATUS_EVENT, function (event) {
|
|
73
|
+
var data = event && event.data;
|
|
74
|
+
if (typeof data === "string") {
|
|
75
|
+
try { data = JSON.parse(data); } catch (e) { return; }
|
|
76
|
+
}
|
|
77
|
+
if (!data || typeof data !== "object") return;
|
|
78
|
+
lastHeard = new Date().getTime();
|
|
79
|
+
render(data);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
function ask() {
|
|
83
|
+
try {
|
|
84
|
+
var ev = new CSEvent(STATUS_REQUEST, "APPLICATION");
|
|
85
|
+
ev.data = "";
|
|
86
|
+
cs.dispatchEvent(ev);
|
|
87
|
+
} catch (e) {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function tick() {
|
|
91
|
+
var now = new Date().getTime();
|
|
92
|
+
var since = lastHeard || startedAt;
|
|
93
|
+
ask();
|
|
94
|
+
var quiet = now - since;
|
|
95
|
+
if (quiet < SILENT_AFTER_MS) return;
|
|
96
|
+
if (!lastHeard && !openedBridge && quiet >= OPEN_AFTER_MS) {
|
|
97
|
+
openedBridge = true;
|
|
98
|
+
try { cs.requestOpenExtension(BRIDGE_ID, ""); } catch (e) {}
|
|
99
|
+
}
|
|
100
|
+
var secs = Math.round(quiet / 1000);
|
|
101
|
+
// The port and the log stay as last reported: they are still true of a
|
|
102
|
+
// bridge that is busy, and the next answer replaces all of it.
|
|
103
|
+
if (quiet >= STOPPED_AFTER_MS) {
|
|
104
|
+
set("status", "no answer for " + secs + "s — the bridge may have stopped; ask the agent to run check_setup", "err");
|
|
105
|
+
} else {
|
|
106
|
+
set("status", "no answer for " + secs + "s (After Effects may be busy)", "warn");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
ask();
|
|
111
|
+
setInterval(tick, ASK_EVERY_MS);
|
|
112
|
+
})();
|