@needle-tools/gltf-build-pipeline 3.0.0-next.7e6c33d → 3.0.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/CHANGELOG.md CHANGED
@@ -4,6 +4,53 @@ All notable changes to this package will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [3.0.0] - 2026-06-23
8
+
9
+ First stable 3.0 release. Summary of the headline changes across the 3.0 alpha/beta cycle.
10
+
11
+ ### Breaking
12
+ - The CLI now requires JWT authentication via `--auth-token`. A missing, malformed, or expired token aborts the run with a clear message that includes the pipeline version.
13
+ - The package ships as a CLI only.
14
+
15
+ ### Added
16
+ - **Job cache**: a file-level cache that skips an entire transform when the same input + config was already processed.
17
+ - **Audio compression**: external audio references are re-encoded to Opus/OGG. Supported input formats are WAV, FLAC, and AIFF; already-lossy formats pass through unchanged.
18
+ - **PMREM CLI command**: `needle-gltf pmrem <input>` converts EXR and Radiance HDR files to PMREM KTX2.
19
+ - **Multiple file inputs**: `transform` accepts comma-separated paths (e.g. `needle-gltf transform a.glb,b.glb out/`).
20
+ - **Diagnostic logging**: authentication failures (with secrets redacted) and pipeline errors — compression failures and unhandled exceptions — are written to a log file to make issues easier to diagnose.
21
+
22
+ ### Fixed
23
+ - VRM thumbnails (and other textures referenced only through an opaque extension) no longer dangle to the wrong image after prune/dedup re-indexing. A stale `KHR_texture_basisu.source` could point past the end of the image array and crash three.js on load (`Cannot read properties of undefined (reading 'uri')`), or silently resolve to a neighbouring image. Re-running the pipeline on an already-compressed VRM is now covered by tests.
24
+ - Audio URIs use forward slashes on every platform; on Windows a `path.join`-built clip path produced backslashes that loaders could not resolve (only affected audio in a subfolder).
25
+ - Cache restore now also restores external files (KTX2, PMREM) alongside the main GLB.
26
+ - PMREM NaN generation that caused artifacts in certain EXR files.
27
+ - xxHash WASM init race condition, now gated by `ensureHashReady`.
28
+ - LOD file detection no longer matches against directory names.
29
+ - toktx now probes the `ktx` binary to match what the compression step actually runs.
30
+ - `getConfig` returns a fresh copy instead of a shared mutable reference.
31
+ - `stats` tolerates undefined errors/warnings in `addFrom` and exits with code 0.
32
+ - Material-only GLBs are no longer incorrectly pruned.
33
+ - `createOpaqueExtension` return type fixed, downstream casts removed.
34
+
35
+ ## [3.0.0-beta.3] - 2026-06-10
36
+
37
+ ### Added
38
+ - Pipeline errors (including compression failures and unhandled exceptions) are now written to the log file to aid diagnosis.
39
+
40
+ ## [3.0.0-beta.2] - 2026-06-10
41
+
42
+ ### Added
43
+ - Authentication failures are now written to a log file (with secrets redacted) to make auth issues easier to diagnose.
44
+
45
+ ### Changed
46
+ - The "Auth failed" message now includes the pipeline version.
47
+
48
+ ## [3.0.0-beta.1] - 2026-06-10
49
+
50
+ ### Fixed
51
+ - VRM thumbnail (and other textures referenced only through an opaque extension) no longer dangle to the wrong image after prune/dedup re-indexing. When an image was deduped, an extension-carried `KHR_texture_basisu.source` was copied verbatim and left stale — the thumbnail's source could point past the end of the image array and crash three.js on load (`Cannot read properties of undefined (reading 'uri')`), while other affected textures silently resolved to a neighbouring image. Re-running the pipeline on an already-compressed VRM is now covered by tests.
52
+ - Audio URIs now use forward slashes on every platform. On Windows the rewritten clip path was built with `path.join`, producing backslashes that loaders cannot resolve (only affected audio assets in a subfolder).
53
+
7
54
  ## [3.0.0-alpha] - 2026-05-26
8
55
 
9
56
  ### Breaking
