Skip to content
Open
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
3 changes: 3 additions & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ const commandStart = Date.now();
const runId = getRunId();
let finalized = false;

// Root-only lifecycle fan-in: telemetry, notices, flushing, then exit code.
// fallow-ignore-next-line complexity
async function finalizeCli(result: CommandResult): Promise<void> {
if (finalized) return;
Expand Down Expand Up @@ -374,6 +375,8 @@ function commandResultForError(error: unknown): CommandResult {
return { exitCode: 1, kind: "runtime_error" };
}

// Root-only command boundary; keeping every result path here prevents modules
// from bypassing output, telemetry, or finalizers.
// fallow-ignore-next-line complexity
async function executeCli(): Promise<void> {
let result: CommandResult = { exitCode: 0, kind: "success" };
Expand Down
21 changes: 5 additions & 16 deletions packages/cli/src/commands/auth/login.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { promises as fs } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { readStore, writeStore } from "../../auth/store.js";
import { setupTempAuthEnv, type EnvFixture } from "../../auth/_test-utils.js";
import { CliRuntimeError } from "../../utils/commandResult.js";

// Mock only AuthClient — keep the real store/resolver so the test
Expand Down Expand Up @@ -43,19 +43,13 @@ const telemetry = vi.hoisted(() => ({
}));
vi.mock("../../telemetry/index.js", () => telemetry);

const ENV_KEYS = ["HEYGEN_API_KEY", "HYPERFRAMES_API_KEY", "HEYGEN_CONFIG_DIR"] as const;

describe("auth login --api-key rollback", () => {
let dir: string;
const saved: Partial<Record<(typeof ENV_KEYS)[number], string | undefined>> = {};
let envFixture: EnvFixture;

beforeEach(async () => {
dir = await fs.mkdtemp(join(tmpdir(), "hf-login-"));
for (const k of ENV_KEYS) {
saved[k] = process.env[k];
delete process.env[k];
}
process.env["HEYGEN_CONFIG_DIR"] = dir;
envFixture = await setupTempAuthEnv("hf-login-");
dir = envFixture.dir;
verifyState.reject = false;
verifyState.user = { email: "alice@example.com" };
for (const fn of Object.values(telemetry)) fn.mockClear();
Expand All @@ -65,12 +59,7 @@ describe("auth login --api-key rollback", () => {

afterEach(async () => {
vi.restoreAllMocks();
for (const k of ENV_KEYS) {
const v = saved[k];
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
await fs.rm(dir, { recursive: true, force: true });
await envFixture.restore();
});

async function runLogin(apiKey: string): Promise<void> {
Expand Down
24 changes: 4 additions & 20 deletions packages/cli/src/commands/auth/status-user.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// fallow-ignore-file code-duplication
import { promises as fs } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { writeStore } from "../../auth/store.js";
import { setupTempAuthEnv, type EnvFixture } from "../../auth/_test-utils.js";
import { consumeCommandResult } from "../../utils/commandResult.js";

// Mock only AuthClient so the live /v3/users/me probe is controllable;
Expand Down Expand Up @@ -33,20 +30,12 @@ vi.mock("../../auth/index.js", async (orig) => {
return { ...actual, AuthClient: MockAuthClient };
});

const ENV_KEYS = ["HEYGEN_API_KEY", "HYPERFRAMES_API_KEY", "HEYGEN_CONFIG_DIR"] as const;

describe("auth status — persisted user block surface", () => {
let dir: string;
const saved: Partial<Record<(typeof ENV_KEYS)[number], string | undefined>> = {};
let envFixture: EnvFixture;
let stdout: string[];

beforeEach(async () => {
dir = await fs.mkdtemp(join(tmpdir(), "hf-status-"));
for (const k of ENV_KEYS) {
saved[k] = process.env[k];
delete process.env[k];
}
process.env["HEYGEN_CONFIG_DIR"] = dir;
envFixture = await setupTempAuthEnv("hf-status-");
probeState.apiReject = false;
probeState.user = { email: "live@example.com" };
stdout = [];
Expand All @@ -60,12 +49,7 @@ describe("auth status — persisted user block surface", () => {
afterEach(async () => {
consumeCommandResult();
vi.restoreAllMocks();
for (const k of ENV_KEYS) {
const v = saved[k];
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
await fs.rm(dir, { recursive: true, force: true });
await envFixture.restore();
});

async function runStatus(asJson: boolean): Promise<number> {
Expand Down
26 changes: 14 additions & 12 deletions packages/cli/src/commands/cloudrun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,18 @@ async function runSites(args: Record<string, unknown>): Promise<void> {

// ── render ──────────────────────────────────────────────────────────────────

function resolveCloudRunFps(
args: Record<string, unknown>,
projectDir: string,
command: "render" | "render-batch",
): 24 | 30 | 60 {
const fps =
parseIntFlag(args.fps) ?? readAllowedCompositionFpsFromDir(projectDir, [24, 30, 60]) ?? 30;
if (fps === 24 || fps === 30 || fps === 60) return fps;
console.error(`[cloudrun ${command}] --fps must be 24, 30, or 60; got ${fps}.`);
failCommand();
}

// fallow-ignore-next-line complexity
async function runRender(args: Record<string, unknown>): Promise<void> {
const projectDir = args.target as string | undefined;
Expand All @@ -543,12 +555,7 @@ async function runRender(args: Record<string, unknown>): Promise<void> {
console.error("[cloudrun render] --width and --height are required.");
failCommand();
}
const fps =
parseIntFlag(args.fps) ?? readAllowedCompositionFpsFromDir(projectDir, [24, 30, 60]) ?? 30;
if (fps !== 24 && fps !== 30 && fps !== 60) {
console.error(`[cloudrun render] --fps must be 24, 30, or 60; got ${fps}.`);
failCommand();
}
const fps = resolveCloudRunFps(args, projectDir, "render");
const state = readState(args);
const variables = resolveAndValidateVariables(args, resolve(projectDir));
const config = buildRenderConfig(args, fps, width, height, variables);
Expand Down Expand Up @@ -654,12 +661,7 @@ async function runRenderBatch(args: Record<string, unknown>): Promise<void> {
console.error("[cloudrun render-batch] --width and --height are required.");
failCommand();
}
const fps =
parseIntFlag(args.fps) ?? readAllowedCompositionFpsFromDir(projectDir, [24, 30, 60]) ?? 30;
if (fps !== 24 && fps !== 30 && fps !== 60) {
console.error(`[cloudrun render-batch] --fps must be 24, 30, or 60; got ${fps}.`);
failCommand();
}
const fps = resolveCloudRunFps(args, projectDir, "render-batch");
if (!existsSync(resolve(batchPath))) {
console.error(`[cloudrun render-batch] batch file not found: ${batchPath}`);
failCommand();
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createRenderPlan, type RenderFormat } from "./render/plan.js";
import { presentRenderPlan } from "./render/present.js";
import { executeRenderPlan } from "./render/execute.js";
export { resolveBrowserGpuForCli } from "./render/plan.js";
// Test-only seam retained at the command boundary for render behavior tests.
export { renderLintContinuationHint } from "./render/execute.js";

export const examples: Example[] = [
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/commands/render/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface RenderExecutionDependencies {
checkResolution: ResolutionPreflight;
}

// Exported only through render.ts so command tests can lock the user-facing guidance.
// fallow-ignore-next-line unused-export
export function renderLintContinuationHint(strictErrors: boolean): string {
return strictErrors
? " Continuing render despite lint warnings. Use --strict-all to block warnings."
Expand Down Expand Up @@ -154,6 +156,7 @@ async function ensureRenderBrowser(plan: RenderPlan): Promise<string> {
}
}

// fallow-ignore-next-line complexity
async function runRenderLint(plan: RenderPlan): Promise<void> {
// lintProject's explicit-entry contract is an absolute source path;
// entryFile remains project-relative for the producer.
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/render/plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,8 @@ export function renderOutputDirectory(plan: RenderPlan): string {
}

/** Resolve browser GPU mode from Docker, CLI, env, then the auto default. */
// Re-exported by render.ts to preserve its tested public seam.
// fallow-ignore-next-line unused-export
export function resolveBrowserGpuForCli(
useDocker: boolean,
browserGpuArg: boolean | undefined,
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/render/present.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { c } from "../../ui/colors.js";
import type { RenderPlan } from "./plan.js";

/** Present warnings and the human render plan. JSON batch output stays silent. */
// This phase intentionally owns every mutually exclusive human-output branch.
// fallow-ignore-next-line complexity
export async function presentRenderPlan(plan: RenderPlan): Promise<void> {
if (plan.invalidAuthoringSkill) {
process.stderr.write(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// fallow-ignore-file code-duplication
import { describe, expect, it, vi } from "vitest";
import {
RenderQualityError,
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/session.timings.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// fallow-ignore-file code-duplication
/**
* WS-C — getElementTimings / setElementTiming / setHold tests.
*
Expand Down
14 changes: 2 additions & 12 deletions packages/studio/src/components/StudioRightPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { BlockParamsPanel } from "./editor/BlockParamsPanel";
import { RenderQueue } from "./renders/RenderQueue";
import { SlideshowPanel } from "./panels/SlideshowPanel";
import type { SceneInfo } from "./panels/SlideshowPanel";
import { VariablesPanel } from "./panels/VariablesPanel";
import { VariablesPanel, type StudioEditPersistenceProps } from "./panels/VariablesPanel";
import { PanelTabButton } from "./PanelTabButton";
import { usePreviewVariablesStore } from "../hooks/previewVariablesStore";
import type { RenderJob } from "./renders/useRenderQueue";
Expand All @@ -20,7 +20,6 @@ import type { Composition } from "@hyperframes/sdk";
import type { EditHistoryKind } from "../utils/editHistory";
import { useSlideshowPersist, type UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
import { DesignPanelPromoteProvider } from "./DesignPanelPromoteProvider";

import { useStudioPlaybackContext, useStudioShellContext } from "../contexts/StudioContext";
import { usePanelLayoutContext } from "../contexts/PanelLayoutContext";
import { useFileManagerContext } from "../contexts/FileManagerContext";
Expand All @@ -36,7 +35,7 @@ import type { BackgroundRemovalProgress } from "./editor/propertyPanelTypes";
import { timelineKeysForSelections, type ToggleHiddenHandler } from "../utils/studioHelpers";
import { useInspectorSplitResize } from "../hooks/useInspectorSplitResize";

export interface StudioRightPanelProps {
export interface StudioRightPanelProps extends StudioEditPersistenceProps {
designPanelActive: boolean;
activeBlockParams?: {
blockName: string;
Expand Down Expand Up @@ -88,7 +87,6 @@ export function StudioRightPanel({
handlePanelResizeMove,
handlePanelResizeEnd,
} = usePanelLayoutContext();

const {
previewIframeRef,
projectId,
Expand All @@ -99,7 +97,6 @@ export function StudioRightPanel({
renderQueue,
} = useStudioShellContext();
const { captionEditMode, refreshKey } = useStudioPlaybackContext();

const {
domEditSelection,
domEditGroupSelections,
Expand Down Expand Up @@ -143,7 +140,6 @@ export function StudioRightPanel({
handleGsapRemoveKeyframe,
handleGsapConvertToKeyframes,
} = useDomEditContext();

const {
assets,
fontAssets,
Expand All @@ -155,7 +151,6 @@ export function StudioRightPanel({
writeProjectFile,
fileTree,
} = useFileManagerContext();

// Discrete ops (toggle, reorder, add/delete, hotspot): persist immediately,
// no coalescing — each is a distinct user action that deserves its own undo entry.
const onPersistSlideshow = useSlideshowPersist({
Expand All @@ -168,7 +163,6 @@ export function StudioRightPanel({
domEditSaveTimestampRef,
publishSdkSession,
});

// Notes path: persists are debounced in SlideshowPanel; coalesceKey ensures
// rapid writes collapse into a single undo entry via the save-queue infra.
const onPersistSlideshowNotes = useSlideshowPersist({
Expand All @@ -182,7 +176,6 @@ export function StudioRightPanel({
publishSdkSession,
coalesceKey: activeCompPath ? `slideshow-notes:${activeCompPath}` : "slideshow-notes",
});

const {
layersPanePercent,
splitContainerRef,
Expand All @@ -191,17 +184,14 @@ export function StudioRightPanel({
handleInspectorSplitResizeEnd,
} = useInspectorSplitResize();
const backgroundRemovalAbortRef = useRef<AbortController | null>(null);

useEffect(
() => () => {
backgroundRemovalAbortRef.current?.abort();
},
[],
);

const renderJobs = renderQueue.jobs as RenderJob[];
const inspectorTabActive = rightPanelTab === "design" || rightPanelTab === "layers";

// Derive scene list from the live clip manifest in the preview iframe.
// fallow-ignore-next-line complexity
const slideshowScenes = useMemo<SceneInfo[]>(() => {
Expand Down
4 changes: 3 additions & 1 deletion packages/studio/src/components/panels/VariablesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function shellSingleQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`;
}

interface VariablesPanelProps {
export interface StudioEditPersistenceProps {
sdkSession: Composition | null;
publishSdkSession: PublishSdkSession;
reloadPreview: () => void;
Expand All @@ -43,6 +43,8 @@ interface VariablesPanelProps {
}) => Promise<void>;
}

type VariablesPanelProps = StudioEditPersistenceProps;

function formatIssue(issue: VariableValidationIssue): string {
switch (issue.kind) {
case "undeclared":
Expand Down
1 change: 1 addition & 0 deletions packages/studio/src/contexts/FileManagerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function useFileManagerContextOptional(): FileManagerValue | null {

export function FileManagerProvider({
value: {
// fallow-ignore-next-line code-duplication
editingFile,
setEditingFile,
projectDir,
Expand Down
1 change: 1 addition & 0 deletions packages/studio/src/hooks/useGsapKeyframeOps.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function renderKeyframeOps(over: {
}) {
const captured: { api: HookApi | null } = { api: null };
function Probe() {
// fallow-ignore-next-line code-duplication
captured.api = useGsapKeyframeOps({
activeCompPath: "index.html",
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test doubles
Expand Down
2 changes: 2 additions & 0 deletions packages/studio/src/hooks/useGsapKeyframeOps.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// fallow-ignore-file code-duplication
// Add/remove operation-family transaction shapes stay parallel until SDK graduation.
import { useCallback } from "react";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import type { Composition } from "@hyperframes/sdk";
Expand Down
1 change: 1 addition & 0 deletions packages/studio/src/hooks/useGsapPropertyDebounce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export function useGsapPropertyDebounce(
const mutation = { type: "update-property", animationId, property, value };
const label = `Edit GSAP ${property}`;
try {
// fallow-ignore-next-line code-duplication
const { sdkSession, sdkDeps, activeCompPath } = sdkRef.current ?? {};
if (sdkSession && sdkDeps) {
const targetPath = selection.sourceFile || activeCompPath || "index.html";
Expand Down
1 change: 1 addition & 0 deletions packages/studio/src/hooks/useRazorSplit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type { RecordEditInput } from "./timelineEditingHelpers";

interface UseRazorSplitOptions {
projectId: string | null;
// fallow-ignore-next-line code-duplication
activeCompPath: string | null;
showToast: (message: string, tone?: "error" | "info") => void;
writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
Expand Down
1 change: 1 addition & 0 deletions packages/studio/src/hooks/useTimelineEditing.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// fallow-ignore-file code-duplication
// @vitest-environment happy-dom

import React, { act, useRef } from "react";
Expand Down
2 changes: 2 additions & 0 deletions packages/studio/src/hooks/useTimelineGroupEditing.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// fallow-ignore-file code-duplication
// Move/resize operation families remain parallel until SDK graduation.
import { useCallback, type MutableRefObject, type RefObject } from "react";
import type { Composition } from "@hyperframes/sdk";
import type { TimelineElement } from "../player";
Expand Down
1 change: 1 addition & 0 deletions packages/studio/src/utils/sdkCutover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
persistSdkCandidateMutation,
persistSdkSerialize,
} from "./sdkCutover";
// fallow-ignore-file code-duplication
import { openComposition } from "@hyperframes/sdk";
import { createMemoryAdapter } from "@hyperframes/sdk/adapters/memory";
import type { PatchOperation } from "./sourcePatcher";
Expand Down
3 changes: 3 additions & 0 deletions scripts/check-cli-process-ownership.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export function listDirectProcessTermination(source, filename = "source.ts") {
const sourceFile = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true);
const issues = [];

// AST traversal mirrors the small set of process-termination spellings.
// fallow-ignore-next-line complexity
function visit(node) {
if (
ts.isPropertyAccessExpression(node) &&
Expand All @@ -30,6 +32,7 @@ export function listDirectProcessTermination(source, filename = "source.ts") {
}

function listTypeScriptFiles(directory, rootCliPath) {
// fallow-ignore-next-line complexity
return readdirSync(directory).flatMap((name) => {
const path = join(directory, name);
if (statSync(path).isDirectory()) return listTypeScriptFiles(path, rootCliPath);
Expand Down
Loading