@helix3/helix-cli 0.1.18-helix3.367 → 0.1.18-helix3.368

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.
@@ -0,0 +1,27 @@
1
+ import { type ArchiveLimits } from '../sceneImport/archive';
2
+ /**
3
+ * Caps for a mod archive downloaded from a repository. Entry count matches the BeamNG importer's own
4
+ * 50,000-file source ceiling; the byte ceilings are per entry and in total, and the compression-ratio
5
+ * guard from the shared archive reader still applies.
6
+ */
7
+ export declare const ZIP_SOURCE_LIMITS: ArchiveLimits;
8
+ export type PreparedBridgeSource = {
9
+ /** Directory (or the original file) the Bridge should read. */
10
+ path: string;
11
+ /** Set when `path` is a temporary extraction of this ZIP. */
12
+ extractedFrom?: {
13
+ archive: string;
14
+ sha256: string;
15
+ entries: number;
16
+ uncompressedBytes: number;
17
+ };
18
+ cleanup(): Promise<void>;
19
+ };
20
+ export declare function isZipSourcePath(source: string): boolean;
21
+ /**
22
+ * Safely extract a `.zip` source to a private temporary directory. The archive is rejected as a whole
23
+ * if any entry is absolute, climbs out with `..`, is duplicated, exceeds the size caps, or looks like a
24
+ * decompression bomb; members are written with exclusive-create into an empty real directory that is
25
+ * removed by `cleanup()` (and on process exit). A non-zip source is returned unchanged.
26
+ */
27
+ export declare function prepareBridgeSource(source: string, onProgress?: (message: string) => void, limits?: ArchiveLimits): Promise<PreparedBridgeSource>;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ZIP_SOURCE_LIMITS = void 0;
4
+ exports.isZipSourcePath = isZipSourcePath;
5
+ exports.prepareBridgeSource = prepareBridgeSource;
6
+ const node_fs_1 = require("node:fs");
7
+ const promises_1 = require("node:fs/promises");
8
+ const node_os_1 = require("node:os");
9
+ const node_path_1 = require("node:path");
10
+ const archive_1 = require("../sceneImport/archive");
11
+ /**
12
+ * Caps for a mod archive downloaded from a repository. Entry count matches the BeamNG importer's own
13
+ * 50,000-file source ceiling; the byte ceilings are per entry and in total, and the compression-ratio
14
+ * guard from the shared archive reader still applies.
15
+ */
16
+ exports.ZIP_SOURCE_LIMITS = {
17
+ maxEntries: 50_000,
18
+ maxEntryBytes: 1_073_741_824,
19
+ maxTotalBytes: 8 * 1_073_741_824,
20
+ maxCompressionRatio: 1_000,
21
+ };
22
+ /** Archive bookkeeping that is never part of a mod's content. */
23
+ const JUNK = /(?:^|\/)(?:__MACOSX|\.DS_Store|Thumbs\.db)(?:\/|$)/;
24
+ function isZipSourcePath(source) {
25
+ const path = (0, node_path_1.resolve)(source);
26
+ if ((0, node_path_1.extname)(path).toLowerCase() !== '.zip')
27
+ return false;
28
+ try {
29
+ return (0, node_fs_1.statSync)(path).isFile();
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ /** A mod zipped with one wrapper folder (`MyMod/vehicles/...`) is read from inside that folder. */
36
+ function unwrapSingleFolder(root) {
37
+ const entries = (0, node_fs_1.readdirSync)(root, { withFileTypes: true });
38
+ if (entries.some((entry) => entry.isDirectory() && entry.name === 'vehicles'))
39
+ return root;
40
+ const folders = entries.filter((entry) => entry.isDirectory());
41
+ if (folders.length === 1 && entries.length === 1) {
42
+ return unwrapSingleFolder((0, node_path_1.join)(root, folders[0].name));
43
+ }
44
+ return root;
45
+ }
46
+ /**
47
+ * Safely extract a `.zip` source to a private temporary directory. The archive is rejected as a whole
48
+ * if any entry is absolute, climbs out with `..`, is duplicated, exceeds the size caps, or looks like a
49
+ * decompression bomb; members are written with exclusive-create into an empty real directory that is
50
+ * removed by `cleanup()` (and on process exit). A non-zip source is returned unchanged.
51
+ */
52
+ async function prepareBridgeSource(source, onProgress, limits = exports.ZIP_SOURCE_LIMITS) {
53
+ if (!isZipSourcePath(source))
54
+ return { path: source, cleanup: async () => { } };
55
+ const archive = (0, node_path_1.resolve)(source);
56
+ const sha256 = await (0, archive_1.sha256File)(archive);
57
+ const inventory = await (0, archive_1.inventorySourceArchive)(archive, sha256, limits);
58
+ const members = inventory.entries
59
+ .filter((entry) => !entry.directory && !JUNK.test(entry.path))
60
+ .map((entry) => entry.path);
61
+ if (members.length === 0)
62
+ throw new Error(`Bridge source archive is empty: ${archive}`);
63
+ const scratch = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'helix-zip-source-'));
64
+ const exitCleanup = () => { try {
65
+ (0, node_fs_1.rmSync)(scratch, { recursive: true, force: true });
66
+ }
67
+ catch { /* best effort */ } };
68
+ process.once('exit', exitCleanup);
69
+ const cleanup = async () => {
70
+ process.removeListener('exit', exitCleanup);
71
+ await (0, promises_1.rm)(scratch, { recursive: true, force: true });
72
+ };
73
+ try {
74
+ const output = (0, node_path_1.join)(scratch, 'source');
75
+ onProgress?.(`extracting ${members.length} file(s) from ${archive} (${inventory.uncompressedBytes} bytes)`);
76
+ await (0, archive_1.extractArchiveMembers)({ archivePath: archive, expectedSha256: sha256, members, outputDir: output, limits });
77
+ return {
78
+ path: unwrapSingleFolder(output),
79
+ extractedFrom: {
80
+ archive, sha256, entries: members.length, uncompressedBytes: inventory.uncompressedBytes,
81
+ },
82
+ cleanup,
83
+ };
84
+ }
85
+ catch (error) {
86
+ await cleanup();
87
+ throw error;
88
+ }
89
+ }
90
+ //# sourceMappingURL=zipSource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zipSource.js","sourceRoot":"","sources":["../../src/bridge/zipSource.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -142,6 +142,20 @@ function bridgeInputs(values) {
142
142
  }
