feat(cli): add agent-first media treatment tools

This commit is contained in:
ukimsanov
2026-07-24 18:42:07 -07:00
parent e710a1686f
commit 4582881d00
28 changed files with 3402 additions and 62 deletions
@@ -73,3 +73,56 @@ describe("media-use TTS documentation", () => {
expect(captions).toContain("heygen-tts.mjs");
});
});
describe("media treatment routing documentation", () => {
it("routes vague composition-media feedback to the canonical workflow", () => {
const router = read("skills", "hyperframes", "SKILL.md");
const mediaUse = read("skills", "media-use", "SKILL.md");
const treatments = read("skills", "media-use", "references", "media-treatments.md");
expect(router).toContain("dark/flat/boring footage");
expect(router).toContain("`/media-use`");
expect(mediaUse).toContain("references/media-treatments.md");
expect(mediaUse).toContain("`hyperframes media-treatment`");
expect(treatments).toContain("Persist pixel settings with `hyperframes media-treatment`");
expect(treatments).toContain("apply to the entire selected real `<img>` or");
expect(treatments).toContain("external segmentation/tracking tool");
});
it("keeps discovery progressive and verification visual", () => {
const treatments = read("skills", "media-use", "references", "media-treatments.md");
const recipes = read("skills", "media-use", "references", "media-treatment-recipes.md");
expect(treatments).toContain("hyperframes media-treatment --capabilities --json");
expect(treatments).toContain("--capability <id>");
expect(treatments).toContain("Recipes are optional macros");
expect(recipes).toContain("optional tested seeds");
expect(treatments).toContain("hyperframes add <name> --dir <project>");
expect(treatments).toContain("snapshots/treatment-before/contact-sheet.jpg");
expect(treatments).toMatch(/Do not report visual\s+quality from command success alone/);
});
it("indexes calibrated treatment recipes without making them mandatory", () => {
const treatments = read("skills", "media-use", "references", "media-treatments.md");
const recipes = read("skills", "media-use", "references", "media-treatment-recipes.md");
for (const heading of [
"Monochrome Screen Print",
"Engraved Illustration",
"Crosshatched Sketch",
"CRT Display",
]) {
expect(treatments).toContain(`\`${heading}\``);
expect(recipes).toContain(`## ${heading}`);
}
});
it("places the media-treatment discovery gate in new project instructions", () => {
for (const file of ["AGENTS.md", "CLAUDE.md"]) {
const template = read("packages", "cli", "src", "templates", "_shared", file);
expect(template).toContain("Changing how real footage or images look or reveal?");
expect(template).toContain("Load `/media-use`");
expect(template).toContain("do not improvise equivalent CSS/SVG filters or overlays");
}
});
});
@@ -0,0 +1,271 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runCommand } from "citty";
import { describe, expect, it, vi } from "vitest";
import {
HF_COLOR_GRADING_ACTIVE_EFFECT_KEYS,
getHfColorGradingCapabilities,
} from "@hyperframes/core";
import {
applyMediaTreatmentToHtml,
getMediaTreatmentCapabilityDetail,
getMediaTreatmentCapabilityOverview,
mediaTreatmentCommand,
} from "./media-treatment.js";
import { CliRuntimeError } from "../utils/commandResult.js";
const VIDEO = `<!doctype html><html><body><video id="hero" src="hero.mp4"></video></body></html>`;
describe("applyMediaTreatmentToHtml", () => {
it("provides a concise first-hop overview of the complete treatment surface", () => {
const overview = getMediaTreatmentCapabilityOverview();
expect(overview.families.find(({ id }) => id === "correction")).not.toHaveProperty("items");
expect(overview.families.find(({ id }) => id === "art")).not.toHaveProperty("items");
expect(overview.families.find(({ id }) => id === "overlays")).toMatchObject({
owner: "registry",
});
expect(overview.families.some(({ id }) => id === "looks" || id === "treatments")).toBe(false);
const discoveredEffects = ["essentials", "retro-glitch", "print", "art"].flatMap((family) => {
const detail = getMediaTreatmentCapabilityDetail(family);
return (
typeof detail === "object" &&
detail !== null &&
"effects" in detail &&
Array.isArray(detail.effects)
? detail.effects
: []
).map((effect) =>
typeof effect === "object" && effect && "id" in effect ? effect.id : null,
);
});
expect(discoveredEffects.sort()).toEqual([...HF_COLOR_GRADING_ACTIVE_EFFECT_KEYS].sort());
expect(JSON.stringify(overview).length).toBeLessThan(3_000);
});
it("returns focused controls and apply data for one capability", () => {
expect(getMediaTreatmentCapabilityDetail("kuwahara")).toMatchObject({
id: "kuwahara",
family: "art",
renderLane: "multipass",
apply: { effects: { kuwahara: 1 } },
animation: {
property: expect.objectContaining({ path: "effects.kuwahara" }),
initial: expect.stringContaining("--hf-color-grading-kuwahara"),
tween: expect.stringContaining("timeline.to"),
},
});
expect(getMediaTreatmentCapabilityDetail("retro-glitch")).toMatchObject({
id: "retro-glitch",
effects: expect.arrayContaining([expect.objectContaining({ id: "chromaBleed" })]),
});
expect(getMediaTreatmentCapabilityDetail("deep-sea")).toMatchObject({
id: "deep-sea",
apply: { palette: expect.arrayContaining(["#0a1628"]) },
});
expect(getMediaTreatmentCapabilityDetail("exposure")).toMatchObject({
id: "exposure",
family: "correction",
animation: {
property: expect.objectContaining({ path: "adjust.exposure" }),
},
});
expect(getMediaTreatmentCapabilityDetail("vignette")).toMatchObject({
id: "vignette",
family: "finishing",
control: expect.objectContaining({ key: "vignette" }),
});
});
it("rejects unknown capability lookups", () => {
expect(() => getMediaTreatmentCapabilityDetail("make-it-cinematic")).toThrow(
/Unknown media-treatment capability/,
);
expect(() => getMediaTreatmentCapabilityDetail("__proto__")).toThrow(
/Unknown media-treatment capability/,
);
});
it("exposes enough canonical metadata to assemble a custom treatment", () => {
const capabilities = getHfColorGradingCapabilities();
expect(capabilities.targetTags).toEqual(["img", "video"]);
expect(capabilities.effects.find(({ key }) => key === "kuwahara")?.apply).toMatchObject({
kuwahara: 1,
kuwaharaRadius: 1 / 7,
});
expect(capabilities.animatable.find(({ path }) => path === "effects.blur")?.name).toBe(
"--hf-color-grading-blur",
);
});
it("normalizes and persists a grading payload on real media", () => {
const result = applyMediaTreatmentToHtml(VIDEO, {
selector: "#hero",
grading: { preset: "warm-daylight", intensity: 0.8 },
});
expect(result.changed).toBe(true);
expect(result.tag).toBe("video");
expect(result.value).toContain('"preset":"warm-daylight"');
expect(result.value).toContain('"intensity":0.8');
expect(result.html).toContain("data-color-grading=");
});
it("merges a validated patch and reports the stored before and after payloads", () => {
const initial = applyMediaTreatmentToHtml(VIDEO, {
selector: "#hero",
grading: {
adjust: { exposure: 0.1 },
details: { grain: 0.2 },
},
});
const patched = applyMediaTreatmentToHtml(initial.html, {
selector: "#hero",
grading: { adjust: { shadows: 0.08 } },
});
expect(patched.before).toMatchObject({
adjust: { exposure: 0.1 },
details: { grain: 0.2 },
});
expect(patched.after).toMatchObject({
adjust: { exposure: 0.1, shadows: 0.08 },
details: { grain: 0.2 },
});
const repeated = applyMediaTreatmentToHtml(patched.html, {
selector: "#hero",
grading: { adjust: { shadows: 0.08 } },
});
expect(repeated.changed).toBe(false);
expect(repeated.html).toBe(patched.html);
expect(repeated.after).toEqual(repeated.before);
});
it("preserves unresolved variable references for runtime resolution", () => {
const wholeGrade = applyMediaTreatmentToHtml(VIDEO, {
selector: "#hero",
grading: "$interviewGrade",
});
expect(wholeGrade.value).toBe("$interviewGrade");
const nested = applyMediaTreatmentToHtml(VIDEO, {
selector: "#hero",
grading: { adjust: { exposure: "$interviewExposure" } },
});
expect(JSON.parse(nested.value ?? "{}")).toMatchObject({
adjust: { exposure: "$interviewExposure" },
});
const storedVariable = VIDEO.replace(" src=", ` data-color-grading="$interviewGrade" src=`);
expect(() =>
applyMediaTreatmentToHtml(storedVariable, {
selector: "#hero",
grading: { adjust: { exposure: 0.1 } },
}),
).toThrow(/Cannot merge.*unresolved whole-grade variable/);
});
it("requires an unambiguous media target", () => {
const source = `<img class="media" src="a.png"><img class="media" src="b.png">`;
expect(() =>
applyMediaTreatmentToHtml(source, { selector: ".media", grading: { preset: "neutral" } }),
).toThrow(/matched 2 elements/);
const result = applyMediaTreatmentToHtml(source, {
selector: ".media",
selectorIndex: 1,
grading: { preset: "warm-daylight" },
});
expect((result.html.match(/data-color-grading/g) ?? []).length).toBe(1);
});
it("persists grading inside composition templates", () => {
const source = `<template><video id="hero" src="hero.mp4"></video></template>`;
const result = applyMediaTreatmentToHtml(source, {
selector: "#hero",
grading: { preset: "warm-daylight" },
});
expect(result.changed).toBe(true);
expect(result.html).toContain("data-color-grading=");
});
it("rejects non-media elements", () => {
expect(() =>
applyMediaTreatmentToHtml(`<div id="hero"></div>`, {
selector: "#hero",
grading: { preset: "warm-daylight" },
}),
).toThrow(/requires an <img> or <video>/);
});
it("rejects unknown keys instead of silently dropping agent mistakes", () => {
expect(() =>
applyMediaTreatmentToHtml(VIDEO, {
selector: "#hero",
grading: { adjustments: { exposure: -0.45 }, effects: { dither: 1 } },
}),
).toThrow(/grading.*adjustments/i);
expect(() =>
applyMediaTreatmentToHtml(VIDEO, {
selector: "#hero",
grading: { effects: { dithering: 1 } },
}),
).toThrow(/effects.*dithering/i);
});
it("requires --apply for --grading while keeping --clear explicit", async () => {
const project = mkdtempSync(join(tmpdir(), "hf-media-treatment-"));
const file = join(project, "index.html");
const grading = '{"adjust":{"exposure":0.1}}';
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
const error = vi.spyOn(console, "error").mockImplementation(() => undefined);
writeFileSync(file, VIDEO);
try {
await expect(
runCommand(mediaTreatmentCommand, {
rawArgs: ["--project", project, "--selector", "#hero", "--grading", grading],
}),
).rejects.toThrow(CliRuntimeError);
expect(error).toHaveBeenLastCalledWith(expect.stringContaining("--grading requires --apply"));
expect(readFileSync(file, "utf8")).toBe(VIDEO);
await runCommand(mediaTreatmentCommand, {
rawArgs: ["--project", project, "--selector", "#hero", "--grading", grading, "--apply"],
});
expect(readFileSync(file, "utf8")).toContain("data-color-grading");
await runCommand(mediaTreatmentCommand, {
rawArgs: ["--project", project, "--selector", "#hero", "--clear"],
});
expect(readFileSync(file, "utf8")).not.toContain("data-color-grading");
} finally {
log.mockRestore();
error.mockRestore();
rmSync(project, { recursive: true, force: true });
}
});
it("clears both explicit and normalized no-op grading", () => {
const graded = VIDEO.replace(" src=", ` data-color-grading='{"preset":"warm-daylight"}' src=`);
expect(
applyMediaTreatmentToHtml(graded, { selector: "#hero", clear: true }).html,
).not.toContain("data-color-grading");
expect(
applyMediaTreatmentToHtml(graded, { selector: "#hero", grading: { preset: "neutral" } }).html,
).not.toContain("data-color-grading");
});
it("does not report or serialize a no-op clear because unrelated HTML formatting differs", () => {
const source = `<!doctype html><html><head><meta charset="utf-8" /></head><body><video id="hero" src="hero.mp4"></video></body></html>`;
const result = applyMediaTreatmentToHtml(source, { selector: "#hero", clear: true });
expect(result.changed).toBe(false);
expect(result.html).toBe(source);
});
});
@@ -0,0 +1,537 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { relative, resolve } from "node:path";
import {
HF_COLOR_GRADING_ATTR,
getHfColorGradingCapabilities,
isHfColorGradingActive,
isPathInside,
normalizeHfColorGrading,
serializeHfColorGrading,
} from "@hyperframes/core";
import {
isColorGradingVariableRef,
validateColorGradingContract,
} from "@hyperframes/parsers/color-grading-contract";
import { patchElementInHtml } from "@hyperframes/studio-server/source-mutation";
import { defineCommand } from "citty";
import { parseHTML } from "linkedom";
import type { Example } from "./_examples.js";
import { c } from "../ui/colors.js";
import { failCommand } from "../utils/commandResult.js";
import { normalizeErrorMessage } from "../utils/errorMessage.js";
import { readOptionalString } from "../utils/pathArgs.js";
import { resolveProject } from "../utils/project.js";
import { withMeta } from "../utils/updateCheck.js";
export function getMediaTreatmentCapabilityOverview() {
const capabilities = getHfColorGradingCapabilities();
const family = (id: string, label: string, description: string) => ({
id,
label,
description,
});
return {
version: capabilities.version,
targetTags: capabilities.targetTags,
colorSpace: capabilities.colorSpace,
families: [
family("correction", "Adjust", "Fix exposure, tonal balance, color casts, and saturation."),
family(
"presets",
"Presets",
"Apply a tested starting point, then tune only when the source or intent requires it.",
),
family("finishing", "Finish", "Shape vignette and deterministic film grain."),
...capabilities.effectFamilies,
family(
"palettes",
"Palettes",
"Reusable two-to-six-color palettes for compatible art effects.",
),
family(
"animation",
"Animation",
"Seek-safe CSS properties that registered GSAP timelines may animate.",
),
family("lut", "Custom LUT", "Apply a user-owned 3D .cube LUT."),
{
id: "overlays",
label: "Overlays",
description: "Install authored HUD, light-leak, flash, or freeze-frame overlay blocks.",
owner: "registry",
},
],
discovery: {
detail: "Use --capability <family-or-item> for exact controls and examples.",
full: "Use --all only for tooling or exhaustive inspection.",
},
};
}
export function getMediaTreatmentCapabilityDetail(id: string): unknown {
const capabilities = getHfColorGradingCapabilities();
const animation = (path: string) => {
const property = capabilities.animatable.find((candidate) => candidate.path === path);
if (!property) return null;
return {
property,
initial: `style="${property.name}: <start>"`,
tween: `timeline.to("<selector>", { "${property.name}": <end>, duration: <seconds> })`,
rules: [
"Author the initial value inline on the media element.",
"Use finite keyframes on a paused timeline registered in window.__timelines.",
"Do not use a frame-zero set, timers, random values, or onUpdate callbacks.",
],
};
};
const effect = capabilities.effects.find(({ key }) => key === id);
if (effect) {
return {
id,
family: effect.family,
label: effect.label,
description: effect.description,
renderLane: effect.renderLane,
supportsPalette: effect.supportsPalette,
apply: { effects: effect.apply },
controls: effect.controls,
animation: animation(`effects.${id}`),
};
}
const effectFamily = capabilities.effectFamilies.find((family) => family.id === id);
if (effectFamily) {
return {
...effectFamily,
effects: capabilities.effects
.filter((candidate) => candidate.family === id)
.map((candidate) => ({
id: candidate.key,
label: candidate.label,
description: candidate.description,
renderLane: candidate.renderLane,
supportsPalette: candidate.supportsPalette,
animatable: capabilities.animatable.some(
({ path }) => path === `effects.${candidate.key}`,
),
})),
};
}
const adjustment = capabilities.adjustments.find(({ key }) => key === id);
if (adjustment) {
return {
id,
family: "correction",
description: `Adjust ${id} within the canonical correction range.`,
control: adjustment,
apply: { adjust: { [id]: adjustment.identity } },
animation: animation(`adjust.${id}`),
};
}
const finishing = capabilities.finishing.find(({ key }) => key === id);
if (finishing) {
return {
id,
family: "finishing",
description: `Adjust ${id} within the canonical finishing range.`,
control: finishing,
apply: { details: { [id]: finishing.identity } },
};
}
const preset = capabilities.presets.find((candidate) => candidate.id === id);
if (preset) {
return {
id: preset.id,
label: preset.label,
description: "Tested built-in media preset.",
apply: { preset: preset.id, intensity: preset.intensity },
};
}
const palette = capabilities.palettes.find((candidate) => candidate.id === id);
if (palette) return { ...palette, apply: { palette: palette.colors } };
const details = {
correction: {
id,
description: "Fix exposure, tonal balance, color casts, and saturation.",
controls: capabilities.adjustments,
},
presets: {
id,
description: "Tested starting points for color and stylized media effects.",
presets: capabilities.presets,
},
finishing: {
id,
description: "Vignette and deterministic film-grain controls.",
controls: capabilities.finishing,
},
palettes: {
id,
description: "Named palettes plus the custom palette contract.",
contract: capabilities.palette,
palettes: capabilities.palettes,
},
animation: {
id,
description: "Seek-safe CSS properties for registered GSAP timelines.",
properties: capabilities.animatable,
},
lut: {
id,
description: "User-owned 3D .cube LUT support.",
contract: capabilities.lut,
},
overlays: {
id,
description: "Authored overlay blocks owned by the HyperFrames Registry.",
discover: "hyperframes catalog",
apply: "hyperframes add <overlay> --dir <project> --no-clipboard --json",
},
} as const;
const detail = Object.hasOwn(details, id) ? Reflect.get(details, id) : undefined;
if (detail) return detail;
throw new Error(`Unknown media-treatment capability: ${id}`);
}
export const examples: Example[] = [
[
"Discover the complete treatment surface without loading every control",
`hyperframes media-treatment --capabilities --json`,
],
[
"Inspect one relevant effect in detail",
`hyperframes media-treatment --capability kuwahara --json`,
],
[
"Inspect the exhaustive machine-readable catalog",
`hyperframes media-treatment --capabilities --all --json`,
],
[
"Apply a resolved treatment to one media element",
`hyperframes media-treatment --selector '#hero' --grading '{"preset":"skin-soft","intensity":0.6}' --apply`,
],
[
"Preview the exact mutation without writing",
`hyperframes media-treatment --file compositions/scene.html --selector 'video' --grading '{"preset":"warm-daylight"}' --apply --dry-run --json`,
],
["Remove a treatment", `hyperframes media-treatment --selector '#hero' --clear`],
];
interface ApplyMediaTreatmentOptions {
selector: string;
selectorIndex?: number;
grading?: unknown;
clear?: boolean;
}
interface ApplyMediaTreatmentResult {
html: string;
changed: boolean;
tag: "img" | "video";
value: string | null;
before: unknown;
after: unknown;
}
function parseSourceDocument(source: string): Document {
if (/<!doctype|<html[\s>]/i.test(source)) return parseHTML(source).document;
return parseHTML(`<!DOCTYPE html><html><body>${source}</body></html>`).document;
}
function assertKnownGradingShape(value: unknown): void {
const issue = validateColorGradingContract(value)[0];
if (issue) {
const hint = issue.hint ? ` ${issue.hint}` : "";
throw new Error(`Invalid color-grading ${issue.path}: ${issue.message}.${hint}`);
}
}
function containsColorGradingVariableRef(value: unknown): boolean {
if (isColorGradingVariableRef(value)) return true;
if (Array.isArray(value)) return value.some(containsColorGradingVariableRef);
if (typeof value !== "object" || value === null) return false;
return Object.values(value).some(containsColorGradingVariableRef);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function parseStoredGrading(raw: string | null): unknown {
if (raw === null) return null;
try {
return JSON.parse(raw);
} catch {
return raw;
}
}
function mergeGradingPatch(current: unknown, patch: unknown): unknown {
if (!isRecord(current) || !isRecord(patch)) return patch;
const merged = { ...current };
for (const [key, value] of Object.entries(patch)) {
merged[key] =
isRecord(value) && isRecord(merged[key]) ? mergeGradingPatch(merged[key], value) : value;
}
return merged;
}
function serializeGradingPatch(before: unknown, patch: unknown): string | null {
assertKnownGradingShape(patch);
if (isColorGradingVariableRef(before) && isRecord(patch)) {
throw new Error("Cannot merge a grading patch into an unresolved whole-grade variable");
}
const current =
typeof before === "string" && !isColorGradingVariableRef(before) ? { preset: before } : before;
const grading = mergeGradingPatch(current, patch);
assertKnownGradingShape(grading);
if (containsColorGradingVariableRef(grading)) {
return typeof grading === "string" ? grading.trim() : JSON.stringify(grading);
}
const normalized = normalizeHfColorGrading(grading);
if (!normalized) throw new Error("--grading must be valid HyperFrames color-grading JSON");
return isHfColorGradingActive(normalized) ? serializeHfColorGrading(normalized) : null;
}
function queryIncludingTemplates(root: Document | Element, selector: string): Element[] {
const matches = Array.from(root.querySelectorAll(selector));
if (matches.length > 0) return matches;
for (const template of root.querySelectorAll("template")) {
const nested = queryIncludingTemplates(template, selector);
if (nested.length > 0) return nested;
}
return [];
}
function selectMediaElement(
source: string,
selector: string,
selectorIndex?: number,
): { element: Element; selectorIndex: number; tag: "img" | "video" } {
const document = parseSourceDocument(source);
let matches: Element[];
try {
matches = queryIncludingTemplates(document, selector);
} catch {
throw new Error(`Invalid selector: ${selector}`);
}
if (matches.length === 0) throw new Error(`Selector did not match: ${selector}`);
if (selectorIndex === undefined && matches.length > 1) {
throw new Error(
`Selector matched ${matches.length} elements; use a unique selector or --selector-index`,
);
}
const resolvedIndex = selectorIndex ?? 0;
const element = matches[resolvedIndex];
if (!element) {
throw new Error(`--selector-index ${resolvedIndex} is outside ${matches.length} matches`);
}
const tag = element.tagName.toLowerCase();
if (tag !== "img" && tag !== "video") {
throw new Error(`Color grading requires an <img> or <video>; selector matched <${tag}>`);
}
return { element, selectorIndex: resolvedIndex, tag };
}
export function applyMediaTreatmentToHtml(
source: string,
options: ApplyMediaTreatmentOptions,
): ApplyMediaTreatmentResult {
const { element, selectorIndex, tag } = selectMediaElement(
source,
options.selector,
options.selectorIndex,
);
const before = parseStoredGrading(element.getAttribute(HF_COLOR_GRADING_ATTR));
const value = options.clear ? null : serializeGradingPatch(before, options.grading);
const changed = element.getAttribute(HF_COLOR_GRADING_ATTR) !== value;
const after = parseStoredGrading(value);
if (!changed) return { html: source, changed: false, tag, value, before, after };
const patched = patchElementInHtml(source, { selector: options.selector, selectorIndex }, [
{ type: "attribute", property: HF_COLOR_GRADING_ATTR, value },
]);
if (!patched.matched) throw new Error(`Could not persist selector: ${options.selector}`);
return { html: patched.html, changed: true, tag, value, before, after };
}
function parseSelectorIndex(raw: string | undefined): number | undefined {
if (raw === undefined) return undefined;
const value = Number(raw);
if (!Number.isInteger(value) || value < 0) {
throw new Error("--selector-index must be a non-negative integer");
}
return value;
}
function parseGrading(raw: string | undefined, apply: boolean, clear: boolean): unknown {
if (clear) {
if (raw !== undefined || apply) {
throw new Error("Use either --apply with --grading or --clear, not both");
}
return undefined;
}
if (!apply) {
if (raw !== undefined) throw new Error("--grading requires --apply");
throw new Error("Use --apply with --grading <json> or --clear");
}
if (raw === undefined) throw new Error("--apply requires --grading <json>");
try {
return JSON.parse(raw);
} catch (error) {
throw new Error(`Could not parse --grading JSON: ${normalizeErrorMessage(error)}`);
}
}
function mutationVerb(action: "apply" | "clear", changed: boolean, dryRun: boolean): string {
if (dryRun) return `Would ${action}`;
if (!changed) return action === "apply" ? "Already applied" : "Already clear";
return action === "apply" ? "Applied" : "Cleared";
}
export const mediaTreatmentCommand = defineCommand({
meta: {
name: "media-treatment",
description: "Discover, apply, or clear deterministic media treatments",
},
args: {
capabilities: {
type: "boolean",
description: "Print a concise agent-readable capability overview",
default: false,
},
capability: {
type: "string",
description: "Inspect one family, control, effect, preset, or palette",
},
all: {
type: "boolean",
description: "Print the exhaustive capability catalog",
default: false,
},
project: { type: "string", description: "Project directory (default: cwd)" },
file: {
type: "string",
description: "Composition file relative to project (default: index.html)",
},
selector: {
type: "string",
description: "Unique CSS selector for one <img> or <video>",
},
"selector-index": {
type: "string",
description: "Zero-based match index when the selector is not unique",
},
grading: { type: "string", description: "Canonical color-grading JSON patch" },
apply: {
type: "boolean",
description: "Apply the validated grading patch (explicit agent form)",
default: false,
},
clear: { type: "boolean", description: "Remove color grading from the target", default: false },
"dry-run": {
type: "boolean",
description: "Validate and report without writing",
default: false,
},
json: { type: "boolean", description: "Output an agent-friendly JSON result", default: false },
},
run({ args }) {
const runCapabilityQuery = () => {
const capability = readOptionalString(args.capability);
const hasMutationOption = [
readOptionalString(args.selector),
readOptionalString(args.grading),
args.clear,
args["dry-run"],
args.apply,
].some(Boolean);
if (hasMutationOption) {
throw new Error("--capabilities cannot be combined with mutation options");
}
if (args.all === true && capability) {
throw new Error("Use either --all or --capability, not both");
}
let capabilities: unknown = getMediaTreatmentCapabilityOverview();
if (args.all === true) capabilities = getHfColorGradingCapabilities();
else if (capability) capabilities = getMediaTreatmentCapabilityDetail(capability);
console.log(JSON.stringify(withMeta({ ok: true, capabilities }), null, 2));
};
const resolveMutationFile = () => {
const project = resolveProject(readOptionalString(args.project));
const fileArg = readOptionalString(args.file) ?? "index.html";
const filePath = resolve(project.dir, fileArg);
if (!isPathInside(filePath, project.dir) || !filePath.toLowerCase().endsWith(".html")) {
throw new Error("--file must be an HTML file inside the project");
}
if (!existsSync(filePath)) throw new Error(`Composition file not found: ${fileArg}`);
return { project, filePath };
};
const prepareMutation = () => {
const { project, filePath } = resolveMutationFile();
const selector = readOptionalString(args.selector);
if (!selector) throw new Error("--selector is required");
const clear = args.clear === true;
const apply = args.apply === true;
const selectorIndex = parseSelectorIndex(readOptionalString(args["selector-index"]));
const result = applyMediaTreatmentToHtml(readFileSync(filePath, "utf8"), {
selector,
selectorIndex,
grading: parseGrading(readOptionalString(args.grading), apply, clear),
clear,
});
const dryRun = args["dry-run"] === true;
if (result.changed && !dryRun) writeFileSync(filePath, result.html);
const action: "clear" | "apply" = result.value === null ? "clear" : "apply";
return {
action,
result,
selector,
payload: {
ok: true,
action,
file: relative(project.dir, filePath) || "index.html",
selector,
selectorIndex: selectorIndex ?? 0,
tag: result.tag,
changed: result.changed,
dryRun,
before: result.before,
after: result.after,
},
};
};
try {
if (args.capabilities === true || readOptionalString(args.capability) || args.all === true) {
return runCapabilityQuery();
}
const { action, result, selector, payload } = prepareMutation();
if (args.json === true) {
console.log(JSON.stringify(withMeta(payload), null, 2));
} else {
const verb = mutationVerb(action, result.changed, payload.dryRun);
console.log(`${c.success("◇")} ${verb} media treatment on ${c.accent(selector)}`);
}
} catch (error) {
const message = normalizeErrorMessage(error);
if (args.json === true) console.log(JSON.stringify(withMeta({ ok: false, error: message })));
else console.error(`${c.error("✗")} ${message}`);
failCommand();
}
},
});