castle-web-cli 0.4.91 → 0.4.93

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 (52) hide show
  1. package/dist/agent.js +141 -22
  2. package/dist/api.d.ts +8 -0
  3. package/dist/api.js +10 -0
  4. package/dist/bundle.js +2 -2
  5. package/dist/get-deck.d.ts +1 -1
  6. package/dist/get-deck.js +93 -22
  7. package/dist/ide.js +123 -15
  8. package/dist/imports.d.ts +22 -0
  9. package/dist/imports.js +549 -0
  10. package/dist/index.js +45 -15
  11. package/dist/init.js +169 -1
  12. package/dist/install.d.ts +1 -1
  13. package/dist/install.js +14 -1
  14. package/dist/metering.d.ts +1 -0
  15. package/dist/metering.js +1 -1
  16. package/dist/native/loop.js +1 -0
  17. package/dist/native/openrouter.d.ts +1 -0
  18. package/dist/native/openrouter.js +13 -6
  19. package/dist/native/types.d.ts +1 -0
  20. package/dist/normalize.js +4 -0
  21. package/dist/openrouter-catalog.d.ts +3 -1
  22. package/dist/openrouter-catalog.js +15 -10
  23. package/dist/save-deck.d.ts +2 -0
  24. package/dist/save-deck.js +25 -19
  25. package/dist/serve.js +2 -2
  26. package/dist/shell/assets/{index-DSIr52Kl.css → index-CWNH9QiB.css} +1 -1
  27. package/dist/shell/assets/{index-BFCG4tLs.js → index-_C2BvstY.js} +21 -21
  28. package/dist/shell/index.html +2 -2
  29. package/dist/vitePlugins.d.ts +1 -0
  30. package/dist/vitePlugins.js +33 -0
  31. package/kits/basic-2d/CLAUDE.md +20 -0
  32. package/kits/basic-2d/behaviors/Sprite.jsx +6 -1
  33. package/kits/basic-2d/editors/BlueprintLibrary.jsx +14 -8
  34. package/kits/basic-2d/editors/SceneEditor.jsx +39 -6
  35. package/kits/basic-2d/editors/behaviorRegistry.js +8 -2
  36. package/kits/basic-2d/engine/behaviorExtensions.js +5 -1
  37. package/kits/basic-2d/engine/blueprint.js +39 -3
  38. package/kits/basic-2d/engine/files.js +26 -5
  39. package/kits/basic-2d/engine/scene.js +14 -3
  40. package/kits/basic-2d/engine/systemRegistry.js +5 -1
  41. package/kits/physics-2d/behaviors/Collider.jsx +190 -153
  42. package/kits/physics-2d/editors/SceneEditor.jsx +42 -6
  43. package/kits/physics-2d/editors/SelectionOverlay.jsx +57 -28
  44. package/kits/physics-2d/engine/collider.js +123 -105
  45. package/kits/physics-2d/engine/scene.js +10 -2
  46. package/kits/physics-2d/physics/behaviors/RigidBody.jsx +3 -0
  47. package/kits/physics-2d/physics/matterBridge.js +73 -24
  48. package/package.json +1 -1
  49. package/dist/pull.d.ts +0 -4
  50. package/dist/pull.js +0 -119
  51. package/kits/physics-2d/engine/behaviorExtensions.js +0 -28
  52. package/kits/physics-2d/physics/extensions/collider.js +0 -15
package/dist/ide.js CHANGED
@@ -14,6 +14,7 @@ import { spawn as spawnPty } from "@lydell/node-pty";
14
14
  import headlessPkg from "@xterm/headless";
15
15
  import { SerializeAddon } from "@xterm/addon-serialize";
16
16
  import { WebSocketServer } from "ws";
17
+ import { IMPORTS_DIR, importStatuses, updateImport } from "./imports.js";
17
18
  const HeadlessTerminal = headlessPkg.Terminal;
18
19
  const DIST_DIR = path.dirname(fileURLToPath(import.meta.url));
19
20
  // The bundled shell app (vite build output). `/` serves its index.html and
