@playcraft/cli 0.0.40 → 0.0.42

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 (130) hide show
  1. package/README.md +66 -3
  2. package/dist/atom-plan/validate-atom-plan.js +298 -0
  3. package/dist/cli-root-help.js +1 -1
  4. package/dist/commands/3d.js +363 -0
  5. package/dist/commands/create.js +337 -0
  6. package/dist/commands/image.js +1337 -43
  7. package/dist/commands/recommend.js +1 -1
  8. package/dist/commands/remix.js +213 -0
  9. package/dist/commands/skills.js +1379 -0
  10. package/dist/commands/tools-3d.js +473 -0
  11. package/dist/commands/tools-generation.js +452 -0
  12. package/dist/commands/tools-project.js +400 -0
  13. package/dist/commands/tools-research.js +37 -0
  14. package/dist/commands/tools-research.test.js +216 -0
  15. package/dist/commands/tools-utils.js +183 -0
  16. package/dist/commands/tools.js +7 -616
  17. package/dist/config.js +2 -0
  18. package/dist/index.js +19 -1
  19. package/dist/utils/version-checker.js +8 -11
  20. package/package.json +9 -3
  21. package/project-template/.claude/agents/designer.md +120 -0
  22. package/project-template/.claude/agents/developer.md +124 -0
  23. package/project-template/.claude/agents/pm.md +164 -0
  24. package/project-template/.claude/agents/refs/README.md +73 -0
  25. package/project-template/.claude/agents/refs/designer-art-style-catalog.md +533 -0
  26. package/project-template/.claude/agents/refs/designer-color-audio-recipes.md +153 -0
  27. package/project-template/.claude/agents/refs/designer-deliverable-spec.md +191 -0
  28. package/project-template/.claude/agents/refs/designer-dimension-axis.md +27 -0
  29. package/project-template/.claude/agents/refs/designer-handoff-v2-checklist.md +68 -0
  30. package/project-template/.claude/agents/refs/designer-master-composite-recipes.md +208 -0
  31. package/project-template/.claude/agents/refs/designer-style-exploration-flow.md +37 -0
  32. package/project-template/.claude/agents/refs/developer-dev-handoff.md +109 -0
  33. package/project-template/.claude/agents/refs/developer-impl-cookbook.md +134 -0
  34. package/project-template/.claude/agents/refs/developer-phase1-flow.md +136 -0
  35. package/project-template/.claude/agents/refs/pm-workflow-detail.md +551 -0
  36. package/project-template/.claude/agents/refs/reviewer-convergence-eval.md +130 -0
  37. package/project-template/.claude/agents/refs/reviewer-six-dimension-eval.md +6 -0
  38. package/project-template/.claude/agents/refs/ta-3d-flip-recipe.md +85 -0
  39. package/project-template/.claude/agents/refs/ta-atlas-deliverable-standard.md +67 -0
  40. package/project-template/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
  41. package/project-template/.claude/agents/refs/ta-image-generation-detail.md +356 -0
  42. package/project-template/.claude/agents/refs/ta-image-ops-reference.md +495 -0
  43. package/project-template/.claude/agents/refs/ta-pipeline-cookbook.md +1108 -0
  44. package/project-template/.claude/agents/refs/ta-tools-reference.md +111 -0
  45. package/project-template/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
  46. package/project-template/.claude/agents/reviewer.md +127 -0
  47. package/project-template/.claude/agents/technical-artist.md +122 -0
  48. package/project-template/.claude/hooks/README.md +44 -0
  49. package/project-template/.claude/hooks/validate-atom-plan.mjs +224 -0
  50. package/project-template/.claude/hooks/validate-workflow-stop.mjs +343 -0
  51. package/project-template/.claude/settings.json +36 -0
  52. package/project-template/.claude/settings.local.json +4 -0
  53. package/project-template/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
  54. package/project-template/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
  55. package/project-template/.claude/skills/playcraft-asset-state-sheet/SKILL.md +141 -0
  56. package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +280 -0
  57. package/project-template/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
  58. package/project-template/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
  59. package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +279 -0
  60. package/project-template/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
  61. package/project-template/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
  62. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +235 -0
  63. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +97 -0
  64. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
  65. package/project-template/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +137 -0
  66. package/project-template/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
  67. package/project-template/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
  68. package/project-template/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
  69. package/project-template/.claude/skills/playcraft-masking/SKILL.md +373 -0
  70. package/project-template/.claude/skills/playcraft-research/SKILL.md +212 -0
  71. package/project-template/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
  72. package/project-template/.claude/skills/playcraft-storyboard/SKILL.md +167 -0
  73. package/project-template/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
  74. package/project-template/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
  75. package/project-template/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
  76. package/project-template/.claude/skills/playcraft-workflow/SKILL.md +485 -0
  77. package/project-template/.claude/skills/playwright-cli/SKILL.md +390 -0
  78. package/project-template/.claude/skills/playwright-cli/references/element-attributes.md +23 -0
  79. package/project-template/.claude/skills/playwright-cli/references/playwright-tests.md +39 -0
  80. package/project-template/.claude/skills/playwright-cli/references/request-mocking.md +87 -0
  81. package/project-template/.claude/skills/playwright-cli/references/running-code.md +240 -0
  82. package/project-template/.claude/skills/playwright-cli/references/session-management.md +226 -0
  83. package/project-template/.claude/skills/playwright-cli/references/spec-driven-testing.md +312 -0
  84. package/project-template/.claude/skills/playwright-cli/references/storage-state.md +275 -0
  85. package/project-template/.claude/skills/playwright-cli/references/test-generation.md +138 -0
  86. package/project-template/.claude/skills/playwright-cli/references/tracing.md +142 -0
  87. package/project-template/.claude/skills/playwright-cli/references/video-recording.md +157 -0
  88. package/project-template/.cursor/hooks.json +17 -0
  89. package/project-template/.cursor/rules/playcraft-orchestrator.mdc +137 -0
  90. package/project-template/.cursor/rules/playcraft-subagent-boundary.mdc +18 -0
  91. package/project-template/CLAUDE.md +280 -0
  92. package/project-template/assets/audio/bgm/.gitkeep +0 -0
  93. package/project-template/assets/audio/sfx/.gitkeep +0 -0
  94. package/project-template/assets/bundles/.gitkeep +0 -0
  95. package/project-template/assets/images/bg/.gitkeep +0 -0
  96. package/project-template/assets/images/reference/.gitkeep +0 -0
  97. package/project-template/assets/images/storyboard/.gitkeep +0 -0
  98. package/project-template/assets/images/tiles/.gitkeep +0 -0
  99. package/project-template/assets/images/ui/.gitkeep +0 -0
  100. package/project-template/assets/images/vfx/.gitkeep +0 -0
  101. package/project-template/assets/models/.gitkeep +0 -0
  102. package/project-template/docs/team/agent-conduct.md +121 -0
  103. package/project-template/docs/team/agent-runtime-matrix.md +62 -0
  104. package/project-template/docs/team/atom-plan-format.md +105 -0
  105. package/project-template/docs/team/collaboration.md +297 -0
  106. package/project-template/docs/team/core-model.md +50 -0
  107. package/project-template/docs/team/platform-capabilities.md +15 -0
  108. package/project-template/docs/team/workflow-changelog.md +65 -0
  109. package/project-template/docs/team/workflow-consistency-checklist.md +140 -0
  110. package/project-template/game/config/.gitkeep +0 -0
  111. package/project-template/game/gameplay/.gitkeep +0 -0
  112. package/project-template/game/scenes/.gitkeep +0 -0
  113. package/project-template/logs/.gitkeep +0 -0
  114. package/project-template/ta-workspace/logs/.gitkeep +0 -0
  115. package/project-template/ta-workspace/scripts/.gitkeep +0 -0
  116. package/project-template/ta-workspace/tmp/.gitkeep +0 -0
  117. package/project-template/templates/atom-plan.template.json +26 -0
  118. package/project-template/templates/atom-plan.template.md +108 -0
  119. package/project-template/templates/design-brief.template.md +195 -0
  120. package/project-template/templates/design-lens-checklist.reference.md +117 -0
  121. package/project-template/templates/design-methodology.md +99 -0
  122. package/project-template/templates/designer-log.template.md +114 -0
  123. package/project-template/templates/developer-log.template.md +134 -0
  124. package/project-template/templates/five-axis-framework.md +186 -0
  125. package/project-template/templates/intent-clarifications.template.md +58 -0
  126. package/project-template/templates/layout-spec.template.md +146 -0
  127. package/project-template/templates/project-state.template.md +237 -0
  128. package/project-template/templates/review-report.template.md +91 -0
  129. package/project-template/templates/style-exploration.template.md +93 -0
  130. package/project-template/templates/ta-log.template.md +343 -0
