castle-web-cli 0.4.129 → 0.4.131

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 (144) hide show
  1. package/dist/agent-prompts.js +4 -4
  2. package/dist/filesChanged.d.ts +2 -0
  3. package/dist/filesChanged.js +6 -2
  4. package/dist/ide.js +15 -4
  5. package/dist/importBrowse.js +1 -0
  6. package/dist/index.js +10 -4
  7. package/dist/init.js +63 -15
  8. package/dist/native/loop.js +1 -1
  9. package/dist/native/tools.js +1 -1
  10. package/dist/serve.d.ts +1 -0
  11. package/dist/serve.js +20 -1
  12. package/dist/shell/assets/index-BpOLUyVO.js +441 -0
  13. package/dist/shell/assets/{index-CAq6f9B5.css → index-D6K-0YDB.css} +1 -1
  14. package/dist/shell/index.html +2 -2
  15. package/kits/physics-2d/CLAUDE.md +9 -10
  16. package/kits/physics-2d/castle.json +1 -1
  17. package/kits/physics-2d/editors/PlayOnly.jsx +16 -6
  18. package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
  19. package/kits/physics-2d/editors/SceneEditor.jsx +10 -10
  20. package/kits/physics-2d/editors/SingleEditor.jsx +37 -8
  21. package/kits/physics-2d/editors/StyleEditor.jsx +7 -4
  22. package/kits/physics-2d/editors/pixelEditorChrome.jsx +7 -7
  23. package/kits/physics-2d/editors/pixelInspector.jsx +13 -7
  24. package/kits/physics-2d/editors/pxArtTimeline.jsx +29 -11
  25. package/kits/physics-2d/editors/pxArtTimeline.module.css +11 -0
  26. package/kits/physics-2d/engine/ScenePlayer.jsx +2 -2
  27. package/kits/physics-2d/engine/blueprint.js +23 -5
  28. package/kits/physics-2d/engine/liveReload.js +33 -21
  29. package/kits/physics-2d/engine/scene.js +17 -5
  30. package/kits/physics-2d/engine/systemRegistry.js +1 -1
  31. package/kits/physics-2d/engine/ui.jsx +2 -2
  32. package/kits/physics-2d/engine/ui.module.css +42 -0
  33. package/kits/physics-3d/CLAUDE.md +108 -0
  34. package/kits/physics-3d/behaviors/Body.jsx +51 -0
  35. package/kits/physics-3d/behaviors/Door.jsx +31 -0
  36. package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
  37. package/kits/physics-3d/behaviors/Model.jsx +14 -0
  38. package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
  39. package/kits/physics-3d/behaviors/Player.jsx +172 -0
  40. package/kits/physics-3d/behaviors/Shape.jsx +75 -0
  41. package/kits/physics-3d/behaviors/Solid.jsx +11 -0
  42. package/kits/physics-3d/behaviors/Transform.jsx +24 -0
  43. package/kits/physics-3d/blueprints/barrel.scene +15 -0
  44. package/kits/physics-3d/blueprints/crate.scene +29 -0
  45. package/kits/physics-3d/blueprints/door.scene +35 -0
  46. package/kits/physics-3d/blueprints/gem.scene +26 -0
  47. package/kits/physics-3d/blueprints/pillar.scene +25 -0
  48. package/kits/physics-3d/blueprints/platform.scene +23 -0
  49. package/kits/physics-3d/blueprints/player.scene +29 -0
  50. package/kits/physics-3d/blueprints/rock.scene +14 -0
  51. package/kits/physics-3d/blueprints/statue.scene +26 -0
  52. package/kits/physics-3d/blueprints/tree.scene +14 -0
  53. package/kits/physics-3d/blueprints/wall.scene +26 -0
  54. package/kits/{basic-2d → physics-3d}/castle.json +23 -5
  55. package/kits/physics-3d/docs/pxmodel-format.md +111 -0
  56. package/kits/physics-3d/drawings/crate.pxart +26 -0
  57. package/kits/physics-3d/drawings/door-edge.pxart +26 -0
  58. package/kits/physics-3d/drawings/door.pxart +27 -0
  59. package/kits/physics-3d/drawings/face.pxart +26 -0
  60. package/kits/physics-3d/drawings/floor.pxart +27 -0
  61. package/kits/physics-3d/drawings/platform.pxart +27 -0
  62. package/kits/physics-3d/drawings/statue.pxart +27 -0
  63. package/kits/physics-3d/drawings/wall.pxart +27 -0
  64. package/kits/physics-3d/engine3d/PlayOnly3D.jsx +36 -0
  65. package/kits/physics-3d/engine3d/PxModelEditor.jsx +619 -0
  66. package/kits/physics-3d/engine3d/Scene3DEditor.jsx +618 -0
  67. package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +309 -0
  68. package/kits/physics-3d/engine3d/editor3dData.js +114 -0
  69. package/kits/physics-3d/engine3d/editor3dInspector.jsx +404 -0
  70. package/kits/physics-3d/engine3d/editorChrome.jsx +218 -0
  71. package/kits/physics-3d/engine3d/editorWorld.js +249 -0
  72. package/kits/physics-3d/engine3d/materials.js +174 -0
  73. package/kits/physics-3d/engine3d/meshops.js +123 -0
  74. package/kits/physics-3d/engine3d/modelEditorWorld.js +439 -0
  75. package/kits/physics-3d/engine3d/modelFiles.js +136 -0
  76. package/kits/physics-3d/engine3d/pxmodel.js +265 -0
  77. package/kits/physics-3d/engine3d/thumbnails.js +137 -0
  78. package/kits/physics-3d/engine3d/world3d.js +216 -0
  79. package/kits/{basic-2d → physics-3d}/eslint.config.js +10 -25
  80. package/kits/physics-3d/index.html +20 -0
  81. package/kits/physics-3d/main.jsx +30 -0
  82. package/kits/physics-3d/models/barrel.pxmodel +10 -0
  83. package/kits/physics-3d/models/player.pxmodel +222 -0
  84. package/kits/physics-3d/models/rock.pxmodel +30 -0
  85. package/kits/physics-3d/models/tree.pxmodel +10 -0
  86. package/kits/{basic-2d → physics-3d}/package-lock.json +140 -119
  87. package/kits/{basic-2d → physics-3d}/package.json +11 -9
  88. package/kits/{basic-2d → physics-3d}/pnpm-lock.yaml +162 -137
  89. package/kits/physics-3d/scenes/main.scene +398 -0
  90. package/kits/physics-3d/scenes/model-lab.scene +111 -0
  91. package/kits/physics-3d/systems/physics3d.js +356 -0
  92. package/package.json +5 -2
  93. package/dist/shell/assets/index-DKu9ejyh.js +0 -436
  94. package/kits/basic-2d/CLAUDE.md +0 -221
  95. package/kits/basic-2d/behaviors/Camera.jsx +0 -43
  96. package/kits/basic-2d/behaviors/Collider.jsx +0 -213
  97. package/kits/basic-2d/behaviors/Layout.jsx +0 -53
  98. package/kits/basic-2d/behaviors/Sprite.jsx +0 -357
  99. package/kits/basic-2d/behaviors/tint.js +0 -47
  100. package/kits/basic-2d/blueprints/cauldron.scene +0 -20
  101. package/kits/basic-2d/docs/pxart-format.md +0 -377
  102. package/kits/basic-2d/drawings/cauldron.pxart +0 -113
  103. package/kits/basic-2d/editors/BlueprintLibrary.jsx +0 -270
  104. package/kits/basic-2d/editors/ErrorBoundary.jsx +0 -59
  105. package/kits/basic-2d/editors/PlayOnly.jsx +0 -31
  106. package/kits/basic-2d/editors/PxArtEditor.jsx +0 -1092
  107. package/kits/basic-2d/editors/SceneEditor.jsx +0 -1780
  108. package/kits/basic-2d/editors/SelectionOverlay.jsx +0 -909
  109. package/kits/basic-2d/editors/SingleEditor.jsx +0 -122
  110. package/kits/basic-2d/editors/behaviorRegistry.js +0 -34
  111. package/kits/basic-2d/editors/editorHistory.js +0 -157
  112. package/kits/basic-2d/editors/inspectorSheet.js +0 -13
  113. package/kits/basic-2d/editors/pixelCanvas.js +0 -11
  114. package/kits/basic-2d/editors/pixelEditorChrome.jsx +0 -74
  115. package/kits/basic-2d/editors/pixelGeometry.js +0 -140
  116. package/kits/basic-2d/editors/pixelInspector.jsx +0 -633
  117. package/kits/basic-2d/editors/pxArtEditorModel.js +0 -732
  118. package/kits/basic-2d/editors/pxArtPlayback.js +0 -92
  119. package/kits/basic-2d/editors/pxArtTimeline.jsx +0 -752
  120. package/kits/basic-2d/editors/pxArtTimeline.module.css +0 -506
  121. package/kits/basic-2d/editors/pxArtTools.js +0 -232
  122. package/kits/basic-2d/editors/useArtboardFit.js +0 -105
  123. package/kits/basic-2d/engine/ScenePlayer.jsx +0 -209
  124. package/kits/basic-2d/engine/SceneUI.jsx +0 -59
  125. package/kits/basic-2d/engine/assets.js +0 -15
  126. package/kits/basic-2d/engine/autoInspector.jsx +0 -70
  127. package/kits/basic-2d/engine/behaviorExtensions.js +0 -32
  128. package/kits/basic-2d/engine/blueprint.js +0 -557
  129. package/kits/basic-2d/engine/collider.js +0 -200
  130. package/kits/basic-2d/engine/files.js +0 -141
  131. package/kits/basic-2d/engine/liveReload.js +0 -88
  132. package/kits/basic-2d/engine/pxart.js +0 -1032
  133. package/kits/basic-2d/engine/pxartSmooth.js +0 -222
  134. package/kits/basic-2d/engine/scene.js +0 -696
  135. package/kits/basic-2d/engine/spriteGeometry.js +0 -32
  136. package/kits/basic-2d/engine/systemRegistry.js +0 -16
  137. package/kits/basic-2d/engine/ui.jsx +0 -695
  138. package/kits/basic-2d/engine/ui.module.css +0 -2287
  139. package/kits/basic-2d/index.html +0 -24
  140. package/kits/basic-2d/main.jsx +0 -24
  141. package/kits/basic-2d/scenes/main.scene +0 -16
  142. package/kits/basic-2d/scripts/draw.mjs +0 -121
  143. /package/kits/{basic-2d → physics-3d}/.prettierrc +0 -0
  144. /package/kits/{basic-2d → physics-3d}/vite.config.js +0 -0
