@playcraft/cli 0.0.40 → 0.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +66 -3
  2. package/dist/atom-plan/validate-atom-plan.js +298 -0
  3. package/dist/cli-root-help.js +1 -1
  4. package/dist/commands/3d.js +363 -0
  5. package/dist/commands/create.js +337 -0
  6. package/dist/commands/image.js +1337 -43
  7. package/dist/commands/recommend.js +1 -1
  8. package/dist/commands/remix.js +213 -0
  9. package/dist/commands/skills.js +1379 -0
  10. package/dist/commands/tools-3d.js +473 -0
  11. package/dist/commands/tools-generation.js +452 -0
  12. package/dist/commands/tools-project.js +400 -0
  13. package/dist/commands/tools-research.js +37 -0
  14. package/dist/commands/tools-research.test.js +216 -0
  15. package/dist/commands/tools-utils.js +183 -0
  16. package/dist/commands/tools.js +7 -616
  17. package/dist/config.js +2 -0
  18. package/dist/index.js +19 -1
  19. package/dist/utils/version-checker.js +8 -11
  20. package/package.json +9 -3
  21. package/project-template/.claude/agents/designer.md +120 -0
  22. package/project-template/.claude/agents/developer.md +124 -0
  23. package/project-template/.claude/agents/pm.md +164 -0
  24. package/project-template/.claude/agents/refs/README.md +73 -0
  25. package/project-template/.claude/agents/refs/designer-art-style-catalog.md +533 -0
  26. package/project-template/.claude/agents/refs/designer-color-audio-recipes.md +153 -0
  27. package/project-template/.claude/agents/refs/designer-deliverable-spec.md +191 -0
  28. package/project-template/.claude/agents/refs/designer-dimension-axis.md +27 -0
  29. package/project-template/.claude/agents/refs/designer-handoff-v2-checklist.md +68 -0
  30. package/project-template/.claude/agents/refs/designer-master-composite-recipes.md +208 -0
  31. package/project-template/.claude/agents/refs/designer-style-exploration-flow.md +37 -0
  32. package/project-template/.claude/agents/refs/developer-dev-handoff.md +109 -0
  33. package/project-template/.claude/agents/refs/developer-impl-cookbook.md +134 -0
  34. package/project-template/.claude/agents/refs/developer-phase1-flow.md +136 -0
  35. package/project-template/.claude/agents/refs/pm-workflow-detail.md +551 -0
  36. package/project-template/.claude/agents/refs/reviewer-convergence-eval.md +130 -0
  37. package/project-template/.claude/agents/refs/reviewer-six-dimension-eval.md +6 -0
  38. package/project-template/.claude/agents/refs/ta-3d-flip-recipe.md +85 -0
  39. package/project-template/.claude/agents/refs/ta-atlas-deliverable-standard.md +67 -0
  40. package/project-template/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
  41. package/project-template/.claude/agents/refs/ta-image-generation-detail.md +356 -0
  42. package/project-template/.claude/agents/refs/ta-image-ops-reference.md +495 -0
  43. package/project-template/.claude/agents/refs/ta-pipeline-cookbook.md +1108 -0
  44. package/project-template/.claude/agents/refs/ta-tools-reference.md +111 -0
  45. package/project-template/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
  46. package/project-template/.claude/agents/reviewer.md +127 -0
  47. package/project-template/.claude/agents/technical-artist.md +122 -0
  48. package/project-template/.claude/hooks/README.md +44 -0
  49. package/project-template/.claude/hooks/validate-atom-plan.mjs +224 -0
  50. package/project-template/.claude/hooks/validate-workflow-stop.mjs +343 -0
  51. package/project-template/.claude/settings.json +36 -0
  52. package/project-template/.claude/settings.local.json +4 -0
  53. package/project-template/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
  54. package/project-template/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
  55. package/project-template/.claude/skills/playcraft-asset-state-sheet/SKILL.md +141 -0
  56. package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +280 -0
  57. package/project-template/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
  58. package/project-template/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
  59. package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +279 -0
  60. package/project-template/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
  61. package/project-template/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
  62. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +235 -0
  63. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +97 -0
  64. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
  65. package/project-template/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +137 -0
  66. package/project-template/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
  67. package/project-template/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
  68. package/project-template/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
  69. package/project-template/.claude/skills/playcraft-masking/SKILL.md +373 -0
  70. package/project-template/.claude/skills/playcraft-research/SKILL.md +212 -0
  71. package/project-template/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
  72. package/project-template/.claude/skills/playcraft-storyboard/SKILL.md +167 -0
  73. package/project-template/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
  74. package/project-template/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
  75. package/project-template/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
  76. package/project-template/.claude/skills/playcraft-workflow/SKILL.md +485 -0
  77. package/project-template/.claude/skills/playwright-cli/SKILL.md +390 -0
  78. package/project-template/.claude/skills/playwright-cli/references/element-attributes.md +23 -0
  79. package/project-template/.claude/skills/playwright-cli/references/playwright-tests.md +39 -0
  80. package/project-template/.claude/skills/playwright-cli/references/request-mocking.md +87 -0
  81. package/project-template/.claude/skills/playwright-cli/references/running-code.md +240 -0
  82. package/project-template/.claude/skills/playwright-cli/references/session-management.md +226 -0
  83. package/project-template/.claude/skills/playwright-cli/references/spec-driven-testing.md +312 -0
  84. package/project-template/.claude/skills/playwright-cli/references/storage-state.md +275 -0
  85. package/project-template/.claude/skills/playwright-cli/references/test-generation.md +138 -0
  86. package/project-template/.claude/skills/playwright-cli/references/tracing.md +142 -0
  87. package/project-template/.claude/skills/playwright-cli/references/video-recording.md +157 -0
  88. package/project-template/.cursor/hooks.json +17 -0
  89. package/project-template/.cursor/rules/playcraft-orchestrator.mdc +137 -0
  90. package/project-template/.cursor/rules/playcraft-subagent-boundary.mdc +18 -0
  91. package/project-template/CLAUDE.md +280 -0
  92. package/project-template/assets/audio/bgm/.gitkeep +0 -0
  93. package/project-template/assets/audio/sfx/.gitkeep +0 -0
  94. package/project-template/assets/bundles/.gitkeep +0 -0
  95. package/project-template/assets/images/bg/.gitkeep +0 -0
  96. package/project-template/assets/images/reference/.gitkeep +0 -0
  97. package/project-template/assets/images/storyboard/.gitkeep +0 -0
  98. package/project-template/assets/images/tiles/.gitkeep +0 -0
  99. package/project-template/assets/images/ui/.gitkeep +0 -0
  100. package/project-template/assets/images/vfx/.gitkeep +0 -0
  101. package/project-template/assets/models/.gitkeep +0 -0
  102. package/project-template/docs/team/agent-conduct.md +121 -0
  103. package/project-template/docs/team/agent-runtime-matrix.md +62 -0
  104. package/project-template/docs/team/atom-plan-format.md +105 -0
  105. package/project-template/docs/team/collaboration.md +297 -0
  106. package/project-template/docs/team/core-model.md +50 -0
  107. package/project-template/docs/team/platform-capabilities.md +15 -0
  108. package/project-template/docs/team/workflow-changelog.md +65 -0
  109. package/project-template/docs/team/workflow-consistency-checklist.md +140 -0
  110. package/project-template/game/config/.gitkeep +0 -0
  111. package/project-template/game/gameplay/.gitkeep +0 -0
  112. package/project-template/game/scenes/.gitkeep +0 -0
  113. package/project-template/logs/.gitkeep +0 -0
  114. package/project-template/ta-workspace/logs/.gitkeep +0 -0
  115. package/project-template/ta-workspace/scripts/.gitkeep +0 -0
  116. package/project-template/ta-workspace/tmp/.gitkeep +0 -0
  117. package/project-template/templates/atom-plan.template.json +26 -0
  118. package/project-template/templates/atom-plan.template.md +108 -0
  119. package/project-template/templates/design-brief.template.md +195 -0
  120. package/project-template/templates/design-lens-checklist.reference.md +117 -0
  121. package/project-template/templates/design-methodology.md +99 -0
  122. package/project-template/templates/designer-log.template.md +114 -0
  123. package/project-template/templates/developer-log.template.md +134 -0
  124. package/project-template/templates/five-axis-framework.md +186 -0
  125. package/project-template/templates/intent-clarifications.template.md +58 -0
  126. package/project-template/templates/layout-spec.template.md +146 -0
  127. package/project-template/templates/project-state.template.md +237 -0
  128. package/project-template/templates/review-report.template.md +91 -0
  129. package/project-template/templates/style-exploration.template.md +93 -0
  130. package/project-template/templates/ta-log.template.md +343 -0
