Resi cue countdowns on ProPresenter and in Companion
The Resi -> ProPresenter bridge, its installer and site kit, and the Companion module resi-studio-cues, as running at North Attleboro. Bridge (resi_to_propresenter.py) - Follows a Resi decoder's playhead and keeps ProPresenter's NextCue timer counting down to the next cue; optional cue name as a stage message, sent once per cue (ProPresenter flashes it on every send) and off by default. - Playhead estimate no longer learns a cadence from the partial first gap after a start, so a 30s Resi doesn't read as paused half the time. - Backs off on 429, holds 2 min before retrying a failed startup login, and logs Resi's actual reason (wrong password, USER_LOCKED, rate limit). - --setup asks for the login, decoder, ProPresenter and timer. install_bridge.sh installs it as a login service (outside ~/Documents, which launchd may not read); make_kit.sh builds the kit for another site. Companion module resi-studio-cues 0.1.4 (source/, package committed) - Same playhead fix; login and request hold-off (15s doubling to 5 min); a 401/403 on a fresh session is a permission, not a reason to sign in again; init never fails on a Resi problem, so Companion can't restart it into a login loop. Tests: test_playhead.py, test_stage_message.py, test_rate_limit.py and source/backoff_test.js run offline; test_end_to_end.py drives a real ProPresenter and refuses to run without an explicit flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Walk every .js in a package and check that each require()/import target resolves,
|
||||
following Node's resolution rules against the vendored node_modules tree."""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = os.path.abspath(sys.argv[1])
|
||||
BUILTIN = set("""assert async_hooks buffer child_process cluster console constants crypto dgram
|
||||
diagnostics_channel dns domain events fs http http2 https inspector module net os path
|
||||
perf_hooks process punycode querystring readline repl stream string_decoder sys timers tls
|
||||
trace_events tty url util v8 vm wasi worker_threads zlib""".split())
|
||||
|
||||
SPEC = re.compile(r"""(?:require\(|(?:^|[\s;{}])(?:import|export)[^;\n]*?\bfrom\s*|import\()\s*['"]([^'"]+)['"]""", re.M)
|
||||
DYNAMIC = re.compile(r"require\(\s*[^'\")\s]")
|
||||
COMMENTS = re.compile(r"/\*.*?\*/|^\s*//.*$", re.S | re.M)
|
||||
# Optional at runtime: loaded only under a config or platform this module never uses.
|
||||
OPTIONAL = {"re2", "expo-random", "tinybench", "uri-js", "neostandard"}
|
||||
SKIP_DIRS = ("test", "benchmark", "benchmarks", "example", "examples", ".github")
|
||||
|
||||
|
||||
def load_json(p):
|
||||
try:
|
||||
with open(p) as fh:
|
||||
return json.load(fh)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def resolve_file(base):
|
||||
for cand in (base, base + ".js", base + ".json", base + ".cjs", base + ".mjs", base + ".node"):
|
||||
if os.path.isfile(cand):
|
||||
return cand
|
||||
if os.path.isdir(base):
|
||||
pj = load_json(os.path.join(base, "package.json"))
|
||||
for key in ("main", "module"):
|
||||
if isinstance(pj.get(key), str):
|
||||
hit = resolve_file(os.path.join(base, pj[key]))
|
||||
if hit:
|
||||
return hit
|
||||
if pj.get("exports"):
|
||||
return base # exports map: trust it, too complex to model here
|
||||
for idx in ("index.js", "index.json", "index.cjs", "index.node"):
|
||||
if os.path.isfile(os.path.join(base, idx)):
|
||||
return os.path.join(base, idx)
|
||||
return None
|
||||
|
||||
|
||||
def resolve_bare(spec, from_dir):
|
||||
d = from_dir
|
||||
while True:
|
||||
hit = resolve_file(os.path.join(d, "node_modules", *spec.split("/")))
|
||||
if hit:
|
||||
return hit
|
||||
# scoped or deep import into a package that exists
|
||||
parts = spec.split("/")
|
||||
pkg = "/".join(parts[:2]) if spec.startswith("@") else parts[0]
|
||||
pkg_dir = os.path.join(d, "node_modules", *pkg.split("/"))
|
||||
if os.path.isdir(pkg_dir) and len(parts) > (2 if spec.startswith("@") else 1):
|
||||
sub = resolve_file(os.path.join(pkg_dir, *parts[(2 if spec.startswith("@") else 1):]))
|
||||
if sub:
|
||||
return sub
|
||||
if load_json(os.path.join(pkg_dir, "package.json")).get("exports"):
|
||||
return pkg_dir
|
||||
if os.path.dirname(d) == d or len(d) <= len(ROOT) - 1:
|
||||
return None
|
||||
d = os.path.dirname(d)
|
||||
|
||||
|
||||
missing, dynamic, files, optional_hits = [], [], 0, set()
|
||||
for dirpath, dirnames, filenames in os.walk(ROOT):
|
||||
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
||||
if dirpath.endswith((os.sep + "browser", os.sep + "types")):
|
||||
continue
|
||||
for fn in filenames:
|
||||
if not fn.endswith((".js", ".cjs", ".mjs")):
|
||||
continue
|
||||
if fn.endswith((".native.js", ".browser.js")) or fn.startswith("eslint.config"):
|
||||
continue
|
||||
path = os.path.join(dirpath, fn)
|
||||
files += 1
|
||||
try:
|
||||
src = COMMENTS.sub("", open(path, encoding="utf-8", errors="replace").read())
|
||||
except OSError:
|
||||
continue
|
||||
if DYNAMIC.search(src):
|
||||
dynamic.append(os.path.relpath(path, ROOT))
|
||||
for spec in set(SPEC.findall(src)):
|
||||
if spec.startswith("node:") or spec.split("/")[0] in BUILTIN:
|
||||
continue
|
||||
if spec in OPTIONAL:
|
||||
optional_hits.add((os.path.relpath(path, ROOT), spec))
|
||||
continue
|
||||
if spec.startswith("."):
|
||||
hit = resolve_file(os.path.normpath(os.path.join(dirpath, spec)))
|
||||
else:
|
||||
hit = resolve_bare(spec, dirpath)
|
||||
if not hit:
|
||||
missing.append((os.path.relpath(path, ROOT), spec))
|
||||
|
||||
print("scanned %d js files" % files)
|
||||
if dynamic:
|
||||
print("\n%d file(s) use a computed require() (not statically checkable):" % len(dynamic))
|
||||
for d in sorted(dynamic)[:12]:
|
||||
print(" ", d)
|
||||
if optional_hits:
|
||||
print("\n%d optional dep(s), not loaded on this platform/config:" % len(optional_hits))
|
||||
for f, spec in sorted(optional_hits):
|
||||
print(" %-64s -> %s" % (f, spec))
|
||||
if missing:
|
||||
print("\nUNRESOLVED (%d):" % len(missing))
|
||||
for f, s in sorted(set(missing)):
|
||||
print(" %-70s -> %s" % (f, s))
|
||||
sys.exit(1)
|
||||
print("\nevery static require/import resolves")
|
||||
Reference in New Issue
Block a user