@odori/cli 0.0.4 → 0.0.6

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/bin/odori.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import {existsSync} from "node:fs";
3
- import {createRequire} from "node:module";
4
3
  import {dirname, resolve} from "node:path";
5
4
  import {fileURLToPath, pathToFileURL} from "node:url";
6
5
 
@@ -19,21 +18,44 @@ import {fileURLToPath, pathToFileURL} from "node:url";
19
18
  const here = dirname(fileURLToPath(import.meta.url));
20
19
  const built = resolve(here, "..", "dist", "cli.js");
21
20
 
21
+ /*
22
+ * import.meta.resolve, not createRequire().resolve.
23
+ *
24
+ * The published runtime is ESM only: its exports declare `import` and no
25
+ * `require`, so a CommonJS resolver cannot see it at all and throws
26
+ * ERR_PACKAGE_PATH_NOT_EXPORTED. The catch below then answered "yes, source",
27
+ * which meant every installed copy took the transform branch. Not just the
28
+ * pause this was written to avoid, but a real failure: the CLI would run its
29
+ * TypeScript through tsx while Node was already stripping types itself, and
30
+ * loading a project's odori.config.ts landed in a require(esm) cycle that
31
+ * Node refuses outright. odori doctor could not run in a new project.
32
+ *
33
+ * import.meta.resolve honours the same conditions the CLI's own imports use,
34
+ * so it answers the question that was actually being asked.
35
+ */
22
36
  const runtimeIsSource = () => {
23
37
  try {
24
- return /\.tsx?$/.test(createRequire(import.meta.url).resolve("odori"));
38
+ return /\.tsx?$/.test(import.meta.resolve("odori"));
25
39
  } catch {
26
40
  // Unresolvable from here: let the transform handle whatever it is.
27
41
  return true;
28
42
  }
29
43
  };
30
44
 
31
- if (existsSync(built) && !runtimeIsSource()) {
32
- const {run} = await import(pathToFileURL(built).href);
33
- process.exitCode = await run(process.argv.slice(2));
34
- } else {
35
- const {register} = await import("tsx/esm/api");
36
- register();
37
- const {run} = await import("../src/cli.ts");
38
- process.exitCode = await run(process.argv.slice(2));
39
- }
45
+ /*
46
+ * The transform is registered either way, because it is not there for the
47
+ * CLI. A project's videos are .tsx, and Node's own type stripping erases
48
+ * types without understanding JSX, so `odori list` on a real project fails
49
+ * with "Unknown file extension .tsx" the moment nothing is registered.
50
+ *
51
+ * What the branch decides is only where the CLI's own code comes from: its
52
+ * build when there is one, its source inside this repository, where `odori`
53
+ * resolves to a .tsx the build could not have linked against.
54
+ */
55
+ const {register} = await import("tsx/esm/api");
56
+ register();
57
+
58
+ const {run} = await import(
59
+ existsSync(built) && !runtimeIsSource() ? pathToFileURL(built).href : "../src/cli.ts"
60
+ );
61
+ process.exitCode = await run(process.argv.slice(2));
@@ -317,7 +317,7 @@ var toComponent = (item) => ({
317
317
  });
