@playcraft/cli 0.0.32 → 0.0.34

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.
@@ -343,12 +343,12 @@ export async function buildCommand(projectPath, options) {
343
343
  console.log(`\n🎬 选中场景: 全部场景`);
344
344
  }
345
345
  else if (scenesInput === 'default') {
346
- // 打包默认场景
346
+ // 打包默认场景(manifest.scenes 首位 = 当前活跃场景)
347
347
  const projectScenes = await detectProjectScenes(resolvedProjectPath);
348
- const defaultScene = projectScenes.find(s => s.isMain === true) || projectScenes[0];
348
+ const defaultScene = projectScenes[0];
349
349
  if (defaultScene) {
350
350
  selectedScenes = [defaultScene.name || String(defaultScene.id)];
351
- console.log(`\n🎬 选中场景: ${defaultScene.name || defaultScene.id}${defaultScene.isMain ? ' (默认场景)' : ' (第一个场景)'}`);
351
+ console.log(`\n🎬 选中场景: ${defaultScene.name || defaultScene.id} (默认场景)`);
352
352
  }
353
353
  else {
354
354
  console.log(`\n⚠️ 未找到默认场景,将打包所有场景`);
@@ -2,8 +2,10 @@ import { inspect } from 'node:util';
2
2
  import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
3
3
  import { join, resolve } from 'path';
4
4
  import JSON5 from 'json5';
5
- import { THEME_SCHEMA_PATH, THEME_INDEX_PATH, THEME_DIR, DEFAULT_GAME_PATH, MANIFEST_PATH, ASSETS_JSON_PATH, LITE_CREATOR_EXTENSIONS_DIR, themeDataPath, scenePath, defaultValueFromJsonSchemaProperty, buildThemeIndexTs, parseThemeIndexImportPath, parseThemePrefabs, mergeThemeDataKey, deleteThemeDataKey, parseExtensionMetaData, parseGameConfig, buildExtensionPrefabs, updateGameConfigValue, updateGameConfigExtensions, findConfigKeyCaseInsensitive, parseConfigKey, listScenesFromManifest, resolveGameConfigPath, validateThemeValue, validateConfigValue, describeJsonSchemaFields, describeConfigSchemaFields, getPrefabFieldDescriptors, getPrefabFieldDiffs, prefabHasSchemaDiff, } from '@playcraft/common/prefab';
5
+ import { THEME_SCHEMA_PATH, THEME_INDEX_PATH, THEME_DIR, DEFAULT_GAME_PATH, MANIFEST_PATH, ASSETS_JSON_PATH, LITE_CREATOR_EXTENSIONS_DIR, themeDataPath, scenePath, defaultValueFromJsonSchemaProperty, buildThemeIndexTs, parseThemeIndexImportPath, parseThemePrefabs, mergeThemeDataKey, deleteThemeDataKey, parseExtensionMetaData, parseGameConfig, buildExtensionPrefabs, updateGameConfigValue, updateGameConfigExtensions, findConfigKeyCaseInsensitive, parseConfigKey, listScenesFromManifest, listScenesWithGameConfig, resolveGameConfigPath, getActiveSceneIdFromManifest, reorderManifestDefaultScene, validateThemeValue, validateConfigValue, describeJsonSchemaFields, describeConfigSchemaFields, getPrefabFieldDescriptors, getPrefabFieldDiffs, prefabHasSchemaDiff, } from '@playcraft/common/prefab';
6
6
  const DEFAULT_PAGE_LIMIT = 50;
7
+ /** 场景在 manifest / CLI 中列出但解析不到 GameConfig 时的人读标签 */
8
+ const NO_SCENE_GAMECONFIG_LABEL = '(无场景级 GameConfig)';
7
9
  /** 无法识别 External / PlayCanvas 项目类型(供测试在 mock process.exit 后通过 instanceof 识别)。 */
8
10
  export class ProjectDetectError extends Error {
9
11
  name = 'ProjectDetectError';
@@ -186,7 +188,7 @@ function resolveVariantResolved(projectDir, systemType, opts) {
186
188
  if (systemType === 'external-theme') {
187
189
  return resolveThemeId(projectDir, opts);
188
190
  }
189
- return null;
191
+ return getMainSceneId(projectDir);
190
192
  }
191
193
  // ─── External Theme helpers ──────────────────────────────────────────────────
192
194
  function loadThemeSchema(projectDir) {
@@ -240,6 +242,22 @@ function resolveThemeId(projectDir, opts) {
240
242
  throw new Error('unreachable');
241
243
  }
242
244
  // ─── PlayCanvas / LiteCreator helpers ────────────────────────────────────────
245
+ /**
246
+ * 当前活跃场景 ID = manifest.scenes[0]。
247
+ *
248
+ * 与 Remix 编辑器和后端 `setDefaultScene`(将目标场景移到首位)的 SSOT 一致。
249
+ */
250
+ function getMainSceneId(projectDir) {
251
+ if (!fileExists(projectDir, MANIFEST_PATH))
252
+ return null;
253
+ try {
254
+ const manifestJson = JSON.parse(readLocalFile(projectDir, MANIFEST_PATH));
255
+ return getActiveSceneIdFromManifest(manifestJson);
256
+ }
257
+ catch {
258
+ return null;
259
+ }
260
+ }
243
261
  function loadGameConfig(projectDir, configPath) {
244
262
  const raw = readLocalFile(projectDir, configPath);
245
263
  return JSON.parse(raw);
@@ -266,24 +284,102 @@ function scanExtensionMetaData(projectDir) {
266
284
  }
267
285
  return map;
268
286
  }
269
- function resolveGameConfigPathForVariant(projectDir, variantId) {
270
- if (!variantId)
271
- return DEFAULT_GAME_PATH;
272
- if (!fileExists(projectDir, scenePath(variantId)))
273
- return DEFAULT_GAME_PATH;
274
- const sceneJson = JSON.parse(readLocalFile(projectDir, scenePath(variantId)));
287
+ function extensionMetaDataFile(projectDir, extensionName) {
288
+ return join(projectDir, LITE_CREATOR_EXTENSIONS_DIR, extensionName, 'MetaData.json');
289
+ }
290
+ function mutateExtensionMetaData(projectDir, extensionName, apply) {
291
+ const metaPath = extensionMetaDataFile(projectDir, extensionName);
292
+ const raw = readFileSync(metaPath, 'utf-8');
293
+ const metaData = JSON.parse(raw);
294
+ const configSchema = metaData.configSchema;
295
+ if (!configSchema) {
296
+ throw new Error(`MetaData.json for "${extensionName}" has no configSchema`);
297
+ }
298
+ apply(configSchema);
299
+ writeFileSync(metaPath, JSON.stringify(metaData, null, 2), 'utf-8');
300
+ }
301
+ function writeMetaDataDefault(projectDir, extensionName, field, value) {
302
+ mutateExtensionMetaData(projectDir, extensionName, (configSchema) => {
303
+ if (!configSchema[field]) {
304
+ throw new Error(`MetaData.json for "${extensionName}" has no configSchema.${field}`);
305
+ }
306
+ configSchema[field].default = value;
307
+ });
308
+ }
309
+ function writeMetaDataDefaults(projectDir, extensionName, values) {
310
+ mutateExtensionMetaData(projectDir, extensionName, (configSchema) => {
311
+ for (const [field, value] of Object.entries(values)) {
312
+ if (configSchema[field]) {
313
+ configSchema[field].default = value;
314
+ }
315
+ }
316
+ });
317
+ }
318
+ /** 仅按场景 ID 解析 GameConfig(不查活跃场景)。 */
319
+ function resolveGameConfigPathForSceneId(projectDir, sceneId) {
320
+ if (!fileExists(projectDir, scenePath(sceneId)))
321
+ return null;
322
+ const sceneJson = JSON.parse(readLocalFile(projectDir, scenePath(sceneId)));
275
323
  if (!fileExists(projectDir, ASSETS_JSON_PATH))
276
- return DEFAULT_GAME_PATH;
324
+ return null;
277
325
  const assetsJson = JSON.parse(readLocalFile(projectDir, ASSETS_JSON_PATH));
278
- const resolved = resolveGameConfigPath(sceneJson, assetsJson);
279
- return resolved ?? DEFAULT_GAME_PATH;
326
+ return resolveGameConfigPath(sceneJson, assetsJson);
327
+ }
328
+ /**
329
+ * 解析当前变体下的 GameConfig 路径。无法解析时返回 null(list 回退 DefaultGame 扩展;set 可写 MetaData)。
330
+ * 未传 variantId 时用 manifest 首位场景(当前活跃场景)。
331
+ */
332
+ function resolveGameConfigPathForVariant(projectDir, variantId) {
333
+ const sceneId = variantId ?? getMainSceneId(projectDir);
334
+ if (!sceneId)
335
+ return null;
336
+ return resolveGameConfigPathForSceneId(projectDir, sceneId);
337
+ }
338
+ /** PlayCanvas:`prefab list` 等人读「变体上下文」一行话。 */
339
+ function liteCreatorVariantHumanNote(projectDir, explicitVariant) {
340
+ if (explicitVariant) {
341
+ const p = resolveGameConfigPathForSceneId(projectDir, explicitVariant);
342
+ return p != null ? `场景 ${explicitVariant} → ${p}` : `场景 ${explicitVariant}(${NO_SCENE_GAMECONFIG_LABEL};set 可走 MetaData)`;
343
+ }
344
+ const mainId = getMainSceneId(projectDir);
345
+ const gc = resolveGameConfigPathForVariant(projectDir, undefined);
346
+ if (mainId != null) {
347
+ return gc != null
348
+ ? `活跃场景 ${mainId} → ${gc}`
349
+ : `活跃场景 ${mainId}(未解析到 GameConfig;list 读 ${DEFAULT_GAME_PATH} 扩展,set 写 MetaData)`;
350
+ }
351
+ return `无场景 → 扩展列表:${DEFAULT_GAME_PATH};set / set-batch:各扩展 MetaData.json`;
280
352
  }
281
353
  function listLocalScenes(projectDir) {
282
354
  if (!fileExists(projectDir, MANIFEST_PATH))
283
355
  return [];
284
356
  const manifestJson = JSON.parse(readLocalFile(projectDir, MANIFEST_PATH));
357
+ if (fileExists(projectDir, ASSETS_JSON_PATH)) {
358
+ const assetsJson = JSON.parse(readLocalFile(projectDir, ASSETS_JSON_PATH));
359
+ return listScenesWithGameConfig(manifestJson, (sceneId) => {
360
+ const sp = scenePath(sceneId);
361
+ if (!fileExists(projectDir, sp))
362
+ return null;
363
+ return JSON.parse(readLocalFile(projectDir, sp));
364
+ }, assetsJson);
365
+ }
285
366
  return listScenesFromManifest(manifestJson);
286
367
  }
368
+ /** 列表展示:补全 GameConfig 路径,并用活跃场景 ID 标记 isActive(与未指定 --variant 的 prefab 上下文一致)。 */
369
+ function annotatePlaycanvasScenesForMainScene(projectDir, scenes) {
370
+ const mainSceneId = getMainSceneId(projectDir);
371
+ const annotated = scenes.map((s) => {
372
+ const resolved = s.gameConfigPath ?? resolveGameConfigPathForSceneId(projectDir, s.id);
373
+ const out = {
374
+ ...s,
375
+ isActive: mainSceneId !== null && s.id === mainSceneId,
376
+ };
377
+ if (resolved != null)
378
+ out.gameConfigPath = resolved;
379
+ return out;
380
+ });
381
+ return { scenes: annotated, mainSceneId };
382
+ }
287
383
  function loadPrefabs(projectDir, systemType, opts) {
288
384
  if (systemType === 'external-theme') {
289
385
  const themeId = resolveThemeId(projectDir, opts);
@@ -291,11 +387,19 @@ function loadPrefabs(projectDir, systemType, opts) {
291
387
  const data = loadThemeData(projectDir, themeId);
292
388
  return parseThemePrefabs(schema, data, themeId);
293
389
  }
294
- const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant);
295
- const gameConfigJson = loadGameConfig(projectDir, configPath);
296
- const { extensions, config } = parseGameConfig(gameConfigJson);
297
390
  const metaMap = scanExtensionMetaData(projectDir);
298
- return buildExtensionPrefabs(metaMap, { extensions, config }, configPath);
391
+ const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant);
392
+ if (configPath) {
393
+ const gameConfigJson = loadGameConfig(projectDir, configPath);
394
+ const { extensions, config } = parseGameConfig(gameConfigJson);
395
+ return buildExtensionPrefabs(metaMap, { extensions, config }, configPath);
396
+ }
397
+ if (fileExists(projectDir, DEFAULT_GAME_PATH)) {
398
+ const defaultGameJson = loadGameConfig(projectDir, DEFAULT_GAME_PATH);
399
+ const { extensions } = parseGameConfig(defaultGameJson);
400
+ return buildExtensionPrefabs(metaMap, { extensions, config: {} }, DEFAULT_GAME_PATH);
401
+ }
402
+ return buildExtensionPrefabs(metaMap, { extensions: [], config: {} }, '');
299
403
  }
300
404
  // ─── dotpath helper ──────────────────────────────────────────────────────────
301
405
  function getByDotPath(obj, path) {
@@ -422,8 +526,18 @@ export function registerPrefabCommands(program) {
422
526
  });
423
527
  }
424
528
  else {
425
- const scenes = listLocalScenes(projectDir);
426
- const payload = { projectType: 'playcanvas', variants: scenes };
529
+ const scenesRaw = listLocalScenes(projectDir);
530
+ const { scenes, mainSceneId } = annotatePlaycanvasScenesForMainScene(projectDir, scenesRaw);
531
+ const gameConfigPath = resolveGameConfigPathForVariant(projectDir, undefined);
532
+ const payload = {
533
+ projectType: 'playcanvas',
534
+ variants: scenes,
535
+ context: {
536
+ mainSceneId,
537
+ gameConfigPath,
538
+ usesMetaDataForSet: gameConfigPath == null,
539
+ },
540
+ };
427
541
  outputResult(asJson, payload, () => {
428
542
  console.log(`项目类型: ${projectTypeLabel('lite-creator')}`);
429
543
  console.log('');
@@ -432,8 +546,19 @@ export function registerPrefabCommands(program) {
432
546
  return;
433
547
  }
434
548
  for (const s of scenes) {
435
- const gc = s.gameConfigPath ? ` [${s.gameConfigPath}]` : '';
436
- console.log(` ${s.name} (${s.id})${gc}`);
549
+ const mark = s.isActive ? ' *' : '';
550
+ const pathLabel = s.gameConfigPath ?? NO_SCENE_GAMECONFIG_LABEL;
551
+ console.log(` ${s.name} (${s.id}) [${pathLabel}]${mark}`);
552
+ }
553
+ if (scenes.some((v) => v.isActive)) {
554
+ console.log('');
555
+ console.log('标 *:manifest 首位场景(当前活跃场景,与 Remix 编辑器一致)。' +
556
+ ' 未带 --variant:优先该场景 GameConfig;若无路径则 list 读 DefaultGame 扩展、set 写 MetaData。' +
557
+ ' 可用 `prefab switch <sceneId>` 切换活跃场景。');
558
+ }
559
+ else {
560
+ console.log('');
561
+ console.log(`无场景:未带 --variant 时扩展来自 ${DEFAULT_GAME_PATH},set / set-batch 写入各扩展 MetaData.json。`);
437
562
  }
438
563
  });
439
564
  }
@@ -466,6 +591,9 @@ export function registerPrefabCommands(program) {
466
591
  if (systemType === 'external-theme' && !opts.variant) {
467
592
  variantNote = resolveThemeId(projectDir, opts);
468
593
  }
594
+ else if (systemType === 'lite-creator') {
595
+ variantNote = liteCreatorVariantHumanNote(projectDir, opts.variant);
596
+ }
469
597
  const summary = buildSummary(systemType, variantResolved, all);
470
598
  const filtered = filterPrefabsList(all, {
471
599
  match: opts.match,
@@ -818,18 +946,12 @@ export function registerPrefabCommands(program) {
818
946
  });
819
947
  }
820
948
  else {
821
- const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant);
822
- const gameConfigJson = loadGameConfig(projectDir, configPath);
823
- const { config } = parseGameConfig(gameConfigJson);
824
949
  const metaMap = scanExtensionMetaData(projectDir);
825
950
  const meta = metaMap.get(key);
826
951
  if (!meta) {
827
952
  console.error(`Error: extension "${key}" 不存在。`);
828
953
  process.exit(1);
829
954
  }
830
- const configKey = meta.configPath ? parseConfigKey(meta.configPath) : meta.name;
831
- const existingKey = findConfigKeyCaseInsensitive(config, configKey);
832
- const currentValues = (existingKey ? config[existingKey] : {}) ?? {};
833
955
  const fieldDef = meta.configSchema?.[field];
834
956
  const fieldType = fieldDef?.type ?? 'string';
835
957
  const coerced = coerceValue(rawValue, fieldType);
@@ -840,9 +962,20 @@ export function registerPrefabCommands(program) {
840
962
  process.exit(1);
841
963
  }
842
964
  }
843
- const newValues = { ...currentValues, [field]: coerced };
844
- const updated = updateGameConfigValue(gameConfigJson, configKey, newValues);
845
- writeGameConfig(projectDir, configPath, updated);
965
+ const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant);
966
+ if (configPath) {
967
+ const gameConfigJson = loadGameConfig(projectDir, configPath);
968
+ const { config } = parseGameConfig(gameConfigJson);
969
+ const configKey = meta.configPath ? parseConfigKey(meta.configPath) : meta.name;
970
+ const existingKey = findConfigKeyCaseInsensitive(config, configKey);
971
+ const currentValues = (existingKey ? config[existingKey] : {}) ?? {};
972
+ const newValues = { ...currentValues, [field]: coerced };
973
+ const updated = updateGameConfigValue(gameConfigJson, configKey, newValues);
974
+ writeGameConfig(projectDir, configPath, updated);
975
+ }
976
+ else {
977
+ writeMetaDataDefault(projectDir, key, field, coerced);
978
+ }
846
979
  const payload = { success: true, key, field, value: coerced };
847
980
  outputResult(asJson, payload, () => {
848
981
  console.log(`已更新 ${key}.${field} = ${formatHumanValue(coerced)}`);
@@ -940,38 +1073,65 @@ export function registerPrefabCommands(program) {
940
1073
  }
941
1074
  else {
942
1075
  const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant);
943
- let gameConfigJson = loadGameConfig(projectDir, configPath);
944
1076
  const metaMap = scanExtensionMetaData(projectDir);
945
- for (const [extKey, fieldMap] of Object.entries(batch)) {
946
- if (!fieldMap || typeof fieldMap !== 'object' || Array.isArray(fieldMap)) {
947
- throw new Error(`extension "${extKey}" 的值必须是对象(字段 map)`);
948
- }
949
- const meta = metaMap.get(extKey);
950
- if (!meta) {
951
- throw new Error(`extension "${extKey}" 不存在`);
1077
+ if (configPath) {
1078
+ let gameConfigJson = loadGameConfig(projectDir, configPath);
1079
+ for (const [extKey, fieldMap] of Object.entries(batch)) {
1080
+ if (!fieldMap || typeof fieldMap !== 'object' || Array.isArray(fieldMap)) {
1081
+ throw new Error(`extension "${extKey}" 的值必须是对象(字段 map)`);
1082
+ }
1083
+ const meta = metaMap.get(extKey);
1084
+ if (!meta) {
1085
+ throw new Error(`extension "${extKey}" 不存在`);
1086
+ }
1087
+ const configKey = meta.configPath ? parseConfigKey(meta.configPath) : meta.name;
1088
+ const { config } = parseGameConfig(gameConfigJson);
1089
+ const existingKey = findConfigKeyCaseInsensitive(config, configKey);
1090
+ let currentValues = (existingKey ? config[existingKey] : {}) ?? {};
1091
+ currentValues = typeof currentValues === 'object' && !Array.isArray(currentValues)
1092
+ ? { ...currentValues }
1093
+ : {};
1094
+ for (const [field, rawVal] of Object.entries(fieldMap)) {
1095
+ const fieldDef = meta.configSchema?.[field];
1096
+ const fieldType = fieldDef?.type ?? 'string';
1097
+ const coerced = coerceBatchLeaf(rawVal, fieldType);
1098
+ if (fieldDef) {
1099
+ const result = validateConfigValue(fieldDef, coerced);
1100
+ if (!result.valid) {
1101
+ throw new Error(`${extKey}.${field}: ${result.errors.join('; ')}`);
1102
+ }
1103
+ }
1104
+ currentValues = setByDotPath(currentValues, field, coerced);
1105
+ }
1106
+ gameConfigJson = updateGameConfigValue(gameConfigJson, configKey, currentValues);
952
1107
  }
953
- const configKey = meta.configPath ? parseConfigKey(meta.configPath) : meta.name;
954
- const { config } = parseGameConfig(gameConfigJson);
955
- const existingKey = findConfigKeyCaseInsensitive(config, configKey);
956
- let currentValues = (existingKey ? config[existingKey] : {}) ?? {};
957
- currentValues = typeof currentValues === 'object' && !Array.isArray(currentValues)
958
- ? { ...currentValues }
959
- : {};
960
- for (const [field, rawVal] of Object.entries(fieldMap)) {
961
- const fieldDef = meta.configSchema?.[field];
962
- const fieldType = fieldDef?.type ?? 'string';
963
- const coerced = coerceBatchLeaf(rawVal, fieldType);
964
- if (fieldDef) {
965
- const result = validateConfigValue(fieldDef, coerced);
966
- if (!result.valid) {
967
- throw new Error(`${extKey}.${field}: ${result.errors.join('; ')}`);
1108
+ writeGameConfig(projectDir, configPath, gameConfigJson);
1109
+ }
1110
+ else {
1111
+ for (const [extKey, fieldMap] of Object.entries(batch)) {
1112
+ if (!fieldMap || typeof fieldMap !== 'object' || Array.isArray(fieldMap)) {
1113
+ throw new Error(`extension "${extKey}" 的值必须是对象(字段 map)`);
1114
+ }
1115
+ const meta = metaMap.get(extKey);
1116
+ if (!meta) {
1117
+ throw new Error(`extension "${extKey}" 不存在`);
1118
+ }
1119
+ const coercedValues = {};
1120
+ for (const [field, rawVal] of Object.entries(fieldMap)) {
1121
+ const fieldDef = meta.configSchema?.[field];
1122
+ const fieldType = fieldDef?.type ?? 'string';
1123
+ const coerced = coerceBatchLeaf(rawVal, fieldType);
1124
+ if (fieldDef) {
1125
+ const result = validateConfigValue(fieldDef, coerced);
1126
+ if (!result.valid) {
1127
+ throw new Error(`${extKey}.${field}: ${result.errors.join('; ')}`);
1128
+ }
968
1129
  }
1130
+ coercedValues[field] = coerced;
969
1131
  }
970
- currentValues = setByDotPath(currentValues, field, coerced);
1132
+ writeMetaDataDefaults(projectDir, extKey, coercedValues);
971
1133
  }
972
- gameConfigJson = updateGameConfigValue(gameConfigJson, configKey, currentValues);
973
1134
  }
974
- writeGameConfig(projectDir, configPath, gameConfigJson);
975
1135
  }
976
1136
  const payload = { success: true, updatedKeys: Object.keys(batch) };
977
1137
  outputResult(asJson, payload, () => {
@@ -1011,7 +1171,7 @@ export function registerPrefabCommands(program) {
1011
1171
  });
1012
1172
  }
1013
1173
  else {
1014
- const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant);
1174
+ const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant) ?? DEFAULT_GAME_PATH;
1015
1175
  const gameConfigJson = loadGameConfig(projectDir, configPath);
1016
1176
  const { extensions } = parseGameConfig(gameConfigJson);
1017
1177
  if (extensions.includes(key)) {
@@ -1055,7 +1215,7 @@ export function registerPrefabCommands(program) {
1055
1215
  });
1056
1216
  }
1057
1217
  else {
1058
- const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant);
1218
+ const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant) ?? DEFAULT_GAME_PATH;
1059
1219
  const gameConfigJson = loadGameConfig(projectDir, configPath);
1060
1220
  const { extensions } = parseGameConfig(gameConfigJson);
1061
1221
  const filtered = extensions.filter((n) => n !== key);
@@ -1105,14 +1265,19 @@ export function registerPrefabCommands(program) {
1105
1265
  console.error(`Error: 场景 "${variant}" 不存在。可用: ${scenes.map((s) => `${s.name}(${s.id})`).join(', ')}`);
1106
1266
  process.exit(1);
1107
1267
  }
1268
+ const manifestRaw = readLocalFile(projectDir, MANIFEST_PATH);
1269
+ const manifestJson = JSON.parse(manifestRaw);
1270
+ const reordered = reorderManifestDefaultScene(manifestJson, found.id);
1271
+ if (reordered && reordered !== manifestJson) {
1272
+ writeFileSync(join(projectDir, MANIFEST_PATH), JSON.stringify(reordered, null, 2), 'utf-8');
1273
+ }
1108
1274
  const payload = {
1109
1275
  success: true,
1110
1276
  activeVariant: found.id,
1111
- message: `PlayCanvas 场景上下文已设为 "${found.name}"。后续命令使用 --variant ${found.id} 来操作此场景的配置。`,
1277
+ message: `PlayCanvas 场景已切换为 "${found.name}" 并写入 manifest.json。`,
1112
1278
  };
1113
1279
  outputResult(asJson, payload, () => {
1114
- console.log(`PlayCanvas 场景上下文已设为「${found.name}」 (${found.id})。`);
1115
- console.log(`后续命令请使用 --variant ${found.id} 操作该场景的配置。`);
1280
+ console.log(`已切换活跃场景为「${found.name}」 (${found.id}),manifest.json 已更新。`);
1116
1281
  });
1117
1282
  }
1118
1283
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,8 +23,8 @@
23
23
  "release": "node scripts/release.js"
24
24
  },
25
25
  "dependencies": {
26
- "@playcraft/build": "^0.0.33",
27
- "@playcraft/common": "^0.0.20",
26
+ "@playcraft/build": "^0.0.35",
27
+ "@playcraft/common": "^0.0.22",
28
28
  "chokidar": "^4.0.3",
29
29
  "commander": "^13.1.0",
30
30
  "cors": "^2.8.6",