@playcraft/cli 0.1.63 → 0.1.65

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 (41) hide show
  1. package/README.md +24 -31
  2. package/SKILLS_REGISTRY.md +1 -1
  3. package/dist/commands/3d.js +41 -33
  4. package/dist/commands/build.js +70 -5
  5. package/dist/commands/create.js +4 -4
  6. package/dist/commands/inspect.js +4 -6
  7. package/dist/index.js +27 -19
  8. package/dist/model-adaptation/node-inspection.js +173 -0
  9. package/dist/model-adaptation/project-template.js +194 -0
  10. package/dist/model-adaptation/project.js +76 -0
  11. package/dist/model-adaptation/source.js +49 -0
  12. package/dist/model-optimization/optimize.js +281 -0
  13. package/dist/model-preparation/bindings.js +35 -0
  14. package/dist/model-preparation/commands.js +28 -0
  15. package/dist/model-preparation/geometry.js +241 -0
  16. package/dist/model-preparation/glb.js +176 -0
  17. package/dist/model-preparation/pipeline-engine.js +157 -0
  18. package/dist/model-preparation/pipeline-files.js +108 -0
  19. package/dist/model-preparation/pipeline-worker.js +32 -0
  20. package/dist/model-preparation/split-rigid.js +302 -0
  21. package/dist/model-preparation/vertex-repairs.js +63 -0
  22. package/dist/workspace-runtime/batch/deliver.js +2 -2
  23. package/dist/workspace-runtime/codex/app-server.js +11 -1
  24. package/dist/workspace-runtime/codex/delivery-tools.js +38 -5
  25. package/dist/workspace-runtime/codex/model-broker.js +19 -1
  26. package/dist/workspace-runtime/previews/publish.js +2 -2
  27. package/dist/workspace-runtime/serve.js +5 -0
  28. package/dist/workspace-runtime/server/directory-access.js +1 -1
  29. package/dist/workspace-runtime/server/dispatch.js +27 -47
  30. package/dist/workspace-runtime/server/http.js +12 -0
  31. package/dist/workspace-runtime/workspaces/build-isolation.js +75 -0
  32. package/dist/workspace-runtime/workspaces/model-build-inputs.js +27 -0
  33. package/dist/workspace-runtime/workspaces/model-preparation-store.js +134 -0
  34. package/dist/workspace-runtime/workspaces/model-preparation.js +357 -0
  35. package/dist/workspace-runtime/workspaces/platform-export.js +1 -0
  36. package/package.json +6 -4
  37. package/project-template/.claude/skills/playcraft-build-optimizer/SKILL.md +19 -39
  38. package/project-templates/empty/AGENTS.md +1 -1
  39. package/project-templates/phaser/.claude/skills/playcraft-build-optimizer/SKILL.md +19 -39
  40. package/project-templates/threejs/.claude/skills/playcraft-build-optimizer/SKILL.md +19 -39
  41. package/dist/workspace-runtime/workspaces/delivery-build.js +0 -440
package/README.md CHANGED
@@ -258,16 +258,16 @@ PLAYCRAFT_TOOL_REQUEST_ID=<错误中显示的请求编号> playcraft tools build
258
258
 
259
259
  ### 构建相关
260
260
 
261
- | 命令 | 说明 | 示例 |
262
- | ---------------- | ---------------------------------------- | ------------------------------------------------------ |
263
- | `build` | 完整打包(阶段1 + 阶段2) | `playcraft build --platform facebook` |
264
- | `build-base` | 仅执行阶段1(生成多文件产物) | `playcraft build-base ./src --output ./build` |
265
- | `build-playable` | 仅执行阶段2(从多文件产物生成 Playable) | `playcraft build-playable ./build --platform facebook` |
266
- | `analyze` | 生成打包分析报告 | `playcraft analyze --platform facebook` |
261
+ | 命令 | 说明 | 示例 |
262
+ | ---------------- | ---------------------------------------------- | ------------------------------------------------------ |
263
+ | `build` | 本地构建预览,产物为 `dist/preview/index.html` | `playcraft build` |
264
+ | `build-base` | 仅执行阶段1(生成多文件产物) | `playcraft build-base ./src --output ./build` |
265
+ | `build-playable` | 仅执行阶段2(从多文件产物生成 Playable) | `playcraft build-playable ./build --platform facebook` |
266
+ | `analyze` | 生成打包分析报告 | `playcraft analyze --platform facebook` |
267
267
 
