@pix3/cli 1.6.4 → 2.0.0-alpha.0
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/LICENSE +201 -201
- package/README.md +722 -788
- package/dist/index.js +557 -421
- package/dist/runtime-types.json +1 -1
- package/dist/smoke/prebuilt/{build-7MIXIMSZ.mjs → build-7REMSA2S.mjs} +52 -52
- package/dist/smoke/prebuilt/chunk-4DGG2BKR.mjs +2 -0
- package/dist/smoke/prebuilt/chunk-YLBMUJ2H.mjs +238 -0
- package/dist/smoke/prebuilt/runtime.mjs +1 -1
- package/dist/smoke/prebuilt/smoke-worker.mjs +211 -2
- package/dist/smoke/prebuilt/tree-defaults.mjs +1 -1
- package/dist/validate/prebuilt/{build-AUGKXER3.js → build-ADF5EEE2.js} +52 -52
- package/dist/validate/prebuilt/bundle-main.js +223 -18
- package/dist/validate/prebuilt/chunk-VIHIF4G2.js +238 -0
- package/dist/validate/prebuilt/runtime.js +1 -1
- package/kit/files/.claude/skills/pix3-editor/SKILL.md +200 -0
- package/kit/files/.claude/skills/pix3-nodes/SKILL.md +13 -15
- package/kit/files/.claude/skills/pix3-nodes/reference.md +77 -71
- package/kit/files/.claude/skills/pix3-scene-format/SKILL.md +38 -41
- package/kit/files/.claude/skills/pix3-scene-format/pix3anim.md +185 -0
- package/kit/files/.claude/skills/pix3-scene-format/project-files.md +244 -0
- package/kit/files/.claude/skills/pix3-scene-format/reference.md +28 -16
- package/kit/files/.claude/skills/pix3-scripts/SKILL.md +34 -24
- package/kit/files/.claude/skills/pix3-scripts/reference.md +68 -141
- package/kit/files/.claude/skills/pix3-verify/SKILL.md +65 -326
- package/kit/files/.claude/skills/pix3-verify/reference.md +2 -14
- package/kit/files/AGENTS.md +76 -84
- package/kit/files/design/tests/bots/pix3-test-bot.d.ts +89 -0
- package/kit/kit.json +8 -5
- package/package.json +62 -54
- package/templates/2d/files/scenes/main.pix3scene +31 -0
- package/templates/2d/template.yaml +11 -0
- package/templates/3d/files/scenes/main.pix3scene +50 -0
- package/templates/3d/template.yaml +11 -0
- package/templates/base/files/README.md +39 -0
- package/templates/base/files/gitignore +11 -0
- package/templates/base/files/index.html +23 -0
- package/templates/base/files/package.json +28 -0
- package/templates/base/files/src/main.ts +3 -0
- package/templates/base/files/tsconfig.json +17 -0
- package/templates/base/files/vite.config.ts +10 -0
- package/dist/smoke/prebuilt/chunk-CBFYKDS7.mjs +0 -2
- package/dist/smoke/prebuilt/chunk-VJHJ5BCO.mjs +0 -239
- package/dist/validate/prebuilt/chunk-CFQFEI52.js +0 -239
- package/templates/empty-2d/files/README.md +0 -20
- package/templates/empty-2d/files/scenes/main.pix3scene +0 -68
- package/templates/empty-2d/files/sprites/pix3-logo.png +0 -0
- package/templates/empty-2d/template.yaml +0 -7
- package/templates/empty-3d/files/README.md +0 -20
- package/templates/empty-3d/files/scenes/main.pix3scene +0 -108
- package/templates/empty-3d/files/sprites/pix3-logo.png +0 -0
- package/templates/empty-3d/template.yaml +0 -7
- package/templates/idea-blank/files/README.md +0 -22
- package/templates/idea-blank/files/scenes/main.pix3scene +0 -37
- package/templates/idea-blank/template.yaml +0 -10
- package/templates/minigame-2d/files/README.md +0 -44
- package/templates/minigame-2d/files/design/tests/reachability.json +0 -1
- package/templates/minigame-2d/files/design/tests/routines/mute-music.json +0 -44
- package/templates/minigame-2d/files/scenes/main.pix3scene +0 -84
- package/templates/minigame-2d/files/scenes/menu.pix3scene +0 -118
- package/templates/minigame-2d/files/scenes/ui/settings-window.pix3scene +0 -138
- package/templates/minigame-2d/files/scripts/GameFlow.ts +0 -133
- package/templates/minigame-2d/files/scripts/MenuFlow.ts +0 -217
- package/templates/minigame-2d/files/scripts/SettingsWindow.ts +0 -198
- package/templates/minigame-2d/template.yaml +0 -10
- package/templates/playable-2d/files/README.md +0 -30
- package/templates/playable-2d/files/design/recipe.md +0 -83
- package/templates/playable-2d/files/design/tests/reachability.json +0 -4
- package/templates/playable-2d/files/design/tests/routines/intro-to-cta.json +0 -32
- package/templates/playable-2d/files/scenes/main.pix3scene +0 -111
- package/templates/playable-2d/files/scenes/ui/end-screen.pix3scene +0 -91
- package/templates/playable-2d/files/scenes/ui/intro.pix3scene +0 -65
- package/templates/playable-2d/files/scripts/CtaButton.ts +0 -55
- package/templates/playable-2d/files/scripts/GameFlow.ts +0 -256
- package/templates/playable-2d/files/sprites/pix3-logo.png +0 -0
- package/templates/playable-2d/template.yaml +0 -9
- package/templates/playable-3d/files/README.md +0 -32
- package/templates/playable-3d/files/design/recipe.md +0 -127
- package/templates/playable-3d/files/design/tests/reachability.json +0 -4
- package/templates/playable-3d/files/design/tests/routines/intro-to-cta.json +0 -32
- package/templates/playable-3d/files/scenes/main.pix3scene +0 -230
- package/templates/playable-3d/files/scenes/ui/end-screen.pix3scene +0 -91
- package/templates/playable-3d/files/scenes/ui/intro.pix3scene +0 -65
- package/templates/playable-3d/files/scripts/CtaButton.ts +0 -55
- package/templates/playable-3d/files/scripts/GameFlow.ts +0 -309
- package/templates/playable-3d/files/scripts/GameRules.ts +0 -290
- package/templates/playable-3d/files/scripts/ScoreHud.ts +0 -113
- package/templates/playable-3d/template.yaml +0 -9
- package/templates/recipe-arena-2d/files/README.md +0 -48
- package/templates/recipe-arena-2d/files/design/recipe.md +0 -127
- package/templates/recipe-arena-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-arena-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-arena-2d/files/scenes/main.pix3scene +0 -292
- package/templates/recipe-arena-2d/files/scenes/menu.pix3scene +0 -100
- package/templates/recipe-arena-2d/files/scenes/prefabs/hazard.pix3scene +0 -42
- package/templates/recipe-arena-2d/files/scenes/prefabs/pickup.pix3scene +0 -45
- package/templates/recipe-arena-2d/files/scenes/ui/result.pix3scene +0 -85
- package/templates/recipe-arena-2d/files/scripts/GameRules.ts +0 -368
- package/templates/recipe-arena-2d/files/scripts/MenuFlow.ts +0 -106
- package/templates/recipe-arena-2d/files/scripts/PlayerController.ts +0 -151
- package/templates/recipe-arena-2d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-arena-2d/files/scripts/Spawner.ts +0 -214
- package/templates/recipe-arena-2d/files/scripts/TouchRules.ts +0 -174
- package/templates/recipe-arena-2d/files/sprites/ph-avatar.png +0 -0
- package/templates/recipe-arena-2d/files/sprites/ph-hazard.png +0 -0
- package/templates/recipe-arena-2d/files/sprites/ph-pickup.png +0 -0
- package/templates/recipe-arena-2d/template.yaml +0 -10
- package/templates/recipe-blank-2d/files/README.md +0 -61
- package/templates/recipe-blank-2d/files/design/recipe.md +0 -148
- package/templates/recipe-blank-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-blank-2d/files/design/tests/routines/restart.json +0 -27
- package/templates/recipe-blank-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-blank-2d/files/scenes/main.pix3scene +0 -224
- package/templates/recipe-blank-2d/files/scenes/ui/result.pix3scene +0 -85
- package/templates/recipe-blank-2d/files/scripts/GameRules.ts +0 -350
- package/templates/recipe-blank-2d/files/scripts/ScoreHud.ts +0 -110
- package/templates/recipe-blank-2d/files/sprites/ph-bg.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-capsule.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-circle.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-orb.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-ring.png +0 -0
- package/templates/recipe-blank-2d/files/sprites/ph-star.png +0 -0
- package/templates/recipe-blank-2d/template.yaml +0 -10
- package/templates/recipe-bouncer-2d/files/README.md +0 -81
- package/templates/recipe-bouncer-2d/files/design/recipe.md +0 -146
- package/templates/recipe-bouncer-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-bouncer-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-bouncer-2d/files/scenes/main.pix3scene +0 -615
- package/templates/recipe-bouncer-2d/files/scenes/menu.pix3scene +0 -191
- package/templates/recipe-bouncer-2d/files/scenes/ui/result.pix3scene +0 -87
- package/templates/recipe-bouncer-2d/files/scripts/BallBody.ts +0 -298
- package/templates/recipe-bouncer-2d/files/scripts/GameRules.ts +0 -377
- package/templates/recipe-bouncer-2d/files/scripts/MenuFlow.ts +0 -109
- package/templates/recipe-bouncer-2d/files/scripts/PaddleController.ts +0 -112
- package/templates/recipe-bouncer-2d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-bouncer-2d/files/scripts/TouchRules.ts +0 -263
- package/templates/recipe-bouncer-2d/files/scripts/ball-collision.ts +0 -307
- package/templates/recipe-bouncer-2d/files/sprites/ph-ball.png +0 -0
- package/templates/recipe-bouncer-2d/files/sprites/ph-bg.png +0 -0
- package/templates/recipe-bouncer-2d/files/sprites/ph-bumper.png +0 -0
- package/templates/recipe-bouncer-2d/files/sprites/ph-paddle.png +0 -0
- package/templates/recipe-bouncer-2d/template.yaml +0 -10
- package/templates/recipe-grid-3d/files/README.md +0 -22
- package/templates/recipe-grid-3d/files/design/recipe.md +0 -110
- package/templates/recipe-grid-3d/files/scenes/main.pix3scene +0 -169
- package/templates/recipe-grid-3d/files/scenes/menu.pix3scene +0 -100
- package/templates/recipe-grid-3d/files/scenes/ui/result.pix3scene +0 -105
- package/templates/recipe-grid-3d/files/scripts/GridBoard.ts +0 -319
- package/templates/recipe-grid-3d/files/scripts/GridRules.ts +0 -278
- package/templates/recipe-grid-3d/files/scripts/MenuFlow.ts +0 -106
- package/templates/recipe-grid-3d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-grid-3d/template.yaml +0 -10
- package/templates/recipe-tapper-2d/files/README.md +0 -39
- package/templates/recipe-tapper-2d/files/design/recipe.md +0 -132
- package/templates/recipe-tapper-2d/files/design/tests/reachability.json +0 -4
- package/templates/recipe-tapper-2d/files/design/tests/routines/terminal-retry.json +0 -23
- package/templates/recipe-tapper-2d/files/scenes/main.pix3scene +0 -269
- package/templates/recipe-tapper-2d/files/scenes/menu.pix3scene +0 -100
- package/templates/recipe-tapper-2d/files/scenes/prefabs/hazard.pix3scene +0 -42
- package/templates/recipe-tapper-2d/files/scenes/prefabs/target.pix3scene +0 -45
- package/templates/recipe-tapper-2d/files/scenes/ui/result.pix3scene +0 -85
- package/templates/recipe-tapper-2d/files/scripts/GameRules.ts +0 -368
- package/templates/recipe-tapper-2d/files/scripts/MenuFlow.ts +0 -106
- package/templates/recipe-tapper-2d/files/scripts/ScoreHud.ts +0 -94
- package/templates/recipe-tapper-2d/files/scripts/Spawner.ts +0 -214
- package/templates/recipe-tapper-2d/files/scripts/TouchRules.ts +0 -192
- package/templates/recipe-tapper-2d/files/sprites/ph-hazard.png +0 -0
- package/templates/recipe-tapper-2d/files/sprites/ph-target.png +0 -0
- package/templates/recipe-tapper-2d/template.yaml +0 -10
package/README.md
CHANGED
|
@@ -1,788 +1,722 @@
|
|
|
1
|
-
# @pix3/cli
|
|
2
|
-
|
|
3
|
-
Command line for Pix3:
|
|
4
|
-
|
|
5
|
-
```text
|
|
6
|
-
pix3 new [<
|
|
7
|
-
pix3
|
|
8
|
-
|
|
9
|
-
pix3 setup [claude|codex]
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
pix3
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
pix3
|
|
21
|
-
|
|
22
|
-
pix3 sfx <preset|"text"> [--out <f.wav>] [--seed <n>] [--json]
|
|
23
|
-
synthesize a sound effect to WAV, offline
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
(`
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
pix3
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
`
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
`
|
|
159
|
-
`
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
|
192
|
-
|
|
|
193
|
-
| `
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
`
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
`
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
`
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
`
|
|
283
|
-
|
|
284
|
-
`
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
`
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
`
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
`
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
`
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
`
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
`
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
`
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
`
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
Anything else the editor refuses (`unknown_tool`, `agent_disabled` when the human switched the
|
|
725
|
-
channel off) is passed through as the editor wrote it.
|
|
726
|
-
|
|
727
|
-
**Generation permission.** The first `generate_asset` / `generate_sfx` of a connection (server
|
|
728
|
-
session + lease + `pix3 mcp` process) opens a prompt in the editor naming the process and what it
|
|
729
|
-
calls itself (unverified); the call waits up to 60 s. Allowed → 20 generations, then it asks again.
|
|
730
|
-
In memory only: a reconnect, a new `pix3 serve` run, a lease takeover or a new `pix3 mcp` process
|
|
731
|
-
resets it; the status-bar Agent pill has a revoke button and switches the channel off.
|
|
732
|
-
|
|
733
|
-
## MCP configuration and `pix3 setup`
|
|
734
|
-
|
|
735
|
-
`pix3 new` and `pix3 kit` write the project's `.mcp.json` (Claude Code's project format), **pinned
|
|
736
|
-
to the CLI version that wrote it** — never a bare `@pix3/cli`:
|
|
737
|
-
|
|
738
|
-
```json
|
|
739
|
-
{ "mcpServers": { "pix3": { "command": "npx", "args": ["-y", "@pix3/cli@<X.Y.Z>", "mcp", "--workspace"] } } }
|
|
740
|
-
```
|
|
741
|
-
|
|
742
|
-
`pix3 setup [claude|codex]` prints (never runs) the registration for a project without one:
|
|
743
|
-
`claude mcp add pix3 -- npx -y @pix3/cli@<X.Y.Z> mcp --workspace` (run in the project folder), and
|
|
744
|
-
for Codex a `~/.codex/config.toml` block — `[mcp_servers.pix3]` with `command`, `args` (plus
|
|
745
|
-
`--project <root>`, since that config is global) and `tool_timeout_sec = 180`. The Codex keys are
|
|
746
|
-
the ones `tools/pix3-agent-bridge` passes to `codex exec -c mcp_servers.pix3.*`; the file form is
|
|
747
|
-
best-effort, check `codex mcp --help` of your Codex version.
|
|
748
|
-
|
|
749
|
-
**Dev mode.** From a checkout of the pix3 repo (the CLI runs from `packages/pix3-cli/src/`), or
|
|
750
|
-
with `PIX3_CLI_DEV=1`, both write `node <repo>/packages/pix3-cli/src/index.ts mcp --workspace`
|
|
751
|
-
instead, so the channel can be tried before that version is on npm. `PIX3_CLI_DEV=0` forces the
|
|
752
|
-
pinned form.
|
|
753
|
-
|
|
754
|
-
## Package layout and publishing
|
|
755
|
-
|
|
756
|
-
The published package has **no runtime `dependencies`**, so a cold `npx -y @pix3/cli@X.Y.Z …`
|
|
757
|
-
fetches one tarball and installs nothing else (measured in
|
|
758
|
-
`.plans/measurements/external-agent-phase0-cold-start.md`). `prepack` builds:
|
|
759
|
-
|
|
760
|
-
```text
|
|
761
|
-
dist/index.js the bin: src/index.ts + yaml + @modelcontextprotocol/sdk (zod, ajv, …) + ws,
|
|
762
|
-
one minified ESM file (scripts/build-bin.mjs); lazy commands stay lazy
|
|
763
|
-
dist/validate/prebuilt/ validate + @pix3/runtime + three (scripts/build-validate.mjs)
|
|
764
|
-
dist/smoke/prebuilt/ smoke worker + tree defaults + @pix3/runtime + three (scripts/build-smoke.mjs)
|
|
765
|
-
dist/runtime-types.json runtime-types/ packed into one file (scripts/build-runtime-types.mjs):
|
|
766
|
-
~1 100 small .d.ts cost npm over a second to unpack on every npx run
|
|
767
|
-
kit/ the agent kit (scripts/build-kit.mjs)
|
|
768
|
-
templates/ copy of src/templates/projects (scripts/copy-templates.mjs; removed at postpack)
|
|
769
|
-
```
|
|
770
|
-
|
|
771
|
-
Left out of the bin on purpose: Node built-ins; `esbuild` (an `optionalDependency` — level 2 of
|
|
772
|
-
`validate`, `smoke` and `check` resolve it from the bin with `import.meta.resolve`, and degrade
|
|
773
|
-
with a note when it is absent) and `typescript` (fetched on demand by `check`); `ws`'s optional
|
|
774
|
-
native `bufferutil` / `utf-8-validate`; and the checkout-only modules (`validate/bundle.ts`,
|
|
775
|
-
`smoke/bundle.ts`, the kit generator), which become a throwing stub. Every file the CLI reads from
|
|
776
|
-
its own package is addressed from the package root (`src/package-root.ts`), never relative to the
|
|
777
|
-
current module — in the bundle every module's `import.meta.url` is the bin's. The sources (`node
|
|
778
|
-
src/index.ts`, the specs) never use a prebuilt bundle, even when an old `dist/` exists.
|
|
779
|
-
`src/bin-bundle.spec.ts` builds the bin into a temp package layout and checks `--version` against
|
|
780
|
-
`package.json` (and that against the lockstep root version), the pinned `@pix3/cli@X.Y.Z` launch,
|
|
781
|
-
and that nothing but built-ins and the optional externals is imported at run time.
|
|
782
|
-
|
|
783
|
-
Publishing is `.github/workflows/publish-packages.yml` (npm Trusted Publishing / OIDC, no token):
|
|
784
|
-
a `runtime-vX.Y.Z` tag publishes `@pix3/runtime` and `@pix3/cli` together, `cli-vX.Y.Z` the CLI
|
|
785
|
-
alone, or run the workflow manually. The job runs `npm ci` at the repo root (the build needs the
|
|
786
|
-
runtime sources, the root TypeScript and esbuild), checks the lockstep version against the root
|
|
787
|
-
and the tag, type-checks, runs the CLI specs and `npm publish`es. Try the tarball locally with
|
|
788
|
-
`npm pack -w packages/pix3-cli` at the repo root, then `npx -y --package ./pix3-cli-X.Y.Z.tgz pix3 …`.
|
|
1
|
+
# @pix3/cli
|
|
2
|
+
|
|
3
|
+
Command line for Pix3:
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
pix3 new [<2d|3d> [dir]] [--name <n>] create an empty 2D or 3D project (npm create pix3)
|
|
7
|
+
pix3 editor [--project <dir>] [--stop] [--stop-chrome] [--chrome-only] [--no-chrome] [--port <n>] [--cdp-port <n>] [--headless]
|
|
8
|
+
find/start the dev server, open the editor in Chrome (CDP behind the token proxy)
|
|
9
|
+
pix3 agent-setup [claude|codex] [--repair] [--cdp-port <n>] [--project <dir>]
|
|
10
|
+
write the project's chrome-devtools-mcp config
|
|
11
|
+
pix3 validate [paths…] [--json] strict scene check
|
|
12
|
+
pix3 check [--json] [--no-hydrate] [--offline] [--no-sync] [--project <dir>]
|
|
13
|
+
validate + tsc over the scripts + versions
|
|
14
|
+
pix3 smoke [scene] [--changed|--all] [--frames N] [--timeout S] [--json] [--project <dir>]
|
|
15
|
+
run the game headless in Node, report what threw
|
|
16
|
+
pix3 tree [scene] [--depth N] [--types A,B] [--props] [--json] [--project <dir>]
|
|
17
|
+
scene outline, one line per node; no scene = overview
|
|
18
|
+
pix3 kit [--update] [--migrate] [--project <dir>]
|
|
19
|
+
install / update the agent kit; --migrate: a 1.x kit → 2.x
|
|
20
|
+
pix3 character-compile <spec> [--dry-run] [--force] [--json] [--project <dir>]
|
|
21
|
+
a 2D character (.pix3anim + prefab) from frame PNGs
|
|
22
|
+
pix3 sfx <preset|"text"> [--out <f.wav>] [--seed <n>] [--json]
|
|
23
|
+
synthesize a sound effect to WAV, offline
|
|
24
|
+
pix3 gap "<what was missing>" [--kind …] [--list] record what the agent had to work around
|
|
25
|
+
pix3 help [command] this overview, or one command's usage
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`pix3 <command> --help` (or `-h`, or `pix3 help <command>`) prints that command's own usage —
|
|
29
|
+
every text lives in `src/usage.ts` (`USAGE`, `COMMAND_USAGE`), and `bin-bundle.spec` holds every
|
|
30
|
+
command the entry dispatches to an entry there, answered by the bundled bin. `validate --help`
|
|
31
|
+
adds its codes, `check --help` the ones it adds.
|
|
32
|
+
|
|
33
|
+
A usage error exits **2**: an unknown option, an option without its value, a bad argument, or no
|
|
34
|
+
`pix3project.yaml` above the folder — with the message (and, for a bad argument, the command's
|
|
35
|
+
usage) on stderr. That holds for `editor`, `agent-setup` and `gap` as for the checks; `kit`
|
|
36
|
+
refuses an unknown option rather than ignoring it (`pix3 kit --updat` is exit 2, not a plain
|
|
37
|
+
install).
|
|
38
|
+
|
|
39
|
+
`pix3 new` is what `npm create pix3` runs: the `base` layer of `packages/create-pix3/templates/`
|
|
40
|
+
(package.json, vite.config.ts with `pix3()`, index.html, src/main.ts, tsconfig.json, gitignore,
|
|
41
|
+
README.md) plus the `2d` or `3d` layer (one empty entry scene), `{{PROJECT_NAME}}` /
|
|
42
|
+
`{{PACKAGE_NAME}}` / `{{PIX3_VERSION}}` substituted, `pix3project.yaml` generated, then the agent
|
|
43
|
+
kit. Nothing in it plays a game. Record: `.plans/templates.md`.
|
|
44
|
+
|
|
45
|
+
Two things a first run trips on: **2.0.0-alpha packages are not published yet** — a new
|
|
46
|
+
project's `npm install` fails until they are; the starter's README says how to link a pix3-core
|
|
47
|
+
checkout meanwhile (`node_modules` → the checkout's, `node <checkout>/packages/cli/src/index.ts`
|
|
48
|
+
for `pix3`). And **the dev port is Vite's**, 5173 only when free: `pix3 editor`, the kit and every
|
|
49
|
+
harness read `.pix3/dev.json` (`editorUrl`, `publicEditorUrl`), never a fixed port.
|
|
50
|
+
|
|
51
|
+
`new`, `kit`, `editor` and `agent-setup` load neither TypeScript nor the kit generator: the kit
|
|
52
|
+
and the runtime types are prebuilt into the package (`kit/`, `dist/runtime-types.json`, at
|
|
53
|
+
`prepack`).
|
|
54
|
+
`tree` reads YAML only; `smoke` and `tree --props` load the runtime from a bundle prebuilt at
|
|
55
|
+
`prepack` (`dist/smoke/prebuilt/`, like validate's), so none of this slows `new` / `editor`.
|
|
56
|
+
|
|
57
|
+
Coding agents drive the open editor tab through Chrome DevTools MCP (`chrome-devtools-mcp`): the
|
|
58
|
+
tab registers its `pix3_*` tools on the page (`packages/editor-core/src/host/bridge-tools.ts`),
|
|
59
|
+
`pix3 editor` puts that tab in a Chrome whose DevTools protocol is reachable only through its
|
|
60
|
+
token proxy, `pix3 agent-setup` points the agent's MCP config at the proxy with the token. There is no Pix3 MCP server (plan §D.2); the agent-facing procedure is
|
|
61
|
+
the kit's `pix3-editor` skill.
|
|
62
|
+
|
|
63
|
+
## `pix3 check` — everything an agent should run after a batch of edits
|
|
64
|
+
|
|
65
|
+
`pix3 check` = `pix3 validate` (both levels) + a TypeScript type-check of the project's scripts +
|
|
66
|
+
a version check. Exit 0 = no errors (warnings
|
|
67
|
+
allowed), 1 = at least one error, 2 = could not run. Files are the truth only once the editor has
|
|
68
|
+
written them: when `.pix3/dev.json` names a live dev server, `check` first asks it to flush the
|
|
69
|
+
editor's unsaved scenes (`POST /__pix3/api/flush`, up to 15 s; `--no-sync` skips it, a dead
|
|
70
|
+
server is ignored). Diagnostics are one list, validate's plus:
|
|
71
|
+
|
|
72
|
+
| Code | Severity | When |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| `E_TYPE` | error | a tsc diagnostic (`message` starts with `TS<code>:`; `file`, `line`) |
|
|
75
|
+
| `E_TYPECHECK_UNAVAILABLE` | error | TypeScript could not be found or installed (`fix` = the command to run) |
|
|
76
|
+
| `E_EDITOR_UNSYNCED` | error | `.pix3/dev.json` points at a live dev server whose editor did not flush its unsaved scenes within 15 s (`--no-sync` reads the disk as it is) |
|
|
77
|
+
| `E_RUNTIME_VERSION` | error | the version gate: the project resolves a `node_modules/@pix3/runtime` that is not the runtime this CLI validates (and smoke-runs) with — run the project's own CLI (`npx pix3 …`) or install the matching runtime |
|
|
78
|
+
| `W_PIX3_VERSION_MISMATCH` | warning | an installed `@pix3/cli`, `@pix3/vite-plugin` or `@pix3/editor-core` is not this CLI's version (lockstep packages) |
|
|
79
|
+
| `W_RUNTIME_NOT_INSTALLED` | warning | own `tsconfig.json`, and no `node_modules/@pix3/runtime` |
|
|
80
|
+
| `W_KIT_OUTDATED` | warning | `metadata.agentKit.version` is not this CLI's version (`pix3 kit --update`) |
|
|
81
|
+
| `W_EDITOR_HMR_API` | warning | `import.meta.hot` in a module the editor runs |
|
|
82
|
+
| `W_EDITOR_CSS_IMPORT` | warning | a stylesheet import (not `?inline`/`?raw`/`?url`) in a module the editor runs |
|
|
83
|
+
| `W_EDITOR_DYNAMIC_IMPORT` | warning | an `import()` whose argument is not one plain string, in a module the editor runs |
|
|
84
|
+
|
|
85
|
+
"A module the editor runs" = the editor's script chain (plugin contract B): `scripts/**`,
|
|
86
|
+
`src/scripts/**`, `design/tests/bots/**` (`.ts`, minus specs/tests/`.d.ts`) and every local module
|
|
87
|
+
they import, transitively; `message` names the root that reaches it. Each of the three makes Vite
|
|
88
|
+
put `/@vite/client` on the editor page, and then the game's `full-reload` reloads the editor too.
|
|
89
|
+
The scan is lexical (`src/check/editor-chain.ts`), bare packages are not followed.
|
|
90
|
+
|
|
91
|
+
`--json`:
|
|
92
|
+
|
|
93
|
+
```text
|
|
94
|
+
{
|
|
95
|
+
"ok": true, "projectRoot": "…", "errorCount": 0, "warningCount": 2,
|
|
96
|
+
"level2": { "state": "ran", "filesHydrated": 5, "filesSkipped": 0, "filesSkippedForScripts?": 0 },
|
|
97
|
+
"files": [{ "file": "scenes/main.pix3scene", "sha256": "<hex of the raw bytes>" }],
|
|
98
|
+
"diagnostics": [{ "severity", "code", "file", "line?", "nodeId?", "path?", "message", "fix?" }],
|
|
99
|
+
"notes": [],
|
|
100
|
+
"typecheck": { "ok": true, "errors": 0, "tsconfig": ".pix3/tsconfig.check.json", "mode": "pix3-types",
|
|
101
|
+
"files": 5, "typescript": { "version": "5.8.3", "source": "cache" } },
|
|
102
|
+
"kit": { "version": "1.6.0", "cliVersion": "1.6.0", "upToDate": true },
|
|
103
|
+
"timingsMs": { "validate": 190, "typecheck": 1180, "total": 1380 }
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`files` holds every scene validated and every script type-checked, hashed over the **raw bytes** —
|
|
108
|
+
the hashes `pix3_sync`'s `expect` takes. `typecheck.errors` counts the
|
|
109
|
+
`E_TYPE` / `E_TYPECHECK_UNAVAILABLE` entries of `diagnostics`.
|
|
110
|
+
|
|
111
|
+
**A compile error hides level 2.** When the scripts do not compile (`E_SCRIPT_COMPILE`), level 2
|
|
112
|
+
skips every scene with `user:` components — their `E_UNKNOWN_CONFIG_KEY` / `E_PROPERTY_TYPE` /
|
|
113
|
+
component checks are *missing*, not passed. The report says so: `level2.filesSkippedForScripts`,
|
|
114
|
+
a `SKIPPED: N scene(s) …` note, and the human summary line reads `0 scene file(s) loaded as the game loads
|
|
115
|
+
them, N NOT loaded (scripts do not compile — user: components unchecked)`. Fix the compile error and run again.
|
|
116
|
+
|
|
117
|
+
**Which tsconfig.** A project with its **own** root `tsconfig.json` (a Vite project — every
|
|
118
|
+
`npm create pix3` starter) is checked with it, as is, against its own
|
|
119
|
+
`node_modules` (`mode: "project"`); nothing is written into it. Otherwise (`mode: "pix3-types"`)
|
|
120
|
+
`check` uses `.pix3/tsconfig.check.json` against the bundled types in `.pix3/types/`, and first
|
|
121
|
+
(re)writes both when they are missing or from another CLI build.
|
|
122
|
+
|
|
123
|
+
**Where TypeScript comes from** — it is not a dependency of `@pix3/cli`: (1) the project's own
|
|
124
|
+
`node_modules/typescript` (or an ancestor's); (2) the CLI's sibling install
|
|
125
|
+
(`import.meta.resolve('typescript')`, e.g. the monorepo); (3) `~/.pix3/typescript/5.8.3/`,
|
|
126
|
+
installed there once with `npm install --prefix ~/.pix3/typescript/5.8.3 typescript@5.8.3 …`
|
|
127
|
+
(printed before it runs). `--offline` never installs: it fails with `E_TYPECHECK_UNAVAILABLE` and
|
|
128
|
+
the command. `PIX3_TYPESCRIPT=<package dir>` overrides the search. Loaded with a dynamic `import()`.
|
|
129
|
+
|
|
130
|
+
Measured (the 1.x recipe-tapper-2d, 5 scripts, packed CLI installed outside the repo, fresh `HOME`):
|
|
131
|
+
first `check` 2.1 s including the TypeScript install, then 1.45 s (validate ~0.2 s, tsc ~1.2 s).
|
|
132
|
+
|
|
133
|
+
### SVG sprites
|
|
134
|
+
|
|
135
|
+
Every texture in Pix3 (editor viewport, play mode, the single-file export) loads as bytes → `Blob`
|
|
136
|
+
→ object URL → three's `TextureLoader` → `<img>`, so an `.svg` renders exactly when a browser
|
|
137
|
+
`<img>` decodes it, at the size the `<img>` reports. Level 1 checks every `.svg` a scene
|
|
138
|
+
references (once per file per scene, at its first reference):
|
|
139
|
+
|
|
140
|
+
| Code | Severity | When |
|
|
141
|
+
| --- | --- | --- |
|
|
142
|
+
| `E_SVG_INVALID` | error | no `<svg>` root, or the root lacks `xmlns="http://www.w3.org/2000/svg"` (the browser refuses to decode it) |
|
|
143
|
+
| `E_SVG_NO_SIZE` | error | no absolute `width`/`height` and no `viewBox` (renders into a cropped 300x150 box), or one dimension without a `viewBox` |
|
|
144
|
+
| `W_SVG_VIEWBOX_ONLY` | warning | a `viewBox` but no absolute `width`/`height` (`%` and `style=` sizes count as none): Chrome gives it 300x150, art letterboxed |
|
|
145
|
+
| `W_SVG_EXTERNAL_REF` | warning | an `href`, CSS `url()` or `@import` to anything but `#fragment` / `data:` — never loads in SVG-as-image mode |
|
|
146
|
+
|
|
147
|
+
Measured in Chromium 129 through the runtime's own `ResourceManager` + `AssetLoader` + `Sprite2D`:
|
|
148
|
+
a sized SVG auto-sizes the sprite to its `width`×`height`; a viewBox-only one to 300×150; one
|
|
149
|
+
without `xmlns`, or any SVG whose Blob type is not `image/svg+xml`, fails to load (browsers do
|
|
150
|
+
not sniff SVG).
|
|
151
|
+
|
|
152
|
+
### `.pix3anim` frames, locale tables and keys
|
|
153
|
+
|
|
154
|
+
Level 1 also follows what a scene reaches through another file. Every `.pix3anim` (all of them on
|
|
155
|
+
a whole-project run, else those the validated scenes name) must be a JSON object, and every image
|
|
156
|
+
it names — each frame's `texturePath`, the spritesheet's top-level one — must exist; an `.svg`
|
|
157
|
+
frame gets the sprite rules above (`E_SVG_*`), reported on the `.pix3anim` with the frame's path.
|
|
158
|
+
An `AnimatedSprite3D` names its frames itself (`frames:` — `res://` paths, bare paths or
|
|
159
|
+
`{ url }`): each must exist (`E_MISSING_RESOURCE`, the SVG rules on an `.svg`), and an entry that
|
|
160
|
+
is not a texture is `E_PROPERTY_TYPE` (a blank frame).
|
|
161
|
+
The locales are the ones the plugin ships: the `localization:` block of `pix3project.yaml`, else
|
|
162
|
+
every `locales/*.json` (`en` the default when there is one). Severity is what the player would
|
|
163
|
+
see: the default or fallback locale's table not loading means every `labelKey` shows its key
|
|
164
|
+
(error); another locale's means its texts fall back (warning). A key is looked up the way
|
|
165
|
+
`LocalizationService` does — the default table, then the fallback's, `""` counting as none.
|
|
166
|
+
|
|
167
|
+
| Code | Severity | When |
|
|
168
|
+
| --- | --- | --- |
|
|
169
|
+
| `E_ANIM_JSON` | error | a `.pix3anim` is not JSON, not an object, or its `clips` are not a list |
|
|
170
|
+
| `E_MISSING_FRAME` | error | a frame image or the spritesheet a `.pix3anim` names does not exist (case hint as for `E_MISSING_RESOURCE`) |
|
|
171
|
+
| `E_LOCALE_MISSING` / `W_LOCALE_MISSING` | error / warning | a declared locale has no `locales/<id>.json` (default or fallback / another); on `pix3project.yaml` at its line |
|
|
172
|
+
| `E_LOCALE_JSON` / `W_LOCALE_JSON` | error / warning | the table is not JSON, not an object, or `strings` / `sprites` is not a map |
|
|
173
|
+
| `E_LOCALE_VALUE` / `W_LOCALE_VALUE` | error / warning | a `strings` / `sprites` value is not a string (the runtime drops it; nested keys are the usual cause) |
|
|
174
|
+
| `E_MISSING_LOCALE_SPRITE` | error | an image a table's `sprites` names does not exist — in any locale: the table wins over a node's own texture, so its keyed nodes draw nothing in that locale; an `.svg` gets `E_SVG_*`, on the table at `sprites.<key>` |
|
|
175
|
+
| `E_LOCALE_KEY` | error | a `labelKey` (on a node or an instance) with no text — the node shows the key |
|
|
176
|
+
| `E_LOCALE_SCRIPT_KEY` | error | a key a script passes as a string literal to `tr` / `setTextKey` / `trPlural` with no text — the game shows the key (`trPlural`: neither `<key>.other` nor `<key>`, its last two steps). Every `.ts`/`.js` of the project but specs, tests and declarations (`*.spec.ts`, `*.test.ts`, `*.d.ts` — the project-script rule's exclusions, `PROJECT_SCRIPT_EXCLUDE_PATTERN`); the same scan as the editor's Localization Scan (`scanScriptLocalizationKeys` in the runtime). A computed key — a variable, an interpolated template — is skipped |
|
|
177
|
+
| `E_LOCALE_SPRITE_KEY` / `W_LOCALE_SPRITE_KEY` | error / warning | a `Sprite2D` `textureKey`, a `Button2D` `stateTextureKeys.<state>` (`texture<State>Key` on an instance) or a script's `trSprite('…')` with no `sprites` entry: an error for a `Sprite2D` with no `texture` of its own (it draws nothing), a warning otherwise (its own texture in every locale; a button keeps its skin; `trSprite` returns null) |
|
|
178
|
+
|
|
179
|
+
The tables, their images and the scripts' keys are checked on whole-project runs (as
|
|
180
|
+
`W_UNUSED_ASSET`); the keys a scene sets on every run. Nothing is said about keys when the project
|
|
181
|
+
has no tables or the default one does not load (that is its own error).
|
|
182
|
+
|
|
183
|
+
### Autoloads
|
|
184
|
+
|
|
185
|
+
`pix3project.yaml` `autoloads:` is checked on every run, against the rule every host builds them
|
|
186
|
+
by (the runtime's `normalizeAutoloads` / `autoloadComponentType`; the player, the editor's play
|
|
187
|
+
mode and `pix3 smoke` run them): each entry needs a `singleton` and a `scriptPath`, the file must
|
|
188
|
+
be a script entry under `scripts/` or `src/scripts/` exporting a `Script` class named like the
|
|
189
|
+
file. A disabled entry is not checked.
|
|
190
|
+
|
|
191
|
+
| Code | Severity | When |
|
|
192
|
+
| --- | --- | --- |
|
|
193
|
+
| `E_AUTOLOAD` | error | an autoload that would never run: no `singleton`/`scriptPath`, a repeated singleton, a script that is missing, outside the script folders, not an entry, or exporting no class named like the file (level 1, by text); at level 2, the class named like the file is not a `Script` |
|
|
194
|
+
|
|
195
|
+
## `pix3 sfx` — sound effects without the editor
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
pix3 sfx coin # -> audio/coin.wav in the project root
|
|
199
|
+
pix3 sfx "short high coin pickup" --out audio/pickup.wav --seed 7
|
|
200
|
+
pix3 sfx explosion --json # { path, res, durationMs, preset, seed, modifiers, bytes, peak, params }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Presets: `coin`, `jump`, `hit`, `explosion`, `powerup`, `click`. Free text picks the preset whose
|
|
204
|
+
keyword appears first (`boom`, `pickup`, `button`, `hurt`, …) and applies modifier words
|
|
205
|
+
(`high`/`low` pitch ×1.4/×0.7, `short`/`long` time ×0.65/×1.6, `soft`). `--seed <n>` renders a
|
|
206
|
+
deterministic variation (pitch ±15 %, times ±20 %, duty/vibrato/cutoff nudged); without it the
|
|
207
|
+
preset as tuned. Output is 44.1 kHz 16-bit mono PCM WAV — `AssetLoader.loadAudio` decodes it with
|
|
208
|
+
`decodeAudioData` like any `.ogg`/`.mp3`, and the export embeds it as `audio/wav`. `res` in the
|
|
209
|
+
JSON is the `res://` path when the file lands inside a project. Exit 2 on a bad argument or a
|
|
210
|
+
description that names no preset.
|
|
211
|
+
|
|
212
|
+
The synth (`src/sfx/synth.ts`, no dependencies) is one jsfxr-style voice: square / saw / sine /
|
|
213
|
+
triangle / noise, exponential pitch slide, pitch jump, vibrato, ADSR, resonant low-pass, DC
|
|
214
|
+
blocker, peak normalisation. It does not use the editor's `@txt2sfx/*` packages: their only
|
|
215
|
+
renderer drives an `OfflineAudioContext` the caller supplies, i.e. a native Web Audio module
|
|
216
|
+
(`node-web-audio-api`) in Node. Each preset renders in well under 10 ms; the whole command runs in
|
|
217
|
+
~0.12 s; files are 2–67 KiB (click 24 ms … explosion 772 ms).
|
|
218
|
+
|
|
219
|
+
## `pix3 character-compile` — a 2D character from frame PNGs
|
|
220
|
+
|
|
221
|
+
```text
|
|
222
|
+
pix3 character-compile art/goblin.yaml # writes into the project around the cwd
|
|
223
|
+
pix3 character-compile art/goblin.yaml --dry-run # the plan, nothing written
|
|
224
|
+
pix3 character-compile art/goblin.yaml --json # { ok, name, slug, animationPath, prefabPath, clips, files, warnings }
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The 1.x Store compiler (`character2d`, `pix3: src/services/library/character-compiler.ts`) as a
|
|
228
|
+
command. A spec (YAML or JSON; frame paths relative to the spec file) groups PNG frames into
|
|
229
|
+
`variant × state` clips:
|
|
230
|
+
|
|
231
|
+
```yaml
|
|
232
|
+
name: Goblin # display name, prefab file name
|
|
233
|
+
slug: goblin # sprite folder (default: from name)
|
|
234
|
+
anchor: { x: 0.5, y: 0.9 } # frame anchor, y from the top: the feet land on the node position
|
|
235
|
+
defaultVariant: sword # the pair the prefab starts on (default: the first clip's)
|
|
236
|
+
defaultState: idle
|
|
237
|
+
clips:
|
|
238
|
+
- { variant: sword, state: idle, fps: 10, frames: [sword/idle_1.png, sword/idle_2.png] }
|
|
239
|
+
- { variant: sword, state: attack, fps: 15, sequence: sword/attack } # sword/attack_<n>.png, numeric order
|
|
240
|
+
- { state: die, sequence: die/die } # no variant: clip "die"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Output, in the managed-sprite-folder layout:
|
|
244
|
+
`sprites/<slug>/<slug>.pix3anim` (clips `<variant>.<state>`, every frame with `texturePath`,
|
|
245
|
+
`anchor`, `sourceSize`), the frames copied beside it as `<variant>_<state>_<nnnn>.png`, and
|
|
246
|
+
`scenes/prefabs/<Name>.pix3scene` — an `AnimatedSprite2D` root (`sizeMode: native`) carrying
|
|
247
|
+
`core:CharacterVisual2D` (`variant`, `state`, `separator`); game code calls
|
|
248
|
+
`playState('attack', { restart: true })` / `setVariant('bow')`. `spriteDirectory` /
|
|
249
|
+
`prefabDirectory` / `separator` override the defaults. Defaults are proposals and print as
|
|
250
|
+
warnings (fps 12; `loop: false` for `attack`, `die`, `death`, `hit`, `hurt`); a sequence with gaps
|
|
251
|
+
warns, one with a duplicate number fails. Writes are all or nothing: a target that exists with
|
|
252
|
+
other bytes is refused (exit 1, nothing written) unless `--force`; identical bytes are `current`.
|
|
253
|
+
Frames must be PNG (the size comes from the header; the Node-runnable CLI does not load the
|
|
254
|
+
engine's image reader). Exit 0 = written or current, 1 = refused, 2 = usage / no project.
|
|
255
|
+
|
|
256
|
+
`src/character/character.spec.ts` holds the output to the runtime (`normalizeAnimationResource` of
|
|
257
|
+
the written `.pix3anim` is the identity; the compiler's types are assignable to
|
|
258
|
+
`AnimationResource`), to the managed folder's frame naming (`<clip>_<nnnn>.png`), and to
|
|
259
|
+
`pix3 validate` on a starter that instances the prefab; `character.headless.spec.ts` boots the
|
|
260
|
+
prefab through the real loader and drives idle → attack → restart → die → variant switch. The
|
|
261
|
+
format itself: the kit's `.claude/skills/pix3-scene-format/pix3anim.md`.
|
|
262
|
+
|
|
263
|
+
## `pix3 smoke` — run the game headless
|
|
264
|
+
|
|
265
|
+
`pix3 smoke [scene]` runs the game in Node — no browser, no editor, no dev server — for
|
|
266
|
+
`--frames N` fixed steps of 1/60 s (default 120) and reports everything that went wrong. It is the
|
|
267
|
+
behavioural check after `pix3 check` when no editor is connected; with the editor open,
|
|
268
|
+
`pix3_game_run` is the stronger one (it renders).
|
|
269
|
+
|
|
270
|
+
What runs: the project's scripts compiled with esbuild (as `validate` level 2 does, but bare
|
|
271
|
+
imports are bundled for real from the project's `node_modules` — Rapier, three addons — and only
|
|
272
|
+
what resolves nowhere becomes an empty module, reported as `W_SMOKE_STUBBED_IMPORT`), the scene
|
|
273
|
+
loaded from disk by the real `SceneLoader`, a real `SceneRunner` in manual time. Frame 0 is loading
|
|
274
|
+
and starting the scene (`onAttach`/`onStart`, then 128 event-loop turns so spawn chains land);
|
|
275
|
+
frame N is the N-th step. The run happens in a worker thread, so a script stuck in a loop is
|
|
276
|
+
stopped by `--timeout` (default 20 s).
|
|
277
|
+
|
|
278
|
+
Stubbed: rendering (a null renderer; post-processing never loads), audio (no Web Audio in Node),
|
|
279
|
+
input (no events), network (`scene.network` is null). Textures resolve empty, glTF models and
|
|
280
|
+
Spine skeletons referenced by nodes are not built; images a script loads through three's
|
|
281
|
+
`TextureLoader` "load" as blank 1×1 pictures (so preloaders finish). A small browser shim provides
|
|
282
|
+
`window` / `document` / canvas / `localStorage` / `matchMedia` / `requestAnimationFrame`; any other
|
|
283
|
+
browser property a script reads is recorded, and an error right after such a read is
|
|
284
|
+
`E_SMOKE_DOM` with the read attached (`domAccess`).
|
|
285
|
+
|
|
286
|
+
Scene: the argument (`res://`, project-relative or a path) runs that one scene. With no argument
|
|
287
|
+
several run, one after another from one bundle, a line each (`src/smoke/select-scenes.ts`):
|
|
288
|
+
|
|
289
|
+
1. In a git work tree with uncommitted changes (staged, unstaged or untracked): the **top-level**
|
|
290
|
+
scenes those changes reach — the scene itself, a prefab / `scenes/ui/` overlay it instances, a
|
|
291
|
+
script exporting a class it attaches as `user:X`, a `res://` file it names. `--changed` forces
|
|
292
|
+
this (exit 2 without git or without changes). Changes under `node_modules/`, `.yalc/` (a linked
|
|
293
|
+
package copy), `.git/`, `.pix3/`, `dist/`, `.vite/`, `.cache/` are not project changes and are
|
|
294
|
+
ignored, and a `*.spec.ts` / `*.test.ts` next to the scripts is not game code — a consumer
|
|
295
|
+
project's `.yalc/@pix3/runtime/**` and its unit tests used to read as hundreds of changed
|
|
296
|
+
scripts that reach no scene and widen every run to everything.
|
|
297
|
+
2. Otherwise — no git, nothing changed, a changed scene/script that reaches no top-level scene (a
|
|
298
|
+
helper module, an unused prefab), or a changed `pix3project.yaml` — **every** top-level scene
|
|
299
|
+
(not instanced by another, not under `prefabs/` / `ui/`, not a `.prefab` — those are only
|
|
300
|
+
instanced, and a whole-project `pix3 validate` checks them as prefabs),
|
|
301
|
+
`scenes/main.pix3scene` first (where the starters and the 1.x recipes put the game), then
|
|
302
|
+
`defaultExportScenePath`, then by path.
|
|
303
|
+
`--all` forces this.
|
|
304
|
+
|
|
305
|
+
It never picks `defaultExportScenePath` alone any more: in every 1.x recipe that is the menu, input is
|
|
306
|
+
empty, PLAY is never pressed, and a game whose `onStart` throws used to smoke green.
|
|
307
|
+
|
|
308
|
+
| Code | |
|
|
309
|
+
| --- | --- |
|
|
310
|
+
| `E_SMOKE_SCRIPT` | A script hook threw (`onAttach`/`onStart`/`onUpdate`…); the engine disabled that component |
|
|
311
|
+
| `E_SMOKE_DOM` | …and it was a browser API the shim does not provide (`domAccess` names it) |
|
|
312
|
+
| `E_SMOKE_TICK` / `E_SMOKE_COMMAND` | The engine tick / a game command handler threw |
|
|
313
|
+
| `E_SMOKE_CONSOLE_ERROR` | `console.error` (not the engine re-reporting one of the above) |
|
|
314
|
+
| `E_SMOKE_UNHANDLED` | An unhandled promise rejection, or a timer callback that threw |
|
|
315
|
+
| `E_SMOKE_LOAD` | The scene failed to load (missing prefab, invalid scene) |
|
|
316
|
+
| `E_SMOKE_SCRIPT_COMPILE` / `E_SMOKE_SCRIPT_IMPORT` | Scripts do not compile / throw at module top level |
|
|
317
|
+
| `W_SMOKE_MISSING_RESOURCE` | A `res://` file does not exist (the node loads without it) |
|
|
318
|
+
| `W_SMOKE_PENDING_COMPONENT` | A component type is not registered — it never runs |
|
|
319
|
+
| `W_SMOKE_CONSOLE_WARN`, `W_SMOKE_LOADER`, `W_SMOKE_STUBBED_IMPORT`, `W_SMOKE_STOPPED` | `console.warn`, loader warnings, an import replaced by `{}`, the game stopped itself |
|
|
320
|
+
|
|
321
|
+
Exit 0 = no errors (warnings allowed), 1 = errors, 2 = could not run (with several scenes, the
|
|
322
|
+
worst run decides): `E_SMOKE_NO_PROJECT`,
|
|
323
|
+
`E_SMOKE_NO_SCENE`, `E_SMOKE_BUNDLE`, `E_SMOKE_UNSUPPORTED` (no esbuild), `E_SMOKE_TIMEOUT`,
|
|
324
|
+
`E_SMOKE_CRASH`, `E_EDITOR_UNSYNCED` (a live editor named by `.pix3/dev.json` did not flush its
|
|
325
|
+
unsaved scenes first; `--no-sync` reads the disk as it is), `E_RUNTIME_VERSION` (the version gate:
|
|
326
|
+
the project installs another `@pix3/runtime` than the one bundled into this CLI, so a run would
|
|
327
|
+
test an engine the game does not ship). The human report prints the `game` snapshot whole — on one line when it is short, pretty-printed
|
|
328
|
+
and indented otherwise, cut only past 4 000 characters (`--json` always carries it whole).
|
|
329
|
+
`--json` prints `{ ok, scene, frames, framesRequested, firstFrameOk, errors:
|
|
330
|
+
[{ code, frame, script?, nodeId?, nodeName?, phase?, message, stack?, domAccess? }], warnings,
|
|
331
|
+
nodes: { start, end }, timingsMs: { compile, load, firstFrame, step: { total, mean, p95, max },
|
|
332
|
+
total }, scripts, domMissing, notes, game, logs }` — `game` is the `registerGameDebug` provider's
|
|
333
|
+
`snapshot()` at the end (when the game registers one), `logs` the first `console.log` lines,
|
|
334
|
+
frame-stamped. Stacks are source-mapped to the project's files. With no scene argument `--json`
|
|
335
|
+
prints `{ ok, selection: "changed" | "all", reason, changed?, runs: [<that report, per scene>] }`.
|
|
336
|
+
|
|
337
|
+
A green smoke run means nothing threw for N frames without input. It does not mean the game
|
|
338
|
+
plays: nothing was tapped, nothing was drawn.
|
|
339
|
+
|
|
340
|
+
## `pix3 tree` — find your way around a scene
|
|
341
|
+
|
|
342
|
+
`pix3 tree <scene>` prints one line per node, indented by depth, from the YAML alone (no loader,
|
|
343
|
+
no project code — instant):
|
|
344
|
+
|
|
345
|
+
```text
|
|
346
|
+
scenes/main.pix3scene — 12 nodes
|
|
347
|
+
Group2D#game-root "Game Root" size=1080x1920 layout=stretch/stretch components=[user:GameRules, user:TouchRules]
|
|
348
|
+
CanvasLayer2D#hud "HUD" size=1080x1920 layout=stretch/stretch components=[user:ScoreHud]
|
|
349
|
+
Label2D#score-label "Score Label" text="SCORE 0" pos=(-340,850) layout=left/top
|
|
350
|
+
Group2D#result-overlay "Result Overlay" ↳ instance res://scenes/ui/result.pix3scene (1 property) hidden
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
`--depth N` stops N levels below the roots (cut subtrees end in `… +K below`); `--types A,B` keeps
|
|
354
|
+
nodes of those types (or carrying those components; `instance` = prefab instances) with their
|
|
355
|
+
ancestors as `·` context lines; `--props` adds, under each node, the properties that differ from
|
|
356
|
+
the node type's defaults (read from a bare instance of the runtime class, through the disk-format
|
|
357
|
+
table — the one step that loads the runtime bundle); `--json` gives the same as nested
|
|
358
|
+
`{ id, type, name, depth, position?, size?, layout?, margins?, resolved?, hidden?, text?, groups?,
|
|
359
|
+
components, instance?: { path, rootType?, rootName?, overrides, properties }, props?, children }`.
|
|
360
|
+
On an instance
|
|
361
|
+
`overrides` counts every `overrides.byLocalId.*.properties` key (edits to nodes inside the prefab)
|
|
362
|
+
and `properties` the instance node's own `properties` keys (applied to the prefab root); the line
|
|
363
|
+
prints `(2 overrides, 1 property)`, or `(no overrides)` when both are 0.
|
|
364
|
+
|
|
365
|
+
A node anchored in the margin form (W21: `layout: { enabled, horizontalAlign, verticalAlign,
|
|
366
|
+
left?, right?, top?, bottom? }`, every side the alignment keeps written) holds `0` in the file for
|
|
367
|
+
the position component of an anchored axis and no `width`/`height` for a stretched one — the
|
|
368
|
+
margins are the rect. The line shows the margins beside the layout and, as `pos` / `size`, the
|
|
369
|
+
rect they give at the design size (the parent's size; `viewportBaseSize` for a root), the way
|
|
370
|
+
`Node2D`'s layout pass computes it; `?` where the file does not say enough (a parent or the node
|
|
371
|
+
without a size in the file):
|
|
372
|
+
|
|
373
|
+
```text
|
|
374
|
+
Group2D#hud size=1080x1920 layout=stretch/stretch(left=0,right=0,top=0,bottom=0)
|
|
375
|
+
Group2D#bar pos=(0,-920) size=1000x40 layout=stretch/bottom(left=40,right=40,bottom=20)
|
|
376
|
+
Group2D#knob pos=(480,5) size=20x20 layout=right/none(right=10)
|
|
377
|
+
Sprite2D#unsized pos=(?,70) layout=left/none(left=10)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
`--json` keeps `position` / `size` as the file has them and adds `margins` and
|
|
381
|
+
`resolved: { position: [x, y], size: [w, h] }` (`null` for `?`). A pre-W21 anchor (no margins in
|
|
382
|
+
the file) prints its file position as before.
|
|
383
|
+
|
|
384
|
+
`pix3 tree` with no scene is the project overview: manifest facts, the `user:` scripts, and every
|
|
385
|
+
scene/prefab/overlay with its node count, node types, components and instances (entry scene
|
|
386
|
+
marked `*`). Exit 0, 1 when a scene does not parse, 2 for bad arguments or a missing file.
|
|
387
|
+
|
|
388
|
+
## `pix3 kit` — the agent kit
|
|
389
|
+
|
|
390
|
+
`pix3 kit` installs the agent kit into an existing project (one made without it, or after a CLI
|
|
391
|
+
upgrade); `pix3 new` runs the same step. What lands in the project:
|
|
392
|
+
|
|
393
|
+
| Path | What |
|
|
394
|
+
| --- | --- |
|
|
395
|
+
| `AGENTS.md` | Root rules (read by Codex, Cursor, …). If the project already has its own, it is kept and the kit goes to `AGENTS.pix3.md` (the command prints the line to add). |
|
|
396
|
+
| `CLAUDE.md` | `@AGENTS.md` (plus `@AGENTS.pix3.md` in that case). A `CLAUDE.md` of the project's own is never touched — the command prints the line to add. |
|
|
397
|
+
| `.claude/skills/pix3-{scene-format,nodes,scripts,verify,editor}/SKILL.md` + `reference.md` | Skills loaded on demand; the `reference.md` files are generated from `docs/` and the runtime's registry; `pix3-scene-format/pix3anim.md` is the `.pix3anim` format |
|
|
398
|
+
| `design/tests/bots/pix3-test-bot.d.ts` | Global types for bot policies (`Pix3TestBot`, `BotPolicy`, …): a policy is `export default { name, tick(bot) { … } } satisfies BotPolicy`, and the starter's `tsconfig.json` includes `design/tests`, so `pix3 check` type-checks it. The editor writes no project files; this is where the 1.x editor-written declaration went |
|
|
399
|
+
| `.gitignore` | `.pix3/*` with `!.pix3/kit-manifest.json` and `!.pix3/template.json` appended when no line ignores `.pix3` yet; existing content kept |
|
|
400
|
+
| `tsconfig.json`, `.pix3/tsconfig.check.json`, `.pix3/types/` | Only without a `tsconfig.json` of the project's own (see below) |
|
|
401
|
+
| `pix3project.yaml` | `metadata.agentKit: { version, files }` (everything else as written) |
|
|
402
|
+
| `.pix3/kit-manifest.json` | sha256 of every file the kit wrote |
|
|
403
|
+
|
|
404
|
+
Without `--update`, existing files are never replaced (missing ones are written; an unchanged file
|
|
405
|
+
from an older kit is reported `outdated`). `--update` replaces every kit file whose bytes still
|
|
406
|
+
match the hash in `.pix3/kit-manifest.json` and **skips** (and reports) every file edited since.
|
|
407
|
+
The starters ignore `.pix3/*` but not `.pix3/kit-manifest.json` (nor `.pix3/template.json`), so a
|
|
408
|
+
clone keeps the manifest. A project whose `.gitignore` ignores all of `.pix3/` (an older starter, or by
|
|
409
|
+
hand) loses it on a fresh clone, and every differing kit file then counts as
|
|
410
|
+
edited — replace that line with the three of the starter.
|
|
411
|
+
|
|
412
|
+
**Generated, not copied.** `scripts/build-kit.mjs` (run at `prepack`; a repo checkout regenerates
|
|
413
|
+
`kit/` automatically whenever an input changed) expands the templates in `kit-src/`: hand-written
|
|
414
|
+
prose plus `{{include:<repo path>#<heading>}}` / `{{include:<repo path>@<paragraph>}}` directives
|
|
415
|
+
over `docs/pix3-specification.md`, `docs/node-types-reference.md`, `docs/nodes-and-systems.md`,
|
|
416
|
+
`packages/cli/kit-includes/engine-api-map.md` and this README, and `{{generated:…}}` blocks
|
|
417
|
+
computed from code (the `core:` component table from the runtime's registry). Syntax:
|
|
418
|
+
`src/kit/generate.ts`. `src/kit.spec.ts` builds the kit and fails on drift: an unresolved
|
|
419
|
+
directive, a `pix3` command or flag not in the usage text, an `npm run` script the starter does
|
|
420
|
+
not have, a `pix3_*` tool, parameter or sync reason the bridge (`bridge-tools.ts`) does not have, a
|
|
421
|
+
retired 1.x in-editor tool, a verdict phrase `GameTestService` does not produce, a diagnostic code
|
|
422
|
+
no command emits, a node type the loader does not know, a property in the nodes skill's tables
|
|
423
|
+
that the disk-format descriptor (`packages/runtime/src/core/scene-disk-format.ts`) does not
|
|
424
|
+
accept, a `core:` component that does not exist, and a `.pix3anim` reference (`pix3anim.md`)
|
|
425
|
+
whose interfaces, fields, optionality, types or "Omitted →" defaults differ from
|
|
426
|
+
`packages/runtime/src/core/AnimationResource.ts` and `normalizeAnimationResource`, and bot-policy
|
|
427
|
+
types missing a member of `Pix3TestBot` / `BotPolicy` / `BotNodeView`
|
|
428
|
+
(`packages/editor-core/src/services/game-test/game-bots.ts`).
|
|
429
|
+
|
|
430
|
+
### `pix3 kit --migrate` — a 1.x project's kit to 2.x
|
|
431
|
+
|
|
432
|
+
Plan §A.4 step 2. It migrates **the kit only** (`src/kit/migrate.ts`), and reports every change:
|
|
433
|
+
|
|
434
|
+
| What | How |
|
|
435
|
+
| --- | --- |
|
|
436
|
+
| `.mcp.json` | the 1.x `pix3 mcp --workspace` server (`npx -y @pix3/cli@1.x mcp …`, or the dev `node …/packages/cli/src/index.ts mcp`) is removed; other servers stay; a file left empty is deleted |
|
|
437
|
+
| Retired kit files | files in `.pix3/kit-manifest.json` that the 2.x kit no longer ships: deleted when unchanged, kept and reported when edited |
|
|
438
|
+
| `pix3project.yaml` | `metadata.pix3Hybrid` (the 1.x cloud link) removed, its value printed; `metadata.agentKit` rewritten |
|
|
439
|
+
| The kit | installed with `--update` semantics: unchanged 1.x kit files replaced, edited ones kept; an edited one that still mentions `pix3 mcp` / `pix3 serve` / `pix3 read` / the live channel / the in-editor tools is flagged `STILL 1.x` and the 2.x text written to `.pix3/kit-migrate/<path>` for a manual merge |
|
|
440
|
+
|
|
441
|
+
Never touched: anything outside the project (`~/.codex/config.toml` — the report says to delete a
|
|
442
|
+
1.x `[mcp_servers.pix3]` table), a project `.codex/config.toml` (reported when it runs `pix3 mcp`),
|
|
443
|
+
the project's own `AGENTS.md` / `CLAUDE.md` (an edited copy of the 1.x kit's `AGENTS.md` is
|
|
444
|
+
reported), scenes, scripts, assets, `package.json`, `vite.config.*`. The 2.x agent config is
|
|
445
|
+
`pix3 agent-setup`'s (the report says to run it). Moving a 1.x project onto Vite (`pix3()` in
|
|
446
|
+
`vite.config`, plan §A.4 step 3) is optional and by hand; the report notes a project without
|
|
447
|
+
`@pix3/vite-plugin`. A second run finds nothing to migrate. Spec: `src/kit/migrate.spec.ts`
|
|
448
|
+
(a 1.x-shaped starter, edits kept, idempotence, a copy of `../DeepCore` when present).
|
|
449
|
+
|
|
450
|
+
### Script types (`.pix3/types/`)
|
|
451
|
+
|
|
452
|
+
`scripts/build-runtime-types.mjs` (at `prepack`; rebuilt on demand in a checkout when the runtime
|
|
453
|
+
sources change) runs `tsc -p packages/runtime/tsconfig.types.json` (declarations of what
|
|
454
|
+
`src/index.ts` reaches — no specs, samples or `testing/`) into `runtime-types/@pix3/runtime/`, and
|
|
455
|
+
copies `@types/three` (without its `node_modules`) to `runtime-types/@types/three/` — the runtime's
|
|
456
|
+
public types extend three.js. The tarball carries that tree packed into one file,
|
|
457
|
+
`dist/runtime-types.json` (see "Package layout" below); the installed CLI expands it on first use. `lit` (re-exported `property`/`state` decorators), `postprocessing`
|
|
458
|
+
and the Spine runtime are not shipped: they are only reached from inside declaration files, which
|
|
459
|
+
`skipLibCheck` leaves alone (they type as `any`). In a project without its own `tsconfig.json`:
|
|
460
|
+
|
|
461
|
+
```text
|
|
462
|
+
.pix3/types/@pix3/runtime/** the runtime's .d.ts (+ package.json with the version)
|
|
463
|
+
.pix3/types/@types/three/** three.js types
|
|
464
|
+
.pix3/types/version.json which CLI build wrote them
|
|
465
|
+
.pix3/tsconfig.check.json paths → ./types/…, strict, noEmit, skipLibCheck, lib ES2022 + DOM,
|
|
466
|
+
include ../scripts/**/*.ts and ../src/scripts/**/*.ts
|
|
467
|
+
tsconfig.json { "extends": "./.pix3/tsconfig.check.json" } — so an IDE sees them
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## `pix3 gap` — what the agent had to work around
|
|
471
|
+
|
|
472
|
+
Plan §G.3 «Чего не хватает?», §G.4. `pix3 gap "<what was missing>" --kind
|
|
473
|
+
capability|tool|node|doc|other [--detail <text>] [--context <file or task>] [--agent <name>]`
|
|
474
|
+
appends one line to the project's `.pix3/gaps.jsonl`:
|
|
475
|
+
|
|
476
|
+
```json
|
|
477
|
+
{"ts":"2026-10-10T12:00:00.000Z","agent":"claude-code","kind":"node","summary":"no 9-slice panel node","detail":"built from 9 Sprite2D","context":"scenes/ui/shop.pix3scene"}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
`agent` comes from `--agent`, else the environment (`CLAUDECODE=1` → `claude-code`, a `CODEX_*`
|
|
481
|
+
variable → `codex`), else it is left out; the summary is one line of ≤200 characters. One
|
|
482
|
+
`O_APPEND` write per record, so two agents never interleave. `pix3 gap --list [--json]` prints
|
|
483
|
+
them. The kit's `AGENTS.md` tells the agent to record a gap only after it worked around
|
|
484
|
+
something Pix3 lacks — not its own mistakes or the game's features. A CLI command rather than a
|
|
485
|
+
bridge tool: it needs no editor, and on a Remote SSH host it runs where the project is. `.pix3/`
|
|
486
|
+
is gitignored in the starters: the file stays with the checkout.
|
|
487
|
+
|
|
488
|
+
## `pix3 editor` — the editor in Chrome, for the agent
|
|
489
|
+
|
|
490
|
+
Plan §D.3 / §D.4 / §D.5. Idempotent; run it from the project (or `--project <dir>`):
|
|
491
|
+
|
|
492
|
+
1. **Dev server.** `.pix3/dev.json` (written by `@pix3/vite-plugin` while Vite listens: `url`,
|
|
493
|
+
`editorUrl`, `port`, `pid`) is probed with `GET <url>__pix3/api/hello`. A live server is
|
|
494
|
+
reused; a stale record is ignored and `node_modules/vite/bin/vite.js` is started **detached**
|
|
495
|
+
(`process.execPath`, `cwd` = the project, output in `.pix3/dev.log`), and the command waits
|
|
496
|
+
up to 15 s for the new `dev.json`. `--port <n>` passes `--port --strictPort` to Vite.
|
|
497
|
+
`--stop` sends SIGTERM to the recorded pid (and drops a record whose process is gone).
|
|
498
|
+
2. **Chrome, owned.** `PIX3_CHROME` names the binary (a wrapper script works if it `exec`s
|
|
499
|
+
Chrome: the pipe is fds 3 and 4), else the platform's usual place (`google-chrome` /
|
|
500
|
+
`chromium` on the PATH, `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`,
|
|
501
|
+
`%ProgramFiles%\Google\Chrome\Application\chrome.exe`). `pix3 editor` starts a **detached
|
|
502
|
+
Chrome owner** (`pix3 __chrome-owner`, log in `~/.pix3/chrome-owner.log`) and returns once its
|
|
503
|
+
proxy answers; the owner launches Chrome with `--app=<editorUrl>
|
|
504
|
+
--user-data-dir=~/.pix3/chrome --remote-debugging-pipe` — **no debugging port** — no first run,
|
|
505
|
+
no default-browser check, background throttling off (`--disable-background-timer-throttling
|
|
506
|
+
--disable-renderer-backgrounding --disable-backgrounding-occluded-windows`). The owner lives
|
|
507
|
+
exactly as long as Chrome: Chrome gone → the owner closes every client, removes
|
|
508
|
+
`~/.pix3/chrome.json` and exits; the owner gone (killed) → the pipe closes and Chrome exits
|
|
509
|
+
with it. `--stop-chrome` asks the owner to close Chrome. Then, launched or reused, the editor
|
|
510
|
+
tab is checked in the proxy's `/json/list` and, when it is not there, opened through the proxy
|
|
511
|
+
(`Target.createTarget`: a new window, a tab in a headless Chrome) and checked again — a binary
|
|
512
|
+
that ignores `--app` (`PIX3_CHROME` pointing at a `chrome-headless-shell`) still gets its tab,
|
|
513
|
+
and `pix3 editor` fails rather than print success without one. A second project gets its tab
|
|
514
|
+
the same way: the profile is never launched a second time (a headless one would start a
|
|
515
|
+
second Chrome that nobody owns and `--stop-chrome` would not end). `--headless` opens the URL
|
|
516
|
+
as a plain tab with `--headless=new` (headless Chrome ignores `--app`). `--no-chrome` stops after the server;
|
|
517
|
+
under `SSH_CONNECTION` Chrome is not launched — see [Remote SSH](#remote-ssh--vs-code-on-a-remote-host-chrome-on-your-machine).
|
|
518
|
+
3. **The token proxy (9333).** The owner serves `ws://127.0.0.1:9333/pix3` (the browser target;
|
|
519
|
+
`…/pix3/page/<targetId>` for one page) and `GET /json/version`, `/json/list` (`/json`) on
|
|
520
|
+
loopback. Every request needs `Authorization: Bearer <token>`, the token of
|
|
521
|
+
`~/.pix3/cdp-token` (32 random bytes, base64url, mode 0600, created once by whichever command
|
|
522
|
+
needs it first; delete the file to rotate, then `agent-setup --repair` and a new Chrome);
|
|
523
|
+
a request with an `Origin` (any web page) or a non-loopback `Host` is refused (403), a
|
|
524
|
+
missing or wrong token is 401 — a WebSocket is refused before the upgrade. Several clients
|
|
525
|
+
share the pipe (chrome-devtools-mcp, a harness, `pix3 editor` itself): each gets its own root
|
|
526
|
+
session (`Target.attachToBrowserTarget`, or `attachToTarget {flatten}` for a page), message
|
|
527
|
+
ids are remapped per client, events go only to the client owning their session, another
|
|
528
|
+
client's session is "not found", a client that leaves is detached with its child sessions.
|
|
529
|
+
4. **The port.** The check sends `X-Pix3-Challenge` and **no token**: a proxy answering with the
|
|
530
|
+
proof that it knows our token (`X-Pix3-Proof`, the HMAC of [Remote SSH](#remote-ssh--vs-code-on-a-remote-host-chrome-on-your-machine))
|
|
531
|
+
= ours → reused (only then is it asked, with the token, for its tabs; when the editor tab is
|
|
532
|
+
open there, nothing is launched). A proxy without that proof (another user's, another
|
|
533
|
+
`PIX3_HOME`, a squatter), a foreign DevTools endpoint or any other listener — none of which
|
|
534
|
+
ever sees the token — → the next of 9334–9339, with a line per skipped port, the chosen one
|
|
535
|
+
recorded in `~/.pix3/chrome.json`, and the reminder to run `pix3 agent-setup --repair` (a
|
|
536
|
+
running Codex / Claude Code session keeps the old endpoint: new thread). A plain DevTools port
|
|
537
|
+
with a `/__pix3/` page (or on the recorded port) is the P1 launch (open port, no token): it
|
|
538
|
+
holds the profile, so `pix3 editor` stops and says to close that Chrome. When the port is the
|
|
539
|
+
usual one but the project's `.mcp.json` / `.codex/config.toml` entry is not the current launch
|
|
540
|
+
(an older one — `--wsHeaders` with the token on its command line, `--browserUrl`), or the
|
|
541
|
+
`~/.pix3/cdp-mcp.json` it names holds another port or token, it says to run
|
|
542
|
+
`pix3 agent-setup --repair`. `--cdp-port <n>` moves the preferred port
|
|
543
|
+
(test harnesses). `PIX3_HOME` relocates `~/.pix3`.
|
|
544
|
+
|
|
545
|
+
Keepalive is the editor's: every bridge call keeps its loops running for 60 s, agent play until
|
|
546
|
+
it stops (`AgentKeepaliveService`); the Chrome flags above keep Chrome from throttling the tab.
|
|
547
|
+
|
|
548
|
+
## `pix3 agent-setup` — chrome-devtools-mcp for Codex and Claude Code
|
|
549
|
+
|
|
550
|
+
Plan §D.6. Writes project-level config, idempotently:
|
|
551
|
+
|
|
552
|
+
- Claude Code — `.mcp.json`, `mcpServers["pix3-browser"]`, other servers kept;
|
|
553
|
+
- Codex — `.codex/config.toml`, the `[mcp_servers.pix3-browser]` table (`startup_timeout_sec =
|
|
554
|
+
20`, `tool_timeout_sec = 300`: `pix3_game_run` outlives the 60 s default), replaced as a whole,
|
|
555
|
+
every other byte of the file kept.
|
|
556
|
+
|
|
557
|
+
The entry is `npx -y chrome-devtools-mcp@1.10.1 --categoryExperimentalThirdParty=true
|
|
558
|
+
--experimentalVision=true --config=<~/.pix3/cdp-mcp.json>` (`cmd /c npx …` on Windows;
|
|
559
|
+
`experimentalVision` enables `click_at {x, y}`, the agent's input at the coordinates
|
|
560
|
+
`pix3_scene` returns as `screen`). `~/.pix3/cdp-mcp.json` (mode 0600, written by this command,
|
|
561
|
+
rewritten whenever the port or the token moves) holds `wsEndpoint` = `ws://127.0.0.1:<port>/pix3`
|
|
562
|
+
and `wsHeaders` = `{"Authorization":"Bearer <token>"}`: 1.10.1's `--config` reads a JSON object
|
|
563
|
+
with the flags' own options and coercions (`build/src/config/mcp-options.js`; `wsEndpoint` /
|
|
564
|
+
`wsHeaders` in `build/src/config/browser-options.js`, `wsHeaders` parsed as a JSON object); with
|
|
565
|
+
`wsEndpoint` puppeteer connects to that WebSocket only, sending the headers on the upgrade, and
|
|
566
|
+
makes no `/json/*` request. So **the token is in no project file and on no command line** (every
|
|
567
|
+
user of a box can read every process's command line). The version is **pinned**:
|
|
568
|
+
the third-party tool category is experimental and may move in a minor release, so it changes
|
|
569
|
+
only after the S4 transport run is repeated (`CHROME_DEVTOOLS_MCP_VERSION` in
|
|
570
|
+
`src/agent-setup/config.ts`); the inline fallback (`window.__PIX3_DEBUG__.call`) works on any
|
|
571
|
+
version. The port is `--cdp-port`, else what `~/.pix3/chrome.json` recorded, else 9333; the token
|
|
572
|
+
is `~/.pix3/cdp-token` (created here if `pix3 editor` has not run yet). `--remote` is the same
|
|
573
|
+
shape for an agent on a Remote SSH host, with `~/.pix3/remote-cdp.json` (see Remote SSH below).
|
|
574
|
+
|
|
575
|
+
The project files name a path in this machine's home, so the starters' `.gitignore` keeps
|
|
576
|
+
listing `.mcp.json` and `.codex/config.toml`. An entry that already matches is `up to date`; one
|
|
577
|
+
that differs (another version or config file, the P1 `--browserUrl` launch or the earlier
|
|
578
|
+
`--wsEndpoint` + `--wsHeaders` launch that carried the token, each named as such) is reported as
|
|
579
|
+
drift and exit 1 — `--repair` rewrites it after copying the file to `<file>.bak` (the `.bak` of a
|
|
580
|
+
`--wsHeaders` entry still holds the token: delete it); that is the migration of a P1 or an early
|
|
581
|
+
P2 project.
|
|
582
|
+
Nothing is done to a sandbox: Codex must be allowed to reach `127.0.0.1` once when it asks.
|
|
583
|
+
The alternative for every project at once is printed (`claude mcp add --scope user …`, or the
|
|
584
|
+
table in `~/.codex/config.toml`).
|
|
585
|
+
|
|
586
|
+
**The proxy (plan §D.5).** Chrome opens no debugging port; the only way in is the proxy with the
|
|
587
|
+
token, so another local user, a web page or a process that cannot read `~/.pix3/cdp-token`
|
|
588
|
+
cannot drive the editor's Chrome (checked: `../pix3-core-spikes/editor-e2e/gate-p2.mjs`). Same-user
|
|
589
|
+
processes can read the token file — the boundary is the user account, as for any file in the
|
|
590
|
+
home directory. Chrome on macOS / Windows is untested (`.plans/agent-bridge.md` debt).
|
|
591
|
+
|
|
592
|
+
## Remote SSH — VS Code on a remote host, Chrome on your machine
|
|
593
|
+
|
|
594
|
+
Plan §E.3. You work in VS Code connected to a Linux box over **Remote SSH**: the project, Vite and
|
|
595
|
+
the coding agent (Claude Code, Codex) run there; the browser runs on your machine. The editor
|
|
596
|
+
reaches your browser through VS Code's port forward; the agent reaches that browser through an SSH
|
|
597
|
+
`RemoteForward` of the CDP token proxy, back onto the remote host's loopback.
|
|
598
|
+
|
|
599
|
+
1. **On the remote host** (VS Code's terminal, in the project): `npm run editor`. Under
|
|
600
|
+
`SSH_CONNECTION` it starts (or reuses) the dev server, launches no Chrome, and prints what to
|
|
601
|
+
do on your machine — with the one remote port this host uses for the forward (9333, or
|
|
602
|
+
`--cdp-port <n>`, which it stores):
|
|
603
|
+
|
|
604
|
+
```text
|
|
605
|
+
On your machine, once — ~/.ssh/config, under the Host you connect to, then reconnect:
|
|
606
|
+
RemoteForward 127.0.0.1:9333 127.0.0.1:9333
|
|
607
|
+
ExitOnForwardFailure yes
|
|
608
|
+
On your machine, each session — VS Code forwards port 5173 (its Ports tab shows the local address):
|
|
609
|
+
npx @pix3/cli@<version> editor --chrome-only --url http://localhost:5173/__pix3/ --remote-port 9333
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
VS Code forwards the dev server's port by itself (it sees the URL in the terminal; the Ports
|
|
613
|
+
tab lists it). When the local port is not the same (5173 was taken on your machine), use the
|
|
614
|
+
local address in `--url`.
|
|
615
|
+
2. **On your machine, once:** add the two lines to `~/.ssh/config` under that `Host`, close the
|
|
616
|
+
remote window and connect again. The first port is the one on the remote host — one port,
|
|
617
|
+
configured on both sides, never "the next free one": if another user of that host holds it,
|
|
618
|
+
the forward must fail visibly (`pix3 editor` there says the port is held; pick another with
|
|
619
|
+
`--cdp-port <n>` there, the same `<n>` in `RemoteForward` and in `--remote-port`); the second
|
|
620
|
+
is your proxy (9333 unless `pix3 editor` said it had to use another). `ExitOnForwardFailure
|
|
621
|
+
yes` is not optional: without it a connection whose forward failed — because someone else on
|
|
622
|
+
the remote host took the port — comes up anyway, and the agent would hand its token to
|
|
623
|
+
whoever holds the port.
|
|
624
|
+
3. **On your machine, each session:** the `npx @pix3/cli … editor --chrome-only --url …` line.
|
|
625
|
+
No project needed there: it checks that the URL answers as a Pix3 editor, starts Chrome behind
|
|
626
|
+
the token proxy (as `pix3 editor` always does), opens the editor, and records `--remote-port`
|
|
627
|
+
(default 9333) in `~/.pix3/cdp-forward-ports.json`: the proxy proves its token for the ports
|
|
628
|
+
listed there and for its own, nothing else. It then prints one line
|
|
629
|
+
that copies its token to the remote host (`--ssh <host>` fills the host in):
|
|
630
|
+
|
|
631
|
+
```text
|
|
632
|
+
ssh <host> 'umask 077 && mkdir -p ~/.pix3 && cat > ~/.pix3/remote-cdp-token' < ~/.pix3/cdp-token
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Run it once (again only after the token changes). The token goes from your token file through
|
|
636
|
+
the SSH channel into a 0600 file on the remote host: it is never printed, typed, pasted or put
|
|
637
|
+
on a command line.
|
|
638
|
+
4. **On the remote host, once:** `npx pix3 agent-setup --remote`, then a new agent thread. It
|
|
639
|
+
asks the configured port (the one `pix3 editor` stored, or `--cdp-port <n>`) for **proof**
|
|
640
|
+
that it knows the token (`X-Pix3-Challenge` + `X-Pix3-Proof-Scope: forward` → `X-Pix3-Proof`,
|
|
641
|
+
an HMAC the proxy computes over the challenge **and the port the client dialled**; the token
|
|
642
|
+
itself is never sent to a port that has not proven it), writes `~/.pix3/remote-cdp.json`
|
|
643
|
+
(0600: the endpoint `ws://127.0.0.1:<port>/pix3` and the token, read by chrome-devtools-mcp's
|
|
644
|
+
`--config`), and the project's `.mcp.json` / `.codex/config.toml` entry `npx -y
|
|
645
|
+
chrome-devtools-mcp@1.10.1 … --config=<that file>`, which holds no secret. Something on the
|
|
646
|
+
port that cannot give the proof gets nothing written (exit 1: a squatter waiting for the
|
|
647
|
+
token, or your proxy not told the port — `--remote-port`); a port nothing answers on yet is
|
|
648
|
+
written with a warning. Bound to the port, a proof cannot be relayed: a squatter on 9333 that
|
|
649
|
+
passes the challenge to your real forward on 9334 gets an answer for 9334, which the agent's
|
|
650
|
+
side rejects.
|
|
651
|
+
`npm run editor` on the remote host says whether the forward is live (`CDP forward: live on
|
|
652
|
+
127.0.0.1:9333`).
|
|
653
|
+
|
|
654
|
+
The agent finds its tab by `.pix3/dev.json`: `publicEditorUrl` is the address your browser
|
|
655
|
+
reached the dev server at — learnt from the editor tab's `Origin` when it differs from the
|
|
656
|
+
server's own (a port forward that is not 1:1), or set with `PIX3_PUBLIC_URL=http://localhost:5174`
|
|
657
|
+
in the environment of the dev server; absent, `editorUrl` is the address.
|
|
658
|
+
|
|
659
|
+
Why it is safe on a shared host: the remote host's loopback is reachable by every user of it, and
|
|
660
|
+
so is the forwarded port — that is what the token is for (no token → 401). On the remote host the
|
|
661
|
+
token lives only in two 0600 files of your home; not in the project, not in any process's command
|
|
662
|
+
line (readable by every user of a host), not in any output. What the token does not protect: a
|
|
663
|
+
process of your own account on the remote host can read it (the same boundary as on your
|
|
664
|
+
machine), and the dev server itself (`/__pix3/api/*` reads and writes the project) answers any
|
|
665
|
+
local user of the remote host, as every Vite dev server does. To rotate: delete
|
|
666
|
+
`~/.pix3/cdp-token` on your machine, `pix3 editor --stop-chrome`, run the `--chrome-only` line and
|
|
667
|
+
the copy line again, then `npx pix3 agent-setup --remote` and a new thread. Untested: a Windows
|
|
668
|
+
or macOS machine on the human side (the `type … | ssh` line is cmd's and PowerShell's).
|
|
669
|
+
|
|
670
|
+
## The editor bridge — what the tab exposes
|
|
671
|
+
|
|
672
|
+
`packages/editor-core/src/host/bridge-tools.ts` is the tool table (names, descriptions, JSON
|
|
673
|
+
Schemas), `debug-bridge.ts` the one implementation behind two transports: the page answers
|
|
674
|
+
chrome-devtools-mcp's `devtoolstooldiscovery` with the `pix3` group
|
|
675
|
+
(`list_3p_developer_tools` / `execute_3p_developer_tool {toolName, params: "<JSON>"}`), and
|
|
676
|
+
`window.__PIX3_DEBUG__.call(name, params)` is the same call from `evaluate_script`
|
|
677
|
+
(`waitForStableDom: false`). Tools: `pix3_status`, `pix3_sync` (`{expect?, timeoutMs?}` →
|
|
678
|
+
flush, rescan, barrier), `pix3_scene` (`{path?, maxDepth?, nodeId?, find?}`), `pix3_play`
|
|
679
|
+
(`{action: start|stop|restart|pause|status, scenePath?, force?}` — the designer's session is
|
|
680
|
+
refused with `not_owner`, `force` included), `pix3_game_run` (`GameTestService.run`),
|
|
681
|
+
`pix3_screenshot` (`{target: game|viewport}` prepares the view; the picture is
|
|
682
|
+
`take_screenshot`), `pix3_errors` (`{since?, clear?}`). Params are validated on both paths;
|
|
683
|
+
every refusal is `{ok:false, reason, detail}`. `pix3 check`, `pix3 smoke` and `vite build`
|
|
684
|
+
flush the editor through `POST /__pix3/api/flush` (the CLI side is `src/editor-sync.ts`),
|
|
685
|
+
without the bridge. The kit's `pix3-editor` skill is held to the table by `src/kit.spec.ts`.
|
|
686
|
+
|
|
687
|
+
## Package layout and publishing
|
|
688
|
+
|
|
689
|
+
The published package has **no runtime `dependencies`**, so a cold `npx -y @pix3/cli@X.Y.Z …`
|
|
690
|
+
fetches one tarball and installs nothing else (measured in
|
|
691
|
+
`.plans/measurements/external-agent-phase0-cold-start.md`). `prepack` builds:
|
|
692
|
+
|
|
693
|
+
```text
|
|
694
|
+
dist/index.js the bin: src/index.ts + yaml, one minified ESM file
|
|
695
|
+
(scripts/build-bin.mjs); lazy commands stay lazy
|
|
696
|
+
dist/validate/prebuilt/ validate + @pix3/runtime + three (scripts/build-validate.mjs)
|
|
697
|
+
dist/smoke/prebuilt/ smoke worker + tree defaults + @pix3/runtime + three (scripts/build-smoke.mjs)
|
|
698
|
+
dist/runtime-types.json runtime-types/ packed into one file (scripts/build-runtime-types.mjs):
|
|
699
|
+
~1 100 small .d.ts cost npm over a second to unpack on every npx run
|
|
700
|
+
kit/ the agent kit (scripts/build-kit.mjs)
|
|
701
|
+
templates/ copy of packages/create-pix3/templates (scripts/copy-templates.mjs; removed at postpack)
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
Left out of the bin on purpose: Node built-ins; `esbuild` (an `optionalDependency` — level 2 of
|
|
705
|
+
`validate`, `smoke` and `check` resolve it from the bin with `import.meta.resolve`, and degrade
|
|
706
|
+
with a note when it is absent) and `typescript` (fetched on demand by `check`); and the
|
|
707
|
+
checkout-only modules (`validate/bundle.ts`,
|
|
708
|
+
`smoke/bundle.ts`, the kit generator), which become a throwing stub. Every file the CLI reads from
|
|
709
|
+
its own package is addressed from the package root (`src/package-root.ts`), never relative to the
|
|
710
|
+
current module — in the bundle every module's `import.meta.url` is the bin's. The sources (`node
|
|
711
|
+
src/index.ts`, the specs) never use a prebuilt bundle, even when an old `dist/` exists.
|
|
712
|
+
`src/bin-bundle.spec.ts` builds the bin into a temp package layout and checks `--version` against
|
|
713
|
+
`package.json` (and that against the lockstep root version), the pinned `chrome-devtools-mcp`
|
|
714
|
+
launch `agent-setup` writes, and that nothing but built-ins and the optional externals is
|
|
715
|
+
imported at run time.
|
|
716
|
+
|
|
717
|
+
Publishing is `.github/workflows/publish-packages.yml` (npm Trusted Publishing / OIDC, no token):
|
|
718
|
+
a `runtime-vX.Y.Z` tag publishes `@pix3/runtime` and `@pix3/cli` together, `cli-vX.Y.Z` the CLI
|
|
719
|
+
alone, or run the workflow manually. The job runs `npm ci` at the repo root (the build needs the
|
|
720
|
+
runtime sources, the root TypeScript and esbuild), checks the lockstep version against the root
|
|
721
|
+
and the tag, type-checks, runs the CLI specs and `npm publish`es. Try the tarball locally with
|
|
722
|
+
`npm pack -w packages/cli` at the repo root, then `npx -y --package ./pix3-cli-X.Y.Z.tgz pix3 …`.
|