@@ -897,12 +897,12 @@ export function buildTaskPrompt(opts) {
897
897
  // -- blind whole-file rewrites made parallel agents clobber each other's
898
898
  // edits. Read-then-edit is slower but safe; that is the right tradeoff.)
899
899
  const wrapUpList = opts.plan
900
- ? "the 90-progress write, the `castle-web save-version` for your paths, the final `npm run restart`, writing the notes file, and the handoff lines"
901
- : "the 90-progress write, the `castle-web save-version` for your paths, the final `npm run restart`, and writing the notes file";
900
+ ? "the 90-progress write, the `castle-web save-version` for your paths, writing the notes file, and the handoff lines"
901
+ : "the 90-progress write, the `castle-web save-version` for your paths, and writing the notes file";
902
902
  const wrapUp = opts.backend === "claude" || opts.backend === "smith"
903
903
  ? `\n- Wrap up in ONE tool call, not several: once your last file edit is done, combine ${wrapUpList} into a single shell command (\`;\`-separated so the notes land even if the restart hiccups). Then stop -- no extra turns after it.`
904
904
  : "";
905
- return `You are a background build agent for the Castle deck "${opts.deckLabel}" (current directory). A separate conversation agent dispatched you with one task. Follow the deck's CLAUDE.md / AGENTS.md conventions, and reload the served deck after changes (\`npm run restart\`).${quickReference}${layout}${deckSource}
905
+ return `You are a background build agent for the Castle deck "${opts.deckLabel}" (current directory). A separate conversation agent dispatched you with one task. Follow the deck's CLAUDE.md / AGENTS.md conventions. Do not reload the served deck -- the person applies your changes when they are ready. \`npm run restart\` exists, but it is HIGHLY flow breaking: it reloads every panel they have open, including the one they are working in. Use it only when a change clearly requires restarting their whole experience.${quickReference}${layout}${deckSource}
906
906
 
907
907
  Your task (id ${opts.taskId}): ${opts.title}
908
908
 
@@ -919,7 +919,7 @@ Operating rules:
919
919
  - NEVER run \`restore-version\`. Going back to an earlier version is the user's decision alone -- not yours, not even to undo your own work. If your change is wrong, fix it forward.
920
920
  - Other task agents may be editing this same deck IN PARALLEL. When you change an existing file, READ it first and make a targeted edit to just the part you need -- never overwrite a whole file you have not read. A blind full-file rewrite clobbers other agents' in-flight changes. Being a bit slower and careful here is the right tradeoff.
921
921
  - Favor real, editable assets: for game objects, characters, and scenery, make pixel-art drawings and place them as real actors in the scene rather than code-drawn shapes -- it keeps the deck editable in the editor and remixable. (Data-driven UI like health bars, score/text, and HUD gauges, plus dynamic things like bullets/particles/effects, stay procedural -- don't force those into drawings.)
922
- - Respect art ownership: if YOUR task is to create art, make it as real, editable drawing files (NOT hand-written pixel grids or code-drawn shapes), following the deck's CLAUDE.md / AGENTS.md for the drawing format and exact command. But if your prompt only REFERENCES drawing names (a sibling task is creating them in parallel), point the scene at those names and do NOT create the drawing files yourself -- two agents drawing the same sprites clobber each other. The deck's CLAUDE.md / AGENTS.md is the source of truth for how to make art. Reference the intended drawing name from the scene right away (the actor renders a plain-block fallback until the file exists), and remember newly written drawing files need \`npm run restart\` before the kit picks them up.
922
+ - Respect art ownership: if YOUR task is to create art, make it as real, editable drawing files (NOT hand-written pixel grids or code-drawn shapes), following the deck's CLAUDE.md / AGENTS.md for the drawing format and exact command. But if your prompt only REFERENCES drawing names (a sibling task is creating them in parallel), point the scene at those names and do NOT create the drawing files yourself -- two agents drawing the same sprites clobber each other. The deck's CLAUDE.md / AGENTS.md is the source of truth for how to make art. Reference the intended drawing name from the scene right away (the actor renders a plain-block fallback until the file exists); a newly written drawing file shows up for the person on their next reload, which is theirs to trigger.
923
923
  - Castle decks are played on phones -- any control you build MUST be touch-friendly by default: respond to touch and drag on the canvas (and tap where natural), never keyboard-only. Keep keyboard working too as a bonus, but a control that only works with a keyboard is broken for most players. (Use the kit's pointer/touch input -- see its CLAUDE.md.)
924
924
  - Narrate in plain prose as you work -- the user watches your output stream live. Lead with a short sentence saying what you're about to do BEFORE your first tool call (never open with a silent tool call), then keep dropping a brief plain line every few steps as you go. A line of thinking or a quick "now wiring the paddle to the keys" is exactly what they want to see.
925
925
  - Update your progress VERY frequently: write a bare integer 0-100 to ${opts.progressPath} (e.g. \`echo 30 > ${opts.progressPath}\`) every time you advance -- at least every 10 points, or every 20 for properly small tasks. Start near 10, write 90 just before wrapping up. Never let it sit stale while you work.
@@ -22,4 +22,6 @@ export declare function installFilesChangedWatcher(opts: {
22
22
  broadcast: (msg: FilesChangedMessage) => void;
23
23
  /** Drop Vite transform caches (so `import.meta.glob` rescans new files). */
24
24
  invalidate: () => void;
25
+ /** Records what was broadcast, so we can see what fires and when. */
26
+ log?: (line: string) => void;
25
27
  }): void;
@@ -14,7 +14,7 @@ const IGNORED_SEGMENTS = new Set(['node_modules', '.castle', '.git']);
14
14
  const DEBOUNCE_MS = 100;
15
15
  const MAX_WAIT_MS = 500;
16
16
  export function installFilesChangedWatcher(opts) {
17
- const { vite, projectDir, broadcast, invalidate } = opts;
17
+ const { vite, projectDir, broadcast, invalidate, log } = opts;
18
18
  const pending = new Map();
19
19
  let debounceTimer = null;
20
20
  let maxTimer = null;
@@ -45,7 +45,11 @@ export function installFilesChangedWatcher(opts) {
45
45
  for (const p of change.affected)
46
46
  affected.add(p);
47
47
  }
48
- broadcast({ type: 'files_changed', changes, affected: [...affected].sort() });
48
+ const affectedList = [...affected].sort();
49
+ log?.(`[watch] ${changes
50
+ .map((c) => `${c.event} ${c.path}${c.affected.length ? ` (+${c.affected.length})` : ''}`)
51
+ .join(', ')} | affected ${affectedList.length}\n`);
52
+ broadcast({ type: 'files_changed', changes, affected: affectedList });
49
53
  };
50
54
  const schedule = () => {
51
55
  if (debounceTimer)
package/dist/ide.js CHANGED
@@ -414,13 +414,24 @@ function handleVersionSave(deckDir, req, res) {
414
414
  // Never forced. Unsaved work that a restore would overwrite comes back as a 409
415
415
  // naming the files, which the panel shows -- saving a version first is the fix,
416
416
  // and it is one click away in the same panel.
417
- function handleVersionRestore(deckDir, req, res) {
417
+ function handleVersionRestore(deckDir, req, res, restart) {
418
418
  withJsonBody(req, res, (body) => {
419
419
  if (typeof body.id !== 'string' || !body.id) {
420
420
  return sendJson(res, 400, { error: 'Missing version id.' });
421
421
  }
422
422
  try {
423
423
  const outcome = applyVersionRestore(deckDir, body.id);
424
+ // A restore rewrites the deck wholesale, so every panel rendering it is
425
+ // showing the wrong thing. Reload them from here rather than from the
426
+ // panel that clicked Restore: a restore can DELETE a file, and a reload
427
+ // racing ahead of Vite's invalidation asks for a module that no longer
428
+ // exists. `restart` invalidates first, then broadcasts, in that order.
429
+ //
430
+ // After the response, not before: dropping every transform means the
431
+ // whole deck is rebuilt on the next load, and the panel that clicked
432
+ // Restore should not sit waiting on that.
433
+ if (!outcome.alreadyThere)
434
+ setImmediate(() => restart?.());
424
435
  sendJson(res, 200, {
425
436
  ok: true,
426
437
  restored: outcome.applied.length,
@@ -684,7 +695,7 @@ function handleFilesDelete(deckDir, req, res) {
684
695
  // The builtin Files + code-editor backend: list / read / write deck files and
685
696
  // report kit-owned editor extensions. Paths are deck-relative; resolveDeckPath
686
697
  // rejects traversal and protected dirs.
687
- function handleFilesApi(deckDir, req, res, reqPath) {
698
+ function handleFilesApi(deckDir, req, res, reqPath, restart) {
688
699
  const action = reqPath.slice(FILES_API_PREFIX.length);
689
700
  if (action === 'info') {
690
701
  const config = readEditorConfig(deckDir);
@@ -751,7 +762,7 @@ function handleFilesApi(deckDir, req, res, reqPath) {
751
762
  return true;
752
763
  }
753
764
  if (action === 'restore-version') {
754
- handleVersionRestore(deckDir, req, res);
765
+ handleVersionRestore(deckDir, req, res, restart);
755
766
  return true;
756
767
  }
757
768
  if (action === 'list') {
@@ -1060,7 +1071,7 @@ export function createIdeServer(opts) {
1060
1071
  return handleCoverApi(deckDir, req, res);
1061
1072
  }
1062
1073
  if (reqPath.startsWith(FILES_API_PREFIX)) {
1063
- return handleFilesApi(deckDir, req, res, reqPath);
1074
+ return handleFilesApi(deckDir, req, res, reqPath, opts.restart);
1064
1075
  }
1065
1076
  if (reqPath.startsWith(IMPORT_API_PREFIX)) {
1066
1077
  return handleImportApi(deckDir, req, res, reqPath, opts.restart);
@@ -38,6 +38,7 @@ const BOOKMARKS_PLAYLIST_PREFIX = 'bookmarks-';
38
38
  // an update to this list.
39
39
  const KIT_DECK_IDS = [
40
40
  'ckRZGFW4iPrx', // physics-2d
41
+ 'JH0SclbPVP0y', // physics-3d
41
42
  ];
42
43
  // Listing is several server round trips, and the picker is opened, closed and
43
44
  // reopened while someone decides. Cached briefly so that costs once.
package/dist/index.js CHANGED
@@ -87,7 +87,7 @@ function getWsPort(dir) {
87
87
  }
88
88
  function usage() {
89
89
  console.log(`Usage:
90
- castle-web init <dir> [--kit NAME] (kits: physics-2d (default), basic-2d, none)
90
+ castle-web init <dir> [--kit NAME] (kits: physics-2d (default), physics-3d, none)
91
91
  castle-web serve [dir] [--port PORT] [--host HOST] [--open] [--detach]
92
92
  castle-web restart [--port PORT]
93
93
  castle-web screenshot [--out FILE] [--port PORT]
@@ -106,7 +106,7 @@ function usage() {
106
106
 
107
107
  Kits:
108
108
  physics-2d Actor / behavior / scene framework for 2D games with matter-js physics (RigidBody / Collider / Joints on top of Layout / Drawing / Camera). Default if --kit is omitted.
109
- basic-2d The physics-free predecessor of physics-2d; being retired.
109
+ physics-3d The same model in 3D: three.js rendering and rapier physics, with a scene editor and a voxel model editor. Built on physics-2d, which it imports.
110
110
  none Minimal vite + canvas setup with the castle-web SDK and no kit.
111
111
 
112
112
  After init, read the scaffolded deck's CLAUDE.md (also AGENTS.md) for the kit's full author guide.`);
@@ -225,9 +225,15 @@ async function main() {
225
225
  const ws = new WS(`ws://localhost:${wsPort}`);
226
226
  ws.on('open', () => {
227
227
  ws.send(JSON.stringify({ type: 'restart' }));
228
- setTimeout(() => { ws.close(); process.exit(0); }, 100);
228
+ setTimeout(() => {
229
+ ws.close();
230
+ process.exit(0);
231
+ }, 100);
232
+ });
233
+ ws.on('error', () => {
234
+ console.error('Could not connect. Is castle-web serve running?');
235
+ process.exit(1);
229
236
  });
230
- ws.on('error', () => { console.error('Could not connect. Is castle-web serve running?'); process.exit(1); });
231
237
  break;
232
238
  }
233
239
  case 'screenshot': {
package/dist/init.js CHANGED
@@ -38,7 +38,7 @@ const DEFAULT_KIT = 'physics-2d';
38
38
  // Registry version of castle-web-sdk to inject when scaffolding from a
39
39
  // globally-installed castle-web (not from inside the workspace). Bumped
40
40
  // alongside cli/sdk version bumps.
41
- const PUBLISHED_SDK_VERSION = '0.4.14';
41
+ const PUBLISHED_SDK_VERSION = '0.4.15';
42
42
  // The account the first-party kits are (to be) published under, so a kit
43
43
  // imported from the CLI's copy is named the same as one fetched from the server.
44
44
  const KIT_AUTHOR = 'castle';
@@ -347,6 +347,51 @@ function withoutFileTypes(editor) {
347
347
  // been published: an unpublished one can only be named as the CLI's own copy,
348
348
  // while a published one is pinned by deckId so the deck can be told about kit
349
349
  // releases and take them, without waiting for a CLI release.
350
+ // One kit copied into `imports/<alias>/`, then the same again for every kit IT
351
+ // imports -- all of them side by side at the deck root, never nested.
352
+ //
353
+ // Flattening is not a tidiness choice: a kit refers to its own dependencies as
354
+ // `@imports/<alias>/...`, which resolves against the deck being served, and
355
+ // there is only ever one deck. A kit left inside another kit's `imports/` sits
356
+ // one level too deep and nothing it names resolves. `addImportTo` already
357
+ // flattens this way for fetched decks (see addTransitiveImports); this is the
358
+ // same rule for the copies the CLI ships.
359
+ //
360
+ // A dependency the CLI does not ship keeps the pin the kit declared, and
361
+ // `install` fetches it from the server (restoreMissingImports) -- so a kit may
362
+ // depend on any published deck, not only on its neighbours in kits/.
363
+ function copyKitImport(projectDir, kitDir, kit, alias, pins, copied) {
364
+ if (copied.has(alias))
365
+ return;
366
+ copied.set(alias, kitDir);
367
+ const importDir = path.join(projectDir, IMPORTS_DIR, alias);
368
+ const nestedImports = path.join(kitDir, IMPORTS_DIR);
369
+ fs.mkdirSync(importDir, { recursive: true });
370
+ fs.cpSync(kitDir, importDir, {
371
+ recursive: true,
372
+ verbatimSymlinks: true,
373
+ // The kit's own `imports/` is left behind with the rest of its derived
374
+ // state: its contents are being re-placed at the deck root instead.
375
+ filter: (src) => src === kitDir || (src !== nestedImports && !KIT_COPY_EXCLUDE.has(path.basename(src))),
376
+ });
377
+ const kitConfig = readJsonFile(path.join(kitDir, 'castle.json')) ?? {};
378
+ pins[alias] = makeKitPin(kitConfig, kit);
379
+ const kitPins = (kitConfig.imports ?? {});
380
+ for (const [depAlias, depPin] of Object.entries(kitPins)) {
381
+ if (copied.has(depAlias) || pins[depAlias])
382
+ continue;
383
+ const depKit = depAlias.startsWith(`${KIT_AUTHOR}.`)
384
+ ? depAlias.slice(KIT_AUTHOR.length + 1)
385
+ : depAlias;
386
+ const depKitDir = path.join(getKitsDir(), depKit);
387
+ if (fs.existsSync(depKitDir) && fs.statSync(depKitDir).isDirectory()) {
388
+ copyKitImport(projectDir, depKitDir, depKit, depAlias, pins, copied);
389
+ }
390
+ else {
391
+ pins[depAlias] = depPin;
392
+ }
393
+ }
394
+ }
350
395
  function scaffoldFromKitImport(kit, projectDir) {
351
396
  const kitDir = requireKitDir(kit);
352
397
  // Qualified like any other import (`<author>.<deck>`), and qualified NOW even
@@ -355,15 +400,10 @@ function scaffoldFromKitImport(kit, projectDir) {
355
400
  // kits under the castle account turns the pin into a deckId; the name it is
356
401
  // already known by stays put.
357
402
  const alias = `${KIT_AUTHOR}.${kit}`;
358
- const importDir = path.join(projectDir, IMPORTS_DIR, alias);
359
- fs.mkdirSync(importDir, { recursive: true });
360
- fs.cpSync(kitDir, importDir, {
361
- recursive: true,
362
- verbatimSymlinks: true,
363
- filter: (src) => src === kitDir || !KIT_COPY_EXCLUDE.has(path.basename(src)),
364
- });
403
+ const pins = {};
404
+ const copied = new Map();
405
+ copyKitImport(projectDir, kitDir, kit, alias, pins, copied);
365
406
  const kitConfig = readJsonFile(path.join(kitDir, 'castle.json')) ?? {};
366
- const kitPkg = readJsonFile(path.join(kitDir, 'package.json')) ?? {};
367
407
  const title = typeof kitConfig.title === 'string' ? kitConfig.title : kit;
368
408
  writeDeckIndexHtml(projectDir, alias, title, deckMainFile(kitDir));
369
409
  writeStarterScene(projectDir, kitDir, alias);
@@ -379,16 +419,22 @@ function scaffoldFromKitImport(kit, projectDir) {
379
419
  // types too, which a copy taken at scaffold time never could.
380
420
  writeJsonFile(path.join(projectDir, 'castle.json'), {
381
421
  ...(kitConfig.editor ? { editor: withoutFileTypes(kitConfig.editor) } : {}),
382
- imports: { [alias]: makeKitPin(kitConfig, kit) },
422
+ imports: pins,
383
423
  });
384
424
  // The kit's code runs from THIS deck's node_modules, so its dependencies are
385
425
  // declared here (see syncImportDependencies, which does the same for imports
386
- // added later).
426
+ // added later). Every copied kit contributes, the scaffolding kit first, so
427
+ // where two of them name the same package the one being scaffolded from wins.
387
428
  const { sdkRef, cliCommand } = resolveScaffoldRefs();
388
429
  const dependencies = {};
389
- for (const [name, range] of Object.entries(kitPkg.dependencies ?? {})) {
390
- dependencies[name] =
391
- name === 'castle-web-sdk' && String(range).startsWith('file:') ? sdkRef : String(range);
430
+ for (const depKitDir of copied.values()) {
431
+ const depPkg = readJsonFile(path.join(depKitDir, 'package.json')) ?? {};
432
+ for (const [name, range] of Object.entries(depPkg.dependencies ?? {})) {
433
+ if (dependencies[name] !== undefined)
434
+ continue;
435
+ dependencies[name] =
436
+ name === 'castle-web-sdk' && String(range).startsWith('file:') ? sdkRef : String(range);
437
+ }
392
438
  }
393
439
  writeJsonFile(path.join(projectDir, 'package.json'), {
394
440
  name: path.basename(projectDir),
@@ -399,7 +445,9 @@ function scaffoldFromKitImport(kit, projectDir) {
399
445
  });
400
446
  fs.writeFileSync(path.join(projectDir, 'CLAUDE.md'), makeImportedKitClaudeMd(alias));
401
447
  appendCommonInstructions(projectDir);
402
- lockImportTree(importDir);
448
+ for (const importedAlias of copied.keys()) {
449
+ lockImportTree(path.join(projectDir, IMPORTS_DIR, importedAlias));
450
+ }
403
451
  }
404
452
  // The scaffold as version 1, so a creator can always get back to the pristine
405
453
  // deck. AFTER the install: it rewrites package.json and writes a lockfile, so a
@@ -313,7 +313,7 @@ const ROLE_FRAMING = {
313
313
  // actually running `npm run restart` in a real shell (they have no
314
314
  // native restart tool, and naming one there would misdirect them into
315
315
  // hallucinating a tool call that doesn't exist for their backend).
316
- "Prefer the restart tool over running `npm run restart` in bash -- it does the same reload in this same process, instantly, without spawning a node process just to send one message.",
316
+ "A native restart tool exists, and is preferable to running `npm run restart` in bash when you do need one. But reloading is HIGHLY flow breaking either way -- it reloads every panel the person has open, including the one they are working in -- so do not reload after ordinary file changes. Use it only when a change clearly requires restarting their whole experience.",
317
317
  "Work to completion, then reply with your final message text.",
318
318
  ].join(" "),
319
319
  };
@@ -551,7 +551,7 @@ const TOOLS = [
551
551
  },
552
552
  {
553
553
  name: "restart",
554
- description: "Reload the deck in every connected browser tab, dropping Vite's stale module cache first (same effect as `npm run restart`, applied in-process). Call this after file changes instead of running `npm run restart` via bash.",
554
+ description: "Reload the deck in every connected browser tab, dropping Vite's stale module cache first (same effect as `npm run restart`, applied in-process). HIGHLY flow breaking: it reloads every panel the person has open, including the one they are working in. Do NOT call it after ordinary file changes -- the person applies those when they are ready. Use it only when a change clearly requires restarting their whole experience.",
555
555
  parameters: { type: "object", properties: {} },
556
556
  // Task-only: the router never edits the deck, so it never has a reason
557
557
  // to reload it.
package/dist/serve.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export declare function appendCapped(file: string, line: string): void;
1
2
  export declare function serve(dir: string, options?: {
2
3
  port?: string;
3
4
  host?: string;
package/dist/serve.js CHANGED
@@ -12,6 +12,24 @@ import { autoUpdateImports } from './imports.js';
12
12
  import * as config from './config.js';
13
13
  import { graphql as castleGraphql } from './api.js';
14
14
  import { executeCommand } from './castle-host/host.js';
15
+ // The log is a tail, not an archive: append, and once it passes the cap drop
16
+ // whole lines off the front until it is back under.
17
+ const LOG_MAX_BYTES = 2 * 1024 * 1024;
18
+ const LOG_KEEP_BYTES = Math.floor(LOG_MAX_BYTES * 0.8);
19
+ export function appendCapped(file, line) {
20
+ try {
21
+ fs.appendFileSync(file, line);
22
+ if (fs.statSync(file).size <= LOG_MAX_BYTES)
23
+ return;
24
+ const text = fs.readFileSync(file, 'utf8');
25
+ const cut = text.length - LOG_KEEP_BYTES;
26
+ const nl = text.indexOf('\n', cut);
27
+ fs.writeFileSync(file, text.slice(nl < 0 ? cut : nl + 1));
28
+ }
29
+ catch {
30
+ // logging must never take the serve down
31
+ }
32
+ }
15
33
  function isPortFree(port) {
16
34
  return new Promise((resolve) => {
17
35
  const srv = net.createServer();
@@ -361,6 +379,7 @@ export async function serve(dir, options = {}) {
361
379
  projectDir,
362
380
  broadcast,
363
381
  invalidate: () => invalidateModuleCaches(vite),
382
+ log: (line) => appendCapped(logFile, line),
364
383
  });
365
384
  await vite.listen();
366
385
  // The PTY WebSocket upgrade is handled directly on Vite's HTTP server rather
@@ -535,7 +554,7 @@ function startWSServer(port, projectDir, logFile, screenshotsDir, viteHolder) {
535
554
  }
536
555
  if (msg.type === 'log') {
537
556
  const line = `[${msg.level}] ${msg.msg}\n`;
538
- fs.appendFileSync(logFile, line);
557
+ appendCapped(logFile, line);
539
558
  if (msg.level === 'error')
540
559
  process.stderr.write(line);
541
560
  else