@officexapp/vidfarm-devcli 0.21.55 → 0.21.57

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.
@@ -7,7 +7,7 @@
7
7
  // else prints as ⚠/✗ but does not fail the command, so agents can run doctor
8
8
  // unconditionally at session start.
9
9
  import { spawnSync } from "node:child_process";
10
- import { existsSync, readdirSync } from "node:fs";
10
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
11
11
  import { createRequire } from "node:module";
12
12
  import net from "node:net";
13
13
  import { homedir } from "node:os";
@@ -143,6 +143,28 @@ export async function runDoctorCommand(argv) {
143
143
  const killOrphans = Boolean(parsed.values["kill-orphans"]);
144
144
  const checks = [];
145
145
  const add = (name, level, detail) => checks.push({ name, level, detail });
146
+ // 0. Are the two halves current, and are they the SAME version? The skill and
147
+ // the devcli ship on one semver, and a gap between them is the usual cause of
148
+ // "that command doesn't exist" — so it belongs in the health check, not only
149
+ // in the update runbook. Never fails the doctor: offline is not a defect, and
150
+ // a pending update is information, not breakage.
151
+ try {
152
+ const { installedDevcliVersion, installedSkillPath, readSkillVersion, compareSemver, readUpdateState, hoursSinceLastCheck } = await import("./update-check.js");
153
+ const devcli = installedDevcliVersion();
154
+ const skillPath = installedSkillPath();
155
+ const skill = skillPath ? readSkillVersion(readFileSync(skillPath, "utf8")) : null;
156
+ const since = hoursSinceLastCheck(readUpdateState());
157
+ const staleNote = since === null ? " · never checked for updates" : since >= 24 ? ` · last update check ${Math.round(since / 24)}d ago` : "";
158
+ if (devcli && skill && compareSemver(devcli, skill) !== 0) {
159
+ add("versions", "warn", `devcli ${devcli} vs skill ${skill} — MISMATCHED. They ship together; run: vidfarm update-check`);
160
+ }
161
+ else {
162
+ add("versions", since !== null && since < 24 ? "ok" : "warn", `devcli ${devcli ?? "?"} · skill ${skill ?? "not installed"}${staleNote}${staleNote ? " — vidfarm update-check" : ""}`);
163
+ }
164
+ }
165
+ catch {
166
+ // A version probe must never take the doctor down with it.
167
+ }
146
168
  // 1. Node version.
147
169
  const nodeMajor = Number(process.versions.node.split(".")[0]);
148
170
  add("node", nodeMajor >= 22 ? "ok" : "fail", `v${process.versions.node}${nodeMajor >= 22 ? "" : " — vidfarm-devcli needs Node >= 22"}`);
@@ -560,7 +560,7 @@ async function startLocalRender(input) {
560
560
  void (async () => {
561
561
  try {
562
562
  const { renderCompositionLocally } = await import("./local-render.js");
563
- await renderCompositionLocally({
563
+ const outcome = await renderCompositionLocally({
564
564
  compositionHtml: compositionHtml,
565
565
  outputPath,
566
566
  stdio: "capture"
@@ -569,6 +569,11 @@ async function startLocalRender(input) {
569
569
  record.outputPath = outputPath;
570
570
  record.endedAt = Date.now();
571
571
  console.log(`[vidfarm] local render ${renderId} finished (${((record.endedAt - record.startedAt) / 1000).toFixed(1)}s) → ${outputPath}`);
572
+ // The editor keeps the file (a still card is a legal output here), but a
573
+ // frozen render is almost always a bug and must not pass silently.
574
+ if (outcome.motion?.frozen) {
575
+ console.warn(`[vidfarm] ⚠ render ${renderId} NEVER MOVES — ${outcome.motion.reason} Check for window.__player/__hf assignments and a registered window.__timelines entry (vidfarm lint).`);
576
+ }
572
577
  }
573
578
  catch (error) {
574
579
  record.status = "FAILED";
@@ -638,7 +643,7 @@ async function startStudioRender(input) {
638
643
  void (async () => {
639
644
  try {
640
645
  const { renderCompositionLocally } = await import("./local-render.js");
641
- await renderCompositionLocally({
646
+ const outcome = await renderCompositionLocally({
642
647
  compositionHtml,
643
648
  outputPath,
644
649
  fps,
@@ -650,6 +655,9 @@ async function startStudioRender(input) {
650
655
  job.stage = "rendering";
651
656
  }
652
657
  });
658
+ if (outcome.motion?.frozen) {
659
+ console.warn(`[vidfarm] ⚠ studio render ${job.filename} NEVER MOVES — ${outcome.motion.reason} Check for window.__player/__hf assignments and a registered window.__timelines entry (vidfarm lint).`);
660
+ }
653
661
  // A cancel marks the job terminal for the UI but cannot abort the child
654
662
  // process — don't resurrect a cancelled job when it eventually finishes.
655
663
  if (job.status === "rendering") {
@@ -20,6 +20,8 @@ import os from "node:os";
20
20
  import path from "node:path";
21
21
  import { prepareProjectMediaForRender } from "../lib/render-media-prep.js";
22
22
  import { resolveBundledFfprobe } from "../lib/ffprobe-path.js";
23
+ import { describeEngineGlobalHits, findEngineOwnedGlobalAssignments } from "../lib/engine-globals.js";
24
+ import { checkRenderMotion } from "../lib/frozen-render.js";
23
25
  import { runHyperframesCommand } from "./hyperframes-cli.js";
24
26
  import { normalizeTikTokCaptionLayout } from "./composition-edit.js";
25
27
  // Same invariant as the backend's forceEvenCompositionDimensions: libx264
@@ -70,6 +72,16 @@ export async function renderCompositionLocally(input) {
70
72
  if (!input.compositionHtml.includes("data-composition-id=")) {
71
73
  throw new Error("Local render requires composition HTML with data-composition-id.");
72
74
  }
75
+ // ── Preflight: engine-owned globals ─────────────────────────────────────────
76
+ // Refuse BEFORE spending 45 seconds producing a still image. This is the one
77
+ // authoring mistake whose render "succeeds" in every observable way, so the
78
+ // only place it can be stopped is here.
79
+ if (!input.allowEngineGlobals) {
80
+ const hits = findEngineOwnedGlobalAssignments(input.compositionHtml);
81
+ if (hits.length > 0) {
82
+ throw new Error(`Refusing to render: ${describeEngineGlobalHits(hits)}`);
83
+ }
84
+ }
73
85
  const startedAt = Date.now();
74
86
  const projectDir = await mkdtemp(path.join(os.tmpdir(), "vidfarm-local-render-"));
75
87
  let prepNotes = [];
@@ -121,13 +133,27 @@ export async function renderCompositionLocally(input) {
121
133
  if (!existsSync(input.outputPath)) {
122
134
  throw new Error(`hyperframes render reported success but wrote no file at ${input.outputPath}`);
123
135
  }
136
+ // ── Post-render: does it actually move? ──────────────────────────────────
137
+ // hyperframes downgrades `sub_timeline_readiness_timeout` to a warning and
138
+ // writes the file anyway, so a correct-looking exit 0 is not evidence of a
139
+ // video. One cheap ffmpeg pass settles it. Never throws — a missing ffmpeg
140
+ // comes back `skipped` rather than failing a render that is probably fine.
141
+ let motion;
142
+ if (!input.skipMotionCheck) {
143
+ motion = await checkRenderMotion(input.outputPath);
144
+ if (motion.frozen)
145
+ log("local_render.frozen", { output: input.outputPath, ...motion });
146
+ else if (motion.skipped)
147
+ log("local_render.motion_unverified", { reason: motion.reason });
148
+ }
124
149
  const durationMs = Date.now() - startedAt;
125
- log("local_render.succeeded", { output: input.outputPath, duration_ms: durationMs });
150
+ log("local_render.succeeded", { output: input.outputPath, duration_ms: durationMs, frozen: motion?.frozen ?? null });
126
151
  return {
127
152
  outputPath: input.outputPath,
128
153
  projectDir: input.keepProjectDir ? projectDir : null,
129
154
  durationMs,
130
- prepNotes
155
+ prepNotes,
156
+ ...(motion ? { motion } : {})
131
157
  };
132
158
  }
133
159
  finally {