@anvpx/motion-cli 0.0.0-stage → 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +121 -2
- package/dist/analyze/command.d.ts +2 -0
- package/dist/analyze/command.js +190 -0
- package/dist/approval/approval.d.ts +32 -0
- package/dist/approval/approval.js +141 -0
- package/dist/approval/command.d.ts +2 -0
- package/dist/approval/command.js +255 -0
- package/dist/approval/gate.d.ts +7 -0
- package/dist/approval/gate.js +23 -0
- package/dist/approval/hash.d.ts +71 -0
- package/dist/approval/hash.js +451 -0
- package/dist/args.d.ts +30 -0
- package/dist/args.js +53 -0
- package/dist/assets/command.d.ts +2 -0
- package/dist/assets/command.js +165 -0
- package/dist/captions/command.d.ts +2 -0
- package/dist/captions/command.js +341 -0
- package/dist/captions/cues.d.ts +10 -0
- package/dist/captions/cues.js +61 -0
- package/dist/catalog/command.d.ts +2 -0
- package/dist/catalog/command.js +60 -0
- package/dist/catalog/format.d.ts +13 -0
- package/dist/catalog/format.js +54 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +35 -0
- package/dist/commands.d.ts +3 -0
- package/dist/commands.js +59 -0
- package/dist/compare/command.d.ts +4 -0
- package/dist/compare/command.js +144 -0
- package/dist/contact/argv.d.ts +24 -0
- package/dist/contact/argv.js +42 -0
- package/dist/contact/command.d.ts +2 -0
- package/dist/contact/command.js +79 -0
- package/dist/doctor/checks.d.ts +25 -0
- package/dist/doctor/checks.js +571 -0
- package/dist/doctor/command.d.ts +4 -0
- package/dist/doctor/command.js +88 -0
- package/dist/doctor/format.d.ts +7 -0
- package/dist/doctor/format.js +41 -0
- package/dist/doctor/heal.d.ts +14 -0
- package/dist/doctor/heal.js +71 -0
- package/dist/doctor/pins.d.ts +8 -0
- package/dist/doctor/pins.js +28 -0
- package/dist/doctor/skills.d.ts +15 -0
- package/dist/doctor/skills.js +58 -0
- package/dist/doctor/types.d.ts +36 -0
- package/dist/doctor/types.js +1 -0
- package/dist/doctor/vendored.d.ts +15 -0
- package/dist/doctor/vendored.js +90 -0
- package/dist/doctor/version.d.ts +8 -0
- package/dist/doctor/version.js +11 -0
- package/dist/exit-codes.d.ts +16 -0
- package/dist/exit-codes.js +15 -0
- package/dist/fit/command.d.ts +5 -0
- package/dist/fit/command.js +154 -0
- package/dist/fit/solve.d.ts +21 -0
- package/dist/fit/solve.js +80 -0
- package/dist/fit/target.d.ts +18 -0
- package/dist/fit/target.js +47 -0
- package/dist/generate3d/command.d.ts +3 -0
- package/dist/generate3d/command.js +38 -0
- package/dist/generate3d/run.d.ts +39 -0
- package/dist/generate3d/run.js +77 -0
- package/dist/icon/command.d.ts +2 -0
- package/dist/icon/command.js +113 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +16 -0
- package/dist/install-mk.d.ts +31 -0
- package/dist/install-mk.js +168 -0
- package/dist/integration/preview.integration.d.ts +1 -0
- package/dist/integration/preview.integration.js +53 -0
- package/dist/integration/smoke.integration.d.ts +1 -0
- package/dist/integration/smoke.integration.js +91 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +4 -0
- package/dist/node-io.d.ts +14 -0
- package/dist/node-io.js +152 -0
- package/dist/optimize/command.d.ts +12 -0
- package/dist/optimize/command.js +110 -0
- package/dist/packs/command.d.ts +2 -0
- package/dist/packs/command.js +109 -0
- package/dist/patch/apply.d.ts +36 -0
- package/dist/patch/apply.js +186 -0
- package/dist/patch/command.d.ts +2 -0
- package/dist/patch/command.js +92 -0
- package/dist/plugin/install.d.ts +40 -0
- package/dist/plugin/install.js +170 -0
- package/dist/preview/command.d.ts +10 -0
- package/dist/preview/command.js +758 -0
- package/dist/preview/manifest.d.ts +94 -0
- package/dist/preview/manifest.js +19 -0
- package/dist/preview/pack.d.ts +24 -0
- package/dist/preview/pack.js +53 -0
- package/dist/preview/plan.d.ts +58 -0
- package/dist/preview/plan.js +160 -0
- package/dist/preview/provenance.d.ts +23 -0
- package/dist/preview/provenance.js +38 -0
- package/dist/publish/command.d.ts +2 -0
- package/dist/publish/command.js +70 -0
- package/dist/publish/player.d.ts +25 -0
- package/dist/publish/player.js +108 -0
- package/dist/publish/static.d.ts +16 -0
- package/dist/publish/static.js +78 -0
- package/dist/qc.d.ts +2 -0
- package/dist/qc.js +192 -0
- package/dist/scene/author.d.ts +16 -0
- package/dist/scene/author.js +260 -0
- package/dist/scene/command.d.ts +2 -0
- package/dist/scene/command.js +110 -0
- package/dist/scene/ops.d.ts +23 -0
- package/dist/scene/ops.js +151 -0
- package/dist/state/command.d.ts +2 -0
- package/dist/state/command.js +128 -0
- package/dist/state/evidence.d.ts +71 -0
- package/dist/state/evidence.js +243 -0
- package/dist/storyboard/check.d.ts +21 -0
- package/dist/storyboard/check.js +39 -0
- package/dist/storyboard/containment.d.ts +24 -0
- package/dist/storyboard/containment.js +43 -0
- package/dist/storyboard/custom.d.ts +15 -0
- package/dist/storyboard/custom.js +133 -0
- package/dist/storyboard/edit.d.ts +19 -0
- package/dist/storyboard/edit.js +39 -0
- package/dist/storyboard/load.d.ts +11 -0
- package/dist/storyboard/load.js +52 -0
- package/dist/storyboard/report.d.ts +5 -0
- package/dist/storyboard/report.js +8 -0
- package/dist/template/coerce.d.ts +6 -0
- package/dist/template/coerce.js +44 -0
- package/dist/template/command.d.ts +5 -0
- package/dist/template/command.js +163 -0
- package/dist/template/env.d.ts +34 -0
- package/dist/template/env.js +38 -0
- package/dist/template/fork.d.ts +8 -0
- package/dist/template/fork.js +156 -0
- package/dist/template/format.d.ts +21 -0
- package/dist/template/format.js +161 -0
- package/dist/template/list.d.ts +15 -0
- package/dist/template/list.js +116 -0
- package/dist/template/new.d.ts +6 -0
- package/dist/template/new.js +302 -0
- package/dist/testing/env.d.ts +8 -0
- package/dist/testing/env.js +19 -0
- package/dist/testing/fakes.d.ts +33 -0
- package/dist/testing/fakes.js +95 -0
- package/dist/testing/fixtures.d.ts +8 -0
- package/dist/testing/fixtures.js +6 -0
- package/dist/testing/probe.d.ts +15 -0
- package/dist/testing/probe.js +20 -0
- package/dist/testing/project.d.ts +22 -0
- package/dist/testing/project.js +26 -0
- package/dist/tooling/session.d.ts +40 -0
- package/dist/tooling/session.js +106 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.js +1 -0
- package/dist/verify/audio-loudness.d.ts +46 -0
- package/dist/verify/audio-loudness.js +186 -0
- package/dist/verify/command.d.ts +2 -0
- package/dist/verify/command.js +161 -0
- package/dist/verify/determinism.d.ts +30 -0
- package/dist/verify/determinism.js +59 -0
- package/dist/verify/expectation.d.ts +15 -0
- package/dist/verify/expectation.js +19 -0
- package/dist/verify/media-events.d.ts +61 -0
- package/dist/verify/media-events.js +174 -0
- package/dist/verify/platform.d.ts +33 -0
- package/dist/verify/platform.js +59 -0
- package/dist/verify/probe.d.ts +14 -0
- package/dist/verify/probe.js +23 -0
- package/dist/verify/report.d.ts +15 -0
- package/dist/verify/report.js +45 -0
- package/dist/verify/run.d.ts +27 -0
- package/dist/verify/run.js +53 -0
- package/dist/voice/command.d.ts +3 -0
- package/dist/voice/command.js +49 -0
- package/dist/voice/manifest.d.ts +33 -0
- package/dist/voice/manifest.js +69 -0
- package/dist/voice/run.d.ts +21 -0
- package/dist/voice/run.js +181 -0
- package/dist/workflow/build.d.ts +2 -0
- package/dist/workflow/build.js +25 -0
- package/dist/workflow/draft.d.ts +9 -0
- package/dist/workflow/draft.js +201 -0
- package/dist/workflow/finish.d.ts +23 -0
- package/dist/workflow/finish.js +28 -0
- package/dist/workflow/project.d.ts +73 -0
- package/dist/workflow/project.js +198 -0
- package/dist/workflow/provenance.d.ts +18 -0
- package/dist/workflow/provenance.js +36 -0
- package/dist/workflow/render.d.ts +31 -0
- package/dist/workflow/render.js +705 -0
- package/dist/workflow/still.d.ts +5 -0
- package/dist/workflow/still.js +62 -0
- package/dist/workflow/validate.d.ts +2 -0
- package/dist/workflow/validate.js +49 -0
- package/package.json +38 -3
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { entriesOf, filterEntries, loadTemplate, rankEntries, } from "@anvpx/motion-build";
|
|
2
|
+
import { EXIT } from "../exit-codes.js";
|
|
3
|
+
import { discover, loadOptions } from "./env.js";
|
|
4
|
+
import { clip, lengthLabel } from "./format.js";
|
|
5
|
+
export const LIST_MAX_LINES = 15;
|
|
6
|
+
/** Ranked or filtered blueprints shown (one header line on top: 9 lines in all). */
|
|
7
|
+
export const BLUEPRINT_ROWS = 8;
|
|
8
|
+
const SUMMARY_MAX = 70;
|
|
9
|
+
const GOOD_FOR_MAX = 70;
|
|
10
|
+
const TOP_TAGS = 4;
|
|
11
|
+
/** `--for`, `--tag`, `--aspect` or `--sec` switch the listing from templates to blueprints. */
|
|
12
|
+
const wantsBlueprints = (request) => request.query !== undefined ||
|
|
13
|
+
(request.filters.tags?.length ?? 0) > 0 ||
|
|
14
|
+
request.filters.aspect !== undefined ||
|
|
15
|
+
request.filters.sec !== undefined;
|
|
16
|
+
const capLines = (lines) => lines.length <= LIST_MAX_LINES
|
|
17
|
+
? lines
|
|
18
|
+
: [...lines.slice(0, LIST_MAX_LINES - 1), `(+${lines.length - LIST_MAX_LINES + 1} more)`];
|
|
19
|
+
const loadAll = (env, io) => {
|
|
20
|
+
const found = discover(env, io);
|
|
21
|
+
const loaded = found.templates.flatMap((candidate) => {
|
|
22
|
+
const result = loadTemplate(candidate.dir, loadOptions(env));
|
|
23
|
+
return result.ok ? [{ template: result.value, source: candidate.source }] : [];
|
|
24
|
+
});
|
|
25
|
+
return { found, loaded };
|
|
26
|
+
};
|
|
27
|
+
const templateRow = (template, source) => {
|
|
28
|
+
const { manifest } = template;
|
|
29
|
+
const tags = manifest.discover?.tags.slice(0, TOP_TAGS) ?? [];
|
|
30
|
+
return {
|
|
31
|
+
id: `${manifest.id}@${manifest.version}`,
|
|
32
|
+
summary: manifest.summary,
|
|
33
|
+
domains: manifest.domains,
|
|
34
|
+
tags,
|
|
35
|
+
aspects: manifest.discover?.aspects ?? [],
|
|
36
|
+
blueprints: manifest.blueprints.map((b) => b.id),
|
|
37
|
+
source,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const dash = (items) => (items.length === 0 ? "-" : items.join(","));
|
|
41
|
+
function listTemplates(request, env, io) {
|
|
42
|
+
const { found, loaded } = loadAll(env, io);
|
|
43
|
+
const wanted = request.domain?.toLowerCase();
|
|
44
|
+
const rows = loaded
|
|
45
|
+
.filter(({ template }) => wanted === undefined || template.manifest.domains.some((d) => d.toLowerCase() === wanted))
|
|
46
|
+
.map(({ template, source }) => templateRow(template, source));
|
|
47
|
+
const shadowed = found.shadowed.map((s) => `! ${s.id}@${s.version} (${s.source}) shadowed by ${s.reason === "root" ? "higher-precedence root" : "newer version"}: ${s.by}`);
|
|
48
|
+
const problems = found.problems.map((p) => `! ${p.path}: ${p.message}`);
|
|
49
|
+
if (request.json) {
|
|
50
|
+
io.stdout(`${JSON.stringify({ templates: rows, shadowed, problems })}\n`);
|
|
51
|
+
return EXIT.OK;
|
|
52
|
+
}
|
|
53
|
+
const lines = [
|
|
54
|
+
...rows.map((r) => `${r.id} | ${clip(r.summary, SUMMARY_MAX)} | ${dash(r.tags)} | ${dash(r.aspects)} | ${r.blueprints.join(",")} | ${r.source}`),
|
|
55
|
+
...shadowed,
|
|
56
|
+
...problems,
|
|
57
|
+
];
|
|
58
|
+
if (lines.length === 0) {
|
|
59
|
+
lines.push(wanted === undefined ? "no templates found" : `no template for domain "${request.domain}"`);
|
|
60
|
+
}
|
|
61
|
+
io.stdout(`${capLines(lines).join("\n")}\n`);
|
|
62
|
+
return EXIT.OK;
|
|
63
|
+
}
|
|
64
|
+
const idOf = (entry) => `${entry.template}/${entry.blueprint}`;
|
|
65
|
+
const pickText = (entry) => clip(entry.goodFor === "" ? entry.purpose : entry.goodFor, GOOD_FOR_MAX);
|
|
66
|
+
const criteria = (request) => {
|
|
67
|
+
const { tags = [], aspect, sec } = request.filters;
|
|
68
|
+
return [
|
|
69
|
+
request.query === undefined ? "" : `for "${request.query}"`,
|
|
70
|
+
tags.length === 0 ? "" : `tag ${tags.join(",")}`,
|
|
71
|
+
aspect === undefined ? "" : `aspect ${aspect}`,
|
|
72
|
+
sec === undefined ? "" : `sec ${sec}`,
|
|
73
|
+
]
|
|
74
|
+
.filter((part) => part !== "")
|
|
75
|
+
.join(" ");
|
|
76
|
+
};
|
|
77
|
+
const rowOf = (entry, hit) => {
|
|
78
|
+
const tail = `${pickText(entry)} | ${lengthLabel(entry.lengths)} | ${dash(entry.aspects)}`;
|
|
79
|
+
if (hit === undefined)
|
|
80
|
+
return `${idOf(entry)} | ${tail}`;
|
|
81
|
+
const mark = hit.notFor ? `${hit.score} NOT-FOR` : String(hit.score);
|
|
82
|
+
return `${idOf(entry)} | ${mark} | ${tail} | why: ${hit.why.join(",")}`;
|
|
83
|
+
};
|
|
84
|
+
function listBlueprints(request, env, io) {
|
|
85
|
+
const { loaded } = loadAll(env, io);
|
|
86
|
+
const filtered = filterEntries(entriesOf(loaded.map((item) => item.template)), request.filters);
|
|
87
|
+
const hits = request.query === undefined ? undefined : rankEntries(filtered, request.query, env.stopwords);
|
|
88
|
+
const shown = hits === undefined
|
|
89
|
+
? filtered.map((entry) => ({ entry, hit: undefined }))
|
|
90
|
+
: hits.map((hit) => ({ entry: hit.entry, hit }));
|
|
91
|
+
const top = shown.slice(0, BLUEPRINT_ROWS);
|
|
92
|
+
if (request.json) {
|
|
93
|
+
const blueprints = top.map(({ entry, hit }) => ({
|
|
94
|
+
id: idOf(entry),
|
|
95
|
+
goodFor: entry.goodFor,
|
|
96
|
+
aspects: entry.aspects,
|
|
97
|
+
lengths: entry.lengths ?? null,
|
|
98
|
+
...(hit === undefined ? {} : { score: hit.score, notFor: hit.notFor, why: hit.why }),
|
|
99
|
+
}));
|
|
100
|
+
io.stdout(`${JSON.stringify({ total: shown.length, blueprints })}\n`);
|
|
101
|
+
return EXIT.OK;
|
|
102
|
+
}
|
|
103
|
+
const count = shown.length > top.length ? `top ${top.length} of ${shown.length}` : `${shown.length}`;
|
|
104
|
+
const header = `blueprints ${criteria(request)}: ${count}`;
|
|
105
|
+
const lines = top.length === 0
|
|
106
|
+
? [header, "no blueprint matches; relax a filter or reword --for"]
|
|
107
|
+
: [header, ...top.map(({ entry, hit }) => rowOf(entry, hit))];
|
|
108
|
+
io.stdout(`${lines.join("\n")}\n`);
|
|
109
|
+
return EXIT.OK;
|
|
110
|
+
}
|
|
111
|
+
/** `mk template list`: templates, or (with `--for/--tag/--aspect/--sec`) ranked or filtered blueprints. */
|
|
112
|
+
export async function list(request, env, io) {
|
|
113
|
+
return wantsBlueprints(request)
|
|
114
|
+
? listBlueprints(request, env, io)
|
|
115
|
+
: listTemplates(request, env, io);
|
|
116
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Command } from "../types.ts";
|
|
2
|
+
import { type TemplateEnv } from "./env.ts";
|
|
3
|
+
export declare const TEMPLATE_PROVENANCE = "template.json";
|
|
4
|
+
/** `mk new`; `env` is injectable for tests. */
|
|
5
|
+
export declare const makeNewCommand: (env?: () => TemplateEnv) => Command;
|
|
6
|
+
export declare const newCommand: Command;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { instantiate, resolveTimeline, sceneDocs, validateStoryboardFull, } from "@anvpx/motion-build";
|
|
4
|
+
import { canonicalJson, LENGTH_CLASSES, } from "@anvpx/motion-schema";
|
|
5
|
+
import { MK_DIR, mkDir, storyboardStem } from "../approval/hash.js";
|
|
6
|
+
import { listFlag, parseFlags, textFlag, usageError } from "../args.js";
|
|
7
|
+
import { EXIT } from "../exit-codes.js";
|
|
8
|
+
import { readJsonFile } from "../storyboard/load.js";
|
|
9
|
+
import { formatFindings } from "../storyboard/report.js";
|
|
10
|
+
import { coerceSets } from "./coerce.js";
|
|
11
|
+
import { defaultEnv, findTemplate, formatErrors, platformsFor } from "./env.js";
|
|
12
|
+
export const TEMPLATE_PROVENANCE = "template.json";
|
|
13
|
+
/** Bare `mk new --demo` target: the most familiar blueprint. */
|
|
14
|
+
const DEFAULT_DEMO = "explain/architecture";
|
|
15
|
+
const USAGE = `mk new [<template>/<blueprint>] [--demo] [--with scene[,scene] ...] [--set k=v ...] [--slots file.json] [--style s] [--length l] [--out storyboard.json] [--json]
|
|
16
|
+
|
|
17
|
+
Instantiate a blueprint into a plain, validated storyboard. Writes the storyboard, vendors the theme(s) it uses
|
|
18
|
+
into <out dir>/themes/ (the project builds without the template installed) and records provenance in <out dir>/.mk/<name>/template.json.
|
|
19
|
+
--demo start from the blueprint's own example; --slots/--set still win. Without a target, demo uses ${DEFAULT_DEMO}.
|
|
20
|
+
--with t[,t] append one base scene per type after the blueprint; props come from the catalog example
|
|
21
|
+
merged with slots["$with"]["<type>"] (same --slots file), then the whole storyboard validates.
|
|
22
|
+
--set k=v one slot; lists as k=a|b|c, backslash-pipe for a literal pipe (repeatable; wins over --slots)
|
|
23
|
+
--slots f JSON object of slots (needed for object slots); may carry a "$with" object for --with props
|
|
24
|
+
--style s style id (default: the blueprint's default)
|
|
25
|
+
--length l short|standard|long (default: the blueprint's default)
|
|
26
|
+
--out f storyboard path (default storyboard.json)
|
|
27
|
+
See \`mk template show <template>/<blueprint>\` for slots and limits.
|
|
28
|
+
`;
|
|
29
|
+
const FLAGS = {
|
|
30
|
+
demo: { type: "boolean" },
|
|
31
|
+
with: { type: "string", multiple: true },
|
|
32
|
+
set: { type: "string", multiple: true },
|
|
33
|
+
slots: { type: "string" },
|
|
34
|
+
style: { type: "string" },
|
|
35
|
+
length: { type: "string" },
|
|
36
|
+
out: { type: "string" },
|
|
37
|
+
json: { type: "boolean" },
|
|
38
|
+
};
|
|
39
|
+
/** Reserved slots key holding per-scene props for `--with`; stripped before slot resolution. */
|
|
40
|
+
const WITH_KEY = "$with";
|
|
41
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
|
+
const MAX_WARNING_LINES = 5;
|
|
43
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
44
|
+
async function slotsFile(path, io) {
|
|
45
|
+
if (path === undefined)
|
|
46
|
+
return { slots: {}, withProps: {} };
|
|
47
|
+
const loaded = await readJsonFile(path, io.host);
|
|
48
|
+
if (!loaded.ok) {
|
|
49
|
+
io.stderr(`mk new: ${loaded.message}\n`);
|
|
50
|
+
return EXIT.USAGE;
|
|
51
|
+
}
|
|
52
|
+
const value = loaded.value;
|
|
53
|
+
if (!isRecord(value)) {
|
|
54
|
+
io.stderr(`mk new: ${path} must hold a JSON object of slots\n`);
|
|
55
|
+
return EXIT.USAGE;
|
|
56
|
+
}
|
|
57
|
+
const { [WITH_KEY]: withRaw, ...slots } = value;
|
|
58
|
+
if (withRaw !== undefined && !isRecord(withRaw)) {
|
|
59
|
+
io.stderr(`mk new: ${path} "${WITH_KEY}" must be an object keyed by scene type\n`);
|
|
60
|
+
return EXIT.USAGE;
|
|
61
|
+
}
|
|
62
|
+
return { slots, withProps: isRecord(withRaw) ? withRaw : {} };
|
|
63
|
+
}
|
|
64
|
+
/** Resolves `<template>/<blueprint>`; a miss prints the known ids and yields an exit code. */
|
|
65
|
+
function resolveTarget(target, env, io) {
|
|
66
|
+
const [templateId = "", blueprintId = "", ...extra] = target.split("/");
|
|
67
|
+
if (blueprintId === "" || extra.length > 0) {
|
|
68
|
+
return usageError("new", USAGE, "target must be <template>/<blueprint>", io).code;
|
|
69
|
+
}
|
|
70
|
+
const found = findTemplate(env, io, templateId);
|
|
71
|
+
if (!found.ok) {
|
|
72
|
+
io.stderr(`mk new: ${found.message}
|
|
73
|
+
`);
|
|
74
|
+
return EXIT.USAGE;
|
|
75
|
+
}
|
|
76
|
+
const blueprint = found.template.blueprints.get(blueprintId);
|
|
77
|
+
if (blueprint === undefined) {
|
|
78
|
+
const known = [...found.template.blueprints.keys()].join("|");
|
|
79
|
+
io.stderr(`mk new: unknown blueprint "${blueprintId}"; use one of: ${known}
|
|
80
|
+
`);
|
|
81
|
+
return EXIT.USAGE;
|
|
82
|
+
}
|
|
83
|
+
return { template: found.template, blueprintId, blueprint };
|
|
84
|
+
}
|
|
85
|
+
/** `--demo`: the blueprint's own example values, or an exit code when it has none (never invented). */
|
|
86
|
+
function demoSlots(target, io) {
|
|
87
|
+
if (Object.keys(target.blueprint.example).length > 0)
|
|
88
|
+
return target.blueprint.example;
|
|
89
|
+
io.stderr(`mk new: ${target.template.manifest.id}/${target.blueprintId} has no example slots; pass --slots or --set
|
|
90
|
+
`);
|
|
91
|
+
return EXIT.USAGE;
|
|
92
|
+
}
|
|
93
|
+
/** `--demo` example first, then `--slots`, then `--set` (which wins); an exit code on bad input. */
|
|
94
|
+
async function gatherSlots(flags, target, io) {
|
|
95
|
+
const base = flags.demo === true ? demoSlots(target, io) : {};
|
|
96
|
+
if (typeof base === "number")
|
|
97
|
+
return base;
|
|
98
|
+
const fromFile = await slotsFile(textFlag(flags, "slots"), io);
|
|
99
|
+
if (typeof fromFile === "number")
|
|
100
|
+
return fromFile;
|
|
101
|
+
const sets = coerceSets(target.blueprint.slots, listFlag(flags, "set"));
|
|
102
|
+
if (sets.ok) {
|
|
103
|
+
return {
|
|
104
|
+
slots: { ...base, ...fromFile.slots, ...sets.value },
|
|
105
|
+
withProps: fromFile.withProps,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
io.stderr(`mk new: ${formatErrors(sets.errors)}
|
|
109
|
+
`);
|
|
110
|
+
return EXIT.USAGE;
|
|
111
|
+
}
|
|
112
|
+
/** Writes the storyboard, the vendored theme(s) and the provenance record; returns the storyboard path. */
|
|
113
|
+
async function writeProject(made, outFlag, io) {
|
|
114
|
+
const out = resolve(outFlag ?? "storyboard.json");
|
|
115
|
+
const dir = dirname(out);
|
|
116
|
+
const text = `${made.text}
|
|
117
|
+
`;
|
|
118
|
+
await io.host.writeText(out, text);
|
|
119
|
+
for (const theme of made.themes)
|
|
120
|
+
await io.host.writeText(join(dir, theme.file), theme.text);
|
|
121
|
+
const record = { ...made.provenance, storyboardSha256: sha256(text) };
|
|
122
|
+
await io.host.writeText(join(mkDir(out), TEMPLATE_PROVENANCE), `${JSON.stringify(record, null, 2)}
|
|
123
|
+
`);
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
function report(made, out, json, io) {
|
|
127
|
+
const { meta } = made.storyboard;
|
|
128
|
+
const overlays = made.storyboard.scenes.filter((s) => s.layer === "overlay").length;
|
|
129
|
+
const scenes = made.sceneCount - overlays;
|
|
130
|
+
const frame = { width: meta.width, height: meta.height, fps: meta.fps };
|
|
131
|
+
const files = made.themes.map((t) => t.file);
|
|
132
|
+
if (json) {
|
|
133
|
+
const { provenance, totalSec } = made;
|
|
134
|
+
const doc = {
|
|
135
|
+
ok: true,
|
|
136
|
+
storyboard: out,
|
|
137
|
+
...provenance,
|
|
138
|
+
scenes,
|
|
139
|
+
overlays,
|
|
140
|
+
totalSec,
|
|
141
|
+
frame,
|
|
142
|
+
themes: files,
|
|
143
|
+
warnings: made.warnings.length,
|
|
144
|
+
};
|
|
145
|
+
io.stdout(`${JSON.stringify(doc)}
|
|
146
|
+
`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const { provenance } = made;
|
|
150
|
+
const label = `${provenance.template.split("@")[0]}/${provenance.blueprint}`;
|
|
151
|
+
io.stdout(`new ok | ${label} style ${provenance.style} length ${provenance.length} | ${scenes} scenes + ${overlays} overlays ${made.totalSec}s ${frame.width}x${frame.height}@${frame.fps} | ${made.warnings.length} warnings
|
|
152
|
+
` +
|
|
153
|
+
`wrote ${out} (+ ${files.join(", ")}, ${MK_DIR}/${storyboardStem(out)}/${TEMPLATE_PROVENANCE})
|
|
154
|
+
` +
|
|
155
|
+
`next: mk validate ${out} && mk preview ${out}
|
|
156
|
+
`);
|
|
157
|
+
}
|
|
158
|
+
const lengthOf = (flags, io) => {
|
|
159
|
+
const length = textFlag(flags, "length");
|
|
160
|
+
if (length === undefined)
|
|
161
|
+
return undefined;
|
|
162
|
+
if (LENGTH_CLASSES.includes(length))
|
|
163
|
+
return length;
|
|
164
|
+
return usageError("new", USAGE, `--length must be ${LENGTH_CLASSES.join("|")}`, io).code;
|
|
165
|
+
};
|
|
166
|
+
const appendScenes = (made, types, withProps) => {
|
|
167
|
+
if (types.length === 0)
|
|
168
|
+
return { ok: true, value: made };
|
|
169
|
+
const docs = new Map(sceneDocs().map((doc) => [doc.type, doc]));
|
|
170
|
+
const ids = new Set(made.storyboard.scenes.map((scene) => scene.id));
|
|
171
|
+
const appended = [];
|
|
172
|
+
for (const type of types) {
|
|
173
|
+
const doc = docs.get(type);
|
|
174
|
+
if (doc === undefined) {
|
|
175
|
+
return { ok: false, message: `unknown scene type "${type}"; run mk catalog` };
|
|
176
|
+
}
|
|
177
|
+
if (doc.layer === "overlay") {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
message: `"${type}" is an overlay; --with appends base scenes (use mk scene add for overlays)`,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const override = withProps[type];
|
|
184
|
+
if (override !== undefined && !isRecord(override)) {
|
|
185
|
+
return { ok: false, message: `"${WITH_KEY}.${type}" must be an object of props` };
|
|
186
|
+
}
|
|
187
|
+
const provided = isRecord(override) ? override : {};
|
|
188
|
+
const missing = doc.fields
|
|
189
|
+
.filter((field) => field.required && provided[field.name] === undefined)
|
|
190
|
+
.map((field) => field.name);
|
|
191
|
+
if (missing.length > 0) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
message: `${type} needs ${missing.join(", ")}; add them under "${WITH_KEY}"."${type}" in --slots`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// The slot object is the complete props value: no example defaults leak in
|
|
198
|
+
// (the kinetic example pins emphasis to a word index that the new words may not have).
|
|
199
|
+
const props = provided;
|
|
200
|
+
let id = type;
|
|
201
|
+
let suffix = 2;
|
|
202
|
+
while (ids.has(id)) {
|
|
203
|
+
id = `${type}-${suffix}`;
|
|
204
|
+
suffix += 1;
|
|
205
|
+
}
|
|
206
|
+
ids.add(id);
|
|
207
|
+
appended.push({ id, type, props });
|
|
208
|
+
}
|
|
209
|
+
const storyboard = {
|
|
210
|
+
...made.storyboard,
|
|
211
|
+
scenes: [...made.storyboard.scenes, ...appended],
|
|
212
|
+
};
|
|
213
|
+
const valid = validateStoryboardFull(storyboard);
|
|
214
|
+
if (!valid.ok)
|
|
215
|
+
return { ok: false, message: formatErrors(valid.errors) };
|
|
216
|
+
const timeline = resolveTimeline(valid.value);
|
|
217
|
+
if (!timeline.ok)
|
|
218
|
+
return { ok: false, message: formatErrors(timeline.errors) };
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
value: {
|
|
222
|
+
...made,
|
|
223
|
+
storyboard: valid.value,
|
|
224
|
+
text: canonicalJson(valid.value),
|
|
225
|
+
sceneCount: valid.value.scenes.length,
|
|
226
|
+
totalSec: timeline.value.total,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
/** `--with t[,t]` entries, trimmed and de-duplicated by scene type. */
|
|
231
|
+
const withTypesOf = (flags) => [
|
|
232
|
+
...new Set(listFlag(flags, "with")
|
|
233
|
+
.flatMap((value) => value.split(","))
|
|
234
|
+
.map((value) => value.trim())
|
|
235
|
+
.filter((value) => value !== "")),
|
|
236
|
+
];
|
|
237
|
+
/** Everything `instantiate` needs, from flags and the template; an exit code on bad input. */
|
|
238
|
+
async function plan(args, env, io) {
|
|
239
|
+
const parsed = parseFlags("new", USAGE, args, FLAGS, io);
|
|
240
|
+
if (parsed.kind === "exit")
|
|
241
|
+
return parsed.code;
|
|
242
|
+
const [positional, ...extra] = parsed.positionals;
|
|
243
|
+
if (extra.length > 0) {
|
|
244
|
+
return usageError("new", USAGE, `unexpected argument "${extra[0]}"`, io).code;
|
|
245
|
+
}
|
|
246
|
+
const demo = parsed.flags.demo === true;
|
|
247
|
+
if (positional === undefined && !demo) {
|
|
248
|
+
return usageError("new", USAGE, "missing <template>/<blueprint> (or pass --demo for the default demo)", io).code;
|
|
249
|
+
}
|
|
250
|
+
const name = positional ?? DEFAULT_DEMO;
|
|
251
|
+
const length = lengthOf(parsed.flags, io);
|
|
252
|
+
const target = typeof length === "number" ? length : resolveTarget(name, env, io);
|
|
253
|
+
if (typeof target === "number")
|
|
254
|
+
return target;
|
|
255
|
+
const gathered = await gatherSlots(parsed.flags, target, io);
|
|
256
|
+
if (typeof gathered === "number")
|
|
257
|
+
return gathered;
|
|
258
|
+
const style = textFlag(parsed.flags, "style");
|
|
259
|
+
const options = {
|
|
260
|
+
...(style === undefined ? {} : { style }),
|
|
261
|
+
...(length === undefined ? {} : { length: length }),
|
|
262
|
+
platforms: await platformsFor(io),
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
target,
|
|
266
|
+
slots: gathered.slots,
|
|
267
|
+
withTypes: withTypesOf(parsed.flags),
|
|
268
|
+
withProps: gathered.withProps,
|
|
269
|
+
options,
|
|
270
|
+
out: textFlag(parsed.flags, "out"),
|
|
271
|
+
json: parsed.flags.json === true,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** `mk new`; `env` is injectable for tests. */
|
|
275
|
+
export const makeNewCommand = (env = defaultEnv) => ({
|
|
276
|
+
name: "new",
|
|
277
|
+
summary: "instantiate a template blueprint into a validated storyboard",
|
|
278
|
+
async run(args, io) {
|
|
279
|
+
const planned = await plan(args, env(), io);
|
|
280
|
+
if (typeof planned === "number")
|
|
281
|
+
return planned;
|
|
282
|
+
const { target, slots, options } = planned;
|
|
283
|
+
const result = instantiate(target.template, target.blueprintId, slots, options);
|
|
284
|
+
if (!result.ok) {
|
|
285
|
+
const label = `${target.template.manifest.id}/${target.blueprintId}`;
|
|
286
|
+
io.stderr(`mk new: ${formatErrors(result.errors)}
|
|
287
|
+
run mk template show ${label} for the required slots (the use line is copy-pasteable)
|
|
288
|
+
`);
|
|
289
|
+
return EXIT.USAGE;
|
|
290
|
+
}
|
|
291
|
+
const appended = appendScenes(result.value, planned.withTypes, planned.withProps);
|
|
292
|
+
if (!appended.ok) {
|
|
293
|
+
io.stderr(`mk new: ${appended.message}\n`);
|
|
294
|
+
return EXIT.USAGE;
|
|
295
|
+
}
|
|
296
|
+
const out = await writeProject(appended.value, planned.out, io);
|
|
297
|
+
report(appended.value, out, planned.json, io);
|
|
298
|
+
io.stderr(formatFindings(appended.value.warnings.slice(0, MAX_WARNING_LINES)));
|
|
299
|
+
return EXIT.OK;
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
export const newCommand = makeNewCommand();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type TemplateEnv } from "../template/env.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The environment the real CLI passes (`defaultEnv`: built-ins, shared themes, stopwords) pinned to a
|
|
4
|
+
* project directory and with `MK_TEMPLATES_PATH` cleared, so tests cannot drift from production.
|
|
5
|
+
*/
|
|
6
|
+
export declare const testEnv: (projectDir: string, overrides?: Partial<TemplateEnv>) => TemplateEnv;
|
|
7
|
+
/** `<id>@<version>` of a built-in template, read from its manifest (never hard-coded in tests). */
|
|
8
|
+
export declare const builtInRef: (id: string) => string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { TEMPLATES_DIR } from "@anvpx/motion-templates";
|
|
4
|
+
import { defaultEnv } from "../template/env.js";
|
|
5
|
+
/**
|
|
6
|
+
* The environment the real CLI passes (`defaultEnv`: built-ins, shared themes, stopwords) pinned to a
|
|
7
|
+
* project directory and with `MK_TEMPLATES_PATH` cleared, so tests cannot drift from production.
|
|
8
|
+
*/
|
|
9
|
+
export const testEnv = (projectDir, overrides = {}) => ({
|
|
10
|
+
...defaultEnv(),
|
|
11
|
+
envPath: undefined,
|
|
12
|
+
projectDir,
|
|
13
|
+
...overrides,
|
|
14
|
+
});
|
|
15
|
+
/** `<id>@<version>` of a built-in template, read from its manifest (never hard-coded in tests). */
|
|
16
|
+
export const builtInRef = (id) => {
|
|
17
|
+
const manifest = JSON.parse(readFileSync(join(TEMPLATES_DIR, id, "template.json"), "utf8"));
|
|
18
|
+
return `${id}@${manifest.version}`;
|
|
19
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CommandResult, Host, Io, Runner, RunOptions } from "../types.ts";
|
|
2
|
+
export declare const NOT_FOUND: CommandResult;
|
|
3
|
+
export declare function ok(stdout?: string): CommandResult;
|
|
4
|
+
export declare function failed(code: number, stderr: string, stdout?: string): CommandResult;
|
|
5
|
+
/** A canned result, or a function that may also act (e.g. create the files a tool would). */
|
|
6
|
+
export type Response = CommandResult | ((options: RunOptions | undefined) => CommandResult | Promise<CommandResult>);
|
|
7
|
+
/**
|
|
8
|
+
* Runner answering by exact "cmd arg arg" key; anything else is command-not-found.
|
|
9
|
+
* `calls` records each key, `options` the RunOptions of the same call.
|
|
10
|
+
*/
|
|
11
|
+
export declare function fakeRunner(responses: Record<string, Response>, calls?: string[], options?: (RunOptions | undefined)[]): Runner;
|
|
12
|
+
export interface HostOptions {
|
|
13
|
+
platform?: NodeJS.Platform;
|
|
14
|
+
nodeVersion?: string;
|
|
15
|
+
homeDir?: string;
|
|
16
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
17
|
+
dirs?: Record<string, string[]>;
|
|
18
|
+
packageJson?: unknown;
|
|
19
|
+
/** Readable files by exact path. */
|
|
20
|
+
files?: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
export interface FakeHost extends Host {
|
|
23
|
+
written: Record<string, string>;
|
|
24
|
+
removed: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare function fakeHost(options?: HostOptions): FakeHost;
|
|
27
|
+
/** The real filesystem (real build into a temp dir) with a stubbed root package.json. */
|
|
28
|
+
export declare function diskHost(packageJson: unknown): Host;
|
|
29
|
+
export interface FakeIo extends Io {
|
|
30
|
+
out: string[];
|
|
31
|
+
err: string[];
|
|
32
|
+
}
|
|
33
|
+
export declare function fakeIo(run: Runner, host?: Host): FakeIo;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { nodeHost } from "../node-io.js";
|
|
3
|
+
export const NOT_FOUND = { code: 127, stdout: "", stderr: "not found" };
|
|
4
|
+
export function ok(stdout = "") {
|
|
5
|
+
return { code: 0, stdout, stderr: "" };
|
|
6
|
+
}
|
|
7
|
+
export function failed(code, stderr, stdout = "") {
|
|
8
|
+
return { code, stdout, stderr };
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Runner answering by exact "cmd arg arg" key; anything else is command-not-found.
|
|
12
|
+
* `calls` records each key, `options` the RunOptions of the same call.
|
|
13
|
+
*/
|
|
14
|
+
export function fakeRunner(responses, calls = [], options = []) {
|
|
15
|
+
return async (cmd, args, runOptions) => {
|
|
16
|
+
const key = [cmd, ...args].join(" ");
|
|
17
|
+
calls.push(key);
|
|
18
|
+
options.push(runOptions);
|
|
19
|
+
const response = responses[key];
|
|
20
|
+
if (response === undefined)
|
|
21
|
+
return NOT_FOUND;
|
|
22
|
+
return typeof response === "function" ? response(runOptions) : response;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function fakeHost(options = {}) {
|
|
26
|
+
const dirs = options.dirs ?? {};
|
|
27
|
+
const files = options.files ?? {};
|
|
28
|
+
const written = {};
|
|
29
|
+
const removed = [];
|
|
30
|
+
return {
|
|
31
|
+
platform: options.platform ?? "win32",
|
|
32
|
+
nodeVersion: options.nodeVersion ?? "24.12.0",
|
|
33
|
+
homeDir: options.homeDir ?? "/home/u",
|
|
34
|
+
env: options.env ?? {},
|
|
35
|
+
async listDir(path) {
|
|
36
|
+
const entries = dirs[path];
|
|
37
|
+
if (!entries)
|
|
38
|
+
throw new Error(`ENOENT: ${path}`);
|
|
39
|
+
return entries;
|
|
40
|
+
},
|
|
41
|
+
async mkdir(path) {
|
|
42
|
+
dirs[path] = dirs[path] ?? [];
|
|
43
|
+
},
|
|
44
|
+
async readRootPackageJson() {
|
|
45
|
+
return options.packageJson;
|
|
46
|
+
},
|
|
47
|
+
async readText(path) {
|
|
48
|
+
const text = files[path] ?? written[path];
|
|
49
|
+
if (text === undefined)
|
|
50
|
+
throw new Error(`ENOENT: ${path}`);
|
|
51
|
+
return text;
|
|
52
|
+
},
|
|
53
|
+
async writeText(path, text) {
|
|
54
|
+
written[path] = text;
|
|
55
|
+
},
|
|
56
|
+
async writeTextAtomic(path, text) {
|
|
57
|
+
written[path] = text;
|
|
58
|
+
},
|
|
59
|
+
async exists(path) {
|
|
60
|
+
return (files[path] ?? written[path]) !== undefined || path in dirs;
|
|
61
|
+
},
|
|
62
|
+
async copyFile(from, to) {
|
|
63
|
+
const text = files[from] ?? written[from];
|
|
64
|
+
if (text === undefined)
|
|
65
|
+
throw new Error(`ENOENT: ${from}`);
|
|
66
|
+
written[to] = text;
|
|
67
|
+
},
|
|
68
|
+
async renameFile(from, to) {
|
|
69
|
+
const text = files[from] ?? written[from];
|
|
70
|
+
if (text === undefined)
|
|
71
|
+
throw new Error(`ENOENT: ${from}`);
|
|
72
|
+
written[to] = text;
|
|
73
|
+
// Mirrors node:rename: the source is gone after the move.
|
|
74
|
+
delete written[from];
|
|
75
|
+
},
|
|
76
|
+
async sha256File(path) {
|
|
77
|
+
const text = files[path] ?? written[path];
|
|
78
|
+
return text === undefined ? undefined : createHash("sha256").update(text).digest("hex");
|
|
79
|
+
},
|
|
80
|
+
async remove(path) {
|
|
81
|
+
removed.push(path);
|
|
82
|
+
},
|
|
83
|
+
written,
|
|
84
|
+
removed,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** The real filesystem (real build into a temp dir) with a stubbed root package.json. */
|
|
88
|
+
export function diskHost(packageJson) {
|
|
89
|
+
return { ...nodeHost(), readRootPackageJson: async () => packageJson };
|
|
90
|
+
}
|
|
91
|
+
export function fakeIo(run, host = fakeHost()) {
|
|
92
|
+
const out = [];
|
|
93
|
+
const err = [];
|
|
94
|
+
return { stdout: (t) => out.push(t), stderr: (t) => err.push(t), run, host, out, err };
|
|
95
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The 5-scene demo storyboard, as raw text. */
|
|
2
|
+
export declare const demoText: () => string;
|
|
3
|
+
export interface LooseBoard {
|
|
4
|
+
scenes: Record<string, unknown>[];
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
/** A fresh deep copy of the demo storyboard for mutation in tests. */
|
|
8
|
+
export declare const demoBoard: () => LooseBoard;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
const DEMO = new URL("../../../build/fixtures/demo.storyboard.json", import.meta.url);
|
|
3
|
+
/** The 5-scene demo storyboard, as raw text. */
|
|
4
|
+
export const demoText = () => readFileSync(DEMO, "utf8");
|
|
5
|
+
/** A fresh deep copy of the demo storyboard for mutation in tests. */
|
|
6
|
+
export const demoBoard = () => JSON.parse(demoText());
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const probeKey: (file: string) => string;
|
|
2
|
+
export interface ProbeInit {
|
|
3
|
+
width?: number;
|
|
4
|
+
height?: number;
|
|
5
|
+
rate?: string;
|
|
6
|
+
frames?: number;
|
|
7
|
+
duration?: number;
|
|
8
|
+
codec?: string;
|
|
9
|
+
pixFmt?: string;
|
|
10
|
+
audio?: string;
|
|
11
|
+
}
|
|
12
|
+
/** ffprobe JSON for the demo storyboard's final render: 1920x1080, 30 fps, 32.5 s, 975 frames. */
|
|
13
|
+
export declare function probeJson(init?: ProbeInit): string;
|
|
14
|
+
/** The same render as a draft: 960x540 at 15 fps (488 frames). */
|
|
15
|
+
export declare const draftProbeJson: (init?: ProbeInit) => string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const probeKey = (file) => `ffprobe -v error -show_streams -show_format -of json ${file}`;
|
|
2
|
+
/** ffprobe JSON for the demo storyboard's final render: 1920x1080, 30 fps, 32.5 s, 975 frames. */
|
|
3
|
+
export function probeJson(init = {}) {
|
|
4
|
+
const streams = [
|
|
5
|
+
{
|
|
6
|
+
codec_type: "video",
|
|
7
|
+
codec_name: init.codec ?? "h264",
|
|
8
|
+
pix_fmt: init.pixFmt ?? "yuv420p",
|
|
9
|
+
width: init.width ?? 1920,
|
|
10
|
+
height: init.height ?? 1080,
|
|
11
|
+
r_frame_rate: init.rate ?? "30/1",
|
|
12
|
+
nb_frames: String(init.frames ?? 975),
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
if (init.audio)
|
|
16
|
+
streams.push({ codec_type: "audio", codec_name: init.audio });
|
|
17
|
+
return JSON.stringify({ streams, format: { duration: String(init.duration ?? 32.5) } });
|
|
18
|
+
}
|
|
19
|
+
/** The same render as a draft: 960x540 at 15 fps (488 frames). */
|
|
20
|
+
export const draftProbeJson = (init = {}) => probeJson({ width: 960, height: 540, rate: "15/1", frames: 488, ...init });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const PIN = "0.8.212";
|
|
2
|
+
export declare const PINNED_PACKAGE: {
|
|
3
|
+
motionKit: {
|
|
4
|
+
pins: {
|
|
5
|
+
hyperframes: string;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
/** Fake-runner key for `node <resolved hyperframes CLI> <args>` (mirrors runInProject). */
|
|
10
|
+
export declare const hfKey: (...args: readonly string[]) => string;
|
|
11
|
+
/** Fake-runner key for the hyperframes pin check. */
|
|
12
|
+
export declare const VERSION_KEY: string;
|
|
13
|
+
export interface TempProject {
|
|
14
|
+
dir: string;
|
|
15
|
+
storyboard: string;
|
|
16
|
+
/** Default build output: `<dir>/out/project`. */
|
|
17
|
+
out: string;
|
|
18
|
+
}
|
|
19
|
+
/** A temp directory holding the demo storyboard; removed when the test finishes. */
|
|
20
|
+
export declare function tempProject(storyboardText?: string): TempProject;
|
|
21
|
+
/** Creates empty files, as a stand-in for what an external tool would produce. */
|
|
22
|
+
export declare function touch(dir: string, ...names: string[]): void;
|