@@ -105,10 +106,20 @@ function parseKitEditorExtensions(deckDir) {
105
106
  }
106
107
  return exts;
107
108
  }
109
+ // Files under `imports/` came from another deck. They are readable like any
110
+ // other deck file -- the engine, editors and pickers all reference them -- but a
111
+ // deck can't edit them, because editing one would mean editing somebody else's
112
+ // deck. (Keeping your own version of a dependency's file is forking, a later
113
+ // feature; copying it out into the deck works meanwhile.)
114
+ function isImportPath(rel) {
115
+ return rel === IMPORTS_DIR || rel.startsWith(`${IMPORTS_DIR}/`);
116
+ }
108
117
  // Resolve a client-supplied deck-relative path to an absolute path inside
109
118
  // `deckDir`, rejecting traversal / protected dirs. Mirrors serve.ts's
110
- // writeProjectFile guards so the builtin editor can't escape the deck.
111
- function resolveDeckPath(deckDir, requestedPath) {
119
+ // writeProjectFile guards so the builtin editor can't escape the deck. Callers
120
+ // that are about to CHANGE the file pass `mutation`, which additionally refuses
121
+ // anything a dependency owns.
122
+ function resolveDeckPath(deckDir, requestedPath, opts = {}) {
112
123
  if (typeof requestedPath !== "string" || requestedPath.trim() === "") {
113
124
  return { ok: false, error: "Missing file path." };
114
125
  }
@@ -128,7 +139,14 @@ function resolveDeckPath(deckDir, requestedPath) {
128
139
  if (rel.startsWith("..") || path.isAbsolute(rel)) {
129
140
  return { ok: false, error: `Path outside the deck: ${requestedPath}` };
130
141
  }
131
- return { ok: true, abs, rel: rel.split(path.sep).join("/") };
142
+ const relPosix = rel.split(path.sep).join("/");
143
+ if (opts.mutation && isImportPath(relPosix)) {
144
+ return {
145
+ ok: false,
146
+ error: `${relPosix} belongs to an imported deck and is read-only.`,
147
+ };
148
+ }
149
+ return { ok: true, abs, rel: relPosix };
132
150
  }
133
151
  // Recursively list deck files (relative POSIX paths, sorted), skipping the
134
152
  // ignore dirs above and dotfiles like .DS_Store.
@@ -191,17 +209,39 @@ function readEditorConfig(deckDir) {
191
209
  // Extensions the kit owns a rich editor for. A kitless (bare) deck has no kit
192
210
  // entry, so this is empty and the builtin editor renders everything. A deck may
193
211
  // override the default list via `castle.json` `editor.extensions`.
212
+ // Where the kit lives for this deck: the deck itself when it holds the kit's
213
+ // files, else the import that provides one. A deck built on an imported kit has
214
+ // no `main.jsx` of its own -- its entry point is the kit's, at
215
+ // `imports/<alias>/main.jsx` -- so looking only in the deck root would read as
216
+ // "no kit" and hand every .scene / .pxart to the builtin text editor.
217
+ function findKitRoot(deckDir) {
218
+ if (fs.existsSync(path.join(deckDir, "main.jsx")))
219
+ return deckDir;
220
+ const importsDir = path.join(deckDir, IMPORTS_DIR);
221
+ let aliases;
222
+ try {
223
+ aliases = fs.readdirSync(importsDir);
224
+ }
225
+ catch {
226
+ return null;
227
+ }
228
+ for (const alias of aliases.sort()) {
229
+ const root = path.join(importsDir, alias);
230
+ if (fs.existsSync(path.join(root, "main.jsx")))
231
+ return root;
232
+ }
233
+ return null;
234
+ }
194
235
  function kitEditorExtensions(deckDir) {
195
236
  const configured = readEditorConfig(deckDir).extensions;
196
237
  if (configured)
197
238
  return configured;
198
- // `main.jsx` is the kit entry point; bare decks scaffold game.js + index.html
199
- // with no main.jsx, so its absence means "no kit, builtin editor for all".
200
- if (!fs.existsSync(path.join(deckDir, "main.jsx")))
239
+ const kitRoot = findKitRoot(deckDir);
240
+ if (!kitRoot)
201
241
  return [];
202
242
  // Prefer the kit's own getFileKind (authoritative per deck); fall back to the
203
243
  // known rich content types if it couldn't be read/parsed.
204
- const parsed = parseKitEditorExtensions(deckDir);
244
+ const parsed = parseKitEditorExtensions(kitRoot);
205
245
  if (parsed && parsed.length > 0)
206
246
  return parsed;
207
247
  return FALLBACK_KIT_EDITOR_EXTS;
@@ -222,6 +262,30 @@ function filterDeckFiles(files, config) {
222
262
  }
223
263
  return result;
224
264
  }
265
+ // Filter each import by the editor config of the deck it came from: strip the
266
+ // `imports/<alias>/` prefix so the dependency's own visible/hidden globs match
267
+ // the paths they were written against, then put it back.
268
+ function filterImportedFiles(deckDir, imported) {
269
+ const byAlias = new Map();
270
+ for (const file of imported) {
271
+ const [, alias, ...rest] = file.split("/");
272
+ if (!alias || rest.length === 0)
273
+ continue;
274
+ const list = byAlias.get(alias);
275
+ if (list)
276
+ list.push(rest.join("/"));
277
+ else
278
+ byAlias.set(alias, [rest.join("/")]);
279
+ }
280
+ const out = [];
281
+ for (const [alias, relFiles] of byAlias) {
282
+ const config = readEditorConfig(path.join(deckDir, IMPORTS_DIR, alias));
283
+ for (const rel of filterDeckFiles(relFiles, config)) {
284
+ out.push(`${IMPORTS_DIR}/${alias}/${rel}`);
285
+ }
286
+ }
287
+ return out;
288
+ }
225
289
  function sendJson(res, status, body) {
226
290
  res.writeHead(status, {
227
291
  "content-type": "application/json; charset=utf-8",
@@ -246,7 +310,7 @@ function handleFilesWrite(deckDir, req, res) {
246
310
  catch {
247
311
  return sendJson(res, 400, { error: "Invalid JSON body." });
248
312
  }
249
- const resolved = resolveDeckPath(deckDir, body.path);
313
+ const resolved = resolveDeckPath(deckDir, body.path, { mutation: true });
250
314
  if (!resolved.ok)
251
315
  return sendJson(res, 400, { error: resolved.error });
252
316
  if (typeof body.contents !== "string") {
@@ -308,7 +372,7 @@ function handleFilesMkdir(deckDir, req, res) {
308
372
  catch {
309
373
  return sendJson(res, 400, { error: "Invalid JSON body." });
310
374
  }
311
- const resolved = resolveDeckPath(deckDir, body.path);
375
+ const resolved = resolveDeckPath(deckDir, body.path, { mutation: true });
312
376
  if (!resolved.ok)
313
377
  return sendJson(res, 400, { error: resolved.error });
314
378
  try {
@@ -345,10 +409,10 @@ function handleFilesRename(deckDir, req, res) {
345
409
  catch {
346
410
  return sendJson(res, 400, { error: "Invalid JSON body." });
347
411
  }
348
- const from = resolveDeckPath(deckDir, body.from);
412
+ const from = resolveDeckPath(deckDir, body.from, { mutation: true });
349
413
  if (!from.ok)
350
414
  return sendJson(res, 400, { error: from.error });
351
- const to = resolveDeckPath(deckDir, body.to);
415
+ const to = resolveDeckPath(deckDir, body.to, { mutation: true });
352
416
  if (!to.ok)
353
417
  return sendJson(res, 400, { error: to.error });
354
418
  if (!fs.existsSync(from.abs)) {
@@ -383,7 +447,7 @@ function handleFilesDelete(deckDir, req, res) {
383
447
  catch {
384
448
  return sendJson(res, 400, { error: "Invalid JSON body." });
385
449
  }
386
- const resolved = resolveDeckPath(deckDir, body.path);
450
+ const resolved = resolveDeckPath(deckDir, body.path, { mutation: true });
387
451
  if (!resolved.ok)
388
452
  return sendJson(res, 400, { error: resolved.error });
389
453
  if (!fs.existsSync(resolved.abs)) {
@@ -418,15 +482,59 @@ function handleFilesApi(deckDir, req, res, reqPath) {
418
482
  });
419
483
  return true;
420
484
  }
485
+ if (action === "imports") {
486
+ // Reports only: whether each import is behind the version its deck has now.
487
+ // Taking the update is a deliberate act (`castle-web update-import`), never
488
+ // something that happens to a deck while it is open.
489
+ void importStatuses(deckDir)
490
+ .then((imports) => sendJson(res, 200, { imports }))
491
+ .catch(() => sendJson(res, 200, { imports: [] }));
492
+ return true;
493
+ }
494
+ if (action === "update-import") {
495
+ // Takes the update for one import. Deliberate by construction: the editor
496
+ // asks first, and this is the only path from the panel that changes what a
497
+ // dependency contains.
498
+ void (async () => {
499
+ let body;
500
+ try {
501
+ body = JSON.parse(await readRequestBody(req));
502
+ }
503
+ catch {
504
+ return sendJson(res, 400, { error: "Invalid JSON body." });
505
+ }
506
+ if (typeof body.alias !== "string" || !body.alias) {
507
+ return sendJson(res, 400, { error: "Missing import name." });
508
+ }
509
+ try {
510
+ await updateImport(deckDir, { alias: body.alias });
511
+ sendJson(res, 200, { ok: true, alias: body.alias });
512
+ }
513
+ catch (e) {
514
+ sendJson(res, 500, { error: e instanceof Error ? e.message : String(e) });
515
+ }
516
+ })();
517
+ return true;
518
+ }
421
519
  if (action === "list") {
422
520
  // `?all=1` returns the unfiltered listing (the "show hidden files & folders"
423
521
  // toggle) -- still minus the always-ignored dirs (node_modules/.castle/...),
424
522
  // just without the deck's visible/hidden path curation.
425
523
  const url = new URL(req.url ?? "/", "http://localhost");
426
524
  const listed = listDeckFiles(deckDir);
427
- const files = url.searchParams.get("all") === "1"
428
- ? listed
429
- : filterDeckFiles(listed, readEditorConfig(deckDir));
525
+ let files = listed;
526
+ if (url.searchParams.get("all") !== "1") {
527
+ // Each deck curates its own files. The importing deck's visible/hidden
528
+ // paths name its own dirs (a kit's name scenes/, drawings/ ...), so
529
+ // applying them to imports would hide every import by omission -- an
530
+ // import is instead filtered by the config of the deck it came from, which
531
+ // is the deck that knows which of its files are worth showing.
532
+ const own = listed.filter((f) => !isImportPath(f));
533
+ files = [
534
+ ...filterDeckFiles(own, readEditorConfig(deckDir)),
535
+ ...filterImportedFiles(deckDir, listed.filter(isImportPath)),
536
+ ].sort((a, b) => a.localeCompare(b));
537
+ }
430
538
  sendJson(res, 200, { files });
431
539
  return true;
432
540
  }
@@ -0,0 +1,22 @@
1
+ export declare const IMPORTS_DIR = "imports";
2
+ export declare function lockImportTree(dir: string): void;
3
+ export declare function syncImportDependencies(deckDir: string): string[];
4
+ export declare function restoreMissingImports(deckDir: string): Promise<string[]>;
5
+ export interface ImportStatus {
6
+ alias: string;
7
+ deckId: string | null;
8
+ version: string;
9
+ latest: string | null;
10
+ updateAvailable: boolean;
11
+ via?: string;
12
+ }
13
+ export declare function importStatuses(deckDir: string): Promise<ImportStatus[]>;
14
+ export declare function addImport(dir: string, options?: {
15
+ deckId?: string;
16
+ alias?: string;
17
+ }): Promise<void>;
18
+ export declare function updateImport(dir: string, options?: {
19
+ alias?: string;
20
+ check?: boolean;
21
+ revert?: boolean;
22
+ }): Promise<void>;