bytecut 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.
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ffmpegDirOf,
4
+ installHint,
5
+ pickAv1Encoder,
6
+ probeFfmpeg,
7
+ probeFfprobeVersion,
8
+ resolveBinary
9
+ } from "./chunk-3PF6BBFK.js";
10
+
11
+ // src/core/doctor/report.ts
12
+ var MIN_NODE_MAJOR = 22;
13
+ function message(error) {
14
+ return error instanceof Error ? error.message : String(error);
15
+ }
16
+ async function collectDoctorReport(deps, options = {}) {
17
+ const major = Number.parseInt(deps.nodeVersion.replace(/^v/, ""), 10);
18
+ const node = { version: deps.nodeVersion, supported: major >= MIN_NODE_MAJOR };
19
+ let sharp;
20
+ try {
21
+ sharp = { available: true, ...await deps.loadSharp() };
22
+ } catch (error) {
23
+ sharp = { available: false, error: message(error) };
24
+ }
25
+ const base = { env: deps.env, platform: deps.platform };
26
+ const ffmpegResolved = resolveBinary(
27
+ "ffmpeg",
28
+ { ...base, flag: options.ffmpegFlag, config: options.ffmpegConfig },
29
+ deps
30
+ );
31
+ const ffprobeResolved = resolveBinary(
32
+ "ffprobe",
33
+ {
34
+ ...base,
35
+ flag: options.ffprobeFlag,
36
+ config: options.ffprobeConfig,
37
+ ffmpegDir: ffmpegDirOf(ffmpegResolved)
38
+ },
39
+ deps
40
+ );
41
+ const ffmpeg = { resolved: ffmpegResolved };
42
+ if (ffmpegResolved.found) {
43
+ try {
44
+ ffmpeg.info = await probeFfmpeg(ffmpegResolved.path, deps.run);
45
+ ffmpeg.version = ffmpeg.info.version;
46
+ } catch (error) {
47
+ ffmpeg.error = message(error);
48
+ }
49
+ }
50
+ const ffprobe = { resolved: ffprobeResolved };
51
+ if (ffprobeResolved.found) {
52
+ try {
53
+ ffprobe.version = await probeFfprobeVersion(ffprobeResolved.path, deps.run);
54
+ } catch (error) {
55
+ ffprobe.error = message(error);
56
+ }
57
+ }
58
+ const scorerResolved = resolveBinary(
59
+ "ssimulacra2",
60
+ {
61
+ env: deps.env,
62
+ platform: deps.platform,
63
+ flag: options.ssimulacra2Flag,
64
+ config: options.ssimulacra2Config
65
+ },
66
+ deps
67
+ );
68
+ const caps = ffmpeg.info?.capabilities;
69
+ const encoders = {
70
+ av1: caps ? pickAv1Encoder(caps) : null,
71
+ h264: caps?.libx264 ?? false,
72
+ vp9: caps?.libvpxVp9 ?? false,
73
+ vmaf: caps?.libvmaf ?? false
74
+ };
75
+ const degradations = [];
76
+ const hint = installHint(deps.platform, deps.osReleaseId);
77
+ if (!ffmpegResolved.found) {
78
+ degradations.push(
79
+ ffmpegResolved.attempted ? `ffmpeg not found at ${ffmpegResolved.attempted} (from ${sourceLabel(ffmpegResolved.source)}). Videos and GIFs will be marked "needs ffmpeg"; images are unaffected.` : `ffmpeg not found. Videos and GIFs will be marked "needs ffmpeg"; images are unaffected. Install it with: ${hint}`
80
+ );
81
+ } else if (ffmpeg.error) {
82
+ degradations.push(
83
+ `ffmpeg at ${ffmpegResolved.path} could not be run (${ffmpeg.error}). Videos and GIFs will be marked "needs ffmpeg".`
84
+ );
85
+ } else {
86
+ if (!ffprobeResolved.found || ffprobe.error) {
87
+ degradations.push(
88
+ `ffprobe is not usable. Videos cannot be probed, so they will be marked "needs ffmpeg". It normally ships alongside ffmpeg; install it with: ${hint}`
89
+ );
90
+ }
91
+ if (!encoders.av1) {
92
+ degradations.push(
93
+ "No AV1 encoder (libsvtav1 / libaom-av1). AV1 outputs will be skipped; H.264 and poster are still produced."
94
+ );
95
+ }
96
+ if (!encoders.h264) {
97
+ degradations.push("libx264 is missing. H.264 MP4 fallbacks cannot be produced.");
98
+ }
99
+ if (!encoders.vmaf) {
100
+ degradations.push(
101
+ "libvmaf is missing. Fixed quality settings from the preset target will be used; the VMAF search is skipped."
102
+ );
103
+ }
104
+ }
105
+ return {
106
+ node,
107
+ sharp,
108
+ ffmpeg,
109
+ ffprobe,
110
+ scorer: { kind: scorerResolved.found ? "native" : "wasm", resolved: scorerResolved },
111
+ encoders,
112
+ degradations,
113
+ ok: node.supported && sharp.available
114
+ };
115
+ }
116
+ function sourceLabel(source) {
117
+ switch (source) {
118
+ case "flag":
119
+ return "flag";
120
+ case "env":
121
+ return "environment variable";
122
+ case "config":
123
+ return "config";
124
+ case "ffmpeg-dir":
125
+ return "ffmpeg's directory";
126
+ case "path":
127
+ return "PATH";
128
+ default:
129
+ return "not found";
130
+ }
131
+ }
132
+ function binaryLine(label, report) {
133
+ const { resolved } = report;
134
+ if (!resolved.found) {
135
+ return resolved.attempted ? `${label}: not found at ${resolved.attempted} (${sourceLabel(resolved.source)})` : `${label}: not found`;
136
+ }
137
+ const detail = report.error ? `failed to run: ${report.error}` : `version ${report.version}`;
138
+ return `${label}: ${resolved.path} (${sourceLabel(resolved.source)}), ${detail}`;
139
+ }
140
+ var yesNo = (value) => value ? "yes" : "no";
141
+ function formatDoctorReport(report) {
142
+ const lines = ["bytecut doctor", ""];
143
+ lines.push(
144
+ `Node: ${report.node.version}${report.node.supported ? "" : ` (needs >= ${MIN_NODE_MAJOR})`}`
145
+ );
146
+ lines.push(
147
+ report.sharp.available ? `sharp: ${report.sharp.version} (libvips ${report.sharp.libvips})` : `sharp: unavailable (${report.sharp.error})`
148
+ );
149
+ lines.push(binaryLine("ffmpeg ", report.ffmpeg));
150
+ lines.push(binaryLine("ffprobe", report.ffprobe));
151
+ {
152
+ const { kind, resolved } = report.scorer;
153
+ lines.push(
154
+ kind === "native" && resolved.found ? `scorer: native ssimulacra2 at ${resolved.path} (${sourceLabel(resolved.source)})` : resolved.found === false && resolved.attempted ? `scorer: built-in WebAssembly (no ssimulacra2 at ${resolved.attempted}, from ${sourceLabel(resolved.source)})` : "scorer: built-in WebAssembly SSIMULACRA2 (no native ssimulacra2 needed)"
155
+ );
156
+ }
157
+ if (report.ffmpeg.info) {
158
+ lines.push("", "Encoders:");
159
+ lines.push(` AV1: ${report.encoders.av1 ?? "none"}`);
160
+ lines.push(` H.264: ${yesNo(report.encoders.h264)} (libx264)`);
161
+ lines.push(` VP9: ${yesNo(report.encoders.vp9)} (libvpx-vp9)`);
162
+ lines.push(` libvmaf: ${yesNo(report.encoders.vmaf)}`);
163
+ }
164
+ if (report.project) {
165
+ const p = report.project;
166
+ lines.push("", `Project: ${p.framework} \u2192 ${p.outDir}`);
167
+ lines.push(`Config: ${p.configFile ?? "none (zero-config)"}`);
168
+ if (p.configError) lines.push(` problem: ${p.configError}`);
169
+ if (p.outDirIgnored) {
170
+ lines.push(
171
+ ` note: ${p.outDir} is matched by a .gitignore. bytecut's outputs are meant to be committed, so deploys need no encoding step. Nothing was changed.`
172
+ );
173
+ }
174
+ }
175
+ lines.push("");
176
+ if (report.degradations.length === 0) {
177
+ lines.push("Everything is available. No degradation.");
178
+ } else {
179
+ lines.push("Degraded:");
180
+ for (const item of report.degradations) lines.push(` - ${item}`);
181
+ }
182
+ return lines.join("\n");
183
+ }
184
+
185
+ export {
186
+ collectDoctorReport,
187
+ formatDoctorReport
188
+ };