pro-visu 0.3.1 → 0.5.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/README.md +22 -14
- package/dist/cli/index.js +951 -402
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +47 -30
- package/dist/index.js.map +1 -1
- package/dist/scene-app/assets/{index-sed_MW8w.js → index-Bu-Vjjhs.js} +1 -1
- package/dist/scene-app/index.html +1 -1
- package/package.json +1 -2
package/dist/cli/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { cac } from "cac";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
|
-
var TOOL_VERSION = true ? "0.
|
|
7
|
+
var TOOL_VERSION = true ? "0.5.0" : "0.0.0-dev";
|
|
8
8
|
|
|
9
9
|
// src/cli/update-check.ts
|
|
10
10
|
import updateNotifier from "update-notifier";
|
|
@@ -31,6 +31,7 @@ function checkForUpdates(version = TOOL_VERSION) {
|
|
|
31
31
|
import path17 from "path";
|
|
32
32
|
import { existsSync as existsSync4, readFileSync } from "fs";
|
|
33
33
|
import { readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
|
|
34
|
+
import { createRequire as createRequire2 } from "module";
|
|
34
35
|
|
|
35
36
|
// src/utils/paths.ts
|
|
36
37
|
import path from "path";
|
|
@@ -155,7 +156,11 @@ async function ensureChromium(opts) {
|
|
|
155
156
|
child.on("error", reject);
|
|
156
157
|
child.on(
|
|
157
158
|
"close",
|
|
158
|
-
(code) => code === 0 ? resolve() : reject(
|
|
159
|
+
(code) => code === 0 ? resolve() : reject(
|
|
160
|
+
new Error(
|
|
161
|
+
`Chromium install failed (exit code ${code}). If you're offline or behind a proxy, set HTTPS_PROXY, or point PLAYWRIGHT_DOWNLOAD_HOST at a mirror.`
|
|
162
|
+
)
|
|
163
|
+
)
|
|
159
164
|
);
|
|
160
165
|
});
|
|
161
166
|
opts.logger.success("Chromium installed.");
|
|
@@ -163,17 +168,111 @@ async function ensureChromium(opts) {
|
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
// src/media/ensure-ffmpeg.ts
|
|
166
|
-
import path5 from "path";
|
|
167
171
|
import { existsSync as existsSync2 } from "fs";
|
|
168
|
-
import { rm as rm3 } from "fs/promises";
|
|
169
172
|
import { spawn as spawn3 } from "child_process";
|
|
170
|
-
import { createRequire as createRequire2 } from "module";
|
|
171
173
|
|
|
172
174
|
// src/media/ffmpeg.ts
|
|
173
|
-
import
|
|
175
|
+
import path5 from "path";
|
|
174
176
|
import { spawn as spawn2 } from "child_process";
|
|
175
|
-
import { rm as
|
|
176
|
-
|
|
177
|
+
import { rm as rm3, writeFile as writeFile2 } from "fs/promises";
|
|
178
|
+
|
|
179
|
+
// src/media/ffmpeg-binary.ts
|
|
180
|
+
import os from "os";
|
|
181
|
+
import path4 from "path";
|
|
182
|
+
import https from "https";
|
|
183
|
+
import { createWriteStream } from "fs";
|
|
184
|
+
import { mkdir as mkdir2, rename, rm as rm2, chmod } from "fs/promises";
|
|
185
|
+
import { createGunzip } from "zlib";
|
|
186
|
+
import { pipeline } from "stream/promises";
|
|
187
|
+
var FFMPEG_RELEASE = process.env.FFMPEG_BINARY_RELEASE || "b6.1.1";
|
|
188
|
+
var BINARIES_URL = process.env.FFMPEG_BINARIES_URL || "https://github.com/eugeneware/ffmpeg-static/releases/download";
|
|
189
|
+
var SUPPORTED = {
|
|
190
|
+
darwin: ["x64", "arm64"],
|
|
191
|
+
linux: ["x64", "ia32", "arm64", "arm"],
|
|
192
|
+
win32: ["x64", "ia32"],
|
|
193
|
+
freebsd: ["x64"]
|
|
194
|
+
};
|
|
195
|
+
function ffmpegIsSupported() {
|
|
196
|
+
return (SUPPORTED[process.platform] ?? []).includes(process.arch);
|
|
197
|
+
}
|
|
198
|
+
function ffmpegCacheDir() {
|
|
199
|
+
const base = process.env.PROVISU_FFMPEG_DIR || path4.join(os.homedir(), ".cache", "pro-visu", "ffmpeg");
|
|
200
|
+
return path4.join(base, FFMPEG_RELEASE);
|
|
201
|
+
}
|
|
202
|
+
function ffmpegCachedBinary() {
|
|
203
|
+
return path4.join(ffmpegCacheDir(), process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg");
|
|
204
|
+
}
|
|
205
|
+
function ffmpegBinaryPath() {
|
|
206
|
+
return process.env.FFMPEG_BIN || ffmpegCachedBinary();
|
|
207
|
+
}
|
|
208
|
+
function ffmpegDownloadUrl() {
|
|
209
|
+
if (!ffmpegIsSupported()) return null;
|
|
210
|
+
return `${BINARIES_URL}/${FFMPEG_RELEASE}/ffmpeg-${process.platform}-${process.arch}.gz`;
|
|
211
|
+
}
|
|
212
|
+
function getFollowing(url, redirects = 5) {
|
|
213
|
+
return new Promise((resolve, reject) => {
|
|
214
|
+
const req = https.get(url, { headers: { "User-Agent": "pro-visu" } }, (res) => {
|
|
215
|
+
const status = res.statusCode ?? 0;
|
|
216
|
+
if (status >= 300 && status < 400 && res.headers.location) {
|
|
217
|
+
res.resume();
|
|
218
|
+
if (redirects <= 0) return reject(new Error("Too many redirects fetching ffmpeg."));
|
|
219
|
+
resolve(getFollowing(new URL(res.headers.location, url).toString(), redirects - 1));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (status !== 200) {
|
|
223
|
+
res.resume();
|
|
224
|
+
reject(new Error(`ffmpeg download failed: HTTP ${status} for ${url}`));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
resolve(res);
|
|
228
|
+
});
|
|
229
|
+
req.on("error", reject);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
var NETWORK_CODES = /* @__PURE__ */ new Set(["ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT"]);
|
|
233
|
+
function withNetworkHint(err) {
|
|
234
|
+
const code = err.code;
|
|
235
|
+
if (code && NETWORK_CODES.has(code)) {
|
|
236
|
+
return new Error(
|
|
237
|
+
`${err.message} \u2014 you appear to be offline or behind a proxy. Set FFMPEG_BINARIES_URL to a reachable mirror, or FFMPEG_BIN to a local ffmpeg binary.`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return err;
|
|
241
|
+
}
|
|
242
|
+
async function downloadFfmpeg(onProgress) {
|
|
243
|
+
const url = ffmpegDownloadUrl();
|
|
244
|
+
if (!url) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`No prebuilt ffmpeg for ${process.platform}/${process.arch}. Set FFMPEG_BIN to a local ffmpeg binary.`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const dest = ffmpegCachedBinary();
|
|
250
|
+
const tmp = `${dest}.download`;
|
|
251
|
+
await mkdir2(path4.dirname(dest), { recursive: true });
|
|
252
|
+
await rm2(tmp, { force: true });
|
|
253
|
+
try {
|
|
254
|
+
const res = await getFollowing(url);
|
|
255
|
+
if (onProgress) {
|
|
256
|
+
const totalHeader = Number(res.headers["content-length"]);
|
|
257
|
+
const total = Number.isFinite(totalHeader) && totalHeader > 0 ? totalHeader : void 0;
|
|
258
|
+
let downloaded = 0;
|
|
259
|
+
res.on("data", (chunk) => {
|
|
260
|
+
downloaded += chunk.length;
|
|
261
|
+
onProgress(downloaded, total);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
await pipeline(res, createGunzip(), createWriteStream(tmp));
|
|
265
|
+
} catch (err) {
|
|
266
|
+
throw withNetworkHint(err);
|
|
267
|
+
}
|
|
268
|
+
await chmod(tmp, 493).catch(() => {
|
|
269
|
+
});
|
|
270
|
+
await rm2(dest, { force: true });
|
|
271
|
+
await rename(tmp, dest);
|
|
272
|
+
return dest;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/media/ffmpeg.ts
|
|
177
276
|
var SCALE_COLOR = "out_color_matrix=bt709:out_range=tv";
|
|
178
277
|
var COLOR_TAGS = [
|
|
179
278
|
"-colorspace",
|
|
@@ -186,11 +285,7 @@ var COLOR_TAGS = [
|
|
|
186
285
|
"tv"
|
|
187
286
|
];
|
|
188
287
|
function ffmpegPath() {
|
|
189
|
-
|
|
190
|
-
if (!p) {
|
|
191
|
-
throw new Error("ffmpeg-static did not provide a binary for this platform.");
|
|
192
|
-
}
|
|
193
|
-
return p;
|
|
288
|
+
return ffmpegBinaryPath();
|
|
194
289
|
}
|
|
195
290
|
function buildTranscodeArgs(args) {
|
|
196
291
|
const seek = args.startOffsetSeconds && args.startOffsetSeconds > 0 ? ["-ss", args.startOffsetSeconds.toFixed(3)] : [];
|
|
@@ -332,7 +427,7 @@ function buildConcatArgs(listFile, outPath) {
|
|
|
332
427
|
];
|
|
333
428
|
}
|
|
334
429
|
async function concatMp4(segments, outPath, logger, signal) {
|
|
335
|
-
await ensureDir(
|
|
430
|
+
await ensureDir(path5.dirname(outPath));
|
|
336
431
|
const listFile = `${outPath}.concat.txt`;
|
|
337
432
|
const list = segments.map((s) => `file '${s.replace(/\\/g, "/")}'`).join("\n");
|
|
338
433
|
await writeFile2(listFile, `${list}
|
|
@@ -340,7 +435,7 @@ async function concatMp4(segments, outPath, logger, signal) {
|
|
|
340
435
|
try {
|
|
341
436
|
await runFfmpeg(buildConcatArgs(listFile, outPath), logger, signal);
|
|
342
437
|
} finally {
|
|
343
|
-
await
|
|
438
|
+
await rm3(listFile, { force: true });
|
|
344
439
|
}
|
|
345
440
|
}
|
|
346
441
|
async function runFfmpeg(argv, logger, signal) {
|
|
@@ -362,7 +457,7 @@ ${stderr.slice(-2e3)}`));
|
|
|
362
457
|
});
|
|
363
458
|
}
|
|
364
459
|
async function transcodeToMp4(args) {
|
|
365
|
-
await ensureDir(
|
|
460
|
+
await ensureDir(path5.dirname(args.outputPath));
|
|
366
461
|
await runFfmpeg(buildTranscodeArgs(args), args.logger, args.signal);
|
|
367
462
|
}
|
|
368
463
|
function aspectTarget(aspect) {
|
|
@@ -467,7 +562,6 @@ function buildStillSegmentArgs(a) {
|
|
|
467
562
|
}
|
|
468
563
|
|
|
469
564
|
// src/media/ensure-ffmpeg.ts
|
|
470
|
-
var require3 = createRequire2(import.meta.url);
|
|
471
565
|
async function ffmpegWorks() {
|
|
472
566
|
let bin;
|
|
473
567
|
try {
|
|
@@ -488,42 +582,35 @@ async function ffmpegWorks() {
|
|
|
488
582
|
child.on("close", (code) => resolve(code === 0));
|
|
489
583
|
});
|
|
490
584
|
}
|
|
491
|
-
function resolveInstallScript() {
|
|
492
|
-
try {
|
|
493
|
-
return require3.resolve("ffmpeg-static/install.js");
|
|
494
|
-
} catch {
|
|
495
|
-
try {
|
|
496
|
-
const pkg = require3.resolve("ffmpeg-static/package.json");
|
|
497
|
-
return path5.join(path5.dirname(pkg), "install.js");
|
|
498
|
-
} catch {
|
|
499
|
-
return null;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
585
|
async function ensureFfmpeg(opts) {
|
|
504
586
|
if (await ffmpegWorks()) return true;
|
|
505
587
|
if (opts.checkOnly) return false;
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
588
|
+
if (process.env.FFMPEG_BIN) {
|
|
589
|
+
opts.logger.error(`FFMPEG_BIN is set to "${process.env.FFMPEG_BIN}" but that binary won't run.`);
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
if (!ffmpegIsSupported()) {
|
|
593
|
+
opts.logger.error(
|
|
594
|
+
`No prebuilt ffmpeg for ${process.platform}/${process.arch}. Set FFMPEG_BIN to a local ffmpeg binary.`
|
|
595
|
+
);
|
|
509
596
|
return false;
|
|
510
597
|
}
|
|
511
598
|
opts.logger.info("Fetching ffmpeg (one-time, ~80 MB)\u2026");
|
|
512
599
|
try {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
600
|
+
let lastQuartile = 0;
|
|
601
|
+
await downloadFfmpeg((downloaded, total) => {
|
|
602
|
+
if (!total) return;
|
|
603
|
+
const quartile = Math.floor(downloaded / total * 4);
|
|
604
|
+
if (quartile > lastQuartile && quartile < 4) {
|
|
605
|
+
lastQuartile = quartile;
|
|
606
|
+
const mb = (n) => (n / 1024 / 1024).toFixed(0);
|
|
607
|
+
opts.logger.info(`ffmpeg download: ${quartile * 25}% (${mb(downloaded)}/${mb(total)} MB)`);
|
|
608
|
+
}
|
|
520
609
|
});
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
);
|
|
526
|
-
});
|
|
610
|
+
} catch (err) {
|
|
611
|
+
opts.logger.error(`ffmpeg download failed: ${err.message}`);
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
527
614
|
if (!await ffmpegWorks()) {
|
|
528
615
|
opts.logger.error("ffmpeg was downloaded but still won't run on this platform.");
|
|
529
616
|
return false;
|
|
@@ -578,6 +665,16 @@ var serverSettingsSchema = z.object({
|
|
|
578
665
|
/** If a server is already reachable at the URL, use it as-is (don't start or stop one). */
|
|
579
666
|
reuseExisting: z.boolean().default(true).describe("If a server is already reachable at the URL, use it as-is (don't start or stop one). Default true.")
|
|
580
667
|
}).strict();
|
|
668
|
+
var captureSettingsSchema = z.object({
|
|
669
|
+
/** Query params appended to every URL-based asset (e.g. `{ capture: "1" }` → `?capture=1`). */
|
|
670
|
+
query: z.record(z.string(), z.string()).optional().describe('Query params appended to every URL-based asset, e.g. { capture: "1" }.'),
|
|
671
|
+
/** Cookies set on every capture context before navigation, scoped to the asset's origin. */
|
|
672
|
+
cookies: z.array(z.object({ name: z.string().min(1), value: z.string() }).strict()).optional().describe("Cookies set on every capture context before navigation (scoped to the asset's origin)."),
|
|
673
|
+
/** localStorage entries seeded before the page's own scripts run. */
|
|
674
|
+
localStorage: z.record(z.string(), z.string()).optional().describe("localStorage entries seeded (per origin) before the page's own scripts run."),
|
|
675
|
+
/** JS source run in every page before its own scripts — e.g. set a global capture flag. */
|
|
676
|
+
initScript: z.string().optional().describe("JS run in every page before its own scripts (e.g. `window.__PV_CAPTURE__ = true`).")
|
|
677
|
+
}).strict();
|
|
581
678
|
var settingsSchema = z.object({
|
|
582
679
|
/** Output directory, relative to the repo root. */
|
|
583
680
|
outDir: z.string().min(1).default("pro-visu").describe('Output directory for generated assets, relative to the repo root (default "pro-visu").'),
|
|
@@ -599,17 +696,19 @@ var settingsSchema = z.object({
|
|
|
599
696
|
defaults: z.record(z.string(), z.record(z.string(), z.unknown())).default({}).describe("Per-generator option defaults, keyed by generator id, merged underneath each asset's own options."),
|
|
600
697
|
/** Optional managed dev/prod server lifecycle (build → start → wait → … → stop). */
|
|
601
698
|
server: serverSettingsSchema.optional().describe("Build \u2192 start \u2192 wait \u2192 capture \u2192 stop a server automatically."),
|
|
699
|
+
/** "Capture mode" toggles (query/cookies/localStorage/init script) applied to every URL capture. */
|
|
700
|
+
capture: captureSettingsSchema.optional().describe("Capture-mode toggles applied to every URL-based asset (e.g. disable animations / hide the cookie banner)."),
|
|
602
701
|
/** Render quality. "draft" lowers fps/scale and speeds the encoder for fast iteration. */
|
|
603
702
|
quality: z.enum(["draft", "final"]).default("final").describe('Render quality; "draft" lowers fps/scale and speeds the encoder for fast iteration (default "final").'),
|
|
604
703
|
/** Skip assets whose inputs+options+tool fingerprint is unchanged (opt-in; can be stale). */
|
|
605
704
|
cache: z.boolean().default(false).describe("Skip assets whose inputs+options+tool fingerprint is unchanged (opt-in; can be stale). Default false.")
|
|
606
|
-
});
|
|
705
|
+
}).strict();
|
|
607
706
|
var assetSpecSchema = z.object({
|
|
608
707
|
name: z.string().min(1),
|
|
609
708
|
/**
|
|
610
709
|
* Page to capture: an absolute `https://…` URL, or a path like `/shop` resolved against the
|
|
611
710
|
* managed server's URL. Optional — with a managed server, a url-based asset that omits it
|
|
612
|
-
* captures the server root; local generators (`
|
|
711
|
+
* captures the server root; local generators (`specimen`, `palette`, `wall`, …) need no url.
|
|
613
712
|
*/
|
|
614
713
|
url: z.string().min(1).refine((s) => /^https?:\/\//i.test(s) || s.startsWith("/"), {
|
|
615
714
|
message: 'url must be an absolute http(s) URL or a path starting with "/" (resolved against the managed server)'
|
|
@@ -624,9 +723,11 @@ var assetSpecSchema = z.object({
|
|
|
624
723
|
inputs: z.record(z.string(), z.string()).default({})
|
|
625
724
|
}).strict();
|
|
626
725
|
var showcaseConfigSchema = z.object({
|
|
726
|
+
/** JSON configs may carry an editor `$schema` pointer; accepted and ignored at runtime. */
|
|
727
|
+
$schema: z.string().optional(),
|
|
627
728
|
settings: settingsSchema.default({}),
|
|
628
729
|
assets: z.array(assetSpecSchema).min(1, "Define at least one asset in `assets`.")
|
|
629
|
-
}).superRefine((cfg, ctx) => {
|
|
730
|
+
}).strict().superRefine((cfg, ctx) => {
|
|
630
731
|
const seen = /* @__PURE__ */ new Set();
|
|
631
732
|
for (const [i, asset] of cfg.assets.entries()) {
|
|
632
733
|
if (seen.has(asset.name)) {
|
|
@@ -648,12 +749,12 @@ import { stat as stat2 } from "fs/promises";
|
|
|
648
749
|
import { z as z2 } from "zod";
|
|
649
750
|
var easingSchema = z2.enum([
|
|
650
751
|
"linear",
|
|
651
|
-
"
|
|
652
|
-
"
|
|
653
|
-
"
|
|
654
|
-
"
|
|
655
|
-
"
|
|
656
|
-
"
|
|
752
|
+
"ease-in-out-cubic",
|
|
753
|
+
"ease-in-out-quad",
|
|
754
|
+
"ease-out-cubic",
|
|
755
|
+
"ease-in-out-sine",
|
|
756
|
+
"ease-in-out-expo",
|
|
757
|
+
"ease-out-quint"
|
|
657
758
|
]);
|
|
658
759
|
var choreographyStepSchema = z2.object({
|
|
659
760
|
/** Target: a 0..1 number, an "NN%" string, or a CSS selector to bring into view. */
|
|
@@ -662,7 +763,7 @@ var choreographyStepSchema = z2.object({
|
|
|
662
763
|
durationMs: z2.number().int().nonnegative().optional().describe("Travel time to this target (ms). Default 1200."),
|
|
663
764
|
/** Hold time at this target after arriving (ms). Default 800. */
|
|
664
765
|
holdMs: z2.number().int().nonnegative().optional().describe("Hold time at this target after arriving (ms). Default 800."),
|
|
665
|
-
easing: easingSchema.optional().describe('Easing for the travel to this target. Default "
|
|
766
|
+
easing: easingSchema.optional().describe('Easing for the travel to this target. Default "ease-in-out-cubic".')
|
|
666
767
|
}).strict();
|
|
667
768
|
var interactionActionSchema = z2.object({
|
|
668
769
|
do: z2.enum(["move", "click", "hover", "type", "scrollTo", "wait"]).describe("What this step does."),
|
|
@@ -728,8 +829,8 @@ var scrollReelOptionsSchema = z2.object({
|
|
|
728
829
|
/** Output frames per second (re-encoded from the recording). */
|
|
729
830
|
fps: z2.number().int().positive().max(120).default(30).describe("Output frames per second (re-encoded from the recording). Default 30."),
|
|
730
831
|
/** Time to scroll from top to bottom (ms). */
|
|
731
|
-
|
|
732
|
-
easing: easingSchema.default("
|
|
832
|
+
durationMs: z2.number().int().positive().default(6e3).describe("Time to scroll from top to bottom (ms). Default 6000."),
|
|
833
|
+
easing: easingSchema.default("ease-in-out-cubic").describe('Easing for the default top\u2192bottom scroll. Default "ease-in-out-cubic".'),
|
|
733
834
|
/** Dwell at the top before scrolling (ms). */
|
|
734
835
|
startDelayMs: z2.number().int().nonnegative().default(500).describe("Dwell at the top before scrolling (ms). Default 500."),
|
|
735
836
|
/** Dwell at the bottom after scrolling (ms). */
|
|
@@ -788,7 +889,7 @@ var scrollReelOptionsSchema = z2.object({
|
|
|
788
889
|
scaleFrom: z2.number().positive().optional().describe("Start scale (1 = no zoom). Default 1."),
|
|
789
890
|
/** End scale. Default 1.08. */
|
|
790
891
|
scaleTo: z2.number().positive().optional().describe("End scale. Default 1.08."),
|
|
791
|
-
easing: easingSchema.optional().describe('Easing for the zoom ramp. Default "
|
|
892
|
+
easing: easingSchema.optional().describe('Easing for the zoom ramp. Default "ease-in-out-cubic".'),
|
|
792
893
|
/** Zoom origin X within the viewport (0 = left, 1 = right). Default 0.5. */
|
|
793
894
|
originX: z2.number().min(0).max(1).optional().describe("Zoom origin X within the viewport (0 = left, 1 = right). Default 0.5."),
|
|
794
895
|
/** Zoom origin Y within the viewport (0 = top, 1 = bottom). Default 0.5. */
|
|
@@ -943,30 +1044,67 @@ var scrollReelOptionsSchema = z2.object({
|
|
|
943
1044
|
import path6 from "path";
|
|
944
1045
|
import { mkdtemp } from "fs/promises";
|
|
945
1046
|
|
|
1047
|
+
// src/pipeline/capture.ts
|
|
1048
|
+
function withCaptureQuery(url, capture) {
|
|
1049
|
+
if (!url || !capture?.query) return url;
|
|
1050
|
+
try {
|
|
1051
|
+
const u = new URL(url);
|
|
1052
|
+
for (const [k, v] of Object.entries(capture.query)) u.searchParams.set(k, v);
|
|
1053
|
+
return u.toString();
|
|
1054
|
+
} catch {
|
|
1055
|
+
return url;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
async function applyCapture(context, capture, url) {
|
|
1059
|
+
if (!capture) return;
|
|
1060
|
+
if (capture.cookies?.length && url) {
|
|
1061
|
+
try {
|
|
1062
|
+
const origin = new URL(url).origin;
|
|
1063
|
+
await context.addCookies(
|
|
1064
|
+
capture.cookies.map((c) => ({ name: c.name, value: c.value, url: origin }))
|
|
1065
|
+
);
|
|
1066
|
+
} catch {
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const scripts = [];
|
|
1070
|
+
if (capture.localStorage) {
|
|
1071
|
+
scripts.push(
|
|
1072
|
+
`try{var e=${JSON.stringify(capture.localStorage)};for(var k in e)localStorage.setItem(k,e[k]);}catch(_){}`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
if (capture.initScript) scripts.push(capture.initScript);
|
|
1076
|
+
if (scripts.length) await context.addInitScript(scripts.join("\n"));
|
|
1077
|
+
}
|
|
1078
|
+
|
|
946
1079
|
// src/generators/scroll-reel/scroll.ts
|
|
947
1080
|
var EASINGS = {
|
|
948
1081
|
linear: (t) => t,
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1082
|
+
"ease-in-out-cubic": (t) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2,
|
|
1083
|
+
"ease-in-out-quad": (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2,
|
|
1084
|
+
"ease-out-cubic": (t) => 1 - Math.pow(1 - t, 3),
|
|
1085
|
+
"ease-in-out-sine": (t) => -(Math.cos(Math.PI * t) - 1) / 2,
|
|
1086
|
+
"ease-in-out-expo": (t) => t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2,
|
|
1087
|
+
"ease-out-quint": (t) => 1 - Math.pow(1 - t, 5)
|
|
955
1088
|
};
|
|
956
1089
|
async function prepareScroll(args) {
|
|
957
1090
|
const target = findScrollTarget(document, getComputedStyle);
|
|
958
1091
|
forceInstant(target);
|
|
959
1092
|
const sleep2 = (ms) => new Promise((resolve) => setTimeout(() => resolve(), ms));
|
|
1093
|
+
const withCap = (p, ms) => Promise.race([p ?? Promise.resolve(), sleep2(ms)]);
|
|
960
1094
|
scrollTargetTo(target, maxScrollOf(target));
|
|
961
1095
|
await sleep2(Math.max(0, args.settleMs));
|
|
962
1096
|
try {
|
|
963
|
-
await (document.fonts?.ready ??
|
|
1097
|
+
await withCap(document.fonts?.ready ?? null, 5e3);
|
|
964
1098
|
} catch {
|
|
965
1099
|
}
|
|
966
1100
|
try {
|
|
967
1101
|
const imgs = Array.from(document.images ?? []);
|
|
968
|
-
await
|
|
969
|
-
|
|
1102
|
+
await withCap(
|
|
1103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1104
|
+
Promise.all(imgs.map((im) => im.decode ? im.decode().catch(() => {
|
|
1105
|
+
}) : null)),
|
|
1106
|
+
4e3
|
|
1107
|
+
);
|
|
970
1108
|
} catch {
|
|
971
1109
|
}
|
|
972
1110
|
scrollTargetTo(target, 0);
|
|
@@ -1311,17 +1449,17 @@ async function pageScroll(args) {
|
|
|
1311
1449
|
const { durationMs, easing, startDelayMs, endDwellMs } = args;
|
|
1312
1450
|
const ease = (t) => {
|
|
1313
1451
|
switch (easing) {
|
|
1314
|
-
case "
|
|
1452
|
+
case "ease-in-out-cubic":
|
|
1315
1453
|
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
1316
|
-
case "
|
|
1454
|
+
case "ease-in-out-quad":
|
|
1317
1455
|
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
1318
|
-
case "
|
|
1456
|
+
case "ease-out-cubic":
|
|
1319
1457
|
return 1 - Math.pow(1 - t, 3);
|
|
1320
|
-
case "
|
|
1458
|
+
case "ease-in-out-sine":
|
|
1321
1459
|
return -(Math.cos(Math.PI * t) - 1) / 2;
|
|
1322
|
-
case "
|
|
1460
|
+
case "ease-in-out-expo":
|
|
1323
1461
|
return t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2;
|
|
1324
|
-
case "
|
|
1462
|
+
case "ease-out-quint":
|
|
1325
1463
|
return 1 - Math.pow(1 - t, 5);
|
|
1326
1464
|
default:
|
|
1327
1465
|
return t;
|
|
@@ -1410,6 +1548,7 @@ async function captureScrollWebm(args) {
|
|
|
1410
1548
|
size: { width: options.width, height: options.height }
|
|
1411
1549
|
}
|
|
1412
1550
|
});
|
|
1551
|
+
await applyCapture(context, args.capture, url);
|
|
1413
1552
|
const page = await context.newPage();
|
|
1414
1553
|
const video = page.video();
|
|
1415
1554
|
const recStart = Date.now();
|
|
@@ -1424,7 +1563,7 @@ async function captureScrollWebm(args) {
|
|
|
1424
1563
|
await page.evaluate(prepareScroll, { settleMs: PREWARM_SETTLE_MS });
|
|
1425
1564
|
leadSeconds = (Date.now() - recStart) / 1e3;
|
|
1426
1565
|
const distance = await page.evaluate(pageScroll, {
|
|
1427
|
-
durationMs: options.
|
|
1566
|
+
durationMs: options.durationMs,
|
|
1428
1567
|
easing: options.easing,
|
|
1429
1568
|
startDelayMs: options.startDelayMs,
|
|
1430
1569
|
endDwellMs: options.endDwellMs
|
|
@@ -1444,10 +1583,10 @@ async function captureScrollWebm(args) {
|
|
|
1444
1583
|
}
|
|
1445
1584
|
|
|
1446
1585
|
// src/media/frame-capture.ts
|
|
1447
|
-
import
|
|
1586
|
+
import os2 from "os";
|
|
1448
1587
|
import path7 from "path";
|
|
1449
1588
|
function autoWorkers() {
|
|
1450
|
-
const cores =
|
|
1589
|
+
const cores = os2.cpus()?.length ?? 2;
|
|
1451
1590
|
return Math.max(1, Math.min(6, Math.floor(cores / 2)));
|
|
1452
1591
|
}
|
|
1453
1592
|
function planFrames(totalFrames, workers) {
|
|
@@ -1895,7 +2034,7 @@ function scrollTimelineTotalMs(o) {
|
|
|
1895
2034
|
if (o.autoSections) {
|
|
1896
2035
|
return autoSectionsBudgetMs(o.autoSections);
|
|
1897
2036
|
}
|
|
1898
|
-
return o.startDelayMs + o.
|
|
2037
|
+
return o.startDelayMs + o.durationMs + o.endDwellMs;
|
|
1899
2038
|
}
|
|
1900
2039
|
function boomerangSpec(spec) {
|
|
1901
2040
|
const forward = spec.segments.map((s) => ({ ...s, durationFraction: s.durationFraction / 2 }));
|
|
@@ -1991,7 +2130,7 @@ async function buildScrollTimeline(page, options, totalSeconds, logger) {
|
|
|
1991
2130
|
return finalize(
|
|
1992
2131
|
defaultTimelineSpec({
|
|
1993
2132
|
startDelayMs: options.startDelayMs,
|
|
1994
|
-
durationMs: options.
|
|
2133
|
+
durationMs: options.durationMs,
|
|
1995
2134
|
endDwellMs: options.endDwellMs,
|
|
1996
2135
|
easing: options.easing
|
|
1997
2136
|
})
|
|
@@ -2019,6 +2158,7 @@ async function captureScrollFrames(a) {
|
|
|
2019
2158
|
signal: a.signal,
|
|
2020
2159
|
prepare: async (page, { logger }) => {
|
|
2021
2160
|
if (a.colorScheme) await page.emulateMedia({ colorScheme: a.colorScheme });
|
|
2161
|
+
await applyCapture(page.context(), a.capture, a.url);
|
|
2022
2162
|
await installNetworkHygiene(page, options);
|
|
2023
2163
|
await installPreNav(page, options);
|
|
2024
2164
|
logger.debug(`navigating to ${a.url} (waitUntil=${options.waitUntil})`);
|
|
@@ -2483,6 +2623,7 @@ async function captureInteractionWebm(args) {
|
|
|
2483
2623
|
deviceScaleFactor: options.deviceScaleFactor,
|
|
2484
2624
|
recordVideo: { dir: recordDir, size: { width: options.width, height: options.height } }
|
|
2485
2625
|
});
|
|
2626
|
+
await applyCapture(context, args.capture, url);
|
|
2486
2627
|
const page = await context.newPage();
|
|
2487
2628
|
const video = page.video();
|
|
2488
2629
|
const actions = options.actions ?? [];
|
|
@@ -2545,6 +2686,7 @@ async function captureFocusWebm(args) {
|
|
|
2545
2686
|
deviceScaleFactor: options.deviceScaleFactor,
|
|
2546
2687
|
recordVideo: { dir: recordDir, size: { width: options.width, height: options.height } }
|
|
2547
2688
|
});
|
|
2689
|
+
await applyCapture(context, args.capture, url);
|
|
2548
2690
|
const page = await context.newPage();
|
|
2549
2691
|
const video = page.video();
|
|
2550
2692
|
const actions = focus.actions ?? [];
|
|
@@ -2639,6 +2781,7 @@ async function run(ctx, options) {
|
|
|
2639
2781
|
ctx.logger.info(`recording ${url} (focus: ${options.focus.selector})`);
|
|
2640
2782
|
const { webmPath, leadSeconds, durationSeconds, cropBox } = await captureFocusWebm({
|
|
2641
2783
|
browser: ctx.browser,
|
|
2784
|
+
capture: ctx.capture,
|
|
2642
2785
|
url,
|
|
2643
2786
|
options,
|
|
2644
2787
|
colorScheme: scheme,
|
|
@@ -2692,6 +2835,7 @@ async function run(ctx, options) {
|
|
|
2692
2835
|
ctx.logger.info(`recording ${url} (interaction, ${options.actions.length} action(s))`);
|
|
2693
2836
|
const { webmPath, leadSeconds, durationSeconds } = await captureInteractionWebm({
|
|
2694
2837
|
browser: ctx.browser,
|
|
2838
|
+
capture: ctx.capture,
|
|
2695
2839
|
url,
|
|
2696
2840
|
options,
|
|
2697
2841
|
colorScheme: scheme,
|
|
@@ -2747,7 +2891,7 @@ async function run(ctx, options) {
|
|
|
2747
2891
|
...options,
|
|
2748
2892
|
choreography: r.choreography,
|
|
2749
2893
|
autoSections: r.autoSections,
|
|
2750
|
-
|
|
2894
|
+
durationMs: r.durationMs ?? options.durationMs
|
|
2751
2895
|
};
|
|
2752
2896
|
const segPath = path10.join(ctx.tmpDir, `${slugify(ctx.target.name)}-route-${i}.mp4`);
|
|
2753
2897
|
ctx.logger.info(
|
|
@@ -2755,6 +2899,7 @@ async function run(ctx, options) {
|
|
|
2755
2899
|
);
|
|
2756
2900
|
await captureScrollFrames({
|
|
2757
2901
|
browser: ctx.browser,
|
|
2902
|
+
capture: ctx.capture,
|
|
2758
2903
|
url: routeUrl,
|
|
2759
2904
|
options: routeOpts,
|
|
2760
2905
|
outPath: segPath,
|
|
@@ -2806,10 +2951,11 @@ async function run(ctx, options) {
|
|
|
2806
2951
|
}
|
|
2807
2952
|
const fileName = options.fileName ?? `${slugify(ctx.target.name)}.mp4`;
|
|
2808
2953
|
const outPath = ctx.resolveOutPath(fileName);
|
|
2809
|
-
const durationSeconds = (options.startDelayMs + options.
|
|
2954
|
+
const durationSeconds = (options.startDelayMs + options.durationMs + options.endDwellMs) / 1e3;
|
|
2810
2955
|
ctx.logger.info(`recording ${url} (realtime)`);
|
|
2811
2956
|
const { webmPath, leadSeconds } = await captureScrollWebm({
|
|
2812
2957
|
browser: ctx.browser,
|
|
2958
|
+
capture: ctx.capture,
|
|
2813
2959
|
url,
|
|
2814
2960
|
options,
|
|
2815
2961
|
tmpDir: ctx.tmpDir,
|
|
@@ -2839,7 +2985,7 @@ async function run(ctx, options) {
|
|
|
2839
2985
|
format: "mp4",
|
|
2840
2986
|
width: options.width,
|
|
2841
2987
|
height: options.height,
|
|
2842
|
-
durationMs: options.startDelayMs + options.
|
|
2988
|
+
durationMs: options.startDelayMs + options.durationMs + options.endDwellMs,
|
|
2843
2989
|
bytes: stats.size,
|
|
2844
2990
|
contentHash,
|
|
2845
2991
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2873,6 +3019,7 @@ async function run(ctx, options) {
|
|
|
2873
3019
|
ctx.logger.info(`recording ${url}${label} (frame-stepped, ${workers} worker(s))`);
|
|
2874
3020
|
await captureScrollFrames({
|
|
2875
3021
|
browser: ctx.browser,
|
|
3022
|
+
capture: ctx.capture,
|
|
2876
3023
|
url,
|
|
2877
3024
|
options: vopts,
|
|
2878
3025
|
outPath: captureMp4,
|
|
@@ -2922,7 +3069,7 @@ import { imageSize } from "image-size";
|
|
|
2922
3069
|
|
|
2923
3070
|
// src/generators/screenshots/options.ts
|
|
2924
3071
|
import { z as z3 } from "zod";
|
|
2925
|
-
var
|
|
3072
|
+
var viewportSchema = z3.object({
|
|
2926
3073
|
name: z3.string().min(1).describe('Label for this viewport \u2014 used in the filename + manifest id (e.g. "desktop").'),
|
|
2927
3074
|
width: z3.number().int().positive().describe("Viewport width in CSS px."),
|
|
2928
3075
|
/** Viewport height. Note: ignored for `fullPage` shots (Playwright resizes to the page height);
|
|
@@ -2930,8 +3077,8 @@ var breakpointSchema = z3.object({
|
|
|
2930
3077
|
height: z3.number().int().positive().default(900).describe(
|
|
2931
3078
|
"Viewport height in CSS px. Default 900. Ignored for fullPage shots; only affects viewport/element captures."
|
|
2932
3079
|
),
|
|
2933
|
-
/** Override the generator-level deviceScaleFactor for this
|
|
2934
|
-
deviceScaleFactor: z3.number().positive().max(4).optional().describe("Override the generator-level deviceScaleFactor for this
|
|
3080
|
+
/** Override the generator-level deviceScaleFactor for this viewport. */
|
|
3081
|
+
deviceScaleFactor: z3.number().positive().max(4).optional().describe("Override the generator-level deviceScaleFactor for this viewport. Omit to inherit it.")
|
|
2935
3082
|
}).strict();
|
|
2936
3083
|
var elementShotSchema = z3.object({
|
|
2937
3084
|
selector: z3.string().min(1).describe("CSS selector of the element to shoot."),
|
|
@@ -2939,7 +3086,7 @@ var elementShotSchema = z3.object({
|
|
|
2939
3086
|
name: z3.string().min(1).describe("Name used in the filename + manifest id for this element shot.")
|
|
2940
3087
|
}).strict();
|
|
2941
3088
|
var screenshotsOptionsSchema = z3.object({
|
|
2942
|
-
|
|
3089
|
+
viewports: z3.array(viewportSchema).min(1).default([
|
|
2943
3090
|
{ name: "desktop", width: 1440, height: 900 },
|
|
2944
3091
|
{ name: "mobile", width: 390, height: 844 }
|
|
2945
3092
|
]).describe(
|
|
@@ -2953,8 +3100,8 @@ var screenshotsOptionsSchema = z3.object({
|
|
|
2953
3100
|
deviceScaleFactor: z3.number().positive().max(4).default(2).describe("Render scale (2 = retina-crisp). Default 2."),
|
|
2954
3101
|
waitUntil: z3.enum(["load", "domcontentloaded", "networkidle", "commit"]).default("networkidle").describe('Page-load milestone to wait for before capturing. Default "networkidle".'),
|
|
2955
3102
|
waitForSelector: z3.string().optional().describe("Optional element to wait for before capturing (e.g. a hero image). Omit to skip."),
|
|
2956
|
-
/** Element captures taken at every
|
|
2957
|
-
elements: z3.array(elementShotSchema).default([]).describe("Specific elements to crop (in addition to the page) at every
|
|
3103
|
+
/** Element captures taken at every viewport. */
|
|
3104
|
+
elements: z3.array(elementShotSchema).default([]).describe("Specific elements to crop (in addition to the page) at every viewport. Default none."),
|
|
2958
3105
|
/** png only: capture with a transparent background. */
|
|
2959
3106
|
omitBackground: z3.boolean().default(false).describe("Capture with a transparent background (png only). Default false."),
|
|
2960
3107
|
/** Extra settle time after load before capturing (ms). */
|
|
@@ -2984,14 +3131,14 @@ async function mapLimit(items, limit, fn) {
|
|
|
2984
3131
|
var MIN_PREPARE_SETTLE_MS = 600;
|
|
2985
3132
|
async function captureScreenshots(args) {
|
|
2986
3133
|
const { options } = args;
|
|
2987
|
-
const
|
|
2988
|
-
options.
|
|
2989
|
-
Math.min(3, options.
|
|
2990
|
-
(bp) =>
|
|
3134
|
+
const perViewport = await mapLimit(
|
|
3135
|
+
options.viewports,
|
|
3136
|
+
Math.min(3, options.viewports.length),
|
|
3137
|
+
(bp) => captureViewport(args, bp)
|
|
2991
3138
|
);
|
|
2992
|
-
return
|
|
3139
|
+
return perViewport.flat();
|
|
2993
3140
|
}
|
|
2994
|
-
async function
|
|
3141
|
+
async function captureViewport(args, bp) {
|
|
2995
3142
|
const { browser, url, options, logger } = args;
|
|
2996
3143
|
const shots = [];
|
|
2997
3144
|
{
|
|
@@ -2999,6 +3146,7 @@ async function captureBreakpoint(args, bp) {
|
|
|
2999
3146
|
viewport: { width: bp.width, height: bp.height },
|
|
3000
3147
|
deviceScaleFactor: bp.deviceScaleFactor ?? options.deviceScaleFactor
|
|
3001
3148
|
});
|
|
3149
|
+
await applyCapture(context, args.capture, url);
|
|
3002
3150
|
const page = await context.newPage();
|
|
3003
3151
|
try {
|
|
3004
3152
|
logger.debug(`[${bp.name}] navigating to ${url}`);
|
|
@@ -3069,7 +3217,8 @@ async function run2(ctx, options) {
|
|
|
3069
3217
|
browser: ctx.browser,
|
|
3070
3218
|
url,
|
|
3071
3219
|
options,
|
|
3072
|
-
logger: ctx.logger
|
|
3220
|
+
logger: ctx.logger,
|
|
3221
|
+
capture: ctx.capture
|
|
3073
3222
|
});
|
|
3074
3223
|
const records = [];
|
|
3075
3224
|
for (const shot of shots) {
|
|
@@ -3114,276 +3263,148 @@ var screenshotsGenerator = {
|
|
|
3114
3263
|
};
|
|
3115
3264
|
|
|
3116
3265
|
// src/generators/wall/options.ts
|
|
3117
|
-
import { z as z6 } from "zod";
|
|
3118
|
-
|
|
3119
|
-
// src/generators/scene/scene-options.ts
|
|
3120
3266
|
import { z as z5 } from "zod";
|
|
3121
3267
|
|
|
3122
|
-
// src/generators/
|
|
3268
|
+
// src/generators/scene/scene-options.ts
|
|
3123
3269
|
import { z as z4 } from "zod";
|
|
3124
|
-
var
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
colors: z4.number().nonnegative().default(0).describe("Fraction of cells whose color changes during this beat (0..1; 1 = every cell once). Default 0."),
|
|
3133
|
-
/**
|
|
3134
|
-
* Target color for this beat's color changes. When set, every color change in the beat goes to
|
|
3135
|
-
* this exact token (a deliberate sweep) instead of a weighted-random pick. Set `colors: 1` with
|
|
3136
|
-
* `pacing: "even"` to wash the whole specimen to one color evenly. Omit for the default
|
|
3137
|
-
* scattered, weighted-random recoloring.
|
|
3138
|
-
*/
|
|
3139
|
-
color: z4.enum(["foreground", "muted", "accent"]).optional().describe("Target color token for this beat's color changes (a deliberate sweep); omit for the default weighted-random recoloring."),
|
|
3140
|
-
/**
|
|
3141
|
-
* How the changes are distributed in time across the beat — like a CSS easing curve:
|
|
3142
|
-
* "linear"/"even" = uniform, "ease-in" = front-loaded, "ease-out" = back-loaded,
|
|
3143
|
-
* "ease-in-out" = bunched at both ends, "random" = scattered.
|
|
3144
|
-
*/
|
|
3145
|
-
pacing: z4.enum(["even", "linear", "ease-in", "ease-out", "ease-in-out", "random"]).default("even").describe("How changes are distributed in time across the beat (CSS-easing-like): even/linear, ease-in, ease-out, ease-in-out, random. Default even.")
|
|
3270
|
+
var specimenWirePulseSchema = z4.object({
|
|
3271
|
+
name: z4.string().default(""),
|
|
3272
|
+
/** Length of this beat, in seconds (wire unit). */
|
|
3273
|
+
duration: z4.number().positive(),
|
|
3274
|
+
chars: z4.number().nonnegative().default(0),
|
|
3275
|
+
colors: z4.number().nonnegative().default(0),
|
|
3276
|
+
color: z4.enum(["foreground", "muted", "accent"]).optional(),
|
|
3277
|
+
pacing: z4.enum(["even", "linear", "ease-in", "ease-out", "ease-in-out", "random"]).default("even")
|
|
3146
3278
|
}).strict();
|
|
3147
|
-
var
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
});
|
|
3154
|
-
var DEFAULT_PULSES = [
|
|
3155
|
-
P("intro hold", 0.8),
|
|
3156
|
-
P("first letters", 0.8, 0.15, 0),
|
|
3157
|
-
P("settle", 1.5),
|
|
3158
|
-
P("ripple", 1, 0.08, 0.04),
|
|
3159
|
-
P("color sweep", 1.2, 0, 0.13),
|
|
3160
|
-
P("rest", 1.2),
|
|
3161
|
-
P("quick burst", 0.6, 0.18, 0),
|
|
3162
|
-
P("drift", 1.2, 0, 0.08),
|
|
3163
|
-
P("finale", 1.2, 0.18, 0.13),
|
|
3164
|
-
P("outro hold", 0.5)
|
|
3165
|
-
];
|
|
3166
|
-
var DEMO_PULSES = [
|
|
3167
|
-
{ name: "linear", duration: 5, chars: 0.5, pacing: "linear" },
|
|
3168
|
-
{ name: "hold", duration: 2 },
|
|
3169
|
-
{ name: "ease-in", duration: 5, chars: 0.5, pacing: "ease-in" },
|
|
3170
|
-
{ name: "hold", duration: 2 },
|
|
3171
|
-
{ name: "ease-out", duration: 5, chars: 0.5, pacing: "ease-out" },
|
|
3172
|
-
{ name: "hold", duration: 2 },
|
|
3173
|
-
{ name: "ease-in-out", duration: 5, chars: 0.5, pacing: "ease-in-out" },
|
|
3174
|
-
{ name: "hold", duration: 2 },
|
|
3175
|
-
{ name: "random", duration: 5, chars: 0.5, pacing: "random" },
|
|
3176
|
-
{ name: "hold", duration: 2 },
|
|
3177
|
-
// Even, per-character color sweeps: each washes every glyph to one token, evenly across the beat.
|
|
3178
|
-
{ name: "sweep \u2192 muted", duration: 4, colors: 1, color: "muted", pacing: "even" },
|
|
3179
|
-
{ name: "sweep \u2192 accent", duration: 4, colors: 1, color: "accent", pacing: "even" },
|
|
3180
|
-
{ name: "sweep \u2192 foreground", duration: 4, colors: 1, color: "foreground", pacing: "even" },
|
|
3181
|
-
{ name: "hold", duration: 2 },
|
|
3182
|
-
{ name: "weighted recolor", duration: 5, colors: 0.6 },
|
|
3183
|
-
{ name: "hold", duration: 2 },
|
|
3184
|
-
{ name: "mingle", duration: 5, chars: 0.3, colors: 0.3 }
|
|
3185
|
-
];
|
|
3186
|
-
var SWEEP_COLORS = {
|
|
3187
|
-
background: "#0b0b0f",
|
|
3188
|
-
foreground: "#f4f4f5",
|
|
3189
|
-
muted: "#6b7280",
|
|
3190
|
-
accent: "#7c9cff"
|
|
3191
|
-
};
|
|
3192
|
-
var SWEEP_PULSES = [
|
|
3193
|
-
{ name: "hold", duration: 0.8 },
|
|
3194
|
-
{ name: "to muted", duration: 2.2, colors: 1, color: "muted", pacing: "ease-in-out" },
|
|
3195
|
-
{ name: "settle", duration: 0.6 },
|
|
3196
|
-
{ name: "to accent", duration: 2.2, colors: 1, color: "accent", pacing: "ease-in-out" },
|
|
3197
|
-
{ name: "settle", duration: 0.6 },
|
|
3198
|
-
{ name: "to foreground", duration: 2.2, colors: 1, color: "foreground", pacing: "ease-in-out" },
|
|
3199
|
-
{ name: "glyph drift", duration: 1.4, chars: 0.5, pacing: "even" },
|
|
3200
|
-
{ name: "hold", duration: 0.6 }
|
|
3201
|
-
];
|
|
3202
|
-
var SPECIMEN_TEMPLATES = {
|
|
3203
|
-
demo: { demo: true, mirror: false, lines: 4, pulses: DEMO_PULSES },
|
|
3204
|
-
sweep: {
|
|
3205
|
-
mirror: true,
|
|
3206
|
-
lines: 4,
|
|
3207
|
-
colors: SWEEP_COLORS,
|
|
3208
|
-
pulses: SWEEP_PULSES
|
|
3209
|
-
}
|
|
3210
|
-
};
|
|
3211
|
-
function applyTemplate(raw) {
|
|
3212
|
-
if (!raw || typeof raw !== "object") return raw;
|
|
3213
|
-
const r = raw;
|
|
3214
|
-
const tmpl = typeof r.template === "string" ? SPECIMEN_TEMPLATES[r.template] : void 0;
|
|
3215
|
-
return tmpl ? { ...tmpl, ...r } : raw;
|
|
3216
|
-
}
|
|
3217
|
-
var specimenObjectSchema = z4.object({
|
|
3218
|
-
font: z4.string().min(1).describe("Font file to showcase (path relative to the working dir, or absolute). Required."),
|
|
3219
|
-
template: z4.enum(["demo", "sweep"]).optional().describe("Load a named option preset (demo or sweep); your explicit options still override what it sets."),
|
|
3220
|
-
name: z4.string().default("").describe('Display name shown bottom-left (e.g. "ABC Oracle"). Default none.'),
|
|
3221
|
-
demo: z4.boolean().default(false).describe("Demo mode: overlay the active pulse's name bottom-right, to see which beat is playing. Default false."),
|
|
3222
|
-
fps: z4.number().int().positive().max(120).default(30).describe("Output frames per second. Default 30."),
|
|
3223
|
-
durationSeconds: z4.number().positive().optional().describe("Clip length in seconds. Defaults to the (mirrored) sum of the pulse durations; set to override."),
|
|
3224
|
-
width: z4.number().int().positive().default(1920).describe("Output frame width in px. Default 1920."),
|
|
3225
|
-
height: z4.number().int().positive().default(1080).describe("Output frame height in px. Default 1080."),
|
|
3226
|
-
deviceScaleFactor: z4.number().positive().max(4).default(1).describe("Render scale (1 = 1:1; higher = crisper capture, downscaled into the video). Default 1."),
|
|
3227
|
-
weight: z4.number().int().min(1).max(1e3).default(820).describe("Glyph weight on the variable-font axis, 1\u20131000. Default 820."),
|
|
3228
|
-
lines: z4.number().int().min(1).max(40).default(3).describe("Number of glyph rows; glyph size is derived so rows fill the top 80% of the frame. Default 3."),
|
|
3229
|
-
leading: z4.number().positive().default(0.78).describe("Line-height of the glyph block. Default 0.78 (tight, cap-height-hugging)."),
|
|
3230
|
-
blacklist: z4.string().default("").describe('Glyphs to exclude from the showcase, e.g. "QXZ" (case-insensitive). Default none.'),
|
|
3231
|
-
characterPool: z4.string().refine((s) => (/* @__PURE__ */ new Set([...s.trim()])).size >= 2, "characterPool needs \u22652 distinct characters").optional().describe("Override the glyph pool the specimen draws from (\u22652 distinct characters). Default A\u2013Z 0\u20139 + symbols."),
|
|
3232
|
-
seed: z4.number().int().default(1).describe("Schedule seed \u2014 same seed \u21D2 identical animation. Change for a different deterministic take. Default 1."),
|
|
3279
|
+
var specimenSceneOptionsSchema = z4.object({
|
|
3280
|
+
label: z4.string().default(""),
|
|
3281
|
+
demo: z4.boolean().default(false),
|
|
3282
|
+
weight: z4.number().min(1).max(1e3).default(820),
|
|
3283
|
+
lines: z4.number().int().min(1).max(40).default(3),
|
|
3284
|
+
blacklist: z4.string().default(""),
|
|
3233
3285
|
colors: z4.object({
|
|
3234
|
-
background: z4.string()
|
|
3235
|
-
foreground: z4.string()
|
|
3236
|
-
muted: z4.string()
|
|
3237
|
-
accent: z4.string().optional()
|
|
3238
|
-
|
|
3239
|
-
label: z4.string().optional().describe("Color of the font-name label (bottom corner); defaults to `foreground` if unset.")
|
|
3240
|
-
// defaults to `foreground` at render
|
|
3241
|
-
}).default({}).describe("Color tokens the glyphs cycle through (any CSS colors). Override any subset. Default: light-grey palette."),
|
|
3242
|
-
colorWeights: z4.object({
|
|
3243
|
-
foreground: z4.number().nonnegative().default(2).describe("Relative likelihood of the foreground token on a random color change. Default 2."),
|
|
3244
|
-
muted: z4.number().nonnegative().default(2).describe("Relative likelihood of the muted token on a random color change. Default 2."),
|
|
3245
|
-
accent: z4.number().nonnegative().default(1).describe("Relative likelihood of the accent token on a random color change. Default 1.")
|
|
3246
|
-
}).default({}).describe("Relative likelihood of each color token on a random (non-targeted) color change. Default 2 / 2 / 1."),
|
|
3247
|
-
pulses: z4.array(pulseSchema).min(1).default(DEFAULT_PULSES).describe("The animation storyboard: an ordered sequence of pulses (beats). Default: a lively built-in storyboard."),
|
|
3248
|
-
characterIntensity: z4.number().nonnegative().default(1).describe("Multiply every pulse's glyph-change fraction (1 = baseline, 2 = twice as busy, 0 = none). Default 1."),
|
|
3249
|
-
colorIntensity: z4.number().nonnegative().default(1).describe("Multiply every pulse's color-change fraction (1 = baseline, 2 = twice as busy, 0 = none). Default 1."),
|
|
3250
|
-
maxLineDrift: z4.number().positive().max(0.5).default(0.05).describe("Max fraction a line's total width may drift as its glyphs change; swaps are width-compensated. Default 0.05."),
|
|
3251
|
-
mirror: z4.boolean().default(true).describe("Mirror the pulses (play out and back) for a seamless loop; doubles the clip length. Default true."),
|
|
3252
|
-
crf: z4.number().int().min(0).max(51).default(18).describe("x264 quality, 0\u201351 (lower = better quality / larger file). Default 18."),
|
|
3253
|
-
fileName: z4.string().optional().describe('Output filename; defaults to "<slug(asset name)>.mp4".')
|
|
3254
|
-
}).strict();
|
|
3255
|
-
var specimenOptionsSchema = z4.preprocess(applyTemplate, specimenObjectSchema);
|
|
3256
|
-
|
|
3257
|
-
// src/generators/scene/scene-options.ts
|
|
3258
|
-
var specimenSceneOptionsSchema = z5.object({
|
|
3259
|
-
label: z5.string().default(""),
|
|
3260
|
-
demo: z5.boolean().default(false),
|
|
3261
|
-
weight: z5.number().min(1).max(1e3).default(820),
|
|
3262
|
-
lines: z5.number().int().min(1).max(40).default(3),
|
|
3263
|
-
blacklist: z5.string().default(""),
|
|
3264
|
-
colors: z5.object({
|
|
3265
|
-
background: z5.string(),
|
|
3266
|
-
foreground: z5.string(),
|
|
3267
|
-
muted: z5.string(),
|
|
3268
|
-
accent: z5.string().optional(),
|
|
3269
|
-
label: z5.string().optional()
|
|
3286
|
+
background: z4.string(),
|
|
3287
|
+
foreground: z4.string(),
|
|
3288
|
+
muted: z4.string(),
|
|
3289
|
+
accent: z4.string().optional(),
|
|
3290
|
+
label: z4.string().optional()
|
|
3270
3291
|
}).partial().default({}),
|
|
3271
|
-
colorWeights:
|
|
3272
|
-
foreground:
|
|
3273
|
-
muted:
|
|
3274
|
-
accent:
|
|
3292
|
+
colorWeights: z4.object({
|
|
3293
|
+
foreground: z4.number().nonnegative(),
|
|
3294
|
+
muted: z4.number().nonnegative(),
|
|
3295
|
+
accent: z4.number().nonnegative()
|
|
3275
3296
|
}).partial().default({}),
|
|
3276
|
-
pulses:
|
|
3277
|
-
mirror:
|
|
3278
|
-
characterIntensity:
|
|
3279
|
-
colorIntensity:
|
|
3297
|
+
pulses: z4.array(specimenWirePulseSchema).default([]),
|
|
3298
|
+
mirror: z4.boolean().default(true),
|
|
3299
|
+
characterIntensity: z4.number().nonnegative().default(1),
|
|
3300
|
+
colorIntensity: z4.number().nonnegative().default(1),
|
|
3280
3301
|
/** Max fraction a line's width may drift as glyphs change (right-edge stability). */
|
|
3281
|
-
maxLineDrift:
|
|
3302
|
+
maxLineDrift: z4.number().positive().max(0.5).default(0.05),
|
|
3282
3303
|
/** Schedule seed — same seed ⇒ identical animation (parallel workers must agree). */
|
|
3283
|
-
seed:
|
|
3304
|
+
seed: z4.number().int().default(1),
|
|
3284
3305
|
/** Line-height of the glyph block. */
|
|
3285
|
-
leading:
|
|
3306
|
+
leading: z4.number().positive().default(0.78),
|
|
3286
3307
|
/** Glyph pool override (≥2 distinct characters after the blacklist). */
|
|
3287
|
-
characterPool:
|
|
3308
|
+
characterPool: z4.string().min(2).optional()
|
|
3288
3309
|
}).strict();
|
|
3289
|
-
var wallEasingEnum =
|
|
3310
|
+
var wallEasingEnum = z4.enum([
|
|
3290
3311
|
"linear",
|
|
3291
3312
|
"ease-in",
|
|
3292
3313
|
"ease-out",
|
|
3293
3314
|
"ease-in-out",
|
|
3294
3315
|
"ease-in-out-strong"
|
|
3295
3316
|
]);
|
|
3296
|
-
var wallPulseSchema =
|
|
3317
|
+
var wallPulseSchema = z4.object({
|
|
3297
3318
|
/** When the pulse starts, as a fraction of the clip (0..1). */
|
|
3298
|
-
at:
|
|
3319
|
+
at: z4.number().min(0).max(1).describe("When the pulse starts, as a fraction of the clip (0..1)."),
|
|
3299
3320
|
/** How long the move takes, as a fraction of the clip (0..1). */
|
|
3300
|
-
|
|
3321
|
+
span: z4.number().positive().max(1).describe("How long the move takes, as a fraction of the clip (0..1)."),
|
|
3301
3322
|
/** How far it travels, in periods (1 = one full tile-set / one wrap). Usually 0..1. */
|
|
3302
|
-
distance:
|
|
3323
|
+
distance: z4.number().nonnegative().describe("How far it travels, in periods (1 = one full tile-set / wrap). Usually 0..1."),
|
|
3303
3324
|
/** Easing of the move's ramp. */
|
|
3304
3325
|
easing: wallEasingEnum.default("ease-in-out").describe("Easing of the move's ramp. Default 'ease-in-out'.")
|
|
3305
3326
|
}).strict();
|
|
3306
|
-
var wallPanSchema =
|
|
3327
|
+
var wallPanSchema = z4.object({
|
|
3307
3328
|
/** Pan direction. */
|
|
3308
|
-
direction:
|
|
3329
|
+
direction: z4.enum(["left", "right"]).default("left").describe("Pan direction. Default 'left'."),
|
|
3309
3330
|
/** Continuous whole-clip horizontal loops (0 = no pan unless `pulses` move it). */
|
|
3310
|
-
loops:
|
|
3331
|
+
loops: z4.number().nonnegative().default(0).describe("Continuous whole-clip horizontal loops (0 = no pan unless pulses move it). Default 0."),
|
|
3311
3332
|
/** Pulses added on top of the base loops. */
|
|
3312
|
-
pulses:
|
|
3333
|
+
pulses: z4.array(wallPulseSchema).default([]).describe("Pulses added on top of the base loops.")
|
|
3313
3334
|
}).strict();
|
|
3314
|
-
var wallColumnSchema =
|
|
3335
|
+
var wallColumnSchema = z4.object({
|
|
3315
3336
|
/** Assets stacked in this column, by name (cycled to fill the height). At least one. */
|
|
3316
|
-
tiles:
|
|
3337
|
+
tiles: z4.array(z4.string().min(1)).min(1).describe("Assets stacked in this column, by name (cycled to fill the height). At least one."),
|
|
3317
3338
|
/** Constant start-position shift, 0..1 of a tile-set — de-aligns columns with similar content. */
|
|
3318
|
-
stagger:
|
|
3339
|
+
stagger: z4.number().min(0).max(1).default(0).describe("Start-position shift (0..1 of a tile-set) that de-aligns columns with similar content. Default 0."),
|
|
3319
3340
|
/** Scroll direction. Defaults to "down". */
|
|
3320
|
-
direction:
|
|
3341
|
+
direction: z4.enum(["up", "down"]).optional().describe("Scroll direction. Default 'down'."),
|
|
3321
3342
|
/** Continuous whole-clip loops for this column. Omit to inherit the wall-level `loops`. */
|
|
3322
|
-
loops:
|
|
3343
|
+
loops: z4.number().nonnegative().optional().describe("Continuous whole-clip loops for this column. Omit to inherit the wall-level loops."),
|
|
3323
3344
|
/** This column's pulses. Omit to inherit the wall-level `pulses`. */
|
|
3324
|
-
pulses:
|
|
3345
|
+
pulses: z4.array(wallPulseSchema).optional().describe("This column's pulses. Omit to inherit the wall-level pulses.")
|
|
3325
3346
|
}).strict();
|
|
3326
|
-
var fauxTileSchema =
|
|
3347
|
+
var fauxTileSchema = z4.object({
|
|
3327
3348
|
/** Box fill (any CSS color). Omit to auto-derive a distinct color from the tile name. */
|
|
3328
|
-
color:
|
|
3349
|
+
color: z4.string().optional().describe("Box fill (any CSS color). Omit to auto-derive a distinct color from the tile name."),
|
|
3329
3350
|
/** Optional caption shown under the name (e.g. "16:9") — purely cosmetic. */
|
|
3330
|
-
size:
|
|
3351
|
+
size: z4.string().optional().describe("Optional caption shown under the name (e.g. 16:9) \u2014 purely cosmetic."),
|
|
3331
3352
|
/** This faux tile's aspect ratio (width / height): 1.78 = 16:9 (short), 0.56 = 9:16 (tall), 1 =
|
|
3332
3353
|
* square. Omit to use the wall's `tileAspect` default. Real tiles use their media's own aspect. */
|
|
3333
|
-
aspect:
|
|
3354
|
+
aspect: z4.number().positive().optional().describe("Faux tile aspect (w/h): 1.78 = 16:9, 0.56 = 9:16, 1 = square. Omit to use the wall's tileAspect.")
|
|
3334
3355
|
}).strict();
|
|
3335
|
-
var wallSceneOptionsSchema =
|
|
3356
|
+
var wallSceneOptionsSchema = z4.object({
|
|
3336
3357
|
/** The columns (≥3) — each its own tiles + motion. Count = array length (fewer = bigger tiles). */
|
|
3337
|
-
columns:
|
|
3358
|
+
columns: z4.array(wallColumnSchema).min(3),
|
|
3338
3359
|
/** Gap between columns and between their tile contents (px). */
|
|
3339
|
-
gap:
|
|
3360
|
+
gap: z4.number().nonnegative().default(16),
|
|
3340
3361
|
/** Default/fallback tile aspect (width / height). Tiles fit the column width and take their OWN
|
|
3341
3362
|
* height from their media's aspect (16:9 → short, 9:16 → tall); this is only used for faux
|
|
3342
3363
|
* (`test`) tiles that don't set their own `aspect`. 1.6 = 16:10 landscape, <1 = portrait. */
|
|
3343
|
-
tileAspect:
|
|
3364
|
+
tileAspect: z4.number().positive().default(1.6),
|
|
3344
3365
|
/** Tile corner radius (px). */
|
|
3345
|
-
cornerRadius:
|
|
3366
|
+
cornerRadius: z4.number().nonnegative().default(12),
|
|
3346
3367
|
/** Backdrop shown in the gap gutters and behind tiles. Defaults to the scene's `background`. */
|
|
3347
|
-
background:
|
|
3368
|
+
background: z4.string().optional(),
|
|
3348
3369
|
/** System 1 — the X pan. */
|
|
3349
3370
|
pan: wallPanSchema.default({}),
|
|
3350
3371
|
/** Default continuous whole-clip loops for columns that omit their own `loops` (0 = static unless
|
|
3351
3372
|
* a pulse moves it; one pulse then rounds the total up to a single loop). */
|
|
3352
|
-
loops:
|
|
3373
|
+
loops: z4.number().nonnegative().default(0),
|
|
3353
3374
|
/** Default pulses for columns that omit their own `pulses` (the uniform wall-level motion). */
|
|
3354
|
-
pulses:
|
|
3375
|
+
pulses: z4.array(wallPulseSchema).default([]),
|
|
3355
3376
|
/** Preview mode: render every tile as a flat labeled color box (see `testTiles`) instead of the
|
|
3356
3377
|
* real assets, so you can dial in layout + motion instantly — no producers run. */
|
|
3357
|
-
test:
|
|
3378
|
+
test: z4.boolean().default(false),
|
|
3358
3379
|
/** Per-tile faux appearance for `test` mode, keyed by tile name. Tiles not listed get an
|
|
3359
3380
|
* auto-derived color and their name as the label. */
|
|
3360
|
-
testTiles:
|
|
3381
|
+
testTiles: z4.record(z4.string(), fauxTileSchema).default({})
|
|
3361
3382
|
}).strict();
|
|
3362
|
-
var reelItemSchema =
|
|
3383
|
+
var reelItemSchema = z4.object({
|
|
3363
3384
|
/** Color display name (already cased per `uppercase`). */
|
|
3364
|
-
name:
|
|
3385
|
+
name: z4.string(),
|
|
3365
3386
|
/** Swatch background hex (`#RRGGBB`). */
|
|
3366
|
-
hex:
|
|
3387
|
+
hex: z4.string(),
|
|
3367
3388
|
/** Contrast-picked text color for this band. */
|
|
3368
|
-
textColor:
|
|
3389
|
+
textColor: z4.string(),
|
|
3369
3390
|
/** Preformatted detail lines revealed on expand (hex / oklch / rgb …). */
|
|
3370
|
-
details:
|
|
3391
|
+
details: z4.array(z4.string())
|
|
3371
3392
|
}).strict();
|
|
3372
|
-
var paletteReelSceneOptionsSchema =
|
|
3373
|
-
items:
|
|
3374
|
-
orientation:
|
|
3375
|
-
holdSeconds:
|
|
3376
|
-
transitionSeconds:
|
|
3377
|
-
bounce:
|
|
3378
|
-
easing:
|
|
3379
|
-
grownFlex:
|
|
3380
|
-
minCrossPx:
|
|
3381
|
-
nameAlwaysVisible:
|
|
3382
|
-
fontWeight:
|
|
3383
|
-
fontSize:
|
|
3384
|
-
detailFontScale:
|
|
3385
|
-
gap:
|
|
3386
|
-
cornerRadius:
|
|
3393
|
+
var paletteReelSceneOptionsSchema = z4.object({
|
|
3394
|
+
items: z4.array(reelItemSchema).min(1),
|
|
3395
|
+
orientation: z4.enum(["rows", "columns"]).default("rows"),
|
|
3396
|
+
holdSeconds: z4.number().positive().default(2),
|
|
3397
|
+
transitionSeconds: z4.number().positive().default(0.7),
|
|
3398
|
+
bounce: z4.boolean().default(true),
|
|
3399
|
+
easing: z4.enum(["linear", "ease-in", "ease-out", "ease-in-out"]).default("ease-in-out"),
|
|
3400
|
+
grownFlex: z4.number().min(1).default(12),
|
|
3401
|
+
minCrossPx: z4.number().nonnegative().default(0),
|
|
3402
|
+
nameAlwaysVisible: z4.boolean().default(true),
|
|
3403
|
+
fontWeight: z4.number().int().min(1).max(1e3).default(700),
|
|
3404
|
+
fontSize: z4.number().positive().optional(),
|
|
3405
|
+
detailFontScale: z4.number().positive().default(0.62),
|
|
3406
|
+
gap: z4.number().nonnegative().default(0),
|
|
3407
|
+
cornerRadius: z4.number().nonnegative().default(0)
|
|
3387
3408
|
}).strict();
|
|
3388
3409
|
var SCENE_OPTION_SCHEMAS = {
|
|
3389
3410
|
specimen: specimenSceneOptionsSchema,
|
|
@@ -3392,64 +3413,64 @@ var SCENE_OPTION_SCHEMAS = {
|
|
|
3392
3413
|
};
|
|
3393
3414
|
|
|
3394
3415
|
// src/generators/wall/options.ts
|
|
3395
|
-
var wallOptionsSchema =
|
|
3416
|
+
var wallOptionsSchema = z5.object({
|
|
3396
3417
|
// --- output ---
|
|
3397
3418
|
/** Output width (CSS px). */
|
|
3398
|
-
width:
|
|
3419
|
+
width: z5.number().int().positive().default(1920).describe("Output width in CSS px. Default 1920."),
|
|
3399
3420
|
/** Output height (CSS px). */
|
|
3400
|
-
height:
|
|
3421
|
+
height: z5.number().int().positive().default(1080).describe("Output height in CSS px. Default 1080."),
|
|
3401
3422
|
/** Render scale (2 = retina-crisp, downscaled into the video). */
|
|
3402
|
-
deviceScaleFactor:
|
|
3423
|
+
deviceScaleFactor: z5.number().positive().max(4).default(2).describe("Render scale (2 = retina-crisp, downscaled into the video). Default 2."),
|
|
3403
3424
|
/** Output frames per second. */
|
|
3404
|
-
fps:
|
|
3405
|
-
/** Clip length (
|
|
3406
|
-
|
|
3425
|
+
fps: z5.number().int().positive().max(120).default(30).describe("Output frames per second. Default 30."),
|
|
3426
|
+
/** Clip length (ms) — the whole loop. Tile videos should loop within a length dividing this. */
|
|
3427
|
+
durationMs: z5.number().positive().default(16e3).describe("Clip length in ms \u2014 the whole loop. Default 16000."),
|
|
3407
3428
|
/** x264 quality, 0–51 (lower = better/larger). */
|
|
3408
|
-
crf:
|
|
3429
|
+
crf: z5.number().int().min(0).max(51).default(18).describe("x264 quality, 0\u201351 (lower = better quality / larger file). Default 18."),
|
|
3409
3430
|
/** Capture strategy. "frames" (default) is deterministic + parallelizable. */
|
|
3410
|
-
capture:
|
|
3431
|
+
capture: z5.enum(["frames", "realtime"]).default("frames").describe('Capture strategy. "frames" (default) is deterministic + parallelizable; "realtime" records the live session.'),
|
|
3411
3432
|
/** Parallel frame-render workers. Video-heavy walls can cold-start black under many workers —
|
|
3412
3433
|
* set 1 (or omit) for those. */
|
|
3413
|
-
workers:
|
|
3434
|
+
workers: z5.number().int().positive().optional().describe("Parallel frame-render workers. Video-heavy walls can cold-start to black under many workers \u2014 set 1 (or omit). Auto-picks from cores."),
|
|
3414
3435
|
/** Intermediate frame format (frames capture only). "jpeg" (default) is fast; "png" is lossless. */
|
|
3415
|
-
frameFormat:
|
|
3436
|
+
frameFormat: z5.enum(["jpeg", "png"]).default("jpeg").describe('Intermediate frame format (frames capture). "jpeg" (default) is fast; "png" is lossless.'),
|
|
3416
3437
|
/** Backdrop shown in the gutters between tiles. */
|
|
3417
|
-
background:
|
|
3438
|
+
background: z5.string().default("#0b0b0f").describe('Backdrop shown in the gutters between tiles. Default "#0b0b0f".'),
|
|
3418
3439
|
/** Output filename; defaults to "<slug(asset name)>.mp4". */
|
|
3419
|
-
fileName:
|
|
3440
|
+
fileName: z5.string().optional().describe('Output filename; defaults to "<slug(asset name)>.mp4".'),
|
|
3420
3441
|
// --- columns (System 2 + layout): each column = its tiles + its own motion ---
|
|
3421
3442
|
/** The columns (≥3) — each its own tiles + motion. Count = array length (fewer = larger tiles). */
|
|
3422
|
-
columns:
|
|
3443
|
+
columns: z5.array(wallColumnSchema).min(3).describe("The columns (\u22653) \u2014 each lists its stacked `tiles` (by name) and may carry its own motion. Count = columns.length."),
|
|
3423
3444
|
/** Gap between columns and between tiles (px). */
|
|
3424
|
-
gap:
|
|
3445
|
+
gap: z5.number().nonnegative().default(8).describe("Gap between columns and between tiles (px). Default 8."),
|
|
3425
3446
|
/** Default/fallback tile aspect (width / height). Tiles fit the column width and take their OWN
|
|
3426
3447
|
* height from their media's aspect (16:9 → short, 9:16 → tall); this is only used for faux
|
|
3427
3448
|
* (`test`) tiles that don't set their own `aspect`. 0.75 = 3:4 portrait. */
|
|
3428
|
-
tileAspect:
|
|
3449
|
+
tileAspect: z5.number().positive().default(0.75).describe("Default/fallback tile aspect (w/h) \u2014 only for faux (test) tiles without their own `aspect`. 0.75 = 3:4 portrait. Default 0.75."),
|
|
3429
3450
|
/** Tile corner radius (px). */
|
|
3430
|
-
cornerRadius:
|
|
3451
|
+
cornerRadius: z5.number().nonnegative().default(6).describe("Tile corner radius (px). Default 6."),
|
|
3431
3452
|
// --- motion (uniform pulse model) ---
|
|
3432
3453
|
/** System 1 — the whole-wall X pan. */
|
|
3433
3454
|
pan: wallPanSchema.default({}).describe("System 1 \u2014 the whole wall's horizontal pan (`direction` / `loops` / `pulses`). Default: no pan."),
|
|
3434
3455
|
/** Default continuous whole-clip loops for columns that omit their own `loops` (0 = static unless
|
|
3435
3456
|
* a pulse moves it; one pulse then rounds the total up to a single loop). */
|
|
3436
|
-
loops:
|
|
3457
|
+
loops: z5.number().nonnegative().default(0).describe("Default continuous whole-clip loops for columns that omit their own `loops`. Default 0 (static unless a pulse moves it)."),
|
|
3437
3458
|
/** Default pulses for columns that omit their own `pulses` (the uniform wall-level motion). */
|
|
3438
|
-
pulses:
|
|
3459
|
+
pulses: z5.array(wallPulseSchema).default([]).describe("Default pulses for columns that omit their own `pulses` (the uniform wall-level motion). Default none."),
|
|
3439
3460
|
// --- test / preview ---
|
|
3440
3461
|
/** Preview mode: render every tile as a flat labeled color box (see `testTiles`) instead of the
|
|
3441
3462
|
* real assets. No producer assets run, so the wall renders in seconds — use it to dial in
|
|
3442
3463
|
* layout + motion, then turn it off for the real render. */
|
|
3443
|
-
test:
|
|
3464
|
+
test: z5.boolean().default(false).describe("Preview mode: render every tile as a flat labeled color box instead of real assets, so the wall renders in seconds. Default false."),
|
|
3444
3465
|
/** Per-tile faux appearance for `test` mode, keyed by tile name. Tiles not listed get an
|
|
3445
3466
|
* auto-derived color and their name as the label. */
|
|
3446
|
-
testTiles:
|
|
3467
|
+
testTiles: z5.record(z5.string(), fauxTileSchema).default({}).describe("Per-tile faux appearance for `test` mode, keyed by tile name (color + caption). Default {} (auto colors + names).")
|
|
3447
3468
|
}).strict();
|
|
3448
3469
|
|
|
3449
3470
|
// src/generators/scene/index.ts
|
|
3450
3471
|
import path13 from "path";
|
|
3451
3472
|
import { fileURLToPath } from "url";
|
|
3452
|
-
import { copyFile as copyFile2, rename, stat as stat4 } from "fs/promises";
|
|
3473
|
+
import { copyFile as copyFile2, rename as rename2, stat as stat4 } from "fs/promises";
|
|
3453
3474
|
|
|
3454
3475
|
// src/scene/serve.ts
|
|
3455
3476
|
import http from "http";
|
|
@@ -3774,7 +3795,7 @@ async function renderScene(ctx, options, generatorId = SCENE_ID) {
|
|
|
3774
3795
|
});
|
|
3775
3796
|
}
|
|
3776
3797
|
try {
|
|
3777
|
-
await
|
|
3798
|
+
await rename2(composedTmp, outPath);
|
|
3778
3799
|
} catch {
|
|
3779
3800
|
await copyFile2(composedTmp, outPath);
|
|
3780
3801
|
}
|
|
@@ -3815,7 +3836,8 @@ async function run3(ctx, o) {
|
|
|
3815
3836
|
background: o.background,
|
|
3816
3837
|
deviceScaleFactor: o.deviceScaleFactor,
|
|
3817
3838
|
fps: o.fps,
|
|
3818
|
-
|
|
3839
|
+
// The scene wire format keeps seconds internally; the authoring surface is milliseconds.
|
|
3840
|
+
durationSeconds: o.durationMs / 1e3,
|
|
3819
3841
|
capture: o.capture,
|
|
3820
3842
|
workers: o.workers,
|
|
3821
3843
|
frameFormat: o.frameFormat,
|
|
@@ -3850,11 +3872,147 @@ var wallGenerator = {
|
|
|
3850
3872
|
run: run3
|
|
3851
3873
|
};
|
|
3852
3874
|
|
|
3875
|
+
// src/generators/specimen/options.ts
|
|
3876
|
+
import { z as z6 } from "zod";
|
|
3877
|
+
var pulseSchema = z6.object({
|
|
3878
|
+
/** Human label for the beat, e.g. "color sweep" — purely to keep the config readable. */
|
|
3879
|
+
name: z6.string().default("").describe('Human label for the beat, e.g. "color sweep" \u2014 purely to keep the config readable.'),
|
|
3880
|
+
/** Length of this beat (ms). */
|
|
3881
|
+
durationMs: z6.number().positive().describe("Length of this beat in ms."),
|
|
3882
|
+
/** Fraction of cells whose glyph changes during this beat (0..1; 1 = every cell once). */
|
|
3883
|
+
chars: z6.number().nonnegative().default(0).describe("Fraction of cells whose glyph changes during this beat (0..1; 1 = every cell once; 0 = a hold). Default 0."),
|
|
3884
|
+
/** Fraction of cells whose color changes during this beat (0..1; 1 = every cell once). */
|
|
3885
|
+
colors: z6.number().nonnegative().default(0).describe("Fraction of cells whose color changes during this beat (0..1; 1 = every cell once). Default 0."),
|
|
3886
|
+
/**
|
|
3887
|
+
* Target color for this beat's color changes. When set, every color change in the beat goes to
|
|
3888
|
+
* this exact token (a deliberate sweep) instead of a weighted-random pick. Set `colors: 1` with
|
|
3889
|
+
* `pacing: "even"` to wash the whole specimen to one color evenly. Omit for the default
|
|
3890
|
+
* scattered, weighted-random recoloring.
|
|
3891
|
+
*/
|
|
3892
|
+
color: z6.enum(["foreground", "muted", "accent"]).optional().describe("Target color token for this beat's color changes (a deliberate sweep); omit for the default weighted-random recoloring."),
|
|
3893
|
+
/**
|
|
3894
|
+
* How the changes are distributed in time across the beat — like a CSS easing curve:
|
|
3895
|
+
* "linear"/"even" = uniform, "ease-in" = front-loaded, "ease-out" = back-loaded,
|
|
3896
|
+
* "ease-in-out" = bunched at both ends, "random" = scattered.
|
|
3897
|
+
*/
|
|
3898
|
+
pacing: z6.enum(["even", "linear", "ease-in", "ease-out", "ease-in-out", "random"]).default("even").describe("How changes are distributed in time across the beat (CSS-easing-like): even/linear, ease-in, ease-out, ease-in-out, random. Default even.")
|
|
3899
|
+
}).strict();
|
|
3900
|
+
var P = (name, durationMs, chars = 0, colors = 0) => ({
|
|
3901
|
+
name,
|
|
3902
|
+
durationMs,
|
|
3903
|
+
chars,
|
|
3904
|
+
colors,
|
|
3905
|
+
pacing: "even"
|
|
3906
|
+
});
|
|
3907
|
+
var DEFAULT_PULSES = [
|
|
3908
|
+
P("intro hold", 800),
|
|
3909
|
+
P("first letters", 800, 0.15, 0),
|
|
3910
|
+
P("settle", 1500),
|
|
3911
|
+
P("ripple", 1e3, 0.08, 0.04),
|
|
3912
|
+
P("color sweep", 1200, 0, 0.13),
|
|
3913
|
+
P("rest", 1200),
|
|
3914
|
+
P("quick burst", 600, 0.18, 0),
|
|
3915
|
+
P("drift", 1200, 0, 0.08),
|
|
3916
|
+
P("finale", 1200, 0.18, 0.13),
|
|
3917
|
+
P("outro hold", 500)
|
|
3918
|
+
];
|
|
3919
|
+
var DEMO_PULSES = [
|
|
3920
|
+
{ name: "linear", durationMs: 5e3, chars: 0.5, pacing: "linear" },
|
|
3921
|
+
{ name: "hold", durationMs: 2e3 },
|
|
3922
|
+
{ name: "ease-in", durationMs: 5e3, chars: 0.5, pacing: "ease-in" },
|
|
3923
|
+
{ name: "hold", durationMs: 2e3 },
|
|
3924
|
+
{ name: "ease-out", durationMs: 5e3, chars: 0.5, pacing: "ease-out" },
|
|
3925
|
+
{ name: "hold", durationMs: 2e3 },
|
|
3926
|
+
{ name: "ease-in-out", durationMs: 5e3, chars: 0.5, pacing: "ease-in-out" },
|
|
3927
|
+
{ name: "hold", durationMs: 2e3 },
|
|
3928
|
+
{ name: "random", durationMs: 5e3, chars: 0.5, pacing: "random" },
|
|
3929
|
+
{ name: "hold", durationMs: 2e3 },
|
|
3930
|
+
// Even, per-character color sweeps: each washes every glyph to one token, evenly across the beat.
|
|
3931
|
+
{ name: "sweep \u2192 muted", durationMs: 4e3, colors: 1, color: "muted", pacing: "even" },
|
|
3932
|
+
{ name: "sweep \u2192 accent", durationMs: 4e3, colors: 1, color: "accent", pacing: "even" },
|
|
3933
|
+
{ name: "sweep \u2192 foreground", durationMs: 4e3, colors: 1, color: "foreground", pacing: "even" },
|
|
3934
|
+
{ name: "hold", durationMs: 2e3 },
|
|
3935
|
+
{ name: "weighted recolor", durationMs: 5e3, colors: 0.6 },
|
|
3936
|
+
{ name: "hold", durationMs: 2e3 },
|
|
3937
|
+
{ name: "mingle", durationMs: 5e3, chars: 0.3, colors: 0.3 }
|
|
3938
|
+
];
|
|
3939
|
+
var SWEEP_COLORS = {
|
|
3940
|
+
background: "#0b0b0f",
|
|
3941
|
+
foreground: "#f4f4f5",
|
|
3942
|
+
muted: "#6b7280",
|
|
3943
|
+
accent: "#7c9cff"
|
|
3944
|
+
};
|
|
3945
|
+
var SWEEP_PULSES = [
|
|
3946
|
+
{ name: "hold", durationMs: 800 },
|
|
3947
|
+
{ name: "to muted", durationMs: 2200, colors: 1, color: "muted", pacing: "ease-in-out" },
|
|
3948
|
+
{ name: "settle", durationMs: 600 },
|
|
3949
|
+
{ name: "to accent", durationMs: 2200, colors: 1, color: "accent", pacing: "ease-in-out" },
|
|
3950
|
+
{ name: "settle", durationMs: 600 },
|
|
3951
|
+
{ name: "to foreground", durationMs: 2200, colors: 1, color: "foreground", pacing: "ease-in-out" },
|
|
3952
|
+
{ name: "glyph drift", durationMs: 1400, chars: 0.5, pacing: "even" },
|
|
3953
|
+
{ name: "hold", durationMs: 600 }
|
|
3954
|
+
];
|
|
3955
|
+
var SPECIMEN_TEMPLATES = {
|
|
3956
|
+
demo: { demo: true, mirror: false, lines: 4, pulses: DEMO_PULSES },
|
|
3957
|
+
sweep: {
|
|
3958
|
+
mirror: true,
|
|
3959
|
+
lines: 4,
|
|
3960
|
+
colors: SWEEP_COLORS,
|
|
3961
|
+
pulses: SWEEP_PULSES
|
|
3962
|
+
}
|
|
3963
|
+
};
|
|
3964
|
+
function applyTemplate(raw) {
|
|
3965
|
+
if (!raw || typeof raw !== "object") return raw;
|
|
3966
|
+
const r = raw;
|
|
3967
|
+
const tmpl = typeof r.template === "string" ? SPECIMEN_TEMPLATES[r.template] : void 0;
|
|
3968
|
+
return tmpl ? { ...tmpl, ...r } : raw;
|
|
3969
|
+
}
|
|
3970
|
+
var specimenObjectSchema = z6.object({
|
|
3971
|
+
font: z6.string().min(1).describe("Font file to showcase (path relative to the working dir, or absolute). Required."),
|
|
3972
|
+
template: z6.enum(["demo", "sweep"]).optional().describe("Load a named option preset (demo or sweep); your explicit options still override what it sets."),
|
|
3973
|
+
name: z6.string().default("").describe('Display name shown bottom-left (e.g. "ABC Oracle"). Default none.'),
|
|
3974
|
+
demo: z6.boolean().default(false).describe("Demo mode: overlay the active pulse's name bottom-right, to see which beat is playing. Default false."),
|
|
3975
|
+
fps: z6.number().int().positive().max(120).default(30).describe("Output frames per second. Default 30."),
|
|
3976
|
+
durationMs: z6.number().positive().optional().describe("Clip length in ms. Defaults to the (mirrored) sum of the pulse durations; set to override."),
|
|
3977
|
+
width: z6.number().int().positive().default(1920).describe("Output frame width in px. Default 1920."),
|
|
3978
|
+
height: z6.number().int().positive().default(1080).describe("Output frame height in px. Default 1080."),
|
|
3979
|
+
deviceScaleFactor: z6.number().positive().max(4).default(1).describe("Render scale (1 = 1:1; higher = crisper capture, downscaled into the video). Default 1."),
|
|
3980
|
+
weight: z6.number().int().min(1).max(1e3).default(820).describe("Glyph weight on the variable-font axis, 1\u20131000. Default 820."),
|
|
3981
|
+
lines: z6.number().int().min(1).max(40).default(3).describe("Number of glyph rows; glyph size is derived so rows fill the top 80% of the frame. Default 3."),
|
|
3982
|
+
leading: z6.number().positive().default(0.78).describe("Line-height of the glyph block. Default 0.78 (tight, cap-height-hugging)."),
|
|
3983
|
+
blacklist: z6.string().default("").describe('Glyphs to exclude from the showcase, e.g. "QXZ" (case-insensitive). Default none.'),
|
|
3984
|
+
characterPool: z6.string().refine((s) => (/* @__PURE__ */ new Set([...s.trim()])).size >= 2, "characterPool needs \u22652 distinct characters").optional().describe("Override the glyph pool the specimen draws from (\u22652 distinct characters). Default A\u2013Z 0\u20139 + symbols."),
|
|
3985
|
+
seed: z6.number().int().default(1).describe("Schedule seed \u2014 same seed \u21D2 identical animation. Change for a different deterministic take. Default 1."),
|
|
3986
|
+
colors: z6.object({
|
|
3987
|
+
background: z6.string().default("#eceef1").describe("Backdrop behind the glyphs."),
|
|
3988
|
+
foreground: z6.string().default("#16181d").describe("Primary glyph color \u2014 the resting majority."),
|
|
3989
|
+
muted: z6.string().default("#a7adb6").describe("Muted/secondary glyph color."),
|
|
3990
|
+
accent: z6.string().optional().describe("Accent color for occasional pops; defaults to `background` (accent glyphs blend in) if unset."),
|
|
3991
|
+
// defaults to `background` at render (accent glyphs blend in)
|
|
3992
|
+
label: z6.string().optional().describe("Color of the font-name label (bottom corner); defaults to `foreground` if unset.")
|
|
3993
|
+
// defaults to `foreground` at render
|
|
3994
|
+
}).default({}).describe("Color tokens the glyphs cycle through (any CSS colors). Override any subset. Default: light-grey palette."),
|
|
3995
|
+
colorWeights: z6.object({
|
|
3996
|
+
foreground: z6.number().nonnegative().default(2).describe("Relative likelihood of the foreground token on a random color change. Default 2."),
|
|
3997
|
+
muted: z6.number().nonnegative().default(2).describe("Relative likelihood of the muted token on a random color change. Default 2."),
|
|
3998
|
+
accent: z6.number().nonnegative().default(1).describe("Relative likelihood of the accent token on a random color change. Default 1.")
|
|
3999
|
+
}).default({}).describe("Relative likelihood of each color token on a random (non-targeted) color change. Default 2 / 2 / 1."),
|
|
4000
|
+
pulses: z6.array(pulseSchema).min(1).default(DEFAULT_PULSES).describe("The animation storyboard: an ordered sequence of pulses (beats). Default: a lively built-in storyboard."),
|
|
4001
|
+
characterIntensity: z6.number().nonnegative().default(1).describe("Multiply every pulse's glyph-change fraction (1 = baseline, 2 = twice as busy, 0 = none). Default 1."),
|
|
4002
|
+
colorIntensity: z6.number().nonnegative().default(1).describe("Multiply every pulse's color-change fraction (1 = baseline, 2 = twice as busy, 0 = none). Default 1."),
|
|
4003
|
+
maxLineDrift: z6.number().positive().max(0.5).default(0.05).describe("Max fraction a line's total width may drift as its glyphs change; swaps are width-compensated. Default 0.05."),
|
|
4004
|
+
mirror: z6.boolean().default(true).describe("Mirror the pulses (play out and back) for a seamless loop; doubles the clip length. Default true."),
|
|
4005
|
+
crf: z6.number().int().min(0).max(51).default(18).describe("x264 quality, 0\u201351 (lower = better quality / larger file). Default 18."),
|
|
4006
|
+
fileName: z6.string().optional().describe('Output filename; defaults to "<slug(asset name)>.mp4".')
|
|
4007
|
+
}).strict();
|
|
4008
|
+
var specimenOptionsSchema = z6.preprocess(applyTemplate, specimenObjectSchema);
|
|
4009
|
+
|
|
3853
4010
|
// src/generators/specimen/index.ts
|
|
3854
4011
|
var SPECIMEN_ID = "specimen";
|
|
3855
4012
|
async function run4(ctx, o) {
|
|
3856
|
-
const
|
|
3857
|
-
const durationSeconds = o.
|
|
4013
|
+
const pulsesTotalMs = o.pulses.reduce((sum, p) => sum + p.durationMs, 0);
|
|
4014
|
+
const durationSeconds = (o.durationMs ?? (o.mirror ? pulsesTotalMs * 2 : pulsesTotalMs)) / 1e3;
|
|
4015
|
+
const wirePulses = o.pulses.map(({ durationMs, ...p }) => ({ ...p, duration: durationMs / 1e3 }));
|
|
3858
4016
|
const sceneOptions = {
|
|
3859
4017
|
scene: "specimen",
|
|
3860
4018
|
width: o.width,
|
|
@@ -3881,7 +4039,7 @@ async function run4(ctx, o) {
|
|
|
3881
4039
|
characterPool: o.characterPool,
|
|
3882
4040
|
colors: o.colors,
|
|
3883
4041
|
colorWeights: o.colorWeights,
|
|
3884
|
-
pulses:
|
|
4042
|
+
pulses: wirePulses,
|
|
3885
4043
|
mirror: o.mirror,
|
|
3886
4044
|
characterIntensity: o.characterIntensity,
|
|
3887
4045
|
colorIntensity: o.colorIntensity,
|
|
@@ -4217,13 +4375,13 @@ var paletteReelObjectSchema = z8.object({
|
|
|
4217
4375
|
details: z8.array(fieldEnum2).default(["hex", "oklch", "rgb"]).describe(
|
|
4218
4376
|
"Fields revealed when a color expands (the name is always shown, so it's ignored here). Default hex + oklch + rgb."
|
|
4219
4377
|
),
|
|
4220
|
-
|
|
4221
|
-
|
|
4378
|
+
holdMs: z8.number().positive().default(2e3).describe("How long each color stays fully open before handing off to the next (ms). Default 2000."),
|
|
4379
|
+
transitionMs: z8.number().positive().default(700).describe("Crossfade length from one open color to the next (ms). Default 700."),
|
|
4222
4380
|
bounce: z8.boolean().default(true).describe(
|
|
4223
4381
|
"Ping-pong the sweep so each handoff is between neighbouring bands; off wraps directly (last to first). Default true."
|
|
4224
4382
|
),
|
|
4225
4383
|
easing: z8.enum(["linear", "ease-in", "ease-out", "ease-in-out"]).default("ease-in-out").describe('Easing applied to the crossfade ramp. Default "ease-in-out".'),
|
|
4226
|
-
|
|
4384
|
+
durationMs: z8.number().positive().optional().describe("Clip length override (ms). Omit to derive (count x (hold + transition)) for a clean loop."),
|
|
4227
4385
|
grownFlex: z8.number().min(1).default(12).describe(
|
|
4228
4386
|
"How many times a sliver's share a fully-open band takes (a collapsed sliver is the baseline). Default 12."
|
|
4229
4387
|
),
|
|
@@ -4255,10 +4413,10 @@ var paletteReelOptionsSchema = paletteReelObjectSchema;
|
|
|
4255
4413
|
|
|
4256
4414
|
// src/generators/palette-reel/index.ts
|
|
4257
4415
|
var PALETTE_REEL_ID = "palette-reel";
|
|
4258
|
-
function
|
|
4416
|
+
function deriveDurationMs(o) {
|
|
4259
4417
|
const n = o.colors.length;
|
|
4260
4418
|
const stops = n <= 1 ? n : o.bounce ? 2 * (n - 1) : n;
|
|
4261
|
-
return stops * (o.
|
|
4419
|
+
return stops * (o.holdMs + o.transitionMs);
|
|
4262
4420
|
}
|
|
4263
4421
|
async function run6(ctx, o) {
|
|
4264
4422
|
const fmt = { uppercaseName: o.uppercase, rgbStyle: o.rgbStyle, oklchStyle: o.oklchStyle };
|
|
@@ -4276,7 +4434,7 @@ async function run6(ctx, o) {
|
|
|
4276
4434
|
details: fields.map((f) => formatField(color, f, fmt))
|
|
4277
4435
|
};
|
|
4278
4436
|
});
|
|
4279
|
-
const durationSeconds = o.
|
|
4437
|
+
const durationSeconds = (o.durationMs ?? deriveDurationMs(o)) / 1e3;
|
|
4280
4438
|
const sceneOptions = {
|
|
4281
4439
|
scene: PALETTE_REEL_ID,
|
|
4282
4440
|
width: o.width,
|
|
@@ -4296,8 +4454,8 @@ async function run6(ctx, o) {
|
|
|
4296
4454
|
sceneOptions: {
|
|
4297
4455
|
items,
|
|
4298
4456
|
orientation: o.orientation,
|
|
4299
|
-
holdSeconds: o.
|
|
4300
|
-
transitionSeconds: o.
|
|
4457
|
+
holdSeconds: o.holdMs / 1e3,
|
|
4458
|
+
transitionSeconds: o.transitionMs / 1e3,
|
|
4301
4459
|
bounce: o.bounce,
|
|
4302
4460
|
easing: o.easing,
|
|
4303
4461
|
grownFlex: o.grownFlex,
|
|
@@ -4402,6 +4560,9 @@ function getGenerator(id) {
|
|
|
4402
4560
|
function listGenerators() {
|
|
4403
4561
|
return [...registry.values()];
|
|
4404
4562
|
}
|
|
4563
|
+
function generatorIds() {
|
|
4564
|
+
return [...registry.keys()];
|
|
4565
|
+
}
|
|
4405
4566
|
register(scrollReelGenerator);
|
|
4406
4567
|
register(screenshotsGenerator);
|
|
4407
4568
|
register(wallGenerator);
|
|
@@ -4501,10 +4662,52 @@ var CONFIG_FILES = [
|
|
|
4501
4662
|
".pro-visurc",
|
|
4502
4663
|
".pro-visurc.json"
|
|
4503
4664
|
];
|
|
4504
|
-
var
|
|
4665
|
+
var FRAMEWORKS = [
|
|
4666
|
+
["next", "Next.js", 3e3],
|
|
4667
|
+
["nuxt", "Nuxt", 3e3],
|
|
4668
|
+
["astro", "Astro", 4321],
|
|
4669
|
+
["@sveltejs/kit", "SvelteKit", 5173],
|
|
4670
|
+
["gatsby", "Gatsby", 8e3],
|
|
4671
|
+
["@remix-run/dev", "Remix", 3e3],
|
|
4672
|
+
["vite", "Vite", 5173]
|
|
4673
|
+
];
|
|
4674
|
+
function detectProject(cwd) {
|
|
4675
|
+
let pkg = {};
|
|
4676
|
+
try {
|
|
4677
|
+
pkg = JSON.parse(readFileSync(path17.join(cwd, "package.json"), "utf8"));
|
|
4678
|
+
} catch {
|
|
4679
|
+
}
|
|
4680
|
+
const pmField = typeof pkg.packageManager === "string" ? pkg.packageManager.split("@")[0] : "";
|
|
4681
|
+
const pm = pmField === "pnpm" || pmField === "yarn" || pmField === "bun" || pmField === "npm" ? pmField : existsSync4(path17.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync4(path17.join(cwd, "yarn.lock")) ? "yarn" : existsSync4(path17.join(cwd, "bun.lockb")) || existsSync4(path17.join(cwd, "bun.lock")) ? "bun" : "npm";
|
|
4682
|
+
const deps = {
|
|
4683
|
+
...pkg.dependencies,
|
|
4684
|
+
...pkg.devDependencies
|
|
4685
|
+
};
|
|
4686
|
+
const match = FRAMEWORKS.find(([dep]) => deps[dep]);
|
|
4687
|
+
const devScript = pkg.scripts?.dev ?? "";
|
|
4688
|
+
const flag = /(?:-p|--port)[ =](\d{2,5})/.exec(devScript);
|
|
4689
|
+
const devPort = flag ? Number(flag[1]) : match?.[2];
|
|
4690
|
+
let localDep = false;
|
|
4691
|
+
try {
|
|
4692
|
+
createRequire2(path17.join(cwd, "package.json")).resolve("pro-visu/package.json");
|
|
4693
|
+
localDep = true;
|
|
4694
|
+
} catch {
|
|
4695
|
+
localDep = false;
|
|
4696
|
+
}
|
|
4697
|
+
return { pm, framework: match?.[1], devPort, localDep };
|
|
4698
|
+
}
|
|
4699
|
+
function runCmd(pm, script) {
|
|
4700
|
+
return pm === "npm" ? `npm run ${script}` : `${pm} ${script}`;
|
|
4701
|
+
}
|
|
4702
|
+
function tsConfigTemplate(info) {
|
|
4703
|
+
const port = info.devPort ?? 3101;
|
|
4704
|
+
const urlComment = info.devPort ? `// Your ${info.framework} dev server's URL \u2014 start it (\`${runCmd(info.pm, "dev")}\`) before generating,
|
|
4705
|
+
// point this at a deployed site, or enable the managed \`server\` block below instead.` : "// URL the capture assets point at \u2014 your running site, a deployed URL, or the managed server below.";
|
|
4706
|
+
const start = info.pm === "npm" ? "npm start" : `${info.pm} start`;
|
|
4707
|
+
return `import { defineConfig } from "pro-visu";
|
|
4505
4708
|
|
|
4506
|
-
|
|
4507
|
-
const URL = "http://localhost
|
|
4709
|
+
${urlComment}
|
|
4710
|
+
const URL = "http://localhost:${port}";
|
|
4508
4711
|
|
|
4509
4712
|
export default defineConfig({
|
|
4510
4713
|
settings: {
|
|
@@ -4514,10 +4717,11 @@ export default defineConfig({
|
|
|
4514
4717
|
// installed Chrome, or args: ["--no-sandbox"] on CI.
|
|
4515
4718
|
browser: { headless: true },
|
|
4516
4719
|
// Optional: let the tool build + start your site, wait for it, capture, then stop it.
|
|
4517
|
-
//
|
|
4720
|
+
// PORT is set in the command's env (default 3101), so PORT-honoring frameworks bind it
|
|
4721
|
+
// automatically. If you enable this, point URL above at the same port.
|
|
4518
4722
|
// server: {
|
|
4519
|
-
// build: "
|
|
4520
|
-
// command: "
|
|
4723
|
+
// build: "${runCmd(info.pm, "build")}",
|
|
4724
|
+
// command: "${start}",
|
|
4521
4725
|
// port: 3101,
|
|
4522
4726
|
// },
|
|
4523
4727
|
defaults: {
|
|
@@ -4530,7 +4734,7 @@ export default defineConfig({
|
|
|
4530
4734
|
name: "home-reel",
|
|
4531
4735
|
url: URL,
|
|
4532
4736
|
generator: "scroll-reel",
|
|
4533
|
-
// options: {
|
|
4737
|
+
// options: { durationMs: 7000, waitForSelector: "main" },
|
|
4534
4738
|
},
|
|
4535
4739
|
// A looping type-specimen from a font file (no URL needed):
|
|
4536
4740
|
// {
|
|
@@ -4541,7 +4745,10 @@ export default defineConfig({
|
|
|
4541
4745
|
],
|
|
4542
4746
|
});
|
|
4543
4747
|
`;
|
|
4544
|
-
|
|
4748
|
+
}
|
|
4749
|
+
function jsonConfigTemplate(info) {
|
|
4750
|
+
const port = info.devPort ?? 3101;
|
|
4751
|
+
return `{
|
|
4545
4752
|
"$schema": "./${DEFAULT_SCHEMA_FILE}",
|
|
4546
4753
|
"settings": {
|
|
4547
4754
|
"outDir": "pro-visu",
|
|
@@ -4552,26 +4759,36 @@ var JSON_CONFIG_TEMPLATE = `{
|
|
|
4552
4759
|
}
|
|
4553
4760
|
},
|
|
4554
4761
|
"assets": [
|
|
4555
|
-
{ "name": "home-reel", "url": "http://localhost
|
|
4762
|
+
{ "name": "home-reel", "url": "http://localhost:${port}", "generator": "scroll-reel" }
|
|
4556
4763
|
]
|
|
4557
4764
|
}
|
|
4558
4765
|
`;
|
|
4766
|
+
}
|
|
4559
4767
|
async function runInit(options = {}) {
|
|
4560
4768
|
const cwd = resolveCwd(options.cwd);
|
|
4561
4769
|
const logger = createLogger("info");
|
|
4562
4770
|
let createdSomething = false;
|
|
4563
|
-
const
|
|
4771
|
+
const info = detectProject(cwd);
|
|
4772
|
+
if (info.framework) {
|
|
4773
|
+
logger.info(`detected ${info.framework} (${info.pm}${info.devPort ? `, dev port ${info.devPort}` : ""})`);
|
|
4774
|
+
}
|
|
4775
|
+
let useJson = Boolean(options.json);
|
|
4776
|
+
if (!useJson && !info.localDep) {
|
|
4777
|
+
useJson = true;
|
|
4778
|
+
logger.info("pro-visu isn't a local dependency \u2014 scaffolding a JSON config (works via npx/global)");
|
|
4779
|
+
}
|
|
4780
|
+
const configFile = useJson ? "pro-visu.config.json" : "pro-visu.config.ts";
|
|
4564
4781
|
const existingConfig = findExistingConfig(cwd);
|
|
4565
4782
|
if (existingConfig) {
|
|
4566
4783
|
logger.info(`config exists (${existingConfig}) \u2014 leaving it untouched`);
|
|
4567
|
-
} else if (
|
|
4568
|
-
await writeFile6(path17.join(cwd, configFile),
|
|
4784
|
+
} else if (useJson) {
|
|
4785
|
+
await writeFile6(path17.join(cwd, configFile), jsonConfigTemplate(info), "utf8");
|
|
4569
4786
|
logger.success(`created ${configFile}`);
|
|
4570
4787
|
await writeFile6(path17.join(cwd, DEFAULT_SCHEMA_FILE), serializeConfigJsonSchema(), "utf8");
|
|
4571
4788
|
logger.success(`created ${DEFAULT_SCHEMA_FILE}`);
|
|
4572
4789
|
createdSomething = true;
|
|
4573
4790
|
} else {
|
|
4574
|
-
await writeFile6(path17.join(cwd, configFile),
|
|
4791
|
+
await writeFile6(path17.join(cwd, configFile), tsConfigTemplate(info), "utf8");
|
|
4575
4792
|
logger.success(`created ${configFile}`);
|
|
4576
4793
|
createdSomething = true;
|
|
4577
4794
|
}
|
|
@@ -4616,8 +4833,9 @@ async function runInit(options = {}) {
|
|
|
4616
4833
|
}
|
|
4617
4834
|
}
|
|
4618
4835
|
logger.log("");
|
|
4836
|
+
const startHint = info.devPort ? `start your dev server (\`${runCmd(info.pm, "dev")}\`)` : "start your site (or point the config at a deployed URL)";
|
|
4619
4837
|
logger.info(
|
|
4620
|
-
createdSomething ? `Next: edit ${configFile},
|
|
4838
|
+
createdSomething ? `Next: edit ${configFile}, ${startHint}, then run \`pro-visu generate\`. (\`pro-visu doctor\` checks the setup.)` : `Already initialized. Edit ${configFile}, then run \`pro-visu generate\`.`
|
|
4621
4839
|
);
|
|
4622
4840
|
}
|
|
4623
4841
|
function findExistingConfig(cwd) {
|
|
@@ -4916,7 +5134,7 @@ function killTreeByPid(pid) {
|
|
|
4916
5134
|
// src/server/manage-server.ts
|
|
4917
5135
|
import path20 from "path";
|
|
4918
5136
|
import http2 from "http";
|
|
4919
|
-
import
|
|
5137
|
+
import https2 from "https";
|
|
4920
5138
|
import { spawn as spawn6 } from "child_process";
|
|
4921
5139
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
4922
5140
|
function resolveServerUrl(server) {
|
|
@@ -4924,7 +5142,7 @@ function resolveServerUrl(server) {
|
|
|
4924
5142
|
}
|
|
4925
5143
|
function probe(url) {
|
|
4926
5144
|
return new Promise((resolve) => {
|
|
4927
|
-
const lib = url.startsWith("https:") ?
|
|
5145
|
+
const lib = url.startsWith("https:") ? https2 : http2;
|
|
4928
5146
|
const req = lib.get(url, (res) => {
|
|
4929
5147
|
res.resume();
|
|
4930
5148
|
resolve((res.statusCode ?? 0) > 0);
|
|
@@ -5085,7 +5303,7 @@ async function startManagedServer(server, baseCwd, logger, tasks = {}, signal) {
|
|
|
5085
5303
|
}
|
|
5086
5304
|
|
|
5087
5305
|
// src/pipeline/runner.ts
|
|
5088
|
-
import
|
|
5306
|
+
import os3 from "os";
|
|
5089
5307
|
import path23 from "path";
|
|
5090
5308
|
import { existsSync as existsSync7 } from "fs";
|
|
5091
5309
|
import { mkdtemp as mkdtemp4 } from "fs/promises";
|
|
@@ -5109,7 +5327,10 @@ async function createContext(args) {
|
|
|
5109
5327
|
await ensureDir(genDir);
|
|
5110
5328
|
return {
|
|
5111
5329
|
browser: args.browser,
|
|
5112
|
-
target
|
|
5330
|
+
// Fold the capture query params into the target URL so every generator's `requireUrl(ctx)`
|
|
5331
|
+
// captures in capture mode without each one re-applying it.
|
|
5332
|
+
target: { ...args.target, url: withCaptureQuery(args.target.url, args.capture) },
|
|
5333
|
+
capture: args.capture,
|
|
5113
5334
|
resolvedInputs: args.resolvedInputs,
|
|
5114
5335
|
outDir: args.outDir,
|
|
5115
5336
|
resolveOutPath: (filename) => path21.join(genDir, filename),
|
|
@@ -5196,7 +5417,7 @@ function computeCacheKey(parts) {
|
|
|
5196
5417
|
// src/manifest/manifest.ts
|
|
5197
5418
|
import path22 from "path";
|
|
5198
5419
|
import { existsSync as existsSync6 } from "fs";
|
|
5199
|
-
import { readFile as readFile7, rename as
|
|
5420
|
+
import { readFile as readFile7, rename as rename3, rm as rm5, writeFile as writeFile8 } from "fs/promises";
|
|
5200
5421
|
|
|
5201
5422
|
// src/manifest/schema.ts
|
|
5202
5423
|
import { z as z10 } from "zod";
|
|
@@ -5263,7 +5484,7 @@ async function writeManifest(outDir, manifest) {
|
|
|
5263
5484
|
const maxAttempts = 5;
|
|
5264
5485
|
for (let attempt = 1; ; attempt++) {
|
|
5265
5486
|
try {
|
|
5266
|
-
await
|
|
5487
|
+
await rename3(tmp, file);
|
|
5267
5488
|
return;
|
|
5268
5489
|
} catch (err) {
|
|
5269
5490
|
const code = err.code;
|
|
@@ -5309,6 +5530,57 @@ var ManifestStore = class _ManifestStore {
|
|
|
5309
5530
|
};
|
|
5310
5531
|
|
|
5311
5532
|
// src/pipeline/runner.ts
|
|
5533
|
+
import { ZodError } from "zod";
|
|
5534
|
+
|
|
5535
|
+
// src/generators/migration.ts
|
|
5536
|
+
var RENAMED_KEYS = {
|
|
5537
|
+
"scroll-reel": {
|
|
5538
|
+
duration: 'renamed to "durationMs" (same unit \u2014 milliseconds)'
|
|
5539
|
+
},
|
|
5540
|
+
screenshots: {
|
|
5541
|
+
breakpoints: 'renamed to "viewports" (same shape)'
|
|
5542
|
+
},
|
|
5543
|
+
wall: {
|
|
5544
|
+
durationSeconds: 'renamed to "durationMs" and now in milliseconds (16 \u2192 16000)',
|
|
5545
|
+
duration: 'pulse "duration" was renamed to "span" (same 0..1 clip fraction)'
|
|
5546
|
+
},
|
|
5547
|
+
specimen: {
|
|
5548
|
+
durationSeconds: 'renamed to "durationMs" and now in milliseconds (10 \u2192 10000)',
|
|
5549
|
+
duration: 'pulse "duration" was renamed to "durationMs" and is now in milliseconds (0.8 \u2192 800)'
|
|
5550
|
+
},
|
|
5551
|
+
"palette-reel": {
|
|
5552
|
+
holdSeconds: 'renamed to "holdMs" and now in milliseconds (2 \u2192 2000)',
|
|
5553
|
+
transitionSeconds: 'renamed to "transitionMs" and now in milliseconds (0.7 \u2192 700)',
|
|
5554
|
+
durationSeconds: 'renamed to "durationMs" and now in milliseconds (10 \u2192 10000)'
|
|
5555
|
+
}
|
|
5556
|
+
};
|
|
5557
|
+
function kebab(value) {
|
|
5558
|
+
return value.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
5559
|
+
}
|
|
5560
|
+
function legacyOptionHint(generatorId, issue) {
|
|
5561
|
+
if (issue.code === "unrecognized_keys") {
|
|
5562
|
+
const renames = RENAMED_KEYS[generatorId];
|
|
5563
|
+
if (!renames) return void 0;
|
|
5564
|
+
const hints = issue.keys.filter((key) => renames[key]).map((key) => `"${key}" ${renames[key]}`);
|
|
5565
|
+
return hints.length ? hints.join("; ") : void 0;
|
|
5566
|
+
}
|
|
5567
|
+
if (issue.code === "invalid_enum_value" && typeof issue.received === "string") {
|
|
5568
|
+
const kebabbed = kebab(issue.received);
|
|
5569
|
+
if (kebabbed !== issue.received && issue.options.includes(kebabbed)) {
|
|
5570
|
+
return `easing names are kebab-case now \u2014 use "${kebabbed}"`;
|
|
5571
|
+
}
|
|
5572
|
+
}
|
|
5573
|
+
return void 0;
|
|
5574
|
+
}
|
|
5575
|
+
|
|
5576
|
+
// src/pipeline/runner.ts
|
|
5577
|
+
function describeOptionIssues(generatorId, err) {
|
|
5578
|
+
return err.issues.map((issue) => {
|
|
5579
|
+
const where = ["options", ...issue.path].join(".");
|
|
5580
|
+
const hint = legacyOptionHint(generatorId, issue);
|
|
5581
|
+
return ` \u2022 ${where}: ${issue.message}${hint ? ` \u2014 ${hint}` : ""}`;
|
|
5582
|
+
}).join("\n");
|
|
5583
|
+
}
|
|
5312
5584
|
function applyQuality(options, quality) {
|
|
5313
5585
|
if (quality !== "draft") return options;
|
|
5314
5586
|
const o = { ...options };
|
|
@@ -5324,7 +5596,7 @@ async function runPipeline(opts) {
|
|
|
5324
5596
|
if (specs.length === 0) return [];
|
|
5325
5597
|
await ensureDir(opts.outDir);
|
|
5326
5598
|
const manifest = await ManifestStore.load(opts.outDir);
|
|
5327
|
-
const tmpRoot = await mkdtemp4(path23.join(
|
|
5599
|
+
const tmpRoot = await mkdtemp4(path23.join(os3.tmpdir(), "pro-visu-"));
|
|
5328
5600
|
const browser = await launchBrowser(opts.config.settings.browser);
|
|
5329
5601
|
opts.onResources?.({ tmpDir: tmpRoot });
|
|
5330
5602
|
const concurrency = Math.max(1, opts.concurrency ?? opts.config.settings.concurrency);
|
|
@@ -5376,7 +5648,9 @@ async function runPipeline(opts) {
|
|
|
5376
5648
|
inputs: inputHashes,
|
|
5377
5649
|
files: fileHashes,
|
|
5378
5650
|
quality,
|
|
5379
|
-
toolVersion: opts.toolVersion
|
|
5651
|
+
toolVersion: opts.toolVersion,
|
|
5652
|
+
// Capture-mode toggles change the rendered output, so a change must bust the cache.
|
|
5653
|
+
capture: opts.config.settings.capture
|
|
5380
5654
|
});
|
|
5381
5655
|
if (cacheEnabled) {
|
|
5382
5656
|
const existing = manifest.find(spec.name);
|
|
@@ -5405,6 +5679,7 @@ async function runPipeline(opts) {
|
|
|
5405
5679
|
toolVersion: opts.toolVersion,
|
|
5406
5680
|
quality,
|
|
5407
5681
|
manifest,
|
|
5682
|
+
capture: opts.config.settings.capture,
|
|
5408
5683
|
onProgress: reporter ? (v) => reporter.progress(spec.name, v) : void 0,
|
|
5409
5684
|
signal: opts.signal
|
|
5410
5685
|
});
|
|
@@ -5417,7 +5692,8 @@ async function runPipeline(opts) {
|
|
|
5417
5692
|
reporter?.status(spec.name, "ok");
|
|
5418
5693
|
return { name: spec.name, generator: spec.generator, status: "ok", records: result.assets };
|
|
5419
5694
|
} catch (error) {
|
|
5420
|
-
const err = error instanceof
|
|
5695
|
+
const err = error instanceof ZodError ? new Error(`Invalid ${spec.generator} options:
|
|
5696
|
+
${describeOptionIssues(spec.generator, error)}`) : error instanceof Error ? error : new Error(String(error));
|
|
5421
5697
|
if (opts.signal?.aborted) {
|
|
5422
5698
|
return { name: spec.name, generator: spec.generator, status: "failed", records: [], error: err, cancelled: true };
|
|
5423
5699
|
}
|
|
@@ -5494,7 +5770,18 @@ function applyDerivedInputs(config) {
|
|
|
5494
5770
|
}
|
|
5495
5771
|
function mergeGeneratorOptions(defaults, spec) {
|
|
5496
5772
|
const generatorDefaults = defaults[spec.generator] ?? {};
|
|
5497
|
-
return
|
|
5773
|
+
return deepMerge(generatorDefaults, spec.options);
|
|
5774
|
+
}
|
|
5775
|
+
function isPlainObject(value) {
|
|
5776
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5777
|
+
}
|
|
5778
|
+
function deepMerge(base, override) {
|
|
5779
|
+
const out = { ...base };
|
|
5780
|
+
for (const [key, value] of Object.entries(override)) {
|
|
5781
|
+
const existing = out[key];
|
|
5782
|
+
out[key] = isPlainObject(existing) && isPlainObject(value) ? deepMerge(existing, value) : value;
|
|
5783
|
+
}
|
|
5784
|
+
return out;
|
|
5498
5785
|
}
|
|
5499
5786
|
|
|
5500
5787
|
// src/cli/dashboard/index.tsx
|
|
@@ -5975,7 +6262,7 @@ var InkReporter = class {
|
|
|
5975
6262
|
}
|
|
5976
6263
|
};
|
|
5977
6264
|
function createReporter(opts) {
|
|
5978
|
-
const forced = process.env.SHOWCASE_LIVE;
|
|
6265
|
+
const forced = process.env.PRO_VISU_LIVE ?? process.env.SHOWCASE_LIVE;
|
|
5979
6266
|
if (forced === "0") return new NoopReporter();
|
|
5980
6267
|
if (forced === "1") return new InkReporter();
|
|
5981
6268
|
return opts.tty && !opts.verbose ? new InkReporter() : new NoopReporter();
|
|
@@ -6064,6 +6351,13 @@ function renderSummary(outcomes, outDir, columns = 80) {
|
|
|
6064
6351
|
}
|
|
6065
6352
|
|
|
6066
6353
|
// src/cli/ui.ts
|
|
6354
|
+
function zodIssueLines(zodError, pathPrefix = "") {
|
|
6355
|
+
return zodError.issues.map((issue) => {
|
|
6356
|
+
const joined = issue.path.join(".");
|
|
6357
|
+
const where = [pathPrefix, joined].filter(Boolean).join(".") || "(root)";
|
|
6358
|
+
return ` \u2022 ${where}: ${issue.message}`;
|
|
6359
|
+
});
|
|
6360
|
+
}
|
|
6067
6361
|
function reportConfigError(logger, err) {
|
|
6068
6362
|
if (err instanceof ConfigNotFoundError) {
|
|
6069
6363
|
logger.error(err.message);
|
|
@@ -6071,10 +6365,7 @@ function reportConfigError(logger, err) {
|
|
|
6071
6365
|
}
|
|
6072
6366
|
if (err instanceof ConfigValidationError) {
|
|
6073
6367
|
logger.error(`Invalid config${err.file ? ` (${err.file})` : ""}:`);
|
|
6074
|
-
for (const
|
|
6075
|
-
const where = issue.path.length ? issue.path.join(".") : "(root)";
|
|
6076
|
-
logger.error(` \u2022 ${where}: ${issue.message}`);
|
|
6077
|
-
}
|
|
6368
|
+
for (const line of zodIssueLines(err.zodError)) logger.error(line);
|
|
6078
6369
|
return;
|
|
6079
6370
|
}
|
|
6080
6371
|
logger.error(err instanceof Error ? err.message : String(err));
|
|
@@ -6088,6 +6379,47 @@ function printSummary(logger, outcomes, outDir) {
|
|
|
6088
6379
|
logger.log(renderSummary(outcomes, outDir, process.stdout.columns ?? 80));
|
|
6089
6380
|
}
|
|
6090
6381
|
|
|
6382
|
+
// src/utils/suggest.ts
|
|
6383
|
+
function editDistance(a, b) {
|
|
6384
|
+
const cols = b.length + 1;
|
|
6385
|
+
const dist = Array.from({ length: cols }, (_, j) => j);
|
|
6386
|
+
for (let i = 1; i <= a.length; i++) {
|
|
6387
|
+
let prev = dist[0] ?? 0;
|
|
6388
|
+
dist[0] = i;
|
|
6389
|
+
for (let j = 1; j < cols; j++) {
|
|
6390
|
+
const tmp = dist[j] ?? 0;
|
|
6391
|
+
dist[j] = Math.min(
|
|
6392
|
+
(dist[j] ?? 0) + 1,
|
|
6393
|
+
// deletion
|
|
6394
|
+
(dist[j - 1] ?? 0) + 1,
|
|
6395
|
+
// insertion
|
|
6396
|
+
prev + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
6397
|
+
// substitution
|
|
6398
|
+
);
|
|
6399
|
+
prev = tmp;
|
|
6400
|
+
}
|
|
6401
|
+
}
|
|
6402
|
+
return dist[cols - 1] ?? 0;
|
|
6403
|
+
}
|
|
6404
|
+
function closestMatch(input, candidates) {
|
|
6405
|
+
const needle = input.toLowerCase();
|
|
6406
|
+
let best;
|
|
6407
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
6408
|
+
for (const candidate of candidates) {
|
|
6409
|
+
const d = editDistance(needle, candidate.toLowerCase());
|
|
6410
|
+
if (d < bestDist) {
|
|
6411
|
+
bestDist = d;
|
|
6412
|
+
best = candidate;
|
|
6413
|
+
}
|
|
6414
|
+
}
|
|
6415
|
+
const threshold = Math.max(2, Math.floor(input.length / 3));
|
|
6416
|
+
return bestDist <= threshold ? best : void 0;
|
|
6417
|
+
}
|
|
6418
|
+
function didYouMean(input, candidates) {
|
|
6419
|
+
const match = closestMatch(input, candidates);
|
|
6420
|
+
return match ? ` (did you mean "${match}"?)` : "";
|
|
6421
|
+
}
|
|
6422
|
+
|
|
6091
6423
|
// src/cli/commands/generate.ts
|
|
6092
6424
|
async function runGenerate(options = {}) {
|
|
6093
6425
|
const cwd = resolveCwd(options.cwd);
|
|
@@ -6105,35 +6437,76 @@ async function runGenerate(options = {}) {
|
|
|
6105
6437
|
if (config.settings.maxMemoryMB) {
|
|
6106
6438
|
bootstrapLog.info(`Node heap limit: ${currentHeapLimitMB()} MB (settings.maxMemoryMB=${config.settings.maxMemoryMB})`);
|
|
6107
6439
|
}
|
|
6108
|
-
const level = options.verbose ? "debug" : config.settings.logLevel;
|
|
6109
|
-
const reporter = createReporter({ tty: Boolean(process.stdout.isTTY), verbose: !!options.verbose });
|
|
6110
|
-
const logger = reporter.isLive ? createReportingLogger(level, reporter) : createLogger(level);
|
|
6111
6440
|
const outDir = resolveOutDir(cwd, config.settings.outDir);
|
|
6441
|
+
let concurrencyOverride;
|
|
6442
|
+
if (options.concurrency != null) {
|
|
6443
|
+
const raw = options.concurrency;
|
|
6444
|
+
const n = typeof raw === "boolean" ? Number.NaN : Number(raw);
|
|
6445
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
6446
|
+
bootstrapLog.error(`Invalid --concurrency "${options.concurrency}" \u2014 expected a positive integer.`);
|
|
6447
|
+
process.exitCode = 1;
|
|
6448
|
+
return;
|
|
6449
|
+
}
|
|
6450
|
+
concurrencyOverride = n;
|
|
6451
|
+
}
|
|
6112
6452
|
const requested = normalizeAssetNames(options.asset);
|
|
6113
6453
|
if (requested) {
|
|
6114
6454
|
const known = new Set(config.assets.map((a) => a.name));
|
|
6115
6455
|
const unknown = requested.filter((name) => !known.has(name));
|
|
6116
|
-
|
|
6456
|
+
for (const name of unknown) bootstrapLog.warn(`Unknown asset "${name}"${didYouMean(name, known)}`);
|
|
6117
6457
|
if (requested.every((name) => !known.has(name))) {
|
|
6118
|
-
|
|
6458
|
+
bootstrapLog.error("No matching assets to generate.");
|
|
6119
6459
|
process.exitCode = 1;
|
|
6120
6460
|
return;
|
|
6121
6461
|
}
|
|
6122
6462
|
}
|
|
6463
|
+
applyDerivedInputs(config);
|
|
6464
|
+
const selected = expandSelection(config.assets, requested);
|
|
6465
|
+
const quality = options.draft ? "draft" : config.settings.quality;
|
|
6466
|
+
if (!validatePlan(bootstrapLog, config, selected, quality)) {
|
|
6467
|
+
process.exitCode = 1;
|
|
6468
|
+
return;
|
|
6469
|
+
}
|
|
6470
|
+
const anyNeedsServer = selected.some((s) => Boolean(getGenerator(s.generator)?.requiresUrl));
|
|
6471
|
+
if (config.settings.server && !options.skipServer && !anyNeedsServer) {
|
|
6472
|
+
bootstrapLog.info("No selected asset needs a URL \u2014 skipping the managed server.");
|
|
6473
|
+
}
|
|
6474
|
+
const baseServerCfg = options.skipServer || !anyNeedsServer ? void 0 : config.settings.server;
|
|
6475
|
+
const serverCfg = baseServerCfg && options.skipBuild ? { ...baseServerCfg, build: void 0 } : baseServerCfg;
|
|
6476
|
+
const serverBase = serverCfg ? resolveServerUrl(serverCfg) : void 0;
|
|
6477
|
+
const resolvedConfig = {
|
|
6478
|
+
...config,
|
|
6479
|
+
assets: resolveTargets(
|
|
6480
|
+
config.assets,
|
|
6481
|
+
serverBase,
|
|
6482
|
+
(id) => Boolean(getGenerator(id)?.requiresUrl)
|
|
6483
|
+
)
|
|
6484
|
+
};
|
|
6485
|
+
if (options.dryRun) {
|
|
6486
|
+
printPlan(bootstrapLog, resolvedConfig, selected, serverCfg, quality, concurrencyOverride);
|
|
6487
|
+
return;
|
|
6488
|
+
}
|
|
6489
|
+
if (!serverCfg && !await preflightUrls(bootstrapLog, resolvedConfig, selected)) {
|
|
6490
|
+
process.exitCode = 1;
|
|
6491
|
+
return;
|
|
6492
|
+
}
|
|
6123
6493
|
const usingManaged = !config.settings.browser.channel && !config.settings.browser.executablePath;
|
|
6124
6494
|
if (!options.skipBrowser && usingManaged) {
|
|
6125
|
-
const ready = await ensureChromium({ logger });
|
|
6495
|
+
const ready = await ensureChromium({ logger: bootstrapLog });
|
|
6126
6496
|
if (!ready) {
|
|
6127
|
-
|
|
6497
|
+
bootstrapLog.error("Chromium is required. Run `pro-visu init` or install it manually.");
|
|
6128
6498
|
process.exitCode = 1;
|
|
6129
6499
|
return;
|
|
6130
6500
|
}
|
|
6131
6501
|
}
|
|
6132
|
-
if (!await ensureFfmpeg({ logger })) {
|
|
6133
|
-
|
|
6502
|
+
if (!await ensureFfmpeg({ logger: bootstrapLog })) {
|
|
6503
|
+
bootstrapLog.error("A working ffmpeg is required for video generators.");
|
|
6134
6504
|
process.exitCode = 1;
|
|
6135
6505
|
return;
|
|
6136
6506
|
}
|
|
6507
|
+
const level = options.verbose ? "debug" : config.settings.logLevel;
|
|
6508
|
+
const reporter = createReporter({ tty: Boolean(process.stdout.isTTY), verbose: !!options.verbose });
|
|
6509
|
+
const logger = reporter.isLive ? createReportingLogger(level, reporter) : createLogger(level);
|
|
6137
6510
|
await ensureDir(outDir);
|
|
6138
6511
|
await startRunState(outDir);
|
|
6139
6512
|
const abort = new AbortController();
|
|
@@ -6174,23 +6547,6 @@ async function runGenerate(options = {}) {
|
|
|
6174
6547
|
}
|
|
6175
6548
|
}, 1500);
|
|
6176
6549
|
memTimer.unref?.();
|
|
6177
|
-
applyDerivedInputs(config);
|
|
6178
|
-
const selected = expandSelection(config.assets, requested);
|
|
6179
|
-
const anyNeedsServer = selected.some((s) => Boolean(getGenerator(s.generator)?.requiresUrl));
|
|
6180
|
-
if (config.settings.server && !options.skipServer && !anyNeedsServer) {
|
|
6181
|
-
logger.info("No selected asset needs a URL \u2014 skipping the managed server.");
|
|
6182
|
-
}
|
|
6183
|
-
const baseServerCfg = options.skipServer || !anyNeedsServer ? void 0 : config.settings.server;
|
|
6184
|
-
const serverCfg = baseServerCfg && options.skipBuild ? { ...baseServerCfg, build: void 0 } : baseServerCfg;
|
|
6185
|
-
const serverBase = serverCfg ? resolveServerUrl(serverCfg) : void 0;
|
|
6186
|
-
const resolvedConfig = {
|
|
6187
|
-
...config,
|
|
6188
|
-
assets: resolveTargets(
|
|
6189
|
-
config.assets,
|
|
6190
|
-
serverBase,
|
|
6191
|
-
(id) => Boolean(getGenerator(id)?.requiresUrl)
|
|
6192
|
-
)
|
|
6193
|
-
};
|
|
6194
6550
|
const gates = [];
|
|
6195
6551
|
const tasks = {};
|
|
6196
6552
|
if (reporter.isLive && serverCfg) {
|
|
@@ -6222,16 +6578,14 @@ async function runGenerate(options = {}) {
|
|
|
6222
6578
|
server = await startManagedServer(serverCfg, cwd, logger, tasks, abort.signal);
|
|
6223
6579
|
await updateRunState(outDir, { serverPid: server?.pid });
|
|
6224
6580
|
}
|
|
6225
|
-
|
|
6226
|
-
const count = requested ? requested.length : config.assets.length;
|
|
6227
|
-
if (!reporter.isLive) logger.start(`Generating ${count} asset(s)\u2026`);
|
|
6581
|
+
if (!reporter.isLive) logger.start(`Generating ${selected.length} asset(s)\u2026`);
|
|
6228
6582
|
outcomes = await runPipeline({
|
|
6229
6583
|
config: resolvedConfig,
|
|
6230
6584
|
outDir,
|
|
6231
6585
|
logger,
|
|
6232
6586
|
toolVersion: TOOL_VERSION,
|
|
6233
6587
|
assetNames: requested,
|
|
6234
|
-
concurrency:
|
|
6588
|
+
concurrency: concurrencyOverride,
|
|
6235
6589
|
quality: options.draft ? "draft" : void 0,
|
|
6236
6590
|
cache: options.cache,
|
|
6237
6591
|
reporter,
|
|
@@ -6270,6 +6624,107 @@ async function runGenerate(options = {}) {
|
|
|
6270
6624
|
process.exitCode = 1;
|
|
6271
6625
|
}
|
|
6272
6626
|
}
|
|
6627
|
+
function validatePlan(log, config, selected, quality) {
|
|
6628
|
+
let ok = true;
|
|
6629
|
+
const ids = generatorIds();
|
|
6630
|
+
const known = new Set(ids);
|
|
6631
|
+
for (const key of Object.keys(config.settings.defaults)) {
|
|
6632
|
+
if (!known.has(key)) {
|
|
6633
|
+
log.warn(`settings.defaults["${key}"] matches no generator${didYouMean(key, ids)} \u2014 it will never apply.`);
|
|
6634
|
+
}
|
|
6635
|
+
}
|
|
6636
|
+
for (const spec of selected) {
|
|
6637
|
+
const generator = getGenerator(spec.generator);
|
|
6638
|
+
if (!generator) {
|
|
6639
|
+
log.error(
|
|
6640
|
+
`Asset "${spec.name}": unknown generator "${spec.generator}"${didYouMean(spec.generator, ids)}. Available: ${ids.join(", ")}.`
|
|
6641
|
+
);
|
|
6642
|
+
ok = false;
|
|
6643
|
+
continue;
|
|
6644
|
+
}
|
|
6645
|
+
const merged = applyQuality(mergeGeneratorOptions(config.settings.defaults, spec), quality);
|
|
6646
|
+
const parsed2 = generator.optionsSchema.safeParse(merged);
|
|
6647
|
+
if (!parsed2.success) {
|
|
6648
|
+
log.error(`Asset "${spec.name}" has invalid ${spec.generator} options:`);
|
|
6649
|
+
for (const issue of parsed2.error.issues) {
|
|
6650
|
+
const where = ["options", ...issue.path].join(".");
|
|
6651
|
+
const hint = legacyOptionHint(spec.generator, issue);
|
|
6652
|
+
log.error(` \u2022 ${where}: ${issue.message}${hint ? ` \u2014 ${hint}` : ""}`);
|
|
6653
|
+
}
|
|
6654
|
+
ok = false;
|
|
6655
|
+
}
|
|
6656
|
+
}
|
|
6657
|
+
return ok;
|
|
6658
|
+
}
|
|
6659
|
+
function printPlan(log, resolvedConfig, selected, serverCfg, quality, concurrencyOverride) {
|
|
6660
|
+
const byName = new Map(resolvedConfig.assets.map((a) => [a.name, a]));
|
|
6661
|
+
const concurrency = concurrencyOverride ?? resolvedConfig.settings.concurrency;
|
|
6662
|
+
log.info(`Plan: ${selected.length} asset(s), quality "${quality}", concurrency ${concurrency}`);
|
|
6663
|
+
for (const s of selected) {
|
|
6664
|
+
const resolved = byName.get(s.name) ?? s;
|
|
6665
|
+
log.log(` \u2022 ${s.name} [${s.generator}]${resolved.url ? ` ${resolved.url}` : ""}`);
|
|
6666
|
+
}
|
|
6667
|
+
if (serverCfg) log.info(`Managed server: ${serverCfg.command}`);
|
|
6668
|
+
log.info("Dry run \u2014 nothing generated.");
|
|
6669
|
+
}
|
|
6670
|
+
async function preflightUrls(log, resolvedConfig, selected) {
|
|
6671
|
+
const byName = new Map(resolvedConfig.assets.map((a) => [a.name, a]));
|
|
6672
|
+
const missing = [];
|
|
6673
|
+
const relative = [];
|
|
6674
|
+
const urls = /* @__PURE__ */ new Set();
|
|
6675
|
+
for (const s of selected) {
|
|
6676
|
+
if (!getGenerator(s.generator)?.requiresUrl) continue;
|
|
6677
|
+
const url = byName.get(s.name)?.url;
|
|
6678
|
+
if (!url) missing.push(s.name);
|
|
6679
|
+
else if (url.startsWith("/")) relative.push(`"${s.name}" (${url})`);
|
|
6680
|
+
else urls.add(url);
|
|
6681
|
+
}
|
|
6682
|
+
let ok = true;
|
|
6683
|
+
if (missing.length) {
|
|
6684
|
+
log.error(
|
|
6685
|
+
`Asset(s) missing a url: ${missing.join(", ")} \u2014 set "url", or configure settings.server so they capture the managed server's root.`
|
|
6686
|
+
);
|
|
6687
|
+
ok = false;
|
|
6688
|
+
}
|
|
6689
|
+
if (relative.length) {
|
|
6690
|
+
log.error(
|
|
6691
|
+
`Relative url(s) require the managed server: ${relative.join(", ")} \u2014 configure settings.server, or use absolute URLs.`
|
|
6692
|
+
);
|
|
6693
|
+
ok = false;
|
|
6694
|
+
}
|
|
6695
|
+
const origins = /* @__PURE__ */ new Map();
|
|
6696
|
+
for (const url of urls) {
|
|
6697
|
+
try {
|
|
6698
|
+
const origin = new URL(url).origin;
|
|
6699
|
+
if (!origins.has(origin)) origins.set(origin, url);
|
|
6700
|
+
} catch {
|
|
6701
|
+
}
|
|
6702
|
+
}
|
|
6703
|
+
const probes = await Promise.all(
|
|
6704
|
+
[...origins.values()].map(async (url) => await urlResponds(url) ? null : url)
|
|
6705
|
+
);
|
|
6706
|
+
const unreachable = probes.filter((url) => url !== null);
|
|
6707
|
+
if (unreachable.length) {
|
|
6708
|
+
for (const url of unreachable) log.error(`Nothing is responding at ${url}.`);
|
|
6709
|
+
log.error(
|
|
6710
|
+
"Start your site (or point the asset urls at a deployed one), or configure settings.server so pro-visu builds and starts it for you."
|
|
6711
|
+
);
|
|
6712
|
+
ok = false;
|
|
6713
|
+
}
|
|
6714
|
+
return ok;
|
|
6715
|
+
}
|
|
6716
|
+
async function urlResponds(url) {
|
|
6717
|
+
const ctrl = new AbortController();
|
|
6718
|
+
const timer = setTimeout(() => ctrl.abort(), 1e4);
|
|
6719
|
+
try {
|
|
6720
|
+
await fetch(url, { signal: ctrl.signal, redirect: "manual" });
|
|
6721
|
+
return true;
|
|
6722
|
+
} catch {
|
|
6723
|
+
return false;
|
|
6724
|
+
} finally {
|
|
6725
|
+
clearTimeout(timer);
|
|
6726
|
+
}
|
|
6727
|
+
}
|
|
6273
6728
|
function taskHandle(reporter, id) {
|
|
6274
6729
|
return {
|
|
6275
6730
|
start: () => reporter.status(id, "running"),
|
|
@@ -6284,6 +6739,83 @@ function normalizeAssetNames(value) {
|
|
|
6284
6739
|
return arr.length ? arr : void 0;
|
|
6285
6740
|
}
|
|
6286
6741
|
|
|
6742
|
+
// src/cli/commands/doctor.ts
|
|
6743
|
+
import { existsSync as existsSync8 } from "fs";
|
|
6744
|
+
async function runDoctor(options = {}) {
|
|
6745
|
+
const cwd = resolveCwd(options.cwd);
|
|
6746
|
+
const log = createLogger("info");
|
|
6747
|
+
let failed = false;
|
|
6748
|
+
const fail = (message) => {
|
|
6749
|
+
failed = true;
|
|
6750
|
+
log.error(message);
|
|
6751
|
+
};
|
|
6752
|
+
const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
|
|
6753
|
+
if (major > 18 || major === 18 && minor >= 18) {
|
|
6754
|
+
log.success(`Node ${process.versions.node}`);
|
|
6755
|
+
} else {
|
|
6756
|
+
fail(`Node ${process.versions.node} \u2014 pro-visu requires >= 18.18.`);
|
|
6757
|
+
}
|
|
6758
|
+
let config;
|
|
6759
|
+
try {
|
|
6760
|
+
const loaded = await loadShowcaseConfig({ cwd, configFile: options.config });
|
|
6761
|
+
config = loaded.config;
|
|
6762
|
+
const where = loaded.configFile ?? 'package.json "pro-visu" key';
|
|
6763
|
+
log.success(`Config OK (${where}) \u2014 ${config.assets.length} asset(s).`);
|
|
6764
|
+
} catch (err) {
|
|
6765
|
+
failed = true;
|
|
6766
|
+
reportConfigError(log, err);
|
|
6767
|
+
}
|
|
6768
|
+
if (config) {
|
|
6769
|
+
if (validatePlan(log, config, config.assets, config.settings.quality)) {
|
|
6770
|
+
log.success("Asset options OK.");
|
|
6771
|
+
} else {
|
|
6772
|
+
failed = true;
|
|
6773
|
+
}
|
|
6774
|
+
try {
|
|
6775
|
+
applyDerivedInputs(config);
|
|
6776
|
+
buildGraph(config.assets);
|
|
6777
|
+
} catch (err) {
|
|
6778
|
+
fail(err.message);
|
|
6779
|
+
}
|
|
6780
|
+
}
|
|
6781
|
+
const browser = config?.settings.browser;
|
|
6782
|
+
if (browser?.executablePath) {
|
|
6783
|
+
if (existsSync8(browser.executablePath)) log.success(`Browser executable: ${browser.executablePath}`);
|
|
6784
|
+
else fail(`browser.executablePath not found: ${browser.executablePath}`);
|
|
6785
|
+
} else if (browser?.channel) {
|
|
6786
|
+
log.info(`Browser channel "${browser.channel}" \u2014 verified at launch.`);
|
|
6787
|
+
} else if (await ensureChromium({ logger: log, checkOnly: true })) {
|
|
6788
|
+
log.success("Chromium installed.");
|
|
6789
|
+
} else {
|
|
6790
|
+
fail("Chromium missing \u2014 run `pro-visu init` (or it installs on first `pro-visu generate`).");
|
|
6791
|
+
}
|
|
6792
|
+
if (await ensureFfmpeg({ logger: log, checkOnly: true })) {
|
|
6793
|
+
log.success(`ffmpeg OK${process.env.FFMPEG_BIN ? " (FFMPEG_BIN)" : ""}.`);
|
|
6794
|
+
} else if (ffmpegIsSupported()) {
|
|
6795
|
+
log.warn("ffmpeg not fetched yet \u2014 it downloads automatically on first `pro-visu generate`.");
|
|
6796
|
+
} else {
|
|
6797
|
+
fail(
|
|
6798
|
+
`No prebuilt ffmpeg for ${process.platform}/${process.arch} \u2014 set FFMPEG_BIN to a local ffmpeg binary.`
|
|
6799
|
+
);
|
|
6800
|
+
}
|
|
6801
|
+
if (config) {
|
|
6802
|
+
if (config.settings.server) {
|
|
6803
|
+
log.info(`Managed server configured: \`${config.settings.server.command}\` (started per run).`);
|
|
6804
|
+
} else if (await preflightUrls(log, config, config.assets)) {
|
|
6805
|
+
log.success("Asset URLs respond.");
|
|
6806
|
+
} else {
|
|
6807
|
+
failed = true;
|
|
6808
|
+
}
|
|
6809
|
+
}
|
|
6810
|
+
log.log("");
|
|
6811
|
+
if (failed) {
|
|
6812
|
+
log.error("Some checks failed \u2014 see above.");
|
|
6813
|
+
process.exitCode = 1;
|
|
6814
|
+
} else {
|
|
6815
|
+
log.success("All checks passed. Run `pro-visu generate` when ready.");
|
|
6816
|
+
}
|
|
6817
|
+
}
|
|
6818
|
+
|
|
6287
6819
|
// src/cli/commands/list.ts
|
|
6288
6820
|
import pc from "picocolors";
|
|
6289
6821
|
async function runList(options = {}) {
|
|
@@ -6303,6 +6835,11 @@ async function runList(options = {}) {
|
|
|
6303
6835
|
}
|
|
6304
6836
|
}
|
|
6305
6837
|
const manifest = await readManifest(outDir);
|
|
6838
|
+
if (options.json) {
|
|
6839
|
+
process.stdout.write(`${JSON.stringify({ outDir, assets: manifest.assets }, null, 2)}
|
|
6840
|
+
`);
|
|
6841
|
+
return;
|
|
6842
|
+
}
|
|
6306
6843
|
if (manifest.assets.length === 0) {
|
|
6307
6844
|
logger.info("Nothing generated yet. Run `pro-visu generate`.");
|
|
6308
6845
|
return;
|
|
@@ -6323,7 +6860,7 @@ import path25 from "path";
|
|
|
6323
6860
|
async function runReset(options = {}) {
|
|
6324
6861
|
const cwd = resolveCwd(options.cwd);
|
|
6325
6862
|
const log = createLogger("info");
|
|
6326
|
-
let outDir = path25.join(cwd,
|
|
6863
|
+
let outDir = path25.join(cwd, DEFAULT_OUTDIR);
|
|
6327
6864
|
try {
|
|
6328
6865
|
const { config } = await loadShowcaseConfig({ cwd, configFile: options.config });
|
|
6329
6866
|
outDir = resolveOutDir(cwd, config.settings.outDir);
|
|
@@ -6358,9 +6895,10 @@ async function runReset(options = {}) {
|
|
|
6358
6895
|
|
|
6359
6896
|
// src/cli/index.ts
|
|
6360
6897
|
var cli = cac("pro-visu");
|
|
6361
|
-
cli.command("init", "Scaffold config, gitignore the output dir, and ensure a browser").option("--cwd <dir>", "Working directory").option("--no-script", "
|
|
6362
|
-
cli.command("generate", "Generate showcase assets defined in your config").alias("gen").option("--config <path>", "Path to a config file").option("--cwd <dir>", "Working directory").option("--asset <name>", "Only generate these assets (repeatable)").option("--concurrency <n>", "Override parallelism").option("--skip-browser", "Skip the Chromium check/install").option("--skip-server", "Skip the managed server (use an already-running site)").option("--skip-build", "Skip the server build step (fast iteration when the site is unchanged)").option("--draft", "Draft quality: faster, lower-fidelity renders for iteration").option("--cache", "Skip assets whose inputs+options are unchanged").option("--verbose", "Verbose (debug) logging").action(runGenerate);
|
|
6363
|
-
cli.command("
|
|
6898
|
+
cli.command("init", "Scaffold config, gitignore the output dir, and ensure a browser").option("--cwd <dir>", "Working directory").option("--no-script", 'Skip adding the "pro-visu" script to package.json').option("--skip-browser", "Do not install Chromium").option("--json", "Scaffold a dependency-free JSON config + JSON Schema (for npx / global use)").action(runInit);
|
|
6899
|
+
cli.command("generate", "Generate showcase assets defined in your config").alias("gen").option("--config <path>", "Path to a config file").option("--cwd <dir>", "Working directory").option("--asset <name>", "Only generate these assets (repeatable)").option("--concurrency <n>", "Override parallelism").option("--skip-browser", "Skip the Chromium check/install").option("--skip-server", "Skip the managed server (use an already-running site)").option("--skip-build", "Skip the server build step (fast iteration when the site is unchanged)").option("--draft", "Draft quality: faster, lower-fidelity renders for iteration").option("--cache", "Skip assets whose inputs+options are unchanged").option("--dry-run", "Validate the config and print the plan without generating").option("--verbose", "Verbose (debug) logging (plain logs instead of the live dashboard)").action(runGenerate);
|
|
6900
|
+
cli.command("doctor", "Check the environment + config (Node, config, Chromium, ffmpeg, URLs)").option("--config <path>", "Path to a config file").option("--cwd <dir>", "Working directory").action(runDoctor);
|
|
6901
|
+
cli.command("list", "List assets recorded in the manifest").alias("ls").option("--config <path>", "Path to a config file").option("--cwd <dir>", "Working directory").option("--json", "Print the manifest as JSON (for scripts/CI)").action(runList);
|
|
6364
6902
|
cli.command("schema", "Write a JSON Schema for pro-visu.config.json (editor autocomplete)").option("--cwd <dir>", "Working directory").option("--out <path>", "Output path (default pro-visu.schema.json)").action(runSchema);
|
|
6365
6903
|
cli.command("reset", "Clean up orphaned processes/temp from an interrupted run").option("--config <path>", "Path to a config file").option("--cwd <dir>", "Working directory").option("--force", "Clean up even if a run still looks active").action(runReset);
|
|
6366
6904
|
cli.help();
|
|
@@ -6369,6 +6907,11 @@ var parsed = cli.parse(process.argv, { run: false });
|
|
|
6369
6907
|
async function main() {
|
|
6370
6908
|
checkForUpdates();
|
|
6371
6909
|
if (!cli.matchedCommand) {
|
|
6910
|
+
if (parsed.args.length > 0) {
|
|
6911
|
+
console.error(`Unknown command "${parsed.args[0]}". Run \`pro-visu --help\` for the command list.`);
|
|
6912
|
+
process.exitCode = 1;
|
|
6913
|
+
return;
|
|
6914
|
+
}
|
|
6372
6915
|
if (!parsed.options.help && !parsed.options.version) cli.outputHelp();
|
|
6373
6916
|
return;
|
|
6374
6917
|
}
|
|
@@ -6376,6 +6919,12 @@ async function main() {
|
|
|
6376
6919
|
}
|
|
6377
6920
|
main().catch((err) => {
|
|
6378
6921
|
process.exitCode = 1;
|
|
6379
|
-
|
|
6922
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6923
|
+
console.error(message);
|
|
6924
|
+
if (!process.argv.includes("--verbose") && err instanceof Error && err.stack) {
|
|
6925
|
+
console.error("Re-run with --verbose for a stack trace.");
|
|
6926
|
+
} else if (err instanceof Error && err.stack) {
|
|
6927
|
+
console.error(err.stack);
|
|
6928
|
+
}
|
|
6380
6929
|
});
|
|
6381
6930
|
//# sourceMappingURL=index.js.map
|