castle-web-cli 0.4.92 → 0.4.94
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/dist/agent.js +141 -22
- package/dist/api.d.ts +8 -0
- package/dist/api.js +10 -0
- package/dist/bundle.js +2 -2
- package/dist/get-deck.d.ts +1 -1
- package/dist/get-deck.js +93 -22
- package/dist/ide.js +123 -15
- package/dist/imports.d.ts +22 -0
- package/dist/imports.js +549 -0
- package/dist/index.js +45 -15
- package/dist/init.js +169 -1
- package/dist/install.d.ts +1 -1
- package/dist/install.js +14 -1
- package/dist/metering.d.ts +1 -0
- package/dist/metering.js +1 -1
- package/dist/native/loop.js +1 -0
- package/dist/native/openrouter.d.ts +1 -0
- package/dist/native/openrouter.js +13 -6
- package/dist/native/types.d.ts +1 -0
- package/dist/normalize.js +4 -0
- package/dist/openrouter-catalog.d.ts +3 -1
- package/dist/openrouter-catalog.js +15 -10
- package/dist/save-deck.d.ts +2 -0
- package/dist/save-deck.js +25 -19
- package/dist/serve.js +2 -2
- package/dist/shell/assets/{index-DSIr52Kl.css → index-0Nmmod9f.css} +1 -1
- package/dist/shell/assets/{index-BFCG4tLs.js → index-DG5ZD4p8.js} +21 -21
- package/dist/shell/index.html +2 -2
- package/dist/vitePlugins.d.ts +1 -0
- package/dist/vitePlugins.js +33 -0
- package/kits/basic-2d/CLAUDE.md +20 -0
- package/kits/basic-2d/behaviors/Sprite.jsx +6 -1
- package/kits/basic-2d/editors/BlueprintLibrary.jsx +14 -8
- package/kits/basic-2d/editors/behaviorRegistry.js +8 -2
- package/kits/basic-2d/engine/behaviorExtensions.js +5 -1
- package/kits/basic-2d/engine/blueprint.js +39 -3
- package/kits/basic-2d/engine/files.js +26 -5
- package/kits/basic-2d/engine/scene.js +4 -1
- package/kits/basic-2d/engine/systemRegistry.js +5 -1
- package/package.json +1 -1
- package/dist/pull.d.ts +0 -4
- package/dist/pull.js +0 -119
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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(
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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>;
|