268
268
  ## 🎯 支持的平台
269
269
 
270
- 现已支持 **10 个主流广告平台**:
270
+ 平台导出支持以下主流广告平台(渠道包通过平台导出生成,`playcraft build` 只做本地预览构建):
271
271
 
272
272
  | 平台 | 格式 | 大小限制 | SDK/API |
273
273
  | ----------------- | -------- | -------------------- | ------------ |
@@ -296,45 +296,39 @@ playcraft start --project your-project-id
296
296
  # 3. 在本地 IDE 编辑代码,云端编辑器自动同步
297
297
  ```
298
298
 
299
- ### 打包 Playable Ads
299
+ ### 本地构建预览
300
300
 
301
301
  ```bash
302
- # Facebook 平台(ZIP,Phaser 产物格式)
303
- playcraft build --platform facebook --format zip --output ./dist/facebook
304
-
305
- # Snapchat 平台(ZIP)
306
- playcraft build --platform snapchat --format zip --output ./dist/snapchat
307
-
308
- # 批量打包所有平台
309
- for platform in facebook snapchat ironsource applovin google tiktok unity liftoff moloco bigo; do
310
- playcraft build --platform $platform --output ./dist/$platform
311
- done
302
+ # 在项目根目录构建,产物固定为 dist/preview/index.html
303
+ playcraft build
312
304
  ```
313
305
 
306
+ `playcraft build` 不提供渠道、格式和输出目录参数,各广告平台的渠道包请通过平台导出生成。下方的优化参数只影响本地预览,平台导出不会读取它们。
307
+
314
308
  ### 构建优化
315
309
 
316
310
  ```bash
317
311
  # 🎬 场景选择(推荐!可减小 30-60% 文件大小)
318
312
  # 交互式选择(自动检测多个场景并提示)
319
- playcraft build --platform facebook
313
+ playcraft build
320
314
 
321
315
  # 指定特定场景
322
- playcraft build --platform facebook --scenes BallGame
316
+ playcraft build --scenes BallGame
323
317
 
324
318
  # 指定多个场景
325
- playcraft build --platform facebook --scenes "MainMenu,Gameplay,Settings"
319
+ playcraft build --scenes "MainMenu,Gameplay,Settings"
326
320
 
327
321
  # 启用图片压缩
328
- playcraft build --platform facebook --compress-images --image-quality 75
322
+ playcraft build --compress-images --image-quality 75
329
323
 
330
324
  # 启用模型压缩
331
- playcraft build --platform facebook --compress-models --model-compression draco
325
+ playcraft build --compress-models --model-compression draco
332
326
 
333
327
  # 生成分析报告
334
- playcraft build --platform facebook --analyze
328
+ playcraft build --analyze
335
329
 
336
330
  # 组合使用(场景选择 + 图片压缩 + 分析)
337
- playcraft build --platform facebook --scenes MainScene --compress-images --analyze
331
+ playcraft build --scenes MainScene --compress-images --analyze
338
332
  ```
339
333
 
340
334
  ## ⚙️ 配置
@@ -350,9 +344,6 @@ playcraft build --platform facebook --scenes MainScene --compress-images --analy
350
344
  "dir": "./"
351
345
  },
352
346
  "build": {
353
- "platform": "facebook",
354
- "format": "html",
355
- "outputDir": "./dist",
356
347
  "useVite": true,
357
348
  "compressImages": true,
358
349
  "imageQuality": 75,
@@ -370,6 +361,8 @@ playcraft build --platform facebook --scenes MainScene --compress-images --analy
370
361
  3. 配置文件(`playcraft.config.json`)
371
362
  4. 默认值
372
363
 
364
+ `build` 字段只为本地构建补充命令行没有给出的优化参数。`playcraft build` 的渠道、格式和输出位置是固定的,配置文件里的 `platform`、`format`、`outputDir` 对它不起作用;渠道包请通过平台导出生成,平台导出不读取这个文件。
365
+
373
366
  ## 📁 数据目录
374
367
 
375
368
  CLI 会在 `~/.playcraft/` 目录下存储:
@@ -384,7 +377,7 @@ CLI 会在 `~/.playcraft/` 目录下存储:
384
377
  当项目包含多个场景时,CLI 会自动提供交互式场景选择:
385
378
 
386
379
  ```bash
387
- playcraft build --platform facebook
380
+ playcraft build
388
381
  ```
389
382
 
390
383
  **交互界面**:
@@ -411,10 +404,10 @@ playcraft build --platform facebook
411
404
 
412
405
  ```bash