318
318
  var snapshotItems = async () => {
319
319
  try {
320
- const loaded = await import("./registry-snapshot-MSH2EA36.js");
320
+ const loaded = await import("./registry-snapshot-ADKSTGDR.js");
321
321
  return loaded.default.items;
322
322
  } catch {
323
323
  throw new Error(
@@ -465,8 +465,9 @@ var installAsset = async (config, component, options = {}) => {
465
465
  };
466
466
 
467
467
  // src/commands/update.ts
468
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
468
+ import { mkdir as mkdir4, readFile as readFile5, rm as rm2, writeFile as writeFile5 } from "fs/promises";
469
469
  import { existsSync as existsSync6 } from "fs";
470
+ import { readdir as readdir2 } from "fs/promises";
470
471
  import { relative as relative4, resolve as resolve6 } from "path";
471
472
  import { hashString } from "odori";
472
473
 
@@ -530,16 +531,24 @@ var formatDiff = (lines, context = 2) => {
530
531
  };
531
532
 
532
533
  // src/commands/update.ts
533
- var provenanceFile = (config) => resolve6(config.root, config.outDir, "components.json");
534
+ var LOCKFILE = "odori.lock.json";
535
+ var provenanceFile = (config) => resolve6(config.root, LOCKFILE);
536
+ var legacyProvenanceFile = (config) => resolve6(config.root, config.outDir, "components.json");
534
537
  var readProvenance = async (config) => {
535
- const file = provenanceFile(config);
538
+ const file = existsSync6(provenanceFile(config)) ? provenanceFile(config) : legacyProvenanceFile(config);
536
539
  if (!existsSync6(file)) return {};
537
- return JSON.parse(await readFile5(file, "utf8"));
540
+ try {
541
+ return JSON.parse(await readFile5(file, "utf8"));
542
+ } catch {
543
+ log.warn(`${relative4(config.root, file)} is not readable JSON. Ignoring it.`);
544
+ return {};
545
+ }
538
546
  };
539
547
  var writeProvenance = async (config, provenance) => {
540
- await mkdir4(resolve6(config.root, config.outDir), { recursive: true });
548
+ await mkdir4(config.root, { recursive: true });
541
549
  await writeFile5(provenanceFile(config), `${JSON.stringify(provenance, null, 2)}
542
550
  `, "utf8");
551
+ await rm2(legacyProvenanceFile(config), { force: true });
543
552
  };
544
553
  var componentStatus = async (config, only) => {
545
554
  const { items: registry } = await resolveRegistry(config);
@@ -583,11 +592,25 @@ var LABELS = {
583
592
  diverged: "modified locally and updated upstream",
584
593
  missing: "files missing"
585
594
  };
595
+ var explainEmpty = async (config, named) => {
596
+ if (named.length > 0) {
597
+ log.detail(`${named.join(", ")} ${named.length === 1 ? "is" : "are"} not recorded in ${LOCKFILE}.`);
598
+ return;
599
+ }
600
+ const components = resolve6(config.root, config.componentsDir);
601
+ const installed = existsSync6(components) ? (await readdir2(components)).filter((e) => !e.startsWith(".")) : [];
602
+ if (installed.length === 0) {
603
+ log.detail("No registry components are installed yet. Run odori add first.");
604
+ return;
605
+ }
606
+ log.warn(`${installed.length} components are in ${config.componentsDir} but none are recorded in ${LOCKFILE}.`);
607
+ log.detail("Run odori add <name> to re-record them, or commit the lockfile if a teammate has one.");
608
+ };
586
609
  var diffCommand = async (names, options = {}) => {
587
610
  const config = await loadConfig(process.cwd());
588
611
  const statuses = await componentStatus(config, names.length > 0 ? names : void 0);
589
612
  if (statuses.length === 0) {
590
- log.detail("No registry components are installed yet. Run odori add first.");
613
+ await explainEmpty(config, names);
591
614
  return;
592
615
  }
593
616
  for (const status of statuses) {
@@ -613,7 +636,7 @@ var updateCommand = async (names, options = {}) => {
613
636
  const config = await loadConfig(process.cwd());
614
637
  const statuses = await componentStatus(config, names.length > 0 ? names : void 0);
615
638
  if (statuses.length === 0) {
616
- log.detail("No registry components are installed yet. Run odori add first.");
639
+ await explainEmpty(config, names);
617
640
  return;
618
641
  }
619
642
  const provenance = await readProvenance(config);
@@ -662,6 +685,7 @@ var addCommand = async (names, options = {}) => {
662
685
  else log.warn(`registry: the copy built into this CLI. It may be older than ${registryUrl(config)}.`);
663
686
  const queue = [...names.map(normalizeComponentName)];
664
687
  const installed = [];
688
+ const kept = [];
665
689
  while (queue.length > 0) {
666
690
  const name = queue.shift();
667
691
  if (installed.includes(name)) continue;
@@ -717,7 +741,8 @@ var addCommand = async (names, options = {}) => {
717
741
  const current = hashString2(await readFile6(destination, "utf8"));
718
742
  const recorded = provenance[component.name]?.hashes[name2];
719
743
  if (current !== recorded) {
720
- log.warn(`${relative5(config.root, destination)} was modified locally. Keeping your version. Use --force to replace it.`);
744
+ log.warn(`${relative5(config.root, destination)} was modified locally. Keeping your version.`);
745
+ kept.push(relative5(config.root, destination));
721
746
  continue;
722
747
  }
723
748
  }
@@ -757,6 +782,11 @@ var addCommand = async (names, options = {}) => {
757
782
  return;
758
783
  }
759
784
  await writeProvenance(config, provenance);
785
+ if (kept.length > 0) {
786
+ log.warn(`Kept ${kept.length} locally modified ${kept.length === 1 ? "file" : "files"}:`);
787
+ for (const file of kept) log.detail(` ${file}`);
788
+ log.detail(`Run odori diff to see what upstream changed, or odori add <name> --force to replace them.`);
789
+ }
760
790
  log.detail("Run odori dev to preview the installed component fixtures.");
761
791
  };
762
792
  var registryCommand = async () => {
@@ -785,7 +815,7 @@ import { existsSync as existsSync16 } from "fs";
785
815
  import { readFile as readFile13 } from "fs/promises";
786
816
 
787
817
  // src/jobs.ts
788
- import { mkdir as mkdir6, readFile as readFile7, readdir as readdir2, rename, writeFile as writeFile7 } from "fs/promises";
818
+ import { mkdir as mkdir6, readFile as readFile7, readdir as readdir3, rename, writeFile as writeFile7 } from "fs/promises";
789
819
  import { existsSync as existsSync8 } from "fs";
790
820
  import { join as join2, resolve as resolve8 } from "path";
791
821
  var buildsDir = (config) => resolve8(config.root, config.outDir, "builds");
@@ -872,7 +902,7 @@ var reconcileJobs = async (config) => {
872
902
  var listJobs = async (config, options = {}) => {
873
903
  if (!existsSync8(buildsDir(config))) return [];
874
904
  if (options.reconcile !== false) await reconcileJobs(config);
875
- const files = (await readdir2(buildsDir(config))).filter((file) => file.endsWith(".json"));
905
+ const files = (await readdir3(buildsDir(config))).filter((file) => file.endsWith(".json"));
876
906
  const jobs = [];
877
907
  for (const file of files) {
878
908
  try {
@@ -894,13 +924,13 @@ var JobQueue = class {
894
924
  };
895
925
 
896
926
  // src/discovery.ts
897
- import { mkdir as mkdir7, readdir as readdir3, readFile as readFile8, stat, writeFile as writeFile8 } from "fs/promises";
927
+ import { mkdir as mkdir7, readdir as readdir4, readFile as readFile8, stat, writeFile as writeFile8 } from "fs/promises";
898
928
  import { existsSync as existsSync9 } from "fs";
899
929
  import { join as join3, relative as relative6, resolve as resolve9, sep as sep2 } from "path";
900
930
  import { hashString as hashString3 } from "odori";
901
931
  var IGNORED = /* @__PURE__ */ new Set(["node_modules", ".git", ".odori", "out", "dist", ".next"]);
902
932
  var walk = async (directory2, files = []) => {
903
- const entries = await readdir3(directory2, { withFileTypes: true });
933
+ const entries = await readdir4(directory2, { withFileTypes: true });
904
934
  for (const entry of entries) {
905
935
  if (entry.name.startsWith(".") || IGNORED.has(entry.name)) continue;
906
936
  const full = join3(directory2, entry.name);
@@ -1167,7 +1197,7 @@ var createIntegrityResolver = async (config) => {
1167
1197
 
1168
1198
  // src/prepare-cache.ts
1169
1199
  import { existsSync as existsSync11 } from "fs";
1170
- import { mkdir as mkdir9, readFile as readFile10, readdir as readdir4, rm as rm2, writeFile as writeFile10 } from "fs/promises";
1200
+ import { mkdir as mkdir9, readFile as readFile10, readdir as readdir5, rm as rm3, writeFile as writeFile10 } from "fs/promises";
1171
1201
  import { join as join4, resolve as resolve11 } from "path";
1172
1202
  import { hashValue } from "odori";
1173
1203
 
@@ -1199,9 +1229,9 @@ var writePrepareCache = async (config, key, value) => {
1199
1229
  var clearPrepareCache = async (config, videoId) => {
1200
1230
  const target = directory(config);
1201
1231
  if (!existsSync11(target)) return 0;
1202
- const files = await readdir4(target);
1232
+ const files = await readdir5(target);
1203
1233
  const matches = files.filter((file) => videoId ? file.startsWith(`${fileKey(videoId)}__`) : file.endsWith(".json"));
1204
- await Promise.all(matches.map((file) => rm2(join4(target, file), { force: true })));
1234
+ await Promise.all(matches.map((file) => rm3(join4(target, file), { force: true })));
1205
1235
  return matches.length;
1206
1236
  };
1207
1237
 
@@ -1442,7 +1472,7 @@ var alphaWarning = (format, transparent) => transparent && !format.alpha ? `${fo
1442
1472
 
1443
1473
  // src/render.ts
1444
1474
  import { spawn as spawn2 } from "child_process";
1445
- import { copyFile as copyFile2, mkdir as mkdir12, rm as rm3, writeFile as writeFile13 } from "fs/promises";
1475
+ import { copyFile as copyFile2, mkdir as mkdir12, rm as rm4, writeFile as writeFile13 } from "fs/promises";
1446
1476
  import { cpus } from "os";
1447
1477
  import { dirname as dirname5, join as join6, resolve as resolve16 } from "path";
1448
1478
  import { chromium } from "playwright-core";
@@ -2065,7 +2095,7 @@ var renderMovie = async (origin, target, output, config, onProgress, options = {
2065
2095
  );
2066
2096
  const captureMs = performance.now() - captureStart;
2067
2097
  onProgress?.(1, "encoding");
2068
- const mixInputs = (target.audio ?? []).map((cue) => {
2098
+ const mixInputs = (options.audio === false ? [] : target.audio ?? []).map((cue) => {
2069
2099
  const file = resolveCueFile(config, cue.src);
2070
2100
  if (!file) log.warn(`Skipping audio cue ${cue.src}: only project-local files can be encoded.`);
2071
2101
  return file ? { file, cue } : null;
@@ -2136,7 +2166,7 @@ var renderMovie = async (origin, target, output, config, onProgress, options = {
2136
2166
  succeeded = true;
2137
2167
  return output;
2138
2168
  } finally {
2139
- if (succeeded && !options.workDir) await rm3(work, { recursive: true, force: true });
2169
+ if (succeeded && !options.workDir) await rm4(work, { recursive: true, force: true });
2140
2170
  else if (!succeeded) log.detail(`Chunks left for inspection in ${work}`);
2141
2171
  }
2142
2172
  };
@@ -2534,6 +2564,7 @@ var runJob = async (config, origin, record, video, options = {}) => exportQueue.
2534
2564
  quality: options.quality ?? record.render?.quality,
2535
2565
  scale: options.scale ?? record.render?.scale,
2536
2566
  format: options.format ?? (record.render?.format ? resolveFormat(record.render.format, record.output) : void 0),
2567
+ audio: options.audio ?? record.render?.audio,
2537
2568
  skipUnchangedFrames: options.skipUnchangedFrames,
2538
2569
  signal: controller.signal,
2539
2570
  onTimings: (timings) => {
@@ -2596,6 +2627,7 @@ var exportCommand = async (id, options = {}) => {
2596
2627
  format: format.name,
2597
2628
  quality,
2598
2629
  scale,
2630
+ audio: options.audio !== false,
2599
2631
  ...options.preset ? { preset: options.preset } : {}
2600
2632
  });
2601
2633
  })();
@@ -2612,6 +2644,7 @@ var exportCommand = async (id, options = {}) => {
2612
2644
  quality: options.retry && options.quality === void 0 ? void 0 : quality,
2613
2645
  scale: options.retry && options.scale === void 0 ? void 0 : scale,
2614
2646
  format: options.retry ? void 0 : format,
2647
+ audio: options.audio,
2615
2648
  skipUnchangedFrames: options.skipUnchangedFrames,
2616
2649
  onProgress: (next) => {
2617
2650
  if (next.status === "rendering" || next.status === "encoding") {
@@ -2756,7 +2789,8 @@ var devCommand = async (options = {}) => {
2756
2789
  exportDestination(config),
2757
2790
  `${outputName(video.entry.metadata.id)}${format.extension}`
2758
2791
  );
2759
- const record = await createJob(config, manifest, output, { format: format.name, quality, scale });
2792
+ const audio = body.audio !== false;
2793
+ const record = await createJob(config, manifest, output, { format: format.name, quality, scale, audio });
2760
2794
  json(response, 202, record.job);
2761
2795
  void runJob(config, origin, record, video).catch((error) => {
2762
2796
  log.error(`Export failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -2827,7 +2861,7 @@ var devCommand = async (options = {}) => {
2827
2861
 
2828
2862
  // src/commands/doctor.ts
2829
2863
  import { constants } from "fs";
2830
- import { access, mkdir as mkdir13, readFile as readFile14, rm as rm4, writeFile as writeFile14 } from "fs/promises";
2864
+ import { access, mkdir as mkdir13, readFile as readFile14, rm as rm5, writeFile as writeFile14 } from "fs/promises";
2831
2865
  import { existsSync as existsSync17 } from "fs";
2832
2866
  import { createRequire as createRequire3 } from "module";
2833
2867
  import { relative as relative8, resolve as resolve20 } from "path";
@@ -2901,7 +2935,7 @@ var runChecks = async (root) => {
2901
2935
  const probe = resolve20(generated, ".doctor");
2902
2936
  await writeFile14(probe, "", "utf8");
2903
2937
  await access(probe, constants.W_OK);
2904
- await rm4(probe, { force: true });
2938
+ await rm5(probe, { force: true });
2905
2939
  writable = true;
2906
2940
  } catch {
2907
2941
  writable = false;
@@ -2909,10 +2943,10 @@ var runChecks = async (root) => {
2909
2943
  const componentsRoot = resolve20(root, config.componentsDir);
2910
2944
  const orphans = [];
2911
2945
  if (existsSync17(componentsRoot)) {
2912
- const { readdir: readdir8 } = await import("fs/promises");
2913
- for (const entry of await readdir8(componentsRoot, { withFileTypes: true })) {
2946
+ const { readdir: readdir9 } = await import("fs/promises");
2947
+ for (const entry of await readdir9(componentsRoot, { withFileTypes: true })) {
2914
2948
  if (!entry.isDirectory()) continue;
2915
- const files = await readdir8(resolve20(componentsRoot, entry.name));
2949
+ const files = await readdir9(resolve20(componentsRoot, entry.name));
2916
2950
  const source = files.some((file) => /\.tsx$/.test(file) && !file.endsWith(".preview.tsx"));
2917
2951
  const fixture = files.some((file) => file.endsWith(".preview.tsx"));
2918
2952
  if (source && !fixture) orphans.push(entry.name);
@@ -2956,12 +2990,12 @@ var doctorCommand = async (root = process.cwd()) => {
2956
2990
  };
2957
2991
 
2958
2992
  // src/commands/init.ts
2959
- import { mkdir as mkdir15, writeFile as writeFile16 } from "fs/promises";
2993
+ import { mkdir as mkdir15, readFile as readFile15, writeFile as writeFile16 } from "fs/promises";
2960
2994
  import { existsSync as existsSync19 } from "fs";
2961
2995
  import { relative as relative10, resolve as resolve22 } from "path";
2962
2996
 
2963
2997
  // src/commands/new.ts
2964
- import { mkdir as mkdir14, readdir as readdir5, writeFile as writeFile15 } from "fs/promises";
2998
+ import { mkdir as mkdir14, readdir as readdir6, writeFile as writeFile15 } from "fs/promises";
2965
2999
  import { existsSync as existsSync18 } from "fs";
2966
3000
  import { relative as relative9, resolve as resolve21 } from "path";
2967
3001
  var titleCase = (value) => value.split(/[-_\s]+/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
@@ -3023,7 +3057,7 @@ ${closing}
3023
3057
  var installedParts = async (config) => {
3024
3058
  const componentsDir = resolve21(config.root, config.componentsDir);
3025
3059
  if (!existsSync18(componentsDir)) return { title: false, end: false };
3026
- const entries = (await readdir5(componentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
3060
+ const entries = (await readdir6(componentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
3027
3061
  return { title: entries.includes("title-reveal"), end: entries.includes("end-card") };
3028
3062
  };
3029
3063
  var newCommand = async (name, options = {}) => {
@@ -3091,8 +3125,28 @@ var initCommand = async (root = process.cwd()) => {
3091
3125
  await writeFile16(file, contents, "utf8");
3092
3126
  log.success(`Created ${relative10(root, file)}`);
3093
3127
  }
3128
+ await ensureModuleType(root);
3094
3129
  log.detail("Next: odori doctor, then odori add title-reveal end-card, then odori dev.");
3095
3130
  };
3131
+ var ensureModuleType = async (root) => {
3132
+ const file = resolve22(root, "package.json");
3133
+ if (!existsSync19(file)) {
3134
+ log.warn('No package.json here. Odori needs an ESM package: run npm init, then add "type": "module".');
3135
+ return;
3136
+ }
3137
+ let manifest;
3138
+ try {
3139
+ manifest = JSON.parse(await readFile15(file, "utf8"));
3140
+ } catch {
3141
+ log.warn('package.json is not readable JSON, so "type": "module" was not set. Odori needs it.');
3142
+ return;
3143
+ }
3144
+ if (manifest.type === "module") return;
3145
+ manifest.type = "module";
3146
+ await writeFile16(file, `${JSON.stringify(manifest, null, 2)}
3147
+ `, "utf8");
3148
+ log.success('Set "type": "module" in package.json');
3149
+ };
3096
3150
 
3097
3151
  // src/commands/inspect.ts
3098
3152
  import { isOdoriSchema, resolveEntryLayout as resolveEntryLayout5 } from "odori";
@@ -3215,7 +3269,7 @@ import { isOdoriSchema as isOdoriSchema2, resolveEntryLayout as resolveEntryLayo
3215
3269
 
3216
3270
  // src/contracts.ts
3217
3271
  import { existsSync as existsSync20 } from "fs";
3218
- import { readdir as readdir6 } from "fs/promises";
3272
+ import { readdir as readdir7 } from "fs/promises";
3219
3273
  import { resolve as resolve24 } from "path";
3220
3274
  import { cueUrl as cueUrl2, isCueDefinition as isCueDefinition2, resolveEntryLayout as resolveEntryLayout8 } from "odori";
3221
3275
  var primaryFamily = (stack) => (stack.split(",")[0] ?? "").trim().replace(/^["']|["']$/g, "");
@@ -3284,7 +3338,7 @@ var checkAudioWindows = (cues, brand, videoId) => {
3284
3338
  };
3285
3339
  var checkInstalledContracts = async (config, videos) => {
3286
3340
  const componentsDir = resolve24(config.root, config.componentsDir);
3287
- const onDisk = existsSync20(componentsDir) ? (await readdir6(componentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
3341
+ const onDisk = existsSync20(componentsDir) ? (await readdir7(componentsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name) : [];
3288
3342
  const names = /* @__PURE__ */ new Set([...Object.keys(await readProvenance(config)), ...onDisk]);
3289
3343
  if (names.size === 0) return [];
3290
3344
  const { items } = await resolveRegistry(config, { allowNetwork: false });
@@ -3305,7 +3359,7 @@ var checkInstalledContracts = async (config, videos) => {
3305
3359
  };
3306
3360
 
3307
3361
  // src/determinism.ts
3308
- import { readdir as readdir7, readFile as readFile15 } from "fs/promises";
3362
+ import { readdir as readdir8, readFile as readFile16 } from "fs/promises";
3309
3363
  import { existsSync as existsSync21 } from "fs";
3310
3364
  import { join as join7, relative as relative11, resolve as resolve25 } from "path";
3311
3365
  var FORBIDDEN = [
@@ -3339,7 +3393,7 @@ var scanSource = (source, file) => {
3339
3393
  return findings;
3340
3394
  };
3341
3395
  var walk2 = async (directory2, files = []) => {
3342
- for (const entry of await readdir7(directory2, { withFileTypes: true })) {
3396
+ for (const entry of await readdir8(directory2, { withFileTypes: true })) {
3343
3397
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
3344
3398
  const full = join7(directory2, entry.name);
3345
3399
  if (entry.isDirectory()) await walk2(full, files);
@@ -3352,7 +3406,7 @@ var checkDeterminism = async (config) => {
3352
3406
  if (!existsSync21(root)) return [];
3353
3407
  const files = await walk2(root);
3354
3408
  const findings = await Promise.all(
3355
- files.map(async (file) => scanSource(await readFile15(file, "utf8"), relative11(config.root, file)))
3409
+ files.map(async (file) => scanSource(await readFile16(file, "utf8"), relative11(config.root, file)))
3356
3410
  );
3357
3411
  return findings.flat();
3358
3412
  };
@@ -3428,6 +3482,18 @@ var FRAME_SCRIPT = `(() => {
3428
3482
  if (!media && text.length === 0) continue;
3429
3483
 
3430
3484
  var label = text.length > 0 ? '"' + text.slice(0, 32) + '"' : "<" + node.tagName.toLowerCase() + ">";
3485
+
3486
+ /*
3487
+ * Chrome is exempt from the readability floor.
3488
+ *
3489
+ * A component that recreates somebody else's app is right to draw that
3490
+ * app's sidebar at the size that app draws it. Slack's rail really is
3491
+ * 16px, and enlarging it until this check is happy produces a Slack that
3492
+ * does not look like Slack. That text is furniture: it says "this is
3493
+ * Slack", and nobody is meant to read it. What has to be legible is the
3494
+ * content the video is actually about, which is what stays checked.
3495
+ */
3496
+ var chrome = node.closest("[data-odori-chrome]") !== null;
3431
3497
  if (
3432
3498
  box.right > bounds.right + 1 ||
3433
3499
  box.left < bounds.left - 1 ||
@@ -3440,8 +3506,18 @@ var FRAME_SCRIPT = `(() => {
3440
3506
  // Normalize against the shorter side, the same reference useDesignScale
3441
3507
  // uses, so a vertical cut is not judged as if it were letterboxed.
3442
3508
  var reference = Math.min(bounds.width, bounds.height);
3443
- var relative = (parseFloat(style.fontSize) / reference) * 1080;
3444
- if (text.length > 0 && relative > 0 && relative < 20) {
3509
+ /*
3510
+ * What the glyphs actually measure on screen, not what the stylesheet
3511
+ * asked for. A scene that pushes in on a surface makes its text bigger,
3512
+ * and reading font-size alone called that a violation while the viewer
3513
+ * was looking at type half again as large. The ratio of the painted box
3514
+ * to the laid-out box is every ancestor transform multiplied together,
3515
+ * which is exactly the correction wanted, and it cancels the root's own
3516
+ * fit scale because the reference is measured through it too.
3517
+ */
3518
+ var zoom = node.offsetHeight > 0 ? box.height / node.offsetHeight : 1;
3519
+ var relative = ((parseFloat(style.fontSize) * zoom) / reference) * 1080;
3520
+ if (!chrome && text.length > 0 && relative > 0 && relative < 20) {
3445
3521
  var note = label + " at " + Math.round(relative) + "px";
3446
3522
  if (small.indexOf(note) < 0) small.push(note);
3447
3523
  }
@@ -3560,6 +3636,8 @@ var testCommand = async (id, options = {}) => {
3560
3636
  };
3561
3637
 
3562
3638
  // src/cli.ts
3639
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["force", "dry-run", "json", "no-audio", "no-frame-skip", "no-open", "open", "help", "version"]);
3640
+ var RENAMED = { still: "frame" };
3563
3641
  var parseArgs = (argv) => {
3564
3642
  const [command2 = "help", ...rest] = argv;
3565
3643
  const positionals = [];
@@ -3574,7 +3652,7 @@ var parseArgs = (argv) => {
3574
3652
  }
3575
3653
  const name = token.slice(2);
3576
3654
  const next = rest[index + 1];
3577
- if (next === void 0 || next.startsWith("--")) flags[name] = true;
3655
+ if (BOOLEAN_FLAGS.has(name) || next === void 0 || next.startsWith("--")) flags[name] = true;
3578
3656
  else {
3579
3657
  flags[name] = next;
3580
3658
  index += 1;
@@ -3618,7 +3696,7 @@ var COMMAND_FLAGS = {
3618
3696
  inspect: ["json", "input"],
3619
3697
  frame: ["at", "output", "input"],
3620
3698
  test: ["json"],
3621
- export: ["output", "input", "concurrency", "preset", "format", "quality", "scale", "no-frame-skip", "retry"],
3699
+ export: ["output", "input", "concurrency", "preset", "format", "quality", "scale", "no-audio", "no-frame-skip", "retry"],
3622
3700
  jobs: [],
3623
3701
  help: []
3624
3702
  };
@@ -3687,11 +3765,12 @@ var USAGE = {
3687
3765
  check, for CI.`,
3688
3766
  export: `odori export <id> [--output <path>] [--input <json>] [--concurrency <n>]
3689
3767
  [--preset <name>] [--format <name>] [--quality <tier>] [--scale <n>]
3690
- [--no-frame-skip] [--retry <job>]
3768
+ [--no-audio] [--no-frame-skip] [--retry <job>]
3691
3769
  Render and encode a distributable file. --format is mp4, webm, prores, gif,
3692
3770
  or png; without it the output's extension decides, and mp4 is the default.
3693
3771
  --quality is studio, social, or web. --scale multiplies the output size,
3694
- 0.25 to 2. A retry keeps the settings its job was created with.`,
3772
+ 0.25 to 2. --no-audio writes the picture with no sound. A retry keeps the
3773
+ settings its job was created with.`,
3695
3774
  jobs: `odori jobs
3696
3775
  List export jobs and their status.`
3697
3776
  };
@@ -3814,6 +3893,7 @@ var run2 = async (argv) => {
3814
3893
  quality: typeof flags.quality === "string" ? flags.quality : void 0,
3815
3894
  scale: numberFlag(flags, "scale"),
3816
3895
  format: typeof flags.format === "string" ? flags.format : void 0,
3896
+ audio: flags["no-audio"] === true ? false : void 0,
3817
3897
  skipUnchangedFrames: flags["no-frame-skip"] === true ? false : void 0,
3818
3898
  retry: typeof flags.retry === "string" ? flags.retry : void 0
3819
3899
  });
@@ -3827,6 +3907,11 @@ var run2 = async (argv) => {
3827
3907
  log.info(HELP);
3828
3908
  return 0;
3829
3909
  default: {
3910
+ const renamed = RENAMED[command2];
3911
+ if (renamed) {
3912
+ log.error(`"odori ${command2}" is now "odori ${renamed}".`);
3913
+ return 1;
3914
+ }
3830
3915
  const commands = Object.keys(COMMAND_FLAGS).filter((name) => name !== "help");
3831
3916
  const suggestion = nearest(command2, commands);
3832
3917
  log.error(`Unknown command "${command2}".${suggestion ? ` Did you mean "odori ${suggestion}"?` : ""}`);
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  checkFlags,
3
3
  parseArgs,
4
4
  run
5
- } from "./chunk-NYXWEZU2.js";
5
+ } from "./chunk-6CE7U5KB.js";
6
6
  export {
7
7
  checkFlags,
8
8
  parseArgs,
package/dist/index.d.ts CHANGED
@@ -290,6 +290,13 @@ type RenderOptions = {
290
290
  /** Container and codec. Defaults to H.264 in MP4. */
291
291
  format?: VideoFormat;
292
292
  skipUnchangedFrames?: boolean;
293
+ /**
294
+ * Mix the composition's cues into the file. Defaults to true, because the
295
+ * score is part of the video. Set false for a silent cut: a loop for a
296
+ * landing page, a clip going into an editor that has its own audio, or a
297
+ * reviewer who just wants the picture.
298
+ */
299
+ audio?: boolean;
293
300
  /** Reuse encoded chunks whose frames still look identical. */
294
301
  cache?: boolean;
295
302
  signal?: AbortSignal;
@@ -337,6 +344,7 @@ type JobRender = {
337
344
  quality?: string;
338
345
  scale?: number;
339
346
  preset?: string;
347
+ audio?: boolean;
340
348
  };
341
349
  type JobRecord = {
342
350
  job: ExportJob;
@@ -538,6 +546,7 @@ declare const runJob: (config: ResolvedConfig, origin: string, record: JobRecord
538
546
  quality?: Quality;
539
547
  scale?: number;
540
548
  format?: VideoFormat;
549
+ audio?: boolean;
541
550
  skipUnchangedFrames?: boolean;
542
551
  signal?: AbortSignal;
543
552
  onProgress?: (job: ExportJob) => void;
@@ -550,6 +559,8 @@ declare const exportCommand: (id: string, options?: {
550
559
  quality?: string;
551
560
  scale?: number;
552
561
  format?: string;
562
+ /** False writes the picture with no audio track. */
563
+ audio?: boolean;
553
564
  skipUnchangedFrames?: boolean;
554
565
  retry?: string;
555
566
  }) => Promise<ExportJob>;
package/dist/index.js CHANGED
@@ -75,7 +75,7 @@ import {
75
75
  withServer,
76
76
  writeGenerated,
77
77
  writePrepareCache
78
- } from "./chunk-NYXWEZU2.js";
78
+ } from "./chunk-6CE7U5KB.js";
79
79
  export {
80
80
  CHROME_BUILD,
81
81
  FORMATS,