@series-inc/stowkit-cli 0.1.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.
Files changed (56) hide show
  1. package/dist/app/blob-store.d.ts +9 -0
  2. package/dist/app/blob-store.js +42 -0
  3. package/dist/app/disk-project.d.ts +84 -0
  4. package/dist/app/disk-project.js +70 -0
  5. package/dist/app/process-cache.d.ts +10 -0
  6. package/dist/app/process-cache.js +126 -0
  7. package/dist/app/state.d.ts +38 -0
  8. package/dist/app/state.js +16 -0
  9. package/dist/app/stowmat-io.d.ts +6 -0
  10. package/dist/app/stowmat-io.js +48 -0
  11. package/dist/app/stowmeta-io.d.ts +14 -0
  12. package/dist/app/stowmeta-io.js +207 -0
  13. package/dist/cleanup.d.ts +3 -0
  14. package/dist/cleanup.js +72 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.js +148 -0
  17. package/dist/core/binary.d.ts +41 -0
  18. package/dist/core/binary.js +118 -0
  19. package/dist/core/constants.d.ts +64 -0
  20. package/dist/core/constants.js +65 -0
  21. package/dist/core/path.d.ts +3 -0
  22. package/dist/core/path.js +27 -0
  23. package/dist/core/types.d.ts +204 -0
  24. package/dist/core/types.js +76 -0
  25. package/dist/encoders/aac-encoder.d.ts +12 -0
  26. package/dist/encoders/aac-encoder.js +179 -0
  27. package/dist/encoders/basis-encoder.d.ts +15 -0
  28. package/dist/encoders/basis-encoder.js +116 -0
  29. package/dist/encoders/draco-encoder.d.ts +11 -0
  30. package/dist/encoders/draco-encoder.js +155 -0
  31. package/dist/encoders/fbx-loader.d.ts +4 -0
  32. package/dist/encoders/fbx-loader.js +540 -0
  33. package/dist/encoders/image-decoder.d.ts +13 -0
  34. package/dist/encoders/image-decoder.js +33 -0
  35. package/dist/encoders/interfaces.d.ts +105 -0
  36. package/dist/encoders/interfaces.js +1 -0
  37. package/dist/encoders/skinned-mesh-builder.d.ts +7 -0
  38. package/dist/encoders/skinned-mesh-builder.js +135 -0
  39. package/dist/format/metadata.d.ts +18 -0
  40. package/dist/format/metadata.js +381 -0
  41. package/dist/format/packer.d.ts +8 -0
  42. package/dist/format/packer.js +87 -0
  43. package/dist/index.d.ts +28 -0
  44. package/dist/index.js +35 -0
  45. package/dist/init.d.ts +1 -0
  46. package/dist/init.js +73 -0
  47. package/dist/node-fs.d.ts +22 -0
  48. package/dist/node-fs.js +148 -0
  49. package/dist/orchestrator.d.ts +20 -0
  50. package/dist/orchestrator.js +301 -0
  51. package/dist/pipeline.d.ts +23 -0
  52. package/dist/pipeline.js +354 -0
  53. package/dist/server.d.ts +9 -0
  54. package/dist/server.js +859 -0
  55. package/package.json +35 -0
  56. package/skill.md +211 -0
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ // Core types and constants
2
+ export * from './core/types.js';
3
+ export * from './core/constants.js';
4
+ export * from './core/binary.js';
5
+ export * from './core/path.js';
6
+ // Format
7
+ export { StowPacker } from './format/packer.js';
8
+ export * from './format/metadata.js';
9
+ // App layer
10
+ export * from './app/disk-project.js';
11
+ export { BlobStore } from './app/blob-store.js';
12
+ export * from './app/state.js';
13
+ export * from './app/stowmeta-io.js';
14
+ export * from './app/stowmat-io.js';
15
+ export * from './app/process-cache.js';
16
+ // Node file I/O
17
+ export * from './node-fs.js';
18
+ // Encoders
19
+ export * from './encoders/interfaces.js';
20
+ export { NodeBasisEncoder } from './encoders/basis-encoder.js';
21
+ export { NodeDracoEncoder, dracoPresetToSettings } from './encoders/draco-encoder.js';
22
+ export { NodeAacEncoder, NodeAudioDecoder } from './encoders/aac-encoder.js';
23
+ export { NodeFbxImporter } from './encoders/fbx-loader.js';
24
+ export { SharpImageDecoder } from './encoders/image-decoder.js';
25
+ export { buildSkinnedMeshData } from './encoders/skinned-mesh-builder.js';
26
+ // Pipeline
27
+ export { processAsset, buildPack, buildAnimationDataBlobsV2 } from './pipeline.js';
28
+ // Orchestrator
29
+ export { fullBuild, scanProject, showStatus } from './orchestrator.js';
30
+ // Server
31
+ export { startServer } from './server.js';
32
+ // Init
33
+ export { initProject } from './init.js';
34
+ // Cleanup
35
+ export { cleanupProject } from './cleanup.js';
package/dist/init.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function initProject(projectDir: string): Promise<void>;
package/dist/init.js ADDED
@@ -0,0 +1,73 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ export async function initProject(projectDir) {
5
+ const absDir = path.resolve(projectDir);
6
+ // Check if already initialized
7
+ const configPath = path.join(absDir, '.felicityproject');
8
+ try {
9
+ await fs.access(configPath);
10
+ console.log(`Project already initialized at ${absDir}`);
11
+ return;
12
+ }
13
+ catch {
14
+ // Does not exist — create it
15
+ }
16
+ // Create srcArtDir with .gitignore for cache files
17
+ const srcArtDir = 'assets';
18
+ await fs.mkdir(path.join(absDir, srcArtDir), { recursive: true });
19
+ const assetsGitignore = path.join(absDir, srcArtDir, '.gitignore');
20
+ try {
21
+ await fs.access(assetsGitignore);
22
+ }
23
+ catch {
24
+ await fs.writeFile(assetsGitignore, '*.stowcache\n');
25
+ }
26
+ // Create public/cdn-assets output dir
27
+ await fs.mkdir(path.join(absDir, 'public/cdn-assets'), { recursive: true });
28
+ // Write .felicityproject
29
+ const config = {
30
+ srcArtDir,
31
+ name: path.basename(absDir),
32
+ cdnAssetsPath: 'public/cdn-assets',
33
+ packs: [{ name: 'default' }],
34
+ };
35
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
36
+ // Add stowkit artifacts to .gitignore
37
+ const gitignorePath = path.join(absDir, '.gitignore');
38
+ const stowkitIgnores = [
39
+ '# StowKit',
40
+ '*.stowcache',
41
+ 'public/cdn-assets/',
42
+ ].join('\n');
43
+ try {
44
+ const existing = await fs.readFile(gitignorePath, 'utf-8');
45
+ if (!existing.includes('*.stowcache')) {
46
+ await fs.writeFile(gitignorePath, existing.trimEnd() + '\n\n' + stowkitIgnores + '\n');
47
+ }
48
+ }
49
+ catch {
50
+ await fs.writeFile(gitignorePath, stowkitIgnores + '\n');
51
+ }
52
+ // Copy Claude skill file
53
+ const skillDir = path.join(absDir, '.claude', 'skills');
54
+ await fs.mkdir(skillDir, { recursive: true });
55
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
56
+ const skillSrc = path.resolve(thisDir, '../skill.md');
57
+ const skillDst = path.join(skillDir, 'stowkit.md');
58
+ try {
59
+ const skillContent = await fs.readFile(skillSrc, 'utf-8');
60
+ await fs.writeFile(skillDst, skillContent);
61
+ }
62
+ catch {
63
+ // Skill file not found in package — skip silently
64
+ }
65
+ console.log(`Initialized StowKit project at ${absDir}`);
66
+ console.log(` Source art dir: ${srcArtDir}/`);
67
+ console.log(` Output dir: public/cdn-assets/`);
68
+ console.log(` Config: .felicityproject`);
69
+ console.log(` Claude skill: .claude/skills/stowkit.md`);
70
+ console.log('');
71
+ console.log('Drop your assets (PNG, JPG, FBX, WAV, etc.) into assets/');
72
+ console.log('Then run: npx stowkit build');
73
+ }
@@ -0,0 +1,22 @@
1
+ import type { FelicityProject, FileSnapshot } from './app/disk-project.js';
2
+ export declare function readFile(basePath: string, relativePath: string): Promise<Uint8Array | null>;
3
+ export declare function readTextFile(basePath: string, relativePath: string): Promise<string | null>;
4
+ export declare function writeFile(basePath: string, relativePath: string, data: string | Uint8Array): Promise<void>;
5
+ export declare function deleteFile(basePath: string, relativePath: string): Promise<void>;
6
+ export declare function fileExists(basePath: string, relativePath: string): Promise<boolean>;
7
+ export declare function getFileSnapshot(basePath: string, relativePath: string): Promise<FileSnapshot | null>;
8
+ export interface ScanResult {
9
+ sourceFiles: FileSnapshot[];
10
+ metaFiles: FileSnapshot[];
11
+ matFiles: FileSnapshot[];
12
+ folders: string[];
13
+ }
14
+ export declare function scanDirectory(basePath: string): Promise<ScanResult>;
15
+ export interface ProjectConfig {
16
+ projectDir: string;
17
+ srcArtDir: string;
18
+ config: FelicityProject;
19
+ projectName: string;
20
+ }
21
+ export declare function readProjectConfig(projectDir: string): Promise<ProjectConfig>;
22
+ export declare function writeFelicityProject(projectDir: string, config: FelicityProject): Promise<void>;
@@ -0,0 +1,148 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ // ─── File I/O ────────────────────────────────────────────────────────────────
4
+ export async function readFile(basePath, relativePath) {
5
+ try {
6
+ const fullPath = path.join(basePath, relativePath);
7
+ const buf = await fs.readFile(fullPath);
8
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export async function readTextFile(basePath, relativePath) {
15
+ try {
16
+ const fullPath = path.join(basePath, relativePath);
17
+ return await fs.readFile(fullPath, 'utf-8');
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ export async function writeFile(basePath, relativePath, data) {
24
+ const fullPath = path.join(basePath, relativePath);
25
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
26
+ await fs.writeFile(fullPath, data);
27
+ }
28
+ export async function deleteFile(basePath, relativePath) {
29
+ try {
30
+ const fullPath = path.join(basePath, relativePath);
31
+ await fs.unlink(fullPath);
32
+ }
33
+ catch {
34
+ // Ignore — file may not exist
35
+ }
36
+ }
37
+ export async function fileExists(basePath, relativePath) {
38
+ try {
39
+ await fs.access(path.join(basePath, relativePath));
40
+ return true;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ export async function getFileSnapshot(basePath, relativePath) {
47
+ try {
48
+ const fullPath = path.join(basePath, relativePath);
49
+ const stat = await fs.stat(fullPath);
50
+ return {
51
+ relativePath,
52
+ size: stat.size,
53
+ lastModified: stat.mtimeMs,
54
+ };
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ // ─── Directory Scanning ──────────────────────────────────────────────────────
61
+ const SOURCE_EXTENSIONS = new Set([
62
+ 'png', 'jpg', 'jpeg', 'bmp', 'tga', 'webp', 'gif',
63
+ 'wav', 'mp3', 'ogg', 'flac', 'aac', 'm4a',
64
+ 'fbx', 'obj', 'gltf', 'glb',
65
+ ]);
66
+ function isSourceFile(name) {
67
+ const ext = name.split('.').pop()?.toLowerCase() ?? '';
68
+ return SOURCE_EXTENSIONS.has(ext);
69
+ }
70
+ function isMetaFile(name) {
71
+ return name.endsWith('.stowmeta');
72
+ }
73
+ function isMatFile(name) {
74
+ return name.endsWith('.stowmat');
75
+ }
76
+ export async function scanDirectory(basePath) {
77
+ const sourceFiles = [];
78
+ const metaFiles = [];
79
+ const matFiles = [];
80
+ const folders = [];
81
+ await walkDirectory(basePath, '', sourceFiles, metaFiles, matFiles, folders);
82
+ return { sourceFiles, metaFiles, matFiles, folders };
83
+ }
84
+ async function walkDirectory(basePath, prefix, sourceFiles, metaFiles, matFiles, folders) {
85
+ const dirPath = prefix ? path.join(basePath, prefix) : basePath;
86
+ let entries;
87
+ try {
88
+ entries = await fs.readdir(dirPath, { withFileTypes: true });
89
+ }
90
+ catch {
91
+ return;
92
+ }
93
+ for (const entry of entries) {
94
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
95
+ if (entry.isDirectory()) {
96
+ if (entry.name.startsWith('.'))
97
+ continue;
98
+ folders.push(relativePath);
99
+ await walkDirectory(basePath, relativePath, sourceFiles, metaFiles, matFiles, folders);
100
+ }
101
+ else if (entry.isFile()) {
102
+ const fullPath = path.join(basePath, relativePath);
103
+ let stat;
104
+ try {
105
+ stat = await fs.stat(fullPath);
106
+ }
107
+ catch {
108
+ continue;
109
+ }
110
+ const snapshot = {
111
+ relativePath,
112
+ size: stat.size,
113
+ lastModified: stat.mtimeMs,
114
+ };
115
+ if (isMetaFile(entry.name)) {
116
+ metaFiles.push(snapshot);
117
+ }
118
+ else if (isMatFile(entry.name)) {
119
+ matFiles.push(snapshot);
120
+ }
121
+ else if (isSourceFile(entry.name)) {
122
+ sourceFiles.push(snapshot);
123
+ }
124
+ }
125
+ }
126
+ }
127
+ export async function readProjectConfig(projectDir) {
128
+ const configPath = path.join(projectDir, '.felicityproject');
129
+ let configText;
130
+ try {
131
+ configText = await fs.readFile(configPath, 'utf-8');
132
+ }
133
+ catch {
134
+ throw new Error(`No .felicityproject found in ${projectDir}. ` +
135
+ `Create one with {"srcArtDir": "."} to use the directory as a project.`);
136
+ }
137
+ const config = JSON.parse(configText);
138
+ if (!config.srcArtDir) {
139
+ throw new Error('.felicityproject is missing "srcArtDir" field.');
140
+ }
141
+ const srcArtDir = path.resolve(projectDir, config.srcArtDir);
142
+ const projectName = config.name || path.basename(projectDir);
143
+ return { projectDir, srcArtDir, config, projectName };
144
+ }
145
+ export async function writeFelicityProject(projectDir, config) {
146
+ const configPath = path.join(projectDir, '.felicityproject');
147
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
148
+ }
@@ -0,0 +1,20 @@
1
+ export interface BuildOptions {
2
+ force?: boolean;
3
+ verbose?: boolean;
4
+ maxConcurrent?: number;
5
+ wasmDir?: string;
6
+ }
7
+ export interface ScanReport {
8
+ projectName: string;
9
+ srcArtDir: string;
10
+ sourceFiles: number;
11
+ metaFiles: number;
12
+ matFiles: number;
13
+ newFiles: string[];
14
+ totalAssets: number;
15
+ }
16
+ export declare function scanProject(projectDir: string, opts?: {
17
+ verbose?: boolean;
18
+ }): Promise<ScanReport>;
19
+ export declare function fullBuild(projectDir: string, opts?: BuildOptions): Promise<void>;
20
+ export declare function showStatus(projectDir: string): Promise<void>;
@@ -0,0 +1,301 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { AssetType } from './core/types.js';
4
+ import { cleanupProject } from './cleanup.js';
5
+ import { defaultAssetSettings } from './app/state.js';
6
+ import { BlobStore } from './app/blob-store.js';
7
+ import { readProjectConfig, scanDirectory, readFile, getFileSnapshot, } from './node-fs.js';
8
+ import { readStowmeta, writeStowmeta, stowmetaToAssetSettings, generateDefaultStowmeta, } from './app/stowmeta-io.js';
9
+ import { readStowmat, stowmatToMaterialConfig } from './app/stowmat-io.js';
10
+ import { readCacheBlobs, writeCacheBlobs, buildCacheStamp, isCacheValid, } from './app/process-cache.js';
11
+ import { processAsset, buildPack } from './pipeline.js';
12
+ // ─── Encoder initialization ────────────────────────────────────────────────
13
+ import { NodeBasisEncoder } from './encoders/basis-encoder.js';
14
+ import { NodeDracoEncoder } from './encoders/draco-encoder.js';
15
+ import { NodeAacEncoder, NodeAudioDecoder } from './encoders/aac-encoder.js';
16
+ import { NodeFbxImporter } from './encoders/fbx-loader.js';
17
+ import { SharpImageDecoder } from './encoders/image-decoder.js';
18
+ export async function scanProject(projectDir, opts) {
19
+ const config = await readProjectConfig(projectDir);
20
+ const scan = await scanDirectory(config.srcArtDir);
21
+ const verbose = opts?.verbose ?? false;
22
+ const existingMeta = new Set(scan.metaFiles.map(f => f.relativePath.replace(/\.stowmeta$/, '')));
23
+ const newFiles = [];
24
+ for (const file of scan.sourceFiles) {
25
+ if (!existingMeta.has(file.relativePath)) {
26
+ newFiles.push(file.relativePath);
27
+ const meta = generateDefaultStowmeta(file.relativePath);
28
+ await writeStowmeta(config.srcArtDir, file.relativePath, meta);
29
+ if (verbose)
30
+ console.log(` [new] ${file.relativePath} → .stowmeta (${meta.type})`);
31
+ }
32
+ }
33
+ return {
34
+ projectName: config.projectName,
35
+ srcArtDir: config.srcArtDir,
36
+ sourceFiles: scan.sourceFiles.length,
37
+ metaFiles: scan.metaFiles.length,
38
+ matFiles: scan.matFiles.length,
39
+ newFiles,
40
+ totalAssets: scan.sourceFiles.length + scan.matFiles.length,
41
+ };
42
+ }
43
+ // ─── Full Build ──────────────────────────────────────────────────────────────
44
+ export async function fullBuild(projectDir, opts) {
45
+ const verbose = opts?.verbose ?? false;
46
+ const force = opts?.force ?? false;
47
+ const maxConcurrent = opts?.maxConcurrent ?? 8;
48
+ const config = await readProjectConfig(projectDir);
49
+ if (verbose)
50
+ console.log(`Project: ${config.projectName}`);
51
+ if (verbose)
52
+ console.log(`Source dir: ${config.srcArtDir}`);
53
+ // 1. Scan
54
+ const scan = await scanDirectory(config.srcArtDir);
55
+ if (verbose)
56
+ console.log(`Found ${scan.sourceFiles.length} source files, ${scan.matFiles.length} materials`);
57
+ // 2. Build asset list
58
+ const assets = [];
59
+ const assetsById = new Map();
60
+ for (const file of scan.sourceFiles) {
61
+ const id = file.relativePath;
62
+ let meta = await readStowmeta(config.srcArtDir, id);
63
+ if (!meta) {
64
+ meta = generateDefaultStowmeta(id);
65
+ await writeStowmeta(config.srcArtDir, id, meta);
66
+ }
67
+ const { type, settings } = stowmetaToAssetSettings(meta);
68
+ const fileName = id.split('/').pop() ?? id;
69
+ const asset = {
70
+ id,
71
+ fileName,
72
+ stringId: meta.stringId,
73
+ type,
74
+ status: 'pending',
75
+ settings,
76
+ sourceSize: file.size,
77
+ processedSize: 0,
78
+ };
79
+ // Check cache
80
+ if (!force && meta.cache) {
81
+ const snapshot = await getFileSnapshot(config.srcArtDir, id);
82
+ if (snapshot && isCacheValid(meta, snapshot, type, settings)) {
83
+ const cached = await readCacheBlobs(config.srcArtDir, id);
84
+ if (cached) {
85
+ for (const [key, data] of cached) {
86
+ if (key === `${id}:__metadata__`) {
87
+ try {
88
+ asset.metadata = JSON.parse(new TextDecoder().decode(data));
89
+ }
90
+ catch { /* skip */ }
91
+ }
92
+ else {
93
+ BlobStore.setProcessed(key, data);
94
+ }
95
+ }
96
+ asset.status = 'ready';
97
+ asset.processedSize = BlobStore.getProcessed(id)?.length ?? 0;
98
+ if (verbose)
99
+ console.log(` [cached] ${id}`);
100
+ }
101
+ }
102
+ }
103
+ assets.push(asset);
104
+ assetsById.set(id, asset);
105
+ }
106
+ // Materials from .stowmat files
107
+ for (const matFile of scan.matFiles) {
108
+ const id = matFile.relativePath;
109
+ const mat = await readStowmat(config.srcArtDir, id);
110
+ if (!mat)
111
+ continue;
112
+ const materialConfig = stowmatToMaterialConfig(mat);
113
+ const fileName = id.split('/').pop() ?? id;
114
+ const baseName = fileName.replace(/\.[^.]+$/, '');
115
+ let meta = await readStowmeta(config.srcArtDir, id);
116
+ if (!meta) {
117
+ meta = generateDefaultStowmeta(id, AssetType.MaterialSchema);
118
+ await writeStowmeta(config.srcArtDir, id, meta);
119
+ }
120
+ const asset = {
121
+ id,
122
+ fileName,
123
+ stringId: meta.stringId || baseName,
124
+ type: AssetType.MaterialSchema,
125
+ status: 'ready',
126
+ settings: { ...defaultAssetSettings(), materialConfig, pack: meta.pack ?? 'default', tags: meta.tags ?? [] },
127
+ sourceSize: matFile.size,
128
+ processedSize: 0,
129
+ };
130
+ assets.push(asset);
131
+ assetsById.set(id, asset);
132
+ }
133
+ // 3. Process stale assets
134
+ const pending = assets.filter(a => a.status === 'pending');
135
+ if (pending.length === 0) {
136
+ if (verbose)
137
+ console.log('All assets cached, nothing to process.');
138
+ }
139
+ else {
140
+ console.log(`Processing ${pending.length} asset(s)...`);
141
+ // Initialize encoders
142
+ const ctx = await initializeEncoders(opts?.wasmDir);
143
+ // Process with concurrency limit
144
+ let processed = 0;
145
+ const queue = [...pending];
146
+ async function processNext() {
147
+ while (queue.length > 0) {
148
+ const asset = queue.shift();
149
+ const id = asset.id;
150
+ try {
151
+ // Load source
152
+ const sourceData = await readFile(config.srcArtDir, id);
153
+ if (!sourceData)
154
+ throw new Error(`Could not read source file: ${id}`);
155
+ BlobStore.setSource(id, sourceData);
156
+ const t0 = performance.now();
157
+ const result = await processAsset(id, sourceData, asset.type, asset.stringId, asset.settings, ctx);
158
+ const elapsed = (performance.now() - t0).toFixed(0);
159
+ asset.status = 'ready';
160
+ asset.metadata = result.metadata;
161
+ asset.processedSize = result.processedSize;
162
+ processed++;
163
+ console.log(` [${processed}/${pending.length}] ${id} (${elapsed}ms)`);
164
+ // Write cache
165
+ const cacheEntries = new Map();
166
+ const processedBlob = BlobStore.getProcessed(id);
167
+ if (processedBlob)
168
+ cacheEntries.set(id, processedBlob);
169
+ if (result.metadata) {
170
+ cacheEntries.set(`${id}:__metadata__`, new TextEncoder().encode(JSON.stringify(result.metadata)));
171
+ }
172
+ // Auxiliary blobs
173
+ for (const suffix of [':skinnedMeta', ':animMeta', ':animCount']) {
174
+ const blob = BlobStore.getProcessed(`${id}${suffix}`);
175
+ if (blob)
176
+ cacheEntries.set(`${id}${suffix}`, blob);
177
+ }
178
+ const animCountBlob = BlobStore.getProcessed(`${id}:animCount`);
179
+ const clipCount = animCountBlob ? animCountBlob[0] : 0;
180
+ for (let ci = 0; ci < clipCount; ci++) {
181
+ const animData = BlobStore.getProcessed(`${id}:anim:${ci}`);
182
+ if (animData)
183
+ cacheEntries.set(`${id}:anim:${ci}`, animData);
184
+ const animMeta = BlobStore.getProcessed(`${id}:animMeta:${ci}`);
185
+ if (animMeta)
186
+ cacheEntries.set(`${id}:animMeta:${ci}`, animMeta);
187
+ }
188
+ if (cacheEntries.size > 0) {
189
+ await writeCacheBlobs(config.srcArtDir, id, cacheEntries);
190
+ // Stamp cache in .stowmeta
191
+ const snapshot = await getFileSnapshot(config.srcArtDir, id);
192
+ if (snapshot) {
193
+ const meta = await readStowmeta(config.srcArtDir, id);
194
+ if (meta) {
195
+ meta.cache = buildCacheStamp(snapshot, asset.type, asset.settings);
196
+ await writeStowmeta(config.srcArtDir, id, meta);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ catch (err) {
202
+ asset.status = 'error';
203
+ asset.error = err instanceof Error ? err.message : String(err);
204
+ processed++;
205
+ console.error(` [${processed}/${pending.length}] ${id} FAILED: ${asset.error}`);
206
+ }
207
+ }
208
+ }
209
+ const workers = [];
210
+ for (let i = 0; i < Math.min(maxConcurrent, pending.length); i++) {
211
+ workers.push(processNext());
212
+ }
213
+ await Promise.all(workers);
214
+ }
215
+ // 4. Clean orphaned caches/metas
216
+ await cleanupProject(config.projectDir, { verbose });
217
+ // 5. Build packs
218
+ const packs = config.config.packs ?? [{ name: 'default' }];
219
+ const cdnDir = path.resolve(config.projectDir, config.config.cdnAssetsPath ?? 'public/cdn-assets');
220
+ await fs.mkdir(cdnDir, { recursive: true });
221
+ for (const pack of packs) {
222
+ const packAssets = assets.filter(a => a.status === 'ready' &&
223
+ (a.settings.pack === pack.name || (!a.settings.pack && pack.name === 'default')));
224
+ if (packAssets.length === 0) {
225
+ if (verbose)
226
+ console.log(`Pack "${pack.name}": no assets, skipping`);
227
+ continue;
228
+ }
229
+ console.log(`Building pack "${pack.name}" (${packAssets.length} assets)...`);
230
+ const packData = buildPack(packAssets, assetsById);
231
+ const outPath = path.join(cdnDir, `${pack.name}.stow`);
232
+ await fs.writeFile(outPath, packData);
233
+ console.log(` → ${outPath} (${(packData.length / 1024).toFixed(0)} KB)`);
234
+ }
235
+ // Summary
236
+ const errors = assets.filter(a => a.status === 'error');
237
+ if (errors.length > 0) {
238
+ console.log(`\n${errors.length} asset(s) failed:`);
239
+ for (const e of errors)
240
+ console.log(` ${e.id}: ${e.error}`);
241
+ }
242
+ console.log('Build complete.');
243
+ }
244
+ // ─── Status ──────────────────────────────────────────────────────────────────
245
+ export async function showStatus(projectDir) {
246
+ const config = await readProjectConfig(projectDir);
247
+ const scan = await scanDirectory(config.srcArtDir);
248
+ console.log(`Project: ${config.projectName}`);
249
+ console.log(`Source dir: ${config.srcArtDir}`);
250
+ console.log(`Source files: ${scan.sourceFiles.length}`);
251
+ console.log(`Material files: ${scan.matFiles.length}`);
252
+ console.log(`Meta files: ${scan.metaFiles.length}`);
253
+ const packs = config.config.packs ?? [{ name: 'default' }];
254
+ console.log(`Packs: ${packs.map(p => p.name).join(', ')}`);
255
+ // Count stale
256
+ let cached = 0;
257
+ let stale = 0;
258
+ for (const file of scan.sourceFiles) {
259
+ const meta = await readStowmeta(config.srcArtDir, file.relativePath);
260
+ if (!meta) {
261
+ stale++;
262
+ continue;
263
+ }
264
+ const { type, settings } = stowmetaToAssetSettings(meta);
265
+ if (meta.cache && isCacheValid(meta, file, type, settings)) {
266
+ cached++;
267
+ }
268
+ else {
269
+ stale++;
270
+ }
271
+ }
272
+ console.log(`Cached: ${cached}, Needs processing: ${stale}`);
273
+ }
274
+ // ─── Initialize Encoders ────────────────────────────────────────────────────
275
+ async function initializeEncoders(wasmDir) {
276
+ const textureEncoder = new NodeBasisEncoder(wasmDir);
277
+ const meshEncoder = new NodeDracoEncoder();
278
+ const aacEncoder = new NodeAacEncoder();
279
+ const audioDecoder = new NodeAudioDecoder();
280
+ const meshImporter = new NodeFbxImporter();
281
+ const imageDecoder = new SharpImageDecoder();
282
+ await Promise.all([
283
+ textureEncoder.initialize(),
284
+ meshEncoder.initialize(),
285
+ aacEncoder.initialize(),
286
+ audioDecoder.initialize(),
287
+ ]);
288
+ return {
289
+ textureEncoder,
290
+ meshEncoder,
291
+ meshImporter,
292
+ imageDecoder,
293
+ audioDecoder,
294
+ aacEncoder,
295
+ onProgress: (id, msg) => {
296
+ // Simple console progress
297
+ const name = id.split('/').pop() ?? id;
298
+ process.stdout.write(` [${name}] ${msg}\r`);
299
+ },
300
+ };
301
+ }
@@ -0,0 +1,23 @@
1
+ import { AssetType } from './core/types.js';
2
+ import type { AnimationClipMetadata } from './core/types.js';
3
+ import type { AssetSettings, ProjectAsset } from './app/state.js';
4
+ import type { ITextureEncoder, IMeshEncoder, IMeshImporter, IImageDecoder, IAudioDecoder, IAacEncoder, ImportedMesh } from './encoders/interfaces.js';
5
+ export interface ProcessingContext {
6
+ textureEncoder: ITextureEncoder;
7
+ meshEncoder: IMeshEncoder;
8
+ meshImporter: IMeshImporter;
9
+ imageDecoder: IImageDecoder;
10
+ audioDecoder: IAudioDecoder;
11
+ aacEncoder: IAacEncoder;
12
+ onProgress?: (id: string, msg: string) => void;
13
+ }
14
+ export interface ProcessResult {
15
+ metadata?: ProjectAsset['metadata'];
16
+ processedSize: number;
17
+ }
18
+ export declare function processAsset(id: string, sourceData: Uint8Array, type: AssetType, stringId: string, settings: AssetSettings, ctx: ProcessingContext): Promise<ProcessResult>;
19
+ export declare function buildPack(assets: ProjectAsset[], assetsById: Map<string, ProjectAsset>): Uint8Array;
20
+ export declare function buildAnimationDataBlobsV2(imported: ImportedMesh): {
21
+ data: Uint8Array;
22
+ metadata: AnimationClipMetadata;
23
+ }[];