@series-inc/stowkit-cli 0.6.16 → 0.6.18
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/app/disk-project.d.ts +24 -1
- package/dist/app/disk-project.js +7 -0
- package/dist/app/process-cache.js +1 -1
- package/dist/app/state.d.ts +2 -1
- package/dist/app/state.js +2 -1
- package/dist/app/stowmeta-io.d.ts +6 -3
- package/dist/app/stowmeta-io.js +62 -21
- package/dist/app/thumbnail-cache.d.ts +29 -0
- package/dist/app/thumbnail-cache.js +137 -0
- package/dist/assets-package.d.ts +64 -0
- package/dist/assets-package.js +80 -0
- package/dist/cleanup.js +11 -2
- package/dist/cli.js +69 -0
- package/dist/core/constants.d.ts +4 -2
- package/dist/core/constants.js +4 -2
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.js +5 -0
- package/dist/encoders/basis-encoder.js +2 -1
- package/dist/format/metadata.js +12 -7
- package/dist/gcs.d.ts +10 -0
- package/dist/gcs.js +158 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/init.js +20 -0
- package/dist/node-fs.d.ts +4 -0
- package/dist/node-fs.js +14 -0
- package/dist/orchestrator.js +37 -10
- package/dist/pipeline.js +1 -0
- package/dist/publish.d.ts +27 -0
- package/dist/publish.js +399 -0
- package/dist/server.js +567 -20
- package/dist/store.d.ts +48 -0
- package/dist/store.js +300 -0
- package/package.json +2 -2
- package/skill.md +63 -0
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Registry } from './assets-package.js';
|
|
2
|
+
export declare function fetchRegistry(bucket?: string): Promise<Registry>;
|
|
3
|
+
export interface SearchResult {
|
|
4
|
+
stringId: string;
|
|
5
|
+
type: string;
|
|
6
|
+
packageName: string;
|
|
7
|
+
version: string;
|
|
8
|
+
file: string;
|
|
9
|
+
size: number;
|
|
10
|
+
tags: string[];
|
|
11
|
+
dependencies: string[];
|
|
12
|
+
thumbnail: boolean;
|
|
13
|
+
thumbnailFormat: 'png' | 'webp' | 'webm';
|
|
14
|
+
thumbnailUrl: string | null;
|
|
15
|
+
filtering?: 'linear' | 'nearest';
|
|
16
|
+
}
|
|
17
|
+
export interface PackageInfo {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
author: string;
|
|
21
|
+
tags: string[];
|
|
22
|
+
latest: string;
|
|
23
|
+
versions: string[];
|
|
24
|
+
assetCount: number;
|
|
25
|
+
totalSize: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function searchAssets(registry: Registry, query: string, opts?: {
|
|
28
|
+
type?: string;
|
|
29
|
+
package?: string;
|
|
30
|
+
}): SearchResult[];
|
|
31
|
+
export declare function listPackages(registry: Registry): PackageInfo[];
|
|
32
|
+
export declare function resolveAssetDeps(registry: Registry, packageName: string, stringIds: string[], version?: string): {
|
|
33
|
+
resolvedIds: string[];
|
|
34
|
+
files: string[];
|
|
35
|
+
};
|
|
36
|
+
export declare function storeSearch(query: string, opts?: {
|
|
37
|
+
type?: string;
|
|
38
|
+
json?: boolean;
|
|
39
|
+
bucket?: string;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
export declare function storeList(opts?: {
|
|
42
|
+
json?: boolean;
|
|
43
|
+
bucket?: string;
|
|
44
|
+
}): Promise<void>;
|
|
45
|
+
export declare function storeInfo(packageName: string, opts?: {
|
|
46
|
+
json?: boolean;
|
|
47
|
+
bucket?: string;
|
|
48
|
+
}): Promise<void>;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { resolveTransitiveDeps, resolveFiles } from './assets-package.js';
|
|
2
|
+
const DEFAULT_BUCKET = 'venus-shared-assets-test';
|
|
3
|
+
// ─── Fetch Registry ──────────────────────────────────────────────────────────
|
|
4
|
+
export async function fetchRegistry(bucket) {
|
|
5
|
+
const bucketName = (bucket ?? DEFAULT_BUCKET).replace(/^gs:\/\//, '').replace(/\/$/, '');
|
|
6
|
+
const url = `https://storage.googleapis.com/${bucketName}/registry.json?t=${Date.now()}`;
|
|
7
|
+
const res = await fetch(url);
|
|
8
|
+
if (!res.ok) {
|
|
9
|
+
if (res.status === 404)
|
|
10
|
+
return { schemaVersion: 1, packages: {} };
|
|
11
|
+
throw new Error(`Failed to fetch registry: ${res.status}`);
|
|
12
|
+
}
|
|
13
|
+
return res.json();
|
|
14
|
+
}
|
|
15
|
+
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
16
|
+
export function searchAssets(registry, query, opts) {
|
|
17
|
+
// Support comma-separated terms: "coral, sea, ocean" matches any term
|
|
18
|
+
const terms = query.split(',').map(t => t.toLowerCase().trim()).filter(Boolean);
|
|
19
|
+
const scored = [];
|
|
20
|
+
const bucket = DEFAULT_BUCKET;
|
|
21
|
+
// When searching within a specific package, skip package-level metadata
|
|
22
|
+
// (name, description, tags) — it matches every asset and adds noise.
|
|
23
|
+
const skipPkgMeta = !!opts?.package;
|
|
24
|
+
for (const [pkgName, pkg] of Object.entries(registry.packages)) {
|
|
25
|
+
if (opts?.package && pkgName !== opts.package)
|
|
26
|
+
continue;
|
|
27
|
+
const verStr = pkg.latest;
|
|
28
|
+
const ver = pkg.versions[verStr];
|
|
29
|
+
if (!ver)
|
|
30
|
+
continue;
|
|
31
|
+
for (const asset of ver.assets) {
|
|
32
|
+
if (opts?.type && asset.type !== opts.type)
|
|
33
|
+
continue;
|
|
34
|
+
const score = terms.length === 0
|
|
35
|
+
? 1
|
|
36
|
+
: scoreAsset(terms, asset, pkg, pkgName, skipPkgMeta);
|
|
37
|
+
if (score === 0)
|
|
38
|
+
continue;
|
|
39
|
+
scored.push({
|
|
40
|
+
score,
|
|
41
|
+
result: {
|
|
42
|
+
stringId: asset.stringId,
|
|
43
|
+
type: asset.type,
|
|
44
|
+
packageName: pkgName,
|
|
45
|
+
version: verStr,
|
|
46
|
+
file: asset.file,
|
|
47
|
+
size: asset.size,
|
|
48
|
+
tags: asset.tags,
|
|
49
|
+
dependencies: asset.dependencies,
|
|
50
|
+
thumbnail: !!asset.thumbnail,
|
|
51
|
+
thumbnailFormat: asset.thumbnailFormat ?? 'png',
|
|
52
|
+
thumbnailUrl: asset.thumbnail
|
|
53
|
+
? `https://storage.googleapis.com/${bucket}/packages/${pkgName}/${verStr}/thumbnails/${asset.stringId.split('/').map(encodeURIComponent).join('/')}.${asset.thumbnailFormat ?? 'png'}`
|
|
54
|
+
: null,
|
|
55
|
+
...(asset.filtering ? { filtering: asset.filtering } : {}),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Sort by score descending, then alphabetically by stringId
|
|
61
|
+
scored.sort((a, b) => b.score - a.score || a.result.stringId.localeCompare(b.result.stringId));
|
|
62
|
+
return scored.map(s => s.result);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Split a string into lowercase words on common boundaries:
|
|
66
|
+
* underscores, hyphens, dots, slashes, camelCase, spaces.
|
|
67
|
+
* e.g. "NinjaBoss_Attack" → ["ninja", "boss", "attack"]
|
|
68
|
+
*/
|
|
69
|
+
function splitWords(s) {
|
|
70
|
+
return s
|
|
71
|
+
// insert boundary before uppercase runs: "NinjaBoss" → "Ninja Boss"
|
|
72
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
73
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
74
|
+
.split(/[_\-./\\\s]+/)
|
|
75
|
+
.map(w => w.toLowerCase())
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Minimum per-term score for a term to count as matched.
|
|
80
|
+
* Prevents weak incidental hits (file-path substring, type contains)
|
|
81
|
+
* from surfacing assets on their own — they can only boost an
|
|
82
|
+
* already-qualifying match.
|
|
83
|
+
*/
|
|
84
|
+
const TERM_THRESHOLD = 20;
|
|
85
|
+
/**
|
|
86
|
+
* Score an asset against search terms. Returns 0 if the asset should
|
|
87
|
+
* not appear in results.
|
|
88
|
+
*
|
|
89
|
+
* Design principles:
|
|
90
|
+
* - Curated metadata (tags) scores highest after exact name matches,
|
|
91
|
+
* because someone intentionally labelled the asset.
|
|
92
|
+
* - AND semantics: every term must clear TERM_THRESHOLD independently.
|
|
93
|
+
* "coral reef" only matches assets that match BOTH "coral" AND "reef".
|
|
94
|
+
* - Weak signals (file-path word, type name) act as tiebreaker boosts
|
|
95
|
+
* but can't qualify an asset on their own.
|
|
96
|
+
*
|
|
97
|
+
* Per-term scoring (best match wins):
|
|
98
|
+
* 100 — exact stringId match
|
|
99
|
+
* 80 — exact tag match (curated, high-intent)
|
|
100
|
+
* 60 — exact word in stringId ("boss" in "actor/boss/idle")
|
|
101
|
+
* 50 — stringId starts with term
|
|
102
|
+
* 45 — word in tag starts with term ("bos" → "boss" tag)
|
|
103
|
+
* 40 — word in stringId starts with term ("att" → "attack")
|
|
104
|
+
* 30 — stringId contains term as substring
|
|
105
|
+
* 25 — tag contains term as substring
|
|
106
|
+
* 90 — exact package name match (cross-pack only)
|
|
107
|
+
* 50 — word in package name matches (cross-pack only)
|
|
108
|
+
* 45 — exact package tag match (cross-pack only)
|
|
109
|
+
* 35 — package name/tag word prefix (cross-pack only)
|
|
110
|
+
* 25 — package name/tag substring (cross-pack only)
|
|
111
|
+
* ── below TERM_THRESHOLD — boost only, can't qualify alone ──
|
|
112
|
+
* 15 — exact word in file path
|
|
113
|
+
* 12 — word in file path starts with term
|
|
114
|
+
* 10 — asset type / package description contains term
|
|
115
|
+
*/
|
|
116
|
+
function scoreAsset(terms, asset, pkg, pkgName, skipPkgMeta = false) {
|
|
117
|
+
let total = 0;
|
|
118
|
+
const sid = asset.stringId.toLowerCase();
|
|
119
|
+
const sidWords = splitWords(asset.stringId);
|
|
120
|
+
const fileWords = splitWords(asset.file);
|
|
121
|
+
for (const t of terms) {
|
|
122
|
+
let best = 0;
|
|
123
|
+
// ── Strong signals (can qualify on their own) ──────────────────
|
|
124
|
+
// stringId
|
|
125
|
+
if (sid === t)
|
|
126
|
+
best = 100;
|
|
127
|
+
else if (sidWords.some(w => w === t))
|
|
128
|
+
best = Math.max(best, 60);
|
|
129
|
+
else if (sid.startsWith(t))
|
|
130
|
+
best = Math.max(best, 50);
|
|
131
|
+
else if (sidWords.some(w => w.startsWith(t)))
|
|
132
|
+
best = Math.max(best, 40);
|
|
133
|
+
else if (sid.includes(t))
|
|
134
|
+
best = Math.max(best, 30);
|
|
135
|
+
// tags (curated — score high)
|
|
136
|
+
if (asset.tags.some(tag => tag.toLowerCase() === t))
|
|
137
|
+
best = Math.max(best, 80);
|
|
138
|
+
else if (asset.tags.some(tag => splitWords(tag).some(w => w === t)))
|
|
139
|
+
best = Math.max(best, 60);
|
|
140
|
+
else if (asset.tags.some(tag => splitWords(tag).some(w => w.startsWith(t))))
|
|
141
|
+
best = Math.max(best, 45);
|
|
142
|
+
else if (asset.tags.some(tag => tag.toLowerCase().includes(t)))
|
|
143
|
+
best = Math.max(best, 25);
|
|
144
|
+
// ── Weak signals (boost only, below threshold) ─────────────────
|
|
145
|
+
// file path (word-level only, no raw substring)
|
|
146
|
+
if (fileWords.some(w => w === t))
|
|
147
|
+
best = Math.max(best, 15);
|
|
148
|
+
else if (fileWords.some(w => w.startsWith(t)))
|
|
149
|
+
best = Math.max(best, 12);
|
|
150
|
+
// asset type
|
|
151
|
+
if (asset.type.toLowerCase().includes(t))
|
|
152
|
+
best = Math.max(best, 10);
|
|
153
|
+
// package metadata (cross-pack discovery only)
|
|
154
|
+
if (!skipPkgMeta) {
|
|
155
|
+
const pkgLower = pkgName.toLowerCase();
|
|
156
|
+
const pkgWords = splitWords(pkgName);
|
|
157
|
+
// Pack name — strongest cross-pack signal
|
|
158
|
+
if (pkgLower === t)
|
|
159
|
+
best = Math.max(best, 90);
|
|
160
|
+
else if (pkgWords.some(w => w === t))
|
|
161
|
+
best = Math.max(best, 50);
|
|
162
|
+
else if (pkgWords.some(w => w.startsWith(t)))
|
|
163
|
+
best = Math.max(best, 35);
|
|
164
|
+
else if (pkgLower.includes(t))
|
|
165
|
+
best = Math.max(best, 25);
|
|
166
|
+
// Pack tags — curated, high signal
|
|
167
|
+
if ((pkg.tags ?? []).some(tag => tag.toLowerCase() === t))
|
|
168
|
+
best = Math.max(best, 45);
|
|
169
|
+
else if ((pkg.tags ?? []).some(tag => splitWords(tag).some(w => w === t || w.startsWith(t))))
|
|
170
|
+
best = Math.max(best, 35);
|
|
171
|
+
else if ((pkg.tags ?? []).some(tag => tag.toLowerCase().includes(t)))
|
|
172
|
+
best = Math.max(best, 25);
|
|
173
|
+
// Pack description — weakest, just a boost
|
|
174
|
+
if (pkg.description.toLowerCase().includes(t))
|
|
175
|
+
best = Math.max(best, 10);
|
|
176
|
+
}
|
|
177
|
+
// AND semantics: if this term didn't clear the threshold,
|
|
178
|
+
// the asset doesn't qualify — bail out early.
|
|
179
|
+
if (best < TERM_THRESHOLD)
|
|
180
|
+
return 0;
|
|
181
|
+
total += best;
|
|
182
|
+
}
|
|
183
|
+
return total;
|
|
184
|
+
}
|
|
185
|
+
// ─── List Packages ───────────────────────────────────────────────────────────
|
|
186
|
+
export function listPackages(registry) {
|
|
187
|
+
return Object.entries(registry.packages).map(([name, pkg]) => {
|
|
188
|
+
const ver = pkg.versions[pkg.latest];
|
|
189
|
+
return {
|
|
190
|
+
name,
|
|
191
|
+
description: pkg.description,
|
|
192
|
+
author: pkg.author,
|
|
193
|
+
tags: pkg.tags ?? [],
|
|
194
|
+
latest: pkg.latest,
|
|
195
|
+
versions: Object.keys(pkg.versions),
|
|
196
|
+
assetCount: ver?.assets.length ?? 0,
|
|
197
|
+
totalSize: ver?.totalSize ?? 0,
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// ─── Resolve Dependencies ────────────────────────────────────────────────────
|
|
202
|
+
export function resolveAssetDeps(registry, packageName, stringIds, version) {
|
|
203
|
+
const pkg = registry.packages[packageName];
|
|
204
|
+
if (!pkg)
|
|
205
|
+
throw new Error(`Package "${packageName}" not found`);
|
|
206
|
+
const verStr = version ?? pkg.latest;
|
|
207
|
+
const ver = pkg.versions[verStr];
|
|
208
|
+
if (!ver)
|
|
209
|
+
throw new Error(`Version "${verStr}" not found`);
|
|
210
|
+
const resolvedIds = resolveTransitiveDeps(stringIds, ver.assets);
|
|
211
|
+
const files = resolveFiles(resolvedIds, ver.assets);
|
|
212
|
+
return { resolvedIds, files };
|
|
213
|
+
}
|
|
214
|
+
// ─── CLI Commands ────────────────────────────────────────────────────────────
|
|
215
|
+
export async function storeSearch(query, opts) {
|
|
216
|
+
const registry = await fetchRegistry(opts?.bucket);
|
|
217
|
+
const results = searchAssets(registry, query, { type: opts?.type });
|
|
218
|
+
if (opts?.json) {
|
|
219
|
+
console.log(JSON.stringify(results, null, 2));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (results.length === 0) {
|
|
223
|
+
console.log(`No assets found matching "${query}"`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
console.log(`\nFound ${results.length} asset${results.length !== 1 ? 's' : ''} matching "${query}":\n`);
|
|
227
|
+
for (const r of results) {
|
|
228
|
+
const tags = r.tags.length > 0 ? ` [${r.tags.join(', ')}]` : '';
|
|
229
|
+
const deps = r.dependencies.length > 0 ? ` → ${r.dependencies.join(', ')}` : '';
|
|
230
|
+
const thumb = r.thumbnail ? ' (has thumbnail)' : '';
|
|
231
|
+
console.log(` [${r.type}] ${r.stringId} — ${r.packageName}@${r.version}${tags}${deps}${thumb}`);
|
|
232
|
+
console.log(` ${r.file} (${formatBytes(r.size)})`);
|
|
233
|
+
}
|
|
234
|
+
console.log('');
|
|
235
|
+
}
|
|
236
|
+
export async function storeList(opts) {
|
|
237
|
+
const registry = await fetchRegistry(opts?.bucket);
|
|
238
|
+
const packages = listPackages(registry);
|
|
239
|
+
if (opts?.json) {
|
|
240
|
+
console.log(JSON.stringify(packages, null, 2));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (packages.length === 0) {
|
|
244
|
+
console.log('No packages published yet.');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
console.log(`\n${packages.length} package${packages.length !== 1 ? 's' : ''} in the asset store:\n`);
|
|
248
|
+
for (const p of packages) {
|
|
249
|
+
const tags = p.tags.length > 0 ? ` [${p.tags.join(', ')}]` : '';
|
|
250
|
+
const desc = p.description ? ` — ${p.description}` : '';
|
|
251
|
+
console.log(` ${p.name}@${p.latest}${desc}${tags}`);
|
|
252
|
+
console.log(` ${p.assetCount} assets, ${formatBytes(p.totalSize)}, ${p.versions.length} version${p.versions.length !== 1 ? 's' : ''}`);
|
|
253
|
+
}
|
|
254
|
+
console.log('');
|
|
255
|
+
}
|
|
256
|
+
export async function storeInfo(packageName, opts) {
|
|
257
|
+
const registry = await fetchRegistry(opts?.bucket);
|
|
258
|
+
const pkg = registry.packages[packageName];
|
|
259
|
+
if (!pkg) {
|
|
260
|
+
console.error(`Package "${packageName}" not found.`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
const ver = pkg.versions[pkg.latest];
|
|
264
|
+
if (opts?.json) {
|
|
265
|
+
console.log(JSON.stringify({
|
|
266
|
+
name: packageName,
|
|
267
|
+
description: pkg.description,
|
|
268
|
+
author: pkg.author,
|
|
269
|
+
tags: pkg.tags ?? [],
|
|
270
|
+
latest: pkg.latest,
|
|
271
|
+
versions: Object.keys(pkg.versions),
|
|
272
|
+
assets: ver?.assets ?? [],
|
|
273
|
+
}, null, 2));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
console.log(`\n${packageName}@${pkg.latest}`);
|
|
277
|
+
if (pkg.description)
|
|
278
|
+
console.log(` ${pkg.description}`);
|
|
279
|
+
if (pkg.author)
|
|
280
|
+
console.log(` Author: ${pkg.author}`);
|
|
281
|
+
if (pkg.tags?.length)
|
|
282
|
+
console.log(` Tags: ${pkg.tags.join(', ')}`);
|
|
283
|
+
console.log(` Versions: ${Object.keys(pkg.versions).join(', ')}`);
|
|
284
|
+
if (ver) {
|
|
285
|
+
console.log(`\nAssets (${ver.assets.length}):\n`);
|
|
286
|
+
for (const a of ver.assets) {
|
|
287
|
+
const tags = a.tags.length > 0 ? ` [${a.tags.join(', ')}]` : '';
|
|
288
|
+
const deps = a.dependencies.length > 0 ? ` → ${a.dependencies.join(', ')}` : '';
|
|
289
|
+
console.log(` [${a.type}] ${a.stringId}${tags}${deps} (${formatBytes(a.size)})`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
console.log('');
|
|
293
|
+
}
|
|
294
|
+
function formatBytes(bytes) {
|
|
295
|
+
if (bytes < 1024)
|
|
296
|
+
return `${bytes} B`;
|
|
297
|
+
if (bytes < 1024 * 1024)
|
|
298
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
299
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
300
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@series-inc/stowkit-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"stowkit": "./dist/cli.js"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@series-inc/stowkit-packer-gui": "^0.1.17",
|
|
21
|
-
"@series-inc/stowkit-editor": "^0.1.
|
|
21
|
+
"@series-inc/stowkit-editor": "^0.1.8",
|
|
22
22
|
"draco3d": "^1.5.7",
|
|
23
23
|
"fbx-parser": "^2.1.3",
|
|
24
24
|
"@strangeape/ffmpeg-audio-wasm": "^0.1.0",
|
package/skill.md
CHANGED
|
@@ -53,6 +53,9 @@ stowkit move <path> <folder> # Move an asset to a different folder (updates G
|
|
|
53
53
|
stowkit delete <path> # Delete an asset and its .stowmeta/.stowcache files
|
|
54
54
|
stowkit set-id <path> <id> # Change an asset's stringId
|
|
55
55
|
stowkit inspect <file.stow> # Show manifest of a built .stow pack (use -v for details)
|
|
56
|
+
stowkit store search <query> # Search the asset store (add --json for machine-readable output)
|
|
57
|
+
stowkit store list # List all packages in the store
|
|
58
|
+
stowkit store info <package> # Show package details and all assets
|
|
56
59
|
stowkit packer [dir] # Open the packer GUI in browser
|
|
57
60
|
stowkit editor [dir] # Open the level editor in browser
|
|
58
61
|
stowkit serve [dir] # Start API server only (no GUI)
|
|
@@ -63,6 +66,8 @@ All commands default to the current directory.
|
|
|
63
66
|
**Options:**
|
|
64
67
|
- `--force` — Ignore cache and reprocess everything
|
|
65
68
|
- `--verbose` / `-v` — Detailed output
|
|
69
|
+
- `--json` — Output store results as JSON (for programmatic/AI use)
|
|
70
|
+
- `--type <type>` — Filter store search by asset type (e.g. `staticMesh`, `texture`)
|
|
66
71
|
- `--port <number>` — Server port (default 3210)
|
|
67
72
|
- `--schema <name>` — Material schema template for `create-material` (default: `pbr`)
|
|
68
73
|
|
|
@@ -525,6 +530,59 @@ To refresh skill files without updating the CLI: `stowkit init --update`
|
|
|
525
530
|
|
|
526
531
|
**When to run this:** If you notice this skill file is missing documentation for commands that exist in `stowkit --help`, or if the user asks you to update StowKit.
|
|
527
532
|
|
|
533
|
+
## Asset Store
|
|
534
|
+
|
|
535
|
+
StowKit includes a shared asset store for publishing, searching, and importing reusable asset packs across projects. Assets are stored in a public GCS bucket with a central `registry.json` manifest.
|
|
536
|
+
|
|
537
|
+
### Searching the store
|
|
538
|
+
|
|
539
|
+
```bash
|
|
540
|
+
stowkit store search coral # Find assets matching "coral"
|
|
541
|
+
stowkit store search ocean --type texture # Find textures tagged/named "ocean"
|
|
542
|
+
stowkit store search staticMesh --json # Machine-readable JSON output
|
|
543
|
+
stowkit store list # List all published packages
|
|
544
|
+
stowkit store info package_test # Show all assets in a package
|
|
545
|
+
stowkit store info package_test --json # Full package details as JSON
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
The `--json` flag outputs structured data with `stringId`, `type`, `packageName`, `version`, `file`, `size`, `tags`, `dependencies`, `thumbnail`, and `thumbnailUrl` for each asset. This is the recommended format for AI agents to consume.
|
|
549
|
+
|
|
550
|
+
### JSON search result format
|
|
551
|
+
|
|
552
|
+
```json
|
|
553
|
+
[
|
|
554
|
+
{
|
|
555
|
+
"stringId": "coral_1",
|
|
556
|
+
"type": "staticMesh",
|
|
557
|
+
"packageName": "ocean_pack",
|
|
558
|
+
"version": "1.0.0",
|
|
559
|
+
"file": "Meshes/Coral_1.fbx",
|
|
560
|
+
"size": 59952,
|
|
561
|
+
"tags": ["coral", "environment"],
|
|
562
|
+
"dependencies": ["M_Sea_Floor"],
|
|
563
|
+
"thumbnail": true,
|
|
564
|
+
"thumbnailUrl": "https://storage.googleapis.com/venus-shared-assets-test/packages/ocean_pack/1.0.0/thumbnails/coral_1.png"
|
|
565
|
+
}
|
|
566
|
+
]
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Dependency resolution
|
|
570
|
+
|
|
571
|
+
Assets have dependency chains: meshes depend on materials, materials depend on textures. When importing an asset, all transitive dependencies are automatically resolved and downloaded. If two assets share the same dependency, it's only downloaded once.
|
|
572
|
+
|
|
573
|
+
### Importing from the store
|
|
574
|
+
|
|
575
|
+
The packer GUI has a **Store** button to browse, search, and import assets. Selected assets and their transitive dependencies are downloaded directly into the project's `srcArtDir`.
|
|
576
|
+
|
|
577
|
+
### Thumbnail URLs
|
|
578
|
+
|
|
579
|
+
Published assets may have thumbnails at:
|
|
580
|
+
```
|
|
581
|
+
https://storage.googleapis.com/{bucket}/packages/{packageName}/{version}/thumbnails/{stringId}.png
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Check the `thumbnail` boolean in search results to know if a thumbnail exists.
|
|
585
|
+
|
|
528
586
|
## Common Tasks for AI Agents
|
|
529
587
|
|
|
530
588
|
### Adding a GLB model (recommended 3D workflow)
|
|
@@ -590,3 +648,8 @@ This is useful for verifying build output, checking which assets ended up in whi
|
|
|
590
648
|
- **Clean orphaned files:** `stowkit clean`
|
|
591
649
|
- **Set up with engine:** `stowkit init --with-engine` (installs `@series-inc/rundot-3d-engine` + `three`)
|
|
592
650
|
- **Update CLI + skill files:** `stowkit update`
|
|
651
|
+
- **Search the asset store:** `stowkit store search sword --json` (returns JSON array of matching assets with thumbnailUrls)
|
|
652
|
+
- **Find all meshes in the store:** `stowkit store search mesh --type staticMesh --json`
|
|
653
|
+
- **List all published packages:** `stowkit store list --json`
|
|
654
|
+
- **Get package details:** `stowkit store info ocean_pack --json`
|
|
655
|
+
- **Show a thumbnail to the user:** Use the `thumbnailUrl` from search results — it's a public PNG URL
|