@@ -1,620 +1,11 @@
1
- import { writeFileSync, mkdirSync, readFileSync } from 'fs';
2
- import { dirname, join, parse } from 'path';
3
- import { tmpdir } from 'os';
4
- import { AgentApiClient } from '../utils/agent-api-client.js';
5
- const TMP_DIR = join(tmpdir(), 'playcraft');
6
- const MAX_REFERENCE_IMAGES = 8;
7
- function ensureTmpDir() {
8
- mkdirSync(TMP_DIR, { recursive: true });
9
- }
10
- function tmpPath(prefix) {
11
- return join(TMP_DIR, `${prefix}-${Date.now()}.json`);
12
- }
13
- function writeResult(prefix, data) {
14
- ensureTmpDir();
15
- const path = tmpPath(prefix);
16
- writeFileSync(path, JSON.stringify(data, null, 2));
17
- return path;
18
- }
19
- function handleError(err) {
20
- const msg = err instanceof Error ? err.message : String(err);
21
- console.error(`Error: ${msg}`);
22
- process.exit(1);
23
- }
24
- /** Fallback extension from API mime when magic-byte sniff fails. */
25
- function extensionForImageMime(mimeType) {
26
- const base = mimeType.toLowerCase().split(';')[0]?.trim() ?? '';
27
- switch (base) {
28
- case 'image/png':
29
- return '.png';
30
- case 'image/jpeg':
31
- case 'image/jpg':
32
- return '.jpg';
33
- case 'image/webp':
34
- return '.webp';
35
- case 'image/gif':
36
- return '.gif';
37
- case 'image/bmp':
38
- return '.bmp';
39
- default:
40
- return '.png';
41
- }
42
- }
43
- /**
44
- * Detect container from decoded bytes (magic). Prefer this over MIME alone.
45
- */
46
- function sniffImageExtension(buf) {
47
- // PNG (8-byte signature)
48
- if (buf.length >= 8 &&
49
- buf[0] === 0x89 &&
50
- buf[1] === 0x50 &&
51
- buf[2] === 0x4e &&
52
- buf[3] === 0x47) {
53
- return '.png';
54
- }
55
- // JPEG (SOI marker)
56
- if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) {
57
- return '.jpg';
58
- }
59
- // GIF87a / GIF89a
60
- if (buf.length >= 6 &&
61
- buf[0] === 0x47 &&
62
- buf[1] === 0x49 &&
63
- buf[2] === 0x46 &&
64
- buf[3] === 0x38 &&
65
- (buf[4] === 0x37 || buf[4] === 0x39) &&
66
- buf[5] === 0x61) {
67
- return '.gif';
68
- }
69
- // WebP: RIFF .... WEBP
70
- if (buf.length >= 12 &&
71
- buf[0] === 0x52 &&
72
- buf[1] === 0x49 &&
73
- buf[2] === 0x46 &&
74
- buf[3] === 0x46 &&
75
- buf[8] === 0x57 &&
76
- buf[9] === 0x45 &&
77
- buf[10] === 0x42 &&
78
- buf[11] === 0x50) {
79
- return '.webp';
80
- }
81
- // BMP
82
- if (buf.length >= 2 && buf[0] === 0x42 && buf[1] === 0x4d) {
83
- return '.bmp';
84
- }
85
- return null;
86
- }
87
- /** Replace output extension when it does not match the chosen format extension. */
88
- function resolveImageOutputPath(outputPath, wantExt) {
89
- try {
90
- const { dir, name, ext } = parse(outputPath);
91
- if (ext.toLowerCase() === wantExt) {
92
- return outputPath;
93
- }
94
- return join(dir, `${name}${wantExt}`);
95
- }
96
- catch (e) {
97
- const detail = e instanceof Error ? e.message : String(e);
98
- console.warn(`Could not adjust output path for extension ${wantExt} (${detail}); using path as given: ${outputPath}`);
99
- return outputPath;
100
- }
101
- }
102
- function collectReferenceImagePaths(value, previous) {
103
- return previous.concat([value]);
104
- }
105
- function mimeTypeForImagePath(filePath) {
106
- const ext = filePath.split('.').pop()?.toLowerCase();
107
- if (ext === 'png')
108
- return 'image/png';
109
- if (ext === 'webp')
110
- return 'image/webp';
111
- if (ext === 'jpg' || ext === 'jpeg')
112
- return 'image/jpeg';
113
- if (ext === undefined || ext === '') {
114
- throw new Error(`Reference image path has no file extension: ${filePath}. Use .png, .webp, .jpg, or .jpeg.`);
115
- }
116
- throw new Error(`Unsupported reference image extension ".${ext}" in ${filePath}. Only png, webp, jpg, and jpeg are allowed.`);
117
- }
118
- /** Load reference images for generate-image; skip unreadable paths after console.warn (same tolerance as output path resolve). */
119
- function collectReferenceImagePayloads(paths) {
120
- if (paths.length === 0)
121
- return undefined;
122
- const out = [];
123
- for (const p of paths) {
124
- try {
125
- const mimeType = mimeTypeForImagePath(p);
126
- out.push({
127
- base64: readFileSync(p).toString('base64'),
128
- mimeType,
129
- });
130
- }
131
- catch (e) {
132
- const detail = e instanceof Error ? e.message : String(e);
133
- const code = e && typeof e === 'object' && 'code' in e ? String(e.code) : '';
134
- const hint = code ? ` (${code})` : '';
135
- console.warn(`Skipping reference image (read failed): ${p}${hint}\n ${detail}`);
136
- }
137
- }
138
- if (paths.length > 0 && out.length === 0) {
139
- console.warn('All reference images failed to load; continuing with text-only generation.');
140
- }
141
- return out.length > 0 ? out : undefined;
142
- }
1
+ import { registerGenerationCommands } from './tools-generation.js';
2
+ import { registerProjectCommands } from './tools-project.js';
3
+ import { register3DCommands } from './tools-3d.js';
143
4
  export function registerToolsCommands(program) {
144
5
  const tools = program
145
6
  .command('tools')
146
- .description('后端 /api/agent/tools;需 PLAYCRAFT_API_URL + PLAYCRAFT_SANDBOX_TOKEN .playcraft.json');
147
- // ─── Image Models ────────────────────────────────────────────
148
- tools.command('list-image-models')
149
- .description('列出后端已配置的可用生图模型(modelKind=image),输出可直接作为 --image-model 的值')
150
- .option('--json', '以 JSON 格式输出(默认为人类可读表格)')
151
- .action(async (opts) => {
152
- try {
153
- const client = new AgentApiClient();
154
- const models = await client.get('/image-models');
155
- if (!models.length) {
156
- console.log('暂无已配置的生图模型。请前往 Admin > AI Settings 添加 modelKind=image 的配置。');
157
- return;
158
- }
159
- if (opts.json) {
160
- console.log(JSON.stringify(models, null, 2));
161
- return;
162
- }
163
- // 人类可读表格
164
- const defaultMark = (m) => (m.isDefault ? ' (default)' : '');
165
- const maxRefLen = Math.max(...models.map((m) => m.ref.length), 'MODEL REF'.length);
166
- console.log('');
167
- console.log(`${'MODEL REF'.padEnd(maxRefLen)} PROVIDER MODEL`);
168
- console.log(`${'-'.repeat(maxRefLen)} ---------------- --------------------------------`);
169
- for (const m of models) {
170
- console.log(`${m.ref.padEnd(maxRefLen)} ${m.provider.padEnd(16)} ${m.model}${defaultMark(m)}`);
171
- }
172
- console.log('');
173
- console.log(`使用方式:playcraft tools generate-image --prompt "..." --output out.png --image-model <MODEL REF>`);
174
- console.log('');
175
- }
176
- catch (err) {
177
- console.error('获取生图模型列表失败:', err instanceof Error ? err.message : String(err));
178
- process.exit(1);
179
- }
180
- });
181
- // ─── Generation ─────────────────────────────────────────────
182
- tools.command('generate-image')
183
- .description('AI 生成图片(支持多张参考图图生图)')
184
- .requiredOption('--prompt <text>', '图片描述')
185
- .option('--aspect-ratio <ratio>', '宽高比 (1:1|16:9|9:16|3:4|4:3)', '1:1')
186
- .requiredOption('--output <path>', '保存路径')
187
- .option('--image-size <size>', '图片尺寸 (1K|2K|4K)')
188
- .option('--reference-image <path>', '参考图路径(可重复多次,最多 8 张),支持 PNG/JPG/WEBP', collectReferenceImagePaths, [])
189
- .option('--image-model <ref>', '生图模型,格式 provider/model-id(如 iegg-litellm/gpt-image-1 或 google/gemini-2.0-flash-preview-image-generation)')
190
- .action(async (opts) => {
191
- try {
192
- const paths = opts.referenceImage ?? [];
193
- if (paths.length > MAX_REFERENCE_IMAGES) {
194
- throw new Error(`Too many reference images: ${paths.length} paths given; maximum is ${MAX_REFERENCE_IMAGES}.`);
195
- }
196
- const referenceImages = collectReferenceImagePayloads(paths);
197
- const client = new AgentApiClient();
198
- const result = await client.post('/generate-image', {
199
- prompt: opts.prompt,
200
- aspectRatio: opts.aspectRatio,
201
- imageSize: opts.imageSize,
202
- referenceImages,
203
- ...(opts.imageModel ? { imageModelRef: opts.imageModel } : {}),
204
- });
205
- const buf = Buffer.from(result.imageBase64, 'base64');
206
- const sniffed = sniffImageExtension(buf);
207
- const fromMime = extensionForImageMime(result.mimeType);
208
- const wantExt = sniffed ?? fromMime;
209
- if (!sniffed) {
210
- console.warn('Could not detect image format from file signature; using Content-Type from API for extension.');
211
- }
212
- else if (sniffed !== fromMime) {
213
- console.warn(`Image bytes look like ${sniffed.slice(1).toUpperCase()} but API reported ${result.mimeType}; ` +
214
- 'extension follows file signature.');
215
- }
216
- const outputPath = resolveImageOutputPath(opts.output, wantExt);
217
- if (outputPath !== opts.output) {
218
- console.log(`Output path adjusted to ${wantExt} payload: ${outputPath}`);
219
- }
220
- mkdirSync(dirname(outputPath), { recursive: true });
221
- writeFileSync(outputPath, buf);
222
- const sizeKB = Math.round(buf.length / 1024);
223
- console.log(`Image saved to ${outputPath} (${sizeKB}KB, ${wantExt.slice(1)})`);
224
- }
225
- catch (e) {
226
- handleError(e);
227
- }
228
- });
229
- tools.command('generate-sfx')
230
- .description('AI 生成音效(SFX),使用 ElevenLabs Sound Effects')
231
- .requiredOption('--prompt <text>', '音效描述,仅英文,如 "crisp UI click, short tail, no voice"')
232
- .option('--duration <seconds>', '时长(秒,0.5-30)', parseFloat)
233
- .option('--loop', '生成可循环的音效', false)
234
- .requiredOption('--output <path>', '保存路径,如 ./assets/audio/click.mp3')
235
- .action(async (opts) => {
236
- try {
237
- const client = new AgentApiClient();
238
- const result = await client.post('/generate-sfx', {
239
- prompt: opts.prompt,
240
- duration: opts.duration,
241
- loop: opts.loop,
242
- });
243
- mkdirSync(dirname(opts.output), { recursive: true });
244
- const buf = Buffer.from(result.audioBase64, 'base64');
245
- writeFileSync(opts.output, buf);
246
- const sizeKB = Math.round(buf.length / 1024);
247
- console.log(`SFX saved to ${opts.output} (${sizeKB}KB, ${result.duration.toFixed(2)}s, provider=${result.provider})`);
248
- }
249
- catch (e) {
250
- handleError(e);
251
- }
252
- });
253
- tools.command('generate-bgm')
254
- .description('AI 生成 BGM(30s 循环),使用 Google Lyria 3')
255
- .requiredOption('--prompt <text>', 'BGM 描述,如 "轻快休闲游戏配乐"')
256
- .option('--style <style>', '音乐风格,如 casual / epic / sci-fi / retro')
257
- .option('--bpm <bpm>', '目标 BPM', parseInt)
258
- .requiredOption('--output <path>', '保存路径,如 ./assets/audio/bgm.mp3')
259
- .action(async (opts) => {
260
- try {
261
- const client = new AgentApiClient();
262
- const result = await client.post('/generate-bgm', {
263
- prompt: opts.prompt,
264
- style: opts.style,
265
- bpm: opts.bpm,
266
- });
267
- mkdirSync(dirname(opts.output), { recursive: true });
268
- const buf = Buffer.from(result.audioBase64, 'base64');
269
- writeFileSync(opts.output, buf);
270
- const sizeKB = Math.round(buf.length / 1024);
271
- console.log(`BGM saved to ${opts.output} (${sizeKB}KB, ${result.duration.toFixed(2)}s, provider=${result.provider})`);
272
- }
273
- catch (e) {
274
- handleError(e);
275
- }
276
- });
277
- // ─── Project Pipeline ───────────────────────────────────────
278
- tools.command('save-to-git')
279
- .description('同步沙箱内容到 Git 仓库')
280
- .requiredOption('--project-id <id>', '项目数字 ID', parseInt)
281
- .requiredOption('--message <text>', '提交信息')
282
- .option('--branch <name>', '分支名', 'main')
283
- .action(async (opts) => {
284
- try {
285
- const client = new AgentApiClient();
286
- const result = await client.post('/save-to-git', {
287
- projectId: opts.projectId,
288
- branch: opts.branch,
289
- commitMessage: opts.message,
290
- });
291
- if (result.success) {
292
- console.log(`Saved to git: commit ${result.commitHash ?? 'unknown'}`);
293
- }
294
- else if (result.conflict) {
295
- console.log(`Conflict detected in ${result.conflictFiles?.length ?? 0} file(s)`);
296
- if (result.conflictFiles?.length) {
297
- for (const f of result.conflictFiles)
298
- console.log(` - ${f}`);
299
- }
300
- process.exit(1);
301
- }
302
- else {
303
- console.error(`Error: ${result.error ?? 'Save failed'}`);
304
- process.exit(1);
305
- }
306
- }
307
- catch (e) {
308
- handleError(e);
309
- }
310
- });
311
- tools.command('build-project')
312
- .description('在沙箱中执行项目构建')
313
- .requiredOption('--project-id <id>', '项目数字 ID', parseInt)
314
- .option('--branch <name>', '分支名', 'main')
315
- .action(async (opts) => {
316
- try {
317
- const client = new AgentApiClient();
318
- const result = await client.post('/build-project', {
319
- projectId: opts.projectId,
320
- branch: opts.branch,
321
- });
322
- if (result.success === false) {
323
- if (result.output) {
324
- const logPath = writeResult('build', { output: result.output, error: result.error });
325
- console.error(`Build failed. Log: ${logPath}`);
326
- }
327
- else {
328
- console.error(`Error: ${result.error ?? 'Build failed'}`);
329
- }
330
- process.exit(1);
331
- }
332
- if (result.output) {
333
- const logPath = writeResult('build', result);
334
- console.log(`Build succeeded`);
335
- console.log(`Log: ${logPath}`);
336
- }
337
- else {
338
- console.log('Build succeeded');
339
- }
340
- }
341
- catch (e) {
342
- handleError(e);
343
- }
344
- });
345
- tools.command('publish')
346
- .description('将构建产物上传到 COS,获取预览 URL')
347
- .requiredOption('--project-id <id>', '项目数字 ID', parseInt)
348
- .option('--branch <name>', '分支名', 'main')
349
- .action(async (opts) => {
350
- try {
351
- const client = new AgentApiClient();
352
- const result = await client.post('/publish-to-cos', {
353
- projectId: opts.projectId,
354
- branch: opts.branch,
355
- });
356
- if (result.success === false) {
357
- console.error(`Error: ${result.error ?? 'Publish failed'}`);
358
- process.exit(1);
359
- }
360
- if (result.previewUrl) {
361
- console.log(`Published: ${result.previewUrl}`);
362
- }
363
- else {
364
- console.log('Published successfully');
365
- }
366
- }
367
- catch (e) {
368
- handleError(e);
369
- }
370
- });
371
- tools.command('create-remix')
372
- .description('基于项目创建 Remix 分支')
373
- .requiredOption('--project-id <id>', '项目数字 ID')
374
- .requiredOption('--name <name>', 'Remix 显示名称')
375
- .option('--source-branch <name>', '源分支', 'main')
376
- .option('--description <text>', '描述')
377
- .action(async (opts) => {
378
- try {
379
- const client = new AgentApiClient();
380
- const projectId = Number(opts.projectId);
381
- if (!Number.isInteger(projectId) || projectId <= 0) {
382
- console.error(`Error: --project-id must be a positive integer, got: ${opts.projectId}`);
383
- process.exit(1);
384
- }
385
- const result = await client.post('/create-remix', {
386
- projectId,
387
- sourceBranch: opts.sourceBranch,
388
- name: opts.name,
389
- description: opts.description,
390
- });
391
- if (result.success === false) {
392
- console.error(`Error: ${result.error ?? 'Create remix failed'}`);
393
- process.exit(1);
394
- }
395
- console.log(`Remix created: ${opts.name} (project ${result.projectId}, branch ${result.branchName})`);
396
- if (result.editorUrl)
397
- console.log(`Editor: ${result.editorUrl}`);
398
- }
399
- catch (e) {
400
- handleError(e);
401
- }
402
- });
403
- // ─── Queries ────────────────────────────────────────────────
404
- tools.command('list-templates')
405
- .description('列出可用项目模板')
406
- .option('--tag <filter>', '按标签过滤')
407
- .action(async (opts) => {
408
- try {
409
- const client = new AgentApiClient();
410
- const params = {};
411
- if (opts.tag)
412
- params.tagFilter = opts.tag;
413
- const result = await client.get('/list-templates', params);
414
- const detailsPath = writeResult('list-templates', result);
415
- if (opts.tag) {
416
- console.log(`Found ${result.templateCount} templates matching "${opts.tag}" (${result.totalCount} total)`);
417
- }
418
- else {
419
- console.log(`Found ${result.templateCount} templates`);
420
- }
421
- console.log(`Details: ${detailsPath}`);
422
- }
423
- catch (e) {
424
- handleError(e);
425
- }
426
- });
427
- tools.command('list-assets')
428
- .description('列出项目资产')
429
- .requiredOption('--project-id <id>', '项目数字 ID')
430
- .option('--branch <name>', '分支名')
431
- .option('--types <list>', '资产类型,逗号分隔(texture,script,audio,model...)')
432
- .option('--search <keyword>', '名称搜索')
433
- .option('--limit <n>', '最大数量', '30')
434
- .option('--skip <n>', '偏移', '0')
435
- .action(async (opts) => {
436
- try {
437
- const client = new AgentApiClient();
438
- const params = { projectId: opts.projectId };
439
- if (opts.branch)
440
- params.branch = opts.branch;
441
- if (opts.types)
442
- params.types = opts.types;
443
- if (opts.search)
444
- params.search = opts.search;
445
- if (opts.limit)
446
- params.limit = opts.limit;
447
- if (opts.skip)
448
- params.skip = opts.skip;
449
- const result = await client.get('/list-project-assets', params);
450
- const detailsPath = writeResult('list-assets', result);
451
- const typeSummary = Object.entries(result.byTypeCount)
452
- .map(([k, v]) => `${k}: ${v}`)
453
- .join(', ');
454
- console.log(`Found ${result.total} assets (${typeSummary})`);
455
- console.log(`Details: ${detailsPath}`);
456
- }
457
- catch (e) {
458
- handleError(e);
459
- }
460
- });
461
- tools.command('list-remixes')
462
- .description('列出可访问的 Remix 项目')
463
- .option('--scope <scope>', 'my 或 gallery', 'my')
464
- .option('--search <keyword>', '搜索')
465
- .option('--limit <n>', '最大数量', '20')
466
- .option('--page <n>', '页码', '1')
467
- .action(async (opts) => {
468
- try {
469
- const client = new AgentApiClient();
470
- const params = {};
471
- if (opts.scope)
472
- params.scope = opts.scope;
473
- if (opts.search)
474
- params.search = opts.search;
475
- if (opts.limit)
476
- params.limit = opts.limit;
477
- if (opts.page)
478
- params.page = opts.page;
479
- const result = await client.get('/list-remixes', params);
480
- const detailsPath = writeResult('list-remixes', result);
481
- console.log(`Found ${result.total} remixes (page ${result.page}/${result.totalPages})`);
482
- console.log(`Details: ${detailsPath}`);
483
- }
484
- catch (e) {
485
- handleError(e);
486
- }
487
- });
488
- // ─── Publish ────────────────────────────────────────────────
489
- tools.command('publish-prefab')
490
- .description('发布 Remixable Prefab 到资产库')
491
- .requiredOption('--remix-project-id <id>', 'Remix 项目 ID', parseInt)
492
- .requiredOption('--template <name>', '模板名')
493
- .requiredOption('--variant <name>', '变体名')
494
- .requiredOption('--prefab <name>', '预制件名')
495
- .requiredOption('--staging-root <path>', 'Staging 目录路径')
496
- .option('--visibility <v>', 'public 或 team', 'team')
497
- .option('--team-id <id>', '团队 ID')
498
- .action(async (opts) => {
499
- try {
500
- const client = new AgentApiClient();
501
- const result = await client.post('/publish-prefab', {
502
- remixProjectId: opts.remixProjectId,
503
- templateName: opts.template,
504
- variantName: opts.variant,
505
- prefabName: opts.prefab,
506
- stagingRoot: opts.stagingRoot,
507
- visibility: opts.visibility,
508
- teamId: opts.teamId,
509
- });
510
- if (result.success === false) {
511
- console.error(`Error: ${result.error ?? 'Publish prefab failed'}`);
512
- process.exit(1);
513
- }
514
- console.log(`Prefab published: ${result.bundlePath} (${result.uploaded} files)`);
515
- }
516
- catch (e) {
517
- handleError(e);
518
- }
519
- });
520
- // ─── Builds ─────────────────────────────────────────────────
521
- tools.command('list-builds')
522
- .description('查看云端构建列表')
523
- .option('--project-id <id>', '项目数字 ID(不填则查看当前用户所有项目)')
524
- .option('--branch <name>', '按分支过滤')
525
- .option('--status <status>', '按状态过滤 (queued|running|success|failed)')
526
- .option('--platform <platform>', '按平台过滤 (facebook|snapchat|playcraft...)')
527
- .option('--search <keyword>', '按名称/提交信息搜索')
528
- .option('--limit <n>', '最大数量', '20')
529
- .option('--skip <n>', '偏移', '0')
530
- .action(async (opts) => {
531
- try {
532
- const client = new AgentApiClient();
533
- const params = {};
534
- if (opts.projectId)
535
- params.projectId = opts.projectId;
536
- if (opts.branch)
537
- params.branchName = opts.branch;
538
- if (opts.status)
539
- params.status = opts.status;
540
- if (opts.platform)
541
- params.platform = opts.platform;
542
- if (opts.search)
543
- params.search = opts.search;
544
- if (opts.limit)
545
- params.limit = opts.limit;
546
- if (opts.skip)
547
- params.skip = opts.skip;
548
- const result = await client.get('/list-builds', params);
549
- const detailsPath = writeResult('list-builds', result);
550
- const statusCounts = {};
551
- for (const item of result.items) {
552
- statusCounts[item.status] = (statusCounts[item.status] ?? 0) + 1;
553
- }
554
- const statusSummary = Object.entries(statusCounts).map(([s, n]) => `${s}: ${n}`).join(', ');
555
- console.log(`Found ${result.total} builds (${statusSummary || 'none'})`);
556
- console.log(`Details: ${detailsPath}`);
557
- }
558
- catch (e) {
559
- handleError(e);
560
- }
561
- });
562
- tools.command('get-build')
563
- .description('查看单次构建详情(状态、产物、校验结果等)')
564
- .requiredOption('--id <buildId>', '构建 ID')
565
- .action(async (opts) => {
566
- try {
567
- const client = new AgentApiClient();
568
- const result = await client.get('/get-build', { id: opts.id });
569
- if ('error' in result && result.error) {
570
- console.error(`Error: ${result.error}`);
571
- process.exit(1);
572
- }
573
- const detailsPath = writeResult('get-build', result);
574
- const statusLine = result.status === 'running' && result.progress != null
575
- ? `${result.status} (${result.progress}%)`
576
- : result.status;
577
- const sizeMB = result.outputSize ? `${(result.outputSize / 1024 / 1024).toFixed(2)}MB` : 'unknown size';
578
- const nameLabel = result.name ? ` "${result.name}"` : '';
579
- console.log(`Build ${result.id}${nameLabel}: ${statusLine} [${result.platform}/${result.format}] ${sizeMB}`);
580
- if (result.error)
581
- console.log(`Error: ${result.error}`);
582
- if (result.downloadUrl)
583
- console.log(`Download: ${result.downloadUrl}`);
584
- if (result.previewUrl)
585
- console.log(`Preview: ${result.previewUrl}`);
586
- if (result.validationPassed === false) {
587
- const errors = (result.validationWarnings ?? []).filter((w) => w.severity === 'error');
588
- console.log(`Validation: FAILED (${errors.length} error(s))`);
589
- }
590
- else if (result.validationPassed === true) {
591
- console.log(`Validation: PASSED`);
592
- }
593
- console.log(`Details: ${detailsPath}`);
594
- }
595
- catch (e) {
596
- handleError(e);
597
- }
598
- });
599
- tools.command('get-build-download')
600
- .description('获取构建产物下载地址')
601
- .requiredOption('--id <buildId>', '构建 ID')
602
- .action(async (opts) => {
603
- try {
604
- const client = new AgentApiClient();
605
- const result = await client.get('/get-build-download', { id: opts.id });
606
- if (result.error) {
607
- console.error(`Error: ${result.error}`);
608
- process.exit(1);
609
- }
610
- console.log(`Download URL: ${result.downloadUrl}`);
611
- if (result.previewUrl)
612
- console.log(`Preview URL: ${result.previewUrl}`);
613
- const sizeMB = result.size ? ` (${(result.size / 1024 / 1024).toFixed(2)}MB)` : '';
614
- console.log(`Platform: ${result.platform}/${result.format}${sizeMB}`);
615
- }
616
- catch (e) {
617
- handleError(e);
618
- }
619
- });
7
+ .description('后端 /api/agent/tools;运行 playcraft login 后即可直接使用,也支持 .playcraft.json 或 PLAYCRAFT_SANDBOX_TOKEN 环境变量');
8
+ registerGenerationCommands(tools);
9
+ registerProjectCommands(tools);
10
+ register3DCommands(tools);
620
11
  }