143
143
  return inputs;
144
144
  }
145
+ /**
146
+ * Bridge sources may be a `.zip` as downloaded (a BeamNG mod from the repository, say): it is extracted
147
+ * safely to a temporary directory for the duration of the command and removed afterwards.
148
+ */
149
+ async function withBridgeSource(source, run) {
150
+ const { prepareBridgeSource } = await Promise.resolve().then(() => __importStar(require('./bridge/zipSource')));
151
+ const prepared = await prepareBridgeSource(source, (message) => console.error(` ${message}`));
152
+ try {
153
+ return await run(prepared.path);
154
+ }
155
+ finally {
156
+ await prepared.cleanup();
157
+ }
158
+ }
145
159
  function bridgeTerminalText(value) {
146
160
  return value.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/gu, (character) => {
147
161
  if (character === '\n')
@@ -178,12 +192,12 @@ const bridge = program
178
192
  bridge
179
193
  .command('detect')
180
194
  .description('Fingerprint a source and identify its engine, format, and likely content domain')
181
- .argument('<source>', 'file or project directory')
195
+ .argument('<source>', 'file, project directory, or a .zip of one (extracted safely to a temp dir)')
182
196
  .option('--json', 'print the machine-readable detection record')
183
197
  .action(async (source, opts) => {
184
198
  try {
185
199
  const { detectBridgeSource } = await Promise.resolve().then(() => __importStar(require('./bridge')));
186
- const detection = await detectBridgeSource(source);
200
+ const detection = await withBridgeSource(source, (resolved) => detectBridgeSource(resolved));
187
201
  if (opts.json)
188
202
  console.log(JSON.stringify(detection, null, 2));
189
203
  else {
@@ -201,13 +215,13 @@ bridge
201
215
  bridge
202
216
  .command('inspect')
203
217
  .description('Inspect a source and show its detected structure plus viable import goals')
204
- .argument('<source>', 'file or project directory')
218
+ .argument('<source>', 'file, project directory, or a .zip of one (extracted safely to a temp dir)')
205
219
  .option('--plugin-dir <paths...>', 'also discover local plugins from these directories')
206
220
  .option('--json', 'print machine-readable inspection and plans')
207
221
  .action(async (source, opts) => {
208
222
  try {
209
223
  const { buildBridgePlan, detectBridgeSource, loadBridgePlugins } = await Promise.resolve().then(() => __importStar(require('./bridge')));
210
- const detection = await detectBridgeSource(source);
224
+ const detection = await withBridgeSource(source, (resolved) => detectBridgeSource(resolved));
211
225
  const plugins = loadBridgePlugins(opts.pluginDir);
212
226
  const plans = ['asset', 'character', 'addon', 'scene', 'animation']
213
227
  .map((type) => buildBridgePlan(detection, { type, target: 'web' }, plugins))
@@ -229,7 +243,7 @@ bridge
229
243
  bridge
230
244
  .command('plan')
231
245
  .description('Resolve installed plugin capabilities into a deterministic conversion graph')
232
- .argument('<source>', 'file or project directory')
246
+ .argument('<source>', 'file, project directory, or a .zip of one (e.g. a BeamNG mod as downloaded; extracted safely to a temp dir)')
233
247
  .option('--type <type>', 'auto, asset, character, addon, scene, animation, or extract', 'auto')
234
248
  .option('--target <target>', 'web, unreal, mobile, or canonical', 'web')
235
249
  .option('--plugin <ids...>', 'restrict non-built-in plugins to these installed ids')
@@ -238,12 +252,12 @@ bridge
238
252
  .option('--eye-texture <image>', 'MetaHuman baked eye texture override (default: packaged canonical bake from PR #99)')
239
253
  .option('--canonical <glb>', 'canonical helix-humanoid body override')
240
254
  .option('--map <json>', 'custom skeleton/bone map')
241
- .option('--input <name=path...>', 'immutable named plugin input; repeat for multiple inputs')
255
+ .option('--input <name=path...>', 'immutable named plugin input; repeat for multiple inputs (BeamNG: palette=<palette.json>, beamngCommon=<index.json>, beamngEngine=<engine.json> with {"cylinders": N} to override the derived cylinder count)')
242
256
  .option('--json', 'print the machine-readable plan')
243
257
  .action(async (source, opts) => {
244
258
  try {
245
259
  const { resolveBridgePlan } = await Promise.resolve().then(() => __importStar(require('./bridge')));
246
- const plan = await resolveBridgePlan(source, {
260
+ const plan = await withBridgeSource(source, (resolved) => resolveBridgePlan(resolved, {
247
261
  type: bridgeType(opts.type),
248
262
  target: bridgeTarget(opts.target),
249
263
  plugin: opts.plugin,
@@ -253,7 +267,7 @@ bridge
253
267
  canonical: opts.canonical,
254
268
  map: opts.map,
255
269
  inputs: bridgeInputs(opts.input),
256
- });
270
+ }));
257
271
  console.log(opts.json ? JSON.stringify(plan, null, 2) : renderBridgePlan(plan));
258
272
  if (plan.blockers.length > 0)
259
273
  process.exitCode = 2;
@@ -265,7 +279,7 @@ bridge
265
279
  bridge
266
280
  .command('import')
267
281
  .description('Detect, plan, convert, validate, package, and write deterministic provenance')
268
- .argument('<source>', 'file or project directory')
282
+ .argument('<source>', 'file, project directory, or a .zip of one (e.g. a BeamNG mod as downloaded; extracted safely to a temp dir)')
269
283
  .option('--type <type>', 'auto, asset, character, addon, scene, animation, or extract', 'auto')
270
284
  .option('--target <target>', 'web, unreal, mobile, or canonical', 'web')
271
285
  .option('-o, --output <directory>', 'empty output directory', './bridge-out')
@@ -275,7 +289,7 @@ bridge
275
289
  .option('--eye-texture <image>', 'MetaHuman baked eye texture override (default: packaged canonical bake from PR #99)')
276
290
  .option('--canonical <glb>', 'canonical helix-humanoid body override')
277
291
  .option('--map <json>', 'custom skeleton/bone map')
278
- .option('--input <name=path...>', 'immutable named plugin input; repeat for multiple inputs')
292
+ .option('--input <name=path...>', 'immutable named plugin input; repeat for multiple inputs (BeamNG: palette=<palette.json>, beamngCommon=<index.json>, beamngEngine=<engine.json> with {"cylinders": N} to override the derived cylinder count)')
279
293
  .option('--expect-plan-key <sha256>', 'execute only if the freshly resolved plan has this exact key')
280
294
  .option('--allow-permission <permissions...>', 'permissions approved for community/local plugins; repeat or comma-separate')
281
295
  .option('--dry-run', 'plan only; write and execute nothing')
@@ -285,7 +299,7 @@ bridge
285
299
  .action(async (source, opts) => {
286
300
  try {
287
301
  const { importWithBridge } = await Promise.resolve().then(() => __importStar(require('./bridge')));
288
- const result = await importWithBridge(source, {
302
+ const result = await withBridgeSource(source, (resolved) => importWithBridge(resolved, {
289
303
  type: bridgeType(opts.type),
290
304
  target: bridgeTarget(opts.target),
291
305
  output: opts.output,
@@ -302,7 +316,7 @@ bridge
302
316
  keepStage: opts.keepStage,
303
317
  allowUntexturedBodyGeometry: opts.allowUntexturedBodyGeometry,
304
318
  onProgress: (message) => console.error(` ${message}`),
305
- });
319
+ }));
306
320
  if (opts.json)
307
321
  console.log(JSON.stringify(result, null, 2));
308
322
  else if (opts.dryRun)