@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,571 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { SHARED_THEMES_DIR, TEMPLATES_DIR } from "@anvpx/motion-templates";
|
|
5
|
+
import { EXIT } from "../exit-codes.js";
|
|
6
|
+
import { hyperframesCliPath, hyperframesPackagePath } from "../tooling/session.js";
|
|
7
|
+
import { PIN_KEYS } from "./pins.js";
|
|
8
|
+
import { checkSkills } from "./skills.js";
|
|
9
|
+
import { formatVersion, parseVersion } from "./version.js";
|
|
10
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
11
|
+
const MIN_NODE_MAJOR = 22;
|
|
12
|
+
const MIN_FFMPEG_MAJOR = 6;
|
|
13
|
+
const WANTED_PYTHON = "3.12";
|
|
14
|
+
const KOKORO_VERSION = "0.9.4";
|
|
15
|
+
const SOUNDFILE_VERSION = "0.14.0";
|
|
16
|
+
const FASTER_WHISPER_VERSION = "1.2.1";
|
|
17
|
+
/** DL-01b voice bar: Kokoro is the local primary; failures are manual-hint-only. */
|
|
18
|
+
export const VOICE_LOCAL_PRIMARY = "Kokoro local primary";
|
|
19
|
+
export const VOICE_WAV_FALLBACK = "pre-rendered WAV fallback in pack";
|
|
20
|
+
export const VOICE_MANUAL_SETUP = "manual setup only; no auto-install";
|
|
21
|
+
export const VOICE_NO_PAID_BACKEND = "no paid backend";
|
|
22
|
+
export const VOICE_ALIGNER = "aligner";
|
|
23
|
+
const VENV_PYS = [".venv-voice/Scripts/python", ".venv-voice/bin/python"];
|
|
24
|
+
const ESPEAK_RELEASES = "https://github.com/espeak-ng/espeak-ng/releases";
|
|
25
|
+
const FFMPEG_VERSION = /version\s+n?(\d+)\.(\d+)(?:\.(\d+))?/;
|
|
26
|
+
const CHROME_CACHE = [".cache", "hyperframes", "chrome", "chrome-headless-shell"];
|
|
27
|
+
const PLATFORM_PREFIX = /^[a-z]+[a-z0-9-]*?-(?=\d+\.\d)/;
|
|
28
|
+
/** The program is not on PATH (the runner reports every spawn failure as 127). */
|
|
29
|
+
const absent = (result) => result.code === EXIT.MISSING_TOOL;
|
|
30
|
+
function lines(text) {
|
|
31
|
+
return text
|
|
32
|
+
.split(/\r?\n/)
|
|
33
|
+
.map((line) => line.trim())
|
|
34
|
+
.filter((line) => line.length > 0);
|
|
35
|
+
}
|
|
36
|
+
const found = (id, version, required = false) => ({
|
|
37
|
+
id,
|
|
38
|
+
status: "ok",
|
|
39
|
+
required,
|
|
40
|
+
version,
|
|
41
|
+
});
|
|
42
|
+
export const checkNode = async ({ host }) => {
|
|
43
|
+
const version = parseVersion(host.nodeVersion);
|
|
44
|
+
const base = { id: "node", required: true, version: host.nodeVersion };
|
|
45
|
+
if (!version)
|
|
46
|
+
return { ...base, status: "unknown", hint: "cannot parse the node version" };
|
|
47
|
+
if (version.major >= MIN_NODE_MAJOR)
|
|
48
|
+
return { ...base, status: "ok" };
|
|
49
|
+
return { ...base, status: "missing", hint: `install Node ${MIN_NODE_MAJOR}+ (24 recommended)` };
|
|
50
|
+
};
|
|
51
|
+
export const checkPnpm = async ({ run }) => {
|
|
52
|
+
const id = "pnpm";
|
|
53
|
+
const result = await run("pnpm", ["--version"]);
|
|
54
|
+
if (absent(result)) {
|
|
55
|
+
return { id, status: "missing", required: true, hint: "corepack enable pnpm" };
|
|
56
|
+
}
|
|
57
|
+
const version = result.code === 0 ? parseVersion(result.stdout) : undefined;
|
|
58
|
+
if (!version) {
|
|
59
|
+
return { id, status: "unknown", required: true, hint: "pnpm ran but gave no version" };
|
|
60
|
+
}
|
|
61
|
+
return found(id, formatVersion(version), true);
|
|
62
|
+
};
|
|
63
|
+
function mediaTool(id) {
|
|
64
|
+
return async ({ run }) => {
|
|
65
|
+
const result = await run(id, ["-version"]);
|
|
66
|
+
if (absent(result)) {
|
|
67
|
+
const hint = `install ffmpeg >= ${MIN_FFMPEG_MAJOR} and put it on PATH`;
|
|
68
|
+
return { id, status: "missing", required: true, hint };
|
|
69
|
+
}
|
|
70
|
+
const version = result.code === 0 ? parseVersion(result.stdout, FFMPEG_VERSION) : undefined;
|
|
71
|
+
if (!version) {
|
|
72
|
+
return { id, status: "unknown", required: true, hint: "unparsable version (git build?)" };
|
|
73
|
+
}
|
|
74
|
+
const text = formatVersion(version);
|
|
75
|
+
if (version.major >= MIN_FFMPEG_MAJOR)
|
|
76
|
+
return found(id, text, true);
|
|
77
|
+
const hint = `upgrade ffmpeg to >= ${MIN_FFMPEG_MAJOR}`;
|
|
78
|
+
return { id, status: "missing", required: true, version: text, hint };
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export const checkFfmpeg = mediaTool("ffmpeg");
|
|
82
|
+
export const checkFfprobe = mediaTool("ffprobe");
|
|
83
|
+
/** Version from an installed package manifest; undefined when absent or unparsable. */
|
|
84
|
+
async function packageVersion(host, path) {
|
|
85
|
+
try {
|
|
86
|
+
const value = JSON.parse(await host.readText(path)).version;
|
|
87
|
+
const parsed = typeof value === "string" ? parseVersion(value) : undefined;
|
|
88
|
+
return parsed ? formatVersion(parsed) : undefined;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export const checkHyperframes = async ({ run, pins, host }) => {
|
|
95
|
+
const id = "hyperframes";
|
|
96
|
+
const pin = pins.hyperframes;
|
|
97
|
+
if (!pin) {
|
|
98
|
+
let manifest;
|
|
99
|
+
try {
|
|
100
|
+
manifest = hyperframesPackagePath();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return {
|
|
104
|
+
id,
|
|
105
|
+
status: "missing",
|
|
106
|
+
required: false,
|
|
107
|
+
hint: "cannot resolve hyperframes; reinstall @anvpx/motion (it ships hyperframes as a runtime dependency)",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const version = await packageVersion(host, manifest);
|
|
111
|
+
if (!version) {
|
|
112
|
+
return {
|
|
113
|
+
id,
|
|
114
|
+
status: "unknown",
|
|
115
|
+
required: false,
|
|
116
|
+
hint: "cannot read the installed hyperframes version; reinstall @anvpx/motion",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return found(id, version);
|
|
120
|
+
}
|
|
121
|
+
let cli;
|
|
122
|
+
try {
|
|
123
|
+
cli = hyperframesCliPath();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return {
|
|
127
|
+
id,
|
|
128
|
+
status: "missing",
|
|
129
|
+
required: false,
|
|
130
|
+
hint: `cannot resolve hyperframes@${pin}; reinstall @anvpx/motion (it ships hyperframes@${pin} as a runtime dependency)`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const result = await run(process.execPath, [cli, "--version"]);
|
|
134
|
+
if (result.code !== 0) {
|
|
135
|
+
return {
|
|
136
|
+
id,
|
|
137
|
+
status: "missing",
|
|
138
|
+
required: false,
|
|
139
|
+
hint: `cannot run hyperframes@${pin} (node ${cli}); reinstall @anvpx/motion so it is installed alongside it`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const installed = parseVersion(result.stdout);
|
|
143
|
+
if (!installed) {
|
|
144
|
+
return { id, status: "unknown", required: false, hint: "hyperframes gave no version" };
|
|
145
|
+
}
|
|
146
|
+
const version = formatVersion(installed);
|
|
147
|
+
if (version === pin)
|
|
148
|
+
return found(id, version);
|
|
149
|
+
const hint = `pinned ${pin}: install hyperframes@${pin} alongside @anvpx/motion (or update ${PIN_KEYS.hyperframes})`;
|
|
150
|
+
return { id, status: "usable", required: false, version, hint };
|
|
151
|
+
};
|
|
152
|
+
async function chromeVersions(ctx) {
|
|
153
|
+
try {
|
|
154
|
+
const entries = await ctx.host.listDir(join(ctx.host.homeDir, ...CHROME_CACHE));
|
|
155
|
+
return entries.map((entry) => entry.replace(PLATFORM_PREFIX, ""));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export const checkChrome = async (ctx) => {
|
|
162
|
+
const id = "chrome-headless-shell";
|
|
163
|
+
const versions = await chromeVersions(ctx);
|
|
164
|
+
if (versions.length === 0) {
|
|
165
|
+
return {
|
|
166
|
+
id,
|
|
167
|
+
status: "missing",
|
|
168
|
+
required: false,
|
|
169
|
+
hint: "first render downloads it; prefetch: npx hyperframes browser ensure",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (!ctx.pinsPresent)
|
|
173
|
+
return found(id, versions.join(", "));
|
|
174
|
+
const pin = ctx.pins.chromeHeadlessShell;
|
|
175
|
+
if (pin && versions.includes(pin))
|
|
176
|
+
return found(id, pin);
|
|
177
|
+
const hint = pin
|
|
178
|
+
? `pinned ${pin}: install it or update ${PIN_KEYS.chromeHeadlessShell}`
|
|
179
|
+
: `add ${PIN_KEYS.chromeHeadlessShell}`;
|
|
180
|
+
return { id, status: "usable", required: false, version: versions.join(", "), hint };
|
|
181
|
+
};
|
|
182
|
+
const NO_GPU_HINT = "software GL is CI-only slow; install a GPU driver for local renders";
|
|
183
|
+
async function gpuNames(ctx) {
|
|
184
|
+
const cim = await ctx.run("powershell", [
|
|
185
|
+
"-NoProfile",
|
|
186
|
+
"-Command",
|
|
187
|
+
"(Get-CimInstance Win32_VideoController).Name",
|
|
188
|
+
]);
|
|
189
|
+
if (cim.code === 0 && lines(cim.stdout).length > 0)
|
|
190
|
+
return lines(cim.stdout);
|
|
191
|
+
const wmic = await ctx.run("wmic", ["path", "win32_videocontroller", "get", "name"]);
|
|
192
|
+
if (wmic.code !== 0)
|
|
193
|
+
return [];
|
|
194
|
+
return lines(wmic.stdout).filter((line) => line.toLowerCase() !== "name");
|
|
195
|
+
}
|
|
196
|
+
export const checkGpu = async (ctx) => {
|
|
197
|
+
const id = "gpu";
|
|
198
|
+
if (ctx.host.platform !== "win32") {
|
|
199
|
+
return { id, status: "unknown", required: false, hint: "detection is Windows-only" };
|
|
200
|
+
}
|
|
201
|
+
const names = await gpuNames(ctx);
|
|
202
|
+
if (names.length === 0) {
|
|
203
|
+
return { id, status: "missing", required: false, hint: NO_GPU_HINT };
|
|
204
|
+
}
|
|
205
|
+
return found(id, names.join(", "));
|
|
206
|
+
};
|
|
207
|
+
async function pythonVersion(ctx) {
|
|
208
|
+
for (const cmd of ["python3", "python"]) {
|
|
209
|
+
const result = await ctx.run(cmd, ["--version"]);
|
|
210
|
+
const version = result.code === 0 ? parseVersion(`${result.stdout}${result.stderr}`) : undefined;
|
|
211
|
+
if (version)
|
|
212
|
+
return formatVersion(version);
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
export const checkPython = async (ctx) => {
|
|
217
|
+
const id = "python";
|
|
218
|
+
const hint = `install Python ${WANTED_PYTHON} (voice phase only)`;
|
|
219
|
+
const version = await pythonVersion(ctx);
|
|
220
|
+
if (!version)
|
|
221
|
+
return { id, status: "missing", required: false, hint };
|
|
222
|
+
if (version.startsWith(`${WANTED_PYTHON}.`))
|
|
223
|
+
return found(id, version);
|
|
224
|
+
const tooNew = (parseVersion(version)?.minor ?? 0) > 12;
|
|
225
|
+
const note = tooNew ? "too new for TTS wheels; " : "";
|
|
226
|
+
return {
|
|
227
|
+
id,
|
|
228
|
+
status: "usable",
|
|
229
|
+
required: false,
|
|
230
|
+
version,
|
|
231
|
+
hint: `${note}want ${WANTED_PYTHON} (voice phase only)`,
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
async function pyLauncherState(run) {
|
|
235
|
+
const launcher = await run("py", ["-3.12", "--version"]);
|
|
236
|
+
if (absent(launcher))
|
|
237
|
+
return "missing";
|
|
238
|
+
if (launcher.code !== 0)
|
|
239
|
+
return "unknown";
|
|
240
|
+
if (!parseVersion(`${launcher.stdout}${launcher.stderr}`))
|
|
241
|
+
return "unknown";
|
|
242
|
+
return "ok";
|
|
243
|
+
}
|
|
244
|
+
async function venvPythonVersion(run) {
|
|
245
|
+
for (const cmd of VENV_PYS) {
|
|
246
|
+
const result = await run(cmd, ["--version"]);
|
|
247
|
+
if (result.code !== 0)
|
|
248
|
+
continue;
|
|
249
|
+
const parsed = parseVersion(`${result.stdout}${result.stderr}`);
|
|
250
|
+
if (!parsed)
|
|
251
|
+
continue;
|
|
252
|
+
const text = formatVersion(parsed);
|
|
253
|
+
return { text, usable: !text.startsWith(`${WANTED_PYTHON}.`) };
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
export const checkPythonVenv = async ({ run }) => {
|
|
258
|
+
const id = "python-venv";
|
|
259
|
+
const state = await pyLauncherState(run);
|
|
260
|
+
if (state === "missing") {
|
|
261
|
+
return {
|
|
262
|
+
id,
|
|
263
|
+
status: "missing",
|
|
264
|
+
required: false,
|
|
265
|
+
hint: "install Python 3.12 from https://www.python.org/downloads/ then py -3.12 -m venv .venv-voice (voice phase only)",
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (state === "unknown") {
|
|
269
|
+
return { id, status: "unknown", required: false, hint: "py -3.12 ran but gave no version" };
|
|
270
|
+
}
|
|
271
|
+
const venv = await venvPythonVersion(run);
|
|
272
|
+
if (!venv) {
|
|
273
|
+
return {
|
|
274
|
+
id,
|
|
275
|
+
status: "missing",
|
|
276
|
+
required: false,
|
|
277
|
+
hint: "py -3.12 -m venv .venv-voice then .venv-voice\\Scripts\\activate (voice phase only)",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (!venv.usable)
|
|
281
|
+
return found(id, venv.text);
|
|
282
|
+
return {
|
|
283
|
+
id,
|
|
284
|
+
status: "usable",
|
|
285
|
+
required: false,
|
|
286
|
+
version: venv.text,
|
|
287
|
+
hint: "venv python is not 3.12; recreate with py -3.12 -m venv .venv-voice (voice phase only)",
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
function espeakVersion(text) {
|
|
291
|
+
const parsed = parseVersion(text);
|
|
292
|
+
return parsed ? formatVersion(parsed) : undefined;
|
|
293
|
+
}
|
|
294
|
+
async function espeakBinary(run) {
|
|
295
|
+
for (const cmd of ["espeak-ng", "espeak"]) {
|
|
296
|
+
const result = await run(cmd, ["--version"]);
|
|
297
|
+
if (result.code === 0) {
|
|
298
|
+
const version = espeakVersion(`${result.stdout}${result.stderr}`);
|
|
299
|
+
if (version)
|
|
300
|
+
return { version };
|
|
301
|
+
return { unknown: `${cmd} ran but gave no version` };
|
|
302
|
+
}
|
|
303
|
+
if (!absent(result))
|
|
304
|
+
return { unknown: `${cmd} ran but failed` };
|
|
305
|
+
}
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
async function espeakEnv(host) {
|
|
309
|
+
const envPath = host.env?.PHONEMIZER_ESPEAK_LIBRARY;
|
|
310
|
+
if (!envPath)
|
|
311
|
+
return undefined;
|
|
312
|
+
let exists = false;
|
|
313
|
+
try {
|
|
314
|
+
exists = await host.exists(envPath);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
exists = false;
|
|
318
|
+
}
|
|
319
|
+
if (exists)
|
|
320
|
+
return { id: "espeak", status: "ok", required: false, version: envPath };
|
|
321
|
+
return {
|
|
322
|
+
id: "espeak",
|
|
323
|
+
status: "usable",
|
|
324
|
+
required: false,
|
|
325
|
+
version: envPath,
|
|
326
|
+
hint: `PHONEMIZER_ESPEAK_LIBRARY points at a missing file; reinstall espeak-ng from ${ESPEAK_RELEASES} and set it to libespeak-ng.dll (voice phase only)`,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
export const checkEspeak = async ({ run, host }) => {
|
|
330
|
+
const id = "espeak";
|
|
331
|
+
const binary = await espeakBinary(run);
|
|
332
|
+
if (binary) {
|
|
333
|
+
if ("version" in binary)
|
|
334
|
+
return found(id, binary.version);
|
|
335
|
+
return { id, status: "unknown", required: false, hint: binary.unknown };
|
|
336
|
+
}
|
|
337
|
+
const env = await espeakEnv(host);
|
|
338
|
+
if (env)
|
|
339
|
+
return env;
|
|
340
|
+
return {
|
|
341
|
+
id,
|
|
342
|
+
status: "missing",
|
|
343
|
+
required: false,
|
|
344
|
+
hint: `install espeak-ng from ${ESPEAK_RELEASES} (.msi) and set PHONEMIZER_ESPEAK_LIBRARY to espeak-ng.dll, e.g. setx PHONEMIZER_ESPEAK_LIBRARY "C:\\Program Files\\eSpeak NG\\libespeak-ng.dll" (voice phase only)`,
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
function pipVersion(text) {
|
|
348
|
+
const match = /Version:\s*([^\s]+)/i.exec(text);
|
|
349
|
+
return match?.[1]?.trim() || undefined;
|
|
350
|
+
}
|
|
351
|
+
async function pipShowVersion(run, cmd, pkg) {
|
|
352
|
+
const result = await run(cmd, ["-m", "pip", "show", pkg]);
|
|
353
|
+
if (result.code === 0) {
|
|
354
|
+
const version = pipVersion(`${result.stdout}\n${result.stderr}`);
|
|
355
|
+
if (version)
|
|
356
|
+
return { ok: version };
|
|
357
|
+
return { unknown: true };
|
|
358
|
+
}
|
|
359
|
+
if (absent(result))
|
|
360
|
+
return { absent: true };
|
|
361
|
+
return { missing: true };
|
|
362
|
+
}
|
|
363
|
+
async function kokoroSoundfile(run, cmd, version) {
|
|
364
|
+
const id = "kokoro";
|
|
365
|
+
const sf = await pipShowVersion(run, cmd, "soundfile");
|
|
366
|
+
if ("ok" in sf)
|
|
367
|
+
return found(id, version);
|
|
368
|
+
if ("unknown" in sf) {
|
|
369
|
+
return { id, status: "unknown", required: false, hint: "pip show soundfile gave no version" };
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
id,
|
|
373
|
+
status: "usable",
|
|
374
|
+
required: false,
|
|
375
|
+
version,
|
|
376
|
+
hint: `pip install soundfile==${SOUNDFILE_VERSION} in .venv-voice (.venv-voice\\Scripts\\activate then pip install soundfile==${SOUNDFILE_VERSION}) (voice phase only)`,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
export const checkKokoro = async ({ run }) => {
|
|
380
|
+
const id = "kokoro";
|
|
381
|
+
const installHint = `activate the venv (.venv-voice\\Scripts\\activate) then pip install kokoro==${KOKORO_VERSION} soundfile==${SOUNDFILE_VERSION} (${VOICE_LOCAL_PRIMARY}; ${VOICE_MANUAL_SETUP}; ${VOICE_NO_PAID_BACKEND}; ${VOICE_WAV_FALLBACK} when env blocked; voice phase only)`;
|
|
382
|
+
let venvFound = false;
|
|
383
|
+
for (const cmd of VENV_PYS) {
|
|
384
|
+
const kok = await pipShowVersion(run, cmd, "kokoro");
|
|
385
|
+
if ("ok" in kok)
|
|
386
|
+
return kokoroSoundfile(run, cmd, kok.ok);
|
|
387
|
+
if ("unknown" in kok) {
|
|
388
|
+
return { id, status: "unknown", required: false, hint: "pip show kokoro gave no version" };
|
|
389
|
+
}
|
|
390
|
+
if ("missing" in kok)
|
|
391
|
+
venvFound = true;
|
|
392
|
+
}
|
|
393
|
+
if (!venvFound) {
|
|
394
|
+
return {
|
|
395
|
+
id,
|
|
396
|
+
status: "missing",
|
|
397
|
+
required: false,
|
|
398
|
+
hint: "create the voice venv first: py -3.12 -m venv .venv-voice then .venv-voice\\Scripts\\activate (voice phase only)",
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return { id, status: "missing", required: false, hint: installHint };
|
|
402
|
+
};
|
|
403
|
+
export const checkFasterWhisper = async ({ run }) => {
|
|
404
|
+
const id = "faster-whisper";
|
|
405
|
+
const installHint = `activate the venv (.venv-voice\\Scripts\\activate) then pip install faster-whisper==${FASTER_WHISPER_VERSION} (${VOICE_ALIGNER}; ${VOICE_MANUAL_SETUP}; ${VOICE_NO_PAID_BACKEND}; ${VOICE_WAV_FALLBACK} when env blocked; voice phase only)`;
|
|
406
|
+
let venvFound = false;
|
|
407
|
+
for (const cmd of VENV_PYS) {
|
|
408
|
+
const result = await run(cmd, ["-m", "pip", "show", "faster-whisper"]);
|
|
409
|
+
if (result.code === 0) {
|
|
410
|
+
const version = pipVersion(`${result.stdout}\n${result.stderr}`);
|
|
411
|
+
if (!version) {
|
|
412
|
+
return {
|
|
413
|
+
id,
|
|
414
|
+
status: "unknown",
|
|
415
|
+
required: false,
|
|
416
|
+
hint: "pip show faster-whisper gave no version",
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
return found(id, version);
|
|
420
|
+
}
|
|
421
|
+
if (!absent(result))
|
|
422
|
+
venvFound = true;
|
|
423
|
+
}
|
|
424
|
+
if (!venvFound) {
|
|
425
|
+
return {
|
|
426
|
+
id,
|
|
427
|
+
status: "missing",
|
|
428
|
+
required: false,
|
|
429
|
+
hint: "create the voice venv first: py -3.12 -m venv .venv-voice then .venv-voice\\Scripts\\activate (voice phase only)",
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return { id, status: "missing", required: false, hint: installHint };
|
|
433
|
+
};
|
|
434
|
+
export const checkFonts = async ({ vendored }) => {
|
|
435
|
+
const id = "fonts";
|
|
436
|
+
const missing = vendored.missingFonts();
|
|
437
|
+
if (missing.length === 0)
|
|
438
|
+
return { id, status: "ok", required: false };
|
|
439
|
+
return {
|
|
440
|
+
id,
|
|
441
|
+
status: "missing",
|
|
442
|
+
required: false,
|
|
443
|
+
version: missing.join(","),
|
|
444
|
+
hint: "reinstall @anvpx/motion (it vendors @fontsource/*)",
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
export const checkIcons = async ({ host, vendored }) => {
|
|
448
|
+
const id = "icons";
|
|
449
|
+
if (await host.exists(vendored.iconsFile))
|
|
450
|
+
return { id, status: "ok", required: false };
|
|
451
|
+
return {
|
|
452
|
+
id,
|
|
453
|
+
status: "missing",
|
|
454
|
+
required: false,
|
|
455
|
+
hint: "reinstall @anvpx/motion (it stages icons/icons.json)",
|
|
456
|
+
};
|
|
457
|
+
};
|
|
458
|
+
export const checkTemplates = async ({ host }) => {
|
|
459
|
+
const id = "templates";
|
|
460
|
+
try {
|
|
461
|
+
const stopwordsFile = join(dirname(TEMPLATES_DIR), "stopwords.json");
|
|
462
|
+
const missing = [];
|
|
463
|
+
if (!(await host.exists(TEMPLATES_DIR).catch(() => false)))
|
|
464
|
+
missing.push("templates");
|
|
465
|
+
if (!(await host.exists(SHARED_THEMES_DIR).catch(() => false)))
|
|
466
|
+
missing.push("shared-themes");
|
|
467
|
+
if (!(await host.exists(stopwordsFile).catch(() => false)))
|
|
468
|
+
missing.push("stopwords");
|
|
469
|
+
if (missing.length === 0)
|
|
470
|
+
return { id, status: "ok", required: false };
|
|
471
|
+
return {
|
|
472
|
+
id,
|
|
473
|
+
status: "missing",
|
|
474
|
+
required: false,
|
|
475
|
+
version: missing.join(","),
|
|
476
|
+
hint: "reinstall @anvpx/motion (it stages templates, shared themes and stopwords)",
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return {
|
|
481
|
+
id,
|
|
482
|
+
status: "missing",
|
|
483
|
+
required: false,
|
|
484
|
+
hint: "reinstall @anvpx/motion (it stages templates, shared themes and stopwords)",
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
function resolveCliMain() {
|
|
489
|
+
try {
|
|
490
|
+
const entry = nodeRequire.resolve("@anvpx/motion-cli");
|
|
491
|
+
return join(dirname(entry), "main.js");
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
return fileURLToPath(new URL("../../dist/main.js", import.meta.url));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function resolveCoreMain() {
|
|
498
|
+
try {
|
|
499
|
+
const entry = nodeRequire.resolve("@anvpx/motion");
|
|
500
|
+
return join(dirname(entry), "main.js");
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return fileURLToPath(new URL("../../../core/dist/main.js", import.meta.url));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
export const checkBin = async ({ host }) => {
|
|
507
|
+
const id = "bin";
|
|
508
|
+
const cliMain = resolveCliMain();
|
|
509
|
+
const coreMain = resolveCoreMain();
|
|
510
|
+
const cliOk = await host.exists(cliMain).catch(() => false);
|
|
511
|
+
const coreOk = await host.exists(coreMain).catch(() => false);
|
|
512
|
+
if (cliOk && coreOk)
|
|
513
|
+
return { id, status: "ok", required: false };
|
|
514
|
+
const missing = [!cliOk ? "cli" : undefined, !coreOk ? "core" : undefined]
|
|
515
|
+
.filter((part) => part !== undefined)
|
|
516
|
+
.join(",");
|
|
517
|
+
return {
|
|
518
|
+
id,
|
|
519
|
+
status: "missing",
|
|
520
|
+
required: false,
|
|
521
|
+
version: missing,
|
|
522
|
+
hint: "reinstall @anvpx/motion (it ships the mk bin: core dist/main.js delegating to cli dist/main.js)",
|
|
523
|
+
};
|
|
524
|
+
};
|
|
525
|
+
function resolveStagedCatalog() {
|
|
526
|
+
try {
|
|
527
|
+
const entry = nodeRequire.resolve("@anvpx/motion");
|
|
528
|
+
return join(dirname(entry), "..", "CATALOG.md");
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
return fileURLToPath(new URL("../../../core/CATALOG.md", import.meta.url));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
export const checkCatalog = async ({ host }) => {
|
|
535
|
+
const id = "catalog";
|
|
536
|
+
const staged = resolveStagedCatalog();
|
|
537
|
+
if (await host.exists(staged).catch(() => false)) {
|
|
538
|
+
return { id, status: "ok", required: false, version: "staged" };
|
|
539
|
+
}
|
|
540
|
+
const cwdFallback = join(process.cwd(), "CATALOG.md");
|
|
541
|
+
if (await host.exists(cwdFallback).catch(() => false)) {
|
|
542
|
+
return { id, status: "ok", required: false, version: "cwd" };
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
545
|
+
id,
|
|
546
|
+
status: "missing",
|
|
547
|
+
required: false,
|
|
548
|
+
version: "CATALOG.md",
|
|
549
|
+
hint: "reinstall @anvpx/motion to restore staged CATALOG.md, or run mk catalog --write in this directory (cwd fallback: ./CATALOG.md)",
|
|
550
|
+
};
|
|
551
|
+
};
|
|
552
|
+
export const CHECKS = [
|
|
553
|
+
checkNode,
|
|
554
|
+
checkPnpm,
|
|
555
|
+
checkFfmpeg,
|
|
556
|
+
checkFfprobe,
|
|
557
|
+
checkHyperframes,
|
|
558
|
+
checkChrome,
|
|
559
|
+
checkGpu,
|
|
560
|
+
checkPython,
|
|
561
|
+
checkPythonVenv,
|
|
562
|
+
checkEspeak,
|
|
563
|
+
checkKokoro,
|
|
564
|
+
checkFasterWhisper,
|
|
565
|
+
checkFonts,
|
|
566
|
+
checkIcons,
|
|
567
|
+
checkTemplates,
|
|
568
|
+
checkBin,
|
|
569
|
+
checkCatalog,
|
|
570
|
+
checkSkills,
|
|
571
|
+
];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { firstSentence } from "../args.js";
|
|
3
|
+
import { EXIT } from "../exit-codes.js";
|
|
4
|
+
import { CHECKS } from "./checks.js";
|
|
5
|
+
import { exitCodeFor, formatJson, formatTable } from "./format.js";
|
|
6
|
+
import { formatHealLines, formatJsonWithHeal, healSafeRows } from "./heal.js";
|
|
7
|
+
import { hasPins, readPins } from "./pins.js";
|
|
8
|
+
import { defaultVendored } from "./vendored.js";
|
|
9
|
+
const USAGE = `mk doctor [--json] [--fix]
|
|
10
|
+
|
|
11
|
+
Install is the npm package install plus this check (plugin install stays separate):
|
|
12
|
+
npm install @anvpx/motion (or npx -y @anvpx/motion@<version>) then mk doctor.
|
|
13
|
+
mk plugin install --out <dir> is a separate explicit step, never part of npm install.
|
|
14
|
+
Check the local toolchain; each tool is ok | usable | missing | unknown.
|
|
15
|
+
Exit 0 when no required tool (node, pnpm, ffmpeg, ffprobe) is missing; 127 when one is;
|
|
16
|
+
2 when a required check could not be decided. chrome is optional: the first render
|
|
17
|
+
downloads it. python is optional (voice phase); fonts and icons (vendored files) report
|
|
18
|
+
ok or missing and never fail the exit code.
|
|
19
|
+
Voice rows (python, python-venv, espeak, kokoro, faster-whisper) are hint-only and
|
|
20
|
+
never auto-install system/pip deps; --fix leaves them untouched.
|
|
21
|
+
--json one line: [{id, status, version?, hint?}]
|
|
22
|
+
--fix safe autoheal only (hyperframes note, fonts/icons/templates/bin/catalog hint,
|
|
23
|
+
chrome: npx hyperframes browser ensure); prints healed/still-missing;
|
|
24
|
+
with --json prints {results, healed: [{id, healed, detail}]}.
|
|
25
|
+
Never deletes, never installs; exit codes unchanged.
|
|
26
|
+
`;
|
|
27
|
+
async function runChecks(io, vendored) {
|
|
28
|
+
const manifest = await io.host.readRootPackageJson();
|
|
29
|
+
const pins = readPins(manifest);
|
|
30
|
+
const ctx = {
|
|
31
|
+
run: io.run,
|
|
32
|
+
host: io.host,
|
|
33
|
+
pins,
|
|
34
|
+
pinsPresent: hasPins(manifest),
|
|
35
|
+
vendored,
|
|
36
|
+
};
|
|
37
|
+
return Promise.all(CHECKS.map((check) => check(ctx)));
|
|
38
|
+
}
|
|
39
|
+
function parseDoctorArgs(args) {
|
|
40
|
+
try {
|
|
41
|
+
const flags = parseArgs({
|
|
42
|
+
args: [...args],
|
|
43
|
+
options: {
|
|
44
|
+
json: { type: "boolean" },
|
|
45
|
+
fix: { type: "boolean" },
|
|
46
|
+
help: { type: "boolean", short: "h" },
|
|
47
|
+
},
|
|
48
|
+
allowPositionals: false,
|
|
49
|
+
}).values;
|
|
50
|
+
if (flags.help)
|
|
51
|
+
return { help: true };
|
|
52
|
+
return { flags };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return { error: firstSentence(error.message) };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function emitResults(results, fix, json, io) {
|
|
59
|
+
if (!fix) {
|
|
60
|
+
io.stdout(json ? formatJson(results) : formatTable(results));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const healed = await healSafeRows(results, io);
|
|
64
|
+
if (json) {
|
|
65
|
+
io.stdout(formatJsonWithHeal(results, healed));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
io.stdout(`${formatTable(results)}${formatHealLines(healed)}`);
|
|
69
|
+
}
|
|
70
|
+
export const makeDoctorCommand = (vendored = defaultVendored) => ({
|
|
71
|
+
name: "doctor",
|
|
72
|
+
summary: "check node, pnpm, ffmpeg, ffprobe, hyperframes, chrome, gpu, python, fonts, icons",
|
|
73
|
+
async run(args, io) {
|
|
74
|
+
const parsed = parseDoctorArgs(args);
|
|
75
|
+
if ("error" in parsed) {
|
|
76
|
+
io.stderr(`mk doctor: ${parsed.error}\n`);
|
|
77
|
+
return EXIT.USAGE;
|
|
78
|
+
}
|
|
79
|
+
if ("help" in parsed) {
|
|
80
|
+
io.stdout(USAGE);
|
|
81
|
+
return EXIT.OK;
|
|
82
|
+
}
|
|
83
|
+
const results = await runChecks(io, vendored());
|
|
84
|
+
await emitResults(results, parsed.flags.fix === true, parsed.flags.json === true, io);
|
|
85
|
+
return exitCodeFor(results);
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
export const doctorCommand = makeDoctorCommand();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CheckResult, CheckStatus } from "./types.ts";
|
|
2
|
+
export declare function summarize(results: readonly CheckResult[]): Record<CheckStatus, number>;
|
|
3
|
+
/** 127 when a required tool is missing, else 2 when a required check is unknown, else 0. */
|
|
4
|
+
export declare function exitCodeFor(results: readonly CheckResult[]): number;
|
|
5
|
+
export declare function formatTable(results: readonly CheckResult[]): string;
|
|
6
|
+
/** One compact line: [{id, status, version?, hint?}]. */
|
|
7
|
+
export declare function formatJson(results: readonly CheckResult[]): string;
|