@series-inc/stowkit-cli 0.1.13 → 0.1.15

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.
@@ -318,10 +318,9 @@ export async function fullBuild(projectDir, opts) {
318
318
  console.error(` [glb] ${container.id} FAILED: ${container.error}`);
319
319
  }
320
320
  }
321
- // 2c. Process GLB mesh children + stale assets via worker pool
322
- const glbMeshChildren = assets.filter(a => a.status === 'pending' && a.parentId && (a.type === AssetType.StaticMesh || a.type === AssetType.SkinnedMesh));
323
- const pending = assets.filter(a => a.status === 'pending' && !a.parentId);
324
- const totalWork = glbMeshChildren.length + pending.length;
321
+ // 2c. Process all pending assets via worker pool (one queue, no split)
322
+ const pending = assets.filter(a => a.status === 'pending');
323
+ const totalWork = pending.length;
325
324
  if (totalWork === 0) {
326
325
  if (verbose)
327
326
  console.log('All assets cached, nothing to process.');
@@ -330,39 +329,34 @@ export async function fullBuild(projectDir, opts) {
330
329
  console.log(`Processing ${totalWork} asset(s)...`);
331
330
  const pool = new WorkerPool({ wasmDir: opts?.wasmDir });
332
331
  let processed = 0;
333
- // Process GLB mesh children
334
- for (const child of glbMeshChildren) {
335
- const extract = glbExtracts.get(child.parentId);
336
- if (!extract)
337
- continue;
338
- const mesh = extract.meshes.find(m => `${child.parentId}/${m.name}` === child.id);
339
- if (!mesh)
340
- continue;
341
- try {
342
- const { result, blobs } = await pool.processExtractedMesh({ childId: child.id, imported: mesh.imported, hasSkeleton: mesh.hasSkeleton, stringId: child.stringId, settings: child.settings });
343
- for (const [key, data] of blobs)
344
- BlobStore.setProcessed(key, data);
345
- child.status = 'ready';
346
- child.metadata = result.metadata;
347
- child.processedSize = result.processedSize;
348
- processed++;
349
- if (verbose)
350
- console.log(` [${processed}/${totalWork}] ${child.id} (glb-mesh)`);
351
- }
352
- catch (err) {
353
- child.status = 'error';
354
- child.error = err instanceof Error ? err.message : String(err);
355
- processed++;
356
- }
357
- }
358
- // Process remaining pending assets with concurrency limit
359
332
  const queue = [...pending];
360
333
  async function processNext() {
361
334
  while (queue.length > 0) {
362
335
  const asset = queue.shift();
363
336
  const id = asset.id;
364
337
  try {
365
- // Load source (GLB texture children have source in BlobStore already)
338
+ // GLB mesh children use processExtractedMesh (needs parsed mesh data)
339
+ const isMeshChild = asset.parentId &&
340
+ (asset.type === AssetType.StaticMesh || asset.type === AssetType.SkinnedMesh);
341
+ if (isMeshChild) {
342
+ const extract = glbExtracts.get(asset.parentId);
343
+ if (!extract)
344
+ throw new Error(`No extract for parent ${asset.parentId}`);
345
+ const mesh = extract.meshes.find(m => `${asset.parentId}/${m.name}` === id);
346
+ if (!mesh)
347
+ throw new Error(`Mesh not found in extract: ${id}`);
348
+ const { result, blobs } = await pool.processExtractedMesh({ childId: id, imported: mesh.imported, hasSkeleton: mesh.hasSkeleton, stringId: asset.stringId, settings: asset.settings });
349
+ for (const [key, data] of blobs)
350
+ BlobStore.setProcessed(key, data);
351
+ asset.status = 'ready';
352
+ asset.metadata = result.metadata;
353
+ asset.processedSize = result.processedSize;
354
+ processed++;
355
+ if (verbose)
356
+ console.log(` [${processed}/${totalWork}] ${id} (glb-mesh)`);
357
+ continue;
358
+ }
359
+ // Everything else uses processAsset (source bytes)
366
360
  let sourceData = BlobStore.getSource(id);
367
361
  if (!sourceData) {
368
362
  const data = await readFile(config.srcArtDir, id);
@@ -381,39 +375,39 @@ export async function fullBuild(projectDir, opts) {
381
375
  asset.processedSize = result.processedSize;
382
376
  processed++;
383
377
  console.log(` [${processed}/${totalWork}] ${id} (${elapsed}ms)`);
384
- // Write cache
385
- const cacheEntries = new Map();
386
- const processedBlob = BlobStore.getProcessed(id);
387
- if (processedBlob)
388
- cacheEntries.set(id, processedBlob);
389
- if (result.metadata) {
390
- cacheEntries.set(`${id}:__metadata__`, new TextEncoder().encode(JSON.stringify(result.metadata)));
391
- }
392
- // Auxiliary blobs
393
- for (const suffix of [':skinnedMeta', ':animMeta', ':animCount']) {
394
- const blob = BlobStore.getProcessed(`${id}${suffix}`);
395
- if (blob)
396
- cacheEntries.set(`${id}${suffix}`, blob);
397
- }
398
- const animCountBlob = BlobStore.getProcessed(`${id}:animCount`);
399
- const clipCount = animCountBlob ? animCountBlob[0] : 0;
400
- for (let ci = 0; ci < clipCount; ci++) {
401
- const animData = BlobStore.getProcessed(`${id}:anim:${ci}`);
402
- if (animData)
403
- cacheEntries.set(`${id}:anim:${ci}`, animData);
404
- const animMeta = BlobStore.getProcessed(`${id}:animMeta:${ci}`);
405
- if (animMeta)
406
- cacheEntries.set(`${id}:animMeta:${ci}`, animMeta);
407
- }
408
- if (cacheEntries.size > 0) {
409
- await writeCacheBlobs(config.srcArtDir, id, cacheEntries);
410
- // Stamp cache in .stowmeta
411
- const snapshot = await getFileSnapshot(config.srcArtDir, id);
412
- if (snapshot) {
413
- const meta = await readStowmeta(config.srcArtDir, id);
414
- if (meta) {
415
- meta.cache = buildCacheStamp(snapshot, asset.type, asset.settings);
416
- await writeStowmeta(config.srcArtDir, id, meta);
378
+ // Write cache (only for top-level assets that have their own file on disk)
379
+ if (!asset.parentId) {
380
+ const cacheEntries = new Map();
381
+ const processedBlob = BlobStore.getProcessed(id);
382
+ if (processedBlob)
383
+ cacheEntries.set(id, processedBlob);
384
+ if (result.metadata) {
385
+ cacheEntries.set(`${id}:__metadata__`, new TextEncoder().encode(JSON.stringify(result.metadata)));
386
+ }
387
+ for (const suffix of [':skinnedMeta', ':animMeta', ':animCount']) {
388
+ const blob = BlobStore.getProcessed(`${id}${suffix}`);
389
+ if (blob)
390
+ cacheEntries.set(`${id}${suffix}`, blob);
391
+ }
392
+ const animCountBlob = BlobStore.getProcessed(`${id}:animCount`);
393
+ const clipCount = animCountBlob ? animCountBlob[0] : 0;
394
+ for (let ci = 0; ci < clipCount; ci++) {
395
+ const animData = BlobStore.getProcessed(`${id}:anim:${ci}`);
396
+ if (animData)
397
+ cacheEntries.set(`${id}:anim:${ci}`, animData);
398
+ const animMeta = BlobStore.getProcessed(`${id}:animMeta:${ci}`);
399
+ if (animMeta)
400
+ cacheEntries.set(`${id}:animMeta:${ci}`, animMeta);
401
+ }
402
+ if (cacheEntries.size > 0) {
403
+ await writeCacheBlobs(config.srcArtDir, id, cacheEntries);
404
+ const snapshot = await getFileSnapshot(config.srcArtDir, id);
405
+ if (snapshot) {
406
+ const meta = await readStowmeta(config.srcArtDir, id);
407
+ if (meta) {
408
+ meta.cache = buildCacheStamp(snapshot, asset.type, asset.settings);
409
+ await writeStowmeta(config.srcArtDir, id, meta);
410
+ }
417
411
  }
418
412
  }
419
413
  }
@@ -427,7 +421,7 @@ export async function fullBuild(projectDir, opts) {
427
421
  }
428
422
  }
429
423
  const workers = [];
430
- for (let i = 0; i < Math.min(maxConcurrent, pending.length); i++) {
424
+ for (let i = 0; i < Math.min(maxConcurrent, queue.length); i++) {
431
425
  workers.push(processNext());
432
426
  }
433
427
  await Promise.all(workers);
package/dist/server.js CHANGED
@@ -721,19 +721,55 @@ async function handleRequest(req, res, staticApps) {
721
721
  if (projectConfig && Date.now() - lastScanTime > 5000) {
722
722
  lastScanTime = Date.now();
723
723
  const scan = await scanDirectory(projectConfig.srcArtDir);
724
- // Only compare top-level (non-child) asset IDs with disk files
725
- const currentIds = new Set(assets.filter(a => !a.parentId).map(a => a.id));
726
- const diskIds = new Set([
727
- ...scan.sourceFiles.map(f => f.relativePath),
728
- ...scan.matFiles.map(f => f.relativePath),
724
+ // Compare top-level (non-child) asset IDs with disk files
725
+ const currentAssets = assets.filter(a => !a.parentId);
726
+ const currentIds = new Set(currentAssets.map(a => a.id));
727
+ const diskFiles = new Map([
728
+ ...scan.sourceFiles.map(f => [f.relativePath, f]),
729
+ ...scan.matFiles.map(f => [f.relativePath, f]),
729
730
  ]);
730
- const changed = currentIds.size !== diskIds.size ||
731
+ const diskIds = new Set(diskFiles.keys());
732
+ // Check for added/removed files
733
+ let changed = currentIds.size !== diskIds.size ||
731
734
  [...currentIds].some(id => !diskIds.has(id)) ||
732
735
  [...diskIds].some(id => !currentIds.has(id));
736
+ // Check for modified files (size or mtime changed)
737
+ const modifiedIds = [];
738
+ if (!changed) {
739
+ for (const asset of currentAssets) {
740
+ const diskFile = diskFiles.get(asset.id);
741
+ if (diskFile && asset.sourceSize > 0 && diskFile.size !== asset.sourceSize) {
742
+ modifiedIds.push(asset.id);
743
+ }
744
+ }
745
+ }
733
746
  if (changed) {
734
747
  await openProject(projectConfig.projectDir);
735
748
  queueProcessing();
736
749
  }
750
+ else if (modifiedIds.length > 0) {
751
+ // Re-read source and reprocess modified assets without full project reload
752
+ for (const id of modifiedIds) {
753
+ const asset = assets.find(a => a.id === id);
754
+ if (!asset)
755
+ continue;
756
+ const diskFile = diskFiles.get(id);
757
+ if (diskFile)
758
+ asset.sourceSize = diskFile.size;
759
+ BlobStore.remove(id);
760
+ // Also clear children for GLB containers
761
+ if (asset.type === AssetType.GlbContainer) {
762
+ const prefix = id + '/';
763
+ for (const child of assets) {
764
+ if (child.id.startsWith(prefix))
765
+ BlobStore.remove(child.id);
766
+ }
767
+ }
768
+ asset.status = 'pending';
769
+ broadcast({ type: 'asset-update', id, updates: { status: 'pending', sourceSize: asset.sourceSize } });
770
+ }
771
+ queueProcessing({ ids: modifiedIds });
772
+ }
737
773
  }
738
774
  json(res, {
739
775
  project: projectConfig ? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-inc/stowkit-cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "stowkit": "./dist/cli.js"
@@ -17,7 +17,7 @@
17
17
  "dev": "tsc --watch"
18
18
  },
19
19
  "dependencies": {
20
- "@series-inc/stowkit-packer-gui": "^0.1.6",
20
+ "@series-inc/stowkit-packer-gui": "^0.1.8",
21
21
  "@series-inc/stowkit-editor": "^0.1.2",
22
22
  "draco3d": "^1.5.7",
23
23
  "fbx-parser": "^2.1.3",
package/skill.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  StowKit is a game asset pipeline that compresses and packs assets into `.stow` binary files for runtime loading.
4
4
 
5
+ ## CRITICAL: Never manually create .stowmeta files
6
+
7
+ `.stowmeta` files are **auto-generated by the CLI**. To add any asset to the project:
8
+
9
+ 1. Place the source file (GLB, PNG, FBX, WAV, etc.) into the `srcArtDir`
10
+ 2. Run `npx stowkit build` (or `npx stowkit scan`)
11
+ 3. The CLI detects the new file, generates the correct `.stowmeta` with proper defaults, and processes it
12
+
13
+ **Do not write `.stowmeta` files by hand.** Only edit an existing `.stowmeta` after it has been generated by the CLI (e.g. to change quality settings, pack assignment, or stringId). The same applies to GLB children — the `children` array is populated automatically on the first build.
14
+
15
+ The only file you should manually create is `.stowmat` (material schema) files — these are user-authored material definitions, not generated metadata.
16
+
5
17
  ## Project Structure
6
18
 
7
19
  A StowKit project has a `.felicityproject` JSON file at its root:
@@ -56,9 +68,9 @@ All commands default to the current directory.
56
68
 
57
69
  **GLB/GLTF is the recommended format for 3D models.** Dropping a `.glb` file into the project is the easiest way to get meshes, textures, materials, and animations into the pipeline — everything is extracted and processed automatically. FBX and OBJ are still supported as standalone mesh formats but lack the automatic material/texture extraction that GLB provides.
58
70
 
59
- ## .stowmeta Files
71
+ ## .stowmeta Files (auto-generated — do not create manually)
60
72
 
61
- Every source asset gets a `.stowmeta` sidecar file (JSON) that controls processing settings.
73
+ Every source asset gets a `.stowmeta` sidecar file generated by `npx stowkit build` or `npx stowkit scan`. These files control processing settings and should only be **edited** (never created) by hand. The examples below are reference for understanding and editing existing files.
62
74
 
63
75
  **Texture example:**
64
76
  ```json
@@ -355,10 +367,12 @@ Add `*.stowcache` to `.gitignore`.
355
367
 
356
368
  ### Other common tasks
357
369
 
358
- - **Add a texture:** Drop a PNG/JPG into `assets/`, run `npx stowkit scan` to generate its `.stowmeta`, optionally edit settings, then `npx stowkit build`
359
- - **Change compression quality:** Edit the `.stowmeta` file's quality/resize fields, then `npx stowkit build`
360
- - **Create a material:** Create a `.stowmat` JSON file in `assets/`, run `npx stowkit scan`
361
- - **Assign material to mesh:** Edit the mesh's `.stowmeta` to add `materialOverrides`
370
+ - **Add a texture:** Place PNG/JPG into `assets/`, run `npx stowkit build`. The CLI auto-generates the `.stowmeta`. Do NOT create it yourself.
371
+ - **Add audio:** Place WAV/MP3/OGG into `assets/`, run `npx stowkit build`. Same rule — never manually create `.stowmeta`.
372
+ - **Add an FBX mesh:** Place FBX into `assets/`, run `npx stowkit build`.
373
+ - **Change compression quality:** Edit the **existing** `.stowmeta` file's quality/resize fields (after it was generated by a build/scan), then `npx stowkit build`
374
+ - **Create a material:** Create a `.stowmat` JSON file in `assets/` (this is the one file type you DO create manually), then run `npx stowkit build`
375
+ - **Assign material to mesh:** Edit the mesh's **existing** `.stowmeta` to add `materialOverrides`
362
376
  - **Check project health:** Run `npx stowkit status`
363
377
  - **Full rebuild:** `npx stowkit build --force`
364
378
  - **Clean orphaned files:** `npx stowkit clean`