@koda-sl/baker-cli 0.270.1-dev.1f1c09c80 → 0.272.0-dev.1f1c09c80
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/dist/cli.js
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
ulid,
|
|
59
59
|
validateCanvasDeep,
|
|
60
60
|
ytDlpBlockSignal
|
|
61
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-4IJX4R4J.js";
|
|
62
62
|
import {
|
|
63
63
|
csvOrJson,
|
|
64
64
|
daysAgoIso,
|
|
@@ -27320,6 +27320,17 @@ var RecurringElement = z27.object({
|
|
|
27320
27320
|
* slot emits a TODO for somebody to drop one by hand.
|
|
27321
27321
|
*/
|
|
27322
27322
|
source_image: z27.string().optional(),
|
|
27323
|
+
/**
|
|
27324
|
+
* The supplied picture is ALREADY a multi-view reference sheet, so no sheet is
|
|
27325
|
+
* built from it.
|
|
27326
|
+
*
|
|
27327
|
+
* A cast Avatar's identity sheet is exactly that: a rendered turnaround of one
|
|
27328
|
+
* person, which is what `buildElementSheets` exists to produce. Fanning it into a
|
|
27329
|
+
* second sheet paid for an image generation to make a generation of a generation,
|
|
27330
|
+
* and every frame then grounded on the copy rather than on the avatar's own
|
|
27331
|
+
* identity — which is the drift the sheet is supposed to prevent.
|
|
27332
|
+
*/
|
|
27333
|
+
source_is_sheet: z27.boolean().optional(),
|
|
27323
27334
|
// Scenes the element appears in. Either a bare list of scene indices (both
|
|
27324
27335
|
// edges) or per-{scene,edge} entries. Both forms are accepted and merged.
|
|
27325
27336
|
scenes: z27.array(z27.number()).optional(),
|
|
@@ -27493,6 +27504,7 @@ function buildElementSlots(elements) {
|
|
|
27493
27504
|
description: el.description,
|
|
27494
27505
|
sameAs: el.same_as ?? void 0,
|
|
27495
27506
|
castId: el.cast_id ?? void 0,
|
|
27507
|
+
sourceIsSheet: el.source_is_sheet === true,
|
|
27496
27508
|
presence: presenceOf(el)
|
|
27497
27509
|
});
|
|
27498
27510
|
});
|
|
@@ -27536,8 +27548,12 @@ var SHEET_SUBJECT_TYPE = {
|
|
|
27536
27548
|
product: "product",
|
|
27537
27549
|
location: "location"
|
|
27538
27550
|
};
|
|
27539
|
-
function buildElementSheets(slots, nodes) {
|
|
27551
|
+
function buildElementSheets(slots, nodes, sheetModel) {
|
|
27540
27552
|
for (const slot of slots) {
|
|
27553
|
+
if (slot.sourceIsSheet) {
|
|
27554
|
+
slot.sheetBacked = true;
|
|
27555
|
+
continue;
|
|
27556
|
+
}
|
|
27541
27557
|
const subjectType = SHEET_SUBJECT_TYPE[slot.type.toLowerCase()];
|
|
27542
27558
|
if (!subjectType) continue;
|
|
27543
27559
|
if (slot.sameAs) continue;
|
|
@@ -27551,7 +27567,7 @@ function buildElementSheets(slots, nodes) {
|
|
|
27551
27567
|
// The lone dropped ingest is the source; the sheet fans it into a turnaround.
|
|
27552
27568
|
inputs: { references: [slot.ref] },
|
|
27553
27569
|
params: {
|
|
27554
|
-
model: ACTOR_SHEET_MODEL,
|
|
27570
|
+
model: sheetModel ?? ACTOR_SHEET_MODEL,
|
|
27555
27571
|
// The clean-plate clause mirrors the frame prompts' CLEAN PLATE block: a sheet
|
|
27556
27572
|
// that comes back with a fake camera app baked in (P2-22) poisons EVERY frame
|
|
27557
27573
|
// grounded on it, so the suppression must live on the sheet too.
|
|
@@ -29616,7 +29632,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
29616
29632
|
}
|
|
29617
29633
|
});
|
|
29618
29634
|
});
|
|
29619
|
-
buildElementSheets(slots, nodes);
|
|
29635
|
+
buildElementSheets(slots, nodes, opts.sheetModel);
|
|
29620
29636
|
const { clips, voTracks, vo_segments, talking_scenes, routes } = buildTimeline(blueprint, slots, opts, nodes);
|
|
29621
29637
|
const outAspect = resolveAspect(blueprint.source?.aspect_ratio, opts.aspect, genAspectsFor(opts.videoModel));
|
|
29622
29638
|
let videoRef = buildSpine(clips, opts.seamDedup ?? "head", canvasDims(outAspect.outAr), nodes);
|
|
@@ -30515,10 +30531,10 @@ function runDirsToPrune(entries, keep, currentRunId) {
|
|
|
30515
30531
|
return runs.slice(0, Math.max(0, runs.length - keep));
|
|
30516
30532
|
}
|
|
30517
30533
|
async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
|
|
30518
|
-
const { readdir:
|
|
30534
|
+
const { readdir: readdir12 } = await import("fs/promises");
|
|
30519
30535
|
let entries;
|
|
30520
30536
|
try {
|
|
30521
|
-
entries = await
|
|
30537
|
+
entries = await readdir12(outputsDir);
|
|
30522
30538
|
} catch {
|
|
30523
30539
|
return;
|
|
30524
30540
|
}
|
|
@@ -30811,7 +30827,7 @@ async function restoreRunSnapshot(manifest, targetDir, opts = {}) {
|
|
|
30811
30827
|
|
|
30812
30828
|
// src/engine/nodes/local/lib/render-review.ts
|
|
30813
30829
|
import { execFile as execFile2 } from "child_process";
|
|
30814
|
-
import { mkdtemp, readFile as readFile11, rm as rm5 } from "fs/promises";
|
|
30830
|
+
import { mkdtemp, readdir as readdir6, readFile as readFile11, rm as rm5 } from "fs/promises";
|
|
30815
30831
|
import { tmpdir } from "os";
|
|
30816
30832
|
import path15 from "path";
|
|
30817
30833
|
import { promisify as promisify2 } from "util";
|
|
@@ -30819,6 +30835,9 @@ import { z as z28 } from "zod";
|
|
|
30819
30835
|
var RENDER_REVIEW_MODEL = "gemini-pro-latest";
|
|
30820
30836
|
var RENDER_REVIEW_FRAMES = 12;
|
|
30821
30837
|
var RENDER_REVIEW_WIDTH = 1024;
|
|
30838
|
+
function sceneAt(windows, atS) {
|
|
30839
|
+
return windows.find((w) => atS >= w.startS && atS < w.endS)?.file;
|
|
30840
|
+
}
|
|
30822
30841
|
function reviewFrameTimes(durationS, frames) {
|
|
30823
30842
|
const n = Math.max(2, frames);
|
|
30824
30843
|
const last = Math.max(0, durationS - 0.15);
|
|
@@ -30921,19 +30940,23 @@ function collapseRepeats(frames) {
|
|
|
30921
30940
|
}
|
|
30922
30941
|
return [...seen.values()];
|
|
30923
30942
|
}
|
|
30924
|
-
function classifyFrameFindings(frames) {
|
|
30943
|
+
function classifyFrameFindings(frames, windows = []) {
|
|
30925
30944
|
const seen = collapseRepeats(frames);
|
|
30926
30945
|
const anatomy = seen.filter((f) => ANATOMY.test(f.what));
|
|
30927
30946
|
const rest = seen.filter((f) => !ANATOMY.test(f.what));
|
|
30928
|
-
const findings = rest.map((f) =>
|
|
30929
|
-
|
|
30930
|
-
|
|
30931
|
-
|
|
30947
|
+
const findings = rest.map((f) => {
|
|
30948
|
+
const scene = sceneAt(windows, f.at[0]);
|
|
30949
|
+
return {
|
|
30950
|
+
severity: "blocking",
|
|
30951
|
+
what: `At ${f.at.map((s) => `${s}s`).join(", ")}${scene ? ` (${scene})` : ""}: ${f.what}`,
|
|
30952
|
+
...scene ? { scene } : {}
|
|
30953
|
+
};
|
|
30954
|
+
});
|
|
30932
30955
|
if (anatomy.length > 0) {
|
|
30933
30956
|
const at = [...new Set(anatomy.flatMap((f) => f.at))].sort((a, b) => a - b);
|
|
30934
30957
|
findings.push({
|
|
30935
30958
|
severity: "blocking",
|
|
30936
|
-
what: `Malformed hands or limbs in ${at.length} of ${frames.length} sampled frames (${at.map((s) => `${s}s`).join(", ")}): ${anatomy.map((f) => f.what).join(" ")} Re-render those beats \u2014 keep hands out of frame, or away from the object being held.`
|
|
30959
|
+
what: `Malformed hands or limbs in ${at.length} of ${frames.length} sampled frames (${at.map((s) => `${s}s${sceneAt(windows, s) ? ` \u2192 ${sceneAt(windows, s)}` : ""}`).join(", ")}): ${anatomy.map((f) => f.what).join(" ")} Re-render those beats \u2014 keep hands out of frame, or away from the object being held.`
|
|
30937
30960
|
});
|
|
30938
30961
|
}
|
|
30939
30962
|
return findings;
|
|
@@ -31058,7 +31081,7 @@ async function reviewRenderedVideo(opts) {
|
|
|
31058
31081
|
}));
|
|
31059
31082
|
const unread = perFrame.filter((f) => f.json === null).length;
|
|
31060
31083
|
if (unread === perFrame.length && reel === null) return null;
|
|
31061
|
-
const findings = [...classifyFrameFindings(perFrame), ...classifyReelFindings(reel)];
|
|
31084
|
+
const findings = [...classifyFrameFindings(perFrame, opts.windows ?? []), ...classifyReelFindings(reel)];
|
|
31062
31085
|
if (unread > 0) {
|
|
31063
31086
|
findings.push({
|
|
31064
31087
|
severity: "warning",
|
|
@@ -31070,6 +31093,24 @@ async function reviewRenderedVideo(opts) {
|
|
|
31070
31093
|
await rm5(dir, { recursive: true, force: true });
|
|
31071
31094
|
}
|
|
31072
31095
|
}
|
|
31096
|
+
async function sceneWindowsBeside(canvasPath) {
|
|
31097
|
+
const dir = path15.join(path15.dirname(canvasPath), "scenes");
|
|
31098
|
+
const files = await readdir6(dir).catch(() => null);
|
|
31099
|
+
if (!files) return [];
|
|
31100
|
+
const windows = [];
|
|
31101
|
+
for (const name of files.filter((f) => f.endsWith(".json")).sort()) {
|
|
31102
|
+
const raw = await readFile11(path15.join(dir, name), "utf-8").catch(() => null);
|
|
31103
|
+
if (!raw) continue;
|
|
31104
|
+
try {
|
|
31105
|
+
const scene = JSON.parse(raw);
|
|
31106
|
+
if (typeof scene.start_s === "number" && typeof scene.end_s === "number") {
|
|
31107
|
+
windows.push({ file: `scenes/${name}`, startS: scene.start_s, endS: scene.end_s });
|
|
31108
|
+
}
|
|
31109
|
+
} catch {
|
|
31110
|
+
}
|
|
31111
|
+
}
|
|
31112
|
+
return windows;
|
|
31113
|
+
}
|
|
31073
31114
|
|
|
31074
31115
|
// src/commands/canvas/run.ts
|
|
31075
31116
|
var runCommand = defineCommand100({
|
|
@@ -31357,10 +31398,13 @@ ${describeRewrites(healed.rewrites)}
|
|
|
31357
31398
|
`));
|
|
31358
31399
|
}
|
|
31359
31400
|
const reviewable = videoPathFromOutput(result.output);
|
|
31360
|
-
const review = reviewable ? await reviewRenderedVideo({ video: reviewable }) : null;
|
|
31401
|
+
const review = reviewable ? await reviewRenderedVideo({ video: reviewable, windows: await sceneWindowsBeside(filePath) }) : null;
|
|
31361
31402
|
const hints = reviewable ? review === null ? [RENDER_REVIEW_UNAVAILABLE] : review.length === 0 ? ["Reviewed the finished video frame by frame \u2014 nothing to report."] : [
|
|
31362
31403
|
`Reviewed the finished video and found ${review.length} thing${review.length === 1 ? "" : "s"} to fix. Show the user what is wrong before you show them the ad.`,
|
|
31363
|
-
...review.map((f) => `${f.severity === "blocking" ? "MUST FIX" : "SHOULD FIX"} \u2014 ${f.what}`)
|
|
31404
|
+
...review.map((f) => `${f.severity === "blocking" ? "MUST FIX" : "SHOULD FIX"} \u2014 ${f.what}`),
|
|
31405
|
+
...review.some((f) => f.scene) ? [
|
|
31406
|
+
`Fix them, then re-run: edit the \`show\` of the scene files named above \u2014 a shot with a person handling equipment is where limbs and objects go wrong, so widen it, move the hands out of frame, or drop the person from that beat. Re-running re-renders ONLY the scenes you edited; everything else comes from the cache (this run: ${result.stats?.cached_nodes ?? 0} of ${result.stats?.total_nodes ?? 0} nodes were cached).`
|
|
31407
|
+
] : []
|
|
31364
31408
|
] : [];
|
|
31365
31409
|
process.stdout.write(
|
|
31366
31410
|
`${JSON.stringify(
|
|
@@ -31905,7 +31949,7 @@ function isValidScaffoldSlug(slug) {
|
|
|
31905
31949
|
}
|
|
31906
31950
|
|
|
31907
31951
|
// src/commands/canvas/sync-definition.ts
|
|
31908
|
-
import { readdir as
|
|
31952
|
+
import { readdir as readdir7, readFile as readFile13, stat as stat3 } from "fs/promises";
|
|
31909
31953
|
import path20 from "path";
|
|
31910
31954
|
import { defineCommand as defineCommand102 } from "citty";
|
|
31911
31955
|
|
|
@@ -32011,7 +32055,7 @@ async function resolveCanvasPath(inputPath) {
|
|
|
32011
32055
|
}
|
|
32012
32056
|
let entries;
|
|
32013
32057
|
try {
|
|
32014
|
-
entries = await
|
|
32058
|
+
entries = await readdir7(dir);
|
|
32015
32059
|
} catch {
|
|
32016
32060
|
return null;
|
|
32017
32061
|
}
|
|
@@ -32658,14 +32702,25 @@ function adSpecCastElements(spec, avatar) {
|
|
|
32658
32702
|
const scenes = spec.beats.map((beat, i) => ({ beat, i })).filter(({ beat, i }) => beat.cast !== false && !(endCardWanted(spec) && i === lastIndex)).map(({ i }) => i);
|
|
32659
32703
|
if (scenes.length === 0) return [];
|
|
32660
32704
|
const sheet = avatar?.sheetUrl?.trim();
|
|
32661
|
-
return [
|
|
32705
|
+
return [
|
|
32706
|
+
{
|
|
32707
|
+
type: "person",
|
|
32708
|
+
label: "customer",
|
|
32709
|
+
description,
|
|
32710
|
+
scenes,
|
|
32711
|
+
// An avatar's identity sheet IS the turnaround the engine would otherwise render,
|
|
32712
|
+
// so it is handed over as one. Left unmarked, the run paid to generate a sheet OF
|
|
32713
|
+
// a sheet and every frame grounded on the copy instead of the avatar.
|
|
32714
|
+
...sheet ? { source_image: sheet, source_is_sheet: true } : {}
|
|
32715
|
+
}
|
|
32716
|
+
];
|
|
32662
32717
|
}
|
|
32663
32718
|
function adSpecToBlueprint(spec) {
|
|
32664
32719
|
const total = spec.beats.length;
|
|
32665
32720
|
const market = marketFor(spec);
|
|
32666
32721
|
const place = market ? ` Set in ${market}: the architecture, streets and styling are ${market}'s.` : "";
|
|
32667
32722
|
const currency = market ? CURRENCY_BY_MARKET[market.toLowerCase()] : void 0;
|
|
32668
|
-
const physics = " Everything obeys real-world physics: paper, card and screens are OPAQUE with nothing showing through from behind, every object is at believable real-world scale next to the people handling it, and every object has its real-world form and construction \u2014 a phone has ONE screen and it is on the front. NO readable text or numbers anywhere in frame \u2014 phone screens, documents and signage stay illegible or out of focus, because any figure the model invents will contradict the script." + (currency ? ` If a currency is unavoidably visible it is ${currency}.` : "");
|
|
32723
|
+
const physics = " Everything obeys real-world physics: paper, card and screens are OPAQUE with nothing showing through from behind, every object is at believable real-world scale next to the people handling it, and every object has its real-world form and construction \u2014 a phone has ONE screen and it is on the front. NO readable text or numbers anywhere in frame \u2014 phone screens, documents and signage stay illegible or out of focus, because any figure the model invents will contradict the script. Hands are kept simple: no close-up of fingers manipulating small parts, no hand gripping the edge of an object, and each person has exactly TWO arms and TWO legs, all attached and all visible or all out of frame. Anyone working does so the way the trade actually does it: nobody stands or kneels on the equipment being installed, nothing is fitted overhanging an edge or floating unsupported, and every part rests on the structure that would really carry it." + (currency ? ` If a currency is unavoidably visible it is ${currency}.` : "");
|
|
32669
32724
|
const closesOnCard = endCardWanted(spec);
|
|
32670
32725
|
let clock = 0;
|
|
32671
32726
|
const scenes = spec.beats.map((beat, i) => {
|
|
@@ -32826,9 +32881,13 @@ function adBrandOverlayCss() {
|
|
|
32826
32881
|
" /* Capped and wrapping, not sized to its content: measured in a real browser,",
|
|
32827
32882
|
" `GET YOUR FREE QUOTE` renders 948px wide against a 1080px canvas, so the next",
|
|
32828
32883
|
" slightly longer call to action runs off the frame. Two lines beat an edge. */",
|
|
32829
|
-
" /*
|
|
32830
|
-
"
|
|
32831
|
-
"
|
|
32884
|
+
" /* The SAME family the caption track uses, and only names the compiler maps.",
|
|
32885
|
+
" Declaring one matters \u2014 rendered without it the call to action inherited the",
|
|
32886
|
+
" document default and came out in Times beside sans-serif captions. But the",
|
|
32887
|
+
" first attempt led with 'BrandFont', which the overlay composition defines and",
|
|
32888
|
+
" the caption composition does not: HyperFrames refuses an unmapped family and",
|
|
32889
|
+
" the whole render died with `No deterministic font mapping for: BrandFont`. */",
|
|
32890
|
+
" .endcard-cta { font-family: 'Arial Black', 'Helvetica Neue', Arial, sans-serif;",
|
|
32832
32891
|
" font-size: 64px; font-weight: 900; text-transform: uppercase;",
|
|
32833
32892
|
" letter-spacing: 0.01em; color: #fff; padding: 28px 58px; border-radius: 56px;",
|
|
32834
32893
|
" max-width: 900px; line-height: 1.15; background: rgba(0,0,0,0.82); }"
|
|
@@ -32846,7 +32905,7 @@ function injectAdBrandOverlay(indexHtml, overlayHtml) {
|
|
|
32846
32905
|
}
|
|
32847
32906
|
|
|
32848
32907
|
// src/engine/scaffold/ad-brand-source.ts
|
|
32849
|
-
import { readdir as
|
|
32908
|
+
import { readdir as readdir8, readFile as readFile15 } from "fs/promises";
|
|
32850
32909
|
import path22 from "path";
|
|
32851
32910
|
var BRAND_DIR = "src/brand";
|
|
32852
32911
|
function paletteFromBrandDoc(markdown) {
|
|
@@ -32886,7 +32945,7 @@ async function readBrandFromWorkspace(root = ".") {
|
|
|
32886
32945
|
if (palette.length > 0) found.palette = palette;
|
|
32887
32946
|
}
|
|
32888
32947
|
const dir = path22.join(root, BRAND_DIR, "logos");
|
|
32889
|
-
const files = await
|
|
32948
|
+
const files = await readdir8(dir).catch(() => null);
|
|
32890
32949
|
if (files) {
|
|
32891
32950
|
const mark = pickBrandMark(files);
|
|
32892
32951
|
if (mark) found.logo = path22.join(BRAND_DIR, "logos", mark);
|
|
@@ -33035,6 +33094,9 @@ var scaffoldAdCommand = defineCommand104({
|
|
|
33035
33094
|
const logoPath = spec.data.brand?.logo?.trim();
|
|
33036
33095
|
const opts = {
|
|
33037
33096
|
imageModel: AD_IMAGE_MODEL,
|
|
33097
|
+
// The sheet on the SAME model as the frames it feeds. A sheet from another
|
|
33098
|
+
// generator hands every frame a look its own model then reinterprets.
|
|
33099
|
+
sheetModel: AD_IMAGE_MODEL,
|
|
33038
33100
|
videoModel: DEFAULT_VIDEO_GENERATE_MODEL,
|
|
33039
33101
|
overlayCompositionPath: path24.relative(outDir, compositionDest),
|
|
33040
33102
|
captionsCompositionPath: path24.relative(outDir, captionsDest),
|
|
@@ -33165,7 +33227,7 @@ function routeVideoModel(input) {
|
|
|
33165
33227
|
|
|
33166
33228
|
// src/engine/nodes/local/lib/sceneDetect.ts
|
|
33167
33229
|
import { execFile as execFile3 } from "child_process";
|
|
33168
|
-
import { mkdtemp as mkdtemp2, readdir as
|
|
33230
|
+
import { mkdtemp as mkdtemp2, readdir as readdir9, readFile as readFile17, rm as rm6 } from "fs/promises";
|
|
33169
33231
|
import { tmpdir as tmpdir2 } from "os";
|
|
33170
33232
|
import { join as join2 } from "path";
|
|
33171
33233
|
import { promisify as promisify3 } from "util";
|
|
@@ -33239,7 +33301,7 @@ async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs)
|
|
|
33239
33301
|
],
|
|
33240
33302
|
{ encoding: "utf-8", maxBuffer: 32 * 1024 * 1024, timeout: timeoutMs }
|
|
33241
33303
|
);
|
|
33242
|
-
const csvName = (await
|
|
33304
|
+
const csvName = (await readdir9(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
|
|
33243
33305
|
if (!csvName) return [];
|
|
33244
33306
|
return parsePySceneDetectCsvCuts(await readFile17(join2(outDir, csvName), "utf-8"));
|
|
33245
33307
|
} finally {
|
|
@@ -42298,7 +42360,7 @@ Full guide: __tooling__/docs/tools/baker/images.md`
|
|
|
42298
42360
|
import { defineCommand as defineCommand167 } from "citty";
|
|
42299
42361
|
|
|
42300
42362
|
// src/commands/landing/critique.ts
|
|
42301
|
-
import { readdir as
|
|
42363
|
+
import { readdir as readdir11, stat as stat6 } from "fs/promises";
|
|
42302
42364
|
import path31 from "path";
|
|
42303
42365
|
import { defineCommand as defineCommand157 } from "citty";
|
|
42304
42366
|
|
|
@@ -43285,7 +43347,7 @@ async function writeCritiqueSnapshot(projectRoot, snapshot) {
|
|
|
43285
43347
|
}
|
|
43286
43348
|
|
|
43287
43349
|
// src/commands/landing/source-version.ts
|
|
43288
|
-
import { readdir as
|
|
43350
|
+
import { readdir as readdir10, readFile as readFile24, stat as stat5 } from "fs/promises";
|
|
43289
43351
|
import path30 from "path";
|
|
43290
43352
|
async function landingSourceRelPaths(landingDir) {
|
|
43291
43353
|
const rel = [];
|
|
@@ -43326,7 +43388,7 @@ async function isFile(p) {
|
|
|
43326
43388
|
async function walkAstro(dir) {
|
|
43327
43389
|
let entries;
|
|
43328
43390
|
try {
|
|
43329
|
-
entries = await
|
|
43391
|
+
entries = await readdir10(dir, { withFileTypes: true });
|
|
43330
43392
|
} catch {
|
|
43331
43393
|
return [];
|
|
43332
43394
|
}
|
|
@@ -43452,7 +43514,7 @@ async function critiqueOne(projectRoot, slug, brand) {
|
|
|
43452
43514
|
}
|
|
43453
43515
|
async function listLandingSlugs(projectRoot) {
|
|
43454
43516
|
try {
|
|
43455
|
-
const entries = await
|
|
43517
|
+
const entries = await readdir11(path31.join(projectRoot, "src", "pages"), { withFileTypes: true });
|
|
43456
43518
|
return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
43457
43519
|
} catch {
|
|
43458
43520
|
return [];
|