@needle-tools/gltf-build-pipeline 3.0.0-beta.1 → 3.0.0-beta.3

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,19 @@ 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-beta.3] - 2026-06-10
8
+
9
+ ### Added
10
+ - Pipeline errors (including compression failures and unhandled exceptions) are now written to the log file to aid diagnosis.
11
+
12
+ ## [3.0.0-beta.2] - 2026-06-10
13
+
14
+ ### Added
15
+ - Authentication failures are now written to a log file (with secrets redacted) to make auth issues easier to diagnose.
16
+
17
+ ### Changed
18
+ - The "Auth failed" message now includes the pipeline version.
19
+
7
20
  ## [3.0.0-beta.1] - 2026-06-10
8
21
 
9
22
  ### Fixed
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
 
@@ -1194,7 +1194,7 @@ function registerExtensions(node) {
1194
1194
  import { createTransform } from "@gltf-transform/functions";
1195
1195
 
1196
1196
  // src/utils/version.gen.ts
1197
- var version = "3.0.0-beta.1";
1197
+ var version = "3.0.0-beta.3";
1198
1198
 
1199
1199
  // src/utils/version.ts
1200
1200
  function getVersion(_marklocal = !0) {
@@ -3607,12 +3607,35 @@ function computeOpusOggUri(sourceUri) {
3607
3607
  return (dir === "." || dir === "" ? newName : `${dir}/${newName}`).replaceAll("\\", "/");
3608
3608
  }
3609
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
+ }
3629
+ }
3630
+
3610
3631
  // src/scripts/pack-gltf.ts
3611
3632
  async function packGLTF(inputFile, outputFile, options) {
3612
3633
  options.logger || (options.logger = new Logger(Verbosity2.INFO));
3613
3634
  let { debug, stats, packedFiles, useCache, logger } = options;
3614
- if (!existsSync11(inputFile))
3615
- 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
+ }
3616
3639
  let cacheKey;
