@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.
- package/README.md +24 -31
- package/SKILLS_REGISTRY.md +1 -1
- package/dist/commands/3d.js +41 -33
- package/dist/commands/build.js +70 -5
- package/dist/commands/create.js +4 -4
- package/dist/commands/inspect.js +4 -6
- package/dist/index.js +27 -19
- package/dist/model-adaptation/node-inspection.js +173 -0
- package/dist/model-adaptation/project-template.js +194 -0
- package/dist/model-adaptation/project.js +76 -0
- package/dist/model-adaptation/source.js +49 -0
- package/dist/model-optimization/optimize.js +281 -0
- package/dist/model-preparation/bindings.js +35 -0
- package/dist/model-preparation/commands.js +28 -0
- package/dist/model-preparation/geometry.js +241 -0
- package/dist/model-preparation/glb.js +176 -0
- package/dist/model-preparation/pipeline-engine.js +157 -0
- package/dist/model-preparation/pipeline-files.js +108 -0
- package/dist/model-preparation/pipeline-worker.js +32 -0
- package/dist/model-preparation/split-rigid.js +302 -0
- package/dist/model-preparation/vertex-repairs.js +63 -0
- package/dist/workspace-runtime/batch/deliver.js +2 -2
- package/dist/workspace-runtime/codex/app-server.js +11 -1
- package/dist/workspace-runtime/codex/delivery-tools.js +38 -5
- package/dist/workspace-runtime/codex/model-broker.js +19 -1
- package/dist/workspace-runtime/previews/publish.js +2 -2
- package/dist/workspace-runtime/serve.js +5 -0
- package/dist/workspace-runtime/server/directory-access.js +1 -1
- package/dist/workspace-runtime/server/dispatch.js +27 -47
- package/dist/workspace-runtime/server/http.js +12 -0
- package/dist/workspace-runtime/workspaces/build-isolation.js +75 -0
- package/dist/workspace-runtime/workspaces/model-build-inputs.js +27 -0
- package/dist/workspace-runtime/workspaces/model-preparation-store.js +134 -0
- package/dist/workspace-runtime/workspaces/model-preparation.js +357 -0
- package/dist/workspace-runtime/workspaces/platform-export.js +1 -0
- package/package.json +6 -4
- package/project-template/.claude/skills/playcraft-build-optimizer/SKILL.md +19 -39
- package/project-templates/empty/AGENTS.md +1 -1
- package/project-templates/phaser/.claude/skills/playcraft-build-optimizer/SKILL.md +19 -39
- package/project-templates/threejs/.claude/skills/playcraft-build-optimizer/SKILL.md +19 -39
- 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` |
|
|
264
|
-
| `build-base` | 仅执行阶段1(生成多文件产物)
|
|
265
|
-
| `build-playable` | 仅执行阶段2(从多文件产物生成 Playable)
|
|
266
|
-
| `analyze` | 生成打包分析报告
|
|
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
|
-
|
|
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
|
-
###
|
|
299
|
+
### 本地构建预览
|
|
300
300
|
|
|
301
301
|
```bash
|
|
302
|
-
#
|
|
303
|
-
playcraft build
|
|
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
|
|
313
|
+
playcraft build
|
|
320
314
|
|
|
321
315
|
# 指定特定场景
|
|
322
|
-
playcraft build --
|
|
316
|
+
playcraft build --scenes BallGame
|
|
323
317
|
|
|
324
318
|
# 指定多个场景
|
|
325
|
-
playcraft build --
|
|
319
|
+
playcraft build --scenes "MainMenu,Gameplay,Settings"
|
|
326
320
|
|
|
327
321
|
# 启用图片压缩
|
|
328
|
-
playcraft build --
|
|
322
|
+
playcraft build --compress-images --image-quality 75
|
|
329
323
|
|
|
330
324
|
# 启用模型压缩
|
|
331
|
-
playcraft build --
|
|
325
|
+
playcraft build --compress-models --model-compression draco
|
|
332
326
|
|
|
333
327
|
# 生成分析报告
|
|
334
|
-
playcraft build --
|
|
328
|
+
playcraft build --analyze
|
|
335
329
|
|
|
336
330
|
# 组合使用(场景选择 + 图片压缩 + 分析)
|
|
337
|
-
playcraft build --
|
|
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
|
|
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 --
|
|
407
|
+
playcraft build --scenes MainScene
|
|
415
408
|
|
|
416
409
|
# 指定多个场景
|
|
417
|
-
playcraft build --
|
|
410
|
+
playcraft build --scenes "MainScene,Gameplay"
|
|
418
411
|
```
|
|
419
412
|
|
|
420
413
|
详见:[场景选择功能文档](../../docs/cli/scene-selection-feature.md)
|
package/SKILLS_REGISTRY.md
CHANGED
|
@@ -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>`
|
|
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
|
|
package/dist/commands/3d.js
CHANGED
|
@@ -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 {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
}
|
package/dist/commands/build.js
CHANGED
|
@@ -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/
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 <项目路径> #
|
|
828
|
+
' 1. playcraft build <项目路径> # 本地构建预览(推荐)\n' +
|
|
764
829
|
' 2. playcraft build-base <项目路径> # 先生成多文件产物\n' +
|
|
765
830
|
' playcraft build-playable <产物路径> # 再转换为 Playable\n\n' +
|
|
766
831
|
'💡 提示: build-playable 命令仅用于已有的多文件构建产物');
|
package/dist/commands/create.js
CHANGED
|
@@ -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('
|
|
1071
|
-
console.log(` ${pc.cyan('playcraft build .
|
|
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}
|
|
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
|
}
|
package/dist/commands/inspect.js
CHANGED
|
@@ -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
|
|
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
|
|
117
|
-
console.log(pc.cyan('方式 2:
|
|
118
|
-
console.log(pc.dim(' 运行: playcraft build
|
|
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
|
-
|
|
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('
|
|
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
|
-
.
|
|
216
|
-
|
|
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,
|
|
273
|
+
.action(async (projectPath, _options, command) => {
|
|
266
274
|
await buildCommand(projectPath, {
|
|
267
|
-
...
|
|
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,
|
|
311
|
+
.action(async (baseBuildDir, _options, command) => {
|
|
304
312
|
await buildCommand(baseBuildDir, {
|
|
305
|
-
...
|
|
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
|