@playcraft/cli 0.0.55 → 0.0.57

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 (170) hide show
  1. package/README.md +18 -0
  2. package/REMIX.md +115 -19
  3. package/dist/atom-tree/fusion-hints.js +8 -3
  4. package/dist/atom-tree/fusion-intent.js +62 -0
  5. package/dist/atom-tree/selection.js +3 -1
  6. package/dist/commands/create.js +299 -85
  7. package/dist/commands/image.js +79 -69
  8. package/dist/commands/levels.js +29 -0
  9. package/dist/commands/patterns.js +75 -0
  10. package/dist/commands/remix.js +93 -0
  11. package/dist/commands/skills.js +48 -17
  12. package/dist/index.js +9 -5
  13. package/dist/remix/clone-source.js +208 -0
  14. package/dist/remix/clone.js +256 -0
  15. package/dist/remix/local-git.js +56 -0
  16. package/dist/remix/pull.js +64 -0
  17. package/dist/remix/push.js +70 -0
  18. package/dist/scaffold/templates.js +1 -1
  19. package/dist/utils/agent-api-client.js +10 -3
  20. package/dist/utils/remix-image-attachments.js +4 -32
  21. package/dist/utils/sandbox-path.js +20 -0
  22. package/package.json +3 -3
  23. package/project-template/.claude/agents/creative-director.md +114 -0
  24. package/project-template/.claude/agents/developer.md +5 -2
  25. package/project-template/.claude/agents/game-designer.md +41 -20
  26. package/project-template/.claude/agents/refs/README.md +8 -0
  27. package/project-template/.claude/agents/refs/creative-director-guide.md +35 -0
  28. package/project-template/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  29. package/project-template/.claude/agents/refs/creative-discovery-guide.md +76 -0
  30. package/project-template/.claude/agents/refs/creative-north-star-examples.md +82 -0
  31. package/project-template/.claude/agents/refs/developer-dev-handoff.md +1 -1
  32. package/project-template/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  33. package/project-template/.claude/agents/refs/game-designer-gameplay-sufficiency.md +4 -4
  34. package/project-template/.claude/agents/reviewer.md +11 -0
  35. package/project-template/.claude/hooks/README.md +12 -12
  36. package/project-template/.claude/hooks/block-developer-preflight.mjs +49 -13
  37. package/project-template/.claude/hooks/block-ring1-tasklist.mjs +1 -1
  38. package/project-template/.claude/hooks/user-prompt.mjs +3 -13
  39. package/project-template/.claude/hooks/validate-workflow-stop.mjs +209 -12
  40. package/project-template/.claude/settings.json +2 -2
  41. package/project-template/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  42. package/project-template/.claude/skills/session-analyzer/scripts/resolve-session.mjs +0 -1
  43. package/project-template/.claude/snapshot.mjs +0 -1
  44. package/project-template/.codebuddy/settings.json +1 -1
  45. package/project-template/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  46. package/project-template/CLAUDE.md +9 -9
  47. package/project-template/docs/project-state.md +1 -1
  48. package/project-template/docs/team/agent-conduct.md +18 -18
  49. package/project-template/docs/team/collaboration.md +19 -7
  50. package/project-template/docs/team/workflow-changelog.md +15 -0
  51. package/project-template/fusion-patterns/catalog.json +77 -0
  52. package/project-template/package.json +2 -0
  53. package/project-template/skills/_shared/scripts/create-playable.mjs +31 -11
  54. package/project-template/skills/_shared/scripts/dev-start.mjs +3 -3
  55. package/project-template/skills/_shared/scripts/disclosure-config.mjs +65 -39
  56. package/project-template/skills/_shared/scripts/harness-smoke.mjs +3 -1
  57. package/project-template/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  58. package/project-template/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  59. package/project-template/skills/_shared/scripts/slice-set.mjs +29 -2
  60. package/project-template/skills/_shared/scripts/state-get.mjs +124 -7
  61. package/project-template/skills/_shared/scripts/state-store.mjs +25 -2
  62. package/project-template/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  63. package/project-template/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  64. package/project-template/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  65. package/project-template/skills/playable-record/scripts/record-playable.mjs +2 -1
  66. package/project-template/skills/playable-record/scripts/verify-contract.mjs +4 -4
  67. package/project-template/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  68. package/project-template/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  69. package/project-template/skills/playable-record/scripts/verify-slice.mjs +2 -2
  70. package/project-template/templates/design-brief.template.md +34 -0
  71. package/project-template/templates/design-methodology.md +1 -2
  72. package/project-template/templates/design.template.md +65 -0
  73. package/project-template/templates/developer-log.template.md +12 -1
  74. package/project-template/templates/five-axis-framework.md +12 -0
  75. package/project-template/templates/game-designer-creative-process.template.md +99 -0
  76. package/project-template/templates/level-sketch.template.json +56 -0
  77. package/project-template/templates/north-star.template.md +43 -0
  78. package/project-template/templates/project-state.template.md +1 -1
  79. package/project-template/vite.config.ts +4 -5
  80. package/project-templates/phaser/.claude/agents/creative-director.md +114 -0
  81. package/project-templates/phaser/.claude/agents/game-designer.md +41 -20
  82. package/project-templates/phaser/.claude/agents/refs/creative-director-guide.md +35 -0
  83. package/project-templates/phaser/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  84. package/project-templates/phaser/.claude/agents/refs/creative-discovery-guide.md +76 -0
  85. package/project-templates/phaser/.claude/agents/refs/creative-north-star-examples.md +82 -0
  86. package/project-templates/phaser/.claude/agents/refs/developer-dev-handoff.md +1 -1
  87. package/project-templates/phaser/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  88. package/project-templates/phaser/.claude/agents/reviewer.md +11 -0
  89. package/project-templates/phaser/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  90. package/project-templates/phaser/.claude/hooks/user-prompt.mjs +17 -18
  91. package/project-templates/phaser/.claude/hooks/validate-workflow-stop.mjs +209 -12
  92. package/project-templates/phaser/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  93. package/project-templates/phaser/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  94. package/project-templates/phaser/CLAUDE.md +88 -54
  95. package/project-templates/phaser/docs/DESIGN.md +25 -0
  96. package/project-templates/phaser/docs/project-state.md +1 -1
  97. package/project-templates/phaser/docs/team/collaboration.md +19 -7
  98. package/project-templates/phaser/docs/team/workflow-changelog.md +14 -0
  99. package/project-templates/phaser/fusion-patterns/catalog.json +77 -0
  100. package/project-templates/phaser/package.json +3 -1
  101. package/project-templates/phaser/skills/_shared/scripts/dev-start.mjs +3 -3
  102. package/project-templates/phaser/skills/_shared/scripts/disclosure-config.mjs +65 -39
  103. package/project-templates/phaser/skills/_shared/scripts/harness-smoke.mjs +3 -1
  104. package/project-templates/phaser/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  105. package/project-templates/phaser/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  106. package/project-templates/phaser/skills/_shared/scripts/state-get.mjs +124 -7
  107. package/project-templates/phaser/skills/_shared/scripts/state-store.mjs +25 -2
  108. package/project-templates/phaser/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  109. package/project-templates/phaser/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  110. package/project-templates/phaser/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  111. package/project-templates/phaser/skills/playable-record/scripts/record-playable.mjs +2 -1
  112. package/project-templates/phaser/skills/playable-record/scripts/verify-contract.mjs +4 -4
  113. package/project-templates/phaser/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  114. package/project-templates/phaser/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  115. package/project-templates/phaser/skills/playable-record/scripts/verify-slice.mjs +2 -2
  116. package/project-templates/phaser/templates/design-brief.template.md +42 -11
  117. package/project-templates/phaser/templates/design-methodology.md +1 -2
  118. package/project-templates/phaser/templates/design.template.md +77 -0
  119. package/project-templates/phaser/templates/five-axis-framework.md +50 -1
  120. package/project-templates/phaser/templates/game-designer-creative-process.template.md +99 -0
  121. package/project-templates/phaser/templates/level-sketch.template.json +56 -0
  122. package/project-templates/phaser/templates/north-star.template.md +43 -0
  123. package/project-templates/phaser/templates/project-state.template.md +1 -1
  124. package/project-templates/phaser/vite.config.ts +4 -5
  125. package/project-templates/threejs/.claude/agents/creative-director.md +114 -0
  126. package/project-templates/threejs/.claude/agents/game-designer.md +41 -20
  127. package/project-templates/threejs/.claude/agents/refs/creative-director-guide.md +35 -0
  128. package/project-templates/threejs/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  129. package/project-templates/threejs/.claude/agents/refs/creative-discovery-guide.md +76 -0
  130. package/project-templates/threejs/.claude/agents/refs/creative-north-star-examples.md +82 -0
  131. package/project-templates/threejs/.claude/agents/refs/developer-dev-handoff.md +1 -1
  132. package/project-templates/threejs/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  133. package/project-templates/threejs/.claude/agents/reviewer.md +11 -0
  134. package/project-templates/threejs/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  135. package/project-templates/threejs/.claude/hooks/user-prompt.mjs +17 -18
  136. package/project-templates/threejs/.claude/hooks/validate-workflow-stop.mjs +209 -12
  137. package/project-templates/threejs/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  138. package/project-templates/threejs/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  139. package/project-templates/threejs/CLAUDE.md +88 -54
  140. package/project-templates/threejs/docs/DESIGN.md +25 -0
  141. package/project-templates/threejs/docs/project-state.md +1 -1
  142. package/project-templates/threejs/docs/team/collaboration.md +19 -7
  143. package/project-templates/threejs/docs/team/workflow-changelog.md +14 -0
  144. package/project-templates/threejs/fusion-patterns/catalog.json +77 -0
  145. package/project-templates/threejs/package.json +3 -1
  146. package/project-templates/threejs/skills/_shared/scripts/dev-start.mjs +3 -3
  147. package/project-templates/threejs/skills/_shared/scripts/disclosure-config.mjs +65 -39
  148. package/project-templates/threejs/skills/_shared/scripts/harness-smoke.mjs +3 -1
  149. package/project-templates/threejs/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  150. package/project-templates/threejs/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  151. package/project-templates/threejs/skills/_shared/scripts/state-get.mjs +124 -7
  152. package/project-templates/threejs/skills/_shared/scripts/state-store.mjs +25 -2
  153. package/project-templates/threejs/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  154. package/project-templates/threejs/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  155. package/project-templates/threejs/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  156. package/project-templates/threejs/skills/playable-record/scripts/record-playable.mjs +2 -1
  157. package/project-templates/threejs/skills/playable-record/scripts/verify-contract.mjs +4 -4
  158. package/project-templates/threejs/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  159. package/project-templates/threejs/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  160. package/project-templates/threejs/skills/playable-record/scripts/verify-slice.mjs +2 -2
  161. package/project-templates/threejs/templates/design-brief.template.md +42 -11
  162. package/project-templates/threejs/templates/design-methodology.md +1 -2
  163. package/project-templates/threejs/templates/design.template.md +77 -0
  164. package/project-templates/threejs/templates/five-axis-framework.md +50 -1
  165. package/project-templates/threejs/templates/game-designer-creative-process.template.md +99 -0
  166. package/project-templates/threejs/templates/level-sketch.template.json +56 -0
  167. package/project-templates/threejs/templates/north-star.template.md +43 -0
  168. package/project-templates/threejs/templates/project-state.template.md +1 -1
  169. package/project-templates/threejs/vite.config.ts +4 -5
  170. package/project-template/docs/DESIGN.md +0 -65