3617
3640
  if (useCache && isMeshLOD(inputFile)) {
3618
3641
  let existing = readFileSync8(inputFile);
@@ -3628,7 +3651,7 @@ async function packGLTF(inputFile, outputFile, options) {
3628
3651
  try {
3629
3652
  let config = options.config;
3630
3653
  outputFile ? outputFile = getOutputPath(inputFile, outputFile) : outputFile = inputFile;
3631
- let prevSizeInMB = statSync5(inputFile).size / 1024 / 1024;
3654
+ let prevSizeInMB = statSync6(inputFile).size / 1024 / 1024;
3632
3655
  logger.debug(`\u2192 Compressing ${prevSizeInMB.toFixed(1)} MB: ${inputFile}`);
3633
3656
  let startTime = Date.now(), io = new NodeIO2().registerExtensions(ALL_EXTENSIONS3).registerExtensions(ALL_EXTENSIONS).registerExtensions([
3634
3657
  NEEDLE_compression_texture,
@@ -3690,32 +3713,32 @@ async function packGLTF(inputFile, outputFile, options) {
3690
3713
  let uri = tex.getURI();
3691
3714
  if (uri && !uri.startsWith("data:")) {
3692
3715
  let absPath = path5.resolve(outDir, uri);
3693
- existsSync11(absPath) && options.externalFiles.push(absPath);
3716
+ existsSync12(absPath) && options.externalFiles.push(absPath);
3694
3717
  }
3695
3718
  }
3696
3719
  for (let buf of root.listBuffers()) {
3697
3720
  let uri = buf.getURI();
3698
3721
  if (uri && !uri.startsWith("data:")) {
3699
3722
  let absPath = path5.resolve(outDir, uri);
3700
- existsSync11(absPath) && options.externalFiles.push(absPath);
3723
+ existsSync12(absPath) && options.externalFiles.push(absPath);
3701
3724
  }
3702
3725
  }
3703
3726
  for (let ref of listAudioRefs()) {
3704
3727
  let uri = ref.newUri ?? ref.sourceUri, absPath = path5.resolve(outDir, uri);
3705
- existsSync11(absPath) && options.externalFiles.push(absPath);
3728
+ existsSync12(absPath) && options.externalFiles.push(absPath);
3706
3729
  }
3707
3730
  }
3708
- logger.debug(`\u2190 Writing to ${outputFile}`), await writeNodeIO(io, outputFile, document), useCache && cacheKey != null && existsSync11(outputFile) && addToCache(cacheKey, readFileSync8(outputFile));
3709
- 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;
3710
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;
3711
3734
  } catch (err) {
3712
3735
  if (err instanceof TestAssertionError) throw err;
3713
3736
  let shortedStackTrace = err instanceof Error && err.stack ? err.stack.split(`
3714
3737
  `).slice(0, 2).join(`
3715
- `) : "";
3716
- 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:
3717
3739
  ${err instanceof Error ? err.message : String(err)}
3718
- ${shortedStackTrace}`), debug) throw err;
3740
+ ${shortedStackTrace}`;
3741
+ if (logger.error(message), logToFile(message), debug) throw err;
3719
3742
  return !1;
3720
3743
  }
3721
3744
  }
@@ -3737,7 +3760,7 @@ async function calculateStats(inputfile, options) {
3737
3760
 
3738
3761
  // src/config/index.ts
3739
3762
  import { Logger as Logger3, Verbosity as Verbosity4 } from "@gltf-transform/core";
3740
- 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";
3741
3764
  import path6, { dirname as dirname7, resolve as resolve2 } from "path";
3742
3765
  var UsecaseOptions = ["product", "world"], defaultConfig = {
3743
3766
  usecase: "default"
@@ -3752,9 +3775,9 @@ function createConfig(usecase) {
3752
3775
  function getConfig(configPath, fallbackSearchDirectory, opts) {
3753
3776
  let logger = opts?.logger;
3754
3777
  if (logger || (logger = new Logger3(Verbosity4.WARN)), configPath != null) {
3755
- if (configPath = resolve2(process.cwd(), configPath), !existsSync12(configPath))
3778
+ if (configPath = resolve2(process.cwd(), configPath), !existsSync13(configPath))
3756
3779
  throw new Error(`No config found at "${configPath}"`);
3757
- if (statSync6(configPath).isDirectory()) {
3780
+ if (statSync7(configPath).isDirectory()) {
3758
3781
  let res2 = searchConfig(configPath, logger);
3759
3782
  if (res2)
3760
3783
  return res2;
@@ -3775,9 +3798,9 @@ function getConfig(configPath, fallbackSearchDirectory, opts) {
3775
3798
  }
3776
3799
  var _cachedConfig;
3777
3800
  function searchConfig(directory, logger) {
3778
- 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; ) {
3779
3802
  let configPath = path6.join(directory, "needle.config.json");
3780
- if (existsSync12(configPath)) {
3803
+ if (existsSync13(configPath)) {
3781
3804
  let content = readFileSync9(configPath, "utf8"), config = resolveConfig(configPath, content, logger);
3782
3805
  if (config)
3783
3806
  return config;
@@ -3809,7 +3832,7 @@ function resolveConfig(filename, content, logger) {
3809
3832
  }
3810
3833
 
3811
3834
  // src/cli/index.ts
3812
- 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";
3813
3836
  import path7, { basename as basename4, dirname as dirname8, resolve as resolve3 } from "path";
3814
3837
 
3815
3838
  // src/constants.ts
@@ -3875,6 +3898,13 @@ function trackError(error, context) {
3875
3898
  return;
3876
3899
  }
3877
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}` : ""}`);
3878
3908
  let properties = {
3879
3909
  version: getVersion(),
3880
3910
  error_name: errorName + (cached && cached.count > 1 ? ` x${cached.count}` : ""),
@@ -3926,9 +3956,9 @@ var PROD_KEYS = [
3926
3956
  var trusted = [...PROD_KEYS], TRUSTED_KEYS = trusted;
3927
3957
 
3928
3958
  // src/auth/verify.ts
3929
- import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
3930
- import { join as join8 } from "path";
3931
- 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";
3932
3962
  var AuthError = class extends Error {
3933
3963
  constructor(message) {
3934
3964
  super(message), this.name = "AuthError";
@@ -3936,29 +3966,32 @@ var AuthError = class extends Error {
3936
3966
  }, LOCAL_FETCH_TIMEOUT_MS = 2e3;
3937
3967
  function getLocalServerToken() {
3938
3968
  try {
3939
- let file = join8(homedir(), ".needle", "local-server-token");
3940
- if (!existsSync13(file)) return null;
3969
+ let file = join9(homedir2(), ".needle", "local-server-token");
3970
+ if (!existsSync14(file)) return null;
3941
3971
  let data = JSON.parse(readFileSync10(file, "utf-8"));
3942
3972
  return typeof data.token == "string" ? data.token : null;
3943
- } catch {
3944
- return null;
3973
+ } catch (err) {
3974
+ return logToFile(`[auth] failed reading local-server-token (${err instanceof Error ? err.name : "unknown error"})`), null;
3945
3975
  }
3946
3976
  }
3947
3977
  async function tryFetchLocalToken(org) {
3978
+ let baseUrl = process.env.NEEDLE_LICENSE_SERVER_URL || "http://localhost:8424/api/license", safeServer = "local license server";
3948
3979
  try {
3949
- let baseUrl = process.env.NEEDLE_LICENSE_SERVER_URL || "http://localhost:8424/api/license", url = new URL(baseUrl);
3950
- org && url.searchParams.set("org", org);
3980
+ let url = new URL(baseUrl);
3981
+ org && url.searchParams.set("org", org), safeServer = `${url.origin}${url.pathname}`;
3951
3982
  let headers = {}, serverToken = getLocalServerToken();
3952
3983
  serverToken && (headers.Authorization = `Bearer ${serverToken}`);
3953
3984
  let res = await fetch(url, {
3954
3985
  signal: AbortSignal.timeout(LOCAL_FETCH_TIMEOUT_MS),
3955
3986
  headers
3956
3987
  });
3957
- 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;
3958
3990
  let jwt = (await res.json()).needle_license_jwt;
3959
- return typeof jwt == "string" && jwt.length > 0 ? jwt : null;
3960
- } catch {
3961
- 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;
3962
3995
  }
3963
3996
  }
3964
3997
  async function verifyWithKey(token, key) {
@@ -4023,7 +4056,7 @@ async function gateAuth(authToken, org, logger) {
4023
4056
  await requireAuth(authToken, typeof org == "string" ? org : void 0);
4024
4057
  } catch (err) {
4025
4058
  let message = err instanceof AuthError || err instanceof Error ? err.message : String(err);
4026
- 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);
4027
4060
  }
4028
4061
  }
4029
4062
  program.cast(!1);
@@ -4077,7 +4110,7 @@ When caching is enabled then previously processed files will be detected and loa
4077
4110
  }
4078
4111
  let set = /* @__PURE__ */ new Set(), useCache = options.cache !== !1, debug = options.debug === !0, verbose = options.verbose === !0;
4079
4112
  await foreachGLTF(inputPaths, async (file) => {
4080
- if (existsSync14(file)) {
4113
+ if (existsSync15(file)) {
4081
4114
  if (file != input && options.progressive === !0 && isLOD(file)) {
4082
4115
  logger.debug(`\u2192 Ignore existing LOD file at ${file}`);
4083
4116
  return;
@@ -4093,12 +4126,12 @@ When caching is enabled then previously processed files will be detected and loa
4093
4126
  config
4094
4127
  }, file) : void 0;
4095
4128
  if (jobKey) {
4096
- 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);
4097
4130
  if (restored) {
4098
- let inputFileSize = statSync7(file).size / 1024 / 1024;
4131
+ let inputFileSize = statSync8(file).size / 1024 / 1024;
4099
4132
  for (let f of restored)
4100
4133
  if (set.add(f), stats) {
4101
- let sizeInMB = statSync7(f).size / 1024 / 1024;
4134
+ let sizeInMB = statSync8(f).size / 1024 / 1024;
4102
4135
  stats.totalFilesProcessed++, stats.totalFileSizeInMB += sizeInMB, stats.totalFileSizeInMBBefore += inputFileSize;
4103
4136
  }
4104
4137
  logger.info(`\u2192 [CACHE] Restored ${restored.length} file(s) for job ${jobKey}`);
@@ -4169,7 +4202,7 @@ raw .exr / .hdr inputs.
4169
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 }) => {
4170
4203
  warnUnknownOptions(options, logger), await gateAuth(options.authToken, options.org, logger), logger.level = options.debug || options.verbose ? "debug" : "info", await ensureHashReady();
4171
4204
  let input = args.input.toString();
4172
- 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));
4173
4206
  let inputFiles = [], inputIsDirectory = isDirectory(input);
4174
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) {
4175
4208
  logger.warn(`No .exr/.hdr files found in ${input}`);
@@ -4208,7 +4241,7 @@ function collectPmremInputFiles(dir, out) {
4208
4241
  for (let entry of readdirSync3(dir)) {
4209
4242
  if (entry === "node_modules" || entry.startsWith(".")) continue;
4210
4243
  let full = path7.join(dir, entry);
4211
- statSync7(full).isDirectory() ? collectPmremInputFiles(full, out) : isPmremInputFile(entry) && out.push(full);
4244
+ statSync8(full).isDirectory() ? collectPmremInputFiles(full, out) : isPmremInputFile(entry) && out.push(full);
4212
4245
  }
4213
4246
  }
4214
4247
  function resolvePmremOutputPath(srcPath, input, inputIsDirectory, outputArg) {
@@ -4219,7 +4252,7 @@ function resolvePmremOutputPath(srcPath, input, inputIsDirectory, outputArg) {
4219
4252
  let rel = path7.relative(input, srcPath), relDir = dirname8(rel), outDir = relDir === "." ? outputArg : path7.join(outputArg, relDir);
4220
4253
  return ensureIsDirectory(outDir), path7.join(outDir, outName);
4221
4254
  }
4222
- 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));
4223
4256
  }
4224
4257
  program.run().catch((err) => {
4225
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-beta.1",
3
+ "version": "3.0.0-beta.3",
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": {