@anvpx/motion-cli 0.0.0-stage → 0.1.1
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 +464 -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 +764 -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 +279 -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 +27 -0
- package/dist/voice/run.js +190 -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 +215 -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 +721 -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,41 @@
|
|
|
1
|
+
import { EXIT } from "../exit-codes.js";
|
|
2
|
+
const STATUSES = ["ok", "usable", "missing", "unknown"];
|
|
3
|
+
const STATUS_WIDTH = Math.max(...STATUSES.map((s) => s.length));
|
|
4
|
+
const GAP = " ";
|
|
5
|
+
export function summarize(results) {
|
|
6
|
+
const counts = { ok: 0, usable: 0, missing: 0, unknown: 0 };
|
|
7
|
+
for (const r of results)
|
|
8
|
+
counts[r.status] += 1;
|
|
9
|
+
return counts;
|
|
10
|
+
}
|
|
11
|
+
/** 127 when a required tool is missing, else 2 when a required check is unknown, else 0. */
|
|
12
|
+
export function exitCodeFor(results) {
|
|
13
|
+
const required = results.filter((r) => r.required);
|
|
14
|
+
if (required.some((r) => r.status === "missing"))
|
|
15
|
+
return EXIT.MISSING_TOOL;
|
|
16
|
+
if (required.some((r) => r.status === "unknown"))
|
|
17
|
+
return EXIT.USAGE;
|
|
18
|
+
return EXIT.OK;
|
|
19
|
+
}
|
|
20
|
+
function row(r, idWidth) {
|
|
21
|
+
const detail = [r.version, r.hint && r.status !== "ok" ? `-> ${r.hint}` : undefined]
|
|
22
|
+
.filter((part) => part !== undefined)
|
|
23
|
+
.join(GAP);
|
|
24
|
+
return [r.id.padEnd(idWidth), r.status.padEnd(STATUS_WIDTH), detail].join(GAP).trimEnd();
|
|
25
|
+
}
|
|
26
|
+
export function formatTable(results) {
|
|
27
|
+
const idWidth = Math.max(0, ...results.map((r) => r.id.length));
|
|
28
|
+
const { ok, usable, missing, unknown } = summarize(results);
|
|
29
|
+
const summary = `doctor: ${ok} ok, ${usable} usable, ${missing} missing, ${unknown} unknown`;
|
|
30
|
+
return `${[...results.map((r) => row(r, idWidth)), summary].join("\n")}\n`;
|
|
31
|
+
}
|
|
32
|
+
/** One compact line: [{id, status, version?, hint?}]. */
|
|
33
|
+
export function formatJson(results) {
|
|
34
|
+
const rows = results.map(({ id, status, version, hint }) => ({
|
|
35
|
+
id,
|
|
36
|
+
status,
|
|
37
|
+
...(version === undefined ? {} : { version }),
|
|
38
|
+
...(hint === undefined ? {} : { hint }),
|
|
39
|
+
}));
|
|
40
|
+
return `${JSON.stringify(rows)}\n`;
|
|
41
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Io } from "../types.ts";
|
|
2
|
+
import type { CheckResult } from "./types.ts";
|
|
3
|
+
/** Rows `mk doctor --fix` is allowed to touch. Everything else is hint-only. */
|
|
4
|
+
export declare const SAFE_HEAL_IDS: readonly string[];
|
|
5
|
+
export interface HealEntry {
|
|
6
|
+
id: string;
|
|
7
|
+
healed: boolean;
|
|
8
|
+
detail: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function healSafeRows(results: readonly CheckResult[], io: Io): Promise<HealEntry[]>;
|
|
11
|
+
/** `healed: a, b` plus `still missing: c` (second line only when needed). */
|
|
12
|
+
export declare function formatHealLines(healed: readonly HealEntry[]): string;
|
|
13
|
+
/** One compact line: {results: [...], healed: [{id, healed, detail}]}. */
|
|
14
|
+
export declare function formatJsonWithHeal(results: readonly CheckResult[], healed: readonly HealEntry[]): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Rows `mk doctor --fix` is allowed to touch. Everything else is hint-only. */
|
|
2
|
+
export const SAFE_HEAL_IDS = [
|
|
3
|
+
"hyperframes",
|
|
4
|
+
"chrome-headless-shell",
|
|
5
|
+
"fonts",
|
|
6
|
+
"icons",
|
|
7
|
+
"templates",
|
|
8
|
+
"bin",
|
|
9
|
+
"catalog",
|
|
10
|
+
];
|
|
11
|
+
const SAFE = new Set(SAFE_HEAL_IDS);
|
|
12
|
+
/**
|
|
13
|
+
* Attempt a safe heal for every non-ok safe row. Never destructive: no delete,
|
|
14
|
+
* no filesystem writes, no network beyond `npx hyperframes browser ensure` for
|
|
15
|
+
* chrome. Hyperframes pin mismatches, fonts, icons, templates, bin and catalog
|
|
16
|
+
* only report their reinstall hint; only chrome `missing` runs the ensure
|
|
17
|
+
* command. Returns one entry per attempted row.
|
|
18
|
+
*/
|
|
19
|
+
function needsHeal(r) {
|
|
20
|
+
return SAFE.has(r.id) && r.status !== "ok";
|
|
21
|
+
}
|
|
22
|
+
function hintEntry(r) {
|
|
23
|
+
return { id: r.id, healed: false, detail: r.hint ?? "reinstall @anvpx/motion" };
|
|
24
|
+
}
|
|
25
|
+
async function ensureChrome(r, io) {
|
|
26
|
+
let code = 127;
|
|
27
|
+
try {
|
|
28
|
+
code = (await io.run("npx", ["hyperframes", "browser", "ensure"])).code;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
code = 1;
|
|
32
|
+
}
|
|
33
|
+
return code === 0
|
|
34
|
+
? { id: r.id, healed: true, detail: "npx hyperframes browser ensure succeeded" }
|
|
35
|
+
: { id: r.id, healed: false, detail: r.hint ?? "npx hyperframes browser ensure failed" };
|
|
36
|
+
}
|
|
37
|
+
async function healOne(r, io) {
|
|
38
|
+
if (!needsHeal(r))
|
|
39
|
+
return undefined;
|
|
40
|
+
if (r.id === "chrome-headless-shell" && r.status === "missing")
|
|
41
|
+
return ensureChrome(r, io);
|
|
42
|
+
return hintEntry(r);
|
|
43
|
+
}
|
|
44
|
+
export async function healSafeRows(results, io) {
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const r of results) {
|
|
47
|
+
const healed = await healOne(r, io);
|
|
48
|
+
if (healed)
|
|
49
|
+
out.push(healed);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** `healed: a, b` plus `still missing: c` (second line only when needed). */
|
|
54
|
+
export function formatHealLines(healed) {
|
|
55
|
+
const healedIds = healed.filter((h) => h.healed).map((h) => h.id);
|
|
56
|
+
const missingIds = healed.filter((h) => !h.healed).map((h) => h.id);
|
|
57
|
+
const healedLine = `healed: ${healedIds.length > 0 ? healedIds.join(", ") : "none"}\n`;
|
|
58
|
+
if (missingIds.length === 0)
|
|
59
|
+
return healedLine;
|
|
60
|
+
return `${healedLine}still missing: ${missingIds.join(", ")}\n`;
|
|
61
|
+
}
|
|
62
|
+
/** One compact line: {results: [...], healed: [{id, healed, detail}]}. */
|
|
63
|
+
export function formatJsonWithHeal(results, healed) {
|
|
64
|
+
const rows = results.map(({ id, status, version, hint }) => ({
|
|
65
|
+
id,
|
|
66
|
+
status,
|
|
67
|
+
...(version === undefined ? {} : { version }),
|
|
68
|
+
...(hint === undefined ? {} : { hint }),
|
|
69
|
+
}));
|
|
70
|
+
return `${JSON.stringify({ results: rows, healed })}\n`;
|
|
71
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Pins } from "./types.ts";
|
|
2
|
+
export declare const PIN_KEYS: {
|
|
3
|
+
readonly hyperframes: "motionKit.pins.hyperframes";
|
|
4
|
+
readonly chromeHeadlessShell: "motionKit.pins.chromeHeadlessShell";
|
|
5
|
+
};
|
|
6
|
+
export declare function readPins(packageJson: unknown): Pins;
|
|
7
|
+
/** True when the manifest declares `motionKit.pins`; only then is pin advice actionable. */
|
|
8
|
+
export declare function hasPins(packageJson: unknown): boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const PIN_KEYS = {
|
|
2
|
+
hyperframes: "motionKit.pins.hyperframes",
|
|
3
|
+
chromeHeadlessShell: "motionKit.pins.chromeHeadlessShell",
|
|
4
|
+
};
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null
|
|
7
|
+
? value
|
|
8
|
+
: undefined;
|
|
9
|
+
}
|
|
10
|
+
function pinString(pins, key) {
|
|
11
|
+
const value = pins?.[key];
|
|
12
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
export function readPins(packageJson) {
|
|
15
|
+
const pins = asRecord(asRecord(asRecord(packageJson)?.motionKit)?.pins);
|
|
16
|
+
const result = {};
|
|
17
|
+
const hyperframes = pinString(pins, "hyperframes");
|
|
18
|
+
const chrome = pinString(pins, "chromeHeadlessShell");
|
|
19
|
+
if (hyperframes)
|
|
20
|
+
result.hyperframes = hyperframes;
|
|
21
|
+
if (chrome)
|
|
22
|
+
result.chromeHeadlessShell = chrome;
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
/** True when the manifest declares `motionKit.pins`; only then is pin advice actionable. */
|
|
26
|
+
export function hasPins(packageJson) {
|
|
27
|
+
return asRecord(asRecord(asRecord(packageJson)?.motionKit)?.pins) !== undefined;
|
|
28
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Check } from "./types.ts";
|
|
2
|
+
/** Turns a package id into a path; injectable so tests need no real package tree. */
|
|
3
|
+
export type CoreResolver = (id: string) => string;
|
|
4
|
+
/**
|
|
5
|
+
* Candidate `skills/motion-kit` dirs, installed copy first. `@anvpx/motion/package.json` is
|
|
6
|
+
* the authoritative root (its exports map ships the manifest); the bare `@anvpx/motion`
|
|
7
|
+
* entry covers older installs that hide it, and the repo checkout covers development runs
|
|
8
|
+
* where the package is not resolvable. The repo is never a candidate once core resolves.
|
|
9
|
+
*/
|
|
10
|
+
export declare const skillsDirs: (resolveCorePkg?: CoreResolver) => string[];
|
|
11
|
+
export declare const skillsFiles: (resolveCorePkg?: CoreResolver) => string[];
|
|
12
|
+
/** `mk --help` target: the installed SKILL.md when it exists, else the repo copy. */
|
|
13
|
+
export declare const helpSkillFile: (resolveCorePkg?: CoreResolver) => string;
|
|
14
|
+
export declare const makeSkillsCheck: (resolveCorePkg?: CoreResolver) => Check;
|
|
15
|
+
export declare const checkSkills: Check;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { resolvePluginSourceDir } from "../plugin/install.js";
|
|
5
|
+
const defaultResolve = (id) => createRequire(import.meta.url).resolve(id);
|
|
6
|
+
const skillsDir = (root) => join(root, "skills", "motion-kit");
|
|
7
|
+
/**
|
|
8
|
+
* Candidate `skills/motion-kit` dirs, installed copy first. `@anvpx/motion/package.json` is
|
|
9
|
+
* the authoritative root (its exports map ships the manifest); the bare `@anvpx/motion`
|
|
10
|
+
* entry covers older installs that hide it, and the repo checkout covers development runs
|
|
11
|
+
* where the package is not resolvable. The repo is never a candidate once core resolves.
|
|
12
|
+
*/
|
|
13
|
+
export const skillsDirs = (resolveCorePkg = defaultResolve) => {
|
|
14
|
+
const installed = [];
|
|
15
|
+
try {
|
|
16
|
+
installed.push(skillsDir(dirname(resolveCorePkg("@anvpx/motion/package.json"))));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Older installs hide ./package.json; the bare export still names the root.
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
installed.push(skillsDir(dirname(dirname(resolveCorePkg("@anvpx/motion")))));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Not resolvable from here: fall back to the repo checkout below.
|
|
26
|
+
}
|
|
27
|
+
if (installed.length > 0)
|
|
28
|
+
return [...new Set(installed)];
|
|
29
|
+
return [skillsDir(dirname(dirname(resolvePluginSourceDir(resolveCorePkg))))];
|
|
30
|
+
};
|
|
31
|
+
export const skillsFiles = (resolveCorePkg = defaultResolve) => skillsDirs(resolveCorePkg).map((dir) => join(dir, "SKILL.md"));
|
|
32
|
+
/** `mk --help` target: the installed SKILL.md when it exists, else the repo copy. */
|
|
33
|
+
export const helpSkillFile = (resolveCorePkg) => {
|
|
34
|
+
const files = skillsFiles(resolveCorePkg);
|
|
35
|
+
return (files.find((file) => existsSync(file)) ?? files[0] ?? join("skills", "motion-kit", "SKILL.md"));
|
|
36
|
+
};
|
|
37
|
+
const REFERENCES_HINT = "reinstall @anvpx/motion (it ships skills/motion-kit/references)";
|
|
38
|
+
const MISSING_HINT = "reinstall @anvpx/motion";
|
|
39
|
+
/** The row for one candidate copy, or undefined when that copy is absent. */
|
|
40
|
+
async function skillAt(host, dir) {
|
|
41
|
+
if (!(await host.exists(join(dir, "SKILL.md")).catch(() => false)))
|
|
42
|
+
return undefined;
|
|
43
|
+
const references = await host.listDir(join(dir, "references")).catch(() => []);
|
|
44
|
+
if (references.length === 0) {
|
|
45
|
+
return { id: "skills", status: "usable", required: false, version: dir, hint: REFERENCES_HINT };
|
|
46
|
+
}
|
|
47
|
+
const count = `${references.length} reference${references.length === 1 ? "" : "s"}`;
|
|
48
|
+
return { id: "skills", status: "ok", required: false, version: `${dir} (${count})` };
|
|
49
|
+
}
|
|
50
|
+
export const makeSkillsCheck = (resolveCorePkg = defaultResolve) => async ({ host }) => {
|
|
51
|
+
for (const dir of skillsDirs(resolveCorePkg)) {
|
|
52
|
+
const found = await skillAt(host, dir);
|
|
53
|
+
if (found)
|
|
54
|
+
return found;
|
|
55
|
+
}
|
|
56
|
+
return { id: "skills", status: "missing", required: false, hint: MISSING_HINT };
|
|
57
|
+
};
|
|
58
|
+
export const checkSkills = makeSkillsCheck();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Host, Runner } from "../types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* ok: present and as wanted. usable: present but not as pinned/preferred. missing: absent or
|
|
4
|
+
* below the minimum. unknown: detection could not decide; never reported as ok or missing.
|
|
5
|
+
*/
|
|
6
|
+
export type CheckStatus = "ok" | "usable" | "missing" | "unknown";
|
|
7
|
+
export interface CheckResult {
|
|
8
|
+
id: string;
|
|
9
|
+
status: CheckStatus;
|
|
10
|
+
/** A required tool that is missing or unknown fails `mk doctor`. */
|
|
11
|
+
required: boolean;
|
|
12
|
+
version?: string;
|
|
13
|
+
/** What to do about it; only shown on non-ok results. */
|
|
14
|
+
hint?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Pins recorded in the repo's package.json under `motionKit.pins`. */
|
|
17
|
+
export interface Pins {
|
|
18
|
+
hyperframes?: string;
|
|
19
|
+
chromeHeadlessShell?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Files `pnpm install` / the repo vendor; injected so tests need no real disk. */
|
|
22
|
+
export interface Vendored {
|
|
23
|
+
/** Font family ids whose bundled woff2/licence files cannot be read. */
|
|
24
|
+
missingFonts(): readonly string[];
|
|
25
|
+
/** The vendored icon set (installed `@anvpx/motion-assets` icons.json). */
|
|
26
|
+
iconsFile: string;
|
|
27
|
+
}
|
|
28
|
+
export interface CheckContext {
|
|
29
|
+
run: Runner;
|
|
30
|
+
host: Host;
|
|
31
|
+
pins: Pins;
|
|
32
|
+
/** Whether the root manifest declares `motionKit.pins` at all; consumer projects do not. */
|
|
33
|
+
pinsPresent: boolean;
|
|
34
|
+
vendored: Vendored;
|
|
35
|
+
}
|
|
36
|
+
export type Check = (ctx: CheckContext) => Promise<CheckResult>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Vendored } from "./types.ts";
|
|
2
|
+
/** Turns a package id into a path; injectable so tests need no real package tree. */
|
|
3
|
+
export type PackageResolver = (id: string) => string;
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the vendored icon set.
|
|
6
|
+
* Published layout first: the staged core ships `icons/icons.json` so a
|
|
7
|
+
* consumer install (`node_modules/@anvpx/motion/icons/icons.json`) works
|
|
8
|
+
* without the workspace checkout or a separate `@anvpx/motion-assets` install.
|
|
9
|
+
* Later candidates cover the installed `@anvpx/motion-assets` package (resolved
|
|
10
|
+
* by name, since installed siblings are `@anvpx/motion-<name>`) and the dev
|
|
11
|
+
* workspace. Every candidate is verified with `existsSync`; a total miss throws
|
|
12
|
+
* a fixable error instead of returning a path that does not exist.
|
|
13
|
+
*/
|
|
14
|
+
export declare const resolveIconsFile: (resolvePkg?: PackageResolver) => string;
|
|
15
|
+
export declare const defaultVendored: () => Vendored;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { bundleFonts, FONT_FAMILIES } from "@anvpx/motion-build";
|
|
6
|
+
/** Families whose pinned `@fontsource/*` files cannot be read (`bundleFonts` is the render-time reader). */
|
|
7
|
+
const missingFonts = () => Object.keys(FONT_FAMILIES).filter((id) => {
|
|
8
|
+
try {
|
|
9
|
+
bundleFonts([id]);
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
17
|
+
const defaultResolve = (id) => nodeRequire.resolve(id);
|
|
18
|
+
/** Icons beside the resolved build package; records misses in `searched`. */
|
|
19
|
+
const buildAnchoredIcons = (resolvePkg, searched) => {
|
|
20
|
+
try {
|
|
21
|
+
const buildDir = dirname(realpathSync(resolvePkg("@anvpx/motion-build")));
|
|
22
|
+
// Installed siblings are `@anvpx/motion-assets`; the dev checkout names the
|
|
23
|
+
// directory `assets`. Both hold `icons/icons.json`, so try each before giving up.
|
|
24
|
+
for (const sibling of ["motion-assets", "assets"]) {
|
|
25
|
+
const candidate = join(buildDir, "..", "..", sibling, "icons", "icons.json");
|
|
26
|
+
if (existsSync(candidate))
|
|
27
|
+
return candidate;
|
|
28
|
+
searched.push(candidate);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Not resolvable here: the caller falls through to the fixable error.
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the vendored icon set.
|
|
38
|
+
* Published layout first: the staged core ships `icons/icons.json` so a
|
|
39
|
+
* consumer install (`node_modules/@anvpx/motion/icons/icons.json`) works
|
|
40
|
+
* without the workspace checkout or a separate `@anvpx/motion-assets` install.
|
|
41
|
+
* Later candidates cover the installed `@anvpx/motion-assets` package (resolved
|
|
42
|
+
* by name, since installed siblings are `@anvpx/motion-<name>`) and the dev
|
|
43
|
+
* workspace. Every candidate is verified with `existsSync`; a total miss throws
|
|
44
|
+
* a fixable error instead of returning a path that does not exist.
|
|
45
|
+
*/
|
|
46
|
+
export const resolveIconsFile = (resolvePkg = defaultResolve) => {
|
|
47
|
+
const searched = [];
|
|
48
|
+
// Staged copies inside the published core tarball (from core/dist or from a
|
|
49
|
+
// cli/dist staged inside a core install). WHATWG URL resolution normalizes
|
|
50
|
+
// the `..` segments, so the returned path never contains parent traversal.
|
|
51
|
+
for (const candidate of [
|
|
52
|
+
fileURLToPath(new URL("../../icons/icons.json", import.meta.url)),
|
|
53
|
+
fileURLToPath(new URL("../icons/icons.json", import.meta.url)),
|
|
54
|
+
]) {
|
|
55
|
+
if (existsSync(candidate))
|
|
56
|
+
return candidate;
|
|
57
|
+
searched.push(candidate);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const direct = resolvePkg("@anvpx/motion-assets/icons/icons.json");
|
|
61
|
+
if (existsSync(direct))
|
|
62
|
+
return direct;
|
|
63
|
+
searched.push(direct);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* fall through to package-dir resolution */
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const pkg = resolvePkg("@anvpx/motion-assets/package.json");
|
|
70
|
+
const viaPkg = join(dirname(pkg), "icons", "icons.json");
|
|
71
|
+
if (existsSync(viaPkg))
|
|
72
|
+
return viaPkg;
|
|
73
|
+
searched.push(viaPkg);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* fall through to build-anchored resolution */
|
|
77
|
+
}
|
|
78
|
+
const viaBuild = buildAnchoredIcons(resolvePkg, searched);
|
|
79
|
+
if (viaBuild !== undefined)
|
|
80
|
+
return viaBuild;
|
|
81
|
+
const fallback = fileURLToPath(new URL("./icons.json", import.meta.url));
|
|
82
|
+
if (existsSync(fallback))
|
|
83
|
+
return fallback;
|
|
84
|
+
searched.push(fallback);
|
|
85
|
+
throw new Error(`Cannot locate icons.json (fixable: reinstall @anvpx/motion with its dependencies). Searched: ${searched.join(", ")}`);
|
|
86
|
+
};
|
|
87
|
+
export const defaultVendored = () => ({
|
|
88
|
+
missingFonts,
|
|
89
|
+
iconsFile: resolveIconsFile(),
|
|
90
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface Version {
|
|
2
|
+
major: number;
|
|
3
|
+
minor: number;
|
|
4
|
+
patch: number;
|
|
5
|
+
}
|
|
6
|
+
/** Pulls the first version out of text; the pattern must capture major, minor, optional patch. */
|
|
7
|
+
export declare function parseVersion(text: string, pattern?: RegExp): Version | undefined;
|
|
8
|
+
export declare function formatVersion(v: Version): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const GENERIC = /(\d+)\.(\d+)(?:\.(\d+))?/;
|
|
2
|
+
/** Pulls the first version out of text; the pattern must capture major, minor, optional patch. */
|
|
3
|
+
export function parseVersion(text, pattern = GENERIC) {
|
|
4
|
+
const match = pattern.exec(text);
|
|
5
|
+
if (!match)
|
|
6
|
+
return undefined;
|
|
7
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3] ?? 0) };
|
|
8
|
+
}
|
|
9
|
+
export function formatVersion(v) {
|
|
10
|
+
return `${v.major}.${v.minor}.${v.patch}`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** The one place exit codes are defined; every command returns one of these. Documented in README.md. */
|
|
2
|
+
export declare const EXIT: {
|
|
3
|
+
/** Success; warnings allowed. */
|
|
4
|
+
readonly OK: 0;
|
|
5
|
+
/** Tool, build or render failure. */
|
|
6
|
+
readonly ERROR: 1;
|
|
7
|
+
/** Invalid input: bad flags, unreadable file, or a storyboard that fails validation. Also a check that could not be decided (verify/qc UNKNOWN, doctor unknown). */
|
|
8
|
+
readonly USAGE: 2;
|
|
9
|
+
/** `mk render --gated` found no approval for the current storyboard hash (ask the user, then `mk approve`). */
|
|
10
|
+
readonly AWAITING_APPROVAL: 4;
|
|
11
|
+
/** QA failed: `mk verify` found a fatal finding. */
|
|
12
|
+
readonly QA_FAIL: 5;
|
|
13
|
+
/** A required external tool is missing or cannot be run. */
|
|
14
|
+
readonly MISSING_TOOL: 127;
|
|
15
|
+
};
|
|
16
|
+
export type ExitCode = (typeof EXIT)[keyof typeof EXIT];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The one place exit codes are defined; every command returns one of these. Documented in README.md. */
|
|
2
|
+
export const EXIT = {
|
|
3
|
+
/** Success; warnings allowed. */
|
|
4
|
+
OK: 0,
|
|
5
|
+
/** Tool, build or render failure. */
|
|
6
|
+
ERROR: 1,
|
|
7
|
+
/** Invalid input: bad flags, unreadable file, or a storyboard that fails validation. Also a check that could not be decided (verify/qc UNKNOWN, doctor unknown). */
|
|
8
|
+
USAGE: 2,
|
|
9
|
+
/** `mk render --gated` found no approval for the current storyboard hash (ask the user, then `mk approve`). */
|
|
10
|
+
AWAITING_APPROVAL: 4,
|
|
11
|
+
/** QA failed: `mk verify` found a fatal finding. */
|
|
12
|
+
QA_FAIL: 5,
|
|
13
|
+
/** A required external tool is missing or cannot be run. */
|
|
14
|
+
MISSING_TOOL: 127,
|
|
15
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { MIN_HOLD_SECONDS, resolveTimeline, sceneWords, validateStoryboardFull, WORDS_PER_SECOND, } from "@anvpx/motion-build";
|
|
2
|
+
import { parseFlags, requireStoryboard, textFlag, usageError } from "../args.js";
|
|
3
|
+
import { readProvenance } from "../preview/provenance.js";
|
|
4
|
+
import { checkStoryboard } from "../storyboard/check.js";
|
|
5
|
+
import { editStoryboard, round1 } from "../storyboard/edit.js";
|
|
6
|
+
import { defaultEnv, findTemplate } from "../template/env.js";
|
|
7
|
+
import { fitDurations } from "./solve.js";
|
|
8
|
+
import { fitTarget } from "./target.js";
|
|
9
|
+
const USAGE = `mk fit <storyboard.json> --seconds N
|
|
10
|
+
mk fit <storyboard.json> --bpm B --bars R
|
|
11
|
+
|
|
12
|
+
Stretch or squeeze a storyboard to N seconds in total. Every base scene is scaled by one common factor,
|
|
13
|
+
kept inside its type's range, then validated and written atomically (deterministic; nothing written when refused).
|
|
14
|
+
Range per scene: from the template's pacing.json (the smallest to the largest seconds of that scene type across
|
|
15
|
+
short/standard/long), never under the reading time of its text (3 words per second) or the 2 s minimum hold.
|
|
16
|
+
Without template provenance or pacing for a type, the range is the floor up to 1.5x the current length.
|
|
17
|
+
Refuses, and says why, when N is outside what the scenes allow: add scenes (\`mk scene add\`) or change N.
|
|
18
|
+
--seconds N target total (the same total \`mk validate\` and \`mk preview\` report)
|
|
19
|
+
Prints before/after on <= 3 lines. Overlays scale with the scene they cover.
|
|
20
|
+
`;
|
|
21
|
+
const FALLBACK_STRETCH = 1.5;
|
|
22
|
+
const MAX_LISTED = 8;
|
|
23
|
+
const MAX_FLOORS = 2;
|
|
24
|
+
const hardFloor = (words) => Math.max(MIN_HOLD_SECONDS, Math.ceil(words / WORDS_PER_SECOND));
|
|
25
|
+
function rowFor(type, cur, words, pacing) {
|
|
26
|
+
const floor = hardFloor(words);
|
|
27
|
+
const seconds = Object.values(pacing?.sceneSeconds[type] ?? {});
|
|
28
|
+
if (seconds.length === 0)
|
|
29
|
+
return { cur, lo: floor, hi: Math.max(floor, cur * FALLBACK_STRETCH) };
|
|
30
|
+
return {
|
|
31
|
+
cur,
|
|
32
|
+
lo: Math.max(floor, Math.min(...seconds, cur)),
|
|
33
|
+
hi: Math.max(floor, cur, ...seconds),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const basesOf = (board, timeline, pacing) => timeline.scenes.map((timed) => {
|
|
37
|
+
const entry = board.scenes[timed.index];
|
|
38
|
+
const words = sceneWords(entry.props, entry.type);
|
|
39
|
+
const read = Math.ceil(words / WORDS_PER_SECOND);
|
|
40
|
+
return {
|
|
41
|
+
id: timed.id,
|
|
42
|
+
cur: timed.duration,
|
|
43
|
+
words,
|
|
44
|
+
read,
|
|
45
|
+
row: rowFor(entry.type, timed.duration, words, pacing),
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
const listed = (before, after) => {
|
|
49
|
+
const changes = before.map((scene, i) => `${scene.id} ${round1(scene.cur)}>${after[i]}`);
|
|
50
|
+
const shown = changes.slice(0, MAX_LISTED).join(", ");
|
|
51
|
+
return changes.length > MAX_LISTED ? `${shown} (+${changes.length - MAX_LISTED} more)` : shown;
|
|
52
|
+
};
|
|
53
|
+
const readingFloors = (bases) => [...bases]
|
|
54
|
+
.sort((a, b) => b.read - a.read)
|
|
55
|
+
.slice(0, MAX_FLOORS)
|
|
56
|
+
.map((scene) => `${scene.id} needs >= ${scene.read}s to read (${scene.words} words)`)
|
|
57
|
+
.join("; ");
|
|
58
|
+
/** Why the target is out of reach, in terms the author can act on. */
|
|
59
|
+
function refusal(reason, range, target, bases, source) {
|
|
60
|
+
if (reason === "below") {
|
|
61
|
+
return `${target}s is under the ${range.min}s these scenes need; reading time floors: ${readingFloors(bases)}. Cut text or scenes, or raise --seconds`;
|
|
62
|
+
}
|
|
63
|
+
return `${target}s is over the ${range.max}s these scenes can stretch to (range ${range.min}-${range.max}s, ${source}). Add scenes (mk scene add) or lower --seconds`;
|
|
64
|
+
}
|
|
65
|
+
/** Overlay `duration` and `offset` follow the scale of the base scene they cover. */
|
|
66
|
+
function scaleOverlay(scene, scale) {
|
|
67
|
+
for (const key of ["duration", "offset"]) {
|
|
68
|
+
const value = scene[key];
|
|
69
|
+
if (typeof value === "number")
|
|
70
|
+
scene[key] = round1(value * scale);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** A copy of the storyboard with the new durations; overlays scale with the scene they cover. */
|
|
74
|
+
function withDurations(doc, bases, seconds) {
|
|
75
|
+
const next = structuredClone(doc);
|
|
76
|
+
const ratio = new Map();
|
|
77
|
+
for (const [i, base] of bases.entries()) {
|
|
78
|
+
const to = seconds[i] ?? base.cur;
|
|
79
|
+
ratio.set(base.id, to / base.cur);
|
|
80
|
+
const scene = next.scenes.find((entry) => entry.id === base.id);
|
|
81
|
+
if (scene)
|
|
82
|
+
scene.duration = to;
|
|
83
|
+
}
|
|
84
|
+
for (const scene of next.scenes) {
|
|
85
|
+
const scale = scene.layer === "overlay" ? ratio.get(String(scene.over)) : undefined;
|
|
86
|
+
if (scale !== undefined)
|
|
87
|
+
scaleOverlay(scene, scale);
|
|
88
|
+
}
|
|
89
|
+
return next;
|
|
90
|
+
}
|
|
91
|
+
/** The valid storyboard and its timeline, or undefined when validation or lint has errors. */
|
|
92
|
+
function resolveBoard(doc) {
|
|
93
|
+
const valid = validateStoryboardFull(doc);
|
|
94
|
+
if (!valid.ok || checkStoryboard(doc).errors.length > 0)
|
|
95
|
+
return undefined;
|
|
96
|
+
const timeline = resolveTimeline(valid.value);
|
|
97
|
+
return timeline.ok ? { board: valid.value, timeline: timeline.value } : undefined;
|
|
98
|
+
}
|
|
99
|
+
/** The edit: resolve, fit, and describe; refuses (never throws) when it cannot. */
|
|
100
|
+
function fitStoryboard(doc, request) {
|
|
101
|
+
const resolved = resolveBoard(doc);
|
|
102
|
+
if (resolved === undefined) {
|
|
103
|
+
return { ok: false, message: "the storyboard has errors; fix them first (mk validate)" };
|
|
104
|
+
}
|
|
105
|
+
const { board, timeline } = resolved;
|
|
106
|
+
const bases = basesOf(board, timeline, request.pacing);
|
|
107
|
+
const overlap = timeline.scenes.reduce((sum, scene) => sum + scene.overlap, 0);
|
|
108
|
+
const fitted = fitDurations(bases.map((base) => base.row), request.target + overlap);
|
|
109
|
+
if (!fitted.ok) {
|
|
110
|
+
const range = { min: round1(fitted.min - overlap), max: round1(fitted.max - overlap) };
|
|
111
|
+
const why = refusal(fitted.reason, range, request.target, bases, request.source);
|
|
112
|
+
return { ok: false, message: `refused, nothing written: ${why}` };
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
value: withDurations(doc, bases, fitted.seconds),
|
|
117
|
+
lines: (after) => [
|
|
118
|
+
`fit ok | ${round1(timeline.total)}s -> ${round1(after.total ?? 0)}s (target ${request.target}s) | ${bases.length} scenes | ${request.source}`,
|
|
119
|
+
listed(bases, fitted.seconds),
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** The pacing of the template that made the storyboard, when it is installed. */
|
|
124
|
+
async function pacingOf(env, io, path) {
|
|
125
|
+
const provenance = await readProvenance(io.host, path);
|
|
126
|
+
const found = provenance && findTemplate(env, io, provenance.template.split("@")[0] ?? "");
|
|
127
|
+
if (!provenance || !found?.ok) {
|
|
128
|
+
return { pacing: undefined, source: "fallback range (no template pacing)" };
|
|
129
|
+
}
|
|
130
|
+
return { pacing: found.template.pacing, source: provenance.template };
|
|
131
|
+
}
|
|
132
|
+
/** `mk fit`; `env` is injectable for tests. */
|
|
133
|
+
export const makeFitCommand = (env = defaultEnv) => ({
|
|
134
|
+
name: "fit",
|
|
135
|
+
summary: "stretch or squeeze a storyboard to --seconds N or --bpm/--bars within each scene's range",
|
|
136
|
+
async run(args, io) {
|
|
137
|
+
const parsed = parseFlags("fit", USAGE, args, { seconds: { type: "string" }, bpm: { type: "string" }, bars: { type: "string" } }, io);
|
|
138
|
+
if (parsed.kind === "exit")
|
|
139
|
+
return parsed.code;
|
|
140
|
+
const path = requireStoryboard("fit", USAGE, parsed.positionals, io);
|
|
141
|
+
if (typeof path !== "string")
|
|
142
|
+
return path.code;
|
|
143
|
+
const resolved = fitTarget({
|
|
144
|
+
seconds: textFlag(parsed.flags, "seconds"),
|
|
145
|
+
bpm: textFlag(parsed.flags, "bpm"),
|
|
146
|
+
bars: textFlag(parsed.flags, "bars"),
|
|
147
|
+
});
|
|
148
|
+
if (!resolved.ok)
|
|
149
|
+
return usageError("fit", USAGE, resolved.message, io).code;
|
|
150
|
+
const request = { target: resolved.target, ...(await pacingOf(env(), io, path)) };
|
|
151
|
+
return editStoryboard("fit", path, io, (doc) => fitStoryboard(doc, request));
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
export const fitCommand = makeFitCommand();
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** One base scene: current seconds and the seconds it may be stretched or squeezed to. */
|
|
2
|
+
export interface FitRow {
|
|
3
|
+
readonly cur: number;
|
|
4
|
+
readonly lo: number;
|
|
5
|
+
readonly hi: number;
|
|
6
|
+
}
|
|
7
|
+
export type FitResult = {
|
|
8
|
+
ok: true;
|
|
9
|
+
seconds: number[];
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
reason: "below" | "above";
|
|
13
|
+
min: number;
|
|
14
|
+
max: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Scales every scene by one common factor, clamped to each scene's [lo, hi], so the durations
|
|
18
|
+
* add up to `targetSum` seconds (to 0.1 s). Deterministic: bisection on the factor, then the
|
|
19
|
+
* rounding remainder is distributed by largest fractional part.
|
|
20
|
+
*/
|
|
21
|
+
export declare function fitDurations(rows: readonly FitRow[], targetSum: number): FitResult;
|