@playcraft/cli 0.1.82 → 0.1.84-beta
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/references.js +4 -2
- package/dist/commands/tools-generation.js +102 -46
- package/dist/commands/tools-utils.js +15 -17
- package/dist/references/messages.js +2 -0
- package/dist/references/protocol.js +4 -1
- package/dist/utils/image-asset-context.js +92 -42
- package/dist/workspace-runtime/codex/app-server.js +1 -1
- package/dist/workspace-runtime/codex/remix-references-broker.js +1 -1
- package/package.json +3 -3
|
@@ -16,7 +16,8 @@ export function registerReferenceCommands(program) {
|
|
|
16
16
|
...(options.tag?.length ? { tagCategory: options.tagCategory ?? 'Gameplay', tagValues: options.tag }
|
|
17
17
|
: options.tagCategory === undefined ? {} : { tagCategory: options.tagCategory }),
|
|
18
18
|
page: Number(options.page), limit: Number(options.limit) } : {}),
|
|
19
|
-
...(action === 'import' ? { projectUuid: options.project, branchName: options.branch } : {})
|
|
19
|
+
...(action === 'import' ? { projectUuid: options.project, branchName: options.branch } : {}),
|
|
20
|
+
...(action === 'prepare-starter-template' ? { templateId: options.template } : {}) });
|
|
20
21
|
const output = { schemaVersion: 'playcraft.references.v1', command: `references.${action}`, ...result };
|
|
21
22
|
if (options.json)
|
|
22
23
|
console.log(JSON.stringify(output));
|
|
@@ -45,7 +46,8 @@ export function registerReferenceCommands(program) {
|
|
|
45
46
|
const scope = (command) => command.option('--workspace <id>', message('workspace'))
|
|
46
47
|
.option('--directory <name>', message('directory')).option('--json', message('json'));
|
|
47
48
|
scope(references.command('list').description(message('list'))).action(options => run('list', options));
|
|
48
|
-
scope(references.command('prepare-starter-template').description(message('prepareStarterTemplate'))
|
|
49
|
+
scope(references.command('prepare-starter-template').description(message('prepareStarterTemplate'))
|
|
50
|
+
.option('--template <id>', message('template'))).action(options => run('prepare-starter-template', options));
|
|
49
51
|
scope(references.command('search [query]').description(message('search'))
|
|
50
52
|
.option('--source <source>', message('source'), 'recommended')
|
|
51
53
|
.option('--tag <value>', message('tag'), (value, previous) => [...previous, value], [])
|
|
@@ -3,8 +3,8 @@ import { writeFileSync, mkdirSync } from 'fs';
|
|
|
3
3
|
import { dirname, parse, join } from 'path';
|
|
4
4
|
import sharp from 'sharp';
|
|
5
5
|
import { captureGeneratedArtifact } from '../workspace-runtime/workspaces/artifact-receipts.js';
|
|
6
|
-
import { resolveImageBackgroundRequirement, validateImageDeliveryOptions } from '@playcraft/common/prefab';
|
|
7
|
-
import { assertImageAssetOutputPaths, readImageAssetContext } from '../utils/image-asset-context.js';
|
|
6
|
+
import { resolveImageBackgroundRequirement, validateImageDeliveryOptions, } from '@playcraft/common/prefab';
|
|
7
|
+
import { assertImageAssetOutputPaths, readImageAssetContext, } from '../utils/image-asset-context.js';
|
|
8
8
|
import { AgentApiClient } from '../utils/agent-api-client.js';
|
|
9
9
|
import { isUnsafeImageRetryError } from '../utils/image-generation-retry.js';
|
|
10
10
|
import { MAX_REFERENCE_IMAGES, collectReferenceImagePaths, parseImageTargetFieldPath, collectReferenceImagePayloads, sniffImageExtension, extensionForImageMime, resolveImageOutputPath, handleError, raceWithTimeout, } from './tools-utils.js';
|
|
@@ -73,7 +73,8 @@ async function verifyMasterCompositeStripSize(imgBuf, target) {
|
|
|
73
73
|
}
|
|
74
74
|
export function registerGenerationCommands(tools) {
|
|
75
75
|
// ─── Image Models ────────────────────────────────────────────
|
|
76
|
-
tools
|
|
76
|
+
tools
|
|
77
|
+
.command('list-image-models')
|
|
77
78
|
.description('列出后端已配置的可用生图模型,按模型名分组展示所有 provider(多个 provider 时按优先级自动 fallback)')
|
|
78
79
|
.option('--json', '以 JSON 格式输出(默认为人类可读表格)')
|
|
79
80
|
.action(async (opts) => {
|
|
@@ -150,7 +151,8 @@ export function registerGenerationCommands(tools) {
|
|
|
150
151
|
}
|
|
151
152
|
});
|
|
152
153
|
// ─── Generation ─────────────────────────────────────────────
|
|
153
|
-
tools
|
|
154
|
+
tools
|
|
155
|
+
.command('generate-image')
|
|
154
156
|
.description('AI 生成图片(支持多张参考图图生图;gpt-image-2 支持 --count N 批量出图)')
|
|
155
157
|
.requiredOption('--prompt <text>', '图片描述')
|
|
156
158
|
.option('--aspect-ratio <ratio>', '宽高比 (1:1|45:16|16:9|9:16|3:4|4:3|2:3|3:2|21:9)', '1:1')
|
|
@@ -174,8 +176,12 @@ export function registerGenerationCommands(tools) {
|
|
|
174
176
|
.option('--count <n>', '一次 API 调用生成多少张图片(默认 1,最多 10;仅 gpt-image-2 实际支持批量)', (v) => parseInt(v, 10), 1)
|
|
175
177
|
.action(async (opts) => {
|
|
176
178
|
try {
|
|
177
|
-
const explicitBackground = opts.background === undefined
|
|
178
|
-
|
|
179
|
+
const explicitBackground = opts.background === undefined
|
|
180
|
+
? opts.removeBackground
|
|
181
|
+
: opts.background === 'transparent';
|
|
182
|
+
if (opts.background !== undefined &&
|
|
183
|
+
opts.removeBackground !== undefined &&
|
|
184
|
+
opts.removeBackground !== explicitBackground) {
|
|
179
185
|
throw new Error('Conflicting background options. Use --background transparent or --background opaque; no image request was sent.');
|
|
180
186
|
}
|
|
181
187
|
opts = { ...opts, removeBackground: explicitBackground };
|
|
@@ -184,28 +190,46 @@ export function registerGenerationCommands(tools) {
|
|
|
184
190
|
}
|
|
185
191
|
const explicitOutputFormat = opts.outputFormat;
|
|
186
192
|
const { dir: outputDir, name: outputName, ext: outputExt } = parse(opts.output);
|
|
187
|
-
const outputCandidates = [
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
const outputCandidates = [
|
|
194
|
+
opts.output,
|
|
195
|
+
...Array.from({ length: opts.count > 1 ? Math.min(opts.count, 10) : 0 }, (_, i) => join(outputDir, `${outputName}_${i + 1}${outputExt}`)),
|
|
196
|
+
];
|
|
197
|
+
const assetContext = opts.targetAsset
|
|
198
|
+
? await readImageAssetContext(opts.targetAsset, process.cwd(), {
|
|
199
|
+
referenceImagePaths: opts.referenceImage,
|
|
200
|
+
useTargetReference: opts.targetReference,
|
|
201
|
+
...(opts.targetFieldPath ? { targetFieldPath: opts.targetFieldPath } : {}),
|
|
202
|
+
outputPaths: outputCandidates,
|
|
203
|
+
prompt: opts.prompt,
|
|
204
|
+
removeBackground: opts.removeBackground,
|
|
205
|
+
})
|
|
206
|
+
: undefined;
|
|
207
|
+
opts = {
|
|
208
|
+
...opts,
|
|
195
209
|
aspectRatio: assetContext?.aspectRatio ?? opts.aspectRatio,
|
|
196
|
-
width: assetContext?.width ?? opts.width,
|
|
197
|
-
|
|
210
|
+
width: assetContext?.width ?? opts.width,
|
|
211
|
+
height: assetContext?.height ?? opts.height,
|
|
212
|
+
outputFormat: assetContext?.outputFormatFromSchema
|
|
213
|
+
? assetContext.outputFormat
|
|
214
|
+
: (opts.outputFormat ?? assetContext?.outputFormat),
|
|
198
215
|
fitMode: assetContext?.fitMode ?? opts.fitMode,
|
|
199
|
-
removeBackground: resolveImageBackgroundRequirement({
|
|
200
|
-
|
|
201
|
-
|
|
216
|
+
removeBackground: resolveImageBackgroundRequirement({
|
|
217
|
+
prompt: opts.prompt,
|
|
218
|
+
schemaRemoveBackground: assetContext?.removeBackgroundFromSchema
|
|
219
|
+
? assetContext.removeBackground
|
|
220
|
+
: undefined,
|
|
221
|
+
removeBackground: opts.removeBackground,
|
|
222
|
+
fallbackRemoveBackground: assetContext?.removeBackground,
|
|
223
|
+
}),
|
|
202
224
|
paddingColor: assetContext?.paddingColor ?? opts.paddingColor,
|
|
203
225
|
imageModel: opts.imageModel || assetContext?.imageModelRef,
|
|
204
226
|
};
|
|
205
227
|
const invalidDelivery = validateImageDeliveryOptions(opts);
|
|
206
228
|
if (invalidDelivery)
|
|
207
229
|
throw new Error(invalidDelivery);
|
|
208
|
-
const paths = opts.referenceImage?.length
|
|
230
|
+
const paths = opts.referenceImage?.length
|
|
231
|
+
? opts.referenceImage
|
|
232
|
+
: (assetContext?.referenceImagePaths ?? []);
|
|
209
233
|
if (paths.length > MAX_REFERENCE_IMAGES) {
|
|
210
234
|
console.error(`Error: Too many reference images: ${paths.length} paths given; maximum is ${MAX_REFERENCE_IMAGES}.`);
|
|
211
235
|
process.exit(1);
|
|
@@ -230,14 +254,26 @@ export function registerGenerationCommands(tools) {
|
|
|
230
254
|
width: assetContext?.width ?? opts.width,
|
|
231
255
|
height: assetContext?.height ?? opts.height,
|
|
232
256
|
outputFormat: opts.outputFormat,
|
|
233
|
-
outputFormatSource: assetContext?.outputFormatFromSchema
|
|
234
|
-
|
|
257
|
+
outputFormatSource: assetContext?.outputFormatFromSchema
|
|
258
|
+
? 'schema'
|
|
259
|
+
: explicitOutputFormat
|
|
260
|
+
? 'explicit'
|
|
261
|
+
: assetContext
|
|
262
|
+
? 'inferred'
|
|
263
|
+
: undefined,
|
|
235
264
|
fitMode: assetContext?.fitMode ?? opts.fitMode,
|
|
236
265
|
paddingColor: assetContext?.paddingColor ?? opts.paddingColor,
|
|
237
|
-
background: opts.background === undefined
|
|
266
|
+
background: opts.background === undefined
|
|
267
|
+
? undefined
|
|
268
|
+
: opts.removeBackground
|
|
269
|
+
? 'transparent'
|
|
270
|
+
: 'opaque',
|
|
238
271
|
removeBackground: opts.removeBackground,
|
|
239
|
-
backgroundRequirementSource: assetContext?.removeBackgroundFromSchema
|
|
240
|
-
|
|
272
|
+
backgroundRequirementSource: assetContext?.removeBackgroundFromSchema
|
|
273
|
+
? 'schema'
|
|
274
|
+
: explicitBackground !== undefined
|
|
275
|
+
? 'explicit'
|
|
276
|
+
: 'inferred',
|
|
241
277
|
referenceImages,
|
|
242
278
|
...(modelRef ? { imageModelRef: modelRef } : {}),
|
|
243
279
|
...(imageCount > 1 ? { imageCount } : {}),
|
|
@@ -265,7 +301,8 @@ export function registerGenerationCommands(tools) {
|
|
|
265
301
|
lines.push(' → Try --image-size 1K to reduce generation time');
|
|
266
302
|
lines.push(' → Or increase timeout with --timeout <seconds> (current default: 1800s)');
|
|
267
303
|
}
|
|
268
|
-
else if (hasReferenceImages &&
|
|
304
|
+
else if (hasReferenceImages &&
|
|
305
|
+
(msg.includes('reference') || msg.includes('image input'))) {
|
|
269
306
|
lines.push('');
|
|
270
307
|
lines.push('Hint: Reference image error — the selected model may not support --reference-image.');
|
|
271
308
|
lines.push(' → Run: playcraft tools list-image-models');
|
|
@@ -313,7 +350,11 @@ export function registerGenerationCommands(tools) {
|
|
|
313
350
|
}
|
|
314
351
|
// 收集所有待保存图片(第 1 张 + 批量额外图)
|
|
315
352
|
const allImages = [
|
|
316
|
-
{
|
|
353
|
+
{
|
|
354
|
+
imageBase64: result.imageBase64,
|
|
355
|
+
mimeType: result.mimeType,
|
|
356
|
+
previewUrl: result.previewUrl,
|
|
357
|
+
},
|
|
317
358
|
...(result.additionalImages ?? []),
|
|
318
359
|
];
|
|
319
360
|
const isBatch = allImages.length > 1;
|
|
@@ -324,12 +365,14 @@ export function registerGenerationCommands(tools) {
|
|
|
324
365
|
const savedPaths = [];
|
|
325
366
|
if (result.transparencySource)
|
|
326
367
|
console.log(`Transparency source: ${result.transparencySource}`);
|
|
327
|
-
const stripTarget = opts.targetAsset || result.transparentBackground
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
368
|
+
const stripTarget = opts.targetAsset || result.transparentBackground
|
|
369
|
+
? null
|
|
370
|
+
: resolveStripTargetDimensions({
|
|
371
|
+
aspectRatio: opts.aspectRatio,
|
|
372
|
+
width: opts.width,
|
|
373
|
+
height: opts.height,
|
|
374
|
+
imageSize: opts.imageSize,
|
|
375
|
+
});
|
|
333
376
|
for (let i = 0; i < allImages.length; i++) {
|
|
334
377
|
const img = allImages[i];
|
|
335
378
|
let imgBuf = Buffer.from(img.imageBase64, 'base64');
|
|
@@ -347,8 +390,7 @@ export function registerGenerationCommands(tools) {
|
|
|
347
390
|
generatedHeight = metaBefore.height;
|
|
348
391
|
if (metaBefore.width != null &&
|
|
349
392
|
metaBefore.height != null &&
|
|
350
|
-
(metaBefore.width !== stripTarget.width ||
|
|
351
|
-
metaBefore.height !== stripTarget.height)) {
|
|
393
|
+
(metaBefore.width !== stripTarget.width || metaBefore.height !== stripTarget.height)) {
|
|
352
394
|
imgBuf = Buffer.from(await letterboxToTarget(imgBuf, stripTarget.width, stripTarget.height));
|
|
353
395
|
console.log(`↑ Letterbox ${generatedWidth}×${generatedHeight} → ${stripTarget.width}×${stripTarget.height}(等比 + 白边,不拉伸)`);
|
|
354
396
|
}
|
|
@@ -404,10 +446,11 @@ export function registerGenerationCommands(tools) {
|
|
|
404
446
|
referenceImageStatus: result.referenceImageStatus ?? null,
|
|
405
447
|
generatedAt: new Date().toISOString(),
|
|
406
448
|
};
|
|
407
|
-
const sidecarPath = resolvedPath.
|
|
449
|
+
const sidecarPath = `${resolvedPath}.generation.json`;
|
|
408
450
|
writeFileSync(sidecarPath, JSON.stringify(sidecar, null, 2));
|
|
409
451
|
await captureGeneratedArtifact(resolvedPath, 'generate-image', {
|
|
410
|
-
...(actualWidth ? { width: actualWidth } : {}),
|
|
452
|
+
...(actualWidth ? { width: actualWidth } : {}),
|
|
453
|
+
...(actualHeight ? { height: actualHeight } : {}),
|
|
411
454
|
});
|
|
412
455
|
console.log(` └─ Generation metadata: ${sidecarPath}`);
|
|
413
456
|
}
|
|
@@ -431,7 +474,8 @@ export function registerGenerationCommands(tools) {
|
|
|
431
474
|
handleError(e);
|
|
432
475
|
}
|
|
433
476
|
});
|
|
434
|
-
tools
|
|
477
|
+
tools
|
|
478
|
+
.command('generate-sfx')
|
|
435
479
|
.description('AI 生成音效(SFX),使用 ElevenLabs Sound Effects')
|
|
436
480
|
.requiredOption('--prompt <text>', '音效描述,仅英文,如 "crisp UI click, short tail, no voice"')
|
|
437
481
|
.option('--duration <seconds>', '时长(秒,0.5-22;省略时由供应商自动决定)', parseFloat)
|
|
@@ -451,9 +495,14 @@ export function registerGenerationCommands(tools) {
|
|
|
451
495
|
const sizeKB = Math.round(buf.length / 1024);
|
|
452
496
|
console.log(`SFX saved to ${opts.output} (${sizeKB}KB, ${result.duration.toFixed(2)}s, provider=${result.provider})`);
|
|
453
497
|
const sfxSidecar = {
|
|
454
|
-
type: 'sfx',
|
|
455
|
-
|
|
456
|
-
|
|
498
|
+
type: 'sfx',
|
|
499
|
+
prompt: opts.prompt,
|
|
500
|
+
duration: opts.duration ?? null,
|
|
501
|
+
loop: opts.loop ?? false,
|
|
502
|
+
provider: result.provider,
|
|
503
|
+
actualDuration: result.duration,
|
|
504
|
+
output: opts.output,
|
|
505
|
+
generatedAt: new Date().toISOString(),
|
|
457
506
|
};
|
|
458
507
|
writeFileSync(opts.output.replace(/\.[^.]+$/, '.json'), JSON.stringify(sfxSidecar, null, 2));
|
|
459
508
|
await captureGeneratedArtifact(opts.output, 'generate-sfx', { duration: result.duration });
|
|
@@ -463,7 +512,8 @@ export function registerGenerationCommands(tools) {
|
|
|
463
512
|
handleError(e);
|
|
464
513
|
}
|
|
465
514
|
});
|
|
466
|
-
tools
|
|
515
|
+
tools
|
|
516
|
+
.command('generate-bgm')
|
|
467
517
|
.description('AI 生成 BGM(30s 循环),使用 Google Lyria 3')
|
|
468
518
|
.requiredOption('--prompt <text>', 'BGM 描述,如 "轻快休闲游戏配乐"')
|
|
469
519
|
.option('--style <style>', '音乐风格,如 casual / epic / sci-fi / retro')
|
|
@@ -484,9 +534,14 @@ export function registerGenerationCommands(tools) {
|
|
|
484
534
|
const sizeKB = Math.round(buf.length / 1024);
|
|
485
535
|
console.log(`BGM saved to ${opts.output} (${sizeKB}KB, ${result.duration.toFixed(2)}s, provider=${result.provider})`);
|
|
486
536
|
const bgmSidecar = {
|
|
487
|
-
type: 'bgm',
|
|
488
|
-
|
|
489
|
-
|
|
537
|
+
type: 'bgm',
|
|
538
|
+
prompt: opts.prompt,
|
|
539
|
+
style: opts.style ?? null,
|
|
540
|
+
bpm: opts.bpm ?? null,
|
|
541
|
+
provider: result.provider,
|
|
542
|
+
actualDuration: result.duration,
|
|
543
|
+
output: opts.output,
|
|
544
|
+
generatedAt: new Date().toISOString(),
|
|
490
545
|
};
|
|
491
546
|
writeFileSync(opts.output.replace(/\.[^.]+$/, '.json'), JSON.stringify(bgmSidecar, null, 2));
|
|
492
547
|
await captureGeneratedArtifact(opts.output, 'generate-bgm', { duration: result.duration });
|
|
@@ -517,7 +572,8 @@ export function registerGenerationCommands(tools) {
|
|
|
517
572
|
}
|
|
518
573
|
});
|
|
519
574
|
// ─── Search Image ──────────────────────────────────────────
|
|
520
|
-
tools
|
|
575
|
+
tools
|
|
576
|
+
.command('search-image')
|
|
521
577
|
.description('搜索参考图片(Unsplash / Pexels),结果 URL 可直接用于 --reference-image')
|
|
522
578
|
.requiredOption('--query <query>', '搜索关键词(建议英文)')
|
|
523
579
|
.option('--source <source>', '图源:unsplash | pexels | auto(默认 auto)', 'auto')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync, readFileSync } from 'fs';
|
|
2
|
-
import { join, parse } from 'path';
|
|
2
|
+
import { join, parse, resolve } from 'path';
|
|
3
3
|
import { tmpdir } from 'os';
|
|
4
4
|
import { InvalidArgumentError } from 'commander';
|
|
5
5
|
export const TMP_DIR = join(tmpdir(), 'playcraft');
|
|
@@ -9,8 +9,12 @@ export function parseImageTargetFieldPath(value) {
|
|
|
9
9
|
try {
|
|
10
10
|
parsed = JSON.parse(value);
|
|
11
11
|
}
|
|
12
|
-
catch {
|
|
13
|
-
|
|
12
|
+
catch {
|
|
13
|
+
/* Report the expected shape below. */
|
|
14
|
+
}
|
|
15
|
+
if (!Array.isArray(parsed) ||
|
|
16
|
+
!parsed.length ||
|
|
17
|
+
parsed.some((key) => typeof key !== 'string' || !key.length)) {
|
|
14
18
|
throw new InvalidArgumentError('Expected an array of field names, e.g. ["endUI","playButton"].');
|
|
15
19
|
}
|
|
16
20
|
return parsed;
|
|
@@ -74,11 +78,7 @@ export function extensionForImageMime(mimeType) {
|
|
|
74
78
|
* Detect container from decoded bytes (magic). Prefer this over MIME alone.
|
|
75
79
|
*/
|
|
76
80
|
export function sniffImageExtension(buf) {
|
|
77
|
-
if (buf.length >= 8 &&
|
|
78
|
-
buf[0] === 0x89 &&
|
|
79
|
-
buf[1] === 0x50 &&
|
|
80
|
-
buf[2] === 0x4e &&
|
|
81
|
-
buf[3] === 0x47) {
|
|
81
|
+
if (buf.length >= 8 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
|
|
82
82
|
return '.png';
|
|
83
83
|
}
|
|
84
84
|
if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
|
|
@@ -133,12 +133,14 @@ export function mimeTypeForImagePath(filePath) {
|
|
|
133
133
|
return 'image/png';
|
|
134
134
|
if (ext === 'webp')
|
|
135
135
|
return 'image/webp';
|
|
136
|
+
if (ext === 'gif')
|
|
137
|
+
return 'image/gif';
|
|
136
138
|
if (ext === 'jpg' || ext === 'jpeg')
|
|
137
139
|
return 'image/jpeg';
|
|
138
140
|
if (ext === undefined || ext === '') {
|
|
139
141
|
throw new Error(`Reference image path has no file extension: ${filePath}. Use .png, .webp, .jpg, or .jpeg.`);
|
|
140
142
|
}
|
|
141
|
-
throw new Error(`Unsupported reference image extension ".${ext}" in ${filePath}. Only png, webp, jpg, and
|
|
143
|
+
throw new Error(`Unsupported reference image extension ".${ext}" in ${filePath}. Only png, webp, jpg, jpeg, and gif are allowed.`);
|
|
142
144
|
}
|
|
143
145
|
async function downloadImageFromUrl(url) {
|
|
144
146
|
let res;
|
|
@@ -155,7 +157,8 @@ async function downloadImageFromUrl(url) {
|
|
|
155
157
|
const contentType = res.headers.get('content-type') ?? '';
|
|
156
158
|
const mimeType = contentType.split(';')[0].trim() || 'image/png';
|
|
157
159
|
const allowed = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'];
|
|
158
|
-
if (!allowed.some((t) => mimeType.startsWith(t.split('/')[0]) && mimeType.includes('/')) ||
|
|
160
|
+
if (!allowed.some((t) => mimeType.startsWith(t.split('/')[0]) && mimeType.includes('/')) ||
|
|
161
|
+
!mimeType.startsWith('image/')) {
|
|
159
162
|
throw new Error(`参考图 URL 返回的 Content-Type 不是图片(${mimeType}):${url}\n 只支持 PNG / JPG / WEBP / GIF。`);
|
|
160
163
|
}
|
|
161
164
|
const buf = await res.arrayBuffer();
|
|
@@ -178,18 +181,13 @@ export async function collectReferenceImagePayloads(paths) {
|
|
|
178
181
|
}
|
|
179
182
|
else {
|
|
180
183
|
const mimeType = mimeTypeForImagePath(p);
|
|
181
|
-
out.push({ base64: readFileSync(p).toString('base64'), mimeType });
|
|
184
|
+
out.push({ base64: readFileSync(resolve(process.cwd(), p)).toString('base64'), mimeType });
|
|
182
185
|
}
|
|
183
186
|
}
|
|
184
187
|
catch (e) {
|
|
185
188
|
const detail = e instanceof Error ? e.message : String(e);
|
|
186
|
-
|
|
187
|
-
const hint = code ? ` (${code})` : '';
|
|
188
|
-
console.warn(`Skipping reference image (load failed): ${p}${hint}\n ${detail}`);
|
|
189
|
+
throw new Error(`Could not load reference image "${p}": ${detail}. No image request was sent.`);
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
|
-
if (paths.length > 0 && out.length === 0) {
|
|
192
|
-
console.warn('All reference images failed to load; continuing with text-only generation.');
|
|
193
|
-
}
|
|
194
192
|
return out.length > 0 ? out : undefined;
|
|
195
193
|
}
|
|
@@ -4,6 +4,7 @@ const messages = {
|
|
|
4
4
|
list: 'List verified local Remix references and their paths', search: 'Search accessible recommended or community Remixes',
|
|
5
5
|
import: 'Import a fixed Remix source snapshot into the current engineering directory',
|
|
6
6
|
prepareStarterTemplate: 'Prepare a verified starter template in a temporary directory',
|
|
7
|
+
template: 'Starter template: cocos (default) or threejs',
|
|
7
8
|
query: 'Search names, branches, tags, creators or templates', source: 'recommended or community', project: 'Source project identifier', branch: 'Exact source branch',
|
|
8
9
|
tag: 'Match a tag value; repeat to match any of the values', tagCategory: 'Tag category; defaults to Gameplay when --tag is used',
|
|
9
10
|
workspace: 'Current cloud workspace identifier (must match this conversation)', directory: 'Current engineering directory (must match this conversation)',
|
|
@@ -24,6 +25,7 @@ const messages = {
|
|
|
24
25
|
description: '为当前云工作区发现并导入 Remix 源码参考',
|
|
25
26
|
list: '列出已验证的本地 Remix 参考及路径', search: '搜索可访问的推荐或社区 Remix', import: '把固定版本的 Remix 源码参考导入当前工程',
|
|
26
27
|
prepareStarterTemplate: '将经过校验的起始模板准备到临时目录',
|
|
28
|
+
template: '起始模板:cocos(默认)或 threejs',
|
|
27
29
|
query: '搜索名称、分支、标签、创建者或模板', source: 'recommended(推荐)或 community(社区)', project: '来源项目标识', branch: '准确的来源分支名',
|
|
28
30
|
tag: '匹配的标签值;可重复指定,匹配任意一个值', tagCategory: '标签分类;使用 --tag 时默认为 Gameplay',
|
|
29
31
|
workspace: '当前云工作区标识(必须与本对话一致)', directory: '当前工程目录(必须与本对话一致)',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { WORKSPACE_STARTER_TEMPLATE_IDS } from '@playcraft/common/cloud-workspace/runtime';
|
|
3
4
|
const scope = {
|
|
4
5
|
cwd: z.string().min(1).max(4096),
|
|
5
6
|
workspaceId: z.string().uuid().optional(),
|
|
@@ -11,7 +12,9 @@ const scope = {
|
|
|
11
12
|
export const remixReferenceToolRequestSchema = z
|
|
12
13
|
.discriminatedUnion('action', [
|
|
13
14
|
z.object({ ...scope, action: z.literal('list') }).strict(),
|
|
14
|
-
z
|
|
15
|
+
z
|
|
16
|
+
.object({ ...scope, action: z.literal('prepare-starter-template'), templateId: z.enum(WORKSPACE_STARTER_TEMPLATE_IDS).optional() })
|
|
17
|
+
.strict(),
|
|
15
18
|
z
|
|
16
19
|
.object({
|
|
17
20
|
...scope,
|
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import JSON5 from 'json5';
|
|
4
4
|
import sharp from 'sharp';
|
|
5
|
-
import { extractAssetFields, getByPath, resolveAiImageTargetConstraints, resolveImageBackgroundRequirement, THEME_SCHEMA_PATH, themeDataPath, } from '@playcraft/common/prefab';
|
|
5
|
+
import { extractAssetFields, getByPath, parseThemeIndexImportPath, resolveAiImageTargetConstraints, resolveImageBackgroundRequirement, THEME_INDEX_PATH, THEME_SCHEMA_PATH, themeDataPath, } from '@playcraft/common/prefab';
|
|
6
6
|
function isWithinDirectory(root, file) {
|
|
7
7
|
const relative = path.relative(root, file);
|
|
8
8
|
return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
@@ -38,20 +38,29 @@ export function assertImageAssetOutputPaths(context, outputPaths, projectDir = p
|
|
|
38
38
|
const root = path.resolve(projectDir);
|
|
39
39
|
const target = canonicalProjectPath(root, context.targetAssetPath);
|
|
40
40
|
const targetStat = statSync(target);
|
|
41
|
-
const outputRoots = [
|
|
41
|
+
const outputRoots = [
|
|
42
|
+
root,
|
|
43
|
+
...allowedOutputRoots.map((outputRoot) => path.resolve(root, outputRoot)),
|
|
44
|
+
];
|
|
42
45
|
for (const output of outputPaths) {
|
|
43
46
|
const mapped = output.startsWith('/project/') ? output.slice('/project/'.length) : output;
|
|
44
47
|
const { dir, name } = path.parse(mapped);
|
|
45
48
|
// The response encoding can change the requested extension.
|
|
46
|
-
const candidates = [
|
|
49
|
+
const candidates = [
|
|
50
|
+
mapped,
|
|
51
|
+
...['.png', '.webp', '.jpg', '.gif', '.bmp'].map((ext) => path.join(dir, `${name}${ext}`)),
|
|
52
|
+
];
|
|
47
53
|
for (const candidate of candidates) {
|
|
48
54
|
const candidatePath = path.resolve(root, candidate);
|
|
49
|
-
const outputRoot = outputRoots.find(allowed => isWithinDirectory(allowed, candidatePath));
|
|
55
|
+
const outputRoot = outputRoots.find((allowed) => isWithinDirectory(allowed, candidatePath));
|
|
50
56
|
if (!outputRoot)
|
|
51
57
|
throw new Error(`Image output path is outside the project and allowed output roots: ${output}`);
|
|
52
58
|
const file = canonicalProjectPath(outputRoot, candidatePath);
|
|
53
59
|
const fileStat = existsSync(file) ? statSync(file) : undefined;
|
|
54
|
-
if (!path.relative(target, file) ||
|
|
60
|
+
if (!path.relative(target, file) ||
|
|
61
|
+
(targetStat.ino !== 0 &&
|
|
62
|
+
fileStat?.ino === targetStat.ino &&
|
|
63
|
+
fileStat.dev === targetStat.dev)) {
|
|
55
64
|
throw new Error(`Cannot overwrite shared image asset ${context.targetAssetPath}: other fields have different requirements. Use a separate outputPath, then explicitly update the selected field reference; no shared asset was changed.`);
|
|
56
65
|
}
|
|
57
66
|
}
|
|
@@ -71,8 +80,10 @@ function projectFile(root, input) {
|
|
|
71
80
|
/** Read the same per-field constraints used by the asset image dialog. */
|
|
72
81
|
export async function readImageAssetContext(assetPath, projectDir = process.cwd(), options = {}) {
|
|
73
82
|
const targetFieldPath = options.targetFieldPath;
|
|
74
|
-
if (targetFieldPath !== undefined &&
|
|
75
|
-
|
|
83
|
+
if (targetFieldPath !== undefined &&
|
|
84
|
+
(!Array.isArray(targetFieldPath) ||
|
|
85
|
+
!targetFieldPath.length ||
|
|
86
|
+
targetFieldPath.some((key) => typeof key !== 'string' || !key.length))) {
|
|
76
87
|
throw new Error('targetFieldPath must be a non-empty array of non-empty field names.');
|
|
77
88
|
}
|
|
78
89
|
const root = path.resolve(projectDir);
|
|
@@ -81,24 +92,36 @@ export async function readImageAssetContext(assetPath, projectDir = process.cwd(
|
|
|
81
92
|
const targetStat = statSync(target);
|
|
82
93
|
const relativeTarget = path.relative(root, target).replace(/\\/g, '/');
|
|
83
94
|
const themeMatch = relativeTarget.match(/^src\/theme\/([^/]+)\/assets\//);
|
|
95
|
+
const indexFile = path.join(root, THEME_INDEX_PATH);
|
|
96
|
+
const themeId = themeMatch?.[1] ??
|
|
97
|
+
(existsSync(indexFile)
|
|
98
|
+
? parseThemeIndexImportPath(readFileSync(projectFile(root, THEME_INDEX_PATH), 'utf8'))
|
|
99
|
+
: null);
|
|
84
100
|
let fields = [];
|
|
85
101
|
const schemaFile = path.join(root, THEME_SCHEMA_PATH);
|
|
86
|
-
if (
|
|
102
|
+
if (themeId && existsSync(schemaFile)) {
|
|
87
103
|
const schema = JSON5.parse(readFileSync(projectFile(root, THEME_SCHEMA_PATH), 'utf8'));
|
|
88
|
-
const dataPath = themeDataPath(
|
|
104
|
+
const dataPath = themeDataPath(themeId);
|
|
89
105
|
const data = existsSync(path.join(root, dataPath))
|
|
90
|
-
? JSON5.parse(readFileSync(projectFile(root, dataPath), 'utf8'))
|
|
106
|
+
? JSON5.parse(readFileSync(projectFile(root, dataPath), 'utf8'))
|
|
107
|
+
: {};
|
|
91
108
|
const themeRoot = path.dirname(path.join(root, dataPath));
|
|
92
|
-
const matches = extractAssetFields(schema, data).filter(candidate => {
|
|
109
|
+
const matches = extractAssetFields(schema, data).filter((candidate) => {
|
|
93
110
|
let configured = getByPath(data, candidate.path);
|
|
94
111
|
if (configured === undefined && ['gameplay', 'gamePlay'].includes(candidate.path[0])) {
|
|
95
|
-
configured = getByPath(data, [
|
|
112
|
+
configured = getByPath(data, [
|
|
113
|
+
candidate.path[0] === 'gameplay' ? 'gamePlay' : 'gameplay',
|
|
114
|
+
...candidate.path.slice(1),
|
|
115
|
+
]);
|
|
96
116
|
}
|
|
97
117
|
const value = typeof configured === 'string' ? configured : candidate.schema.default;
|
|
98
118
|
if (typeof value !== 'string' || /^https?:\/\//i.test(value))
|
|
99
119
|
return false;
|
|
100
|
-
const configuredPath = value.startsWith('/project/')
|
|
101
|
-
|
|
120
|
+
const configuredPath = value.startsWith('/project/')
|
|
121
|
+
? path.resolve(root, value.slice('/project/'.length))
|
|
122
|
+
: /^\/?src\//.test(value)
|
|
123
|
+
? path.resolve(root, value.replace(/^\//, ''))
|
|
124
|
+
: path.resolve(themeRoot, value);
|
|
102
125
|
if (configuredPath === target)
|
|
103
126
|
return true;
|
|
104
127
|
try {
|
|
@@ -106,19 +129,22 @@ export async function readImageAssetContext(assetPath, projectDir = process.cwd(
|
|
|
106
129
|
if (!path.relative(realTarget, realpathSync(candidateFile)))
|
|
107
130
|
return true;
|
|
108
131
|
const candidateStat = statSync(candidateFile);
|
|
109
|
-
return targetStat.ino !== 0 &&
|
|
132
|
+
return (targetStat.ino !== 0 &&
|
|
133
|
+
candidateStat.ino === targetStat.ino &&
|
|
134
|
+
candidateStat.dev === targetStat.dev);
|
|
110
135
|
}
|
|
111
136
|
catch {
|
|
112
137
|
// Other fields may reference missing files or paths outside this project.
|
|
113
138
|
return false;
|
|
114
139
|
}
|
|
115
140
|
});
|
|
116
|
-
if (!matches.length)
|
|
141
|
+
if (!matches.length && (themeMatch || targetFieldPath))
|
|
117
142
|
throw new Error(`No image schema field matches ${relativeTarget}; check theme.data.json5 before generating.`);
|
|
118
143
|
fields = matches;
|
|
119
|
-
if (targetFieldPath &&
|
|
120
|
-
|
|
121
|
-
|
|
144
|
+
if (targetFieldPath &&
|
|
145
|
+
!fields.some((candidate) => candidate.path.length === targetFieldPath.length &&
|
|
146
|
+
candidate.path.every((key, index) => key === targetFieldPath[index]))) {
|
|
147
|
+
throw new Error(`targetFieldPath ${JSON.stringify(targetFieldPath)} does not reference ${relativeTarget}. Matching fields: ${matches.map((candidate) => JSON.stringify(candidate.path)).join(', ')}.`);
|
|
122
148
|
}
|
|
123
149
|
}
|
|
124
150
|
else if (targetFieldPath) {
|
|
@@ -131,22 +157,35 @@ export async function readImageAssetContext(assetPath, projectDir = process.cwd(
|
|
|
131
157
|
const ext = xp?.ext;
|
|
132
158
|
// Schema references follow the dialog's root/theme conventions; explicit tool paths do not change.
|
|
133
159
|
const schemaReferences = Array.isArray(ext?.reference_image)
|
|
134
|
-
? ext.reference_image
|
|
160
|
+
? ext.reference_image
|
|
161
|
+
.filter((ref) => typeof ref === 'string' && ref.trim())
|
|
162
|
+
.map((ref) => {
|
|
135
163
|
const raw = ref.trim().replace(/\\/g, '/');
|
|
136
|
-
const normalized = raw
|
|
164
|
+
const normalized = raw
|
|
165
|
+
.replace(/^\.\/+/, '')
|
|
166
|
+
.replace(/^\/+/, '')
|
|
167
|
+
.replace(/\/{2,}/g, '/');
|
|
137
168
|
if (/^\/?(?:assets|src|public)\//.test(raw) || normalized.startsWith('src/'))
|
|
138
169
|
return normalized;
|
|
139
|
-
if (!
|
|
170
|
+
if (!themeId || path.isAbsolute(raw) || /^https?:\/\//i.test(raw))
|
|
140
171
|
return raw;
|
|
141
|
-
return path.posix.join('src/theme',
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
172
|
+
return path.posix.join('src/theme', themeId, normalized);
|
|
173
|
+
})
|
|
174
|
+
: [];
|
|
175
|
+
const references = options.referenceImagePaths?.length
|
|
176
|
+
? options.referenceImagePaths
|
|
177
|
+
: options.useTargetReference === false
|
|
178
|
+
? []
|
|
179
|
+
: schemaReferences.length
|
|
180
|
+
? schemaReferences
|
|
181
|
+
: [relativeTarget];
|
|
182
|
+
const referenceImagePaths = references.map((ref) => /^https?:\/\//i.test(ref)
|
|
183
|
+
? ref
|
|
146
184
|
: path.relative(realpathSync(root), canonicalProjectPath(root, ref)).replace(/\\/g, '/'));
|
|
147
185
|
const hasSchemaBackground = typeof ext?.remove_background === 'boolean';
|
|
148
186
|
if (!hasSchemaBackground && targetHasTransparency === undefined) {
|
|
149
|
-
targetHasTransparency =
|
|
187
|
+
targetHasTransparency =
|
|
188
|
+
!!targetMeta.hasAlpha && (await sharp(target).stats()).channels.at(-1).min < 250;
|
|
150
189
|
}
|
|
151
190
|
const removeBackground = resolveImageBackgroundRequirement({
|
|
152
191
|
prompt: options.prompt ?? '',
|
|
@@ -171,12 +210,16 @@ export async function readImageAssetContext(assetPath, projectDir = process.cwd(
|
|
|
171
210
|
targetAssetPath: relativeTarget,
|
|
172
211
|
fieldPath: field?.path,
|
|
173
212
|
referenceImagePaths,
|
|
174
|
-
width: delivery.width,
|
|
213
|
+
width: delivery.width,
|
|
214
|
+
height: delivery.height,
|
|
175
215
|
aspectRatio: typeof ext?.ratio === 'string' ? ext.ratio : `${delivery.width}:${delivery.height}`,
|
|
176
216
|
outputFormat: (removeBackground && delivery.format === 'jpeg' && !delivery.fromSchemaMimeType
|
|
177
|
-
? 'png'
|
|
217
|
+
? 'png'
|
|
218
|
+
: delivery.format),
|
|
178
219
|
outputFormatFromSchema: delivery.fromSchemaMimeType,
|
|
179
|
-
fitMode: delivery.fitMode,
|
|
220
|
+
fitMode: delivery.fitMode,
|
|
221
|
+
removeBackground: delivery.removeBackground,
|
|
222
|
+
paddingColor: delivery.paddingColor,
|
|
180
223
|
removeBackgroundFromSchema: hasSchemaBackground,
|
|
181
224
|
imageModelRef: typeof ext?.imageModelRef === 'string' ? ext.imageModelRef : undefined,
|
|
182
225
|
};
|
|
@@ -188,33 +231,40 @@ export async function readImageAssetContext(assetPath, projectDir = process.cwd(
|
|
|
188
231
|
contexts.push(await contextForField(field));
|
|
189
232
|
}
|
|
190
233
|
catch (error) {
|
|
191
|
-
const selected = field?.path.length === targetFieldPath?.length
|
|
192
|
-
|
|
234
|
+
const selected = field?.path.length === targetFieldPath?.length &&
|
|
235
|
+
field?.path.every((key, index) => key === targetFieldPath?.[index]);
|
|
193
236
|
if (!targetFieldPath || selected)
|
|
194
237
|
throw error;
|
|
195
238
|
// Invalid sibling requirements still protect the original shared file.
|
|
196
239
|
unresolvedSharedField = true;
|
|
197
240
|
}
|
|
198
241
|
}
|
|
199
|
-
const context = targetFieldPath
|
|
200
|
-
|
|
242
|
+
const context = targetFieldPath
|
|
243
|
+
? contexts.find((candidate) => candidate.fieldPath?.length === targetFieldPath.length &&
|
|
244
|
+
candidate.fieldPath.every((key, index) => key === targetFieldPath[index]))
|
|
245
|
+
: contexts[0];
|
|
201
246
|
const deliveryKey = ({ fieldPath: _fieldPath, outputFormatFromSchema: _formatSource, removeBackgroundFromSchema: _backgroundSource, aspectRatio, ...delivery }) => {
|
|
202
247
|
const ratio = aspectRatio.match(/^\s*(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)\s*$/);
|
|
203
|
-
return JSON.stringify({
|
|
248
|
+
return JSON.stringify({
|
|
249
|
+
...delivery,
|
|
250
|
+
aspectRatio: ratio && Number(ratio[2]) > 0 ? Number(ratio[1]) / Number(ratio[2]) : aspectRatio,
|
|
251
|
+
});
|
|
204
252
|
};
|
|
205
|
-
const conflicting = unresolvedSharedField ||
|
|
253
|
+
const conflicting = unresolvedSharedField ||
|
|
254
|
+
contexts.some((candidate) => deliveryKey(candidate) !== deliveryKey(context));
|
|
206
255
|
if (conflicting && !targetFieldPath) {
|
|
207
|
-
throw new Error(`Multiple image fields use ${relativeTarget} with different references or delivery constraints. Set targetFieldPath (or --target-field-path) to one of: ${fields.map(field => JSON.stringify(field.path)).join(', ')}.`);
|
|
256
|
+
throw new Error(`Multiple image fields use ${relativeTarget} with different references or delivery constraints. Set targetFieldPath (or --target-field-path) to one of: ${fields.map((field) => JSON.stringify(field.path)).join(', ')}.`);
|
|
208
257
|
}
|
|
209
258
|
if (targetFieldPath) {
|
|
210
259
|
// A separate output retains exactly the selected field's requirements.
|
|
211
|
-
if (conflicting ||
|
|
212
|
-
|
|
260
|
+
if (conflicting ||
|
|
261
|
+
contexts.some((candidate) => (candidate.outputFormatFromSchema && !context.outputFormatFromSchema) ||
|
|
262
|
+
(candidate.removeBackgroundFromSchema && !context.removeBackgroundFromSchema)))
|
|
213
263
|
context.requiresSeparateOutput = true;
|
|
214
264
|
}
|
|
215
265
|
else {
|
|
216
|
-
context.outputFormatFromSchema = contexts.some(candidate => candidate.outputFormatFromSchema);
|
|
217
|
-
context.removeBackgroundFromSchema = contexts.some(candidate => candidate.removeBackgroundFromSchema);
|
|
266
|
+
context.outputFormatFromSchema = contexts.some((candidate) => candidate.outputFormatFromSchema);
|
|
267
|
+
context.removeBackgroundFromSchema = contexts.some((candidate) => candidate.removeBackgroundFromSchema);
|
|
218
268
|
}
|
|
219
269
|
assertImageAssetOutputPaths(context, options.outputPaths ?? [], root, options.allowedOutputRoots);
|
|
220
270
|
if (context.referenceImagePaths.length > 8)
|
|
@@ -33,7 +33,7 @@ function withModelShell(config, modelConfigId) {
|
|
|
33
33
|
const ENGINEERING_BATCH_INSTRUCTIONS = [
|
|
34
34
|
'For a single replica, work directly in the current repository and preserve the user-provided references. Do not create a batch for a single replica.',
|
|
35
35
|
'If the repository is empty, initialize it using the default starter template before implementing the replica; preserve existing workspace metadata and files. If the template is unavailable, report the failure instead of substituting another template.',
|
|
36
|
-
'A repository is empty only when it has no user project files, including ignored files; workspace metadata, skills, references and attachments do not count as a project. For an empty repository, run playcraft references prepare-starter-template --json and merge the returned staged template into the current repository. Preserve existing files and workspace configuration, merge .gitignore without replacing it, preserve executable permissions, and report conflicts or preparation failures instead of overwriting files or creating a batch. Once finished, remove only the exact staging directory returned by the command.',
|
|
36
|
+
'A repository is empty only when it has no user project files, including ignored files; workspace metadata, skills, references and attachments do not count as a project. For an empty repository, run playcraft references prepare-starter-template --template <cocos|threejs> --json and merge the returned staged template into the current repository. Always declare --template explicitly: use threejs when the user asks for a Three.js project, and cocos when the user asks for Cocos or does not specify an engine. Preserve existing files and workspace configuration, merge .gitignore without replacing it, preserve executable permissions, and report conflicts or preparation failures instead of overwriting files or creating a batch. Once finished, remove only the exact staging directory returned by the command.',
|
|
37
37
|
'When the user requests multiple independent engineering variants, create a real platform batch using playcraft tools create-batch --prompt <the complete request, including constraints established in this conversation> --workspace-id <the trusted batch request workspaceId> --source-directory <the trusted batch request sourceDirectoryName> --client-request-id <the trusted batch request clientRequestId> --json.',
|
|
38
38
|
'Distinguish independent variants from multiple assets, levels or themes within one engineering project. Use the current session directory supplied by the runtime as the batch source; do not infer it from a preview or another conversation.',
|
|
39
39
|
'The platform freezes the current source and generates each planned variant in its own Remix directory. Submission authorizes automatic generation without another prompt approval. Keep the source unchanged for this batch; do not replace independent variants with theme switching in the source.',
|
|
@@ -183,7 +183,7 @@ export class WorkspaceRemixReferencesBroker {
|
|
|
183
183
|
sessionId: context.sessionId, wait: false,
|
|
184
184
|
}, async () => {
|
|
185
185
|
await this.current(context, valid, true);
|
|
186
|
-
const raw = await this.upstream(scope, { action: 'starter-template' }, WORKSPACE_REMIX_REFERENCE_BODY_MAX_BYTES);
|
|
186
|
+
const raw = await this.upstream(scope, { action: 'starter-template', templateId: input.templateId }, WORKSPACE_REMIX_REFERENCE_BODY_MAX_BYTES);
|
|
187
187
|
await this.current(context, valid, true);
|
|
188
188
|
return prepareStarterTemplate(context.cwd, raw, () => this.operation(context, valid, true));
|
|
189
189
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.84-beta",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"@gltf-transform/core": "^4.3.0",
|
|
58
58
|
"@gltf-transform/extensions": "^4.3.0",
|
|
59
59
|
"@gltf-transform/functions": "^4.3.0",
|
|
60
|
-
"@playcraft/build": "0.1.
|
|
61
|
-
"@playcraft/common": "0.1.
|
|
60
|
+
"@playcraft/build": "0.1.84-beta",
|
|
61
|
+
"@playcraft/common": "0.1.84-beta",
|
|
62
62
|
"better-sqlite3-pinned": "npm:better-sqlite3@13.0.3",
|
|
63
63
|
"chokidar": "^4.0.3",
|
|
64
64
|
"commander": "^13.1.0",
|