@series-inc/stowkit-cli 0.6.23 → 0.6.35

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/init.js CHANGED
@@ -95,6 +95,7 @@ export async function initProject(projectDir, opts) {
95
95
  const stowkitIgnores = [
96
96
  '# StowKit',
97
97
  '*.stowcache',
98
+ '.stow-thumbnails/',
98
99
  'public/cdn-assets/',
99
100
  'Open Packer.bat',
100
101
  'open-packer.sh',
@@ -104,6 +105,16 @@ export async function initProject(projectDir, opts) {
104
105
  if (!existing.includes('*.stowcache')) {
105
106
  await fs.writeFile(gitignorePath, existing.trimEnd() + '\n\n' + stowkitIgnores + '\n');
106
107
  }
108
+ else {
109
+ // Existing project — ensure newer ignore entries are present
110
+ let updated = existing;
111
+ if (!existing.includes('.stow-thumbnails/')) {
112
+ updated = updated.trimEnd() + '\n.stow-thumbnails/\n';
113
+ }
114
+ if (updated !== existing) {
115
+ await fs.writeFile(gitignorePath, updated);
116
+ }
117
+ }
107
118
  }
108
119
  catch {
109
120
  await fs.writeFile(gitignorePath, stowkitIgnores + '\n');
@@ -118,35 +129,135 @@ export async function initProject(projectDir, opts) {
118
129
  console.log(` Config: .felicityproject`);
119
130
  console.log(` AI skills: .claude/skills/stowkit/SKILL.md, .cursor/rules/stowkit.mdc`);
120
131
  console.log(` Launcher: Open Packer.bat (Windows) / open-packer.sh (macOS/Linux)`);
132
+ // Add engine-specific gitignore entries
133
+ if (withEngine) {
134
+ const engineIgnores = [
135
+ '# Engine (generated by postinstall)',
136
+ 'node_modules/',
137
+ 'dist/',
138
+ 'public/basis/',
139
+ 'public/stowkit/',
140
+ 'public/stowkit_reader.wasm',
141
+ ].join('\n');
142
+ try {
143
+ const existing = await fs.readFile(gitignorePath, 'utf-8');
144
+ if (!existing.includes('public/stowkit_reader.wasm')) {
145
+ await fs.writeFile(gitignorePath, existing.trimEnd() + '\n\n' + engineIgnores + '\n');
146
+ }
147
+ }
148
+ catch {
149
+ // gitignore should exist from earlier step, but handle gracefully
150
+ }
151
+ }
121
152
  // Install engine if selected
122
153
  if (withEngine) {
123
154
  await installEngine(absDir);
124
155
  }
125
156
  console.log('');
126
- console.log('Drop your assets (PNG, JPG, FBX, WAV, etc.) into assets/');
127
- console.log('Then run: stowkit build');
128
- console.log('Or double-click "Open Packer.bat" to launch the packer GUI.');
157
+ if (withEngine) {
158
+ console.log('Ready to go! Run:');
159
+ console.log(' npm run dev');
160
+ }
161
+ else {
162
+ console.log('Drop your assets (PNG, JPG, FBX, WAV, etc.) into assets/');
163
+ console.log('Then run: stowkit build');
164
+ console.log('Or double-click "Open Packer.bat" to launch the packer GUI.');
165
+ }
166
+ }
167
+ async function copyTemplateFiles(templateDir, targetDir) {
168
+ const copied = [];
169
+ const entries = await fs.readdir(templateDir, { withFileTypes: true });
170
+ for (const entry of entries) {
171
+ const src = path.join(templateDir, entry.name);
172
+ const dest = path.join(targetDir, entry.name);
173
+ if (entry.isDirectory()) {
174
+ const sub = await copyTemplateFiles(src, dest);
175
+ copied.push(...sub);
176
+ }
177
+ else {
178
+ // Skip package.json — handled separately via merge logic
179
+ if (entry.name === 'package.json')
180
+ continue;
181
+ try {
182
+ await fs.access(dest);
183
+ // File exists — don't clobber
184
+ }
185
+ catch {
186
+ await fs.mkdir(path.dirname(dest), { recursive: true });
187
+ await fs.copyFile(src, dest);
188
+ copied.push(path.relative(targetDir, dest));
189
+ }
190
+ }
191
+ }
192
+ return copied;
193
+ }
194
+ async function mergePackageJson(templatePkgPath, targetDir) {
195
+ const templatePkg = JSON.parse(await fs.readFile(templatePkgPath, 'utf-8'));
196
+ const targetPkgPath = path.join(targetDir, 'package.json');
197
+ let targetPkg;
198
+ try {
199
+ targetPkg = JSON.parse(await fs.readFile(targetPkgPath, 'utf-8'));
200
+ }
201
+ catch {
202
+ // No existing package.json — use template as-is
203
+ templatePkg.name = path.basename(targetDir);
204
+ await fs.writeFile(targetPkgPath, JSON.stringify(templatePkg, null, 2) + '\n');
205
+ return;
206
+ }
207
+ // Merge scripts (don't overwrite existing scripts)
208
+ targetPkg.scripts = { ...templatePkg.scripts, ...targetPkg.scripts };
209
+ // Ensure postinstall includes copy-decoders
210
+ if (!targetPkg.scripts.postinstall?.includes('copy-decoders')) {
211
+ targetPkg.scripts.postinstall = templatePkg.scripts.postinstall;
212
+ }
213
+ // Merge dependencies
214
+ for (const depType of ['dependencies', 'devDependencies', 'peerDependencies']) {
215
+ if (templatePkg[depType]) {
216
+ targetPkg[depType] = { ...templatePkg[depType], ...(targetPkg[depType] ?? {}) };
217
+ }
218
+ }
219
+ // Ensure type: module
220
+ if (!targetPkg.type)
221
+ targetPkg.type = 'module';
222
+ await fs.writeFile(targetPkgPath, JSON.stringify(targetPkg, null, 2) + '\n');
129
223
  }
130
224
  async function installEngine(absDir) {
225
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
226
+ const templateDir = path.resolve(thisDir, '../templates/engine');
227
+ // Verify template directory exists
228
+ try {
229
+ await fs.access(templateDir);
230
+ }
231
+ catch {
232
+ console.error('Engine template not found. Please update @series-inc/stowkit-cli.');
233
+ return;
234
+ }
131
235
  console.log('');
132
- console.log('Installing @series-inc/rundot-3d-engine and three...');
236
+ console.log('Setting up 3D engine project...');
237
+ // Copy template files (skip existing, skip package.json)
238
+ const copied = await copyTemplateFiles(templateDir, absDir);
239
+ if (copied.length > 0) {
240
+ console.log(` Created: ${copied.join(', ')}`);
241
+ }
242
+ // Merge or create package.json
243
+ await mergePackageJson(path.join(templateDir, 'package.json'), absDir);
244
+ // Install dependencies
245
+ console.log(' Installing dependencies...');
133
246
  const { execSync } = await import('node:child_process');
134
247
  try {
135
- execSync('npm install @series-inc/rundot-3d-engine three', {
248
+ execSync('npm install', {
136
249
  cwd: absDir,
137
250
  stdio: 'inherit',
138
251
  });
139
252
  }
140
253
  catch {
141
- console.error('Failed to install engine packages. You can install manually:');
142
- console.error(' npm install @series-inc/rundot-3d-engine three');
254
+ console.error('Failed to install dependencies. Run `npm install` manually.');
143
255
  return;
144
256
  }
257
+ // Copy engine skill files
145
258
  await copyEngineSkillFiles(absDir);
146
259
  console.log('');
147
- console.log(' 3D Engine installed:');
148
- console.log(' @series-inc/rundot-3d-engine');
149
- console.log(' three');
260
+ console.log(' 3D Engine project ready!');
150
261
  console.log(' AI skills: .claude/skills/stowkit-engine/SKILL.md, .cursor/rules/stowkit-engine.mdc');
151
262
  }
152
263
  async function copySkillFiles(absDir) {
package/dist/publish.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as fs from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { readProjectConfig, scanDirectory } from './node-fs.js';
4
- import { readAssetsPackage, initAssetsPackage, createEmptyRegistry, } from './assets-package.js';
4
+ import { readAssetsPackage, initAssetsPackage, } from './assets-package.js';
5
+ import { createFirestoreClient } from './firestore.js';
5
6
  import { createGCSClient } from './gcs.js';
6
7
  import { readStowmeta } from './app/stowmeta-io.js';
7
8
  // ─── Build full dependency graph from srcArtDir ──────────────────────────────
@@ -145,7 +146,7 @@ export async function publishPackage(projectDir, opts = {}) {
145
146
  const bucketUri = opts.bucket
146
147
  ?? pkg.bucket
147
148
  ?? process.env.STOWKIT_BUCKET
148
- ?? 'gs://venus-shared-assets-test';
149
+ ?? 'gs://rungame-shared-assets-test';
149
150
  log(`Bucket: ${bucketUri}`);
150
151
  // Step 4: Scan srcArtDir
151
152
  const scan = await scanDirectory(project.srcArtDir);
@@ -252,29 +253,15 @@ export async function publishPackage(projectDir, opts = {}) {
252
253
  }
253
254
  // Step 7: Create GCS client and auth
254
255
  const emitProgress = opts.onProgress ?? (() => { });
255
- const totalUploads = filesToUpload.length + Object.keys(thumbMap).length + 2; // +2 for assets-package.json and registry.json
256
+ const totalUploads = filesToUpload.length + Object.keys(thumbMap).length + 2; // +2 for assets-package.json and Firestore writes
256
257
  let totalDone = 0;
257
258
  log('Authenticating with GCS...');
258
259
  emitProgress({ phase: 'auth', done: 0, total: totalUploads, message: 'Authenticating with GCS...' });
259
260
  const gcs = await createGCSClient(projectDir, bucketUri);
260
- // Step 8: Download current registry
261
- log('Fetching registry...');
262
- const registryResult = await gcs.downloadWithGeneration('registry.json');
263
- let registry;
264
- let generation;
265
- if (registryResult) {
266
- registry = JSON.parse(registryResult.data);
267
- generation = registryResult.generation;
268
- vlog(`Registry loaded (generation: ${generation})`);
269
- }
270
- else {
271
- registry = createEmptyRegistry();
272
- generation = null;
273
- vlog('No existing registry — will create new one');
274
- }
275
- // Step 9: Check version not already published
276
- const existingPkg = registry.packages[pkg.name];
277
- if (existingPkg?.versions[pkg.version] && !opts.force) {
261
+ // Step 8: Create Firestore client and check version
262
+ const firestore = await createFirestoreClient(projectDir);
263
+ const existingVer = await firestore.getVersion(pkg.name, pkg.version);
264
+ if (existingVer && !opts.force) {
278
265
  throw new Error(`Version ${pkg.version} of "${pkg.name}" is already published. ` +
279
266
  `Bump the version in assets-package.json or use --force to overwrite.`);
280
267
  }
@@ -338,27 +325,17 @@ export async function publishPackage(projectDir, opts = {}) {
338
325
  }
339
326
  catch { /* file doesn't exist, try next */ }
340
327
  }
341
- // Step 13: Update and upload registry
328
+ // Step 13: Write to Firestore
342
329
  log('Updating registry...');
343
- if (!registry.packages[pkg.name]) {
344
- registry.packages[pkg.name] = {
345
- description: pkg.description,
346
- author: pkg.author,
347
- tags: pkg.tags ?? [],
348
- latest: pkg.version,
349
- versions: {},
350
- };
351
- }
352
- const regPkg = registry.packages[pkg.name];
353
- regPkg.versions[pkg.version] = versionEntry;
354
- regPkg.latest = pkg.version;
355
- regPkg.description = pkg.description;
356
- regPkg.author = pkg.author;
357
- regPkg.tags = pkg.tags ?? [];
358
- if (packThumbnail)
359
- regPkg.thumbnail = packThumbnail;
360
330
  emitProgress({ phase: 'registry', done: totalDone, total: totalUploads, message: 'Updating registry...' });
361
- await gcs.uploadWithGeneration('registry.json', JSON.stringify(registry, null, 2), generation);
331
+ await firestore.setVersion(pkg.name, pkg.version, versionEntry);
332
+ await firestore.setPackage(pkg.name, {
333
+ description: pkg.description,
334
+ author: pkg.author,
335
+ tags: pkg.tags ?? [],
336
+ latest: pkg.version,
337
+ thumbnail: packThumbnail ?? null,
338
+ });
362
339
  totalDone++;
363
340
  emitProgress({ phase: 'registry', done: totalDone, total: totalUploads, message: 'Done' });
364
341
  log(`\nPublished ${pkg.name}@${pkg.version} successfully!`);
package/dist/server.js CHANGED
@@ -55,6 +55,8 @@ function broadcast(msg) {
55
55
  ws.send(data);
56
56
  }
57
57
  }
58
+ /** Tracks the most recent fire-and-forget processing promise so wait=true can join it. */
59
+ let activeProcessingTask = null;
58
60
  /**
59
61
  * Single entry point for all processing orchestration.
60
62
  * - Skips materials (they don't need processing)
@@ -136,10 +138,15 @@ function queueProcessing(opts = {}) {
136
138
  }
137
139
  broadcast({ type: 'processing-complete' });
138
140
  })();
141
+ // Track the active task so wait=true callers can join in-flight processing
142
+ activeProcessingTask = task.finally(() => {
143
+ if (activeProcessingTask === task)
144
+ activeProcessingTask = null;
145
+ });
139
146
  if (opts.await)
140
- return task;
147
+ return activeProcessingTask;
141
148
  // Fire-and-forget: log errors but don't block caller
142
- task.catch(err => console.error('[server] queueProcessing error:', err));
149
+ activeProcessingTask.catch(err => console.error('[server] queueProcessing error:', err));
143
150
  return Promise.resolve();
144
151
  }
145
152
  // ─── Helpers ────────────────────────────────────────────────────────────────
@@ -1028,10 +1035,16 @@ async function handleRequest(req, res, staticApps) {
1028
1035
  json(res, { error: 'No project open' }, 400);
1029
1036
  return;
1030
1037
  }
1031
- // If wait=true, process all pending assets synchronously before building
1038
+ // If wait=true, ensure all asset processing completes before building.
1039
+ // First join any in-flight processing, then start a new run for any remaining pending assets.
1032
1040
  const body = await readBody(req).then(b => b ? JSON.parse(b) : {}).catch(() => ({}));
1033
1041
  const wait = body.wait === true || url.searchParams.get('wait') === 'true';
1034
1042
  if (wait) {
1043
+ // Await any already-running background processing
1044
+ if (activeProcessingTask) {
1045
+ await activeProcessingTask;
1046
+ }
1047
+ // Pick up any assets that became pending after the previous run
1035
1048
  const pending = assets.filter(a => a.status === 'pending' || a.status === 'processing');
1036
1049
  if (pending.length > 0) {
1037
1050
  await queueProcessing({ await: true });
@@ -1767,21 +1780,28 @@ async function handleRequest(req, res, staticApps) {
1767
1780
  // GET /api/asset-store/registry — fetch the public registry
1768
1781
  if (pathname === '/api/asset-store/registry' && req.method === 'GET') {
1769
1782
  try {
1770
- const bucketParam = url.searchParams.get('bucket') ?? 'venus-shared-assets-test';
1771
- const bucketName = bucketParam.replace(/^gs:\/\//, '').replace(/\/$/, '');
1772
- const registryUrl = `https://storage.googleapis.com/${bucketName}/registry.json?t=${Date.now()}`;
1773
- const fetchRes = await fetch(registryUrl);
1774
- if (!fetchRes.ok) {
1775
- if (fetchRes.status === 404) {
1776
- json(res, { schemaVersion: 1, packages: {} });
1783
+ const { createFirestoreReader } = await import('./firestore.js');
1784
+ const reader = createFirestoreReader();
1785
+ const allPackages = await reader.listPackages();
1786
+ const packages = {};
1787
+ for (const { name, data: pkg } of allPackages) {
1788
+ const versionKeys = await reader.listVersionKeys(name);
1789
+ const versions = {};
1790
+ for (const ver of versionKeys) {
1791
+ const versionDoc = await reader.getVersion(name, ver);
1792
+ if (versionDoc)
1793
+ versions[ver] = versionDoc;
1777
1794
  }
1778
- else {
1779
- json(res, { error: `Failed to fetch registry: ${fetchRes.status}` }, 502);
1780
- }
1781
- return;
1795
+ packages[name] = {
1796
+ description: pkg.description,
1797
+ author: pkg.author,
1798
+ tags: pkg.tags ?? [],
1799
+ latest: pkg.latest,
1800
+ thumbnail: pkg.thumbnail,
1801
+ versions,
1802
+ };
1782
1803
  }
1783
- const registry = await fetchRes.json();
1784
- json(res, registry);
1804
+ json(res, { schemaVersion: 1, packages });
1785
1805
  }
1786
1806
  catch (err) {
1787
1807
  json(res, { error: err.message }, 500);
@@ -1797,9 +1817,11 @@ async function handleRequest(req, res, staticApps) {
1797
1817
  const bucketParam = url.searchParams.get('bucket') ?? undefined;
1798
1818
  const limitParam = url.searchParams.get('limit');
1799
1819
  const limit = limitParam ? parseInt(limitParam, 10) : undefined;
1800
- const { fetchRegistry, searchAssets } = await import('./store.js');
1801
- const registry = await fetchRegistry(bucketParam);
1802
- let results = searchAssets(registry, query, { type, package: pkg });
1820
+ const { createFirestoreReader } = await import('./firestore.js');
1821
+ const { searchAssets } = await import('./store.js');
1822
+ const reader = createFirestoreReader();
1823
+ const bucket = (bucketParam ?? 'rungame-shared-assets-test').replace(/^gs:\/\//, '').replace(/\/$/, '');
1824
+ let results = await searchAssets(reader, bucket, query, { type, package: pkg });
1803
1825
  if (limit && limit > 0)
1804
1826
  results = results.slice(0, limit);
1805
1827
  json(res, results);
@@ -1813,9 +1835,11 @@ async function handleRequest(req, res, staticApps) {
1813
1835
  if (pathname === '/api/asset-store/packages' && req.method === 'GET') {
1814
1836
  try {
1815
1837
  const bucketParam = url.searchParams.get('bucket') ?? undefined;
1816
- const { fetchRegistry, listPackages } = await import('./store.js');
1817
- const registry = await fetchRegistry(bucketParam);
1818
- const packages = listPackages(registry);
1838
+ const { createFirestoreReader } = await import('./firestore.js');
1839
+ const { listStorePackages } = await import('./store.js');
1840
+ const reader = createFirestoreReader();
1841
+ const bucket = (bucketParam ?? 'rungame-shared-assets-test').replace(/^gs:\/\//, '').replace(/\/$/, '');
1842
+ const packages = await listStorePackages(reader, bucket);
1819
1843
  json(res, packages);
1820
1844
  }
1821
1845
  catch (err) {
@@ -1827,22 +1851,22 @@ async function handleRequest(req, res, staticApps) {
1827
1851
  if (pathname.startsWith('/api/asset-store/package/') && req.method === 'GET') {
1828
1852
  try {
1829
1853
  const packageName = decodeURIComponent(pathname.slice('/api/asset-store/package/'.length));
1830
- const bucketParam = url.searchParams.get('bucket') ?? undefined;
1831
- const { fetchRegistry } = await import('./store.js');
1832
- const registry = await fetchRegistry(bucketParam);
1833
- const pkg = registry.packages[packageName];
1854
+ const { createFirestoreReader } = await import('./firestore.js');
1855
+ const reader = createFirestoreReader();
1856
+ const pkg = await reader.getPackage(packageName);
1834
1857
  if (!pkg) {
1835
1858
  json(res, { error: `Package "${packageName}" not found` }, 404);
1836
1859
  return;
1837
1860
  }
1838
- const ver = pkg.versions[pkg.latest];
1861
+ const ver = await reader.getVersion(packageName, pkg.latest);
1862
+ const versionKeys = await reader.listVersionKeys(packageName);
1839
1863
  json(res, {
1840
1864
  name: packageName,
1841
1865
  description: pkg.description,
1842
1866
  author: pkg.author,
1843
1867
  tags: pkg.tags ?? [],
1844
1868
  latest: pkg.latest,
1845
- versions: Object.keys(pkg.versions),
1869
+ versions: versionKeys,
1846
1870
  assets: ver?.assets ?? [],
1847
1871
  });
1848
1872
  }
@@ -1864,21 +1888,17 @@ async function handleRequest(req, res, staticApps) {
1864
1888
  json(res, { error: 'Missing packageName, version, or stringIds' }, 400);
1865
1889
  return;
1866
1890
  }
1867
- const bucketName = (bucketParam ?? 'venus-shared-assets-test').replace(/^gs:\/\//, '').replace(/\/$/, '');
1891
+ const bucketName = (bucketParam ?? 'rungame-shared-assets-test').replace(/^gs:\/\//, '').replace(/\/$/, '');
1868
1892
  const baseUrl = `https://storage.googleapis.com/${bucketName}`;
1869
- // Fetch registry to resolve deps
1870
- const regRes = await fetch(`${baseUrl}/registry.json`);
1871
- if (!regRes.ok) {
1872
- json(res, { error: 'Could not fetch registry' }, 502);
1873
- return;
1874
- }
1875
- const registry = await regRes.json();
1876
- const pkg = registry.packages[packageName];
1893
+ // Resolve deps via Firestore
1894
+ const { createFirestoreReader } = await import('./firestore.js');
1895
+ const reader = createFirestoreReader();
1896
+ const pkg = await reader.getPackage(packageName);
1877
1897
  if (!pkg) {
1878
1898
  json(res, { error: `Package "${packageName}" not found` }, 404);
1879
1899
  return;
1880
1900
  }
1881
- const ver = pkg.versions[version];
1901
+ const ver = await reader.getVersion(packageName, version);
1882
1902
  if (!ver) {
1883
1903
  json(res, { error: `Version "${version}" not found` }, 404);
1884
1904
  return;
package/dist/store.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import type { Registry } from './assets-package.js';
2
- export declare function fetchRegistry(bucket?: string): Promise<Registry>;
1
+ import type { FirestoreReader } from './firestore.js';
3
2
  export interface SearchResult {
4
3
  stringId: string;
5
4
  type: string;
@@ -27,15 +26,15 @@ export interface PackageInfo {
27
26
  /** Full URL to pack-level thumbnail, if uploaded */
28
27
  thumbnailUrl?: string;
29
28
  }
30
- export declare function searchAssets(registry: Registry, query: string, opts?: {
29
+ export declare function searchAssets(firestore: FirestoreReader, bucket: string, query: string, opts?: {
31
30
  type?: string;
32
31
  package?: string;
33
- }): SearchResult[];
34
- export declare function listPackages(registry: Registry): PackageInfo[];
35
- export declare function resolveAssetDeps(registry: Registry, packageName: string, stringIds: string[], version?: string): {
32
+ }): Promise<SearchResult[]>;
33
+ export declare function listStorePackages(firestore: FirestoreReader, bucket: string): Promise<PackageInfo[]>;
34
+ export declare function resolveAssetDeps(firestore: FirestoreReader, packageName: string, stringIds: string[], version?: string): Promise<{
36
35
  resolvedIds: string[];
37
36
  files: string[];
38
- };
37
+ }>;
39
38
  export declare function storeSearch(query: string, opts?: {
40
39
  type?: string;
41
40
  json?: boolean;
package/dist/store.js CHANGED
@@ -1,31 +1,21 @@
1
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
- }
2
+ import { createFirestoreReader } from './firestore.js';
3
+ const DEFAULT_BUCKET = 'rungame-shared-assets-test';
15
4
  // ─── Search ──────────────────────────────────────────────────────────────────
16
- export function searchAssets(registry, query, opts) {
5
+ export async function searchAssets(firestore, bucket, query, opts) {
17
6
  // Support comma-separated terms: "coral, sea, ocean" matches any term
18
7
  const terms = query.split(',').map(t => t.toLowerCase().trim()).filter(Boolean);
19
8
  const scored = [];
20
- const bucket = DEFAULT_BUCKET;
21
9
  // When searching within a specific package, skip package-level metadata
22
10
  // (name, description, tags) — it matches every asset and adds noise.
23
11
  const skipPkgMeta = !!opts?.package;
24
- for (const [pkgName, pkg] of Object.entries(registry.packages)) {
25
- if (opts?.package && pkgName !== opts.package)
26
- continue;
12
+ let packages = await firestore.listPackages();
13
+ if (opts?.package) {
14
+ packages = packages.filter(p => p.name === opts.package);
15
+ }
16
+ for (const { name: pkgName, data: pkg } of packages) {
27
17
  const verStr = pkg.latest;
28
- const ver = pkg.versions[verStr];
18
+ const ver = await firestore.getVersion(pkgName, verStr);
29
19
  if (!ver)
30
20
  continue;
31
21
  for (const asset of ver.assets) {
@@ -186,33 +176,36 @@ function scoreAsset(terms, asset, pkg, pkgName, skipPkgMeta = false) {
186
176
  return total;
187
177
  }
188
178
  // ─── List Packages ───────────────────────────────────────────────────────────
189
- export function listPackages(registry) {
190
- const bucket = DEFAULT_BUCKET;
191
- return Object.entries(registry.packages).map(([name, pkg]) => {
192
- const ver = pkg.versions[pkg.latest];
179
+ export async function listStorePackages(firestore, bucket) {
180
+ const packages = await firestore.listPackages();
181
+ const results = [];
182
+ for (const { name, data: pkg } of packages) {
183
+ const ver = await firestore.getVersion(name, pkg.latest);
184
+ const versionKeys = await firestore.listVersionKeys(name);
193
185
  const thumbnailUrl = pkg.thumbnail
194
186
  ? `https://storage.googleapis.com/${bucket}/packages/${name}/${pkg.latest}/${pkg.thumbnail}`
195
187
  : undefined;
196
- return {
188
+ results.push({
197
189
  name,
198
190
  description: pkg.description,
199
191
  author: pkg.author,
200
192
  tags: pkg.tags ?? [],
201
193
  latest: pkg.latest,
202
- versions: Object.keys(pkg.versions),
194
+ versions: versionKeys,
203
195
  assetCount: ver?.assets.length ?? 0,
204
196
  totalSize: ver?.totalSize ?? 0,
205
197
  thumbnailUrl,
206
- };
207
- });
198
+ });
199
+ }
200
+ return results;
208
201
  }
209
202
  // ─── Resolve Dependencies ────────────────────────────────────────────────────
210
- export function resolveAssetDeps(registry, packageName, stringIds, version) {
211
- const pkg = registry.packages[packageName];
203
+ export async function resolveAssetDeps(firestore, packageName, stringIds, version) {
204
+ const pkg = await firestore.getPackage(packageName);
212
205
  if (!pkg)
213
206
  throw new Error(`Package "${packageName}" not found`);
214
207
  const verStr = version ?? pkg.latest;
215
- const ver = pkg.versions[verStr];
208
+ const ver = await firestore.getVersion(packageName, verStr);
216
209
  if (!ver)
217
210
  throw new Error(`Version "${verStr}" not found`);
218
211
  const resolvedIds = resolveTransitiveDeps(stringIds, ver.assets);
@@ -221,8 +214,9 @@ export function resolveAssetDeps(registry, packageName, stringIds, version) {
221
214
  }
222
215
  // ─── CLI Commands ────────────────────────────────────────────────────────────
223
216
  export async function storeSearch(query, opts) {
224
- const registry = await fetchRegistry(opts?.bucket);
225
- let results = searchAssets(registry, query, { type: opts?.type });
217
+ const bucket = (opts?.bucket ?? DEFAULT_BUCKET).replace(/^gs:\/\//, '').replace(/\/$/, '');
218
+ const firestore = createFirestoreReader();
219
+ let results = await searchAssets(firestore, bucket, query, { type: opts?.type });
226
220
  if (opts?.limit && opts.limit > 0)
227
221
  results = results.slice(0, opts.limit);
228
222
  if (opts?.json) {
@@ -244,8 +238,9 @@ export async function storeSearch(query, opts) {
244
238
  console.log('');
245
239
  }
246
240
  export async function storeList(opts) {
247
- const registry = await fetchRegistry(opts?.bucket);
248
- const packages = listPackages(registry);
241
+ const bucket = (opts?.bucket ?? DEFAULT_BUCKET).replace(/^gs:\/\//, '').replace(/\/$/, '');
242
+ const firestore = createFirestoreReader();
243
+ const packages = await listStorePackages(firestore, bucket);
249
244
  if (opts?.json) {
250
245
  console.log(JSON.stringify(packages, null, 2));
251
246
  return;
@@ -264,13 +259,15 @@ export async function storeList(opts) {
264
259
  console.log('');
265
260
  }
266
261
  export async function storeInfo(packageName, opts) {
267
- const registry = await fetchRegistry(opts?.bucket);
268
- const pkg = registry.packages[packageName];
262
+ const bucket = (opts?.bucket ?? DEFAULT_BUCKET).replace(/^gs:\/\//, '').replace(/\/$/, '');
263
+ const firestore = createFirestoreReader();
264
+ const pkg = await firestore.getPackage(packageName);
269
265
  if (!pkg) {
270
266
  console.error(`Package "${packageName}" not found.`);
271
267
  process.exit(1);
272
268
  }
273
- const ver = pkg.versions[pkg.latest];
269
+ const ver = await firestore.getVersion(packageName, pkg.latest);
270
+ const versionKeys = await firestore.listVersionKeys(packageName);
274
271
  if (opts?.json) {
275
272
  console.log(JSON.stringify({
276
273
  name: packageName,
@@ -278,7 +275,7 @@ export async function storeInfo(packageName, opts) {
278
275
  author: pkg.author,
279
276
  tags: pkg.tags ?? [],
280
277
  latest: pkg.latest,
281
- versions: Object.keys(pkg.versions),
278
+ versions: versionKeys,
282
279
  assets: ver?.assets ?? [],
283
280
  }, null, 2));
284
281
  return;
@@ -290,7 +287,7 @@ export async function storeInfo(packageName, opts) {
290
287
  console.log(` Author: ${pkg.author}`);
291
288
  if (pkg.tags?.length)
292
289
  console.log(` Tags: ${pkg.tags.join(', ')}`);
293
- console.log(` Versions: ${Object.keys(pkg.versions).join(', ')}`);
290
+ console.log(` Versions: ${versionKeys.join(', ')}`);
294
291
  if (ver) {
295
292
  console.log(`\nAssets (${ver.assets.length}):\n`);
296
293
  for (const a of ver.assets) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-inc/stowkit-cli",
3
- "version": "0.6.23",
3
+ "version": "0.6.35",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "stowkit": "./dist/cli.js"
@@ -10,7 +10,8 @@
10
10
  "files": [
11
11
  "dist",
12
12
  "skill.md",
13
- "wasm"
13
+ "wasm",
14
+ "templates"
14
15
  ],
15
16
  "scripts": {
16
17
  "build": "tsc",
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
6
+ <title>My Game</title>
7
+ </head>
8
+ <body>
9
+ <canvas id="renderCanvas"></canvas>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>