pro-visu 0.4.0 → 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 +780 -347
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +31 -29
- 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 -1
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.");
|
|
@@ -224,7 +229,17 @@ function getFollowing(url, redirects = 5) {
|
|
|
224
229
|
req.on("error", reject);
|
|
225
230
|
});
|
|
226
231
|
}
|
|
227
|
-
|
|
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) {
|
|
228
243
|
const url = ffmpegDownloadUrl();
|
|
229
244
|
if (!url) {
|
|
230
245
|
throw new Error(
|
|
@@ -235,8 +250,21 @@ async function downloadFfmpeg() {
|
|
|
235
250
|
const tmp = `${dest}.download`;
|
|
236
251
|
await mkdir2(path4.dirname(dest), { recursive: true });
|
|
237
252
|
await rm2(tmp, { force: true });
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
}
|
|
240
268
|
await chmod(tmp, 493).catch(() => {
|
|
241
269
|
});
|
|
242
270
|
await rm2(dest, { force: true });
|
|
@@ -569,7 +597,16 @@ async function ensureFfmpeg(opts) {
|
|
|
569
597
|
}
|
|
570
598
|
opts.logger.info("Fetching ffmpeg (one-time, ~80 MB)\u2026");
|
|
571
599
|
try {
|
|
572
|
-
|
|
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
|
+
}
|
|
609
|
+
});
|
|
573
610
|
} catch (err) {
|
|
574
611
|
opts.logger.error(`ffmpeg download failed: ${err.message}`);
|
|
575
612
|
return false;
|
|
@@ -665,13 +702,13 @@ var settingsSchema = z.object({
|
|
|
665
702
|
quality: z.enum(["draft", "final"]).default("final").describe('Render quality; "draft" lowers fps/scale and speeds the encoder for fast iteration (default "final").'),
|
|
666
703
|
/** Skip assets whose inputs+options+tool fingerprint is unchanged (opt-in; can be stale). */
|
|
667
704
|
cache: z.boolean().default(false).describe("Skip assets whose inputs+options+tool fingerprint is unchanged (opt-in; can be stale). Default false.")
|
|
668
|
-
});
|
|
705
|
+
}).strict();
|
|
669
706
|
var assetSpecSchema = z.object({
|
|
670
707
|
name: z.string().min(1),
|
|
671
708
|
/**
|
|
672
709
|
* Page to capture: an absolute `https://…` URL, or a path like `/shop` resolved against the
|
|
673
710
|
* managed server's URL. Optional — with a managed server, a url-based asset that omits it
|
|
674
|
-
* captures the server root; local generators (`
|
|
711
|
+
* captures the server root; local generators (`specimen`, `palette`, `wall`, …) need no url.
|
|
675
712
|
*/
|
|
676
713
|
url: z.string().min(1).refine((s) => /^https?:\/\//i.test(s) || s.startsWith("/"), {
|
|
677
714
|
message: 'url must be an absolute http(s) URL or a path starting with "/" (resolved against the managed server)'
|
|
@@ -686,9 +723,11 @@ var assetSpecSchema = z.object({
|
|
|
686
723
|
inputs: z.record(z.string(), z.string()).default({})
|
|
687
724
|
}).strict();
|
|
688
725
|
var showcaseConfigSchema = z.object({
|
|
726
|
+
/** JSON configs may carry an editor `$schema` pointer; accepted and ignored at runtime. */
|
|
727
|
+
$schema: z.string().optional(),
|
|
689
728
|
settings: settingsSchema.default({}),
|
|
690
729
|
assets: z.array(assetSpecSchema).min(1, "Define at least one asset in `assets`.")
|
|
691
|
-
}).superRefine((cfg, ctx) => {
|
|
730
|
+
}).strict().superRefine((cfg, ctx) => {
|
|
692
731
|
const seen = /* @__PURE__ */ new Set();
|
|
693
732
|
for (const [i, asset] of cfg.assets.entries()) {
|
|
694
733
|
if (seen.has(asset.name)) {
|
|
@@ -710,12 +749,12 @@ import { stat as stat2 } from "fs/promises";
|
|
|
710
749
|
import { z as z2 } from "zod";
|
|
711
750
|
var easingSchema = z2.enum([
|
|
712
751
|
"linear",
|
|
713
|
-
"
|
|
714
|
-
"
|
|
715
|
-
"
|
|
716
|
-
"
|
|
717
|
-
"
|
|
718
|
-
"
|
|
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"
|
|
719
758
|
]);
|
|
720
759
|
var choreographyStepSchema = z2.object({
|
|
721
760
|
/** Target: a 0..1 number, an "NN%" string, or a CSS selector to bring into view. */
|
|
@@ -724,7 +763,7 @@ var choreographyStepSchema = z2.object({
|
|
|
724
763
|
durationMs: z2.number().int().nonnegative().optional().describe("Travel time to this target (ms). Default 1200."),
|
|
725
764
|
/** Hold time at this target after arriving (ms). Default 800. */
|
|
726
765
|
holdMs: z2.number().int().nonnegative().optional().describe("Hold time at this target after arriving (ms). Default 800."),
|
|
727
|
-
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".')
|
|
728
767
|
}).strict();
|
|
729
768
|
var interactionActionSchema = z2.object({
|
|
730
769
|
do: z2.enum(["move", "click", "hover", "type", "scrollTo", "wait"]).describe("What this step does."),
|
|
@@ -790,8 +829,8 @@ var scrollReelOptionsSchema = z2.object({
|
|
|
790
829
|
/** Output frames per second (re-encoded from the recording). */
|
|
791
830
|
fps: z2.number().int().positive().max(120).default(30).describe("Output frames per second (re-encoded from the recording). Default 30."),
|
|
792
831
|
/** Time to scroll from top to bottom (ms). */
|
|
793
|
-
|
|
794
|
-
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".'),
|
|
795
834
|
/** Dwell at the top before scrolling (ms). */
|
|
796
835
|
startDelayMs: z2.number().int().nonnegative().default(500).describe("Dwell at the top before scrolling (ms). Default 500."),
|
|
797
836
|
/** Dwell at the bottom after scrolling (ms). */
|
|
@@ -850,7 +889,7 @@ var scrollReelOptionsSchema = z2.object({
|
|
|
850
889
|
scaleFrom: z2.number().positive().optional().describe("Start scale (1 = no zoom). Default 1."),
|
|
851
890
|
/** End scale. Default 1.08. */
|
|
852
891
|
scaleTo: z2.number().positive().optional().describe("End scale. Default 1.08."),
|
|
853
|
-
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".'),
|
|
854
893
|
/** Zoom origin X within the viewport (0 = left, 1 = right). Default 0.5. */
|
|
855
894
|
originX: z2.number().min(0).max(1).optional().describe("Zoom origin X within the viewport (0 = left, 1 = right). Default 0.5."),
|
|
856
895
|
/** Zoom origin Y within the viewport (0 = top, 1 = bottom). Default 0.5. */
|
|
@@ -1040,12 +1079,12 @@ async function applyCapture(context, capture, url) {
|
|
|
1040
1079
|
// src/generators/scroll-reel/scroll.ts
|
|
1041
1080
|
var EASINGS = {
|
|
1042
1081
|
linear: (t) => t,
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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)
|
|
1049
1088
|
};
|
|
1050
1089
|
async function prepareScroll(args) {
|
|
1051
1090
|
const target = findScrollTarget(document, getComputedStyle);
|
|
@@ -1410,17 +1449,17 @@ async function pageScroll(args) {
|
|
|
1410
1449
|
const { durationMs, easing, startDelayMs, endDwellMs } = args;
|
|
1411
1450
|
const ease = (t) => {
|
|
1412
1451
|
switch (easing) {
|
|
1413
|
-
case "
|
|
1452
|
+
case "ease-in-out-cubic":
|
|
1414
1453
|
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
1415
|
-
case "
|
|
1454
|
+
case "ease-in-out-quad":
|
|
1416
1455
|
return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
|
1417
|
-
case "
|
|
1456
|
+
case "ease-out-cubic":
|
|
1418
1457
|
return 1 - Math.pow(1 - t, 3);
|
|
1419
|
-
case "
|
|
1458
|
+
case "ease-in-out-sine":
|
|
1420
1459
|
return -(Math.cos(Math.PI * t) - 1) / 2;
|
|
1421
|
-
case "
|
|
1460
|
+
case "ease-in-out-expo":
|
|
1422
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;
|
|
1423
|
-
case "
|
|
1462
|
+
case "ease-out-quint":
|
|
1424
1463
|
return 1 - Math.pow(1 - t, 5);
|
|
1425
1464
|
default:
|
|
1426
1465
|
return t;
|
|
@@ -1524,7 +1563,7 @@ async function captureScrollWebm(args) {
|
|
|
1524
1563
|
await page.evaluate(prepareScroll, { settleMs: PREWARM_SETTLE_MS });
|
|
1525
1564
|
leadSeconds = (Date.now() - recStart) / 1e3;
|
|
1526
1565
|
const distance = await page.evaluate(pageScroll, {
|
|
1527
|
-
durationMs: options.
|
|
1566
|
+
durationMs: options.durationMs,
|
|
1528
1567
|
easing: options.easing,
|
|
1529
1568
|
startDelayMs: options.startDelayMs,
|
|
1530
1569
|
endDwellMs: options.endDwellMs
|
|
@@ -1995,7 +2034,7 @@ function scrollTimelineTotalMs(o) {
|
|
|
1995
2034
|
if (o.autoSections) {
|
|
1996
2035
|
return autoSectionsBudgetMs(o.autoSections);
|
|
1997
2036
|
}
|
|
1998
|
-
return o.startDelayMs + o.
|
|
2037
|
+
return o.startDelayMs + o.durationMs + o.endDwellMs;
|
|
1999
2038
|
}
|
|
2000
2039
|
function boomerangSpec(spec) {
|
|
2001
2040
|
const forward = spec.segments.map((s) => ({ ...s, durationFraction: s.durationFraction / 2 }));
|
|
@@ -2091,7 +2130,7 @@ async function buildScrollTimeline(page, options, totalSeconds, logger) {
|
|
|
2091
2130
|
return finalize(
|
|
2092
2131
|
defaultTimelineSpec({
|
|
2093
2132
|
startDelayMs: options.startDelayMs,
|
|
2094
|
-
durationMs: options.
|
|
2133
|
+
durationMs: options.durationMs,
|
|
2095
2134
|
endDwellMs: options.endDwellMs,
|
|
2096
2135
|
easing: options.easing
|
|
2097
2136
|
})
|
|
@@ -2852,7 +2891,7 @@ async function run(ctx, options) {
|
|
|
2852
2891
|
...options,
|
|
2853
2892
|
choreography: r.choreography,
|
|
2854
2893
|
autoSections: r.autoSections,
|
|
2855
|
-
|
|
2894
|
+
durationMs: r.durationMs ?? options.durationMs
|
|
2856
2895
|
};
|
|
2857
2896
|
const segPath = path10.join(ctx.tmpDir, `${slugify(ctx.target.name)}-route-${i}.mp4`);
|
|
2858
2897
|
ctx.logger.info(
|
|
@@ -2912,7 +2951,7 @@ async function run(ctx, options) {
|
|
|
2912
2951
|
}
|
|
2913
2952
|
const fileName = options.fileName ?? `${slugify(ctx.target.name)}.mp4`;
|
|
2914
2953
|
const outPath = ctx.resolveOutPath(fileName);
|
|
2915
|
-
const durationSeconds = (options.startDelayMs + options.
|
|
2954
|
+
const durationSeconds = (options.startDelayMs + options.durationMs + options.endDwellMs) / 1e3;
|
|
2916
2955
|
ctx.logger.info(`recording ${url} (realtime)`);
|
|
2917
2956
|
const { webmPath, leadSeconds } = await captureScrollWebm({
|
|
2918
2957
|
browser: ctx.browser,
|
|
@@ -2946,7 +2985,7 @@ async function run(ctx, options) {
|
|
|
2946
2985
|
format: "mp4",
|
|
2947
2986
|
width: options.width,
|
|
2948
2987
|
height: options.height,
|
|
2949
|
-
durationMs: options.startDelayMs + options.
|
|
2988
|
+
durationMs: options.startDelayMs + options.durationMs + options.endDwellMs,
|
|
2950
2989
|
bytes: stats.size,
|
|
2951
2990
|
contentHash,
|
|
2952
2991
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3030,7 +3069,7 @@ import { imageSize } from "image-size";
|
|
|
3030
3069
|
|
|
3031
3070
|
// src/generators/screenshots/options.ts
|
|
3032
3071
|
import { z as z3 } from "zod";
|
|
3033
|
-
var
|
|
3072
|
+
var viewportSchema = z3.object({
|
|
3034
3073
|
name: z3.string().min(1).describe('Label for this viewport \u2014 used in the filename + manifest id (e.g. "desktop").'),
|
|
3035
3074
|
width: z3.number().int().positive().describe("Viewport width in CSS px."),
|
|
3036
3075
|
/** Viewport height. Note: ignored for `fullPage` shots (Playwright resizes to the page height);
|
|
@@ -3038,8 +3077,8 @@ var breakpointSchema = z3.object({
|
|
|
3038
3077
|
height: z3.number().int().positive().default(900).describe(
|
|
3039
3078
|
"Viewport height in CSS px. Default 900. Ignored for fullPage shots; only affects viewport/element captures."
|
|
3040
3079
|
),
|
|
3041
|
-
/** Override the generator-level deviceScaleFactor for this
|
|
3042
|
-
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.")
|
|
3043
3082
|
}).strict();
|
|
3044
3083
|
var elementShotSchema = z3.object({
|
|
3045
3084
|
selector: z3.string().min(1).describe("CSS selector of the element to shoot."),
|
|
@@ -3047,7 +3086,7 @@ var elementShotSchema = z3.object({
|
|
|
3047
3086
|
name: z3.string().min(1).describe("Name used in the filename + manifest id for this element shot.")
|
|
3048
3087
|
}).strict();
|
|
3049
3088
|
var screenshotsOptionsSchema = z3.object({
|
|
3050
|
-
|
|
3089
|
+
viewports: z3.array(viewportSchema).min(1).default([
|
|
3051
3090
|
{ name: "desktop", width: 1440, height: 900 },
|
|
3052
3091
|
{ name: "mobile", width: 390, height: 844 }
|
|
3053
3092
|
]).describe(
|
|
@@ -3061,8 +3100,8 @@ var screenshotsOptionsSchema = z3.object({
|
|
|
3061
3100
|
deviceScaleFactor: z3.number().positive().max(4).default(2).describe("Render scale (2 = retina-crisp). Default 2."),
|
|
3062
3101
|
waitUntil: z3.enum(["load", "domcontentloaded", "networkidle", "commit"]).default("networkidle").describe('Page-load milestone to wait for before capturing. Default "networkidle".'),
|
|
3063
3102
|
waitForSelector: z3.string().optional().describe("Optional element to wait for before capturing (e.g. a hero image). Omit to skip."),
|
|
3064
|
-
/** Element captures taken at every
|
|
3065
|
-
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."),
|
|
3066
3105
|
/** png only: capture with a transparent background. */
|
|
3067
3106
|
omitBackground: z3.boolean().default(false).describe("Capture with a transparent background (png only). Default false."),
|
|
3068
3107
|
/** Extra settle time after load before capturing (ms). */
|
|
@@ -3092,14 +3131,14 @@ async function mapLimit(items, limit, fn) {
|
|
|
3092
3131
|
var MIN_PREPARE_SETTLE_MS = 600;
|
|
3093
3132
|
async function captureScreenshots(args) {
|
|
3094
3133
|
const { options } = args;
|
|
3095
|
-
const
|
|
3096
|
-
options.
|
|
3097
|
-
Math.min(3, options.
|
|
3098
|
-
(bp) =>
|
|
3134
|
+
const perViewport = await mapLimit(
|
|
3135
|
+
options.viewports,
|
|
3136
|
+
Math.min(3, options.viewports.length),
|
|
3137
|
+
(bp) => captureViewport(args, bp)
|
|
3099
3138
|
);
|
|
3100
|
-
return
|
|
3139
|
+
return perViewport.flat();
|
|
3101
3140
|
}
|
|
3102
|
-
async function
|
|
3141
|
+
async function captureViewport(args, bp) {
|
|
3103
3142
|
const { browser, url, options, logger } = args;
|
|
3104
3143
|
const shots = [];
|
|
3105
3144
|
{
|
|
@@ -3224,276 +3263,148 @@ var screenshotsGenerator = {
|
|
|
3224
3263
|
};
|
|
3225
3264
|
|
|
3226
3265
|
// src/generators/wall/options.ts
|
|
3227
|
-
import { z as z6 } from "zod";
|
|
3228
|
-
|
|
3229
|
-
// src/generators/scene/scene-options.ts
|
|
3230
3266
|
import { z as z5 } from "zod";
|
|
3231
3267
|
|
|
3232
|
-
// src/generators/
|
|
3268
|
+
// src/generators/scene/scene-options.ts
|
|
3233
3269
|
import { z as z4 } from "zod";
|
|
3234
|
-
var
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
colors: z4.number().nonnegative().default(0).describe("Fraction of cells whose color changes during this beat (0..1; 1 = every cell once). Default 0."),
|
|
3243
|
-
/**
|
|
3244
|
-
* Target color for this beat's color changes. When set, every color change in the beat goes to
|
|
3245
|
-
* this exact token (a deliberate sweep) instead of a weighted-random pick. Set `colors: 1` with
|
|
3246
|
-
* `pacing: "even"` to wash the whole specimen to one color evenly. Omit for the default
|
|
3247
|
-
* scattered, weighted-random recoloring.
|
|
3248
|
-
*/
|
|
3249
|
-
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."),
|
|
3250
|
-
/**
|
|
3251
|
-
* How the changes are distributed in time across the beat — like a CSS easing curve:
|
|
3252
|
-
* "linear"/"even" = uniform, "ease-in" = front-loaded, "ease-out" = back-loaded,
|
|
3253
|
-
* "ease-in-out" = bunched at both ends, "random" = scattered.
|
|
3254
|
-
*/
|
|
3255
|
-
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")
|
|
3256
3278
|
}).strict();
|
|
3257
|
-
var
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
});
|
|
3264
|
-
var DEFAULT_PULSES = [
|
|
3265
|
-
P("intro hold", 0.8),
|
|
3266
|
-
P("first letters", 0.8, 0.15, 0),
|
|
3267
|
-
P("settle", 1.5),
|
|
3268
|
-
P("ripple", 1, 0.08, 0.04),
|
|
3269
|
-
P("color sweep", 1.2, 0, 0.13),
|
|
3270
|
-
P("rest", 1.2),
|
|
3271
|
-
P("quick burst", 0.6, 0.18, 0),
|
|
3272
|
-
P("drift", 1.2, 0, 0.08),
|
|
3273
|
-
P("finale", 1.2, 0.18, 0.13),
|
|
3274
|
-
P("outro hold", 0.5)
|
|
3275
|
-
];
|
|
3276
|
-
var DEMO_PULSES = [
|
|
3277
|
-
{ name: "linear", duration: 5, chars: 0.5, pacing: "linear" },
|
|
3278
|
-
{ name: "hold", duration: 2 },
|
|
3279
|
-
{ name: "ease-in", duration: 5, chars: 0.5, pacing: "ease-in" },
|
|
3280
|
-
{ name: "hold", duration: 2 },
|
|
3281
|
-
{ name: "ease-out", duration: 5, chars: 0.5, pacing: "ease-out" },
|
|
3282
|
-
{ name: "hold", duration: 2 },
|
|
3283
|
-
{ name: "ease-in-out", duration: 5, chars: 0.5, pacing: "ease-in-out" },
|
|
3284
|
-
{ name: "hold", duration: 2 },
|
|
3285
|
-
{ name: "random", duration: 5, chars: 0.5, pacing: "random" },
|
|
3286
|
-
{ name: "hold", duration: 2 },
|
|
3287
|
-
// Even, per-character color sweeps: each washes every glyph to one token, evenly across the beat.
|
|
3288
|
-
{ name: "sweep \u2192 muted", duration: 4, colors: 1, color: "muted", pacing: "even" },
|
|
3289
|
-
{ name: "sweep \u2192 accent", duration: 4, colors: 1, color: "accent", pacing: "even" },
|
|
3290
|
-
{ name: "sweep \u2192 foreground", duration: 4, colors: 1, color: "foreground", pacing: "even" },
|
|
3291
|
-
{ name: "hold", duration: 2 },
|
|
3292
|
-
{ name: "weighted recolor", duration: 5, colors: 0.6 },
|
|
3293
|
-
{ name: "hold", duration: 2 },
|
|
3294
|
-
{ name: "mingle", duration: 5, chars: 0.3, colors: 0.3 }
|
|
3295
|
-
];
|
|
3296
|
-
var SWEEP_COLORS = {
|
|
3297
|
-
background: "#0b0b0f",
|
|
3298
|
-
foreground: "#f4f4f5",
|
|
3299
|
-
muted: "#6b7280",
|
|
3300
|
-
accent: "#7c9cff"
|
|
3301
|
-
};
|
|
3302
|
-
var SWEEP_PULSES = [
|
|
3303
|
-
{ name: "hold", duration: 0.8 },
|
|
3304
|
-
{ name: "to muted", duration: 2.2, colors: 1, color: "muted", pacing: "ease-in-out" },
|
|
3305
|
-
{ name: "settle", duration: 0.6 },
|
|
3306
|
-
{ name: "to accent", duration: 2.2, colors: 1, color: "accent", pacing: "ease-in-out" },
|
|
3307
|
-
{ name: "settle", duration: 0.6 },
|
|
3308
|
-
{ name: "to foreground", duration: 2.2, colors: 1, color: "foreground", pacing: "ease-in-out" },
|
|
3309
|
-
{ name: "glyph drift", duration: 1.4, chars: 0.5, pacing: "even" },
|
|
3310
|
-
{ name: "hold", duration: 0.6 }
|
|
3311
|
-
];
|
|
3312
|
-
var SPECIMEN_TEMPLATES = {
|
|
3313
|
-
demo: { demo: true, mirror: false, lines: 4, pulses: DEMO_PULSES },
|
|
3314
|
-
sweep: {
|
|
3315
|
-
mirror: true,
|
|
3316
|
-
lines: 4,
|
|
3317
|
-
colors: SWEEP_COLORS,
|
|
3318
|
-
pulses: SWEEP_PULSES
|
|
3319
|
-
}
|
|
3320
|
-
};
|
|
3321
|
-
function applyTemplate(raw) {
|
|
3322
|
-
if (!raw || typeof raw !== "object") return raw;
|
|
3323
|
-
const r = raw;
|
|
3324
|
-
const tmpl = typeof r.template === "string" ? SPECIMEN_TEMPLATES[r.template] : void 0;
|
|
3325
|
-
return tmpl ? { ...tmpl, ...r } : raw;
|
|
3326
|
-
}
|
|
3327
|
-
var specimenObjectSchema = z4.object({
|
|
3328
|
-
font: z4.string().min(1).describe("Font file to showcase (path relative to the working dir, or absolute). Required."),
|
|
3329
|
-
template: z4.enum(["demo", "sweep"]).optional().describe("Load a named option preset (demo or sweep); your explicit options still override what it sets."),
|
|
3330
|
-
name: z4.string().default("").describe('Display name shown bottom-left (e.g. "ABC Oracle"). Default none.'),
|
|
3331
|
-
demo: z4.boolean().default(false).describe("Demo mode: overlay the active pulse's name bottom-right, to see which beat is playing. Default false."),
|
|
3332
|
-
fps: z4.number().int().positive().max(120).default(30).describe("Output frames per second. Default 30."),
|
|
3333
|
-
durationSeconds: z4.number().positive().optional().describe("Clip length in seconds. Defaults to the (mirrored) sum of the pulse durations; set to override."),
|
|
3334
|
-
width: z4.number().int().positive().default(1920).describe("Output frame width in px. Default 1920."),
|
|
3335
|
-
height: z4.number().int().positive().default(1080).describe("Output frame height in px. Default 1080."),
|
|
3336
|
-
deviceScaleFactor: z4.number().positive().max(4).default(1).describe("Render scale (1 = 1:1; higher = crisper capture, downscaled into the video). Default 1."),
|
|
3337
|
-
weight: z4.number().int().min(1).max(1e3).default(820).describe("Glyph weight on the variable-font axis, 1\u20131000. Default 820."),
|
|
3338
|
-
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."),
|
|
3339
|
-
leading: z4.number().positive().default(0.78).describe("Line-height of the glyph block. Default 0.78 (tight, cap-height-hugging)."),
|
|
3340
|
-
blacklist: z4.string().default("").describe('Glyphs to exclude from the showcase, e.g. "QXZ" (case-insensitive). Default none.'),
|
|
3341
|
-
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."),
|
|
3342
|
-
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(""),
|
|
3343
3285
|
colors: z4.object({
|
|
3344
|
-
background: z4.string()
|
|
3345
|
-
foreground: z4.string()
|
|
3346
|
-
muted: z4.string()
|
|
3347
|
-
accent: z4.string().optional()
|
|
3348
|
-
|
|
3349
|
-
label: z4.string().optional().describe("Color of the font-name label (bottom corner); defaults to `foreground` if unset.")
|
|
3350
|
-
// defaults to `foreground` at render
|
|
3351
|
-
}).default({}).describe("Color tokens the glyphs cycle through (any CSS colors). Override any subset. Default: light-grey palette."),
|
|
3352
|
-
colorWeights: z4.object({
|
|
3353
|
-
foreground: z4.number().nonnegative().default(2).describe("Relative likelihood of the foreground token on a random color change. Default 2."),
|
|
3354
|
-
muted: z4.number().nonnegative().default(2).describe("Relative likelihood of the muted token on a random color change. Default 2."),
|
|
3355
|
-
accent: z4.number().nonnegative().default(1).describe("Relative likelihood of the accent token on a random color change. Default 1.")
|
|
3356
|
-
}).default({}).describe("Relative likelihood of each color token on a random (non-targeted) color change. Default 2 / 2 / 1."),
|
|
3357
|
-
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."),
|
|
3358
|
-
characterIntensity: z4.number().nonnegative().default(1).describe("Multiply every pulse's glyph-change fraction (1 = baseline, 2 = twice as busy, 0 = none). Default 1."),
|
|
3359
|
-
colorIntensity: z4.number().nonnegative().default(1).describe("Multiply every pulse's color-change fraction (1 = baseline, 2 = twice as busy, 0 = none). Default 1."),
|
|
3360
|
-
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."),
|
|
3361
|
-
mirror: z4.boolean().default(true).describe("Mirror the pulses (play out and back) for a seamless loop; doubles the clip length. Default true."),
|
|
3362
|
-
crf: z4.number().int().min(0).max(51).default(18).describe("x264 quality, 0\u201351 (lower = better quality / larger file). Default 18."),
|
|
3363
|
-
fileName: z4.string().optional().describe('Output filename; defaults to "<slug(asset name)>.mp4".')
|
|
3364
|
-
}).strict();
|
|
3365
|
-
var specimenOptionsSchema = z4.preprocess(applyTemplate, specimenObjectSchema);
|
|
3366
|
-
|
|
3367
|
-
// src/generators/scene/scene-options.ts
|
|
3368
|
-
var specimenSceneOptionsSchema = z5.object({
|
|
3369
|
-
label: z5.string().default(""),
|
|
3370
|
-
demo: z5.boolean().default(false),
|
|
3371
|
-
weight: z5.number().min(1).max(1e3).default(820),
|
|
3372
|
-
lines: z5.number().int().min(1).max(40).default(3),
|
|
3373
|
-
blacklist: z5.string().default(""),
|
|
3374
|
-
colors: z5.object({
|
|
3375
|
-
background: z5.string(),
|
|
3376
|
-
foreground: z5.string(),
|
|
3377
|
-
muted: z5.string(),
|
|
3378
|
-
accent: z5.string().optional(),
|
|
3379
|
-
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()
|
|
3380
3291
|
}).partial().default({}),
|
|
3381
|
-
colorWeights:
|
|
3382
|
-
foreground:
|
|
3383
|
-
muted:
|
|
3384
|
-
accent:
|
|
3292
|
+
colorWeights: z4.object({
|
|
3293
|
+
foreground: z4.number().nonnegative(),
|
|
3294
|
+
muted: z4.number().nonnegative(),
|
|
3295
|
+
accent: z4.number().nonnegative()
|
|
3385
3296
|
}).partial().default({}),
|
|
3386
|
-
pulses:
|
|
3387
|
-
mirror:
|
|
3388
|
-
characterIntensity:
|
|
3389
|
-
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),
|
|
3390
3301
|
/** Max fraction a line's width may drift as glyphs change (right-edge stability). */
|
|
3391
|
-
maxLineDrift:
|
|
3302
|
+
maxLineDrift: z4.number().positive().max(0.5).default(0.05),
|
|
3392
3303
|
/** Schedule seed — same seed ⇒ identical animation (parallel workers must agree). */
|
|
3393
|
-
seed:
|
|
3304
|
+
seed: z4.number().int().default(1),
|
|
3394
3305
|
/** Line-height of the glyph block. */
|
|
3395
|
-
leading:
|
|
3306
|
+
leading: z4.number().positive().default(0.78),
|
|
3396
3307
|
/** Glyph pool override (≥2 distinct characters after the blacklist). */
|
|
3397
|
-
characterPool:
|
|
3308
|
+
characterPool: z4.string().min(2).optional()
|
|
3398
3309
|
}).strict();
|
|
3399
|
-
var wallEasingEnum =
|
|
3310
|
+
var wallEasingEnum = z4.enum([
|
|
3400
3311
|
"linear",
|
|
3401
3312
|
"ease-in",
|
|
3402
3313
|
"ease-out",
|
|
3403
3314
|
"ease-in-out",
|
|
3404
3315
|
"ease-in-out-strong"
|
|
3405
3316
|
]);
|
|
3406
|
-
var wallPulseSchema =
|
|
3317
|
+
var wallPulseSchema = z4.object({
|
|
3407
3318
|
/** When the pulse starts, as a fraction of the clip (0..1). */
|
|
3408
|
-
at:
|
|
3319
|
+
at: z4.number().min(0).max(1).describe("When the pulse starts, as a fraction of the clip (0..1)."),
|
|
3409
3320
|
/** How long the move takes, as a fraction of the clip (0..1). */
|
|
3410
|
-
|
|
3321
|
+
span: z4.number().positive().max(1).describe("How long the move takes, as a fraction of the clip (0..1)."),
|
|
3411
3322
|
/** How far it travels, in periods (1 = one full tile-set / one wrap). Usually 0..1. */
|
|
3412
|
-
distance:
|
|
3323
|
+
distance: z4.number().nonnegative().describe("How far it travels, in periods (1 = one full tile-set / wrap). Usually 0..1."),
|
|
3413
3324
|
/** Easing of the move's ramp. */
|
|
3414
3325
|
easing: wallEasingEnum.default("ease-in-out").describe("Easing of the move's ramp. Default 'ease-in-out'.")
|
|
3415
3326
|
}).strict();
|
|
3416
|
-
var wallPanSchema =
|
|
3327
|
+
var wallPanSchema = z4.object({
|
|
3417
3328
|
/** Pan direction. */
|
|
3418
|
-
direction:
|
|
3329
|
+
direction: z4.enum(["left", "right"]).default("left").describe("Pan direction. Default 'left'."),
|
|
3419
3330
|
/** Continuous whole-clip horizontal loops (0 = no pan unless `pulses` move it). */
|
|
3420
|
-
loops:
|
|
3331
|
+
loops: z4.number().nonnegative().default(0).describe("Continuous whole-clip horizontal loops (0 = no pan unless pulses move it). Default 0."),
|
|
3421
3332
|
/** Pulses added on top of the base loops. */
|
|
3422
|
-
pulses:
|
|
3333
|
+
pulses: z4.array(wallPulseSchema).default([]).describe("Pulses added on top of the base loops.")
|
|
3423
3334
|
}).strict();
|
|
3424
|
-
var wallColumnSchema =
|
|
3335
|
+
var wallColumnSchema = z4.object({
|
|
3425
3336
|
/** Assets stacked in this column, by name (cycled to fill the height). At least one. */
|
|
3426
|
-
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."),
|
|
3427
3338
|
/** Constant start-position shift, 0..1 of a tile-set — de-aligns columns with similar content. */
|
|
3428
|
-
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."),
|
|
3429
3340
|
/** Scroll direction. Defaults to "down". */
|
|
3430
|
-
direction:
|
|
3341
|
+
direction: z4.enum(["up", "down"]).optional().describe("Scroll direction. Default 'down'."),
|
|
3431
3342
|
/** Continuous whole-clip loops for this column. Omit to inherit the wall-level `loops`. */
|
|
3432
|
-
loops:
|
|
3343
|
+
loops: z4.number().nonnegative().optional().describe("Continuous whole-clip loops for this column. Omit to inherit the wall-level loops."),
|
|
3433
3344
|
/** This column's pulses. Omit to inherit the wall-level `pulses`. */
|
|
3434
|
-
pulses:
|
|
3345
|
+
pulses: z4.array(wallPulseSchema).optional().describe("This column's pulses. Omit to inherit the wall-level pulses.")
|
|
3435
3346
|
}).strict();
|
|
3436
|
-
var fauxTileSchema =
|
|
3347
|
+
var fauxTileSchema = z4.object({
|
|
3437
3348
|
/** Box fill (any CSS color). Omit to auto-derive a distinct color from the tile name. */
|
|
3438
|
-
color:
|
|
3349
|
+
color: z4.string().optional().describe("Box fill (any CSS color). Omit to auto-derive a distinct color from the tile name."),
|
|
3439
3350
|
/** Optional caption shown under the name (e.g. "16:9") — purely cosmetic. */
|
|
3440
|
-
size:
|
|
3351
|
+
size: z4.string().optional().describe("Optional caption shown under the name (e.g. 16:9) \u2014 purely cosmetic."),
|
|
3441
3352
|
/** This faux tile's aspect ratio (width / height): 1.78 = 16:9 (short), 0.56 = 9:16 (tall), 1 =
|
|
3442
3353
|
* square. Omit to use the wall's `tileAspect` default. Real tiles use their media's own aspect. */
|
|
3443
|
-
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.")
|
|
3444
3355
|
}).strict();
|
|
3445
|
-
var wallSceneOptionsSchema =
|
|
3356
|
+
var wallSceneOptionsSchema = z4.object({
|
|
3446
3357
|
/** The columns (≥3) — each its own tiles + motion. Count = array length (fewer = bigger tiles). */
|
|
3447
|
-
columns:
|
|
3358
|
+
columns: z4.array(wallColumnSchema).min(3),
|
|
3448
3359
|
/** Gap between columns and between their tile contents (px). */
|
|
3449
|
-
gap:
|
|
3360
|
+
gap: z4.number().nonnegative().default(16),
|
|
3450
3361
|
/** Default/fallback tile aspect (width / height). Tiles fit the column width and take their OWN
|
|
3451
3362
|
* height from their media's aspect (16:9 → short, 9:16 → tall); this is only used for faux
|
|
3452
3363
|
* (`test`) tiles that don't set their own `aspect`. 1.6 = 16:10 landscape, <1 = portrait. */
|
|
3453
|
-
tileAspect:
|
|
3364
|
+
tileAspect: z4.number().positive().default(1.6),
|
|
3454
3365
|
/** Tile corner radius (px). */
|
|
3455
|
-
cornerRadius:
|
|
3366
|
+
cornerRadius: z4.number().nonnegative().default(12),
|
|
3456
3367
|
/** Backdrop shown in the gap gutters and behind tiles. Defaults to the scene's `background`. */
|
|
3457
|
-
background:
|
|
3368
|
+
background: z4.string().optional(),
|
|
3458
3369
|
/** System 1 — the X pan. */
|
|
3459
3370
|
pan: wallPanSchema.default({}),
|
|
3460
3371
|
/** Default continuous whole-clip loops for columns that omit their own `loops` (0 = static unless
|
|
3461
3372
|
* a pulse moves it; one pulse then rounds the total up to a single loop). */
|
|
3462
|
-
loops:
|
|
3373
|
+
loops: z4.number().nonnegative().default(0),
|
|
3463
3374
|
/** Default pulses for columns that omit their own `pulses` (the uniform wall-level motion). */
|
|
3464
|
-
pulses:
|
|
3375
|
+
pulses: z4.array(wallPulseSchema).default([]),
|
|
3465
3376
|
/** Preview mode: render every tile as a flat labeled color box (see `testTiles`) instead of the
|
|
3466
3377
|
* real assets, so you can dial in layout + motion instantly — no producers run. */
|
|
3467
|
-
test:
|
|
3378
|
+
test: z4.boolean().default(false),
|
|
3468
3379
|
/** Per-tile faux appearance for `test` mode, keyed by tile name. Tiles not listed get an
|
|
3469
3380
|
* auto-derived color and their name as the label. */
|
|
3470
|
-
testTiles:
|
|
3381
|
+
testTiles: z4.record(z4.string(), fauxTileSchema).default({})
|
|
3471
3382
|
}).strict();
|
|
3472
|
-
var reelItemSchema =
|
|
3383
|
+
var reelItemSchema = z4.object({
|
|
3473
3384
|
/** Color display name (already cased per `uppercase`). */
|
|
3474
|
-
name:
|
|
3385
|
+
name: z4.string(),
|
|
3475
3386
|
/** Swatch background hex (`#RRGGBB`). */
|
|
3476
|
-
hex:
|
|
3387
|
+
hex: z4.string(),
|
|
3477
3388
|
/** Contrast-picked text color for this band. */
|
|
3478
|
-
textColor:
|
|
3389
|
+
textColor: z4.string(),
|
|
3479
3390
|
/** Preformatted detail lines revealed on expand (hex / oklch / rgb …). */
|
|
3480
|
-
details:
|
|
3391
|
+
details: z4.array(z4.string())
|
|
3481
3392
|
}).strict();
|
|
3482
|
-
var paletteReelSceneOptionsSchema =
|
|
3483
|
-
items:
|
|
3484
|
-
orientation:
|
|
3485
|
-
holdSeconds:
|
|
3486
|
-
transitionSeconds:
|
|
3487
|
-
bounce:
|
|
3488
|
-
easing:
|
|
3489
|
-
grownFlex:
|
|
3490
|
-
minCrossPx:
|
|
3491
|
-
nameAlwaysVisible:
|
|
3492
|
-
fontWeight:
|
|
3493
|
-
fontSize:
|
|
3494
|
-
detailFontScale:
|
|
3495
|
-
gap:
|
|
3496
|
-
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)
|
|
3497
3408
|
}).strict();
|
|
3498
3409
|
var SCENE_OPTION_SCHEMAS = {
|
|
3499
3410
|
specimen: specimenSceneOptionsSchema,
|
|
@@ -3502,58 +3413,58 @@ var SCENE_OPTION_SCHEMAS = {
|
|
|
3502
3413
|
};
|
|
3503
3414
|
|
|
3504
3415
|
// src/generators/wall/options.ts
|
|
3505
|
-
var wallOptionsSchema =
|
|
3416
|
+
var wallOptionsSchema = z5.object({
|
|
3506
3417
|
// --- output ---
|
|
3507
3418
|
/** Output width (CSS px). */
|
|
3508
|
-
width:
|
|
3419
|
+
width: z5.number().int().positive().default(1920).describe("Output width in CSS px. Default 1920."),
|
|
3509
3420
|
/** Output height (CSS px). */
|
|
3510
|
-
height:
|
|
3421
|
+
height: z5.number().int().positive().default(1080).describe("Output height in CSS px. Default 1080."),
|
|
3511
3422
|
/** Render scale (2 = retina-crisp, downscaled into the video). */
|
|
3512
|
-
deviceScaleFactor:
|
|
3423
|
+
deviceScaleFactor: z5.number().positive().max(4).default(2).describe("Render scale (2 = retina-crisp, downscaled into the video). Default 2."),
|
|
3513
3424
|
/** Output frames per second. */
|
|
3514
|
-
fps:
|
|
3515
|
-
/** Clip length (
|
|
3516
|
-
|
|
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."),
|
|
3517
3428
|
/** x264 quality, 0–51 (lower = better/larger). */
|
|
3518
|
-
crf:
|
|
3429
|
+
crf: z5.number().int().min(0).max(51).default(18).describe("x264 quality, 0\u201351 (lower = better quality / larger file). Default 18."),
|
|
3519
3430
|
/** Capture strategy. "frames" (default) is deterministic + parallelizable. */
|
|
3520
|
-
capture:
|
|
3431
|
+
capture: z5.enum(["frames", "realtime"]).default("frames").describe('Capture strategy. "frames" (default) is deterministic + parallelizable; "realtime" records the live session.'),
|
|
3521
3432
|
/** Parallel frame-render workers. Video-heavy walls can cold-start black under many workers —
|
|
3522
3433
|
* set 1 (or omit) for those. */
|
|
3523
|
-
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."),
|
|
3524
3435
|
/** Intermediate frame format (frames capture only). "jpeg" (default) is fast; "png" is lossless. */
|
|
3525
|
-
frameFormat:
|
|
3436
|
+
frameFormat: z5.enum(["jpeg", "png"]).default("jpeg").describe('Intermediate frame format (frames capture). "jpeg" (default) is fast; "png" is lossless.'),
|
|
3526
3437
|
/** Backdrop shown in the gutters between tiles. */
|
|
3527
|
-
background:
|
|
3438
|
+
background: z5.string().default("#0b0b0f").describe('Backdrop shown in the gutters between tiles. Default "#0b0b0f".'),
|
|
3528
3439
|
/** Output filename; defaults to "<slug(asset name)>.mp4". */
|
|
3529
|
-
fileName:
|
|
3440
|
+
fileName: z5.string().optional().describe('Output filename; defaults to "<slug(asset name)>.mp4".'),
|
|
3530
3441
|
// --- columns (System 2 + layout): each column = its tiles + its own motion ---
|
|
3531
3442
|
/** The columns (≥3) — each its own tiles + motion. Count = array length (fewer = larger tiles). */
|
|
3532
|
-
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."),
|
|
3533
3444
|
/** Gap between columns and between tiles (px). */
|
|
3534
|
-
gap:
|
|
3445
|
+
gap: z5.number().nonnegative().default(8).describe("Gap between columns and between tiles (px). Default 8."),
|
|
3535
3446
|
/** Default/fallback tile aspect (width / height). Tiles fit the column width and take their OWN
|
|
3536
3447
|
* height from their media's aspect (16:9 → short, 9:16 → tall); this is only used for faux
|
|
3537
3448
|
* (`test`) tiles that don't set their own `aspect`. 0.75 = 3:4 portrait. */
|
|
3538
|
-
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."),
|
|
3539
3450
|
/** Tile corner radius (px). */
|
|
3540
|
-
cornerRadius:
|
|
3451
|
+
cornerRadius: z5.number().nonnegative().default(6).describe("Tile corner radius (px). Default 6."),
|
|
3541
3452
|
// --- motion (uniform pulse model) ---
|
|
3542
3453
|
/** System 1 — the whole-wall X pan. */
|
|
3543
3454
|
pan: wallPanSchema.default({}).describe("System 1 \u2014 the whole wall's horizontal pan (`direction` / `loops` / `pulses`). Default: no pan."),
|
|
3544
3455
|
/** Default continuous whole-clip loops for columns that omit their own `loops` (0 = static unless
|
|
3545
3456
|
* a pulse moves it; one pulse then rounds the total up to a single loop). */
|
|
3546
|
-
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)."),
|
|
3547
3458
|
/** Default pulses for columns that omit their own `pulses` (the uniform wall-level motion). */
|
|
3548
|
-
pulses:
|
|
3459
|
+
pulses: z5.array(wallPulseSchema).default([]).describe("Default pulses for columns that omit their own `pulses` (the uniform wall-level motion). Default none."),
|
|
3549
3460
|
// --- test / preview ---
|
|
3550
3461
|
/** Preview mode: render every tile as a flat labeled color box (see `testTiles`) instead of the
|
|
3551
3462
|
* real assets. No producer assets run, so the wall renders in seconds — use it to dial in
|
|
3552
3463
|
* layout + motion, then turn it off for the real render. */
|
|
3553
|
-
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."),
|
|
3554
3465
|
/** Per-tile faux appearance for `test` mode, keyed by tile name. Tiles not listed get an
|
|
3555
3466
|
* auto-derived color and their name as the label. */
|
|
3556
|
-
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).")
|
|
3557
3468
|
}).strict();
|
|
3558
3469
|
|
|
3559
3470
|
// src/generators/scene/index.ts
|
|
@@ -3925,7 +3836,8 @@ async function run3(ctx, o) {
|
|
|
3925
3836
|
background: o.background,
|
|
3926
3837
|
deviceScaleFactor: o.deviceScaleFactor,
|
|
3927
3838
|
fps: o.fps,
|
|
3928
|
-
|
|
3839
|
+
// The scene wire format keeps seconds internally; the authoring surface is milliseconds.
|
|
3840
|
+
durationSeconds: o.durationMs / 1e3,
|
|
3929
3841
|
capture: o.capture,
|
|
3930
3842
|
workers: o.workers,
|
|
3931
3843
|
frameFormat: o.frameFormat,
|
|
@@ -3960,11 +3872,147 @@ var wallGenerator = {
|
|
|
3960
3872
|
run: run3
|
|
3961
3873
|
};
|
|
3962
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
|
+
|
|
3963
4010
|
// src/generators/specimen/index.ts
|
|
3964
4011
|
var SPECIMEN_ID = "specimen";
|
|
3965
4012
|
async function run4(ctx, o) {
|
|
3966
|
-
const
|
|
3967
|
-
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 }));
|
|
3968
4016
|
const sceneOptions = {
|
|
3969
4017
|
scene: "specimen",
|
|
3970
4018
|
width: o.width,
|
|
@@ -3991,7 +4039,7 @@ async function run4(ctx, o) {
|
|
|
3991
4039
|
characterPool: o.characterPool,
|
|
3992
4040
|
colors: o.colors,
|
|
3993
4041
|
colorWeights: o.colorWeights,
|
|
3994
|
-
pulses:
|
|
4042
|
+
pulses: wirePulses,
|
|
3995
4043
|
mirror: o.mirror,
|
|
3996
4044
|
characterIntensity: o.characterIntensity,
|
|
3997
4045
|
colorIntensity: o.colorIntensity,
|
|
@@ -4327,13 +4375,13 @@ var paletteReelObjectSchema = z8.object({
|
|
|
4327
4375
|
details: z8.array(fieldEnum2).default(["hex", "oklch", "rgb"]).describe(
|
|
4328
4376
|
"Fields revealed when a color expands (the name is always shown, so it's ignored here). Default hex + oklch + rgb."
|
|
4329
4377
|
),
|
|
4330
|
-
|
|
4331
|
-
|
|
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."),
|
|
4332
4380
|
bounce: z8.boolean().default(true).describe(
|
|
4333
4381
|
"Ping-pong the sweep so each handoff is between neighbouring bands; off wraps directly (last to first). Default true."
|
|
4334
4382
|
),
|
|
4335
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".'),
|
|
4336
|
-
|
|
4384
|
+
durationMs: z8.number().positive().optional().describe("Clip length override (ms). Omit to derive (count x (hold + transition)) for a clean loop."),
|
|
4337
4385
|
grownFlex: z8.number().min(1).default(12).describe(
|
|
4338
4386
|
"How many times a sliver's share a fully-open band takes (a collapsed sliver is the baseline). Default 12."
|
|
4339
4387
|
),
|
|
@@ -4365,10 +4413,10 @@ var paletteReelOptionsSchema = paletteReelObjectSchema;
|
|
|
4365
4413
|
|
|
4366
4414
|
// src/generators/palette-reel/index.ts
|
|
4367
4415
|
var PALETTE_REEL_ID = "palette-reel";
|
|
4368
|
-
function
|
|
4416
|
+
function deriveDurationMs(o) {
|
|
4369
4417
|
const n = o.colors.length;
|
|
4370
4418
|
const stops = n <= 1 ? n : o.bounce ? 2 * (n - 1) : n;
|
|
4371
|
-
return stops * (o.
|
|
4419
|
+
return stops * (o.holdMs + o.transitionMs);
|
|
4372
4420
|
}
|
|
4373
4421
|
async function run6(ctx, o) {
|
|
4374
4422
|
const fmt = { uppercaseName: o.uppercase, rgbStyle: o.rgbStyle, oklchStyle: o.oklchStyle };
|
|
@@ -4386,7 +4434,7 @@ async function run6(ctx, o) {
|
|
|
4386
4434
|
details: fields.map((f) => formatField(color, f, fmt))
|
|
4387
4435
|
};
|
|
4388
4436
|
});
|
|
4389
|
-
const durationSeconds = o.
|
|
4437
|
+
const durationSeconds = (o.durationMs ?? deriveDurationMs(o)) / 1e3;
|
|
4390
4438
|
const sceneOptions = {
|
|
4391
4439
|
scene: PALETTE_REEL_ID,
|
|
4392
4440
|
width: o.width,
|
|
@@ -4406,8 +4454,8 @@ async function run6(ctx, o) {
|
|
|
4406
4454
|
sceneOptions: {
|
|
4407
4455
|
items,
|
|
4408
4456
|
orientation: o.orientation,
|
|
4409
|
-
holdSeconds: o.
|
|
4410
|
-
transitionSeconds: o.
|
|
4457
|
+
holdSeconds: o.holdMs / 1e3,
|
|
4458
|
+
transitionSeconds: o.transitionMs / 1e3,
|
|
4411
4459
|
bounce: o.bounce,
|
|
4412
4460
|
easing: o.easing,
|
|
4413
4461
|
grownFlex: o.grownFlex,
|
|
@@ -4512,6 +4560,9 @@ function getGenerator(id) {
|
|
|
4512
4560
|
function listGenerators() {
|
|
4513
4561
|
return [...registry.values()];
|
|
4514
4562
|
}
|
|
4563
|
+
function generatorIds() {
|
|
4564
|
+
return [...registry.keys()];
|
|
4565
|
+
}
|
|
4515
4566
|
register(scrollReelGenerator);
|
|
4516
4567
|
register(screenshotsGenerator);
|
|
4517
4568
|
register(wallGenerator);
|
|
@@ -4611,10 +4662,52 @@ var CONFIG_FILES = [
|
|
|
4611
4662
|
".pro-visurc",
|
|
4612
4663
|
".pro-visurc.json"
|
|
4613
4664
|
];
|
|
4614
|
-
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";
|
|
4615
4708
|
|
|
4616
|
-
|
|
4617
|
-
const URL = "http://localhost
|
|
4709
|
+
${urlComment}
|
|
4710
|
+
const URL = "http://localhost:${port}";
|
|
4618
4711
|
|
|
4619
4712
|
export default defineConfig({
|
|
4620
4713
|
settings: {
|
|
@@ -4624,10 +4717,11 @@ export default defineConfig({
|
|
|
4624
4717
|
// installed Chrome, or args: ["--no-sandbox"] on CI.
|
|
4625
4718
|
browser: { headless: true },
|
|
4626
4719
|
// Optional: let the tool build + start your site, wait for it, capture, then stop it.
|
|
4627
|
-
//
|
|
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.
|
|
4628
4722
|
// server: {
|
|
4629
|
-
// build: "
|
|
4630
|
-
// command: "
|
|
4723
|
+
// build: "${runCmd(info.pm, "build")}",
|
|
4724
|
+
// command: "${start}",
|
|
4631
4725
|
// port: 3101,
|
|
4632
4726
|
// },
|
|
4633
4727
|
defaults: {
|
|
@@ -4640,7 +4734,7 @@ export default defineConfig({
|
|
|
4640
4734
|
name: "home-reel",
|
|
4641
4735
|
url: URL,
|
|
4642
4736
|
generator: "scroll-reel",
|
|
4643
|
-
// options: {
|
|
4737
|
+
// options: { durationMs: 7000, waitForSelector: "main" },
|
|
4644
4738
|
},
|
|
4645
4739
|
// A looping type-specimen from a font file (no URL needed):
|
|
4646
4740
|
// {
|
|
@@ -4651,7 +4745,10 @@ export default defineConfig({
|
|
|
4651
4745
|
],
|
|
4652
4746
|
});
|
|
4653
4747
|
`;
|
|
4654
|
-
|
|
4748
|
+
}
|
|
4749
|
+
function jsonConfigTemplate(info) {
|
|
4750
|
+
const port = info.devPort ?? 3101;
|
|
4751
|
+
return `{
|
|
4655
4752
|
"$schema": "./${DEFAULT_SCHEMA_FILE}",
|
|
4656
4753
|
"settings": {
|
|
4657
4754
|
"outDir": "pro-visu",
|
|
@@ -4662,26 +4759,36 @@ var JSON_CONFIG_TEMPLATE = `{
|
|
|
4662
4759
|
}
|
|
4663
4760
|
},
|
|
4664
4761
|
"assets": [
|
|
4665
|
-
{ "name": "home-reel", "url": "http://localhost
|
|
4762
|
+
{ "name": "home-reel", "url": "http://localhost:${port}", "generator": "scroll-reel" }
|
|
4666
4763
|
]
|
|
4667
4764
|
}
|
|
4668
4765
|
`;
|
|
4766
|
+
}
|
|
4669
4767
|
async function runInit(options = {}) {
|
|
4670
4768
|
const cwd = resolveCwd(options.cwd);
|
|
4671
4769
|
const logger = createLogger("info");
|
|
4672
4770
|
let createdSomething = false;
|
|
4673
|
-
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";
|
|
4674
4781
|
const existingConfig = findExistingConfig(cwd);
|
|
4675
4782
|
if (existingConfig) {
|
|
4676
4783
|
logger.info(`config exists (${existingConfig}) \u2014 leaving it untouched`);
|
|
4677
|
-
} else if (
|
|
4678
|
-
await writeFile6(path17.join(cwd, configFile),
|
|
4784
|
+
} else if (useJson) {
|
|
4785
|
+
await writeFile6(path17.join(cwd, configFile), jsonConfigTemplate(info), "utf8");
|
|
4679
4786
|
logger.success(`created ${configFile}`);
|
|
4680
4787
|
await writeFile6(path17.join(cwd, DEFAULT_SCHEMA_FILE), serializeConfigJsonSchema(), "utf8");
|
|
4681
4788
|
logger.success(`created ${DEFAULT_SCHEMA_FILE}`);
|
|
4682
4789
|
createdSomething = true;
|
|
4683
4790
|
} else {
|
|
4684
|
-
await writeFile6(path17.join(cwd, configFile),
|
|
4791
|
+
await writeFile6(path17.join(cwd, configFile), tsConfigTemplate(info), "utf8");
|
|
4685
4792
|
logger.success(`created ${configFile}`);
|
|
4686
4793
|
createdSomething = true;
|
|
4687
4794
|
}
|
|
@@ -4726,8 +4833,9 @@ async function runInit(options = {}) {
|
|
|
4726
4833
|
}
|
|
4727
4834
|
}
|
|
4728
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)";
|
|
4729
4837
|
logger.info(
|
|
4730
|
-
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\`.`
|
|
4731
4839
|
);
|
|
4732
4840
|
}
|
|
4733
4841
|
function findExistingConfig(cwd) {
|
|
@@ -5422,6 +5530,57 @@ var ManifestStore = class _ManifestStore {
|
|
|
5422
5530
|
};
|
|
5423
5531
|
|
|
5424
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
|
+
}
|
|
5425
5584
|
function applyQuality(options, quality) {
|
|
5426
5585
|
if (quality !== "draft") return options;
|
|
5427
5586
|
const o = { ...options };
|
|
@@ -5533,7 +5692,8 @@ async function runPipeline(opts) {
|
|
|
5533
5692
|
reporter?.status(spec.name, "ok");
|
|
5534
5693
|
return { name: spec.name, generator: spec.generator, status: "ok", records: result.assets };
|
|
5535
5694
|
} catch (error) {
|
|
5536
|
-
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));
|
|
5537
5697
|
if (opts.signal?.aborted) {
|
|
5538
5698
|
return { name: spec.name, generator: spec.generator, status: "failed", records: [], error: err, cancelled: true };
|
|
5539
5699
|
}
|
|
@@ -5610,7 +5770,18 @@ function applyDerivedInputs(config) {
|
|
|
5610
5770
|
}
|
|
5611
5771
|
function mergeGeneratorOptions(defaults, spec) {
|
|
5612
5772
|
const generatorDefaults = defaults[spec.generator] ?? {};
|
|
5613
|
-
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;
|
|
5614
5785
|
}
|
|
5615
5786
|
|
|
5616
5787
|
// src/cli/dashboard/index.tsx
|
|
@@ -6091,7 +6262,7 @@ var InkReporter = class {
|
|
|
6091
6262
|
}
|
|
6092
6263
|
};
|
|
6093
6264
|
function createReporter(opts) {
|
|
6094
|
-
const forced = process.env.SHOWCASE_LIVE;
|
|
6265
|
+
const forced = process.env.PRO_VISU_LIVE ?? process.env.SHOWCASE_LIVE;
|
|
6095
6266
|
if (forced === "0") return new NoopReporter();
|
|
6096
6267
|
if (forced === "1") return new InkReporter();
|
|
6097
6268
|
return opts.tty && !opts.verbose ? new InkReporter() : new NoopReporter();
|
|
@@ -6180,6 +6351,13 @@ function renderSummary(outcomes, outDir, columns = 80) {
|
|
|
6180
6351
|
}
|
|
6181
6352
|
|
|
6182
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
|
+
}
|
|
6183
6361
|
function reportConfigError(logger, err) {
|
|
6184
6362
|
if (err instanceof ConfigNotFoundError) {
|
|
6185
6363
|
logger.error(err.message);
|
|
@@ -6187,10 +6365,7 @@ function reportConfigError(logger, err) {
|
|
|
6187
6365
|
}
|
|
6188
6366
|
if (err instanceof ConfigValidationError) {
|
|
6189
6367
|
logger.error(`Invalid config${err.file ? ` (${err.file})` : ""}:`);
|
|
6190
|
-
for (const
|
|
6191
|
-
const where = issue.path.length ? issue.path.join(".") : "(root)";
|
|
6192
|
-
logger.error(` \u2022 ${where}: ${issue.message}`);
|
|
6193
|
-
}
|
|
6368
|
+
for (const line of zodIssueLines(err.zodError)) logger.error(line);
|
|
6194
6369
|
return;
|
|
6195
6370
|
}
|
|
6196
6371
|
logger.error(err instanceof Error ? err.message : String(err));
|
|
@@ -6204,6 +6379,47 @@ function printSummary(logger, outcomes, outDir) {
|
|
|
6204
6379
|
logger.log(renderSummary(outcomes, outDir, process.stdout.columns ?? 80));
|
|
6205
6380
|
}
|
|
6206
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
|
+
|
|
6207
6423
|
// src/cli/commands/generate.ts
|
|
6208
6424
|
async function runGenerate(options = {}) {
|
|
6209
6425
|
const cwd = resolveCwd(options.cwd);
|
|
@@ -6221,35 +6437,76 @@ async function runGenerate(options = {}) {
|
|
|
6221
6437
|
if (config.settings.maxMemoryMB) {
|
|
6222
6438
|
bootstrapLog.info(`Node heap limit: ${currentHeapLimitMB()} MB (settings.maxMemoryMB=${config.settings.maxMemoryMB})`);
|
|
6223
6439
|
}
|
|
6224
|
-
const level = options.verbose ? "debug" : config.settings.logLevel;
|
|
6225
|
-
const reporter = createReporter({ tty: Boolean(process.stdout.isTTY), verbose: !!options.verbose });
|
|
6226
|
-
const logger = reporter.isLive ? createReportingLogger(level, reporter) : createLogger(level);
|
|
6227
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
|
+
}
|
|
6228
6452
|
const requested = normalizeAssetNames(options.asset);
|
|
6229
6453
|
if (requested) {
|
|
6230
6454
|
const known = new Set(config.assets.map((a) => a.name));
|
|
6231
6455
|
const unknown = requested.filter((name) => !known.has(name));
|
|
6232
|
-
|
|
6456
|
+
for (const name of unknown) bootstrapLog.warn(`Unknown asset "${name}"${didYouMean(name, known)}`);
|
|
6233
6457
|
if (requested.every((name) => !known.has(name))) {
|
|
6234
|
-
|
|
6458
|
+
bootstrapLog.error("No matching assets to generate.");
|
|
6235
6459
|
process.exitCode = 1;
|
|
6236
6460
|
return;
|
|
6237
6461
|
}
|
|
6238
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
|
+
}
|
|
6239
6493
|
const usingManaged = !config.settings.browser.channel && !config.settings.browser.executablePath;
|
|
6240
6494
|
if (!options.skipBrowser && usingManaged) {
|
|
6241
|
-
const ready = await ensureChromium({ logger });
|
|
6495
|
+
const ready = await ensureChromium({ logger: bootstrapLog });
|
|
6242
6496
|
if (!ready) {
|
|
6243
|
-
|
|
6497
|
+
bootstrapLog.error("Chromium is required. Run `pro-visu init` or install it manually.");
|
|
6244
6498
|
process.exitCode = 1;
|
|
6245
6499
|
return;
|
|
6246
6500
|
}
|
|
6247
6501
|
}
|
|
6248
|
-
if (!await ensureFfmpeg({ logger })) {
|
|
6249
|
-
|
|
6502
|
+
if (!await ensureFfmpeg({ logger: bootstrapLog })) {
|
|
6503
|
+
bootstrapLog.error("A working ffmpeg is required for video generators.");
|
|
6250
6504
|
process.exitCode = 1;
|
|
6251
6505
|
return;
|
|
6252
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);
|
|
6253
6510
|
await ensureDir(outDir);
|
|
6254
6511
|
await startRunState(outDir);
|
|
6255
6512
|
const abort = new AbortController();
|
|
@@ -6290,23 +6547,6 @@ async function runGenerate(options = {}) {
|
|
|
6290
6547
|
}
|
|
6291
6548
|
}, 1500);
|
|
6292
6549
|
memTimer.unref?.();
|
|
6293
|
-
applyDerivedInputs(config);
|
|
6294
|
-
const selected = expandSelection(config.assets, requested);
|
|
6295
|
-
const anyNeedsServer = selected.some((s) => Boolean(getGenerator(s.generator)?.requiresUrl));
|
|
6296
|
-
if (config.settings.server && !options.skipServer && !anyNeedsServer) {
|
|
6297
|
-
logger.info("No selected asset needs a URL \u2014 skipping the managed server.");
|
|
6298
|
-
}
|
|
6299
|
-
const baseServerCfg = options.skipServer || !anyNeedsServer ? void 0 : config.settings.server;
|
|
6300
|
-
const serverCfg = baseServerCfg && options.skipBuild ? { ...baseServerCfg, build: void 0 } : baseServerCfg;
|
|
6301
|
-
const serverBase = serverCfg ? resolveServerUrl(serverCfg) : void 0;
|
|
6302
|
-
const resolvedConfig = {
|
|
6303
|
-
...config,
|
|
6304
|
-
assets: resolveTargets(
|
|
6305
|
-
config.assets,
|
|
6306
|
-
serverBase,
|
|
6307
|
-
(id) => Boolean(getGenerator(id)?.requiresUrl)
|
|
6308
|
-
)
|
|
6309
|
-
};
|
|
6310
6550
|
const gates = [];
|
|
6311
6551
|
const tasks = {};
|
|
6312
6552
|
if (reporter.isLive && serverCfg) {
|
|
@@ -6338,16 +6578,14 @@ async function runGenerate(options = {}) {
|
|
|
6338
6578
|
server = await startManagedServer(serverCfg, cwd, logger, tasks, abort.signal);
|
|
6339
6579
|
await updateRunState(outDir, { serverPid: server?.pid });
|
|
6340
6580
|
}
|
|
6341
|
-
|
|
6342
|
-
const count = requested ? requested.length : config.assets.length;
|
|
6343
|
-
if (!reporter.isLive) logger.start(`Generating ${count} asset(s)\u2026`);
|
|
6581
|
+
if (!reporter.isLive) logger.start(`Generating ${selected.length} asset(s)\u2026`);
|
|
6344
6582
|
outcomes = await runPipeline({
|
|
6345
6583
|
config: resolvedConfig,
|
|
6346
6584
|
outDir,
|
|
6347
6585
|
logger,
|
|
6348
6586
|
toolVersion: TOOL_VERSION,
|
|
6349
6587
|
assetNames: requested,
|
|
6350
|
-
concurrency:
|
|
6588
|
+
concurrency: concurrencyOverride,
|
|
6351
6589
|
quality: options.draft ? "draft" : void 0,
|
|
6352
6590
|
cache: options.cache,
|
|
6353
6591
|
reporter,
|
|
@@ -6386,6 +6624,107 @@ async function runGenerate(options = {}) {
|
|
|
6386
6624
|
process.exitCode = 1;
|
|
6387
6625
|
}
|
|
6388
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
|
+
}
|
|
6389
6728
|
function taskHandle(reporter, id) {
|
|
6390
6729
|
return {
|
|
6391
6730
|
start: () => reporter.status(id, "running"),
|
|
@@ -6400,6 +6739,83 @@ function normalizeAssetNames(value) {
|
|
|
6400
6739
|
return arr.length ? arr : void 0;
|
|
6401
6740
|
}
|
|
6402
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
|
+
|
|
6403
6819
|
// src/cli/commands/list.ts
|
|
6404
6820
|
import pc from "picocolors";
|
|
6405
6821
|
async function runList(options = {}) {
|
|
@@ -6419,6 +6835,11 @@ async function runList(options = {}) {
|
|
|
6419
6835
|
}
|
|
6420
6836
|
}
|
|
6421
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
|
+
}
|
|
6422
6843
|
if (manifest.assets.length === 0) {
|
|
6423
6844
|
logger.info("Nothing generated yet. Run `pro-visu generate`.");
|
|
6424
6845
|
return;
|
|
@@ -6439,7 +6860,7 @@ import path25 from "path";
|
|
|
6439
6860
|
async function runReset(options = {}) {
|
|
6440
6861
|
const cwd = resolveCwd(options.cwd);
|
|
6441
6862
|
const log = createLogger("info");
|
|
6442
|
-
let outDir = path25.join(cwd,
|
|
6863
|
+
let outDir = path25.join(cwd, DEFAULT_OUTDIR);
|
|
6443
6864
|
try {
|
|
6444
6865
|
const { config } = await loadShowcaseConfig({ cwd, configFile: options.config });
|
|
6445
6866
|
outDir = resolveOutDir(cwd, config.settings.outDir);
|
|
@@ -6474,9 +6895,10 @@ async function runReset(options = {}) {
|
|
|
6474
6895
|
|
|
6475
6896
|
// src/cli/index.ts
|
|
6476
6897
|
var cli = cac("pro-visu");
|
|
6477
|
-
cli.command("init", "Scaffold config, gitignore the output dir, and ensure a browser").option("--cwd <dir>", "Working directory").option("--no-script", "
|
|
6478
|
-
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);
|
|
6479
|
-
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);
|
|
6480
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);
|
|
6481
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);
|
|
6482
6904
|
cli.help();
|
|
@@ -6485,6 +6907,11 @@ var parsed = cli.parse(process.argv, { run: false });
|
|
|
6485
6907
|
async function main() {
|
|
6486
6908
|
checkForUpdates();
|
|
6487
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
|
+
}
|
|
6488
6915
|
if (!parsed.options.help && !parsed.options.version) cli.outputHelp();
|
|
6489
6916
|
return;
|
|
6490
6917
|
}
|
|
@@ -6492,6 +6919,12 @@ async function main() {
|
|
|
6492
6919
|
}
|
|
6493
6920
|
main().catch((err) => {
|
|
6494
6921
|
process.exitCode = 1;
|
|
6495
|
-
|
|
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
|
+
}
|
|
6496
6929
|
});
|
|
6497
6930
|
//# sourceMappingURL=index.js.map
|