413
406
  # 指定单个场景
414
- playcraft build --platform facebook --scenes MainScene
407
+ playcraft build --scenes MainScene
415
408
 
416
409
  # 指定多个场景
417
- playcraft build --platform facebook --scenes "MainScene,Gameplay"
410
+ playcraft build --scenes "MainScene,Gameplay"
418
411
  ```
419
412
 
420
413
  详见:[场景选择功能文档](../../docs/cli/scene-selection-feature.md)
@@ -90,7 +90,7 @@ playcraft skills registry platform push ./playcraft-video-understanding-1.0.1.ta
90
90
  playcraft skills registry platform push ./playcraft-replica-validation-1.0.1.tar.gz --changelog '更新复刻验收指引与评估用例' --json
91
91
  ```
92
92
 
93
- `platform push <source>` 接受完整目录或已准备的压缩包。目录必须已含原生 `manifest.json`;命令保留其文件内容,不补充托管技能默认字段。已有压缩包原样发送,不重打包。可选的 `--ref`、`--version` 必须与清单一致;可用 `--evaluation-template <templateId>` 指定初始评估模板。原生入口当前仅接受上述两个引用,要求平台类型、单一 `sandbox-agent` surface、对齐的名称与版本、无 imports/relationships;服务端执行完整校验与摘要归一化,并返回最终 `artifactHash`。登记不会自动评估、审核或发布。
93
+ `platform push <source>` 接受完整目录或已准备的压缩包。目录必须已含原生 `manifest.json`;命令保留其文件内容,不补充托管技能默认字段。已有压缩包原样发送,不重打包。可选的 `--ref`、`--version` 必须与清单一致;可用 `--evaluation-template <templateId>` 指定初始评估模板。原生入口按公共内置技能表允许引用,包含上述两项视频指南及显式导入的 `remix/playcraft-procedural-3d`、`remix/playcraft-model-adaptation`;要求平台类型、单一 `sandbox-agent` surface、对齐的名称与版本、无 imports/relationships。服务端执行完整校验与摘要归一化,并返回最终 `artifactHash`。登记不会自动评估、审核或发布。模型适配指南的独立源版本与门禁见[发布说明](../skills/docs/model-adaptation-skill-release.md)。
94
94
 
95
95
  此入口要求具有平台管理权限的真实账号凭据;命令复用目标环境登录,不发送本地身份模拟请求头。服务端需已部署允许真实管理员个人访问令牌访问原生登记和评估接口的版本。
96
96
 
@@ -1,6 +1,10 @@
1
1
  import { existsSync, mkdirSync, readFileSync, statSync, } from 'fs';
2
2
  import { basename, dirname, extname, join, resolve } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
+ import { inspectModelSourceFiles } from '../model-adaptation/source.js';
5
+ import { generateModelAdaptationProject } from '../model-adaptation/project.js';
6
+ import { optimizeModel } from '../model-optimization/optimize.js';
7
+ import { registerModelPreparationCommands } from '../model-preparation/commands.js';
4
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
5
9
  const PRIMITIVES_DIR = join(__dirname, '../../assets/models/primitives');
6
10
  const PRIMITIVES_METADATA = join(PRIMITIVES_DIR, 'metadata.json');
