@aura3d/cli 1.3.3 → 1.4.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/dist/asset-constants.d.ts +5 -0
- package/dist/asset-constants.d.ts.map +1 -0
- package/dist/asset-constants.js +5 -0
- package/dist/asset-constants.js.map +1 -0
- package/dist/asset-core-types.d.ts +206 -0
- package/dist/asset-core-types.d.ts.map +1 -0
- package/dist/asset-core-types.js +2 -0
- package/dist/asset-core-types.js.map +1 -0
- package/dist/asset-inspection-types.d.ts +131 -0
- package/dist/asset-inspection-types.d.ts.map +1 -0
- package/dist/asset-inspection-types.js +2 -0
- package/dist/asset-inspection-types.js.map +1 -0
- package/dist/asset-manifest.d.ts +8 -0
- package/dist/asset-manifest.d.ts.map +1 -0
- package/dist/asset-manifest.js +116 -0
- package/dist/asset-manifest.js.map +1 -0
- package/dist/asset-readiness-types.d.ts +164 -0
- package/dist/asset-readiness-types.d.ts.map +1 -0
- package/dist/asset-readiness-types.js +2 -0
- package/dist/asset-readiness-types.js.map +1 -0
- package/dist/asset-source-ast.d.ts +8 -0
- package/dist/asset-source-ast.d.ts.map +1 -0
- package/dist/asset-source-ast.js +241 -0
- package/dist/asset-source-ast.js.map +1 -0
- package/dist/asset-source-roles.d.ts +4 -0
- package/dist/asset-source-roles.d.ts.map +1 -0
- package/dist/asset-source-roles.js +79 -0
- package/dist/asset-source-roles.js.map +1 -0
- package/dist/asset-source-validation.d.ts +4 -0
- package/dist/asset-source-validation.d.ts.map +1 -0
- package/dist/asset-source-validation.js +200 -0
- package/dist/asset-source-validation.js.map +1 -0
- package/dist/character-assembly-types.d.ts +39 -0
- package/dist/character-assembly-types.d.ts.map +1 -0
- package/dist/character-assembly-types.js +2 -0
- package/dist/character-assembly-types.js.map +1 -0
- package/dist/cli-help.d.ts +4 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +48 -0
- package/dist/cli-help.js.map +1 -0
- package/dist/cli-options.d.ts +66 -0
- package/dist/cli-options.d.ts.map +1 -0
- package/dist/cli-options.js +234 -0
- package/dist/cli-options.js.map +1 -0
- package/dist/cli.js +20 -177
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +12 -402
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +948 -90
- package/dist/index.js.map +1 -1
- package/dist/pull-bridge/adapters.d.ts +4 -0
- package/dist/pull-bridge/adapters.d.ts.map +1 -0
- package/dist/pull-bridge/adapters.js +38 -0
- package/dist/pull-bridge/adapters.js.map +1 -0
- package/dist/pull-bridge/download.d.ts +6 -0
- package/dist/pull-bridge/download.d.ts.map +1 -0
- package/dist/pull-bridge/download.js +208 -0
- package/dist/pull-bridge/download.js.map +1 -0
- package/dist/pull-bridge/profiles.d.ts +14 -0
- package/dist/pull-bridge/profiles.d.ts.map +1 -0
- package/dist/pull-bridge/profiles.js +63 -0
- package/dist/pull-bridge/profiles.js.map +1 -0
- package/dist/pull-bridge/provenance.d.ts +9 -0
- package/dist/pull-bridge/provenance.d.ts.map +1 -0
- package/dist/pull-bridge/provenance.js +99 -0
- package/dist/pull-bridge/provenance.js.map +1 -0
- package/dist/pull-bridge/pullable.d.ts +14 -0
- package/dist/pull-bridge/pullable.d.ts.map +1 -0
- package/dist/pull-bridge/pullable.js +58 -0
- package/dist/pull-bridge/pullable.js.map +1 -0
- package/dist/pull-bridge/scoring.d.ts +21 -0
- package/dist/pull-bridge/scoring.d.ts.map +1 -0
- package/dist/pull-bridge/scoring.js +177 -0
- package/dist/pull-bridge/scoring.js.map +1 -0
- package/dist/pull-bridge/search.d.ts +39 -0
- package/dist/pull-bridge/search.d.ts.map +1 -0
- package/dist/pull-bridge/search.js +114 -0
- package/dist/pull-bridge/search.js.map +1 -0
- package/dist/pull-bridge/types.d.ts +9 -0
- package/dist/pull-bridge/types.d.ts.map +1 -0
- package/dist/pull-bridge/types.js +2 -0
- package/dist/pull-bridge/types.js.map +1 -0
- package/dist/pull-bridge/vector3.d.ts +2 -0
- package/dist/pull-bridge/vector3.d.ts.map +1 -0
- package/dist/pull-bridge/vector3.js +4 -0
- package/dist/pull-bridge/vector3.js.map +1 -0
- package/dist/pull-bridge.d.ts +18 -100
- package/dist/pull-bridge.d.ts.map +1 -1
- package/dist/pull-bridge.js +59 -526
- package/dist/pull-bridge.js.map +1 -1
- package/package.json +3 -2
package/dist/pull-bridge.js
CHANGED
|
@@ -8,500 +8,30 @@
|
|
|
8
8
|
* Two surfaces:
|
|
9
9
|
* - `assets search <query>` — rank + print candidates, label what is/ isn't
|
|
10
10
|
* auto-pullable, and surface marketplace deep-links when nothing is.
|
|
11
|
-
* - `assets resolve <query> --name <name>` —
|
|
12
|
-
*
|
|
11
|
+
* - `assets resolve <query> --name <name>` — rank AUTO-PULLABLE candidates,
|
|
12
|
+
* download each in order, inspect post-download, and run the normal
|
|
13
|
+
* `addAsset` flow only for a candidate-level match.
|
|
13
14
|
*
|
|
14
15
|
* License safety mirrors the index: we NEVER auto-pull an UNVERIFIED or
|
|
15
16
|
* deep-link-only asset. `selectPullable` is the single decision seam and is
|
|
16
17
|
* deliberately pure so it can be unit-tested without a network or the CLI.
|
|
17
18
|
*/
|
|
18
19
|
import { createHash } from "node:crypto";
|
|
19
|
-
import {
|
|
20
|
+
import { mkdtempSync, readFileSync } from "node:fs";
|
|
20
21
|
import { tmpdir } from "node:os";
|
|
21
|
-
import {
|
|
22
|
-
import { inflateRawSync } from "node:zlib";
|
|
22
|
+
import { join } from "node:path";
|
|
23
23
|
import * as assetIndex from "@aura3d/asset-index";
|
|
24
|
-
import { addAsset } from "./index.js";
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Assemble the live source adapters for the CLI:
|
|
37
|
-
* - the zero-auth verified defaults (Khronos, OS3A, + Poly Haven when present),
|
|
38
|
-
* - key-gated sources only when their API key env var is set,
|
|
39
|
-
* Each optional factory degrades to "absent" rather than throwing.
|
|
40
|
-
*/
|
|
41
|
-
export function buildSearchAdapters(env = process.env) {
|
|
42
|
-
const starterPack = optionalFactory("createAnimationStarterPackAdapter");
|
|
43
|
-
const starterAdapters = starterPack ? [starterPack()] : [];
|
|
44
|
-
// Primary: the hosted ~850k Aura3D catalog via its `/search` endpoint (hybrid
|
|
45
|
-
// keyword + semantic + quality ranking). It already aggregates Objaverse,
|
|
46
|
-
// Sketchfab, Poly Pizza, Poly Haven, OS3A, Khronos and the CC0 mirror, so it
|
|
47
|
-
// supersedes per-source live federation. Fall back to live sources only if the
|
|
48
|
-
// hosted-catalog adapter isn't available in this build.
|
|
49
|
-
const auraIndex = optionalFactory("createAuraIndexAdapter");
|
|
50
|
-
if (auraIndex)
|
|
51
|
-
return [...starterAdapters, auraIndex()];
|
|
52
|
-
const adapters = [...starterAdapters, ...defaultAdapters()];
|
|
53
|
-
const polyHaven = optionalFactory("createPolyHavenAdapter");
|
|
54
|
-
if (polyHaven && !adapters.some((a) => a.id === "polyhaven")) {
|
|
55
|
-
adapters.push(polyHaven());
|
|
56
|
-
}
|
|
57
|
-
// Key-gated sources: include only when the key is present; the adapter itself
|
|
58
|
-
// also degrades to [] when the key is missing, but we avoid even constructing
|
|
59
|
-
// it without a key so a no-key environment stays fully offline-deterministic.
|
|
60
|
-
if (env.SKETCHFAB_API_TOKEN || env.SKETCHFAB_TOKEN) {
|
|
61
|
-
const sketchfab = optionalFactory("createSketchfabAdapter");
|
|
62
|
-
if (sketchfab)
|
|
63
|
-
adapters.push(sketchfab());
|
|
64
|
-
}
|
|
65
|
-
if (env.POLY_PIZZA_API_KEY || env.POLYPIZZA_API_KEY) {
|
|
66
|
-
const polyPizza = optionalFactory("createPolyPizzaAdapter");
|
|
67
|
-
if (polyPizza)
|
|
68
|
-
adapters.push(polyPizza());
|
|
69
|
-
}
|
|
70
|
-
return adapters;
|
|
71
|
-
}
|
|
72
|
-
/** Marketplace deep-link adapter, when the concurrently-authored factory exists. */
|
|
73
|
-
export function buildDeepLinkAdapter() {
|
|
74
|
-
const factory = optionalFactory("createMarketplaceDeepLinkAdapter");
|
|
75
|
-
return factory ? factory() : undefined;
|
|
76
|
-
}
|
|
77
|
-
/** Translate CLI flags into the index's ResolveConstraints shape. */
|
|
78
|
-
export function toResolveConstraints(cli, redistributableOnly) {
|
|
79
|
-
const constraints = {};
|
|
80
|
-
if (cli.profile === "fighting-character") {
|
|
81
|
-
constraints.license = cli.license && cli.license.length > 0 ? cli.license : ["CC0", "CC-BY"];
|
|
82
|
-
constraints.animated = true;
|
|
83
|
-
constraints.format = "glb";
|
|
84
|
-
constraints.maxTriangles = cli.maxTriangles ?? 200_000;
|
|
85
|
-
}
|
|
86
|
-
else if (isAnimationCliProfile(cli.profile)) {
|
|
87
|
-
constraints.license = cli.license && cli.license.length > 0 ? cli.license : ["CC0", "CC-BY"];
|
|
88
|
-
constraints.format = "glb";
|
|
89
|
-
constraints.maxTriangles = cli.maxTriangles ?? animationProfileMaxTriangles(cli.profile);
|
|
90
|
-
if (cli.profile === "animation-character")
|
|
91
|
-
constraints.animated = true;
|
|
92
|
-
else if (typeof cli.animated === "boolean")
|
|
93
|
-
constraints.animated = cli.animated;
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
if (cli.license && cli.license.length > 0)
|
|
97
|
-
constraints.license = cli.license;
|
|
98
|
-
if (typeof cli.maxTriangles === "number")
|
|
99
|
-
constraints.maxTriangles = cli.maxTriangles;
|
|
100
|
-
if (typeof cli.animated === "boolean")
|
|
101
|
-
constraints.animated = cli.animated;
|
|
102
|
-
}
|
|
103
|
-
if (redistributableOnly)
|
|
104
|
-
constraints.redistributableOnly = true;
|
|
105
|
-
return constraints;
|
|
106
|
-
}
|
|
107
|
-
export function selectPullable(candidates, options = {}) {
|
|
108
|
-
const profile = options.profile ?? "general";
|
|
109
|
-
const pullable = candidates.find((c) => {
|
|
110
|
-
if (!isAutoPullable(c.asset))
|
|
111
|
-
return false;
|
|
112
|
-
if (profile === "general")
|
|
113
|
-
return true;
|
|
114
|
-
return evaluateAssetProfile(c.asset, profile).suitable;
|
|
115
|
-
});
|
|
116
|
-
if (pullable)
|
|
117
|
-
return { ok: true, candidate: pullable };
|
|
118
|
-
if (candidates.length === 0) {
|
|
119
|
-
return {
|
|
120
|
-
ok: false,
|
|
121
|
-
reason: "No candidates matched the query. Try a broader query or relax constraints.",
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
if (profile !== "general") {
|
|
125
|
-
const rejectedPullable = candidates.filter((c) => isAutoPullable(c.asset));
|
|
126
|
-
if (rejectedPullable.length > 0) {
|
|
127
|
-
return {
|
|
128
|
-
ok: false,
|
|
129
|
-
reason: `No auto-pullable candidate passed the ${profile} profile. ` +
|
|
130
|
-
rejectedPullable.slice(0, 5).map((candidate) => {
|
|
131
|
-
const evaluation = evaluateAssetProfile(candidate.asset, profile);
|
|
132
|
-
const reasons = evaluation.rejectionReasons.length > 0
|
|
133
|
-
? evaluation.rejectionReasons.join("; ")
|
|
134
|
-
: "profile did not report a concrete reason";
|
|
135
|
-
return `"${candidate.asset.title}" (${candidate.asset.id}): ${reasons}`;
|
|
136
|
-
}).join(" | "),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
const top = candidates[0].asset;
|
|
141
|
-
return {
|
|
142
|
-
ok: false,
|
|
143
|
-
reason: `No auto-pullable candidate found. The best match "${top.title}" (${top.id}) ` +
|
|
144
|
-
`is ${describeWhyNotPullable(top)}. Aura will not auto-pull an asset whose ` +
|
|
145
|
-
`license is unverified or that is marketplace deep-link only. ` +
|
|
146
|
-
`Review it at ${top.sourcePage ?? top.url} and add it manually once the ` +
|
|
147
|
-
`license is confirmed.`,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
function describeWhyNotPullable(asset) {
|
|
151
|
-
if (asset.access !== "direct-download")
|
|
152
|
-
return "marketplace deep-link only";
|
|
153
|
-
if (!asset.license.verified)
|
|
154
|
-
return `license UNVERIFIED (raw: "${asset.license.raw}")`;
|
|
155
|
-
if (!asset.license.redistributable)
|
|
156
|
-
return `license "${asset.license.spdx}" is not redistributable`;
|
|
157
|
-
return "not auto-pullable";
|
|
158
|
-
}
|
|
159
|
-
/** First four bytes of a ZIP local-file header ("PK\x03\x04"). */
|
|
160
|
-
const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
|
|
161
|
-
/** glTF binary container magic ("glTF"). */
|
|
162
|
-
const GLB_MAGIC = Buffer.from([0x67, 0x6c, 0x54, 0x46]);
|
|
163
|
-
/**
|
|
164
|
-
* Default downloader backed by global fetch.
|
|
165
|
-
*
|
|
166
|
-
* Objaverse URLs are direct `.glb`; Poly Pizza / Sketchfab artifacts can arrive
|
|
167
|
-
* as a ZIP (gltf + bin + textures, or a packaged .glb). We sniff the leading
|
|
168
|
-
* bytes: a GLB/gltf body is written through as-is; a `PK` body is unpacked next
|
|
169
|
-
* to `destPath` and the assembled .glb/.gltf path is returned. Auth-gated
|
|
170
|
-
* Sketchfab `download` endpoints return JSON (a signed-URL envelope, or a 401);
|
|
171
|
-
* we surface a clear, non-fatal-to-the-tool error instead of writing garbage.
|
|
172
|
-
*/
|
|
173
|
-
export const defaultDownloadFile = async (url, destPath) => {
|
|
174
|
-
const res = await fetch(url, { headers: { accept: "model/gltf-binary,application/zip,*/*" } });
|
|
175
|
-
if (!res.ok) {
|
|
176
|
-
if (res.status === 401 || res.status === 403) {
|
|
177
|
-
throw new Error(`Aura3D resolve failed: ${url} requires authentication (HTTP ${res.status}). ` +
|
|
178
|
-
`This source (e.g. Sketchfab) needs an OAuth download token; set SKETCHFAB_API_TOKEN ` +
|
|
179
|
-
`or pull a direct-download (Objaverse) candidate instead.`);
|
|
180
|
-
}
|
|
181
|
-
throw new Error(`Aura3D resolve failed: download of ${url} returned HTTP ${res.status}.`);
|
|
182
|
-
}
|
|
183
|
-
const buffer = Buffer.from(await res.arrayBuffer());
|
|
184
|
-
if (buffer.length === 0) {
|
|
185
|
-
throw new Error(`Aura3D resolve failed: download of ${url} was empty.`);
|
|
186
|
-
}
|
|
187
|
-
// A Sketchfab download endpoint that 200s but returns a JSON signed-URL
|
|
188
|
-
// envelope rather than model bytes — follow the envelope when we can, else fail
|
|
189
|
-
// clearly. We never feed JSON into the GLB inspector.
|
|
190
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
191
|
-
if (contentType.includes("application/json") || buffer[0] === 0x7b /* '{' */) {
|
|
192
|
-
const followed = await followSignedDownload(buffer, destPath);
|
|
193
|
-
if (followed)
|
|
194
|
-
return followed;
|
|
195
|
-
throw new Error(`Aura3D resolve failed: ${url} returned a JSON envelope, not model bytes ` +
|
|
196
|
-
`(auth-gated download requiring an OAuth token). Pull a direct-download candidate instead.`);
|
|
197
|
-
}
|
|
198
|
-
if (startsWith(buffer, ZIP_MAGIC)) {
|
|
199
|
-
return unpackZipToModel(buffer, destPath);
|
|
200
|
-
}
|
|
201
|
-
writeFileSync(destPath, buffer);
|
|
202
|
-
// A GLB/gltf can still reference EXTERNAL textures/.bin (e.g. the Kenney mirror
|
|
203
|
-
// GLBs point at `Textures/*.png`). Fetch those siblings relative to the source
|
|
204
|
-
// URL so the add pipeline's dependency check resolves them. Failures here are
|
|
205
|
-
// soft: the add step reports any that are genuinely unreachable.
|
|
206
|
-
await fetchExternalDependencies(buffer, url, destPath);
|
|
207
|
-
return { path: destPath };
|
|
208
|
-
};
|
|
209
|
-
/** External `uri` refs (images + buffers) in a GLB JSON chunk or a .gltf file. */
|
|
210
|
-
function externalUris(modelBytes, destPath) {
|
|
211
|
-
let json;
|
|
212
|
-
try {
|
|
213
|
-
if (startsWith(modelBytes, GLB_MAGIC)) {
|
|
214
|
-
// GLB: 12-byte header, then chunk [u32 len][u32 type=JSON][bytes].
|
|
215
|
-
if (modelBytes.length < 20)
|
|
216
|
-
return [];
|
|
217
|
-
const jsonLen = modelBytes.readUInt32LE(12);
|
|
218
|
-
json = JSON.parse(modelBytes.toString("utf8", 20, 20 + jsonLen));
|
|
219
|
-
}
|
|
220
|
-
else if (destPath.toLowerCase().endsWith(".gltf")) {
|
|
221
|
-
json = JSON.parse(modelBytes.toString("utf8"));
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
return [];
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
catch {
|
|
228
|
-
return [];
|
|
229
|
-
}
|
|
230
|
-
const doc = json;
|
|
231
|
-
const uris = [...(doc.images ?? []), ...(doc.buffers ?? [])]
|
|
232
|
-
.map((entry) => entry.uri)
|
|
233
|
-
.filter((uri) => typeof uri === "string" && uri.length > 0 && !uri.startsWith("data:"));
|
|
234
|
-
return [...new Set(uris)];
|
|
235
|
-
}
|
|
236
|
-
/** Best-effort fetch of a model's external sibling resources next to destPath. */
|
|
237
|
-
async function fetchExternalDependencies(modelBytes, sourceUrl, destPath) {
|
|
238
|
-
const uris = externalUris(modelBytes, destPath);
|
|
239
|
-
if (uris.length === 0)
|
|
240
|
-
return;
|
|
241
|
-
const dir = dirname(destPath);
|
|
242
|
-
await Promise.all(uris.map(async (uri) => {
|
|
243
|
-
const decoded = decodeURIComponent(uri);
|
|
244
|
-
const target = join(dir, decoded);
|
|
245
|
-
let resolvedUrl;
|
|
246
|
-
try {
|
|
247
|
-
resolvedUrl = new URL(uri, sourceUrl).toString();
|
|
248
|
-
}
|
|
249
|
-
catch {
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
try {
|
|
253
|
-
const res = await fetch(resolvedUrl, { headers: { accept: "*/*" } });
|
|
254
|
-
if (!res.ok)
|
|
255
|
-
return;
|
|
256
|
-
const bytes = Buffer.from(await res.arrayBuffer());
|
|
257
|
-
if (bytes.length === 0)
|
|
258
|
-
return;
|
|
259
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
260
|
-
writeFileSync(target, bytes);
|
|
261
|
-
}
|
|
262
|
-
catch {
|
|
263
|
-
// Soft failure: leave the file absent; addAsset surfaces the gap.
|
|
264
|
-
}
|
|
265
|
-
}));
|
|
266
|
-
}
|
|
267
|
-
function startsWith(buffer, magic) {
|
|
268
|
-
return buffer.length >= magic.length && buffer.subarray(0, magic.length).equals(magic);
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Some catalog `download` endpoints return `{ gltf: { url }, ... }`. When the
|
|
272
|
-
* sandbox has no creds the URL is usually still un-fetchable, but we attempt the
|
|
273
|
-
* obvious direct artifact field so a future credentialed run works end-to-end.
|
|
274
|
-
*/
|
|
275
|
-
async function followSignedDownload(buffer, destPath) {
|
|
276
|
-
let parsed;
|
|
277
|
-
try {
|
|
278
|
-
parsed = JSON.parse(buffer.toString("utf8"));
|
|
279
|
-
}
|
|
280
|
-
catch {
|
|
281
|
-
return undefined;
|
|
282
|
-
}
|
|
283
|
-
const envelope = parsed;
|
|
284
|
-
const directUrl = pickUrl(envelope?.["glb"]) ?? pickUrl(envelope?.["gltf"]) ?? pickUrl(envelope?.["model"]);
|
|
285
|
-
if (!directUrl)
|
|
286
|
-
return undefined;
|
|
287
|
-
const res = await fetch(directUrl, { headers: { accept: "model/gltf-binary,application/zip,*/*" } });
|
|
288
|
-
if (!res.ok)
|
|
289
|
-
return undefined;
|
|
290
|
-
const inner = Buffer.from(await res.arrayBuffer());
|
|
291
|
-
if (inner.length === 0)
|
|
292
|
-
return undefined;
|
|
293
|
-
if (startsWith(inner, ZIP_MAGIC))
|
|
294
|
-
return unpackZipToModel(inner, destPath);
|
|
295
|
-
writeFileSync(destPath, inner);
|
|
296
|
-
return { path: destPath };
|
|
297
|
-
}
|
|
298
|
-
function pickUrl(value) {
|
|
299
|
-
if (typeof value === "string")
|
|
300
|
-
return value;
|
|
301
|
-
if (value && typeof value === "object") {
|
|
302
|
-
const url = value["url"];
|
|
303
|
-
if (typeof url === "string")
|
|
304
|
-
return url;
|
|
305
|
-
}
|
|
306
|
-
return undefined;
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Minimal, dependency-free ZIP reader: walk the End-Of-Central-Directory and
|
|
310
|
-
* central directory, inflate (or copy stored) entries, write them next to
|
|
311
|
-
* `destPath`, and return the path to the assembled .glb (preferred) or .gltf.
|
|
312
|
-
* Sibling .bin/texture files land in the same dir so the add pipeline's
|
|
313
|
-
* dependency copier finds them.
|
|
314
|
-
*/
|
|
315
|
-
function unpackZipToModel(zip, destPath) {
|
|
316
|
-
const dir = dirname(destPath);
|
|
317
|
-
const entries = readZipEntries(zip);
|
|
318
|
-
if (entries.length === 0) {
|
|
319
|
-
throw new Error("Aura3D resolve failed: downloaded ZIP contained no files.");
|
|
320
|
-
}
|
|
321
|
-
let glbPath;
|
|
322
|
-
let gltfPath;
|
|
323
|
-
for (const entry of entries) {
|
|
324
|
-
if (entry.name.endsWith("/"))
|
|
325
|
-
continue;
|
|
326
|
-
const safeName = basename(entry.name);
|
|
327
|
-
if (!safeName)
|
|
328
|
-
continue;
|
|
329
|
-
const outPath = join(dir, safeName);
|
|
330
|
-
writeFileSync(outPath, entry.data);
|
|
331
|
-
const lower = safeName.toLowerCase();
|
|
332
|
-
if (lower.endsWith(".glb") && (!glbPath || isPlausibleGlb(entry.data)))
|
|
333
|
-
glbPath = outPath;
|
|
334
|
-
else if (lower.endsWith(".gltf") && !gltfPath)
|
|
335
|
-
gltfPath = outPath;
|
|
336
|
-
}
|
|
337
|
-
const chosen = glbPath ?? gltfPath;
|
|
338
|
-
if (!chosen) {
|
|
339
|
-
throw new Error("Aura3D resolve failed: ZIP did not contain a .glb or .gltf model.");
|
|
340
|
-
}
|
|
341
|
-
return { path: chosen };
|
|
342
|
-
}
|
|
343
|
-
function isPlausibleGlb(data) {
|
|
344
|
-
return startsWith(data, GLB_MAGIC);
|
|
345
|
-
}
|
|
346
|
-
/** Parse central-directory records and inflate each entry's local data. */
|
|
347
|
-
function readZipEntries(zip) {
|
|
348
|
-
const eocd = findEocd(zip);
|
|
349
|
-
if (eocd < 0)
|
|
350
|
-
return [];
|
|
351
|
-
const total = zip.readUInt16LE(eocd + 10);
|
|
352
|
-
let cd = zip.readUInt32LE(eocd + 16);
|
|
353
|
-
const entries = [];
|
|
354
|
-
for (let i = 0; i < total; i += 1) {
|
|
355
|
-
if (cd + 46 > zip.length || zip.readUInt32LE(cd) !== 0x02014b50)
|
|
356
|
-
break;
|
|
357
|
-
const method = zip.readUInt16LE(cd + 10);
|
|
358
|
-
const compSize = zip.readUInt32LE(cd + 20);
|
|
359
|
-
const nameLen = zip.readUInt16LE(cd + 28);
|
|
360
|
-
const extraLen = zip.readUInt16LE(cd + 30);
|
|
361
|
-
const commentLen = zip.readUInt16LE(cd + 32);
|
|
362
|
-
const localOffset = zip.readUInt32LE(cd + 42);
|
|
363
|
-
const name = zip.toString("utf8", cd + 46, cd + 46 + nameLen);
|
|
364
|
-
const data = inflateLocalEntry(zip, localOffset, method, compSize);
|
|
365
|
-
if (data)
|
|
366
|
-
entries.push({ name, data });
|
|
367
|
-
cd += 46 + nameLen + extraLen + commentLen;
|
|
368
|
-
}
|
|
369
|
-
return entries;
|
|
370
|
-
}
|
|
371
|
-
function inflateLocalEntry(zip, localOffset, method, compSize) {
|
|
372
|
-
if (localOffset + 30 > zip.length || zip.readUInt32LE(localOffset) !== 0x04034b50)
|
|
373
|
-
return undefined;
|
|
374
|
-
const nameLen = zip.readUInt16LE(localOffset + 26);
|
|
375
|
-
const extraLen = zip.readUInt16LE(localOffset + 28);
|
|
376
|
-
const dataStart = localOffset + 30 + nameLen + extraLen;
|
|
377
|
-
const comp = zip.subarray(dataStart, dataStart + compSize);
|
|
378
|
-
if (method === 0)
|
|
379
|
-
return Buffer.from(comp); // stored
|
|
380
|
-
if (method === 8) {
|
|
381
|
-
try {
|
|
382
|
-
return inflateRawSync(comp);
|
|
383
|
-
}
|
|
384
|
-
catch {
|
|
385
|
-
return undefined;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
return undefined; // unsupported compression method
|
|
389
|
-
}
|
|
390
|
-
function findEocd(zip) {
|
|
391
|
-
// EOCD signature 0x06054b50, scanning back from the end past any comment.
|
|
392
|
-
const min = Math.max(0, zip.length - 0xffff - 22);
|
|
393
|
-
for (let i = zip.length - 22; i >= min; i -= 1) {
|
|
394
|
-
if (zip.readUInt32LE(i) === 0x06054b50)
|
|
395
|
-
return i;
|
|
396
|
-
}
|
|
397
|
-
return -1;
|
|
398
|
-
}
|
|
399
|
-
function toLine(candidate, profile = "general") {
|
|
400
|
-
const { asset } = candidate;
|
|
401
|
-
const line = {
|
|
402
|
-
id: asset.id,
|
|
403
|
-
source: asset.source,
|
|
404
|
-
title: asset.title,
|
|
405
|
-
license: asset.license.spdx,
|
|
406
|
-
autoPullable: isAutoPullable(asset),
|
|
407
|
-
access: asset.access,
|
|
408
|
-
};
|
|
409
|
-
if (asset.sourcePage)
|
|
410
|
-
line.sourcePage = asset.sourcePage;
|
|
411
|
-
if (profile !== "general") {
|
|
412
|
-
const evaluation = evaluateAssetProfile(asset, profile);
|
|
413
|
-
line.profile = {
|
|
414
|
-
name: profile,
|
|
415
|
-
suitable: evaluation.suitable,
|
|
416
|
-
rejectionReasons: evaluation.rejectionReasons,
|
|
417
|
-
warnings: evaluation.warnings,
|
|
418
|
-
validationHooks: evaluation.validationHooks ?? [],
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
return line;
|
|
422
|
-
}
|
|
423
|
-
function makeDefaultResolver(adapters, limit) {
|
|
424
|
-
return new FederatedResolver({ adapters, limit });
|
|
425
|
-
}
|
|
426
|
-
/** Run a federated search and produce a printable report. */
|
|
427
|
-
export async function runSearch(options) {
|
|
428
|
-
const env = options.env ?? process.env;
|
|
429
|
-
const limit = options.limit ?? 10;
|
|
430
|
-
const adapters = buildSearchAdapters(env);
|
|
431
|
-
const resolver = options.makeResolver
|
|
432
|
-
? options.makeResolver(adapters)
|
|
433
|
-
: makeDefaultResolver(adapters, limit);
|
|
434
|
-
const constraints = toResolveConstraints(options.constraints ?? {}, false);
|
|
435
|
-
const profile = options.constraints?.profile ?? "general";
|
|
436
|
-
const result = await resolver.resolve({
|
|
437
|
-
text: options.query,
|
|
438
|
-
constraints,
|
|
439
|
-
});
|
|
440
|
-
const rankedCandidates = rankForProfile(result.candidates, profile);
|
|
441
|
-
const candidateLines = rankedCandidates.map((candidate) => toLine(candidate, profile));
|
|
442
|
-
const candidates = profile !== "general"
|
|
443
|
-
? candidateLines.filter((candidate) => candidate.profile?.suitable === true)
|
|
444
|
-
: candidateLines;
|
|
445
|
-
const rejectedCandidates = profile !== "general"
|
|
446
|
-
? candidateLines.filter((candidate) => candidate.profile?.suitable !== true)
|
|
447
|
-
: [];
|
|
448
|
-
const anyPullable = candidates.some((c) => c.autoPullable);
|
|
449
|
-
const anyProfileSuitable = profile === "general" || candidates.some((c) => c.profile?.suitable);
|
|
450
|
-
const anyProfilePullable = profile === "general" || candidates.some((c) => c.autoPullable && c.profile?.suitable);
|
|
451
|
-
const warnings = [...result.warnings];
|
|
452
|
-
if (profile !== "general") {
|
|
453
|
-
for (const candidate of candidateLines) {
|
|
454
|
-
for (const warning of candidate.profile?.warnings ?? []) {
|
|
455
|
-
warnings.push(`${candidate.id}: ${warning}`);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
let deepLinks = [];
|
|
460
|
-
if (!anyPullable) {
|
|
461
|
-
const deepLinkAdapter = buildDeepLinkAdapter();
|
|
462
|
-
if (deepLinkAdapter) {
|
|
463
|
-
try {
|
|
464
|
-
const records = await deepLinkAdapter.search({ text: options.query, constraints: {} }, { fetchJson: assetIndex.defaultFetchJson });
|
|
465
|
-
deepLinks = records.map((asset) => toLine({ asset, score: 0 }));
|
|
466
|
-
}
|
|
467
|
-
catch (err) {
|
|
468
|
-
warnings.push(`${deepLinkAdapter.id}: ${err.message}`);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
const messages = [];
|
|
473
|
-
if (candidateLines.length === 0) {
|
|
474
|
-
messages.push(`No candidates found for "${options.query}".`);
|
|
475
|
-
}
|
|
476
|
-
else {
|
|
477
|
-
messages.push(`${candidates.length} candidate(s) for "${options.query}" using ${profile} profile.`);
|
|
478
|
-
if (rejectedCandidates.length > 0) {
|
|
479
|
-
messages.push(`${rejectedCandidates.length} rejected candidate(s) moved to rejectedCandidates by the ${profile} profile.`);
|
|
480
|
-
}
|
|
481
|
-
if (!anyPullable) {
|
|
482
|
-
messages.push("No auto-pullable candidate. Listed assets need a manual license check before use.");
|
|
483
|
-
}
|
|
484
|
-
if (profile !== "general" && !anyProfileSuitable) {
|
|
485
|
-
messages.push(`No ${profile}-ready candidate. Listed candidates were rejected by the asset profile; inspect rejectionReasons before resolving.`);
|
|
486
|
-
}
|
|
487
|
-
else if (profile !== "general" && !anyProfilePullable) {
|
|
488
|
-
messages.push(`No auto-pullable ${profile}-ready candidate. Resolve will refuse until a downloadable licensed candidate also passes the profile.`);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
if (deepLinks.length > 0) {
|
|
492
|
-
messages.push(`${deepLinks.length} marketplace deep-link(s) — manual download (license check required).`);
|
|
493
|
-
}
|
|
494
|
-
return {
|
|
495
|
-
ok: true,
|
|
496
|
-
query: options.query,
|
|
497
|
-
profile,
|
|
498
|
-
candidates,
|
|
499
|
-
rejectedCandidates,
|
|
500
|
-
deepLinks,
|
|
501
|
-
warnings,
|
|
502
|
-
messages,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
24
|
+
import { addAsset, inspectAsset } from "./index.js";
|
|
25
|
+
import { buildSearchAdapters } from "./pull-bridge/adapters.js";
|
|
26
|
+
import { defaultDownloadFile } from "./pull-bridge/download.js";
|
|
27
|
+
import { evaluateAssetProfile, rankForProfile, toResolveConstraints, } from "./pull-bridge/profiles.js";
|
|
28
|
+
import { createPostDownloadCandidateBlockingWarnings, createPreDownloadCandidateBlockingWarnings, createResolveCandidateProvenance, mapCanonicalRoleToCli, } from "./pull-bridge/provenance.js";
|
|
29
|
+
import { selectPullable } from "./pull-bridge/pullable.js";
|
|
30
|
+
import { rankResolveCandidates, scoreResolveCandidate, } from "./pull-bridge/scoring.js";
|
|
31
|
+
import { makeDefaultResolver, runSearch, toLine, } from "./pull-bridge/search.js";
|
|
32
|
+
export { defaultDownloadFile, rankResolveCandidates, runSearch, selectPullable, scoreResolveCandidate, toResolveConstraints, };
|
|
33
|
+
const { isAutoPullable } = assetIndex;
|
|
34
|
+
export { buildDeepLinkAdapter, buildSearchAdapters } from "./pull-bridge/adapters.js";
|
|
505
35
|
/**
|
|
506
36
|
* Resolve a query to the top auto-pullable candidate, download its .glb to a
|
|
507
37
|
* temp path, and run the EXISTING `addAsset` pipeline so it lands as a typed
|
|
@@ -521,7 +51,10 @@ export async function runResolve(options) {
|
|
|
521
51
|
const constraints = toResolveConstraints(options.constraints ?? {}, true);
|
|
522
52
|
const profile = options.constraints?.profile ?? "general";
|
|
523
53
|
const result = await resolver.resolve({ text: options.query, constraints });
|
|
524
|
-
const ranked = rankForProfile(result.candidates, profile)
|
|
54
|
+
const ranked = rankResolveCandidates(rankForProfile(result.candidates, profile), {
|
|
55
|
+
query: options.query,
|
|
56
|
+
profile,
|
|
57
|
+
});
|
|
525
58
|
const selection = selectPullable(ranked, { profile });
|
|
526
59
|
if (!selection.ok) {
|
|
527
60
|
throw new Error(`Aura3D resolve refused: ${selection.reason}`);
|
|
@@ -540,11 +73,16 @@ export async function runResolve(options) {
|
|
|
540
73
|
const download = options.download ?? defaultDownloadFile;
|
|
541
74
|
const tmpRoot = options.tmpRoot ?? tmpdir();
|
|
542
75
|
const addFn = options.addAssetFn ?? addAsset;
|
|
76
|
+
const inspectFn = options.inspectAssetFn ?? inspectAsset;
|
|
543
77
|
const retrievedAt = options.retrievedAt ?? new Date().toISOString();
|
|
544
78
|
const attemptWarnings = [];
|
|
545
79
|
for (const candidateChoice of pullable) {
|
|
546
80
|
const asset = candidateChoice.asset;
|
|
547
81
|
try {
|
|
82
|
+
const preDownloadWarnings = createPreDownloadCandidateBlockingWarnings(asset);
|
|
83
|
+
if (preDownloadWarnings.length > 0) {
|
|
84
|
+
throw new Error(`pre-download candidate quality blocked: ${preDownloadWarnings.join("; ")}`);
|
|
85
|
+
}
|
|
548
86
|
const dir = mkdtempSync(join(tmpRoot, "aura3d-resolve-"));
|
|
549
87
|
const ext = asset.format === "gltf" ? "gltf" : "glb";
|
|
550
88
|
const tempFile = join(dir, `${options.name}.${ext}`);
|
|
@@ -556,28 +94,58 @@ export async function runResolve(options) {
|
|
|
556
94
|
// timestamp (injectable for determinism) so provenance records exactly what
|
|
557
95
|
// was fetched. (#19/#26)
|
|
558
96
|
const sha256 = `sha256-${createHash("sha256").update(readFileSync(resolvedFile)).digest("hex")}`;
|
|
97
|
+
const inspection = inspectFn({
|
|
98
|
+
file: resolvedFile,
|
|
99
|
+
...(options.projectDir ? { projectDir: options.projectDir } : {}),
|
|
100
|
+
animation: true,
|
|
101
|
+
humanoid: true,
|
|
102
|
+
skeleton: true,
|
|
103
|
+
morphs: true,
|
|
104
|
+
license: true,
|
|
105
|
+
});
|
|
106
|
+
const postDownloadWarnings = createPostDownloadCandidateBlockingWarnings(asset, inspection, profile);
|
|
107
|
+
if (postDownloadWarnings.length > 0) {
|
|
108
|
+
throw new Error(`post-download inspection blocked candidate: ${postDownloadWarnings.join("; ")}`);
|
|
109
|
+
}
|
|
110
|
+
const candidateScore = scoreResolveCandidate(candidateChoice, {
|
|
111
|
+
query: options.query,
|
|
112
|
+
profile,
|
|
113
|
+
});
|
|
114
|
+
const resolveCandidate = createResolveCandidateProvenance(candidateChoice, options.query, candidateScore, inspection);
|
|
559
115
|
const add = addFn({
|
|
560
116
|
file: resolvedFile,
|
|
561
117
|
name: options.name,
|
|
562
118
|
...(options.projectDir ? { projectDir: options.projectDir } : {}),
|
|
119
|
+
sourcePage: asset.sourcePage,
|
|
120
|
+
downloadUrl: asset.downloadUrl ?? asset.url,
|
|
563
121
|
sourceUrl: asset.sourcePage ?? asset.url,
|
|
564
122
|
license: asset.license.spdx,
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
123
|
+
licenseName: (asset.licenseName ?? asset.license.raw) || asset.license.spdx,
|
|
124
|
+
licenseUrl: asset.licenseUrl ?? asset.license.sourcePage,
|
|
125
|
+
licenseRaw: asset.license.raw,
|
|
126
|
+
sourceFamily: asset.sourceFamily ?? asset.source,
|
|
127
|
+
attribution: asset.attribution ?? asset.author,
|
|
128
|
+
author: asset.author ?? asset.attribution,
|
|
568
129
|
sha256,
|
|
130
|
+
resolveCandidate,
|
|
569
131
|
retrievedAt,
|
|
132
|
+
quality: "candidate",
|
|
133
|
+
role: mapCanonicalRoleToCli(asset.intendedRole),
|
|
134
|
+
suitabilityReason: asset.roleSuitability ?? "Resolved catalog candidate; release validation and rendered-probe proof are still required.",
|
|
570
135
|
});
|
|
571
136
|
const typedRef = `model(assets.${options.name})`;
|
|
572
137
|
const messages = [...attemptWarnings, ...add.messages];
|
|
573
|
-
messages.push(`
|
|
138
|
+
messages.push(`Resolved candidate ${asset.id} (${asset.license.spdx}) from ${asset.source}.`);
|
|
574
139
|
messages.push(`Provenance: sha256 ${sha256}, retrieved ${retrievedAt}, source ${asset.sourcePage ?? asset.url}.`);
|
|
140
|
+
messages.push(`Candidate score ${candidateScore.total}: ${candidateScore.reasons.join("; ") || "no positive score reasons"}. ` +
|
|
141
|
+
`Release validation is still required before public examples.`);
|
|
575
142
|
if (asset.license.attributionRequired) {
|
|
576
143
|
const credit = asset.attribution ?? asset.sourcePage ?? asset.source;
|
|
577
144
|
messages.push(`Attribution required (${asset.license.spdx}): credit "${credit}". ` +
|
|
578
145
|
`See ${asset.sourcePage ?? asset.url}.`);
|
|
579
146
|
}
|
|
580
147
|
messages.push(`Use the typed ref: ${typedRef}`);
|
|
148
|
+
messages.push(`Next: run aura3d assets validate --release --source <entrypoint> and attach retained rendered-probe proof before claiming release readiness.`);
|
|
581
149
|
return {
|
|
582
150
|
ok: add.ok,
|
|
583
151
|
profile,
|
|
@@ -595,39 +163,4 @@ export async function runResolve(options) {
|
|
|
595
163
|
throw new Error(`Aura3D resolve failed: every auto-pullable candidate failed to download or assemble. ` +
|
|
596
164
|
attemptWarnings.join(" | "));
|
|
597
165
|
}
|
|
598
|
-
function rankForProfile(candidates, profile) {
|
|
599
|
-
if (profile === "general")
|
|
600
|
-
return candidates;
|
|
601
|
-
return [...candidates].sort((a, b) => {
|
|
602
|
-
const aEval = evaluateAssetProfile(a.asset, profile);
|
|
603
|
-
const bEval = evaluateAssetProfile(b.asset, profile);
|
|
604
|
-
return (Number(bEval.suitable) - Number(aEval.suitable) ||
|
|
605
|
-
(b.score + bEval.scoreBonus) - (a.score + aEval.scoreBonus) ||
|
|
606
|
-
a.asset.id.localeCompare(b.asset.id));
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
function isAnimationCliProfile(profile) {
|
|
610
|
-
return typeof profile === "string" && isAnimationAssetProfile(profile);
|
|
611
|
-
}
|
|
612
|
-
function evaluateAssetProfile(asset, profile) {
|
|
613
|
-
// CLI search/resolve runs against the hosted catalog *before* downloading, so
|
|
614
|
-
// animation profiles evaluate in pre-download mode: absent rig/animation/triangle
|
|
615
|
-
// metadata down-ranks and defers to a post-download validation hook instead of
|
|
616
|
-
// hard-rejecting every catalog candidate. (#20/#23)
|
|
617
|
-
return profile === "fighting-character"
|
|
618
|
-
? evaluateGameAssetProfile(asset, "fighting-character")
|
|
619
|
-
: evaluateAnimationAssetProfile(asset, profile, { preDownload: true });
|
|
620
|
-
}
|
|
621
|
-
function animationProfileMaxTriangles(profile) {
|
|
622
|
-
switch (profile) {
|
|
623
|
-
case "animation-character":
|
|
624
|
-
return 160_000;
|
|
625
|
-
case "animation-prop":
|
|
626
|
-
return 100_000;
|
|
627
|
-
case "animation-set":
|
|
628
|
-
return 350_000;
|
|
629
|
-
case "animation-environment":
|
|
630
|
-
return 250_000;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
166
|
//# sourceMappingURL=pull-bridge.js.map
|