@jgengine/assets 0.8.0 → 0.9.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 +16 -0
- package/README.md +29 -8
- package/dist/cli/paths.d.ts +4 -0
- package/dist/cli/paths.js +11 -0
- package/dist/cli/pull.d.ts +4 -0
- package/dist/cli/pull.js +133 -16
- package/dist/download.d.ts +17 -2
- package/dist/download.js +24 -2
- package/dist/find.d.ts +35 -0
- package/dist/find.js +147 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/indexGen.js +23 -2
- package/dist/registry-catalog.json +455 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +6 -0
- package/dist/snippet.d.ts +13 -0
- package/dist/snippet.js +47 -0
- package/dist/sources/kaykit.js +1 -1
- package/dist/sources/quaternius.js +2 -0
- package/llms.txt +210 -647
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,22 @@ Agents building on the published SDK can also read this programmatically:
|
|
|
11
11
|
same data as typed values, so an updater can diff its installed version against
|
|
12
12
|
the latest and surface the migration steps.
|
|
13
13
|
|
|
14
|
+
## Unreleased
|
|
15
|
+
|
|
16
|
+
## 0.9.0
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Mobile HUD fit — design-resolution UI scaling now applies to every game by default: `HudCanvas` measures the live viewport and scales the whole HUD from `PlayableGame.hudFit.designSize` (default 1600×900, clamps `minScale`/`maxScale` 0.4–1) so desktop-authored layouts shrink to fit a phone instead of overflowing it; `hudFit.mobile` overrides tune the phone fit separately. Pure math lives in `@jgengine/core/ui/hudScale` (`hudScaleForViewport`, `resolveHudFit`, `rectOverflow`, `overflowingPanels`); the shell mounts `@jgengine/react`'s `HudViewportProvider` around `GameUI` so games need no wiring.
|
|
21
|
+
- Graphics → UI scale — a player-facing `graphics.uiScale` slider (0.5–1.5, default 1) multiplies the computed HUD scale on every platform; the same resolution system drives desktop preference and mobile shrink.
|
|
22
|
+
- HUD overflow gate — `HudCanvas` measures every `HudPanel` against the viewport at runtime and reports offenders on a `data-hud-overflow` attribute (plus a console warning); `bun run shoot <game> --device mobile|both` now exits non-zero naming the panels that escape the viewport.
|
|
23
|
+
- Automatic visibility & streaming defaults — an engine-level `VisibilitySystem` (exported from `@jgengine/core`) gives every scene renderer-agnostic culling and asset-streaming policy with no per-object wiring: distance culling, preload margins, hysteresis, delayed unloading, multi-camera awareness (including cameras excluded from streaming), and per-object overrides (always-visible, never-unload, disabled culling/streaming, custom render distance and preload margin).
|
|
24
|
+
- Self-hosted asset mirror — `DEFAULT_RELEASE_BASE` now points at this repo's rolling `packs` release instead of the upstream host; a catalog-driven `mirror-assets` workflow (weekly cron + manual dispatch) keeps the mirror in sync with the asset catalog.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Mobile HUD fit is on by default — design-resolution HUD scaling now applies to every game with no config change (previously opt-in via `platforms: ["web", "mobile"]`). A desktop-only game keeps the legacy fixed 0.85 compact zoom by declaring `platforms: ["web"]` (without `"mobile"`).
|
|
29
|
+
|
|
14
30
|
## 0.8.0
|
|
15
31
|
|
|
16
32
|
Transport-agnostic multiplayer (socket.io, WebRTC p2p, LAN/Fly adapters) plus grid/voxel
|
package/README.md
CHANGED
|
@@ -18,33 +18,54 @@ A self-generating, license-verified index of thousands of CC0 3D models — host
|
|
|
18
18
|
|
|
19
19
|
Everything collapses into the core `AssetCatalog` via `buildCatalog({ basePath })`.
|
|
20
20
|
|
|
21
|
+
## `add` — one command for anything
|
|
22
|
+
|
|
23
|
+
`assets add <query>` is the front door. It fuzzy-searches **every** catalog at once — 3D models, whole packs, HUD components (the shadcn registry), and `game-icon` glyphs — then does the fetch and prints the exact copy-paste wiring. One mental model instead of four.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
assets add astronaut --dir ../../apps/dev/public # model → pull + reindex + print the assets.ts snippet
|
|
27
|
+
assets add nature --dir ../../apps/dev/public # whole pack → pull + reindex + how to wire an id
|
|
28
|
+
assets add "mana bar" # HUD component → the `npx shadcn add` cmd + <VitalBar/> usage
|
|
29
|
+
assets add sword # icon → the game-icon name to drop in a slot
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- A **model / pack** match is fully automated: if the pack isn't already in `<dir>/models/`, it's pulled and extracted, then `reindex` runs so the id is addressable, then the `buildCatalog` + model-seam snippet is printed.
|
|
33
|
+
- A **component / icon** match prints the one-liner to run and the import + usage — no bytes to pull.
|
|
34
|
+
- Ambiguous query? `add` lists the top matches across kinds; narrow with `--kind model|pack|component|icon` or a more specific term. `--json` emits the ranked matches for scripting.
|
|
35
|
+
|
|
36
|
+
The same ranking is available programmatically: `import { findAssets } from "@jgengine/assets"`.
|
|
37
|
+
|
|
21
38
|
## CLI
|
|
22
39
|
|
|
23
40
|
```
|
|
41
|
+
assets add <query> [--kind <k>] [--dir <dir>] [--mirror <baseUrl>] [--json]
|
|
42
|
+
# unified import: models, packs, components, icons
|
|
24
43
|
assets list [--category <c>] [--source <s>] # browse the generated index
|
|
25
44
|
assets search <term> # grep the index
|
|
26
45
|
assets pull <source-id> [--dir public] [--mirror <baseUrl>] [--offline]
|
|
27
|
-
# download + extract GLBs into <dir>/models/<source>/
|
|
28
|
-
assets
|
|
46
|
+
# download + extract a whole pack's GLBs into <dir>/models/<source>/
|
|
47
|
+
assets register <path|url> --category <c> --license <l> [--author <a>]
|
|
48
|
+
# register a one-off single into the shipped index
|
|
29
49
|
assets reindex [public/models] # regenerate generated/*.json from pulled packs
|
|
30
50
|
assets verify # license + alias-integrity gate
|
|
31
51
|
```
|
|
32
52
|
|
|
33
|
-
Run in-repo with `bun run --cwd packages/assets src/cli/pull.ts <verb> …`.
|
|
53
|
+
Run in-repo with `bun run --cwd packages/assets src/cli/pull.ts <verb> …`. (`add <path|url> --license …` still works as an alias for `register`.)
|
|
34
54
|
|
|
35
55
|
### Mirror fallback and offline pulls
|
|
36
56
|
|
|
37
57
|
`pull` never has just one path to the bytes. For a given `source-id` it tries, in order, until one succeeds:
|
|
38
58
|
|
|
39
59
|
1. **Mirror base override** — `--mirror <baseUrl>` (or the `JGENGINE_ASSETS_MIRROR` env var if `--mirror` is not passed) — the archive is expected at `<baseUrl>/<provider>/<source-id>.zip`, e.g. `https://my-mirror.example.com/kenney/kenney-nature.zip`.
|
|
40
|
-
2. **The
|
|
41
|
-
3. **The
|
|
60
|
+
2. **The default GitHub-release mirror** — `https://github.com/Noisemaker111/jgengine/releases/download/packs/<provider>-<source-id>.zip`, on this repo's own rolling `packs` release (no separate assets repo). github.com is reachable from every cloud sandbox with no network-policy change, so zero-setup sessions still pull. `.github/workflows/mirror-assets.yml` (weekly cron + manual dispatch) keeps the release in sync with `src/sources/*.ts` automatically — adding a catalog entry is the whole publishing step, no manual upload. Skip this hop with `JGENGINE_ASSETS_NO_DEFAULT_MIRROR=1`.
|
|
61
|
+
3. **The primary provider path** — the source's pinned `{ url, sha256? }` or, for providers that rotate URLs (Kenney), a scrape of the asset page.
|
|
62
|
+
4. **The pack's own `mirror`** — an optional direct archive URL set on the `AssetSource` entry itself (`src/sources/*.ts`), tried as a last resort.
|
|
42
63
|
|
|
43
|
-
If every attempt fails, `pull` throws one aggregated error naming every URL it tried and why each one failed. Whenever the source's `download` is pinned with a `sha256`, the downloaded bytes are hashed and checked against it **no matter which
|
|
64
|
+
If every attempt fails, `pull` throws one aggregated error naming every URL it tried and why each one failed. Whenever the source's `download` is pinned with a `sha256`, the downloaded bytes are hashed and checked against it **no matter which path supplied them** — a mirror serving stale or tampered bytes is rejected and the next source in the chain is tried instead.
|
|
44
65
|
|
|
45
66
|
`--offline` skips the network entirely: it succeeds immediately if `<dir>/models/<source-id>/` already has files in it, and otherwise fails fast with a message telling you to pull once on a connected machine or point `--mirror`/`JGENGINE_ASSETS_MIRROR` at a reachable archive — useful for CI scripts that should error in seconds instead of hanging on a blocked fetch.
|
|
46
67
|
|
|
47
|
-
**Network-restricted environments** — pull once on a machine that can reach the providers, then
|
|
68
|
+
**Network-restricted environments** — a `403` on `CONNECT` from an outbound proxy means the environment's network policy blocks that provider host (common in sandboxed cloud sessions); it is a policy decision, not a transient failure, so retrying never helps. Either allowlist the host in the environment settings, or pull once on a machine that can reach the providers, then:
|
|
48
69
|
|
|
49
70
|
- commit (or otherwise host) the resulting `public/models/<source-id>/` directory so restricted environments read it straight off disk and use `assets pull --offline` as a fast, honest no-op check, or
|
|
50
71
|
- host your own mirror of the zip archives at `<baseUrl>/<provider>/<source-id>.zip` and set `JGENGINE_ASSETS_MIRROR=<baseUrl>` (or pass `--mirror <baseUrl>`) so `pull` fetches from it instead of the original provider.
|
|
@@ -96,7 +117,7 @@ export const game: PlayableGame = {
|
|
|
96
117
|
};
|
|
97
118
|
```
|
|
98
119
|
|
|
99
|
-
`buildCatalog({ sources: ["kenney-nature"] })` restricts to chosen packs; `includeAliases` / `includeSingles` default true. Discover ids with `assets search <term>` / `assets list --category <c>` instead of memorizing them.
|
|
120
|
+
`buildCatalog({ sources: ["kenney-nature"] })` restricts to chosen packs; `includeAliases` / `includeSingles` default true. Discover ids with `assets add <query>` (or `assets search <term>` / `assets list --category <c>`) instead of memorizing them.
|
|
100
121
|
|
|
101
122
|
### Serving the bytes
|
|
102
123
|
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import. */
|
|
2
|
+
export declare function resolveGeneratedDir(cliDir: string): string;
|
|
3
|
+
export declare function resolvePackageTreeRoot(cliDir: string): string;
|
|
4
|
+
export declare function resolvePackageRoot(cliDir: string): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
/** Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import. */
|
|
3
|
+
export function resolveGeneratedDir(cliDir) {
|
|
4
|
+
return join(resolve(cliDir, ".."), "generated");
|
|
5
|
+
}
|
|
6
|
+
export function resolvePackageTreeRoot(cliDir) {
|
|
7
|
+
return resolve(cliDir, "..");
|
|
8
|
+
}
|
|
9
|
+
export function resolvePackageRoot(cliDir) {
|
|
10
|
+
return resolve(cliDir, "..", "..");
|
|
11
|
+
}
|
package/dist/cli/pull.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/** Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import. */
|
|
3
|
+
export declare const generatedDir: string;
|
|
4
|
+
export { resolveGeneratedDir } from "./paths.js";
|
|
2
5
|
export declare function flag(argv: string[], name: string): string | undefined;
|
|
6
|
+
export declare function describeNetworkFailure(error: unknown): string;
|
|
3
7
|
export declare function isPopulated(dir: string): boolean;
|
|
4
8
|
export declare function cmdPull(argv: string[]): Promise<void>;
|
package/dist/cli/pull.js
CHANGED
|
@@ -3,16 +3,22 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFi
|
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { downloadPackArchive, extractGlbs, extractTextures } from "../download.js";
|
|
6
|
+
import { rankAssets } from "../find.js";
|
|
6
7
|
import { generatedIndex } from "../generated/index.js";
|
|
7
8
|
import { reindex } from "../indexGen.js";
|
|
8
9
|
import { isScrapeDownload } from "../manifest.js";
|
|
10
|
+
import { registryCatalog } from "../registry.js";
|
|
11
|
+
import { componentWiringSnippet, iconWiringSnippet, modelWiringSnippet } from "../snippet.js";
|
|
9
12
|
import { sourceById } from "../sources/index.js";
|
|
10
13
|
import { verifyManifest } from "../verify.js";
|
|
14
|
+
import { resolveGeneratedDir, resolvePackageRoot, resolvePackageTreeRoot } from "./paths.js";
|
|
11
15
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
+
const packageTreeRoot = resolvePackageTreeRoot(here);
|
|
17
|
+
const pkgRoot = resolvePackageRoot(here);
|
|
18
|
+
/** Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import. */
|
|
19
|
+
export const generatedDir = resolveGeneratedDir(here);
|
|
20
|
+
export { resolveGeneratedDir } from "./paths.js";
|
|
21
|
+
const singlesJson = join(packageTreeRoot, "singles.json");
|
|
16
22
|
const localDir = join(pkgRoot, "local");
|
|
17
23
|
const CDN_BASE = "https://cdn.jsdelivr.net/gh/Noisemaker111/jgengine@main/packages/assets/local";
|
|
18
24
|
export function flag(argv, name) {
|
|
@@ -23,6 +29,19 @@ function fail(message) {
|
|
|
23
29
|
console.error(`error: ${message}`);
|
|
24
30
|
process.exit(1);
|
|
25
31
|
}
|
|
32
|
+
export function describeNetworkFailure(error) {
|
|
33
|
+
const chain = [];
|
|
34
|
+
let current = error;
|
|
35
|
+
while (current instanceof Error) {
|
|
36
|
+
chain.push(current.message);
|
|
37
|
+
current = current.cause;
|
|
38
|
+
}
|
|
39
|
+
const text = chain.length > 0 ? chain.join(" — ") : String(error);
|
|
40
|
+
const looksPolicyBlocked = /\b403\b|CONNECT|proxy|ENOTFOUND|ECONNREFUSED|ETIMEDOUT|fetch failed/i.test(text);
|
|
41
|
+
return looksPolicyBlocked
|
|
42
|
+
? `${text}\nhint: a 403/CONNECT failure here usually means the sandbox network policy blocks this host — not a transient error, retrying won't help. Allowlist the host in the environment settings, point JGENGINE_ASSETS_MIRROR at a reachable mirror, or build with procedural geometry.`
|
|
43
|
+
: text;
|
|
44
|
+
}
|
|
26
45
|
function cmdList(argv) {
|
|
27
46
|
const category = flag(argv, "category");
|
|
28
47
|
const source = flag(argv, "source");
|
|
@@ -69,13 +88,19 @@ export async function cmdPull(argv) {
|
|
|
69
88
|
console.log(`offline: ${outDir} already populated, skipping network`);
|
|
70
89
|
return;
|
|
71
90
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
const result = await fetchPackInto(source, outRoot, { mirrorBase });
|
|
92
|
+
const textureNote = result.textures > 0 ? ` + ${result.textures} texture(s)` : "";
|
|
93
|
+
console.log(`pulled ${result.models} models${textureNote} -> ${result.outDir}`);
|
|
94
|
+
}
|
|
95
|
+
async function fetchPackInto(source, outRoot, options) {
|
|
96
|
+
const outDir = join(outRoot, "models", source.id);
|
|
97
|
+
console.log(`resolving ${source.id}${isScrapeDownload(source.download) ? " (scrape)" : ""}` +
|
|
98
|
+
`${options.mirrorBase !== undefined ? ` [mirror override: ${options.mirrorBase}]` : ""}…`);
|
|
99
|
+
const { archive, url, attempted } = await downloadPackArchive(source, { mirrorBase: options.mirrorBase });
|
|
75
100
|
console.log(`downloaded ${url}${attempted.length > 1 ? ` (after ${attempted.length - 1} failed attempt(s))` : ""}`);
|
|
76
101
|
const glbs = extractGlbs(archive);
|
|
77
102
|
if (glbs.length === 0)
|
|
78
|
-
fail(`no .glb files found in ${
|
|
103
|
+
fail(`no .glb files found in ${source.id} archive`);
|
|
79
104
|
mkdirSync(outDir, { recursive: true });
|
|
80
105
|
for (const glb of glbs)
|
|
81
106
|
writeFileSync(join(outDir, glb.file), glb.bytes);
|
|
@@ -85,8 +110,7 @@ export async function cmdPull(argv) {
|
|
|
85
110
|
mkdirSync(dirname(dest), { recursive: true });
|
|
86
111
|
writeFileSync(dest, texture.bytes);
|
|
87
112
|
}
|
|
88
|
-
|
|
89
|
-
console.log(`pulled ${glbs.length} models${textureNote} -> ${outDir}`);
|
|
113
|
+
return { outDir, models: glbs.length, textures: textures.length, url };
|
|
90
114
|
}
|
|
91
115
|
function readSingles() {
|
|
92
116
|
if (!existsSync(singlesJson))
|
|
@@ -96,10 +120,11 @@ function readSingles() {
|
|
|
96
120
|
function writeSingles(list) {
|
|
97
121
|
writeFileSync(singlesJson, `${JSON.stringify(list, null, 2)}\n`);
|
|
98
122
|
}
|
|
99
|
-
function
|
|
123
|
+
function cmdRegisterSingle(argv) {
|
|
100
124
|
const target = argv[0];
|
|
101
|
-
if (target === undefined)
|
|
102
|
-
fail("usage:
|
|
125
|
+
if (target === undefined) {
|
|
126
|
+
fail("usage: register <path|url> --category <c> --license <l> [--author <a>] [--id <id>]");
|
|
127
|
+
}
|
|
103
128
|
const license = flag(argv, "license");
|
|
104
129
|
if (license === undefined)
|
|
105
130
|
fail("add requires --license");
|
|
@@ -132,6 +157,95 @@ function cmdAdd(argv) {
|
|
|
132
157
|
writeSingles(singles);
|
|
133
158
|
console.log(`added single ${id} -> ${url}${isUrl ? " (0 bytes stored)" : " (copied to local/)"}`);
|
|
134
159
|
}
|
|
160
|
+
const KINDS = ["model", "pack", "component", "icon"];
|
|
161
|
+
function describeMatch(match) {
|
|
162
|
+
switch (match.kind) {
|
|
163
|
+
case "model":
|
|
164
|
+
return `[model] ${match.id}${match.via === "alias" ? " (alias)" : match.via === "single" ? " (single)" : ""}`;
|
|
165
|
+
case "pack":
|
|
166
|
+
return `[pack] ${match.source} — ${match.title}`;
|
|
167
|
+
case "component":
|
|
168
|
+
return `[component] ${match.name} — ${match.title}`;
|
|
169
|
+
case "icon":
|
|
170
|
+
return `[icon] ${match.name}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function performAdd(match, argv) {
|
|
174
|
+
if (match.kind === "component") {
|
|
175
|
+
const component = registryCatalog.components.find((entry) => entry.name === match.name);
|
|
176
|
+
if (component === undefined)
|
|
177
|
+
fail(`component vanished from catalog: ${match.name}`);
|
|
178
|
+
console.log(`component: ${component.title} — ${component.description}\n`);
|
|
179
|
+
console.log(componentWiringSnippet(component));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (match.kind === "icon") {
|
|
183
|
+
console.log(`icon: ${match.name}\n`);
|
|
184
|
+
console.log(iconWiringSnippet(match.name));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const source = sourceById.get(match.source);
|
|
188
|
+
if (source === undefined)
|
|
189
|
+
fail(`match resolves to unknown source: ${match.source}`);
|
|
190
|
+
const outRoot = resolve(flag(argv, "dir") ?? "public");
|
|
191
|
+
const outDir = join(outRoot, "models", match.source);
|
|
192
|
+
const mirrorBase = flag(argv, "mirror") ?? process.env.JGENGINE_ASSETS_MIRROR;
|
|
193
|
+
if (isPopulated(outDir)) {
|
|
194
|
+
console.log(`${match.source} already present in ${outDir}`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const result = await fetchPackInto(source, outRoot, { mirrorBase });
|
|
198
|
+
const textureNote = result.textures > 0 ? ` + ${result.textures} texture(s)` : "";
|
|
199
|
+
console.log(`pulled ${result.models} models${textureNote} -> ${result.outDir}`);
|
|
200
|
+
}
|
|
201
|
+
const result = reindex(join(outRoot, "models"), generatedDir);
|
|
202
|
+
console.log(`reindexed ${result.total} entries -> ${generatedDir}`);
|
|
203
|
+
if (match.kind === "model") {
|
|
204
|
+
console.log(`\nwire it in (characters/enemies go in entityModels instead):\n`);
|
|
205
|
+
console.log(modelWiringSnippet(match.id));
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.log(`\n${match.title}: browse ids with \`assets list --source ${match.source}\`, then wire like:\n`);
|
|
209
|
+
console.log(modelWiringSnippet(`${match.source}/<model>`));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function cmdAdd(argv) {
|
|
213
|
+
const query = argv[0];
|
|
214
|
+
if (query === undefined) {
|
|
215
|
+
fail("usage: add <query> [--kind model|pack|component|icon] [--dir <dir>] [--mirror <baseUrl>] [--json]\n" +
|
|
216
|
+
" add <path|url> --license <l> [--category <c>] (register a one-off single into the shipped index)");
|
|
217
|
+
}
|
|
218
|
+
// Back-compat: the old `add` registered a one-off single, always with --license.
|
|
219
|
+
if (flag(argv, "license") !== undefined) {
|
|
220
|
+
cmdRegisterSingle(argv);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const kindFlag = flag(argv, "kind");
|
|
224
|
+
if (kindFlag !== undefined && !KINDS.includes(kindFlag)) {
|
|
225
|
+
fail(`--kind must be one of ${KINDS.join(", ")}`);
|
|
226
|
+
}
|
|
227
|
+
const kind = kindFlag;
|
|
228
|
+
const ranked = rankAssets(query, { kind, limit: Number(flag(argv, "limit") ?? "12") });
|
|
229
|
+
if (ranked.length === 0) {
|
|
230
|
+
fail(`no asset matches "${query}" — try a broader term or \`assets search <term>\``);
|
|
231
|
+
}
|
|
232
|
+
if (argv.includes("--json")) {
|
|
233
|
+
console.log(JSON.stringify(ranked, null, 2));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const top = ranked[0];
|
|
237
|
+
const runnerUp = ranked[1]?.score ?? 0;
|
|
238
|
+
const decisive = ranked.length === 1 || kind !== undefined || top.score - runnerUp >= 20;
|
|
239
|
+
if (!decisive) {
|
|
240
|
+
console.log(`several matches for "${query}" — narrow with --kind or a more specific term:\n`);
|
|
241
|
+
for (const entry of ranked.slice(0, 8))
|
|
242
|
+
console.log(` ${describeMatch(entry.match)}`);
|
|
243
|
+
console.log(`\nthen re-run, e.g. \`assets add "${query}" --kind ${top.match.kind}\``);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
console.log(`→ ${describeMatch(top.match)}\n`);
|
|
247
|
+
await performAdd(top.match, argv);
|
|
248
|
+
}
|
|
135
249
|
function cmdReindex(argv) {
|
|
136
250
|
const modelsDir = resolve(argv[0] ?? join("public", "models"));
|
|
137
251
|
if (!existsSync(modelsDir))
|
|
@@ -161,10 +275,13 @@ if (import.meta.main) {
|
|
|
161
275
|
cmdSearch(rest);
|
|
162
276
|
break;
|
|
163
277
|
case "pull":
|
|
164
|
-
await cmdPull(rest);
|
|
278
|
+
await cmdPull(rest).catch((error) => fail(describeNetworkFailure(error)));
|
|
165
279
|
break;
|
|
166
280
|
case "add":
|
|
167
|
-
cmdAdd(rest);
|
|
281
|
+
await cmdAdd(rest).catch((error) => fail(describeNetworkFailure(error)));
|
|
282
|
+
break;
|
|
283
|
+
case "register":
|
|
284
|
+
cmdRegisterSingle(rest);
|
|
168
285
|
break;
|
|
169
286
|
case "reindex":
|
|
170
287
|
cmdReindex(rest);
|
|
@@ -173,7 +290,7 @@ if (import.meta.main) {
|
|
|
173
290
|
cmdVerify();
|
|
174
291
|
break;
|
|
175
292
|
default:
|
|
176
|
-
console.log("usage: assets <list|search|pull|
|
|
293
|
+
console.log("usage: assets <add|list|search|pull|register|reindex|verify> [...args]");
|
|
177
294
|
if (command !== undefined && command !== "help")
|
|
178
295
|
process.exit(1);
|
|
179
296
|
}
|
package/dist/download.d.ts
CHANGED
|
@@ -18,6 +18,19 @@ export declare function downloadArchive(url: string, fetchImpl?: FetchLike): Pro
|
|
|
18
18
|
* `https://my-mirror.example.com/kenney/kenney-nature.zip`.
|
|
19
19
|
*/
|
|
20
20
|
export declare function mirrorOverrideUrl(baseUrl: string, source: AssetSource): string;
|
|
21
|
+
/**
|
|
22
|
+
* Default asset mirror: this repo's own GitHub Releases, reachable from every
|
|
23
|
+
* cloud sandbox without network-policy changes (github.com is on the default
|
|
24
|
+
* allowlist). Assets live on the rolling `packs` release, one flat zip per
|
|
25
|
+
* pack named `<provider>-<packId>.zip`, kept in sync with the source catalog
|
|
26
|
+
* by `.github/workflows/mirror-assets.yml` — adding a catalog entry is the
|
|
27
|
+
* whole publishing step. Override the chain with `--mirror` /
|
|
28
|
+
* `JGENGINE_ASSETS_MIRROR`, or disable this hop with
|
|
29
|
+
* `JGENGINE_ASSETS_NO_DEFAULT_MIRROR=1`.
|
|
30
|
+
*/
|
|
31
|
+
export declare const DEFAULT_RELEASE_BASE = "https://github.com/Noisemaker111/jgengine/releases/download/packs";
|
|
32
|
+
/** URL of `source`'s archive on the default GitHub-release mirror. */
|
|
33
|
+
export declare function defaultReleaseUrl(source: AssetSource): string;
|
|
21
34
|
export interface DownloadPackOptions {
|
|
22
35
|
/** Explicit mirror base override (CLI `--mirror` / `JGENGINE_ASSETS_MIRROR`), tried before the primary provider path. */
|
|
23
36
|
mirrorBase?: string;
|
|
@@ -32,8 +45,10 @@ export interface DownloadPackResult {
|
|
|
32
45
|
/**
|
|
33
46
|
* Resolves and downloads a pack's archive, trying sources in order until one
|
|
34
47
|
* succeeds: (1) the mirror base override at `mirrorOverrideUrl`, (2) the
|
|
35
|
-
*
|
|
36
|
-
*
|
|
48
|
+
* default GitHub-release mirror at `defaultReleaseUrl` (skipped when
|
|
49
|
+
* `JGENGINE_ASSETS_NO_DEFAULT_MIRROR=1`), (3) the primary provider path
|
|
50
|
+
* (`resolveArchiveUrl`: scrape or pinned URL), (4) the pack's own `mirror`
|
|
51
|
+
* URL. A pinned `sha256` is verified against whichever
|
|
37
52
|
* source supplied the bytes; a mismatch is treated as a failed attempt so the
|
|
38
53
|
* next source in the chain is tried. Throws with every attempted URL and its
|
|
39
54
|
* failure reason when all sources fail.
|
package/dist/download.js
CHANGED
|
@@ -55,6 +55,21 @@ export function mirrorOverrideUrl(baseUrl, source) {
|
|
|
55
55
|
const base = baseUrl.replace(/\/+$/, "");
|
|
56
56
|
return `${base}/${source.provider}/${source.id}.zip`;
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Default asset mirror: this repo's own GitHub Releases, reachable from every
|
|
60
|
+
* cloud sandbox without network-policy changes (github.com is on the default
|
|
61
|
+
* allowlist). Assets live on the rolling `packs` release, one flat zip per
|
|
62
|
+
* pack named `<provider>-<packId>.zip`, kept in sync with the source catalog
|
|
63
|
+
* by `.github/workflows/mirror-assets.yml` — adding a catalog entry is the
|
|
64
|
+
* whole publishing step. Override the chain with `--mirror` /
|
|
65
|
+
* `JGENGINE_ASSETS_MIRROR`, or disable this hop with
|
|
66
|
+
* `JGENGINE_ASSETS_NO_DEFAULT_MIRROR=1`.
|
|
67
|
+
*/
|
|
68
|
+
export const DEFAULT_RELEASE_BASE = "https://github.com/Noisemaker111/jgengine/releases/download/packs";
|
|
69
|
+
/** URL of `source`'s archive on the default GitHub-release mirror. */
|
|
70
|
+
export function defaultReleaseUrl(source) {
|
|
71
|
+
return `${DEFAULT_RELEASE_BASE}/${source.provider}-${source.id}.zip`;
|
|
72
|
+
}
|
|
58
73
|
async function verifyPinnedSha(source, archive) {
|
|
59
74
|
const download = source.download;
|
|
60
75
|
if (isScrapeDownload(download) || download.sha256 === undefined)
|
|
@@ -67,8 +82,10 @@ async function verifyPinnedSha(source, archive) {
|
|
|
67
82
|
/**
|
|
68
83
|
* Resolves and downloads a pack's archive, trying sources in order until one
|
|
69
84
|
* succeeds: (1) the mirror base override at `mirrorOverrideUrl`, (2) the
|
|
70
|
-
*
|
|
71
|
-
*
|
|
85
|
+
* default GitHub-release mirror at `defaultReleaseUrl` (skipped when
|
|
86
|
+
* `JGENGINE_ASSETS_NO_DEFAULT_MIRROR=1`), (3) the primary provider path
|
|
87
|
+
* (`resolveArchiveUrl`: scrape or pinned URL), (4) the pack's own `mirror`
|
|
88
|
+
* URL. A pinned `sha256` is verified against whichever
|
|
72
89
|
* source supplied the bytes; a mismatch is treated as a failed attempt so the
|
|
73
90
|
* next source in the chain is tried. Throws with every attempted URL and its
|
|
74
91
|
* failure reason when all sources fail.
|
|
@@ -94,6 +111,11 @@ export async function downloadPackArchive(source, options = {}) {
|
|
|
94
111
|
if (result !== undefined)
|
|
95
112
|
return result;
|
|
96
113
|
}
|
|
114
|
+
if (process.env.JGENGINE_ASSETS_NO_DEFAULT_MIRROR !== "1") {
|
|
115
|
+
const result = await tryUrl(defaultReleaseUrl(source));
|
|
116
|
+
if (result !== undefined)
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
97
119
|
try {
|
|
98
120
|
const primaryUrl = await resolveArchiveUrl(source, fetchImpl);
|
|
99
121
|
const result = await tryUrl(primaryUrl);
|
package/dist/find.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type AssetKind = "model" | "pack" | "component" | "icon";
|
|
2
|
+
export type AssetMatch = {
|
|
3
|
+
kind: "model";
|
|
4
|
+
id: string;
|
|
5
|
+
source: string;
|
|
6
|
+
file?: string;
|
|
7
|
+
via: "index" | "alias" | "single";
|
|
8
|
+
} | {
|
|
9
|
+
kind: "pack";
|
|
10
|
+
source: string;
|
|
11
|
+
title: string;
|
|
12
|
+
categories: readonly string[];
|
|
13
|
+
} | {
|
|
14
|
+
kind: "component";
|
|
15
|
+
name: string;
|
|
16
|
+
title: string;
|
|
17
|
+
description: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "icon";
|
|
20
|
+
name: string;
|
|
21
|
+
};
|
|
22
|
+
export interface FindOptions {
|
|
23
|
+
/** Restrict results to one kind. */
|
|
24
|
+
kind?: AssetKind;
|
|
25
|
+
/** Cap the number of matches returned (default 12). */
|
|
26
|
+
limit?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface RankedMatch {
|
|
29
|
+
match: AssetMatch;
|
|
30
|
+
score: number;
|
|
31
|
+
}
|
|
32
|
+
/** Rank every catalog entry — models, packs, HUD components, icons — against one query. */
|
|
33
|
+
export declare function rankAssets(query: string, options?: FindOptions): RankedMatch[];
|
|
34
|
+
/** The ranked matches for a query — models, packs, HUD components, and icons in one list. */
|
|
35
|
+
export declare function findAssets(query: string, options?: FindOptions): AssetMatch[];
|
package/dist/find.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { aliases } from "./aliases.js";
|
|
2
|
+
import { generatedIndex } from "./generated/index.js";
|
|
3
|
+
import { keyFromFile } from "./indexGen.js";
|
|
4
|
+
import { registryCatalog } from "./registry.js";
|
|
5
|
+
import { singles } from "./singles.js";
|
|
6
|
+
import { sources } from "./sources/index.js";
|
|
7
|
+
function normalize(value) {
|
|
8
|
+
return value.toLowerCase().replace(/[\s_/-]+/g, " ").trim();
|
|
9
|
+
}
|
|
10
|
+
function tokenScore(token, hay, words) {
|
|
11
|
+
if (words.includes(token))
|
|
12
|
+
return 70;
|
|
13
|
+
if (hay.startsWith(token))
|
|
14
|
+
return 55;
|
|
15
|
+
if (words.some((word) => word.startsWith(token)))
|
|
16
|
+
return 45;
|
|
17
|
+
if (hay.includes(token))
|
|
18
|
+
return 30;
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 0 (no match) … 100 (exact). Single-word queries reward whole-word and prefix
|
|
23
|
+
* hits over loose substrings; a multi-word query ("mana bar") scores by the mean
|
|
24
|
+
* of its tokens and only counts when *every* token lands in the field.
|
|
25
|
+
*/
|
|
26
|
+
function scoreText(needle, hay) {
|
|
27
|
+
const n = normalize(needle);
|
|
28
|
+
const h = normalize(hay);
|
|
29
|
+
if (n.length === 0 || h.length === 0)
|
|
30
|
+
return 0;
|
|
31
|
+
if (h === n)
|
|
32
|
+
return 100;
|
|
33
|
+
const words = h.split(" ");
|
|
34
|
+
const tokens = n.split(" ").filter((token) => token.length > 0);
|
|
35
|
+
if (tokens.length <= 1)
|
|
36
|
+
return tokenScore(n, h, words);
|
|
37
|
+
let sum = 0;
|
|
38
|
+
for (const token of tokens) {
|
|
39
|
+
const score = tokenScore(token, h, words);
|
|
40
|
+
if (score === 0)
|
|
41
|
+
return 0;
|
|
42
|
+
sum += score;
|
|
43
|
+
}
|
|
44
|
+
return Math.min(90, Math.round(sum / tokens.length));
|
|
45
|
+
}
|
|
46
|
+
function best(needle, ...hays) {
|
|
47
|
+
let top = 0;
|
|
48
|
+
for (const hay of hays) {
|
|
49
|
+
if (hay === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
top = Math.max(top, scoreText(needle, hay));
|
|
52
|
+
}
|
|
53
|
+
return top;
|
|
54
|
+
}
|
|
55
|
+
function sourceOf(id) {
|
|
56
|
+
const slash = id.indexOf("/");
|
|
57
|
+
return slash === -1 ? id : id.slice(0, slash);
|
|
58
|
+
}
|
|
59
|
+
/** Rank every catalog entry — models, packs, HUD components, icons — against one query. */
|
|
60
|
+
export function rankAssets(query, options = {}) {
|
|
61
|
+
const q = query.trim();
|
|
62
|
+
const ranked = [];
|
|
63
|
+
const push = (score, match) => {
|
|
64
|
+
if (score > 0)
|
|
65
|
+
ranked.push({ score, match });
|
|
66
|
+
};
|
|
67
|
+
if (options.kind === undefined || options.kind === "model") {
|
|
68
|
+
for (const entry of generatedIndex) {
|
|
69
|
+
push(best(q, entry.id, keyFromFile(entry.file), ...entry.categories), {
|
|
70
|
+
kind: "model",
|
|
71
|
+
id: entry.id,
|
|
72
|
+
source: entry.source,
|
|
73
|
+
file: entry.file,
|
|
74
|
+
via: "index",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
for (const alias of aliases) {
|
|
78
|
+
push(best(q, alias.key, alias.target), {
|
|
79
|
+
kind: "model",
|
|
80
|
+
id: alias.target,
|
|
81
|
+
source: sourceOf(alias.target),
|
|
82
|
+
via: "alias",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
for (const single of singles) {
|
|
86
|
+
push(best(q, single.id, ...single.categories), {
|
|
87
|
+
kind: "model",
|
|
88
|
+
id: single.id,
|
|
89
|
+
source: sourceOf(single.id),
|
|
90
|
+
via: "single",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (options.kind === undefined || options.kind === "pack") {
|
|
95
|
+
for (const source of sources) {
|
|
96
|
+
push(best(q, source.id, source.title, source.provider, ...source.categories), {
|
|
97
|
+
kind: "pack",
|
|
98
|
+
source: source.id,
|
|
99
|
+
title: source.title,
|
|
100
|
+
categories: source.categories,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (options.kind === undefined || options.kind === "component") {
|
|
105
|
+
for (const component of registryCatalog.components) {
|
|
106
|
+
push(best(q, component.name, component.title, component.description), {
|
|
107
|
+
kind: "component",
|
|
108
|
+
name: component.name,
|
|
109
|
+
title: component.title,
|
|
110
|
+
description: component.description,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (options.kind === undefined || options.kind === "icon") {
|
|
115
|
+
for (const icon of registryCatalog.icons) {
|
|
116
|
+
push(best(q, icon), { kind: "icon", name: icon });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
ranked.sort((a, b) => b.score - a.score || matchKey(a.match).localeCompare(matchKey(b.match)));
|
|
120
|
+
return dedupe(ranked).slice(0, options.limit ?? 12);
|
|
121
|
+
}
|
|
122
|
+
function matchKey(match) {
|
|
123
|
+
switch (match.kind) {
|
|
124
|
+
case "model":
|
|
125
|
+
return `model:${match.id}`;
|
|
126
|
+
case "pack":
|
|
127
|
+
return `pack:${match.source}`;
|
|
128
|
+
default:
|
|
129
|
+
return `${match.kind}:${match.name}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function dedupe(ranked) {
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
const out = [];
|
|
135
|
+
for (const entry of ranked) {
|
|
136
|
+
const key = matchKey(entry.match);
|
|
137
|
+
if (seen.has(key))
|
|
138
|
+
continue;
|
|
139
|
+
seen.add(key);
|
|
140
|
+
out.push(entry);
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
/** The ranked matches for a query — models, packs, HUD components, and icons in one list. */
|
|
145
|
+
export function findAssets(query, options = {}) {
|
|
146
|
+
return rankAssets(query, options).map((entry) => entry.match);
|
|
147
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,7 @@ export { generatedIndex, generatedBySource } from "./generated/index.js";
|
|
|
6
6
|
export { readGlbDims } from "./dims.js";
|
|
7
7
|
export { buildCatalog, entryUrl, type BuildCatalogOptions } from "./catalogs/build.js";
|
|
8
8
|
export { createStarterCatalog } from "./catalogs/starter.js";
|
|
9
|
+
export { findAssets, rankAssets, type AssetKind, type AssetMatch, type FindOptions, type RankedMatch, } from "./find.js";
|
|
10
|
+
export { registryCatalog, componentInstallUrl, type RegistryCatalog, type RegistryComponent } from "./registry.js";
|
|
11
|
+
export { modelWiringSnippet, componentWiringSnippet, iconWiringSnippet, type ModelSnippetOptions } from "./snippet.js";
|
|
9
12
|
export { verifyManifest, type VerifyResult } from "./verify.js";
|
package/dist/index.js
CHANGED
|
@@ -6,4 +6,7 @@ export { generatedIndex, generatedBySource } from "./generated/index.js";
|
|
|
6
6
|
export { readGlbDims } from "./dims.js";
|
|
7
7
|
export { buildCatalog, entryUrl } from "./catalogs/build.js";
|
|
8
8
|
export { createStarterCatalog } from "./catalogs/starter.js";
|
|
9
|
+
export { findAssets, rankAssets, } from "./find.js";
|
|
10
|
+
export { registryCatalog, componentInstallUrl } from "./registry.js";
|
|
11
|
+
export { modelWiringSnippet, componentWiringSnippet, iconWiringSnippet } from "./snippet.js";
|
|
9
12
|
export { verifyManifest } from "./verify.js";
|