@@ -0,0 +1,363 @@
1
+ import { existsSync, mkdirSync, readFileSync, statSync, } from 'fs';
2
+ import { basename, dirname, extname, join, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ const PRIMITIVES_DIR = join(__dirname, '../../assets/models/primitives');
6
+ const PRIMITIVES_METADATA = join(PRIMITIVES_DIR, 'metadata.json');
7
+ // ─────────────────────────────────────────────────────────
8
+ // Helpers
9
+ // ─────────────────────────────────────────────────────────
10
+ function sizeLabel(bytes) {
11
+ if (bytes < 1024)
12
+ return `${bytes}B`;
13
+ if (bytes < 1024 * 1024)
14
+ return `${(bytes / 1024).toFixed(1)}KB`;
15
+ return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
16
+ }
17
+ function handleError(e) {
18
+ const msg = e instanceof Error ? e.message : String(e);
19
+ console.error(`Error: ${msg}`);
20
+ process.exit(1);
21
+ }
22
+ function ensureOutputDir(outputPath) {
23
+ const dir = dirname(resolve(outputPath));
24
+ if (!existsSync(dir)) {
25
+ mkdirSync(dir, { recursive: true });
26
+ }
27
+ }
28
+ function loadPrimitivesMetadata() {
29
+ if (!existsSync(PRIMITIVES_METADATA)) {
30
+ console.error(`Error: primitives metadata not found at ${PRIMITIVES_METADATA}`);
31
+ console.error('Run: npx tsx scripts/generate-primitives.ts (from packages/cli)');
32
+ process.exit(1);
33
+ }
34
+ const raw = JSON.parse(readFileSync(PRIMITIVES_METADATA, 'utf-8'));
35
+ return raw.primitives;
36
+ }
37
+ // ─────────────────────────────────────────────────────────
38
+ // list-models
39
+ // ─────────────────────────────────────────────────────────
40
+ function registerListModelsCmd(parent) {
41
+ parent
42
+ .command('list-models')
43
+ .description('List all bundled white model primitives')
44
+ .option('--json', 'Output as JSON')
45
+ .action((opts) => {
46
+ try {
47
+ const primitives = loadPrimitivesMetadata();
48
+ if (opts.json) {
49
+ console.log(JSON.stringify({ primitives }, null, 2));
50
+ return;
51
+ }
52
+ console.log('\nBundled White Model Primitives:\n');
53
+ for (const p of primitives) {
54
+ const filePath = join(PRIMITIVES_DIR, p.file);
55
+ const size = existsSync(filePath) ? sizeLabel(statSync(filePath).size) : 'missing';
56
+ const dim = `${p.dimensions.width}×${p.dimensions.height}×${p.dimensions.depth}`;
57
+ console.log(` ${p.name.padEnd(12)} ${size.padEnd(8)} UV:${p.uvType.padEnd(14)} ${p.description}`);
58
+ console.log(` Best for: ${p.bestFor.join(', ')}`);
59
+ }
60
+ console.log(`\nUsage: playcraft 3d apply-texture --model <name> --texture <img> --output <out.glb>`);
61
+ }
62
+ catch (e) {
63
+ handleError(e);
64
+ }
65
+ });
66
+ }
67
+ // ─────────────────────────────────────────────────────────
68
+ // info
69
+ // ─────────────────────────────────────────────────────────
70
+ function registerInfoCmd(parent) {
71
+ parent
72
+ .command('info <model>')
73
+ .description('Show info about a GLB file or bundled white model')
74
+ .option('--json', 'Output as JSON')
75
+ .action(async (model, opts) => {
76
+ try {
77
+ const { NodeIO } = await import('@gltf-transform/core');
78
+ let filePath = model;
79
+ // Check if it's a bundled primitive name (no extension)
80
+ if (!extname(model) && !existsSync(model)) {
81
+ const primitives = loadPrimitivesMetadata();
82
+ const found = primitives.find((p) => p.name === model);
83
+ if (found) {
84
+ filePath = join(PRIMITIVES_DIR, found.file);
85
+ }
86
+ else {
87
+ console.error(`Error: model '${model}' not found. Use 'playcraft 3d list-models' to see available models.`);
88
+ process.exit(1);
89
+ }
90
+ }
91
+ if (!existsSync(filePath)) {
92
+ console.error(`Error: file not found: ${filePath}`);
93
+ process.exit(1);
94
+ }
95
+ const io = new NodeIO();
96
+ const doc = await io.read(filePath);
97
+ const root = doc.getRoot();
98
+ const meshes = root.listMeshes();
99
+ const meshInfo = meshes.map((mesh) => {
100
+ const primitives = mesh.listPrimitives();
101
+ return {
102
+ name: mesh.getName(),
103
+ primitives: primitives.map((prim) => {
104
+ const posAttr = prim.getAttribute('POSITION');
105
+ const normAttr = prim.getAttribute('NORMAL');
106
+ const uvAttr = prim.getAttribute('TEXCOORD_0');
107
+ const indices = prim.getIndices();
108
+ const vertCount = posAttr?.getCount() ?? 0;
109
+ const triCount = indices ? indices.getCount() / 3 : 0;
110
+ return {
111
+ vertices: vertCount,
112
+ triangles: Math.round(triCount),
113
+ hasNormals: !!normAttr,
114
+ hasUVs: !!uvAttr,
115
+ };
116
+ }),
117
+ };
118
+ });
119
+ const fileSize = statSync(filePath).size;
120
+ const info = {
121
+ file: filePath,
122
+ size: sizeLabel(fileSize),
123
+ sizeBytes: fileSize,
124
+ meshes: meshInfo,
125
+ totalVertices: meshInfo.reduce((s, m) => s + m.primitives.reduce((ps, p) => ps + p.vertices, 0), 0),
126
+ totalTriangles: meshInfo.reduce((s, m) => s + m.primitives.reduce((ps, p) => ps + p.triangles, 0), 0),
127
+ };
128
+ if (opts.json) {
129
+ console.log(JSON.stringify(info, null, 2));
130
+ return;
131
+ }
132
+ console.log(`\nGLB Info: ${info.file}`);
133
+ console.log(` Size: ${info.size}`);
134
+ console.log(` Meshes: ${meshInfo.length}`);
135
+ console.log(` Total vertices: ${info.totalVertices}`);
136
+ console.log(` Total triangles: ${info.totalTriangles}`);
137
+ for (const mesh of meshInfo) {
138
+ for (const prim of mesh.primitives) {
139
+ console.log(` Normals: ${prim.hasNormals ? '✓' : '✗'} UVs: ${prim.hasUVs ? '✓' : '✗'}`);
140
+ }
141
+ }
142
+ }
143
+ catch (e) {
144
+ handleError(e);
145
+ }
146
+ });
147
+ }
148
+ // ─────────────────────────────────────────────────────────
149
+ // apply-texture
150
+ // ─────────────────────────────────────────────────────────
151
+ function registerApplyTextureCmd(parent) {
152
+ parent
153
+ .command('apply-texture')
154
+ .description('Apply an image texture to a white model GLB')
155
+ .requiredOption('--model <name>', 'Bundled primitive name (sphere, cube, etc.) or path to a GLB file')
156
+ .requiredOption('--texture <path>', 'Image file path (PNG/JPG/WebP) to use as base color texture')
157
+ .requiredOption('--output <path>', 'Output GLB file path')
158
+ .option('--scale <n>', 'Uniform scale factor (default: 1.0)', '1.0')
159
+ .option('--color <hex>', 'Solid base color instead of texture (e.g. "#FF4400")')
160
+ .option('--roughness <n>', 'Material roughness 0.0–1.0 (default: 0.8)', '0.8')
161
+ .option('--metallic <n>', 'Material metalness 0.0–1.0 (default: 0.0)', '0.0')
162
+ .action(async (opts) => {
163
+ try {
164
+ const { NodeIO, TextureChannel } = await import('@gltf-transform/core');
165
+ const sharp = (await import('sharp')).default;
166
+ // Resolve source GLB
167
+ let sourceGlb = opts.model;
168
+ if (!extname(opts.model) && !existsSync(opts.model)) {
169
+ const primitives = loadPrimitivesMetadata();
170
+ const found = primitives.find((p) => p.name === opts.model);
171
+ if (!found) {
172
+ console.error(`Error: model '${opts.model}' not found. Use 'playcraft 3d list-models' to see available models.`);
173
+ process.exit(1);
174
+ }
175
+ sourceGlb = join(PRIMITIVES_DIR, found.file);
176
+ }
177
+ if (!existsSync(sourceGlb)) {
178
+ console.error(`Error: GLB file not found: ${sourceGlb}`);
179
+ process.exit(1);
180
+ }
181
+ const io = new NodeIO();
182
+ const doc = await io.read(sourceGlb);
183
+ // Apply scale
184
+ const scale = parseFloat(opts.scale);
185
+ if (scale !== 1.0) {
186
+ for (const node of doc.getRoot().listNodes()) {
187
+ const cur = node.getScale();
188
+ node.setScale([cur[0] * scale, cur[1] * scale, cur[2] * scale]);
189
+ }
190
+ }
191
+ // Create/update material
192
+ for (const mesh of doc.getRoot().listMeshes()) {
193
+ for (const prim of mesh.listPrimitives()) {
194
+ let mat = prim.getMaterial();
195
+ if (!mat) {
196
+ mat = doc.createMaterial('material');
197
+ prim.setMaterial(mat);
198
+ }
199
+ const roughness = parseFloat(opts.roughness);
200
+ const metallic = parseFloat(opts.metallic);
201
+ mat.setRoughnessFactor(roughness);
202
+ mat.setMetallicFactor(metallic);
203
+ if (opts.color) {
204
+ // Parse hex color
205
+ const hex = opts.color.replace('#', '');
206
+ const r = parseInt(hex.substring(0, 2), 16) / 255;
207
+ const g = parseInt(hex.substring(2, 4), 16) / 255;
208
+ const b = parseInt(hex.substring(4, 6), 16) / 255;
209
+ mat.setBaseColorFactor([r, g, b, 1]);
210
+ mat.setBaseColorTexture(null);
211
+ }
212
+ else if (opts.texture) {
213
+ // Load texture image
214
+ if (!existsSync(opts.texture)) {
215
+ console.error(`Error: texture file not found: ${opts.texture}`);
216
+ process.exit(1);
217
+ }
218
+ // Resize to power-of-2 for WebGL compatibility
219
+ const imgInfo = await sharp(opts.texture).metadata();
220
+ const w = imgInfo.width ?? 512;
221
+ const h = imgInfo.height ?? 512;
222
+ const pow2w = Math.pow(2, Math.ceil(Math.log2(Math.min(w, 1024))));
223
+ const pow2h = Math.pow(2, Math.ceil(Math.log2(Math.min(h, 1024))));
224
+ const imgBuffer = await sharp(opts.texture)
225
+ .resize(pow2w, pow2h, { fit: 'fill' })
226
+ .png()
227
+ .toBuffer();
228
+ const texture = doc
229
+ .createTexture(basename(opts.texture))
230
+ .setImage(imgBuffer)
231
+ .setMimeType('image/png');
232
+ mat.setBaseColorTexture(texture);
233
+ mat.setBaseColorFactor([1, 1, 1, 1]);
234
+ }
235
+ }
236
+ }
237
+ ensureOutputDir(opts.output);
238
+ await io.write(opts.output, doc);
239
+ const outSize = statSync(opts.output).size;
240
+ console.log(`Texture applied: ${opts.output} (${sizeLabel(outSize)})`);
241
+ if (opts.texture) {
242
+ console.log(` Source model: ${sourceGlb}`);
243
+ console.log(` Texture: ${opts.texture}`);
244
+ }
245
+ if (opts.color) {
246
+ console.log(` Base color: ${opts.color}`);
247
+ }
248
+ }
249
+ catch (e) {
250
+ handleError(e);
251
+ }
252
+ });
253
+ }
254
+ // ─────────────────────────────────────────────────────────
255
+ // convert
256
+ // ─────────────────────────────────────────────────────────
257
+ function registerConvertCmd(parent) {
258
+ parent
259
+ .command('convert')
260
+ .description('Convert between GLB and OBJ formats')
261
+ .requiredOption('--input <path>', 'Input file path (GLB)')
262
+ .requiredOption('--output <path>', 'Output file path (GLB or OBJ)')
263
+ .action(async (opts) => {
264
+ try {
265
+ const { NodeIO } = await import('@gltf-transform/core');
266
+ if (!existsSync(opts.input)) {
267
+ console.error(`Error: input file not found: ${opts.input}`);
268
+ process.exit(1);
269
+ }
270
+ const inExt = extname(opts.input).toLowerCase();
271
+ const outExt = extname(opts.output).toLowerCase();
272
+ if (!['.glb', '.gltf'].includes(inExt)) {
273
+ console.error('Error: input must be a .glb or .gltf file');
274
+ process.exit(1);
275
+ }
276
+ if (outExt === '.glb' || outExt === '.gltf') {
277
+ // GLB ↔ GLTF conversion
278
+ const io = new NodeIO();
279
+ const doc = await io.read(opts.input);
280
+ ensureOutputDir(opts.output);
281
+ await io.write(opts.output, doc);
282
+ const outSize = statSync(opts.output).size;
283
+ console.log(`Converted: ${opts.input} → ${opts.output} (${sizeLabel(outSize)})`);
284
+ }
285
+ else {
286
+ console.error('Error: output must be .glb or .gltf (OBJ export not yet supported)');
287
+ process.exit(1);
288
+ }
289
+ }
290
+ catch (e) {
291
+ handleError(e);
292
+ }
293
+ });
294
+ }
295
+ // ─────────────────────────────────────────────────────────
296
+ // optimize
297
+ // ─────────────────────────────────────────────────────────
298
+ function registerOptimizeCmd(parent) {
299
+ parent
300
+ .command('optimize')
301
+ .description('Optimize a GLB to reduce file size (texture resize, dedup)')
302
+ .requiredOption('--input <path>', 'Input GLB file')
303
+ .requiredOption('--output <path>', 'Output optimized GLB file')
304
+ .option('--texture-size <n>', 'Max texture dimension (default: 512)', '512')
305
+ .option('--target-size <s>', 'Target size hint (e.g. 200KB) — informational only')
306
+ .action(async (opts) => {
307
+ try {
308
+ const { NodeIO, TextureChannel } = await import('@gltf-transform/core');
309
+ const { dedup, prune } = await import('@gltf-transform/functions');
310
+ void TextureChannel; // unused but imported for type context
311
+ if (!existsSync(opts.input)) {
312
+ console.error(`Error: input file not found: ${opts.input}`);
313
+ process.exit(1);
314
+ }
315
+ const origSize = statSync(opts.input).size;
316
+ const io = new NodeIO();
317
+ const doc = await io.read(opts.input);
318
+ const maxTexSize = parseInt(opts.textureSize, 10);
319
+ const sharp = (await import('sharp')).default;
320
+ // Resize textures that exceed maxTexSize using sharp
321
+ const textures = doc.getRoot().listTextures();
322
+ for (const tex of textures) {
323
+ const imgData = tex.getImage();
324
+ if (!imgData)
325
+ continue;
326
+ const meta = await sharp(Buffer.from(imgData)).metadata();
327
+ const w = meta.width ?? 0;
328
+ const h = meta.height ?? 0;
329
+ if (w > maxTexSize || h > maxTexSize) {
330
+ const resized = await sharp(Buffer.from(imgData))
331
+ .resize(maxTexSize, maxTexSize, { fit: 'inside', withoutEnlargement: true })
332
+ .png()
333
+ .toBuffer();
334
+ tex.setImage(new Uint8Array(resized)).setMimeType('image/png');
335
+ }
336
+ }
337
+ await doc.transform(dedup(), prune());
338
+ ensureOutputDir(opts.output);
339
+ await io.write(opts.output, doc);
340
+ const outSize = statSync(opts.output).size;
341
+ const saving = origSize - outSize;
342
+ const savingPct = ((saving / origSize) * 100).toFixed(1);
343
+ console.log(`Optimized: ${opts.output}`);
344
+ console.log(` ${sizeLabel(origSize)} → ${sizeLabel(outSize)} (saved ${sizeLabel(saving)}, ${savingPct}%)`);
345
+ }
346
+ catch (e) {
347
+ handleError(e);
348
+ }
349
+ });
350
+ }
351
+ // ─────────────────────────────────────────────────────────
352
+ // Register all `playcraft 3d` subcommands
353
+ // ─────────────────────────────────────────────────────────
354
+ export function register3DCommands(program) {
355
+ const threeD = program
356
+ .command('3d')
357
+ .description('3D model utilities — white model library, texture application, GLB optimization');
358
+ registerListModelsCmd(threeD);
359
+ registerInfoCmd(threeD);
360
+ registerApplyTextureCmd(threeD);
361
+ registerConvertCmd(threeD);
362
+ registerOptimizeCmd(threeD);
363
+ }