Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/developers/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ remains authoritative: run `npx hyperframes <command> --help`.
| --- | --- |
| Add a Catalog item | `npx hyperframes add <name>` |
| List compositions | `npx hyperframes compositions` |
| Print tracks and clips | `npx hyperframes timeline` (`--json` for agents) |
| Inspect keyframe behavior | `npx hyperframes keyframes` |
| Compare two or more versions | `npx hyperframes compare v1/ v2/` |

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ const commandLoaders = {
layout: () => import("./commands/layout.js").then((m) => m.default),
info: () => import("./commands/info.js").then((m) => m.default),
compositions: () => import("./commands/compositions.js").then((m) => m.default),
timeline: () => import("./commands/timeline.js").then((m) => m.default),
benchmark: () => import("./commands/benchmark.js").then((m) => m.default),
browser: () => import("./commands/browser.js").then((m) => m.default),
"remove-background": () => import("./commands/remove-background.js").then((m) => m.default),
Expand Down
70 changes: 3 additions & 67 deletions packages/cli/src/commands/compositions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { defineCommand } from "citty";
import { parseNumeric, parseStartExpression } from "@hyperframes/core";
import type { Example } from "./_examples.js";
import { existsSync, readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
Expand All @@ -11,6 +10,7 @@ export const examples: Example[] = [
import { c } from "../ui/colors.js";
import { ensureDOMParser } from "../utils/dom.js";
import { resolveProject } from "../utils/project.js";
import { resolveReferencedStart } from "@hyperframes/engine";
import { withMeta } from "../utils/updateCheck.js";

interface CompositionInfo {
Expand Down Expand Up @@ -46,70 +46,6 @@ function estimateDurationFromScripts(root: ParentNode): number {
return duration;
}

function findReferenceTargetEl(doc: Document, refId: string): Element | null {
return doc.getElementById(refId) ?? doc.querySelector(`[data-composition-id="${refId}"]`);
}

function resolveStart(
doc: Document,
el: Element,
startCache: Map<Element, number>,
visiting: Set<Element>,
): number {
const cached = startCache.get(el);
if (cached !== undefined) return cached;
if (visiting.has(el)) return 0;
visiting.add(el);

try {
const expression = parseStartExpression(el.getAttribute("data-start"));
if (!expression) {
startCache.set(el, 0);
return 0;
}

if (expression.kind === "absolute") {
const value = Math.max(0, expression.value);
startCache.set(el, value);
return value;
}

const target = findReferenceTargetEl(doc, expression.refId);
if (!target) {
startCache.set(el, 0);
return 0;
}

const targetStart = resolveStart(doc, target, startCache, visiting);
const targetDuration = resolveReferencedDuration(doc, target, startCache, visiting);
const resolved =
targetDuration != null && targetDuration > 0
? Math.max(0, targetStart + targetDuration + expression.offset)
: Math.max(0, targetStart + expression.offset);
startCache.set(el, resolved);
return resolved;
} finally {
visiting.delete(el);
}
}

function resolveReferencedDuration(
doc: Document,
el: Element,
startCache: Map<Element, number>,
visiting: Set<Element>,
): number | null {
const durationAttr = parseNumeric(el.getAttribute("data-duration"));
if (durationAttr != null && durationAttr > 0) return durationAttr;
const endAttr = parseNumeric(el.getAttribute("data-end"));
if (endAttr != null) {
const start = resolveStart(doc, el, startCache, visiting);
const delta = endAttr - start;
if (Number.isFinite(delta) && delta > 0) return delta;
}
return null;
}

export function parseCompositions(html: string, baseDir: string): CompositionInfo[] {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
Expand Down Expand Up @@ -142,7 +78,7 @@ export function parseCompositions(html: string, baseDir: string): CompositionInf

timedChildren.forEach((el) => {
elementCount++;
const start = resolveStart(doc, el, startCache, visiting);
const start = resolveReferencedStart(doc, el, startCache, visiting);
const endAttr = el.getAttribute("data-end");
const durationAttr = el.getAttribute("data-duration");

Expand Down Expand Up @@ -212,7 +148,7 @@ export function parseSubComposition(
const visiting = new Set<Element>();
timedEls.forEach((el) => {
elementCount = Math.max(elementCount, timedEls.length);
const start = resolveStart(doc, el, startCache, visiting);
const start = resolveReferencedStart(doc, el, startCache, visiting);
const endAttr = el.getAttribute("data-end");
const durAttr = el.getAttribute("data-duration");

Expand Down
31 changes: 31 additions & 0 deletions packages/cli/src/commands/timeline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { defineCommand } from "citty";
import type { Example } from "./_examples.js";
import { describeProject } from "../timeline/describeProject.js";
import { formatTimeline } from "../timeline/formatTimeline.js";
import { ensureDOMParser } from "../utils/dom.js";
import { resolveProject } from "../utils/project.js";
import { withMeta } from "../utils/updateCheck.js";

export const examples: Example[] = [
["Show every track and clip of the project in the current directory", "hyperframes timeline"],
["Same, as JSON for an agent", "hyperframes timeline ./my-video --json"],
];

export default defineCommand({
meta: {
name: "timeline",
description: "Print the project's tracks and clips (start, duration, source, volume, rate)",
},
args: {
dir: { type: "positional", description: "Project directory", required: false },
json: { type: "boolean", description: "Output as JSON", default: false },
},
async run({ args }) {
const project = resolveProject(args.dir);
ensureDOMParser();
const timeline = describeProject(project.indexPath);
console.log(
args.json ? JSON.stringify(withMeta({ timeline }), null, 2) : formatTimeline(timeline),
);
},
});
1 change: 1 addition & 0 deletions packages/cli/src/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const GROUPS: Group[] = [
["compare", "Render composition variants into one labeled comparison sheet"],
["info", "Print project metadata"],
["compositions", "List all compositions in a project"],
["timeline", "Print the project's tracks and clips"],
["docs", "View inline documentation in the terminal"],
],
},
Expand Down
Loading
Loading