@@ -302,46 +306,23 @@ function registerOptimizeCmd(parent) {
302
306
  .requiredOption('--input <path>', 'Input GLB file')
303
307
  .requiredOption('--output <path>', 'Output optimized GLB file')
304
308
  .option('--texture-size <n>', 'Max texture dimension (default: 512)', '512')
309
+ .option('--texture-format <format>', 'Explicit texture format: png or jpeg (opaque color textures only)')
310
+ .option('--quality <n>', 'JPEG texture quality, 1–100 (default: 85)', '85')
311
+ .option('--textures-only', 'Preserve geometry and node structure; do not deduplicate or prune')
312
+ .option('--max-output-bytes <n>', 'Reject output larger than this explicit byte budget')
313
+ .option('--lineage <path>', 'New independent optimization receipt (default: <output>.optimization.json)')
305
314
  .option('--target-size <s>', 'Target size hint (e.g. 200KB) — informational only')
306
315
  .action(async (opts) => {
307
316
  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;
317
+ const optimized = await optimizeModel({ input: opts.input, output: opts.output, textureSize: Number(opts.textureSize),
318
+ textureFormat: opts.textureFormat, quality: Number(opts.quality), texturesOnly: opts.texturesOnly,
319
+ maxOutputBytes: opts.maxOutputBytes === undefined ? undefined : Number(opts.maxOutputBytes), lineagePath: opts.lineage });
320
+ const origSize = optimized.lineage.before.modelBytes, outSize = optimized.lineage.after.modelBytes;
341
321
  const saving = origSize - outSize;
342
322
  const savingPct = ((saving / origSize) * 100).toFixed(1);
343
323
  console.log(`Optimized: ${opts.output}`);
344
324
  console.log(` ${sizeLabel(origSize)} → ${sizeLabel(outSize)} (saved ${sizeLabel(saving)}, ${savingPct}%)`);
325
+ console.log(` Lineage: ${optimized.lineagePath}`);
345
326
  }
346
327
  catch (e) {
347
328
  handleError(e);
@@ -360,4 +341,31 @@ export function register3DCommands(program) {
360
341
  registerApplyTextureCmd(threeD);
361
342
  registerConvertCmd(threeD);
362
343
  registerOptimizeCmd(threeD);
344
+ registerModelPreparationCommands(threeD);
345
+ threeD.command('inspect-source <model>').description('Inspect saved-model dependencies without rendering or inference')
346
+ .option('--project-root <directory>', 'Source project directory', process.cwd()).option('--json', 'Output structured source metadata')
347
+ .action(async (model, options) => {
348
+ try {
349
+ const result = await inspectModelSourceFiles({ projectRoot: options.projectRoot, modelPath: model });
350
+ console.log(JSON.stringify({ source: result.source, packedHash: result.packedHash, metadata: result.metadata, modelInference: false, acceptance: false }, null, 2));
351
+ }
352
+ catch (error) {
353
+ handleError(error);
354
+ }
355
+ });
356
+ threeD.command('adapt-project').description('Prepare a new rigid-model adaptation project without building or publishing')
357
+ .requiredOption('--config <file>', 'Source-bound adaptation configuration').option('--project-root <directory>', 'Source project directory', process.cwd())
358
+ .requiredOption('--output <directory>', 'New output directory').option('--candidate-id <id>', 'Explicit source candidate identity')
359
+ .option('--fixture-only', 'Mark an engineering fixture without granting acceptance')
360
+ .action(async (options) => {
361
+ try {
362
+ const result = await generateModelAdaptationProject({ projectRoot: options.projectRoot, planPath: options.config, outputRoot: options.output,
363
+ candidateId: options.candidateId, fixtureOnly: options.fixtureOnly });
364
+ console.log(JSON.stringify({ outputRoot: result.outputRoot, source: result.manifest.source, coreHash: result.manifest.coreHash,
365
+ packedHash: result.manifest.packedHash, geometryValidation: result.manifest.geometryValidation, modelInference: false, acceptance: false }, null, 2));
366
+ }
367
+ catch (error) {
368
+ handleError(error);
369
+ }
370
+ });
363
371
  }
@@ -8,9 +8,10 @@ import { cleanupBuildTempDirectory, createBuildTempDirectory } from './build-tem
8
8
  import inquirer from 'inquirer';
9
9
  import { tmpdir } from 'node:os';
10
10
  import { generatedWorkspace } from '../workspace-runtime/workspaces/artifact-receipts.js';
11
- import { isolateWorkspaceForBuild } from '../workspace-runtime/workspaces/delivery-build.js';
11
+ import { isolateWorkspaceForBuild } from '../workspace-runtime/workspaces/build-isolation.js';
12
12
  import { finalizeWorkspacePlayable, readWorkspacePlayable, workspacePlayableSignature } from '../workspace-runtime/previews/finalize.js';
13
13
  import { AgentApiClient } from '../utils/agent-api-client.js';
14
+ import { WORKSPACE_PLAYABLE_OUTPUT_PATH } from '@playcraft/common/cloud-workspace/runtime';
14
15
  /**
15
16
  * 类型守卫:检查对象是否符合 PlayableScriptsConfig 类型
16
17
  */
@@ -247,13 +248,52 @@ async function analyzeBaseBuild(baseBuildDir, ammoReplacement) {
247
248
  suggestedEngine: ammoCheck.suggestedEngine,
248
249
  };
249
250
  }
251
+ /**
252
+ * Format and clean the former `playcraft build` flags carried. Vite and draco model compression are
253
+ * already the builder's defaults, and leaving them unset lets playcraft.config.json supply them.
254
+ */
255
+ const BUILD_DEFAULTS = { format: 'html', clean: true };
256
+ /**
257
+ * `playcraft build`: a local review build of the current project. It always produces one HTML file at
258
+ * `dist/preview/index.html`; channel packaging is done by platform export, not by this command. The
259
+ * tuning options (compression, scenes, engine, ...) apply to this preview only.
260
+ * `output` and `base` are internal: the preview publisher builds into its own scratch folder, and the
261
+ * batch runtime only needs the multi-file base build.
262
+ */
263
+ export async function previewBuildCommand(projectPath, options = {}) {
264
+ const { output, base, ...tuning } = options;
265
+ const project = path.resolve(projectPath);
266
+ if (base)
267
+ return buildCommand(project, { ...BUILD_DEFAULTS, ...tuning, mode: 'base', ...(output ? { output } : {}) });
268
+ const preview = { ...BUILD_DEFAULTS, ...tuning, mode: 'full', platform: 'playcraft' };
269
+ if (output)
270
+ return buildCommand(project, { ...preview, output });
271
+ const workspace = process.env.PLAYCRAFT_BUILD_ISOLATED !== '1' ? await generatedWorkspace(project) : undefined;
272
+ // A workspace build isolates, validates and publishes the canonical preview itself.
273
+ if (workspace?.root === project)
274
+ return buildCommand(project, preview);
275
+ const scratch = await fs.mkdtemp(path.join(tmpdir(), 'playcraft-preview-build-'));
276
+ try {
277
+ await buildCommand(project, { ...preview, output: scratch });
278
+ const entries = await fs.readdir(scratch, { withFileTypes: true });
279
+ if (entries.length !== 1 || !entries[0].isFile() || !entries[0].name.endsWith('.html'))
280
+ throw new Error('preview_not_single_file');
281
+ const target = path.join(project, WORKSPACE_PLAYABLE_OUTPUT_PATH);
282
+ await fs.mkdir(path.dirname(target), { recursive: true });
283
+ await fs.copyFile(path.join(scratch, entries[0].name), target);
284
+ console.log(target);
285
+ }
286
+ finally {
287
+ await fs.rm(scratch, { recursive: true, force: true });
288
+ }
289
+ }
250
290
  export async function buildCommand(projectPath, options) {
251
291
  // 解析项目路径 - 在 spinner 之前执行以便调试
252
292
  const resolvedProjectPath = path.resolve(projectPath);
253
293
  const workspace = options.workspaceArtifact !== false && process.env.PLAYCRAFT_BUILD_ISOLATED !== '1' && options.mode === 'full' && options.platform === 'playcraft'
254
294
  && (!options.format || options.format === 'html') ? await generatedWorkspace(resolvedProjectPath) : undefined;
255
295
  const requestedOutput = options.output ? path.resolve(options.output) : path.join(resolvedProjectPath, 'dist');
256
- const canonicalOutput = [path.join(resolvedProjectPath, 'dist'), path.join(resolvedProjectPath, 'dist/playable')].includes(requestedOutput);
296
+ const canonicalOutput = [path.join(resolvedProjectPath, 'dist'), path.join(resolvedProjectPath, path.dirname(WORKSPACE_PLAYABLE_OUTPUT_PATH))].includes(requestedOutput);
257
297
  if (workspace?.root === resolvedProjectPath) {
258
298
  const prefix = requestedOutput.endsWith(path.sep) ? requestedOutput : `${requestedOutput}${path.sep}`;
259
299
  if (requestedOutput === workspace.root || workspace.root.startsWith(prefix))
@@ -269,7 +309,7 @@ export async function buildCommand(projectPath, options) {
269
309
  if (entries.length !== 1 || !entries[0].isFile() || !entries[0].name.endsWith('.html'))
270
310
  throw new Error('preview_not_single_file');
271
311
  await finalizeWorkspacePlayable(workspace.root, await fs.readFile(path.join(output, entries[0].name)), 'playcraft build', { expectedSignature: signature });
272
- console.log(path.join(workspace.root, 'dist/playable/index.html'));
312
+ console.log(path.join(workspace.root, WORKSPACE_PLAYABLE_OUTPUT_PATH));
273
313
  try {
274
314
  const { bytes } = await readWorkspacePlayable(workspace.root);
275
315
  const { previewUrl } = await new AgentApiClient().publishWorkspacePlayable({ workspaceId: workspace.workspaceId, directoryName: path.basename(workspace.root) }, bytes);
@@ -744,7 +784,32 @@ export async function buildCommand(projectPath, options) {
744
784
  ...(options.tracking === false ? { tracking: { enabled: false } } : {}),
745
785
  };
746
786
  if (fileConfig) {
747
- Object.assign(buildOptions, fileConfig);
787
+ // playcraft.config.json 只补充调用方没有给出的参数:显式参数,以及上面已确定的渠道、格式和
788
+ // 输出目录始终优先(playcraft build 固定这三项,配置文件不能把它改成渠道包或别的输出位置)。
789
+ const explicit = {
790
+ platform: buildOptions.platform,
791
+ format: buildOptions.format,
792
+ outputDir: buildOptions.outputDir,
793
+ compressEngine: options.compress,
794
+ compressConfigJson: options.compressConfig,
795
+ compressJS: options.compressJs,
796
+ analyze: options.analyze,
797
+ useVite: options.useVite,
798
+ cssMinify: options.cssMinify,
799
+ jsMinify: options.jsMinify,
800
+ compressImages: options.compressImages,
801
+ imageQuality: options.imageQuality,
802
+ convertToWebP: options.convertToWebP,
803
+ compressModels: options.compressModels,
804
+ modelCompression: options.modelCompression,
805
+ esmMode: options.esmMode,
806
+ storeUrls: options.storeUrls,
807
+ tracking: options.tracking === false ? false : undefined,
808
+ };
809
+ for (const [key, value] of Object.entries(fileConfig)) {
810
+ if (explicit[key] === undefined)
811
+ Object.assign(buildOptions, { [key]: value });
812
+ }
748
813
  }
749
814
  buildOptions.validation = {
750
815
  ...buildOptions.validation,
@@ -760,7 +825,7 @@ export async function buildCommand(projectPath, options) {
760
825
  if (isEditorProject) {
761
826
  throw new Error('检测到这是 PlayCanvas 编辑器项目,不是多文件构建产物。\n\n' +
762
827
  '请使用以下命令之一:\n' +
763
- ' 1. playcraft build <项目路径> # 完整构建(推荐)\n' +
828
+ ' 1. playcraft build <项目路径> # 本地构建预览(推荐)\n' +
764
829
  ' 2. playcraft build-base <项目路径> # 先生成多文件产物\n' +
765
830
  ' playcraft build-playable <产物路径> # 再转换为 Playable\n\n' +
766
831
  '💡 提示: build-playable 命令仅用于已有的多文件构建产物');
@@ -1067,8 +1067,8 @@ function printCreateNextSteps(template, _targetDir, name, here, worktree) {
1067
1067
  console.log(` ${pc.cyan('playcraft remix init-template --from-atom-tree')}`);
1068
1068
  console.log(` ${pc.cyan('npm run verify:contract && npm run dev')}`);
1069
1069
  console.log();
1070
- console.log(pc.dim('如需使用 PlayCraft CLI 打渠道包:'));
1071
- console.log(` ${pc.cyan('playcraft build . -p facebook')}`);
1070
+ console.log(pc.dim('本地构建预览(产物在 dist/preview/index.html,渠道包请使用平台导出):'));
1071
+ console.log(` ${pc.cyan('playcraft build .')}`);
1072
1072
  console.log();
1073
1073
  console.log(pc.dim('正式接入 PlayCraft 后,替换项目 ID:'));
1074
1074
  console.log(` ${pc.cyan('playcraft config set --key projectId --value <真实项目ID>')}`);
@@ -1081,8 +1081,8 @@ function printCreateNextSteps(template, _targetDir, name, here, worktree) {
1081
1081
  console.log(` ${pc.cyan('npm run build -- google')}`);
1082
1082
  console.log(` ${pc.cyan('npm run builds')}`);
1083
1083
  console.log();
1084
- console.log(pc.dim('或使用 PlayCraft CLI:'));
1085
- console.log(` ${pc.cyan(`playcraft build ${here ? '.' : name} -p facebook`)}`);
1084
+ console.log(pc.dim('或使用 PlayCraft CLI 本地构建预览(产物在 dist/preview/index.html):'));
1085
+ console.log(` ${pc.cyan(`playcraft build ${here ? '.' : name}`)}`);
1086
1086
  }
1087
1087
  console.log();
1088
1088
  }
@@ -106,18 +106,16 @@ export async function inspectCommand(projectPath) {
106
106
  console.log('\n' + pc.bold('💡 建议:\n'));
107
107
  if (projectType === 'build') {
108
108
  console.log(pc.green('✅ 这是一个构建后的项目,可以直接打包为 Playable Ad'));
109
- console.log(pc.dim(' 运行: playcraft build --platform facebook'));
109
+ console.log(pc.dim(' 运行: playcraft build'));
110
110
  }
111
111
  else if (projectType === 'playcraft-source' || projectType === 'playcanvas-source') {
112
112
  console.log(pc.yellow('⚠️ 这是源代码项目,建议先构建再打包\n'));
113
113
  console.log(pc.cyan('方式 1 (推荐): 使用 PlayCanvas REST API 构建'));
114
114
  console.log(pc.dim(' 1. 在 PlayCanvas Editor 中发布项目'));
115
115
  console.log(pc.dim(' 2. 使用 REST API 下载构建'));
116
- console.log(pc.dim(' 3. 运行: playcraft build --platform facebook\n'));
117
- console.log(pc.cyan('方式 2: 使用本地构建(实验性功能)'));
118
- console.log(pc.dim(' 运行: playcraft build --platform facebook --auto-build\n'));
119
- console.log(pc.cyan('方式 3: 直接从源代码打包(可能不完整)'));
120
- console.log(pc.dim(' 运行: playcraft build --platform facebook --skip-build'));
116
+ console.log(pc.dim(' 3. 运行: playcraft build\n'));
117
+ console.log(pc.cyan('方式 2: 直接本地构建'));
118
+ console.log(pc.dim(' 运行: playcraft build'));
121
119
  }
122
120
  else {
123
121
  console.log(pc.red('❌ 无法识别项目类型,请确保:'));
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { Command, CommanderError } from 'commander';
2
+ import { Command, CommanderError, Option } from 'commander';
3
3
  import { accountActivationMessage } from './auth/messages.js';
4
4
  import { readFileSync } from 'fs';
5
5
  import { fileURLToPath } from 'url';
@@ -11,7 +11,7 @@ import { stopCommand } from './commands/stop.js';
11
11
  import { statusCommand } from './commands/status.js';
12
12
  import { logsCommand } from './commands/logs.js';
13
13
  import { configCommand } from './commands/config.js';
14
- import { buildCommand } from './commands/build.js';
14
+ import { buildCommand, previewBuildCommand } from './commands/build.js';
15
15
  import { buildAllCommand } from './commands/build-all.js';
16
16
  import { upgradeCommand } from './commands/upgrade.js';
17
17
  import { checkForUpdates } from './utils/version-checker.js';
@@ -174,28 +174,34 @@ program
174
174
  token: options.token,
175
175
  });
176
176
  });
177
- // build 命令 - 完整流程(阶段1 + 阶段2)
177
+ /**
178
+ * 只保留命令行上显式给出的参数。commander 的默认值与构建器默认值相同,去掉后 playcraft.config.json
179
+ * 才能补充这些参数;否则默认值会被当成显式参数,压过配置文件。
180
+ */
181
+ function explicitOptions(command) {
182
+ const options = command.opts();
183
+ return Object.fromEntries(Object.keys(options)
184
+ .filter(key => command.getOptionValueSource(key) !== 'default')
185
+ .map(key => [key, options[key]]));
186
+ }
187
+ /** commander 把 --convert-webp / --no-convert-webp 解析成 convertWebp,构建器使用的字段是 convertToWebP。 */
188
+ function withConvertToWebP({ convertWebp, ...options }) {
189
+ return { ...options, convertToWebP: convertWebp };
190
+ }
191
+ // build 命令 - 本地构建当前项目用于 review,产物固定为 dist/preview/index.html;渠道导出走平台导出
178
192
  program
179
193
  .command('build')
180
- .description('打包项目为 Playable Ad(自动执行基础构建和渠道打包)')
194
+ .description('本地构建当前项目,产物输出到 dist/preview/index.html(渠道导出请使用平台导出)')
181
195
  .argument('[project-path]', '项目路径', process.cwd())
182
- .option('-p, --platform <platform>', '目标平台 (facebook|snapchat|ironsource|applovin|google|tiktok|unity|liftoff|moloco|bigo|inmobi|adikteev|remerge|mintegral)')
183
- .option('-f, --format <format>', '输出格式 (html|zip)', 'html')
184
- .option('-o, --output <path>', '输出目录', './dist')
185
196
  .option('-c, --config <file>', '配置文件路径')
186
197
  .option('-s, --scenes <scenes>', '选择场景: "all"=所有场景, "default"=默认场景, 或逗号分隔的场景名,如 "MainMenu,Gameplay"')
187
198
  .option('-e, --engine <engine>', '指定引擎类型 (playcanvas|phaser|pixijs|threejs|cocos|babylonjs|layaair|egret|generic),默认自动检测')
188
- .option('--clean', '构建前清理旧的输出目录(默认启用)', true)
189
- .option('--no-clean', '跳过清理旧的输出目录')
190
199
  .option('--compress', '压缩引擎代码(使用 LZ4,仅 PlayCanvas)')
191
200
  .option('--compress-config', '压缩 config.json(使用 LZ4,可减少 60-70% 体积,仅 PlayCanvas)')
192
201
  .option('--compress-js', '压缩所有 JS 代码(LZ4 源码保护,浏览器中无法查看变量名和方法名)')
193
202
  .option('--analyze', '生成打包分析报告')
194
203
  .option('--replace-ammo', '检测到 Ammo 时自动替换为 p2 或 cannon(仅 PlayCanvas)')
195
204
  .option('--ammo-engine <engine>', '指定替换物理引擎 (p2|cannon),仅 PlayCanvas')
196
- .option('--auto-build', '如果是源代码项目,自动执行本地构建')
197
- .option('--skip-build', '跳过本地构建,直接从源代码打包(可能不完整)')
198
- .option('--mode <mode>', '构建模式 (full|base|playable)')
199
205
  .option('--use-vite', '使用 Vite 构建(默认启用)', true)
200
206
  .option('--no-use-vite', '禁用 Vite,使用旧构建器')
201
207
  .option('--css-minify', '压缩 CSS(默认根据平台配置)')
@@ -212,8 +218,10 @@ program
212
218
  .option('--model-compression <method>', '模型压缩方法 (draco|meshopt)', 'draco')
213
219
  .option('--use-playable-scripts', '使用 @playcraft/devkit 构建工具打包(可玩广告项目专用)', false)
214
220
  .option('--no-tracking', '禁用埋点 tracking adapter 注入(默认启用)')
215
- .action(async (projectPath, options) => {
216
- await buildCommand(projectPath, options);
221
+ .addOption(new Option('-o, --output <path>').hideHelp())
222
+ .addOption(new Option('--base').hideHelp())
223
+ .action(async (projectPath, _options, command) => {
224
+ await previewBuildCommand(projectPath, withConvertToWebP(explicitOptions(command)));
217
225
  });
218
226
  // build-base 命令 - 只执行阶段1:基础构建
219
227
  program
@@ -262,9 +270,9 @@ program
262
270
  .option('--model-compression <method>', '模型压缩方法 (draco|meshopt)', 'draco')
263
271
  .option('--esm-mode <mode>', 'ESM 模块处理模式 (auto|enabled|disabled)', 'auto')
264
272
  .option('--no-tracking', '禁用埋点 tracking adapter 注入(默认启用)')
265
- .action(async (projectPath, options) => {
273
+ .action(async (projectPath, _options, command) => {
266
274
  await buildCommand(projectPath, {
267
- ...options,
275
+ ...withConvertToWebP(explicitOptions(command)),
268
276
  analyze: true,
269
277
  });
270
278
  });
@@ -300,9 +308,9 @@ program
300
308
  .option('--model-compression <method>', '模型压缩方法 (draco|meshopt)', 'draco')
301
309
  .option('--esm-mode <mode>', 'ESM 模块处理模式 (auto|enabled|disabled)', 'auto')
302
310
  .option('--no-tracking', '禁用埋点 tracking adapter 注入(默认启用)')
303
- .action(async (baseBuildDir, options) => {
311
+ .action(async (baseBuildDir, _options, command) => {
304
312
  await buildCommand(baseBuildDir, {
305
- ...options,
313
+ ...withConvertToWebP(explicitOptions(command)),
306
314
  mode: 'playable',
307
315
  });
308
316
  });
@@ -343,7 +351,7 @@ program
343
351
  .option('--theme <theme>', '指定要构建的主题(devkit 项目专用)')
344
352
  .option('--no-tracking', '禁用埋点 tracking adapter 注入(默认启用)')
345
353
  .action(async (projectPath, options) => {
346
- await buildAllCommand(projectPath, options);
354
+ await buildAllCommand(projectPath, withConvertToWebP(options));
347
355
  });
348
356
  // 新增:inspect 命令 - 检查项目类型
349
357
  program