package/dist/cli/index.js CHANGED
@@ -8,7 +8,7 @@ import path5 from "path";
8
8
  import { Logger, NodeIO as NodeIO2, PropertyType as PropertyType12, Verbosity as Verbosity2 } from "@gltf-transform/core";
9
9
  import { ALL_EXTENSIONS as ALL_EXTENSIONS3 } from "@gltf-transform/extensions";
10
10
  import draco3d2 from "draco3dgltf";
11
- import { existsSync as existsSync11, readFileSync as readFileSync8, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
11
+ import { existsSync as existsSync12, readFileSync as readFileSync8, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
12
12
  import { MeshoptDecoder as MeshoptDecoder2, MeshoptEncoder as MeshoptEncoder3 } from "meshoptimizer";
13
13
  import { dedup as dedup2, metalRough as metalRough2, prune as prune2, resample } from "@gltf-transform/functions";
14
14
 
@@ -689,7 +689,11 @@ var GenericWriter = class {
689
689
  }
690
690
  this.debug && console.log("Could not find texture, creating a new one: " + this._debugName);
691
691
  let extensions = this.inputDefinition.extensions ? { ...this.inputDefinition.extensions } : void 0;
692
- extensions && (delete extensions[NEEDLE_progressive_texture_settings.EXTENSION_NAME], extensions.EXT_texture_exr && this.inputObject.getMimeType() !== "image/exr" && delete extensions.EXT_texture_exr, Object.keys(extensions).length === 0 && (extensions = void 0));
692
+ if (extensions && (delete extensions[NEEDLE_progressive_texture_settings.EXTENSION_NAME], extensions.EXT_texture_exr && this.inputObject.getMimeType() !== "image/exr" && delete extensions.EXT_texture_exr, Object.keys(extensions).length === 0 && (extensions = void 0)), extensions)
693
+ for (let key of Object.keys(extensions)) {
694
+ let extDef = extensions[key];
695
+ extDef?.source !== void 0 && (extensions[key] = { ...extDef, source: imgIndex });
696
+ }
693
697
  let textureDef = {
694
698
  source: extensions && Object.keys(extensions).some((key) => extensions[key]?.source !== void 0) ? void 0 : imgIndex,
695
699
  sampler: this.findOrCreateSampler(context.jsonDoc.json),
@@ -1190,7 +1194,7 @@ function registerExtensions(node) {
1190
1194
  import { createTransform } from "@gltf-transform/functions";
1191
1195
 
1192
1196
  // src/utils/version.gen.ts
1193
- var version = "3.0.0-next.7e6c33d";
1197
+ var version = "3.0.0";
1194
1198
 
1195
1199
  // src/utils/version.ts
1196
1200
  function getVersion(_marklocal = !0) {
@@ -3600,15 +3604,38 @@ function needle_audio_transform(options) {
3600
3604
  }
3601
3605
  function computeOpusOggUri(sourceUri) {
3602
3606
  let dir = dirname6(sourceUri), ext = extname(sourceUri), newName = `${basename3(sourceUri, ext)}.opus.ogg`;
3603
- return dir === "." || dir === "" ? newName : join6(dir, newName);
3607
+ return (dir === "." || dir === "" ? newName : `${dir}/${newName}`).replaceAll("\\", "/");
3608
+ }
3609
+
3610
+ // src/utils/logfile.ts
3611
+ import { appendFileSync, existsSync as existsSync11, mkdirSync as mkdirSync8, renameSync, statSync as statSync5 } from "fs";
3612
+ import { join as join7 } from "path";
3613
+ import { homedir } from "os";
3614
+ var LOG_DIR = join7(homedir(), ".needle", "optimization-pipeline", "logs"), LOG_FILE = join7(LOG_DIR, "pipeline.log"), PREV_LOG_FILE = join7(LOG_DIR, "pipeline.prev.log"), MAX_LOG_BYTES = 1024 * 1024;
3615
+ function redactSecrets(message) {
3616
+ return message.replace(/([a-z][a-z0-9+.-]*:\/\/)[^/@\s]+@/gi, "$1").replace(/\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, "[REDACTED_JWT]").replace(/\bnc_[A-Za-z0-9_-]{6,}\b/g, "[REDACTED_TOKEN]");
3617
+ }
3618
+ function logToFile(message) {
3619
+ try {
3620
+ mkdirSync8(LOG_DIR, { recursive: !0 });
3621
+ try {
3622
+ existsSync11(LOG_FILE) && statSync5(LOG_FILE).size > MAX_LOG_BYTES && renameSync(LOG_FILE, PREV_LOG_FILE);
3623
+ } catch {
3624
+ }
3625
+ appendFileSync(LOG_FILE, `[${(/* @__PURE__ */ new Date()).toISOString()}] ${redactSecrets(message)}
3626
+ `);
3627
+ } catch {
3628
+ }
3604
3629
  }
3605
3630
 
3606
3631
  // src/scripts/pack-gltf.ts
3607
3632
  async function packGLTF(inputFile, outputFile, options) {
3608
3633
  options.logger || (options.logger = new Logger(Verbosity2.INFO));
3609
3634
  let { debug, stats, packedFiles, useCache, logger } = options;
3610
- if (!existsSync11(inputFile))
3611
- return logger.error(`WARN: can not pack, file does not exist: "${inputFile}"`), !1;
3635
+ if (!existsSync12(inputFile)) {
3636
+ let message = `WARN: can not pack, file does not exist: "${inputFile}"`;
3637
+ return logger.error(message), logToFile(message), !1;
3638
+ }
3612
3639
  let cacheKey;
3613
3640
  if (useCache && isMeshLOD(inputFile)) {
3614
3641
  let existing = readFileSync8(inputFile);
@@ -3624,7 +3651,7 @@ async function packGLTF(inputFile, outputFile, options) {
3624
3651
  try {
3625
3652
  let config = options.config;
3626
3653
  outputFile ? outputFile = getOutputPath(inputFile, outputFile) : outputFile = inputFile;
3627
- let prevSizeInMB = statSync5(inputFile).size / 1024 / 1024;
3654
+ let prevSizeInMB = statSync6(inputFile).size / 1024 / 1024;
3628
3655
  logger.debug(`\u2192 Compressing ${prevSizeInMB.toFixed(1)} MB: ${inputFile}`);
3629
3656
  let startTime = Date.now(), io = new NodeIO2().registerExtensions(ALL_EXTENSIONS3).registerExtensions(ALL_EXTENSIONS).registerExtensions([
3630
3657
  NEEDLE_compression_texture,
@@ -3686,32 +3713,32 @@ async function packGLTF(inputFile, outputFile, options) {
3686
3713
  let uri = tex.getURI();
3687
3714
  if (uri && !uri.startsWith("data:")) {
3688
3715
  let absPath = path5.resolve(outDir, uri);
3689
- existsSync11(absPath) && options.externalFiles.push(absPath);
3716
+ existsSync12(absPath) && options.externalFiles.push(absPath);
3690
3717
  }
3691
3718
  }
3692
3719
  for (let buf of root.listBuffers()) {
3693
3720
  let uri = buf.getURI();
3694
3721
  if (uri && !uri.startsWith("data:")) {
3695
3722
  let absPath = path5.resolve(outDir, uri);
3696
- existsSync11(absPath) && options.externalFiles.push(absPath);
3723
+ existsSync12(absPath) && options.externalFiles.push(absPath);
3697
3724
  }
3698
3725
  }
3699
3726
  for (let ref of listAudioRefs()) {
3700
3727
  let uri = ref.newUri ?? ref.sourceUri, absPath = path5.resolve(outDir, uri);
3701
- existsSync11(absPath) && options.externalFiles.push(absPath);
3728
+ existsSync12(absPath) && options.externalFiles.push(absPath);
3702
3729
  }
3703
3730
  }
3704
- logger.debug(`\u2190 Writing to ${outputFile}`), await writeNodeIO(io, outputFile, document), useCache && cacheKey != null && existsSync11(outputFile) && addToCache(cacheKey, readFileSync8(outputFile));
3705
- let duration = Date.now() - startTime, sizeInMB = statSync5(outputFile).size / 1024 / 1024, filename = path5.basename(outputFile), gpuMemoryInMB = totalGPUTextureMemory / 1024 / 1024;
3731
+ logger.debug(`\u2190 Writing to ${outputFile}`), await writeNodeIO(io, outputFile, document), useCache && cacheKey != null && existsSync12(outputFile) && addToCache(cacheKey, readFileSync8(outputFile));
3732
+ let duration = Date.now() - startTime, sizeInMB = statSync6(outputFile).size / 1024 / 1024, filename = path5.basename(outputFile), gpuMemoryInMB = totalGPUTextureMemory / 1024 / 1024;
3706
3733
  return logger.info(`\u2190 Compressed '${filename}' ${prevSizeInMB.toFixed(1)} MB \u2192 ${sizeInMB.toFixed(1)} MB (GPU: ${gpuMemoryInMB.toFixed(1)} MB textures) in ${(duration / 1e3).toFixed(1)} sec`), stats && (stats.totalFilesProcessed++, stats.totalFileSizeInMB += sizeInMB, stats.totalFileSizeInMBBefore += prevSizeInMB, stats.totalGPUMemoryInMB += gpuMemoryInMB), !0;
3707
3734
  } catch (err) {
3708
3735
  if (err instanceof TestAssertionError) throw err;
3709
3736
  let shortedStackTrace = err instanceof Error && err.stack ? err.stack.split(`
3710
3737
  `).slice(0, 2).join(`
3711
- `) : "";
3712
- if (logger.error(`Packing of "${path5.basename(inputFile)}" using version ${getVersion()} failed with error:
3738
+ `) : "", message = `Packing of "${path5.basename(inputFile)}" using version ${getVersion()} failed with error:
3713
3739
  ${err instanceof Error ? err.message : String(err)}
3714
- ${shortedStackTrace}`), debug) throw err;
3740
+ ${shortedStackTrace}`;
3741
+ if (logger.error(message), logToFile(message), debug) throw err;
3715
3742
  return !1;
3716
3743
  }
3717
3744
  }
@@ -3733,7 +3760,7 @@ async function calculateStats(inputfile, options) {
3733
3760
 
3734
3761
  // src/config/index.ts
3735
3762
  import { Logger as Logger3, Verbosity as Verbosity4 } from "@gltf-transform/core";
3736
- import { existsSync as existsSync12, readFileSync as readFileSync9, statSync as statSync6 } from "fs";
3763
+ import { existsSync as existsSync13, readFileSync as readFileSync9, statSync as statSync7 } from "fs";
3737
3764
  import path6, { dirname as dirname7, resolve as resolve2 } from "path";
3738
3765
  var UsecaseOptions = ["product", "world"], defaultConfig = {
3739
3766
  usecase: "default"
@@ -3748,9 +3775,9 @@ function createConfig(usecase) {
3748
3775
  function getConfig(configPath, fallbackSearchDirectory, opts) {
3749
3776
  let logger = opts?.logger;
3750
3777
  if (logger || (logger = new Logger3(Verbosity4.WARN)), configPath != null) {
3751
- if (configPath = resolve2(process.cwd(), configPath), !existsSync12(configPath))
3778
+ if (configPath = resolve2(process.cwd(), configPath), !existsSync13(configPath))
3752
3779
  throw new Error(`No config found at "${configPath}"`);
3753
- if (statSync6(configPath).isDirectory()) {
3780
+ if (statSync7(configPath).isDirectory()) {
3754
3781
  let res2 = searchConfig(configPath, logger);
3755
3782
  if (res2)
3756
3783
  return res2;
@@ -3771,9 +3798,9 @@ function getConfig(configPath, fallbackSearchDirectory, opts) {
3771
3798
  }
3772
3799
  var _cachedConfig;
3773
3800
  function searchConfig(directory, logger) {
3774
- for (statSync6(directory).isFile() && (directory = dirname7(directory)), directory = directory.replaceAll("\\", "/"); directory.length > 0; ) {
3801
+ for (statSync7(directory).isFile() && (directory = dirname7(directory)), directory = directory.replaceAll("\\", "/"); directory.length > 0; ) {
3775
3802
  let configPath = path6.join(directory, "needle.config.json");
3776
- if (existsSync12(configPath)) {
3803
+ if (existsSync13(configPath)) {
3777
3804
  let content = readFileSync9(configPath, "utf8"), config = resolveConfig(configPath, content, logger);
3778
3805
  if (config)
3779
3806
  return config;
@@ -3805,7 +3832,7 @@ function resolveConfig(filename, content, logger) {
3805
3832
  }
3806
3833
 
3807
3834
  // src/cli/index.ts
3808
- import { existsSync as existsSync14, readFileSync as readFileSync11, readdirSync as readdirSync3, statSync as statSync7, writeFileSync as writeFileSync7 } from "fs";
3835
+ import { existsSync as existsSync15, readFileSync as readFileSync11, readdirSync as readdirSync3, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
3809
3836
  import path7, { basename as basename4, dirname as dirname8, resolve as resolve3 } from "path";
3810
3837
 
3811
3838
  // src/constants.ts
@@ -3871,6 +3898,13 @@ function trackError(error, context) {
3871
3898
  return;
3872
3899
  }
3873
3900
  recentErrors.set(key, { time: now, count: (cached?.count || 0) + 1 });
3901
+ let ctx = [
3902
+ context?.mode && `mode=${context.mode}`,
3903
+ context?.usecase && `usecase=${context.usecase}`,
3904
+ context?.file && `file=${context.file}`
3905
+ ].filter(Boolean).join(", ");
3906
+ logToFile(`[error] ${errorName}: ${message}${ctx ? ` (${ctx})` : ""}${stack ? `
3907
+ ${stack}` : ""}`);
3874
3908
  let properties = {
3875
3909
  version: getVersion(),
3876
3910
  error_name: errorName + (cached && cached.count > 1 ? ` x${cached.count}` : ""),
@@ -3922,9 +3956,9 @@ var PROD_KEYS = [
3922
3956
  var trusted = [...PROD_KEYS], TRUSTED_KEYS = trusted;
3923
3957
 
3924
3958
  // src/auth/verify.ts
3925
- import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
3926
- import { join as join8 } from "path";
3927
- import { homedir } from "os";
3959
+ import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
3960
+ import { join as join9 } from "path";
3961
+ import { homedir as homedir2 } from "os";
3928
3962
  var AuthError = class extends Error {
3929
3963
  constructor(message) {
3930
3964
  super(message), this.name = "AuthError";
@@ -3932,29 +3966,32 @@ var AuthError = class extends Error {
3932
3966
  }, LOCAL_FETCH_TIMEOUT_MS = 2e3;
3933
3967
  function getLocalServerToken() {
3934
3968
  try {
3935
- let file = join8(homedir(), ".needle", "local-server-token");
3936
- if (!existsSync13(file)) return null;
3969
+ let file = join9(homedir2(), ".needle", "local-server-token");
3970
+ if (!existsSync14(file)) return null;
3937
3971
  let data = JSON.parse(readFileSync10(file, "utf-8"));
3938
3972
  return typeof data.token == "string" ? data.token : null;
3939
- } catch {
3940
- return null;
3973
+ } catch (err) {
3974
+ return logToFile(`[auth] failed reading local-server-token (${err instanceof Error ? err.name : "unknown error"})`), null;
3941
3975
  }
3942
3976
  }
3943
3977
  async function tryFetchLocalToken(org) {
3978
+ let baseUrl = process.env.NEEDLE_LICENSE_SERVER_URL || "http://localhost:8424/api/license", safeServer = "local license server";
3944
3979
  try {
3945
- let baseUrl = process.env.NEEDLE_LICENSE_SERVER_URL || "http://localhost:8424/api/license", url = new URL(baseUrl);
3946
- org && url.searchParams.set("org", org);
3980
+ let url = new URL(baseUrl);
3981
+ org && url.searchParams.set("org", org), safeServer = `${url.origin}${url.pathname}`;
3947
3982
  let headers = {}, serverToken = getLocalServerToken();
3948
3983
  serverToken && (headers.Authorization = `Bearer ${serverToken}`);
3949
3984
  let res = await fetch(url, {
3950
3985
  signal: AbortSignal.timeout(LOCAL_FETCH_TIMEOUT_MS),
3951
3986
  headers
3952
3987
  });
3953
- if (!res.ok) return null;
3988
+ if (!res.ok)
3989
+ return logToFile(`[auth] license server (${safeServer}) returned HTTP ${res.status} ${res.statusText}${org ? ` \u2014 verify the --org value "${org}"` : ""}`), null;
3954
3990
  let jwt = (await res.json()).needle_license_jwt;
3955
- return typeof jwt == "string" && jwt.length > 0 ? jwt : null;
3956
- } catch {
3957
- return null;
3991
+ return typeof jwt == "string" && jwt.length > 0 ? jwt : (logToFile(`[auth] license server (${safeServer}) responded without a license token${org ? ` for org "${org}"` : ""}`), null);
3992
+ } catch (err) {
3993
+ let msg = err instanceof Error ? err.message : String(err);
3994
+ return logToFile(`[auth] could not reach license server (${safeServer})${org ? ` for org "${org}"` : ""}: ${msg}`), null;
3958
3995
  }
3959
3996
  }
3960
3997
  async function verifyWithKey(token, key) {
@@ -4019,7 +4056,7 @@ async function gateAuth(authToken, org, logger) {
4019
4056
  await requireAuth(authToken, typeof org == "string" ? org : void 0);
4020
4057
  } catch (err) {
4021
4058
  let message = err instanceof AuthError || err instanceof Error ? err.message : String(err);
4022
- logger.error(`[Needle Build Pipeline] Auth failed: ${message}`), process.exit(ERROR_CODES.AUTH_FAILED);
4059
+ logger.error(`[Needle Build Pipeline] v${getVersion()} Auth failed: ${message}`), logToFile(`[Needle Build Pipeline] v${getVersion()} Auth failed: ${message}`), process.exit(ERROR_CODES.AUTH_FAILED);
4023
4060
  }
4024
4061
  }
4025
4062
  program.cast(!1);
@@ -4073,7 +4110,7 @@ When caching is enabled then previously processed files will be detected and loa
4073
4110
  }
4074
4111
  let set = /* @__PURE__ */ new Set(), useCache = options.cache !== !1, debug = options.debug === !0, verbose = options.verbose === !0;
4075
4112
  await foreachGLTF(inputPaths, async (file) => {
4076
- if (existsSync14(file)) {
4113
+ if (existsSync15(file)) {
4077
4114
  if (file != input && options.progressive === !0 && isLOD(file)) {
4078
4115
  logger.debug(`\u2192 Ignore existing LOD file at ${file}`);
4079
4116
  return;
@@ -4089,12 +4126,12 @@ When caching is enabled then previously processed files will be detected and loa
4089
4126
  config
4090
4127
  }, file) : void 0;
4091
4128
  if (jobKey) {
4092
- let outputDir = output ? existsSync14(output) && isDirectory(output) ? output : dirname8(output) : dirname8(file), restored = tryRestoreJobResult(jobKey, outputDir, logger);
4129
+ let outputDir = output ? existsSync15(output) && isDirectory(output) ? output : dirname8(output) : dirname8(file), restored = tryRestoreJobResult(jobKey, outputDir, logger);
4093
4130
  if (restored) {
4094
- let inputFileSize = statSync7(file).size / 1024 / 1024;
4131
+ let inputFileSize = statSync8(file).size / 1024 / 1024;
4095
4132
  for (let f of restored)
4096
4133
  if (set.add(f), stats) {
4097
- let sizeInMB = statSync7(f).size / 1024 / 1024;
4134
+ let sizeInMB = statSync8(f).size / 1024 / 1024;
4098
4135
  stats.totalFilesProcessed++, stats.totalFileSizeInMB += sizeInMB, stats.totalFileSizeInMBBefore += inputFileSize;
4099
4136
  }
4100
4137
  logger.info(`\u2192 [CACHE] Restored ${restored.length} file(s) for job ${jobKey}`);
@@ -4165,7 +4202,7 @@ raw .exr / .hdr inputs.
4165
4202
  `).argument("<input>", "Path to a .exr/.hdr file OR a directory containing .exr/.hdr files (recursive)", { validator: program.STRING }).argument("<output>", "Output file (when input is a single .exr/.hdr) or output directory. Defaults to writing next to the input file.", { default: "", validator: program.STRING }).option("--cache", 'Use "--cache False" to disable caching.', { validator: program.BOOLEAN, default: !0 }).option("--debug", 'Enable verbose output with "--debug True"', { validator: program.BOOLEAN, default: !1 }).option("--verbose", 'Enable verbose output with "--verbose True"', { validator: program.BOOLEAN, default: !1 }).option("--auth-token <jwt>", "Required. A JWT minted by needle-cloud (alternatively set NEEDLE_CLOUD_TOKEN). Note: an `nc_*` needle-cloud access token is NOT a JWT and will be rejected.", { validator: program.STRING }).option("--org <org>", "Organization ID for local license-server fallback (or set NEEDLE_CLOUD_ORG).", { validator: program.STRING }).hide().action(async ({ logger, args, options }) => {
4166
4203
  warnUnknownOptions(options, logger), await gateAuth(options.authToken, options.org, logger), logger.level = options.debug || options.verbose ? "debug" : "info", await ensureHashReady();
4167
4204
  let input = args.input.toString();
4168
- existsSync14(input) || (logger.error(`Input does not exist: ${input}`), process.exit(ERROR_CODES.INVALID_ARGS));
4205
+ existsSync15(input) || (logger.error(`Input does not exist: ${input}`), process.exit(ERROR_CODES.INVALID_ARGS));
4169
4206
  let inputFiles = [], inputIsDirectory = isDirectory(input);
4170
4207
  if (inputIsDirectory ? collectPmremInputFiles(input, inputFiles) : isPmremInputFile(input) ? inputFiles.push(input) : (logger.error(`Input is not an .exr/.hdr file or directory: ${input}`), process.exit(ERROR_CODES.INVALID_ARGS)), inputFiles.length === 0) {
4171
4208
  logger.warn(`No .exr/.hdr files found in ${input}`);
@@ -4204,7 +4241,7 @@ function collectPmremInputFiles(dir, out) {
4204
4241
  for (let entry of readdirSync3(dir)) {
4205
4242
  if (entry === "node_modules" || entry.startsWith(".")) continue;
4206
4243
  let full = path7.join(dir, entry);
4207
- statSync7(full).isDirectory() ? collectPmremInputFiles(full, out) : isPmremInputFile(entry) && out.push(full);
4244
+ statSync8(full).isDirectory() ? collectPmremInputFiles(full, out) : isPmremInputFile(entry) && out.push(full);
4208
4245
  }
4209
4246
  }
4210
4247
  function resolvePmremOutputPath(srcPath, input, inputIsDirectory, outputArg) {
@@ -4215,7 +4252,7 @@ function resolvePmremOutputPath(srcPath, input, inputIsDirectory, outputArg) {
4215
4252
  let rel = path7.relative(input, srcPath), relDir = dirname8(rel), outDir = relDir === "." ? outputArg : path7.join(outputArg, relDir);
4216
4253
  return ensureIsDirectory(outDir), path7.join(outDir, outName);
4217
4254
  }
4218
- return existsSync14(outputArg) && isDirectory(outputArg) ? path7.join(outputArg, outName) : outputArg.toLowerCase().endsWith(".ktx2") ? (ensureIsDirectory(dirname8(outputArg)), outputArg) : (ensureIsDirectory(outputArg), path7.join(outputArg, outName));
4255
+ return existsSync15(outputArg) && isDirectory(outputArg) ? path7.join(outputArg, outName) : outputArg.toLowerCase().endsWith(".ktx2") ? (ensureIsDirectory(dirname8(outputArg)), outputArg) : (ensureIsDirectory(outputArg), path7.join(outputArg, outName));
4219
4256
  }
4220
4257
  program.run().catch((err) => {
4221
4258
  if (!(err.message.includes("Unknown command") && !err.message.includes("Did you mean")))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-build-pipeline",
3
- "version": "3.0.0-next.7e6c33d",
3
+ "version": "3.0.0",
4
4
  "description": "Pipeline and tools for optimizing gltf files using gltf-transform and compression settings within glTF extensions",
5
5
  "type": "module",
6
6
  "exports": {