@playcraft/cli 0.0.27 → 0.0.28
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/dist/commands/image.js +20 -4
- package/dist/commands/prefab.js +572 -125
- package/dist/commands/tools.js +15 -6
- package/package.json +3 -3
package/dist/commands/image.js
CHANGED
|
@@ -48,6 +48,21 @@ function handleError(err) {
|
|
|
48
48
|
console.error(`Error: ${msg}`);
|
|
49
49
|
process.exit(1);
|
|
50
50
|
}
|
|
51
|
+
/** 多圈边缘羽化默认 alpha;若 edgeLayers 更大则自最后一档按衰减补齐。 */
|
|
52
|
+
const DEFAULT_EDGE_LAYER_ALPHAS = [200, 120, 60];
|
|
53
|
+
function buildEdgeLayerAlphas(edgeLayers) {
|
|
54
|
+
const base = DEFAULT_EDGE_LAYER_ALPHAS;
|
|
55
|
+
if (edgeLayers <= base.length) {
|
|
56
|
+
return Array.from(base.slice(0, edgeLayers));
|
|
57
|
+
}
|
|
58
|
+
const out = Array.from(base);
|
|
59
|
+
let last = base[base.length - 1];
|
|
60
|
+
while (out.length < edgeLayers) {
|
|
61
|
+
last = Math.max(8, Math.round(last * 0.62));
|
|
62
|
+
out.push(last);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
51
66
|
/**
|
|
52
67
|
* 从图片四条边向内 BFS flood-fill,自动检测边缘主色并移除连通的同色背景区域。
|
|
53
68
|
* 适用于 AI 生成的素材(纯色、灰色、棋盘格背景),前景纹理零损失。
|
|
@@ -143,7 +158,7 @@ async function floodFillRemoveBg(inputPath, opts) {
|
|
|
143
158
|
}
|
|
144
159
|
// ── 3. Edge smoothing (multi-layer alpha gradient) ──
|
|
145
160
|
if (opts.edgeLayers > 0) {
|
|
146
|
-
const layerAlphas =
|
|
161
|
+
const layerAlphas = buildEdgeLayerAlphas(opts.edgeLayers);
|
|
147
162
|
let borderSet = new Set();
|
|
148
163
|
for (let y = 1; y < h - 1; y++) {
|
|
149
164
|
for (let x = 1; x < w - 1; x++) {
|
|
@@ -155,12 +170,13 @@ async function floodFillRemoveBg(inputPath, opts) {
|
|
|
155
170
|
borderSet.add(pos);
|
|
156
171
|
}
|
|
157
172
|
}
|
|
158
|
-
for (let layer = 0; layer <
|
|
173
|
+
for (let layer = 0; layer < layerAlphas.length; layer++) {
|
|
159
174
|
const alpha = layerAlphas[layer];
|
|
160
175
|
for (const pos of borderSet) {
|
|
161
176
|
result[pos * 4 + 3] = Math.min(result[pos * 4 + 3], alpha);
|
|
162
177
|
}
|
|
163
|
-
if (layer <
|
|
178
|
+
if (layer < layerAlphas.length - 1) {
|
|
179
|
+
const nextAlpha = layerAlphas[layer + 1];
|
|
164
180
|
const nextBorder = new Set();
|
|
165
181
|
for (const pos of borderSet) {
|
|
166
182
|
const x = pos % w;
|
|
@@ -172,7 +188,7 @@ async function floodFillRemoveBg(inputPath, opts) {
|
|
|
172
188
|
continue;
|
|
173
189
|
const npos = ny * w + nx;
|
|
174
190
|
if (result[npos * 4 + 3] === 0 && !borderSet.has(npos)) {
|
|
175
|
-
result[npos * 4 + 3] = Math.min(60,
|
|
191
|
+
result[npos * 4 + 3] = Math.min(60, nextAlpha);
|
|
176
192
|
nextBorder.add(npos);
|
|
177
193
|
}
|
|
178
194
|
}
|
package/dist/commands/prefab.js
CHANGED
|
@@ -2,7 +2,19 @@ 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, } 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, resolveGameConfigPath, validateThemeValue, validateConfigValue, describeJsonSchemaFields, describeConfigSchemaFields, getPrefabFieldDescriptors, getPrefabFieldDiffs, prefabHasSchemaDiff, } from '@playcraft/common/prefab';
|
|
6
|
+
const DEFAULT_PAGE_LIMIT = 50;
|
|
7
|
+
/** 无法识别 External / PlayCanvas 项目类型(供测试在 mock process.exit 后通过 instanceof 识别)。 */
|
|
8
|
+
export class ProjectDetectError extends Error {
|
|
9
|
+
name = 'ProjectDetectError';
|
|
10
|
+
constructor(message = 'Project type could not be detected') {
|
|
11
|
+
super(message);
|
|
12
|
+
Error.captureStackTrace?.(this, ProjectDetectError);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function isProjectDetectError(e) {
|
|
16
|
+
return e instanceof ProjectDetectError;
|
|
17
|
+
}
|
|
6
18
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
7
19
|
function resolveProjectDir(opts) {
|
|
8
20
|
return opts.projectDir ? resolve(opts.projectDir) : process.cwd();
|
|
@@ -21,11 +33,10 @@ function detectProjectType(projectDir) {
|
|
|
21
33
|
console.error('Error: 无法识别项目类型。当前仅支持 External(需 src/theme/theme.schema.json5)' +
|
|
22
34
|
'和 PlayCanvas(需 assets/DefaultGame.json)两种项目类型。');
|
|
23
35
|
process.exit(1);
|
|
24
|
-
throw new
|
|
36
|
+
throw new ProjectDetectError();
|
|
25
37
|
}
|
|
26
38
|
const HUMAN_INSPECT = { colors: false, depth: 10, maxArrayLength: 200 };
|
|
27
39
|
const JSON_OPT_DESC = '以 JSON 输出(便于脚本解析;默认为人可读文本)';
|
|
28
|
-
/** 为子命令追加 `--json`(默认人类可读) */
|
|
29
40
|
function withJsonOption(cmd) {
|
|
30
41
|
return cmd.option('--json', JSON_OPT_DESC);
|
|
31
42
|
}
|
|
@@ -67,6 +78,116 @@ function describeFieldLine(f) {
|
|
|
67
78
|
parts.push(`默认: ${formatHumanValue(f.default)}`);
|
|
68
79
|
return parts.join(' | ');
|
|
69
80
|
}
|
|
81
|
+
function isFieldDescriptor(v) {
|
|
82
|
+
if (v === null || typeof v !== 'object')
|
|
83
|
+
return false;
|
|
84
|
+
const o = v;
|
|
85
|
+
return typeof o.path === 'string' && typeof o.type === 'string';
|
|
86
|
+
}
|
|
87
|
+
/** 规范化 describe 的 fields,剔除不符合 FieldDescriptor 形态的项(避免 JSON/终端输出依赖不安全断言)。 */
|
|
88
|
+
function normalizeDescribeFields(raw) {
|
|
89
|
+
if (!Array.isArray(raw))
|
|
90
|
+
return [];
|
|
91
|
+
return raw.filter(isFieldDescriptor);
|
|
92
|
+
}
|
|
93
|
+
function isDescribeJsonWithFields(b) {
|
|
94
|
+
return 'fields' in b && Array.isArray(b.fields);
|
|
95
|
+
}
|
|
96
|
+
function slicePage(arr, limit, offset) {
|
|
97
|
+
const total = arr.length;
|
|
98
|
+
const slice = arr.slice(offset, offset + limit);
|
|
99
|
+
const hasMore = offset + slice.length < total;
|
|
100
|
+
return { slice, page: { limit, offset, total, hasMore } };
|
|
101
|
+
}
|
|
102
|
+
function tryCompileRegex(pattern, matchCase) {
|
|
103
|
+
try {
|
|
104
|
+
return new RegExp(pattern, matchCase ? '' : 'i');
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
108
|
+
throw new Error(`Invalid regex pattern "${pattern}": ${msg}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function prefabMetaMatches(p, re) {
|
|
112
|
+
if (re.test(p.key))
|
|
113
|
+
return true;
|
|
114
|
+
if (re.test(p.name))
|
|
115
|
+
return true;
|
|
116
|
+
if (p.description && re.test(p.description))
|
|
117
|
+
return true;
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
function filterPrefabsList(all, o) {
|
|
121
|
+
let list = all;
|
|
122
|
+
if (o.usedOnly)
|
|
123
|
+
list = list.filter((p) => p.isUsed);
|
|
124
|
+
if (o.unusedOnly)
|
|
125
|
+
list = list.filter((p) => !p.isUsed);
|
|
126
|
+
if (o.changedOnly)
|
|
127
|
+
list = list.filter((p) => prefabHasSchemaDiff(p));
|
|
128
|
+
if (o.match) {
|
|
129
|
+
const re = tryCompileRegex(o.match, Boolean(o.matchCase));
|
|
130
|
+
list = list.filter((p) => prefabMetaMatches(p, re));
|
|
131
|
+
}
|
|
132
|
+
return list;
|
|
133
|
+
}
|
|
134
|
+
function buildSummary(systemType, variantResolved, all) {
|
|
135
|
+
const enabled = all.filter((p) => p.isUsed).length;
|
|
136
|
+
const changed = all.filter((p) => prefabHasSchemaDiff(p)).length;
|
|
137
|
+
return {
|
|
138
|
+
projectType: systemType === 'external-theme' ? 'external' : 'playcanvas',
|
|
139
|
+
variant: variantResolved,
|
|
140
|
+
total: all.length,
|
|
141
|
+
enabled,
|
|
142
|
+
disabled: all.length - enabled,
|
|
143
|
+
changed,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function buildDescribeGuide(prefabKey, isUsed) {
|
|
147
|
+
return {
|
|
148
|
+
workflow: [
|
|
149
|
+
'playcraft prefab list --json --limit 50 --offset 0',
|
|
150
|
+
`playcraft prefab diff --json --limit ${DEFAULT_PAGE_LIMIT} --offset 0`,
|
|
151
|
+
`playcraft prefab get ${prefabKey} --json`,
|
|
152
|
+
],
|
|
153
|
+
setExample: `playcraft prefab set ${prefabKey} <field> <value>`,
|
|
154
|
+
whenDisabled: isUsed ? undefined : `playcraft prefab enable ${prefabKey}`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function valuePreview(p, maxLen = 120) {
|
|
158
|
+
if (!p.isUsed)
|
|
159
|
+
return null;
|
|
160
|
+
const s = formatHumanValue(p.currentValue);
|
|
161
|
+
if (s.length <= maxLen)
|
|
162
|
+
return s;
|
|
163
|
+
return `${s.slice(0, maxLen)}…`;
|
|
164
|
+
}
|
|
165
|
+
function withPagingOptions(cmd) {
|
|
166
|
+
return cmd
|
|
167
|
+
.option('--limit <n>', '分页条数', (v) => parseInt(v, 10), DEFAULT_PAGE_LIMIT)
|
|
168
|
+
.option('--offset <n>', '分页偏移', (v) => parseInt(v, 10), 0);
|
|
169
|
+
}
|
|
170
|
+
function withFilterOptions(cmd) {
|
|
171
|
+
return cmd
|
|
172
|
+
.option('--match <regex>', 'prefab 级正则过滤(key/name/description)')
|
|
173
|
+
.option('--match-case', '正则区分大小写(默认忽略大小写)')
|
|
174
|
+
.option('--used-only', '仅已启用')
|
|
175
|
+
.option('--unused-only', '仅未启用')
|
|
176
|
+
.option('--changed-only', '相对 schema 默认值有差异的 prefab');
|
|
177
|
+
}
|
|
178
|
+
function parsePaging(opts) {
|
|
179
|
+
const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? opts.limit : DEFAULT_PAGE_LIMIT;
|
|
180
|
+
const offset = Number.isFinite(opts.offset) && opts.offset >= 0 ? opts.offset : 0;
|
|
181
|
+
return { limit, offset };
|
|
182
|
+
}
|
|
183
|
+
function resolveVariantResolved(projectDir, systemType, opts) {
|
|
184
|
+
if (opts.variant)
|
|
185
|
+
return opts.variant;
|
|
186
|
+
if (systemType === 'external-theme') {
|
|
187
|
+
return resolveThemeId(projectDir, opts);
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
70
191
|
// ─── External Theme helpers ──────────────────────────────────────────────────
|
|
71
192
|
function loadThemeSchema(projectDir) {
|
|
72
193
|
const raw = readLocalFile(projectDir, THEME_SCHEMA_PATH);
|
|
@@ -116,6 +237,7 @@ function resolveThemeId(projectDir, opts) {
|
|
|
116
237
|
return themes[0];
|
|
117
238
|
console.error('Error: 没有找到可用的主题。请使用 --variant 指定主题 ID。');
|
|
118
239
|
process.exit(1);
|
|
240
|
+
throw new Error('unreachable');
|
|
119
241
|
}
|
|
120
242
|
// ─── PlayCanvas / LiteCreator helpers ────────────────────────────────────────
|
|
121
243
|
function loadGameConfig(projectDir, configPath) {
|
|
@@ -162,7 +284,6 @@ function listLocalScenes(projectDir) {
|
|
|
162
284
|
const manifestJson = JSON.parse(readLocalFile(projectDir, MANIFEST_PATH));
|
|
163
285
|
return listScenesFromManifest(manifestJson);
|
|
164
286
|
}
|
|
165
|
-
// ─── Unified prefab loading ──────────────────────────────────────────────────
|
|
166
287
|
function loadPrefabs(projectDir, systemType, opts) {
|
|
167
288
|
if (systemType === 'external-theme') {
|
|
168
289
|
const themeId = resolveThemeId(projectDir, opts);
|
|
@@ -222,12 +343,58 @@ function coerceValue(rawValue, fieldType) {
|
|
|
222
343
|
return rawValue;
|
|
223
344
|
}
|
|
224
345
|
}
|
|
346
|
+
/** Coerce batch JSON value: strings go through coerceValue; other types used as-is. */
|
|
347
|
+
function coerceBatchLeaf(raw, fieldType) {
|
|
348
|
+
if (typeof raw === 'string')
|
|
349
|
+
return coerceValue(raw, fieldType);
|
|
350
|
+
return raw;
|
|
351
|
+
}
|
|
352
|
+
function readBatchJson(filePath) {
|
|
353
|
+
if (filePath)
|
|
354
|
+
return readFileSync(filePath, 'utf-8');
|
|
355
|
+
return readFileSync(0, 'utf-8');
|
|
356
|
+
}
|
|
357
|
+
function buildSingleDescribeJson(found) {
|
|
358
|
+
if (found.systemType === 'external-theme' && found.jsonSchema) {
|
|
359
|
+
const fields = normalizeDescribeFields(describeJsonSchemaFields(found.jsonSchema, found.currentValue));
|
|
360
|
+
return {
|
|
361
|
+
key: found.key,
|
|
362
|
+
type: found.systemType,
|
|
363
|
+
isUsed: found.isUsed,
|
|
364
|
+
description: found.description,
|
|
365
|
+
fields,
|
|
366
|
+
guide: buildDescribeGuide(found.key, found.isUsed),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
if (found.configSchema) {
|
|
370
|
+
const currentValues = (found.currentValue && typeof found.currentValue === 'object')
|
|
371
|
+
? found.currentValue
|
|
372
|
+
: {};
|
|
373
|
+
const fields = normalizeDescribeFields(describeConfigSchemaFields(found.configSchema, currentValues));
|
|
374
|
+
return {
|
|
375
|
+
key: found.key,
|
|
376
|
+
type: found.systemType,
|
|
377
|
+
isUsed: found.isUsed,
|
|
378
|
+
description: found.description,
|
|
379
|
+
configPath: found.configPath,
|
|
380
|
+
fields,
|
|
381
|
+
guide: buildDescribeGuide(found.key, found.isUsed),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
key: found.key,
|
|
386
|
+
type: found.systemType,
|
|
387
|
+
isUsed: found.isUsed,
|
|
388
|
+
description: found.description,
|
|
389
|
+
currentValue: found.currentValue,
|
|
390
|
+
guide: buildDescribeGuide(found.key, found.isUsed),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
225
393
|
// ─── Command registration ────────────────────────────────────────────────────
|
|
226
394
|
export function registerPrefabCommands(program) {
|
|
227
395
|
const prefab = program
|
|
228
396
|
.command('prefab')
|
|
229
397
|
.description('Remix prefab 配置管理(自动检测 External Theme / PlayCanvas 项目类型)');
|
|
230
|
-
// ─── variants / themes / scenes (aliases) ────────────────────
|
|
231
398
|
const variantsHandler = (opts) => {
|
|
232
399
|
const projectDir = resolveProjectDir(opts);
|
|
233
400
|
const systemType = detectProjectType(projectDir);
|
|
@@ -283,144 +450,294 @@ export function registerPrefabCommands(program) {
|
|
|
283
450
|
.description('variants 别名(对齐前端「场景列表」术语)')
|
|
284
451
|
.option('--project-dir <path>', '项目根目录(默认 cwd)')
|
|
285
452
|
.action(variantsHandler);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
.description('列出所有 prefab 及状态')
|
|
453
|
+
withPagingOptions(withFilterOptions(withJsonOption(prefab.command('list'))))
|
|
454
|
+
.description('列出 prefab 及状态(支持分页与正则过滤)')
|
|
289
455
|
.option('--variant <id>', '指定变体(External: themeId;PlayCanvas: sceneId)')
|
|
290
456
|
.option('--project-dir <path>', '项目根目录(默认 cwd)')
|
|
457
|
+
.option('--with-values', '附带当前值摘要(已启用项)')
|
|
291
458
|
.action((opts) => {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
variantNote =
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const label = p.key.padEnd(keyW);
|
|
323
|
-
console.log(` ${label} ${status}`);
|
|
324
|
-
const detailIndent = ' ';
|
|
325
|
-
if (p.name && p.name !== p.key) {
|
|
326
|
-
console.log(`${detailIndent}名称: ${p.name}`);
|
|
327
|
-
}
|
|
328
|
-
const desc = p.description?.trim();
|
|
329
|
-
if (desc) {
|
|
330
|
-
console.log(`${detailIndent}说明: ${desc}`);
|
|
459
|
+
try {
|
|
460
|
+
const projectDir = resolveProjectDir(opts);
|
|
461
|
+
const systemType = detectProjectType(projectDir);
|
|
462
|
+
const all = loadPrefabs(projectDir, systemType, opts);
|
|
463
|
+
const asJson = Boolean(opts.json);
|
|
464
|
+
const variantResolved = resolveVariantResolved(projectDir, systemType, opts);
|
|
465
|
+
let variantNote = opts.variant ?? null;
|
|
466
|
+
if (systemType === 'external-theme' && !opts.variant) {
|
|
467
|
+
variantNote = resolveThemeId(projectDir, opts);
|
|
468
|
+
}
|
|
469
|
+
const summary = buildSummary(systemType, variantResolved, all);
|
|
470
|
+
const filtered = filterPrefabsList(all, {
|
|
471
|
+
match: opts.match,
|
|
472
|
+
matchCase: Boolean(opts.matchCase),
|
|
473
|
+
usedOnly: Boolean(opts.usedOnly),
|
|
474
|
+
unusedOnly: Boolean(opts.unusedOnly),
|
|
475
|
+
changedOnly: Boolean(opts.changedOnly),
|
|
476
|
+
});
|
|
477
|
+
const { limit, offset } = parsePaging(opts);
|
|
478
|
+
const { slice, page } = slicePage(filtered, limit, offset);
|
|
479
|
+
const prefabRows = slice.map((p) => {
|
|
480
|
+
const row = {
|
|
481
|
+
key: p.key,
|
|
482
|
+
name: p.name,
|
|
483
|
+
description: p.description,
|
|
484
|
+
isUsed: p.isUsed,
|
|
485
|
+
type: p.systemType,
|
|
486
|
+
};
|
|
487
|
+
if (opts.withValues) {
|
|
488
|
+
row.valuePreview = valuePreview(p);
|
|
331
489
|
}
|
|
332
|
-
|
|
333
|
-
|
|
490
|
+
return row;
|
|
491
|
+
});
|
|
492
|
+
const payload = {
|
|
493
|
+
summary,
|
|
494
|
+
page,
|
|
495
|
+
prefabs: prefabRows,
|
|
496
|
+
};
|
|
497
|
+
outputResult(asJson, payload, () => {
|
|
498
|
+
console.log(`项目类型: ${projectTypeLabel(systemType)}`);
|
|
499
|
+
console.log(`变体上下文: ${variantNote ?? '(默认)'}`);
|
|
500
|
+
console.log(`概况: 共 ${summary.total} 个 prefab,已启用 ${summary.enabled},未启用 ${summary.disabled},` +
|
|
501
|
+
`相对默认值有改动 ${summary.changed} 个。`);
|
|
502
|
+
console.log(`本页: ${filtered.length === 0 ? 0 : offset + 1}-${offset + slice.length} / 过滤后 ${page.total} 条` +
|
|
503
|
+
(page.hasMore ? `(尚有更多,使用 --offset ${offset + limit})` : ''));
|
|
504
|
+
console.log('');
|
|
505
|
+
const maxKeyLen = slice.reduce((m, p) => Math.max(m, p.key.length), 0);
|
|
506
|
+
const keyW = Math.min(28, Math.max(8, maxKeyLen));
|
|
507
|
+
for (let i = 0; i < slice.length; i++) {
|
|
508
|
+
const p = slice[i];
|
|
509
|
+
const status = p.isUsed ? '已启用' : '未启用';
|
|
510
|
+
const label = p.key.padEnd(keyW);
|
|
511
|
+
console.log(` ${label} ${status}`);
|
|
512
|
+
const detailIndent = ' ';
|
|
513
|
+
if (opts.withValues && p.isUsed) {
|
|
514
|
+
const pv = valuePreview(p, 200);
|
|
515
|
+
if (pv)
|
|
516
|
+
console.log(`${detailIndent}值摘要: ${pv}`);
|
|
517
|
+
}
|
|
518
|
+
if (p.name && p.name !== p.key) {
|
|
519
|
+
console.log(`${detailIndent}名称: ${p.name}`);
|
|
520
|
+
}
|
|
521
|
+
const desc = p.description?.trim();
|
|
522
|
+
if (desc) {
|
|
523
|
+
console.log(`${detailIndent}说明: ${desc}`);
|
|
524
|
+
}
|
|
525
|
+
if (i < slice.length - 1)
|
|
526
|
+
console.log('');
|
|
334
527
|
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
catch (e) {
|
|
531
|
+
if (isProjectDetectError(e))
|
|
532
|
+
throw e;
|
|
533
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
534
|
+
console.error(`Error: ${msg}`);
|
|
535
|
+
process.exit(1);
|
|
536
|
+
}
|
|
337
537
|
});
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
.description('显示 prefab 完整字段信息')
|
|
538
|
+
withPagingOptions(withFilterOptions(withJsonOption(prefab.command('diff'))))
|
|
539
|
+
.description('仅显示相对 schema 默认值有差异的字段')
|
|
341
540
|
.option('--variant <id>', '指定变体')
|
|
342
541
|
.option('--project-dir <path>', '项目根目录(默认 cwd)')
|
|
343
|
-
.action((
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
542
|
+
.action((opts) => {
|
|
543
|
+
try {
|
|
544
|
+
const projectDir = resolveProjectDir(opts);
|
|
545
|
+
const systemType = detectProjectType(projectDir);
|
|
546
|
+
const all = loadPrefabs(projectDir, systemType, opts);
|
|
547
|
+
const asJson = Boolean(opts.json);
|
|
548
|
+
const variantResolved = resolveVariantResolved(projectDir, systemType, opts);
|
|
549
|
+
const summary = buildSummary(systemType, variantResolved, all);
|
|
550
|
+
const withDiff = all.filter((p) => getPrefabFieldDiffs(p).length > 0);
|
|
551
|
+
const filtered = filterPrefabsList(withDiff, {
|
|
552
|
+
match: opts.match,
|
|
553
|
+
matchCase: Boolean(opts.matchCase),
|
|
554
|
+
usedOnly: Boolean(opts.usedOnly),
|
|
555
|
+
unusedOnly: Boolean(opts.unusedOnly),
|
|
556
|
+
changedOnly: Boolean(opts.changedOnly),
|
|
557
|
+
});
|
|
558
|
+
const { limit, offset } = parsePaging(opts);
|
|
559
|
+
const { slice, page } = slicePage(filtered, limit, offset);
|
|
560
|
+
const items = slice.map((p) => ({
|
|
561
|
+
key: p.key,
|
|
562
|
+
isUsed: p.isUsed,
|
|
563
|
+
diffs: getPrefabFieldDiffs(p),
|
|
564
|
+
}));
|
|
565
|
+
const payload = { summary, page, prefabs: items };
|
|
362
566
|
outputResult(asJson, payload, () => {
|
|
363
|
-
console.log(
|
|
364
|
-
console.log(
|
|
365
|
-
console.log(`状态: ${found.isUsed ? '已启用' : '未启用'}`);
|
|
366
|
-
if (found.description)
|
|
367
|
-
console.log(`说明: ${found.description}`);
|
|
567
|
+
console.log(`概况: ${summary.total} 个 prefab,其中 ${withDiff.length} 个有字段级差异。`);
|
|
568
|
+
console.log(`本页: ${page.total === 0 ? 0 : offset + 1}-${offset + slice.length} / ${page.total}`);
|
|
368
569
|
console.log('');
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
570
|
+
for (const it of items) {
|
|
571
|
+
console.log(` ${it.key}:`);
|
|
572
|
+
for (const d of it.diffs) {
|
|
573
|
+
console.log(` ${d.path}: 当前 ${formatHumanValue(d.current)} | 默认 ${formatHumanValue(d.default)}`);
|
|
574
|
+
}
|
|
575
|
+
console.log('');
|
|
372
576
|
}
|
|
373
577
|
});
|
|
374
578
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
579
|
+
catch (e) {
|
|
580
|
+
if (isProjectDetectError(e))
|
|
581
|
+
throw e;
|
|
582
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
583
|
+
console.error(`Error: ${msg}`);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
withPagingOptions(withFilterOptions(withJsonOption(prefab.command('describe [key]'))))
|
|
588
|
+
.description('显示 prefab 字段信息;省略 key 时需 --all')
|
|
589
|
+
.option('--all', '列出(过滤后)全部 prefab 的字段详情,分页输出')
|
|
590
|
+
.option('--variant <id>', '指定变体')
|
|
591
|
+
.option('--project-dir <path>', '项目根目录(默认 cwd)')
|
|
592
|
+
.action((key, opts) => {
|
|
593
|
+
try {
|
|
594
|
+
const projectDir = resolveProjectDir(opts);
|
|
595
|
+
const systemType = detectProjectType(projectDir);
|
|
596
|
+
const prefabs = loadPrefabs(projectDir, systemType, opts);
|
|
597
|
+
const asJson = Boolean(opts.json);
|
|
598
|
+
const variantResolved = resolveVariantResolved(projectDir, systemType, opts);
|
|
599
|
+
const summary = buildSummary(systemType, variantResolved, prefabs);
|
|
600
|
+
const allMode = Boolean(opts.all);
|
|
601
|
+
if (allMode && key) {
|
|
602
|
+
console.error('Error: 不能同时使用位置参数 key 与 --all。');
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
if (!allMode && !key) {
|
|
606
|
+
console.error('Error: 请指定 <key> 或传入 --all。');
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
if (!allMode && key) {
|
|
610
|
+
const found = prefabs.find((p) => p.key === key);
|
|
611
|
+
if (!found) {
|
|
612
|
+
console.error(`Error: prefab "${key}" 不存在。可用的 key: ${prefabs.map((p) => p.key).join(', ')}`);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
const payload = buildSingleDescribeJson(found);
|
|
616
|
+
outputResult(asJson, payload, () => {
|
|
617
|
+
console.log(`Prefab: ${found.key}`);
|
|
618
|
+
console.log(`类型: ${projectTypeLabel(found.systemType)}`);
|
|
619
|
+
console.log(`状态: ${found.isUsed ? '已启用' : '未启用'}`);
|
|
620
|
+
if (found.description)
|
|
621
|
+
console.log(`说明: ${found.description}`);
|
|
622
|
+
console.log('');
|
|
623
|
+
if (isDescribeJsonWithFields(payload)) {
|
|
624
|
+
console.log('字段:');
|
|
625
|
+
for (const f of payload.fields) {
|
|
626
|
+
console.log(` • ${describeFieldLine(f)}`);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
console.log('当前值:');
|
|
631
|
+
console.log(formatHumanValue(payload.currentValue));
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const filtered = filterPrefabsList(prefabs, {
|
|
637
|
+
match: opts.match,
|
|
638
|
+
matchCase: Boolean(opts.matchCase),
|
|
639
|
+
usedOnly: Boolean(opts.usedOnly),
|
|
640
|
+
unusedOnly: Boolean(opts.unusedOnly),
|
|
641
|
+
changedOnly: Boolean(opts.changedOnly),
|
|
642
|
+
});
|
|
643
|
+
const { limit, offset } = parsePaging(opts);
|
|
644
|
+
const { slice, page } = slicePage(filtered, limit, offset);
|
|
645
|
+
const blocks = slice.map((p) => buildSingleDescribeJson(p));
|
|
380
646
|
const payload = {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
647
|
+
summary,
|
|
648
|
+
page,
|
|
649
|
+
guide: {
|
|
650
|
+
workflow: [
|
|
651
|
+
'playcraft prefab list --json --limit 20 --offset 0',
|
|
652
|
+
'playcraft prefab describe --all --json --match "<regex>" --limit 20 --offset 0',
|
|
653
|
+
],
|
|
654
|
+
},
|
|
655
|
+
prefabs: blocks,
|
|
387
656
|
};
|
|
388
657
|
outputResult(asJson, payload, () => {
|
|
389
|
-
console.log(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
658
|
+
console.log(`概况: ${summary.total} 个 prefab;本页描述 ${blocks.length} 个(过滤后共 ${page.total})。`);
|
|
659
|
+
for (const b of blocks) {
|
|
660
|
+
console.log('');
|
|
661
|
+
console.log(`=== ${b.key} ===`);
|
|
662
|
+
if (isDescribeJsonWithFields(b)) {
|
|
663
|
+
for (const f of b.fields) {
|
|
664
|
+
console.log(` • ${describeFieldLine(f)}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
console.log(formatHumanValue(b.currentValue));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (page.hasMore) {
|
|
672
|
+
console.log('');
|
|
673
|
+
console.log(`下一页: --offset ${offset + limit}`);
|
|
400
674
|
}
|
|
401
675
|
});
|
|
402
676
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
677
|
+
catch (e) {
|
|
678
|
+
if (isProjectDetectError(e))
|
|
679
|
+
throw e;
|
|
680
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
681
|
+
console.error(`Error: ${msg}`);
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
withPagingOptions(withJsonOption(prefab.command('search <pattern>')))
|
|
686
|
+
.description('按正则搜索字段 path /说明 / 枚举(所有 prefab)')
|
|
687
|
+
.option('--match-case', '区分大小写')
|
|
688
|
+
.option('--variant <id>', '指定变体')
|
|
689
|
+
.option('--project-dir <path>', '项目根目录(默认 cwd)')
|
|
690
|
+
.action((pattern, opts) => {
|
|
691
|
+
try {
|
|
692
|
+
const projectDir = resolveProjectDir(opts);
|
|
693
|
+
const systemType = detectProjectType(projectDir);
|
|
694
|
+
const prefabs = loadPrefabs(projectDir, systemType, opts);
|
|
695
|
+
const asJson = Boolean(opts.json);
|
|
696
|
+
const variantResolved = resolveVariantResolved(projectDir, systemType, opts);
|
|
697
|
+
const summary = buildSummary(systemType, variantResolved, prefabs);
|
|
698
|
+
const re = tryCompileRegex(pattern, Boolean(opts.matchCase));
|
|
699
|
+
const hits = [];
|
|
700
|
+
for (const p of prefabs) {
|
|
701
|
+
for (const f of getPrefabFieldDescriptors(p)) {
|
|
702
|
+
const hay = [
|
|
703
|
+
f.path,
|
|
704
|
+
f.description ?? '',
|
|
705
|
+
...(f.enumOptions ?? []),
|
|
706
|
+
].join('\n');
|
|
707
|
+
if (re.test(hay)) {
|
|
708
|
+
hits.push({
|
|
709
|
+
prefabKey: p.key,
|
|
710
|
+
fieldPath: f.path,
|
|
711
|
+
type: f.type,
|
|
712
|
+
description: f.description,
|
|
713
|
+
currentValue: f.currentValue,
|
|
714
|
+
default: f.default,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const { limit, offset } = parsePaging(opts);
|
|
720
|
+
const { slice, page } = slicePage(hits, limit, offset);
|
|
721
|
+
const payload = { summary, page, hits: slice };
|
|
411
722
|
outputResult(asJson, payload, () => {
|
|
412
|
-
console.log(
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
723
|
+
console.log(`命中 ${hits.length} 条字段(本页 ${slice.length} 条)`);
|
|
724
|
+
for (const h of slice) {
|
|
725
|
+
console.log(` ${h.prefabKey}.${h.fieldPath} (${h.type})`);
|
|
726
|
+
if (h.description)
|
|
727
|
+
console.log(` ${h.description}`);
|
|
728
|
+
}
|
|
729
|
+
if (page.hasMore)
|
|
730
|
+
console.log(`下一页: --offset ${offset + limit}`);
|
|
420
731
|
});
|
|
421
732
|
}
|
|
733
|
+
catch (e) {
|
|
734
|
+
if (isProjectDetectError(e))
|
|
735
|
+
throw e;
|
|
736
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
737
|
+
console.error(`Error: ${msg}`);
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
422
740
|
});
|
|
423
|
-
// ─── get ─────────────────────────────────────────────────────
|
|
424
741
|
withJsonOption(prefab.command('get <key> [field]'))
|
|
425
742
|
.description('获取 prefab 或子字段的当前值')
|
|
426
743
|
.option('--variant <id>', '指定变体')
|
|
@@ -454,7 +771,6 @@ export function registerPrefabCommands(program) {
|
|
|
454
771
|
}
|
|
455
772
|
});
|
|
456
773
|
});
|
|
457
|
-
// ─── set ─────────────────────────────────────────────────────
|
|
458
774
|
withJsonOption(prefab.command('set <key> <field> <value>'))
|
|
459
775
|
.description('修改 prefab 子字段值(带校验)')
|
|
460
776
|
.option('--variant <id>', '指定变体')
|
|
@@ -533,7 +849,141 @@ export function registerPrefabCommands(program) {
|
|
|
533
849
|
});
|
|
534
850
|
}
|
|
535
851
|
});
|
|
536
|
-
|
|
852
|
+
withJsonOption(prefab.command('set-batch'))
|
|
853
|
+
.description('从文件或 stdin 读取 JSON,一次修改多个 prefab 字段(校验全部通过后写盘)')
|
|
854
|
+
.option('--file <path>', 'JSON 文件路径(默认读 stdin)')
|
|
855
|
+
.option('--variant <id>', '指定变体')
|
|
856
|
+
.option('--project-dir <path>', '项目根目录(默认 cwd)')
|
|
857
|
+
.action((opts) => {
|
|
858
|
+
const projectDir = resolveProjectDir(opts);
|
|
859
|
+
const systemType = detectProjectType(projectDir);
|
|
860
|
+
const asJson = Boolean(opts.json);
|
|
861
|
+
let raw;
|
|
862
|
+
try {
|
|
863
|
+
raw = readBatchJson(opts.file);
|
|
864
|
+
}
|
|
865
|
+
catch (e) {
|
|
866
|
+
console.error(`Error: 无法读取 batch输入: ${e instanceof Error ? e.message : e}`);
|
|
867
|
+
process.exit(1);
|
|
868
|
+
}
|
|
869
|
+
const trimmed = raw.trim();
|
|
870
|
+
if (!trimmed) {
|
|
871
|
+
console.error('Error: batch 输入为空。请使用 --file 或管道传入 JSON。');
|
|
872
|
+
process.exit(1);
|
|
873
|
+
}
|
|
874
|
+
let batch;
|
|
875
|
+
try {
|
|
876
|
+
batch = JSON.parse(trimmed);
|
|
877
|
+
}
|
|
878
|
+
catch (e) {
|
|
879
|
+
console.error(`Error: batch JSON 解析失败: ${e instanceof Error ? e.message : e}`);
|
|
880
|
+
process.exit(1);
|
|
881
|
+
}
|
|
882
|
+
if (!batch || typeof batch !== 'object' || Array.isArray(batch)) {
|
|
883
|
+
console.error('Error: batch 必须是形如 { "prefabKey": { "field": value } } 的对象。');
|
|
884
|
+
process.exit(1);
|
|
885
|
+
}
|
|
886
|
+
try {
|
|
887
|
+
if (systemType === 'external-theme') {
|
|
888
|
+
const themeId = resolveThemeId(projectDir, opts);
|
|
889
|
+
const schema = loadThemeSchema(projectDir);
|
|
890
|
+
let data = loadThemeData(projectDir, themeId);
|
|
891
|
+
const prefabs = parseThemePrefabs(schema, data, themeId);
|
|
892
|
+
for (const [prefabKey, fieldMap] of Object.entries(batch)) {
|
|
893
|
+
if (!fieldMap || typeof fieldMap !== 'object' || Array.isArray(fieldMap)) {
|
|
894
|
+
throw new Error(`prefab "${prefabKey}" 的值必须是对象(字段 map)`);
|
|
895
|
+
}
|
|
896
|
+
const found = prefabs.find((p) => p.key === prefabKey);
|
|
897
|
+
if (!found) {
|
|
898
|
+
throw new Error(`prefab "${prefabKey}" 不存在`);
|
|
899
|
+
}
|
|
900
|
+
const currentValue = data[prefabKey];
|
|
901
|
+
let newValue;
|
|
902
|
+
if (found.jsonSchema?.type === 'object') {
|
|
903
|
+
let obj = (currentValue && typeof currentValue === 'object' && !Array.isArray(currentValue))
|
|
904
|
+
? { ...currentValue }
|
|
905
|
+
: {};
|
|
906
|
+
for (const [field, rawVal] of Object.entries(fieldMap)) {
|
|
907
|
+
const fieldSchema = resolveJsonSchemaField(found.jsonSchema, field);
|
|
908
|
+
const fieldType = fieldSchema?.type ?? 'string';
|
|
909
|
+
const coerced = coerceBatchLeaf(rawVal, fieldType);
|
|
910
|
+
if (fieldSchema) {
|
|
911
|
+
const result = validateThemeValue(fieldSchema, coerced);
|
|
912
|
+
if (!result.valid) {
|
|
913
|
+
throw new Error(`${prefabKey}.${field}: ${result.errors.join('; ')}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
obj = setByDotPath(obj, field, coerced);
|
|
917
|
+
}
|
|
918
|
+
newValue = obj;
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
const entries = Object.entries(fieldMap);
|
|
922
|
+
if (entries.length !== 1) {
|
|
923
|
+
throw new Error(`prefab "${prefabKey}" 非 object schema,batch 内只能包含一个字段`);
|
|
924
|
+
}
|
|
925
|
+
const [field, rawVal] = entries[0];
|
|
926
|
+
const fieldSchema = resolveJsonSchemaField(found.jsonSchema, field);
|
|
927
|
+
const fieldType = fieldSchema?.type ?? 'string';
|
|
928
|
+
const coerced = coerceBatchLeaf(rawVal, fieldType);
|
|
929
|
+
if (fieldSchema) {
|
|
930
|
+
const result = validateThemeValue(fieldSchema, coerced);
|
|
931
|
+
if (!result.valid) {
|
|
932
|
+
throw new Error(`${prefabKey}.${field}: ${result.errors.join('; ')}`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
newValue = coerced;
|
|
936
|
+
}
|
|
937
|
+
data = mergeThemeDataKey(data, prefabKey, newValue);
|
|
938
|
+
}
|
|
939
|
+
writeThemeData(projectDir, themeId, data);
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
const configPath = resolveGameConfigPathForVariant(projectDir, opts.variant);
|
|
943
|
+
let gameConfigJson = loadGameConfig(projectDir, configPath);
|
|
944
|
+
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}" 不存在`);
|
|
952
|
+
}
|
|
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('; ')}`);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
currentValues = setByDotPath(currentValues, field, coerced);
|
|
971
|
+
}
|
|
972
|
+
gameConfigJson = updateGameConfigValue(gameConfigJson, configKey, currentValues);
|
|
973
|
+
}
|
|
974
|
+
writeGameConfig(projectDir, configPath, gameConfigJson);
|
|
975
|
+
}
|
|
976
|
+
const payload = { success: true, updatedKeys: Object.keys(batch) };
|
|
977
|
+
outputResult(asJson, payload, () => {
|
|
978
|
+
console.log(`已批量更新: ${Object.keys(batch).join(', ')}`);
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
catch (e) {
|
|
982
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
983
|
+
console.error(`Error: ${msg}`);
|
|
984
|
+
process.exit(1);
|
|
985
|
+
}
|
|
986
|
+
});
|
|
537
987
|
withJsonOption(prefab.command('enable <key>'))
|
|
538
988
|
.description('启用 prefab')
|
|
539
989
|
.option('--variant <id>', '指定变体')
|
|
@@ -579,7 +1029,6 @@ export function registerPrefabCommands(program) {
|
|
|
579
1029
|
});
|
|
580
1030
|
}
|
|
581
1031
|
});
|
|
582
|
-
// ─── disable ─────────────────────────────────────────────────
|
|
583
1032
|
withJsonOption(prefab.command('disable <key>'))
|
|
584
1033
|
.description('禁用 prefab')
|
|
585
1034
|
.option('--variant <id>', '指定变体')
|
|
@@ -625,7 +1074,6 @@ export function registerPrefabCommands(program) {
|
|
|
625
1074
|
});
|
|
626
1075
|
}
|
|
627
1076
|
});
|
|
628
|
-
// ─── switch ──────────────────────────────────────────────────
|
|
629
1077
|
withJsonOption(prefab.command('switch <variant>'))
|
|
630
1078
|
.description('切换活跃变体(External: 修改 index.ts;PlayCanvas: 指定场景上下文)')
|
|
631
1079
|
.option('--project-dir <path>', '项目根目录(默认 cwd)')
|
|
@@ -669,7 +1117,6 @@ export function registerPrefabCommands(program) {
|
|
|
669
1117
|
}
|
|
670
1118
|
});
|
|
671
1119
|
}
|
|
672
|
-
// ─── JSON Schema field resolver ──────────────────────────────────────────────
|
|
673
1120
|
function resolveJsonSchemaField(schema, dotPath) {
|
|
674
1121
|
const parts = dotPath.split('.');
|
|
675
1122
|
let cur = schema;
|
package/dist/commands/tools.js
CHANGED
|
@@ -102,6 +102,20 @@ function mimeTypeForImagePath(filePath) {
|
|
|
102
102
|
return 'image/webp';
|
|
103
103
|
return 'image/jpeg';
|
|
104
104
|
}
|
|
105
|
+
function readReferenceImagePayload(filePath) {
|
|
106
|
+
try {
|
|
107
|
+
return {
|
|
108
|
+
base64: readFileSync(filePath).toString('base64'),
|
|
109
|
+
mimeType: mimeTypeForImagePath(filePath),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
114
|
+
const code = e && typeof e === 'object' && 'code' in e ? String(e.code) : '';
|
|
115
|
+
const hint = code ? ` (${code})` : '';
|
|
116
|
+
throw new Error(`Failed to read reference image: ${filePath}${hint}\n ${detail}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
105
119
|
export function registerToolsCommands(program) {
|
|
106
120
|
const tools = program
|
|
107
121
|
.command('tools')
|
|
@@ -117,12 +131,7 @@ export function registerToolsCommands(program) {
|
|
|
117
131
|
.action(async (opts) => {
|
|
118
132
|
try {
|
|
119
133
|
const paths = opts.referenceImage ?? [];
|
|
120
|
-
const referenceImages = paths.length > 0
|
|
121
|
-
? paths.map((p) => ({
|
|
122
|
-
base64: readFileSync(p).toString('base64'),
|
|
123
|
-
mimeType: mimeTypeForImagePath(p),
|
|
124
|
-
}))
|
|
125
|
-
: undefined;
|
|
134
|
+
const referenceImages = paths.length > 0 ? paths.map((p) => readReferenceImagePayload(p)) : undefined;
|
|
126
135
|
const client = new AgentApiClient();
|
|
127
136
|
const result = await client.post('/generate-image', {
|
|
128
137
|
prompt: opts.prompt,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"release": "node scripts/release.js"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@playcraft/build": "^0.0.
|
|
26
|
-
"@playcraft/common": "^0.0.
|
|
25
|
+
"@playcraft/build": "^0.0.28",
|
|
26
|
+
"@playcraft/common": "^0.0.17",
|
|
27
27
|
"chokidar": "^4.0.3",
|
|
28
28
|
"commander": "^13.1.0",
|
|
29
29
|
"cors": "^2.8.6",
|