@@ -0,0 +1,256 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { loadGlobalConfig } from '../config.js';
5
+ import { PlayCraftHttpClient } from '../utils/playcraft-http-client.js';
6
+ import { isRemoteCloneSource, resolveRemixCloneTarget, } from './clone-source.js';
7
+ /** Build the branch-scoped Git Smart HTTP URL (no credentials in URL). */
8
+ export function buildRemixGitProxyUrl(apiBaseUrl, projectId, branch) {
9
+ const base = apiBaseUrl.replace(/\/+$/, '');
10
+ const encodedBranch = encodeURIComponent(branch);
11
+ return `${base}/api/git/projects/${encodeURIComponent(projectId)}/branches/${encodedBranch}.git`;
12
+ }
13
+ export function resolveCloneTargetDir(projectId, branch, dir) {
14
+ if (dir?.trim())
15
+ return path.resolve(dir.trim());
16
+ const safeBranch = branch.replace(/[/\\]+/g, '-').replace(/[^a-zA-Z0-9._-]+/g, '-');
17
+ return path.resolve(process.cwd(), `${projectId}-${safeBranch}`);
18
+ }
19
+ function localServiceUrl(baseUrl, fromPort, toPort) {
20
+ try {
21
+ const url = new URL(baseUrl);
22
+ const isLocal = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
23
+ if (isLocal && url.port === fromPort)
24
+ url.port = toPort;
25
+ return url.toString().replace(/\/+$/, '');
26
+ }
27
+ catch {
28
+ return baseUrl.replace(/\/+$/, '');
29
+ }
30
+ }
31
+ function createApiClient(options) {
32
+ const configured = new PlayCraftHttpClient(options);
33
+ const apiBaseUrl = localServiceUrl(configured.baseUrl, '3000', '3001');
34
+ if (apiBaseUrl === configured.baseUrl)
35
+ return configured;
36
+ return new PlayCraftHttpClient({
37
+ url: apiBaseUrl,
38
+ token: configured.token,
39
+ requestId: configured.requestId,
40
+ });
41
+ }
42
+ /**
43
+ * Prefer explicit --url; else env/login config (via PlayCraftHttpClient).
44
+ * Only use pasted portal origin when no API base is configured yet.
45
+ */
46
+ function resolveApiBaseHint(options, portalOrigin) {
47
+ if (options.url?.trim())
48
+ return options.url.trim();
49
+ const globalConfig = loadGlobalConfig();
50
+ const configured = process.env.PLAYCRAFT_API_URL ||
51
+ process.env.PLAYCRAFT_URL ||
52
+ (typeof globalConfig.backendUrl === 'string' && globalConfig.backendUrl) ||
53
+ (typeof globalConfig.url === 'string' && globalConfig.url);
54
+ if (configured)
55
+ return undefined;
56
+ if (portalOrigin?.trim()) {
57
+ return localServiceUrl(portalOrigin.trim(), '3000', '3001');
58
+ }
59
+ return undefined;
60
+ }
61
+ function writePlaycraftMeta(targetDir, meta) {
62
+ writeFileSync(path.join(targetDir, '.playcraft.json'), `${JSON.stringify(meta, null, 2)}\n`, 'utf8');
63
+ }
64
+ function configureRepoCredentialHelper(targetDir) {
65
+ // Prefer invoking the same CLI binary the user used.
66
+ const helper = `!playcraft git-credential`;
67
+ spawnSync('git', ['config', 'credential.helper', helper], {
68
+ cwd: targetDir,
69
+ encoding: 'utf8',
70
+ });
71
+ spawnSync('git', ['config', 'credential.useHttpPath', 'true'], {
72
+ cwd: targetDir,
73
+ encoding: 'utf8',
74
+ });
75
+ }
76
+ async function resolveProjectIdFromGitHost(client, repositoryUrl) {
77
+ try {
78
+ const data = await client.get(`/git/resolve-repo?repositoryUrl=${encodeURIComponent(repositoryUrl)}`);
79
+ const projectId = String(data?.projectId || data?.projectUuid || '').trim();
80
+ if (!projectId) {
81
+ throw new Error('empty_project_id');
82
+ }
83
+ return projectId;
84
+ }
85
+ catch {
86
+ throw new Error('无法根据工蜂/Git 地址定位 PlayCraft 项目(无权限或不存在)。请改用 Remix 详情页 URL,或 --project-id / --branch。');
87
+ }
88
+ }
89
+ export async function remixClone(options) {
90
+ const target = resolveRemixCloneTarget({
91
+ source: options.source,
92
+ projectId: options.projectId,
93
+ branch: options.branch,
94
+ });
95
+ let projectId = target.projectId?.trim() || '';
96
+ const branch = String(target.branch || '').trim();
97
+ if (!branch) {
98
+ throw new Error('缺少 Remix 分支名。');
99
+ }
100
+ if (branch === 'main' || branch === 'master') {
101
+ throw new Error('不允许 clone 主干分支;请指定 Remix 分支(例如 remix/...)。');
102
+ }
103
+ const client = createApiClient({
104
+ url: resolveApiBaseHint(options, target.origin),
105
+ token: options.token,
106
+ });
107
+ if (target.kind === 'git-host') {
108
+ if (!target.repositoryUrl) {
109
+ throw new Error('工蜂/Git 地址无效。');
110
+ }
111
+ projectId = await resolveProjectIdFromGitHost(client, target.repositoryUrl);
112
+ }
113
+ if (!projectId) {
114
+ throw new Error('缺少项目 ID。');
115
+ }
116
+ const cloneUrl = buildRemixGitProxyUrl(client.baseUrl, projectId, branch);
117
+ const targetDir = resolveCloneTargetDir(projectId, branch, options.dir);
118
+ if (existsSync(targetDir) && existsSync(path.join(targetDir, '.git'))) {
119
+ throw new Error(`目标目录已存在 Git 仓库:${targetDir}`);
120
+ }
121
+ mkdirSync(path.dirname(targetDir), { recursive: true });
122
+ if (!options.json) {
123
+ process.stderr.write(`[clone] ${cloneUrl}\n`);
124
+ process.stderr.write(`[clone] → ${targetDir}\n`);
125
+ }
126
+ const result = spawnSync('git', [
127
+ '-c',
128
+ `http.extraHeader=Authorization: Bearer ${client.token}`,
129
+ 'clone',
130
+ '--branch',
131
+ branch,
132
+ '--single-branch',
133
+ '--depth',
134
+ '1',
135
+ cloneUrl,
136
+ targetDir,
137
+ ], {
138
+ encoding: 'utf8',
139
+ stdio: options.json ? 'pipe' : 'inherit',
140
+ });
141
+ if (result.status !== 0) {
142
+ const detail = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
143
+ throw new Error(detail || `git clone 失败(exit ${result.status})`);
144
+ }
145
+ const meta = {
146
+ schemaVersion: '1.0',
147
+ projectId,
148
+ branch,
149
+ cloneUrl,
150
+ clonedAt: new Date().toISOString(),
151
+ };
152
+ writePlaycraftMeta(targetDir, meta);
153
+ configureRepoCredentialHelper(targetDir);
154
+ let projectType;
155
+ try {
156
+ const project = await client.get(`/projects/${encodeURIComponent(projectId)}`);
157
+ projectType = project?.projectType;
158
+ }
159
+ catch {
160
+ // optional — next-step hints only
161
+ }
162
+ if (options.install || options.dev) {
163
+ const install = spawnSync('npm', ['install'], {
164
+ cwd: targetDir,
165
+ encoding: 'utf8',
166
+ stdio: options.json ? 'pipe' : 'inherit',
167
+ shell: process.platform === 'win32',
168
+ });
169
+ if (install.status !== 0) {
170
+ throw new Error('npm install 失败');
171
+ }
172
+ }
173
+ if (options.dev) {
174
+ spawnSync('npm', ['run', 'dev'], {
175
+ cwd: targetDir,
176
+ encoding: 'utf8',
177
+ stdio: 'inherit',
178
+ shell: process.platform === 'win32',
179
+ });
180
+ }
181
+ if (options.json) {
182
+ console.log(JSON.stringify({
183
+ schemaVersion: '1.0',
184
+ ok: true,
185
+ command: 'remix.clone',
186
+ requestId: client.requestId,
187
+ data: { ...meta, targetDir, projectType, sourceKind: target.kind },
188
+ }));
189
+ return;
190
+ }
191
+ console.log(`✓ Remix 已克隆到 ${targetDir}`);
192
+ console.log(` 分支: ${branch}`);
193
+ console.log(` remote: ${cloneUrl}`);
194
+ console.log('');
195
+ console.log('下一步:');
196
+ console.log(` cd ${targetDir}`);
197
+ if (projectType === 'external') {
198
+ console.log(' npm install && npm run dev');
199
+ }
200
+ else {
201
+ console.log(' playcraft start --project <project-id> # PlayCanvas / 本地 Agent 预览');
202
+ }
203
+ console.log(' playcraft remix pull # 拉取远端(沙箱 Save 后)');
204
+ console.log(' playcraft remix push # 推回 Remix(Windows 勿直接 git push/pull)');
205
+ }
206
+ export { isRemoteCloneSource, resolveRemixCloneTarget };
207
+ /**
208
+ * Git credential helper (get/store/erase). Only `get` is implemented:
209
+ * returns PAT from ~/.playcraft/config.json for PlayCraft git proxy hosts.
210
+ */
211
+ export function gitCredentialCommand(action) {
212
+ const op = (action || process.argv[3] || 'get').trim();
213
+ if (op !== 'get') {
214
+ // store/erase: no-op (PAT lives in global config)
215
+ return;
216
+ }
217
+ const input = readFileSync(0, 'utf8');
218
+ const attrs = {};
219
+ for (const line of input.split(/\r?\n/)) {
220
+ const eq = line.indexOf('=');
221
+ if (eq <= 0)
222
+ continue;
223
+ attrs[line.slice(0, eq)] = line.slice(eq + 1);
224
+ }
225
+ const host = attrs.host || '';
226
+ const protocol = attrs.protocol || 'https';
227
+ const reqPath = attrs.path || '';
228
+ if (!reqPath.includes('/api/git/projects/')) {
229
+ return;
230
+ }
231
+ const globalConfig = loadGlobalConfig();
232
+ const token = process.env.PLAYCRAFT_TOKEN ||
233
+ (typeof globalConfig.token === 'string' ? globalConfig.token : undefined);
234
+ if (!token || !token.startsWith('playcraft_pat_')) {
235
+ return;
236
+ }
237
+ // Optional host check against saved backend URL
238
+ const configured = (typeof globalConfig.backendUrl === 'string' && globalConfig.backendUrl) ||
239
+ (typeof globalConfig.url === 'string' && globalConfig.url) ||
240
+ process.env.PLAYCRAFT_API_URL ||
241
+ process.env.PLAYCRAFT_URL ||
242
+ '';
243
+ if (configured && host) {
244
+ try {
245
+ const configuredHost = new URL(configured).host;
246
+ if (configuredHost && configuredHost !== host) {
247
+ // Still allow if path is clearly our git proxy (e.g. custom CDN host)
248
+ }
249
+ }
250
+ catch {
251
+ // ignore
252
+ }
253
+ }
254
+ process.stdout.write(`username=playcraft\npassword=${token}\n\n`);
255
+ void protocol;
256
+ }
@@ -0,0 +1,56 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ /** @internal exported for tests */
5
+ export function readRemixLocalMeta(repoDir) {
6
+ const metaPath = path.join(repoDir, '.playcraft.json');
7
+ if (!existsSync(metaPath)) {
8
+ throw new Error(`未找到 .playcraft.json(当前目录:${repoDir})。请先在该目录执行 playcraft remix clone,或 cd 到 clone 得到的目录。`);
9
+ }
10
+ try {
11
+ const raw = JSON.parse(readFileSync(metaPath, 'utf8'));
12
+ const projectId = String(raw.projectId || '').trim();
13
+ const branch = String(raw.branch || '').trim();
14
+ const cloneUrl = String(raw.cloneUrl || '').trim();
15
+ if (!projectId || !branch || !cloneUrl) {
16
+ throw new Error('invalid_meta');
17
+ }
18
+ return {
19
+ schemaVersion: '1.0',
20
+ projectId,
21
+ branch,
22
+ cloneUrl,
23
+ clonedAt: typeof raw.clonedAt === 'string' ? raw.clonedAt : new Date().toISOString(),
24
+ };
25
+ }
26
+ catch (error) {
27
+ if (error instanceof Error && error.message.includes('.playcraft.json'))
28
+ throw error;
29
+ throw new Error(`.playcraft.json 无效或损坏:${metaPath}`);
30
+ }
31
+ }
32
+ /** @internal exported for tests */
33
+ export function assertPlaycraftProxyRemote(cloneUrl) {
34
+ if (!cloneUrl.includes('/api/git/projects/')) {
35
+ throw new Error(`当前 .playcraft.json 的 cloneUrl 不是 PlayCraft Git 代理地址。\n` +
36
+ `请用 playcraft remix clone 重新克隆,不要改 remote 到工蜂 / 其它 Git 主机。\n` +
37
+ `cloneUrl: ${cloneUrl}`);
38
+ }
39
+ }
40
+ /**
41
+ * 运行带 PlayCraft PAT 的 git 子命令:注入 Bearer,并清空 credential.helper,
42
+ * 避免 Windows Git Credential Manager 弹窗。
43
+ */
44
+ export function runAuthenticatedGit(gitCommandAndArgs, options) {
45
+ return spawnSync('git', [
46
+ '-c',
47
+ 'credential.helper=',
48
+ '-c',
49
+ `http.extraHeader=Authorization: Bearer ${options.token}`,
50
+ ...gitCommandAndArgs,
51
+ ], {
52
+ cwd: options.cwd,
53
+ encoding: 'utf8',
54
+ stdio: options.pipe ? 'pipe' : 'inherit',
55
+ });
56
+ }
@@ -0,0 +1,64 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { PlayCraftHttpClient } from '../utils/playcraft-http-client.js';
5
+ import { assertPlaycraftProxyRemote, readRemixLocalMeta, runAuthenticatedGit, } from './local-git.js';
6
+ /**
7
+ * 从 PlayCraft Git 代理拉取当前 Remix。
8
+ * 与 remix push 相同:注入 Bearer PAT,并禁用 credential.helper(避免 Windows GCM)。
9
+ */
10
+ export async function remixPull(options = {}) {
11
+ const repoDir = path.resolve(options.dir?.trim() || process.cwd());
12
+ if (!existsSync(path.join(repoDir, '.git'))) {
13
+ throw new Error(`目录不是 Git 仓库:${repoDir}`);
14
+ }
15
+ const meta = readRemixLocalMeta(repoDir);
16
+ assertPlaycraftProxyRemote(meta.cloneUrl);
17
+ const client = new PlayCraftHttpClient({
18
+ url: options.url,
19
+ token: options.token,
20
+ });
21
+ const gitArgs = options.gitArgs?.length ? options.gitArgs : [];
22
+ const effectiveArgs = gitArgs.length > 0
23
+ ? gitArgs
24
+ : (() => {
25
+ const upstream = spawnSync('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], {
26
+ cwd: repoDir,
27
+ encoding: 'utf8',
28
+ });
29
+ if (upstream.status === 0 && upstream.stdout.trim()) {
30
+ return [];
31
+ }
32
+ return ['origin', meta.branch];
33
+ })();
34
+ if (!options.json) {
35
+ process.stderr.write(`[pull] ${meta.cloneUrl}\n`);
36
+ process.stderr.write(`[pull] branch=${meta.branch} dir=${repoDir}\n`);
37
+ }
38
+ const result = runAuthenticatedGit(['pull', ...effectiveArgs], {
39
+ cwd: repoDir,
40
+ token: client.token,
41
+ pipe: Boolean(options.json),
42
+ });
43
+ if (result.status !== 0) {
44
+ const detail = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
45
+ throw new Error(detail || `git pull 失败(exit ${result.status})`);
46
+ }
47
+ if (options.json) {
48
+ console.log(JSON.stringify({
49
+ schemaVersion: '1.0',
50
+ ok: true,
51
+ command: 'remix.pull',
52
+ requestId: client.requestId,
53
+ data: {
54
+ projectId: meta.projectId,
55
+ branch: meta.branch,
56
+ cloneUrl: meta.cloneUrl,
57
+ dir: repoDir,
58
+ gitArgs: effectiveArgs,
59
+ },
60
+ }));
61
+ return;
62
+ }
63
+ console.log(`✓ 已从 Remix 分支 ${meta.branch} 拉取`);
64
+ }
@@ -0,0 +1,70 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { PlayCraftHttpClient } from '../utils/playcraft-http-client.js';
5
+ import { assertPlaycraftProxyRemote, readRemixLocalMeta, runAuthenticatedGit, } from './local-git.js';
6
+ export { assertPlaycraftProxyRemote, readRemixLocalMeta } from './local-git.js';
7
+ /**
8
+ * Push 当前 Remix 仓库到 PlayCraft Git 代理。
9
+ *
10
+ * 故意不用 `git push` + credential.helper:Windows 上 Git Credential Manager
11
+ * 经常抢走鉴权并弹窗。这里与 clone 一样,用一次性 `http.extraHeader` 注入 PAT,
12
+ * 并清空本命令的 credential.helper,避免 GCM 介入。
13
+ */
14
+ export async function remixPush(options = {}) {
15
+ const repoDir = path.resolve(options.dir?.trim() || process.cwd());
16
+ if (!existsSync(path.join(repoDir, '.git'))) {
17
+ throw new Error(`目录不是 Git 仓库:${repoDir}`);
18
+ }
19
+ const meta = readRemixLocalMeta(repoDir);
20
+ assertPlaycraftProxyRemote(meta.cloneUrl);
21
+ const client = new PlayCraftHttpClient({
22
+ url: options.url,
23
+ token: options.token,
24
+ });
25
+ const gitArgs = options.gitArgs?.length ? options.gitArgs : [];
26
+ // 默认推当前分支;不强制改用户已设的 upstream。若无 upstream 且未传参数,
27
+ // 用 meta.branch 设 -u,避免「没有 upstream」报错。
28
+ const effectiveArgs = gitArgs.length > 0
29
+ ? gitArgs
30
+ : (() => {
31
+ const upstream = spawnSync('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], {
32
+ cwd: repoDir,
33
+ encoding: 'utf8',
34
+ });
35
+ if (upstream.status === 0 && upstream.stdout.trim()) {
36
+ return [];
37
+ }
38
+ return ['-u', 'origin', meta.branch];
39
+ })();
40
+ if (!options.json) {
41
+ process.stderr.write(`[push] ${meta.cloneUrl}\n`);
42
+ process.stderr.write(`[push] branch=${meta.branch} dir=${repoDir}\n`);
43
+ }
44
+ const result = runAuthenticatedGit(['push', ...effectiveArgs], {
45
+ cwd: repoDir,
46
+ token: client.token,
47
+ pipe: Boolean(options.json),
48
+ });
49
+ if (result.status !== 0) {
50
+ const detail = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
51
+ throw new Error(detail || `git push 失败(exit ${result.status})`);
52
+ }
53
+ if (options.json) {
54
+ console.log(JSON.stringify({
55
+ schemaVersion: '1.0',
56
+ ok: true,
57
+ command: 'remix.push',
58
+ requestId: client.requestId,
59
+ data: {
60
+ projectId: meta.projectId,
61
+ branch: meta.branch,
62
+ cloneUrl: meta.cloneUrl,
63
+ dir: repoDir,
64
+ gitArgs: effectiveArgs,
65
+ },
66
+ }));
67
+ return;
68
+ }
69
+ console.log(`✓ 已推送到 Remix 分支 ${meta.branch}`);
70
+ }
@@ -3,7 +3,7 @@ import { resolveCliPackageRoot } from './compose.js';
3
3
  export const CREATE_TEMPLATES = {
4
4
  agent: {
5
5
  id: 'agent',
6
- dir: 'project-template',
6
+ dir: 'project-template-v2',
7
7
  label: 'Agent Team 工作流(默认)',
8
8
  kind: 'agent',
9
9
  },
@@ -26,6 +26,7 @@ export class AgentApiClient {
26
26
  apiUrl;
27
27
  token;
28
28
  isPat;
29
+ playcraftEnv;
29
30
  constructor() {
30
31
  const localConfig = AgentApiClient.loadLocalConfig();
31
32
  const globalConfig = loadGlobalConfig();
@@ -47,13 +48,19 @@ export class AgentApiClient {
47
48
  this.apiUrl = apiUrl;
48
49
  this.token = token;
49
50
  this.isPat = isPatToken(token);
51
+ this.playcraftEnv = (process.env.PLAYCRAFT_ENV ||
52
+ localConfig.env ||
53
+ '').trim();
50
54
  }
51
55
  /** 根据 token 类型构造鉴权 headers */
52
56
  authHeaders() {
53
- if (this.isPat) {
54
- return { 'Authorization': `Bearer ${this.token}` };
57
+ const headers = this.isPat
58
+ ? { Authorization: `Bearer ${this.token}` }
59
+ : { 'X-Sandbox-Token': this.token };
60
+ if (this.playcraftEnv) {
61
+ headers['playcraft-env'] = this.playcraftEnv;
55
62
  }
56
- return { 'X-Sandbox-Token': this.token };
63
+ return headers;
57
64
  }
58
65
  /** Absolute pathname under /api/agent/tools (path may omit leading slash). */
59
66
  toolsRequestUrl(path) {
@@ -1,15 +1,9 @@
1
1
  import { readFileSync, statSync } from 'node:fs';
2
2
  import { basename, extname } from 'node:path';
3
3
  import { PlayCraftApiError } from './playcraft-http-client.js';
4
+ import { appendRemixReferenceImagesPrompt, referenceImageExtensions, referenceImageMimeFromExtension, } from '@playcraft/common/agent/reference-images';
4
5
  const MAX_IMAGES = 4;
5
6
  const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
6
- const IMAGE_MIME_BY_EXTENSION = {
7
- '.gif': 'image/gif',
8
- '.jpeg': 'image/jpeg',
9
- '.jpg': 'image/jpeg',
10
- '.png': 'image/png',
11
- '.webp': 'image/webp',
12
- };
13
7
  function attachmentError(code, message, details, retryable = false) {
14
8
  return new PlayCraftApiError({ code, message, details, retryable });
15
9
  }
@@ -22,9 +16,9 @@ export function readImageAttachments(paths) {
22
16
  }
23
17
  return candidates.map((sourcePath) => {
24
18
  const extension = extname(sourcePath).toLowerCase();
25
- const mimeType = IMAGE_MIME_BY_EXTENSION[extension];
19
+ const mimeType = referenceImageMimeFromExtension(extension);
26
20
  if (!mimeType) {
27
- throw attachmentError('ATTACHMENT_TYPE_UNSUPPORTED', `不支持的图片格式:${sourcePath}。仅支持 JPEG (Joint Photographic Experts Group, 联合图像专家组)、PNG (Portable Network Graphics, 便携式网络图形)、WebP 和 GIF (Graphics Interchange Format, 图形交换格式)。`, { path: sourcePath, supportedExtensions: Object.keys(IMAGE_MIME_BY_EXTENSION) });
21
+ throw attachmentError('ATTACHMENT_TYPE_UNSUPPORTED', `不支持的图片格式:${sourcePath}。仅支持 JPEG (Joint Photographic Experts Group, 联合图像专家组)、PNG (Portable Network Graphics, 便携式网络图形)、WebP 和 GIF (Graphics Interchange Format, 图形交换格式)。`, { path: sourcePath, supportedExtensions: [...referenceImageExtensions()] });
28
22
  }
29
23
  let size;
30
24
  try {
@@ -98,28 +92,6 @@ export async function uploadImageAttachments(client, projectId, branch, attachme
98
92
  return uploaded;
99
93
  }
100
94
  export function buildReferenceImagesPrompt(prompt, attachments) {
101
- if (attachments.length === 0)
102
- return prompt;
103
- const pathList = attachments.map((attachment) => `- ${attachment.sandboxPath}`).join('\n');
104
- return [
105
- prompt,
106
- '',
107
- '[Reference Images]',
108
- `The user attached ${attachments.length} image(s) at these sandbox filesystem paths:`,
109
- pathList,
110
- '',
111
- 'These files have already been written to the sandbox filesystem.',
112
- '',
113
- '**IMPORTANT:** Do NOT read, open, or inspect any of these image files. Do NOT use tools like read_file, cat, or any other file reader on them. The paths themselves are all you need.',
114
- '',
115
- "Infer each image's purpose from the user's message (e.g. reference for style/composition, UI mockup, screenshot illustrating an issue, asset to import, etc.). For any image you judge to be a visual reference, pass its path directly to `playcraft-generate-image` — do NOT ask the user to confirm, and do NOT attempt to examine the image content first.",
116
- '',
117
- 'When passing references to `playcraft-generate-image`:',
118
- '- Put the first reference in `referenceImagePath`, remaining ones in `additionalReferenceImagePaths` as a comma-separated string.',
119
- '- Preserve the original order.',
120
- '- Up to 8 references total; paths must not contain commas.',
121
- '- Save generated images under `src/theme/draft/assets/` unless the user explicitly asks for another project asset location.',
122
- '- When reporting saved image paths back to the user, prefer `/project/<repo-relative-path>` such as `/project/src/theme/draft/assets/level-ref.png`, not the full `/projects/<projectId>/<branch>/...` path.',
123
- ].join('\n');
95
+ return appendRemixReferenceImagesPrompt(prompt, attachments.map((attachment) => attachment.sandboxPath));
124
96
  }
125
97
  export const IMAGE_OPTION_HELP = '一个或多个图片路径(最多 4 张;JPEG (Joint Photographic Experts Group, 联合图像专家组)、PNG (Portable Network Graphics, 便携式网络图形)、WebP、GIF (Graphics Interchange Format, 图形交换格式),每张不超过 5 MB (Megabyte, 兆字节))';
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Map legacy sandbox project root aliases onto the real cwd.
3
+ *
4
+ * Multi-slot sandboxes use workdir `/projects/{projectId}/{branch}` while many
5
+ * agent prompts / tools still advertise `/project/...`. CLI commands must treat
6
+ * `/project` as cwd so post-processing (e.g. remove-background) finds files.
7
+ */
8
+ import { resolve } from 'path';
9
+ export function resolveSandboxPath(userPath, cwd = process.cwd()) {
10
+ if (!userPath)
11
+ return userPath;
12
+ const forward = userPath.replace(/\\/g, '/');
13
+ if (forward === '/project') {
14
+ return cwd;
15
+ }
16
+ if (forward.startsWith('/project/')) {
17
+ return resolve(cwd, forward.slice('/project/'.length));
18
+ }
19
+ return userPath;
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/cli",
3
- "version": "0.0.55",
3
+ "version": "0.0.57",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -45,8 +45,8 @@
45
45
  "@gltf-transform/core": "^4.3.0",
46
46
  "@gltf-transform/extensions": "^4.3.0",
47
47
  "@gltf-transform/functions": "^4.3.0",
48
- "@playcraft/build": "^0.0.55",
49
- "@playcraft/common": "^0.0.55",
48
+ "@playcraft/build": "^0.0.57",
49
+ "@playcraft/common": "^0.0.57",
50
50
  "chokidar": "^4.0.3",
51
51
  "commander": "^13.1.0",
52
52
  "cors": "^2.8.6",