package/dist/config.js CHANGED
@@ -11,6 +11,7 @@ export const ConfigSchema = z.object({
11
11
  port: z.number().default(2468),
12
12
  url: z.string().optional(), // Cloud API URL
13
13
  mode: z.enum(['full-local', 'hybrid']).default('full-local'),
14
+ skillsDir: z.string().optional(), // 本地 skills 目录,供 playcraft skills 命令使用
14
15
  });
15
16
  const explorer = cosmiconfig('playcraft', {
16
17
  searchPlaces: ['playcraft.config.json', 'playcraft.agent.config.json'],
@@ -53,6 +54,7 @@ export async function loadConfig(cliOptions) {
53
54
  token: cliOptions.token || process.env.PLAYCRAFT_TOKEN || agentConfig.token || globalConfig.token,
54
55
  port: cliOptions.port || (process.env.PLAYCRAFT_PORT ? parseInt(process.env.PLAYCRAFT_PORT) : undefined) || agentConfig.port || globalConfig.port,
55
56
  mode: cliOptions.mode || process.env.PLAYCRAFT_MODE || agentConfig.mode || globalConfig.mode,
57
+ skillsDir: cliOptions.skillsDir || process.env.AGENT_SKILLS_DIR || agentConfig.skillsDir || globalConfig.skillsDir,
56
58
  });
57
59
  // Resolve absolute path for dir
58
60
  config.dir = path.resolve(process.cwd(), config.dir);