@jgengine/assets 0.7.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 +63 -0
- package/README.md +45 -5
- package/dist/cli/paths.d.ts +4 -0
- package/dist/cli/paths.js +11 -0
- package/dist/cli/pull.d.ts +7 -1
- package/dist/cli/pull.js +175 -48
- package/dist/download.d.ts +45 -2
- package/dist/download.js +93 -4
- 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/manifest.d.ts +2 -0
- 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 +997 -0
- package/package.json +5 -3
package/dist/download.js
CHANGED
|
@@ -26,11 +26,11 @@ function score(candidate) {
|
|
|
26
26
|
value += 1;
|
|
27
27
|
return value;
|
|
28
28
|
}
|
|
29
|
-
export async function resolveArchiveUrl(source) {
|
|
29
|
+
export async function resolveArchiveUrl(source, fetchImpl = fetch) {
|
|
30
30
|
const download = source.download;
|
|
31
31
|
if (!isScrapeDownload(download))
|
|
32
32
|
return download.url;
|
|
33
|
-
const response = await
|
|
33
|
+
const response = await fetchImpl(download.scrape, { redirect: "follow" });
|
|
34
34
|
if (!response.ok)
|
|
35
35
|
throw new Error(`scrape ${download.scrape} -> HTTP ${response.status}`);
|
|
36
36
|
const html = await response.text();
|
|
@@ -40,12 +40,101 @@ export async function resolveArchiveUrl(source) {
|
|
|
40
40
|
}
|
|
41
41
|
return url;
|
|
42
42
|
}
|
|
43
|
-
export async function downloadArchive(url) {
|
|
44
|
-
const response = await
|
|
43
|
+
export async function downloadArchive(url, fetchImpl = fetch) {
|
|
44
|
+
const response = await fetchImpl(url, { redirect: "follow" });
|
|
45
45
|
if (!response.ok)
|
|
46
46
|
throw new Error(`download ${url} -> HTTP ${response.status}`);
|
|
47
47
|
return new Uint8Array(await response.arrayBuffer());
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Layout for the `--mirror` / `JGENGINE_ASSETS_MIRROR` base URL override: the
|
|
51
|
+
* archive for a pack is expected at `<baseUrl>/<provider>/<packId>.zip`, e.g.
|
|
52
|
+
* `https://my-mirror.example.com/kenney/kenney-nature.zip`.
|
|
53
|
+
*/
|
|
54
|
+
export function mirrorOverrideUrl(baseUrl, source) {
|
|
55
|
+
const base = baseUrl.replace(/\/+$/, "");
|
|
56
|
+
return `${base}/${source.provider}/${source.id}.zip`;
|
|
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
|
+
}
|
|
73
|
+
async function verifyPinnedSha(source, archive) {
|
|
74
|
+
const download = source.download;
|
|
75
|
+
if (isScrapeDownload(download) || download.sha256 === undefined)
|
|
76
|
+
return;
|
|
77
|
+
const actual = await sha256Hex(archive);
|
|
78
|
+
if (actual !== download.sha256) {
|
|
79
|
+
throw new Error(`sha256 mismatch: expected ${download.sha256}, got ${actual}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolves and downloads a pack's archive, trying sources in order until one
|
|
84
|
+
* succeeds: (1) the mirror base override at `mirrorOverrideUrl`, (2) the
|
|
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
|
|
89
|
+
* source supplied the bytes; a mismatch is treated as a failed attempt so the
|
|
90
|
+
* next source in the chain is tried. Throws with every attempted URL and its
|
|
91
|
+
* failure reason when all sources fail.
|
|
92
|
+
*/
|
|
93
|
+
export async function downloadPackArchive(source, options = {}) {
|
|
94
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
95
|
+
const attempted = [];
|
|
96
|
+
const errors = [];
|
|
97
|
+
const tryUrl = async (url) => {
|
|
98
|
+
attempted.push(url);
|
|
99
|
+
try {
|
|
100
|
+
const archive = await downloadArchive(url, fetchImpl);
|
|
101
|
+
await verifyPinnedSha(source, archive);
|
|
102
|
+
return { archive, url, attempted };
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
errors.push(`${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
if (options.mirrorBase !== undefined) {
|
|
110
|
+
const result = await tryUrl(mirrorOverrideUrl(options.mirrorBase, source));
|
|
111
|
+
if (result !== undefined)
|
|
112
|
+
return result;
|
|
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
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const primaryUrl = await resolveArchiveUrl(source, fetchImpl);
|
|
121
|
+
const result = await tryUrl(primaryUrl);
|
|
122
|
+
if (result !== undefined)
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
const label = isScrapeDownload(source.download) ? source.download.scrape : source.download.url;
|
|
127
|
+
errors.push(`${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
128
|
+
attempted.push(label);
|
|
129
|
+
}
|
|
130
|
+
if (source.mirror !== undefined) {
|
|
131
|
+
const result = await tryUrl(source.mirror);
|
|
132
|
+
if (result !== undefined)
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`failed to download ${source.id} from all ${attempted.length} source(s):\n` +
|
|
136
|
+
errors.map((line) => ` - ${line}`).join("\n"));
|
|
137
|
+
}
|
|
49
138
|
export function extractGlbs(archive) {
|
|
50
139
|
const entries = unzipSync(archive, {
|
|
51
140
|
filter: (file) => /\.glb$/i.test(file.name),
|
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";
|
package/dist/indexGen.js
CHANGED
|
@@ -49,7 +49,7 @@ export function indexSourceDir(source, dir) {
|
|
|
49
49
|
function safeIdentifier(sourceId) {
|
|
50
50
|
return `s_${sourceId.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
51
51
|
}
|
|
52
|
-
function
|
|
52
|
+
function renderTsBarrel(sourceIds) {
|
|
53
53
|
const ordered = [...sourceIds].sort((a, b) => a.localeCompare(b));
|
|
54
54
|
const imports = ordered
|
|
55
55
|
.map((id) => `import ${safeIdentifier(id)} from "./${id}.json" with { type: "json" };`)
|
|
@@ -68,6 +68,22 @@ ${table}
|
|
|
68
68
|
export const generatedIndex: IndexEntry[] = Object.values(generatedBySource).flat();
|
|
69
69
|
`;
|
|
70
70
|
}
|
|
71
|
+
function renderJsBarrel(sourceIds) {
|
|
72
|
+
const ordered = [...sourceIds].sort((a, b) => a.localeCompare(b));
|
|
73
|
+
const imports = ordered
|
|
74
|
+
.map((id) => `import ${safeIdentifier(id)} from "./${id}.json" with { type: "json" };`)
|
|
75
|
+
.join("\n");
|
|
76
|
+
const table = ordered.map((id) => ` ${JSON.stringify(id)}: ${safeIdentifier(id)},`).join("\n");
|
|
77
|
+
return `// AUTO-GENERATED by \`assets reindex\`. Do not edit by hand.
|
|
78
|
+
${imports}
|
|
79
|
+
|
|
80
|
+
export const generatedBySource = {
|
|
81
|
+
${table}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const generatedIndex = Object.values(generatedBySource).flat();
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
71
87
|
export function reindex(modelsDir, outDir) {
|
|
72
88
|
mkdirSync(outDir, { recursive: true });
|
|
73
89
|
const perSource = [];
|
|
@@ -86,6 +102,11 @@ export function reindex(modelsDir, outDir) {
|
|
|
86
102
|
sourceIds.push(sourceId);
|
|
87
103
|
perSource.push({ source: sourceId, count: entries.length });
|
|
88
104
|
}
|
|
89
|
-
writeFileSync(join(outDir, "index.ts"),
|
|
105
|
+
writeFileSync(join(outDir, "index.ts"), renderTsBarrel(sourceIds));
|
|
106
|
+
// Published installs only ship dist/; write a JS barrel there so buildCatalog can re-import after reindex.
|
|
107
|
+
// Source-tree reindex keeps the TypeScript barrel that the package build compiles.
|
|
108
|
+
if (!/[/\\]src[/\\]generated$/i.test(outDir)) {
|
|
109
|
+
writeFileSync(join(outDir, "index.js"), renderJsBarrel(sourceIds));
|
|
110
|
+
}
|
|
90
111
|
return { perSource, total: perSource.reduce((sum, entry) => sum + entry.count, 0) };
|
|
91
112
|
}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface AssetSource {
|
|
|
19
19
|
categories: readonly string[];
|
|
20
20
|
download: AssetDownload;
|
|
21
21
|
homepage?: string;
|
|
22
|
+
/** Direct archive URL tried as a last resort when the primary provider path fails; see `downloadPackArchive`. */
|
|
23
|
+
mirror?: string;
|
|
22
24
|
}
|
|
23
25
|
export interface IndexEntry {
|
|
24
26
|
id: string;
|