castle-web-cli 0.4.75 → 0.4.77
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-prompts.d.ts +13 -0
- package/dist/agent-prompts.js +43 -13
- package/dist/agent.d.ts +29 -0
- package/dist/agent.js +1004 -257
- package/dist/castle-host/host.js +59 -5
- package/dist/commonInstructions.d.ts +1 -1
- package/dist/commonInstructions.js +11 -1
- package/dist/filesChanged.d.ts +25 -0
- package/dist/filesChanged.js +140 -0
- package/dist/ide.d.ts +1 -0
- package/dist/ide.js +250 -1
- package/dist/init.js +46 -4
- package/dist/save-deck.js +8 -1
- package/dist/serve.js +45 -3
- package/dist/shell/assets/index-CvHiGhAV.js +141 -0
- package/dist/shell/assets/{index-WNbOHPBj.css → index-QteLRDnK.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/kits/basic-2d/CLAUDE.md +13 -5
- package/kits/basic-2d/castle.json +15 -0
- package/kits/basic-2d/drawings/pig.pxart +22 -55
- package/kits/basic-2d/editors/PlayOnly.jsx +8 -3
- package/kits/basic-2d/editors/PxArtEditor.jsx +237 -4
- package/kits/basic-2d/editors/SingleEditor.jsx +30 -59
- package/kits/basic-2d/editors/pixelEditorChrome.jsx +17 -11
- package/kits/basic-2d/editors/pixelGeometry.js +95 -0
- package/kits/basic-2d/editors/pixelInspector.jsx +229 -51
- package/kits/basic-2d/editors/pxArtTools.js +109 -1
- package/kits/basic-2d/engine/ScenePlayer.jsx +11 -160
- package/kits/basic-2d/engine/liveReload.js +88 -0
- package/kits/basic-2d/engine/ui.jsx +15 -0
- package/kits/basic-2d/engine/ui.module.css +44 -117
- package/kits/basic-2d/main.jsx +6 -10
- package/package.json +10 -1
- package/dist/shell/assets/index-Dfn29Bkt.js +0 -108
- package/kits/basic-2d/editors/App.jsx +0 -226
- package/kits/basic-2d/editors/CodeEditor.jsx +0 -79
- package/kits/basic-2d/editors/FileBrowser.jsx +0 -349
- package/kits/basic-2d/editors/codeTheme.js +0 -135
- package/kits/basic-2d/engine/playConsole.js +0 -66
- package/kits/basic-2d/pnpm-workspace.yaml +0 -3
- package/kits/basic-3d/.prettierrc +0 -8
- package/kits/basic-3d/CLAUDE.md +0 -162
- package/kits/basic-3d/behaviors/Camera.jsx +0 -56
- package/kits/basic-3d/behaviors/Collider.jsx +0 -78
- package/kits/basic-3d/behaviors/Mesh.jsx +0 -82
- package/kits/basic-3d/behaviors/Model.jsx +0 -61
- package/kits/basic-3d/behaviors/Transform.jsx +0 -35
- package/kits/basic-3d/editors/App.jsx +0 -147
- package/kits/basic-3d/editors/CodeEditor.jsx +0 -112
- package/kits/basic-3d/editors/FileBrowser.jsx +0 -143
- package/kits/basic-3d/editors/ModelEditor.jsx +0 -400
- package/kits/basic-3d/editors/PlayOnly.jsx +0 -22
- package/kits/basic-3d/editors/SceneEditor.jsx +0 -1081
- package/kits/basic-3d/editors/behaviorRegistry.js +0 -24
- package/kits/basic-3d/editors/editorHistory.js +0 -52
- package/kits/basic-3d/editors/viewportRig.js +0 -90
- package/kits/basic-3d/engine/ScenePlayer.jsx +0 -58
- package/kits/basic-3d/engine/SceneUI.jsx +0 -67
- package/kits/basic-3d/engine/SceneViewport.jsx +0 -102
- package/kits/basic-3d/engine/autoInspector.jsx +0 -51
- package/kits/basic-3d/engine/files.js +0 -73
- package/kits/basic-3d/engine/scene.js +0 -502
- package/kits/basic-3d/engine/threeUtil.js +0 -260
- package/kits/basic-3d/engine/ui.jsx +0 -352
- package/kits/basic-3d/engine/ui.module.css +0 -944
- package/kits/basic-3d/eslint.config.js +0 -51
- package/kits/basic-3d/index.html +0 -11
- package/kits/basic-3d/main.jsx +0 -10
- package/kits/basic-3d/models/block.model +0 -14
- package/kits/basic-3d/package-lock.json +0 -2713
- package/kits/basic-3d/package.json +0 -41
- package/kits/basic-3d/pnpm-lock.yaml +0 -1769
- package/kits/basic-3d/scenes/main.scene +0 -76
- package/kits/basic-3d/vite.config.js +0 -1
package/dist/castle-host/host.js
CHANGED
|
@@ -26,13 +26,19 @@ const COMMAND_NAMES = [
|
|
|
26
26
|
"time.getServerTime",
|
|
27
27
|
"pass.has",
|
|
28
28
|
"pass.offer",
|
|
29
|
+
"portal.open",
|
|
30
|
+
"portal.prefetch",
|
|
29
31
|
];
|
|
30
32
|
// Platform/capability commands: NOT serviced by graphqlFetch. They're dispatched
|
|
31
33
|
// to the host's optional platformHandler (mobile renders native UI; web shows an
|
|
32
34
|
// upsell). A host with no platformHandler returns the command's normalized
|
|
33
35
|
// "unavailable" outcome rather than an error — capability divergence is the
|
|
34
36
|
// host's concern, never a deck-facing gate.
|
|
35
|
-
const PLATFORM_COMMAND_NAMES = [
|
|
37
|
+
const PLATFORM_COMMAND_NAMES = [
|
|
38
|
+
"pass.offer",
|
|
39
|
+
"portal.open",
|
|
40
|
+
"portal.prefetch",
|
|
41
|
+
];
|
|
36
42
|
function isCommandName(value) {
|
|
37
43
|
return (typeof value === "string" &&
|
|
38
44
|
COMMAND_NAMES.includes(value));
|
|
@@ -98,6 +104,8 @@ function runCommand(ctx, command, params, caps) {
|
|
|
98
104
|
// Platform commands are handled above; listed here to keep the switch
|
|
99
105
|
// exhaustive over CommandName.
|
|
100
106
|
case "pass.offer":
|
|
107
|
+
case "portal.open":
|
|
108
|
+
case "portal.prefetch":
|
|
101
109
|
return runPlatformCommand(ctx, command, params, caps);
|
|
102
110
|
}
|
|
103
111
|
}
|
|
@@ -110,8 +118,12 @@ async function runPlatformCommand(ctx, command, params, caps) {
|
|
|
110
118
|
switch (command) {
|
|
111
119
|
case "pass.offer":
|
|
112
120
|
return passesOffer(ctx, params, caps);
|
|
121
|
+
case "portal.open":
|
|
122
|
+
return portalOpen(ctx, params, caps);
|
|
123
|
+
case "portal.prefetch":
|
|
124
|
+
return portalPrefetch(ctx, params, caps);
|
|
113
125
|
default:
|
|
114
|
-
return unavailableOutcome(
|
|
126
|
+
return unavailableOutcome();
|
|
115
127
|
}
|
|
116
128
|
}
|
|
117
129
|
async function passHas(ctx, params, caps) {
|
|
@@ -154,13 +166,55 @@ function normalizePassOutcome(value) {
|
|
|
154
166
|
}
|
|
155
167
|
return { status: "cancelled" };
|
|
156
168
|
}
|
|
157
|
-
function
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
async function portalOpen(ctx, params, caps) {
|
|
170
|
+
const targetDeckId = asString(params.targetDeckId, "targetDeckId", "portal.open");
|
|
171
|
+
// Hosts that can't navigate the feed (dev CLI — no handler; editor) get a
|
|
172
|
+
// normalized unavailable, never an error.
|
|
173
|
+
if (!caps.platformHandler)
|
|
160
174
|
return { status: "unavailable" };
|
|
175
|
+
// Pass the source deckId from trusted context (like pass.offer), never one
|
|
176
|
+
// the deck supplied, so a deck can't spoof navigating from another deck.
|
|
177
|
+
const deckId = requireDeckId(ctx, "portal.open");
|
|
178
|
+
const outcome = await caps.platformHandler("portal.open", { targetDeckId, deckId }, ctx);
|
|
179
|
+
return normalizePortalOutcome(outcome);
|
|
180
|
+
}
|
|
181
|
+
function normalizePortalOutcome(value) {
|
|
182
|
+
const record = typeof value === "object" && value !== null
|
|
183
|
+
? value
|
|
184
|
+
: {};
|
|
185
|
+
const status = record.status;
|
|
186
|
+
const valid = ["navigating", "unavailable"];
|
|
187
|
+
if (typeof status === "string" && valid.includes(status)) {
|
|
188
|
+
return { status: status };
|
|
161
189
|
}
|
|
162
190
|
return { status: "unavailable" };
|
|
163
191
|
}
|
|
192
|
+
async function portalPrefetch(ctx, params, caps) {
|
|
193
|
+
const targetDeckId = asString(params.targetDeckId, "targetDeckId", "portal.prefetch");
|
|
194
|
+
if (!caps.platformHandler)
|
|
195
|
+
return { status: "unavailable" };
|
|
196
|
+
const deckId = requireDeckId(ctx, "portal.prefetch");
|
|
197
|
+
const outcome = await caps.platformHandler("portal.prefetch", { targetDeckId, deckId }, ctx);
|
|
198
|
+
return normalizePortalPrefetchOutcome(outcome);
|
|
199
|
+
}
|
|
200
|
+
function normalizePortalPrefetchOutcome(value) {
|
|
201
|
+
const record = typeof value === "object" && value !== null
|
|
202
|
+
? value
|
|
203
|
+
: {};
|
|
204
|
+
const status = record.status;
|
|
205
|
+
const valid = [
|
|
206
|
+
"prefetching",
|
|
207
|
+
"rejected",
|
|
208
|
+
"unavailable",
|
|
209
|
+
];
|
|
210
|
+
if (typeof status === "string" && valid.includes(status)) {
|
|
211
|
+
return { status: status };
|
|
212
|
+
}
|
|
213
|
+
return { status: "unavailable" };
|
|
214
|
+
}
|
|
215
|
+
function unavailableOutcome() {
|
|
216
|
+
return { status: "unavailable" };
|
|
217
|
+
}
|
|
164
218
|
async function deckStorageLoad(ctx, gql) {
|
|
165
219
|
const deckId = requireDeckId(ctx, "deckStorage.load");
|
|
166
220
|
const data = await graphql(gql, DECK_STORAGE_QUERY, { deckId, sessionId: ctx.sessionId }, "deckStorage.load");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const COMMON_INSTRUCTIONS = "## Touch controls (every deck)\n\n- **Playable on a touchscreen, with only the controls the game actually needs.** Castle decks are played on phones, so whatever input a game does use must work by touch \u2014 direct tap/drag on the game itself wherever possible, and on-screen buttons only where the mechanics genuinely call for them. Do NOT add controls a game doesn't need: never drop in a generic d-pad or movement overlay by default. Prefer touching the game directly over an overlay that just mirrors keyboard keys. Keyboard input is fine to support on top for desktop play. Match the controls to the actual mechanics \u2014 a game with no directional movement should have no movement controls at all.\n";
|
|
1
|
+
export declare const COMMON_INSTRUCTIONS = "## Assets (every deck)\n\n- **Load static assets (drawings, audio, etc.) through the bundler \u2014 never runtime-`fetch` a loose file path.** Use a static `import`, `import.meta.glob('./drawings/*.svg', { eager: true, import: 'default' })`, or inline the asset directly. The dev serve happens to serve loose files over HTTP, so `fetch('drawings/qb.svg')` looks like it works locally \u2014 but `save-deck` bundles the whole deck into a single file, loose files are no longer served, and the fetch silently fails on every platform. Kit decks: use the kit's own drawing/asset-loading APIs instead of a raw `fetch`.\n\n## Touch controls (every deck)\n\n- **Playable on a touchscreen, with only the controls the game actually needs.** Castle decks are played on phones, so whatever input a game does use must work by touch \u2014 direct tap/drag on the game itself wherever possible, and on-screen buttons only where the mechanics genuinely call for them. Do NOT add controls a game doesn't need: never drop in a generic d-pad or movement overlay by default. Prefer touching the game directly over an overlay that just mirrors keyboard keys. Keyboard input is fine to support on top for desktop play. Match the controls to the actual mechanics \u2014 a game with no directional movement should have no movement controls at all.\n\n## Fit the card (every deck)\n\n- **The deck plays inside a fixed 5:7 portrait card, not the full window.** The card is sized to fit the screen (at most about 450x630px), clips overflow, and does not scroll. Design the whole layout to fit inside that portrait box: size UI relative to the card with percentages, flex/grid, `min()`, `clamp()`, or viewport-relative units instead of fixed tall panels. Let playfields scale down on smaller cards rather than overflowing; anything outside the card edges is cut off. The SDK exports `CARD_RATIO` (= 5 / 7) if you need the exact ratio.\n- **Hand-rolled `<canvas>` elements must account for devicePixelRatio, or the game looks blurry on phones.** Size the backing store to the CSS layout size times `devicePixelRatio` (e.g. `canvas.width = rect.width * dpr`), keep the CSS width/height as the layout size, and scale the 2D context (`ctx.scale(dpr, dpr)`) so drawing code stays in CSS units \u2014 re-apply on resize. Kit decks don't need to do this by hand; the kit's engine already configures its canvas for DPR.\n - Exception: deliberate pixel art wants a fixed low-resolution backing store with `image-rendering: pixelated` CSS instead \u2014 don't DPR-scale that; the crisp chunky look is the point.\n";
|
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
// deck's CLAUDE.md, regardless of kit (or no kit). Single source of truth —
|
|
3
3
|
// edit here, not in the kits. Keep it truly kit-agnostic; kit-specific rules
|
|
4
4
|
// (e.g. Space being reserved for play/stop) live in each kit's own CLAUDE.md.
|
|
5
|
-
export const COMMON_INSTRUCTIONS = `##
|
|
5
|
+
export const COMMON_INSTRUCTIONS = `## Assets (every deck)
|
|
6
|
+
|
|
7
|
+
- **Load static assets (drawings, audio, etc.) through the bundler — never runtime-\`fetch\` a loose file path.** Use a static \`import\`, \`import.meta.glob('./drawings/*.svg', { eager: true, import: 'default' })\`, or inline the asset directly. The dev serve happens to serve loose files over HTTP, so \`fetch('drawings/qb.svg')\` looks like it works locally — but \`save-deck\` bundles the whole deck into a single file, loose files are no longer served, and the fetch silently fails on every platform. Kit decks: use the kit's own drawing/asset-loading APIs instead of a raw \`fetch\`.
|
|
8
|
+
|
|
9
|
+
## Touch controls (every deck)
|
|
6
10
|
|
|
7
11
|
- **Playable on a touchscreen, with only the controls the game actually needs.** Castle decks are played on phones, so whatever input a game does use must work by touch — direct tap/drag on the game itself wherever possible, and on-screen buttons only where the mechanics genuinely call for them. Do NOT add controls a game doesn't need: never drop in a generic d-pad or movement overlay by default. Prefer touching the game directly over an overlay that just mirrors keyboard keys. Keyboard input is fine to support on top for desktop play. Match the controls to the actual mechanics — a game with no directional movement should have no movement controls at all.
|
|
12
|
+
|
|
13
|
+
## Fit the card (every deck)
|
|
14
|
+
|
|
15
|
+
- **The deck plays inside a fixed 5:7 portrait card, not the full window.** The card is sized to fit the screen (at most about 450x630px), clips overflow, and does not scroll. Design the whole layout to fit inside that portrait box: size UI relative to the card with percentages, flex/grid, \`min()\`, \`clamp()\`, or viewport-relative units instead of fixed tall panels. Let playfields scale down on smaller cards rather than overflowing; anything outside the card edges is cut off. The SDK exports \`CARD_RATIO\` (= 5 / 7) if you need the exact ratio.
|
|
16
|
+
- **Hand-rolled \`<canvas>\` elements must account for devicePixelRatio, or the game looks blurry on phones.** Size the backing store to the CSS layout size times \`devicePixelRatio\` (e.g. \`canvas.width = rect.width * dpr\`), keep the CSS width/height as the layout size, and scale the 2D context (\`ctx.scale(dpr, dpr)\`) so drawing code stays in CSS units — re-apply on resize. Kit decks don't need to do this by hand; the kit's engine already configures its canvas for DPR.
|
|
17
|
+
- Exception: deliberate pixel art wants a fixed low-resolution backing store with \`image-rendering: pixelated\` CSS instead — don't DPR-scale that; the crisp chunky look is the point.
|
|
8
18
|
`;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ViteDevServer } from 'vite';
|
|
2
|
+
export interface FileChangeEntry {
|
|
3
|
+
/** Deck-relative POSIX path. */
|
|
4
|
+
path: string;
|
|
5
|
+
event: 'add' | 'change' | 'delete';
|
|
6
|
+
/**
|
|
7
|
+
* Deck-relative paths of modules affected via the Vite import graph: the
|
|
8
|
+
* changed file itself (when it's part of the module graph) plus every
|
|
9
|
+
* transitive importer. Empty when the file isn't imported by anything.
|
|
10
|
+
*/
|
|
11
|
+
affected: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface FilesChangedMessage {
|
|
14
|
+
type: 'files_changed';
|
|
15
|
+
changes: FileChangeEntry[];
|
|
16
|
+
/** Union of every change's path + affected set, deduped. */
|
|
17
|
+
affected: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare function installFilesChangedWatcher(opts: {
|
|
20
|
+
vite: ViteDevServer;
|
|
21
|
+
projectDir: string;
|
|
22
|
+
broadcast: (msg: FilesChangedMessage) => void;
|
|
23
|
+
/** Drop Vite transform caches (so `import.meta.glob` rescans new files). */
|
|
24
|
+
invalidate: () => void;
|
|
25
|
+
}): void;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Serve-side files-changed broadcast. Watches the deck dir through Vite's own
|
|
2
|
+
// chokidar watcher (one watcher for everything), coalesces bursts of fs events
|
|
3
|
+
// into a single `files_changed` message, and folds in the Vite module graph:
|
|
4
|
+
// each change carries the transitive importer closure of the changed file, so
|
|
5
|
+
// consumers can tell "code my bundle depends on changed" apart from "a loose
|
|
6
|
+
// file changed". Vite's own HMR stays fully OFF (`server.hmr: false` in
|
|
7
|
+
// serve.ts) — reload decisions belong to consumers (the kit engine, editors,
|
|
8
|
+
// the shell), not to Vite.
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
const IGNORED_SEGMENTS = new Set(['node_modules', '.castle', '.git']);
|
|
11
|
+
// Coalescing: a burst of writes (agent editing several files, an editor's
|
|
12
|
+
// debounced saves landing together) becomes one message. DEBOUNCE resets per
|
|
13
|
+
// event; MAX bounds total latency under a sustained stream.
|
|
14
|
+
const DEBOUNCE_MS = 100;
|
|
15
|
+
const MAX_WAIT_MS = 500;
|
|
16
|
+
export function installFilesChangedWatcher(opts) {
|
|
17
|
+
const { vite, projectDir, broadcast, invalidate } = opts;
|
|
18
|
+
const pending = new Map();
|
|
19
|
+
let debounceTimer = null;
|
|
20
|
+
let maxTimer = null;
|
|
21
|
+
const flush = () => {
|
|
22
|
+
if (debounceTimer)
|
|
23
|
+
clearTimeout(debounceTimer);
|
|
24
|
+
if (maxTimer)
|
|
25
|
+
clearTimeout(maxTimer);
|
|
26
|
+
debounceTimer = null;
|
|
27
|
+
maxTimer = null;
|
|
28
|
+
if (pending.size === 0)
|
|
29
|
+
return;
|
|
30
|
+
const changes = [...pending.entries()]
|
|
31
|
+
.map(([relPath, change]) => ({
|
|
32
|
+
path: relPath,
|
|
33
|
+
event: change.event,
|
|
34
|
+
affected: [...change.affected].sort(),
|
|
35
|
+
}))
|
|
36
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
37
|
+
pending.clear();
|
|
38
|
+
// New/deleted files can be picked up by `import.meta.glob` importers whose
|
|
39
|
+
// cached transforms wouldn't rescan; drop caches like `restart` does.
|
|
40
|
+
if (changes.some((c) => c.event !== 'change'))
|
|
41
|
+
invalidate();
|
|
42
|
+
const affected = new Set();
|
|
43
|
+
for (const change of changes) {
|
|
44
|
+
affected.add(change.path);
|
|
45
|
+
for (const p of change.affected)
|
|
46
|
+
affected.add(p);
|
|
47
|
+
}
|
|
48
|
+
broadcast({ type: 'files_changed', changes, affected: [...affected].sort() });
|
|
49
|
+
};
|
|
50
|
+
const schedule = () => {
|
|
51
|
+
if (debounceTimer)
|
|
52
|
+
clearTimeout(debounceTimer);
|
|
53
|
+
debounceTimer = setTimeout(flush, DEBOUNCE_MS);
|
|
54
|
+
if (!maxTimer)
|
|
55
|
+
maxTimer = setTimeout(flush, MAX_WAIT_MS);
|
|
56
|
+
};
|
|
57
|
+
vite.watcher.on('all', (watchEvent, absFile) => {
|
|
58
|
+
const event = watchEvent === 'add' || watchEvent === 'change'
|
|
59
|
+
? watchEvent
|
|
60
|
+
: watchEvent === 'unlink'
|
|
61
|
+
? 'delete'
|
|
62
|
+
: null;
|
|
63
|
+
if (!event)
|
|
64
|
+
return; // addDir / unlinkDir
|
|
65
|
+
const relPath = deckRelative(projectDir, absFile);
|
|
66
|
+
if (!relPath)
|
|
67
|
+
return;
|
|
68
|
+
// Compute the importer closure at event time: for deletes, Vite prunes the
|
|
69
|
+
// file's graph entry shortly after the event, so waiting until flush would
|
|
70
|
+
// lose the edges.
|
|
71
|
+
const affected = importerClosure(vite, projectDir, absFile);
|
|
72
|
+
const existing = pending.get(relPath);
|
|
73
|
+
if (existing) {
|
|
74
|
+
// Merge events: a file created then edited within the window is still an
|
|
75
|
+
// 'add' for consumers; otherwise the newest event wins.
|
|
76
|
+
if (!(existing.event === 'add' && event === 'change'))
|
|
77
|
+
existing.event = event;
|
|
78
|
+
for (const p of affected)
|
|
79
|
+
existing.affected.add(p);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
pending.set(relPath, { event, affected: new Set(affected) });
|
|
83
|
+
}
|
|
84
|
+
schedule();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Deck-relative POSIX path, or null when the file is outside the deck or under
|
|
88
|
+
// an ignored directory.
|
|
89
|
+
function deckRelative(projectDir, absFile) {
|
|
90
|
+
const rel = path.relative(projectDir, absFile);
|
|
91
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel))
|
|
92
|
+
return null;
|
|
93
|
+
const posix = rel.split(path.sep).join('/');
|
|
94
|
+
if (posix.split('/').some((segment) => IGNORED_SEGMENTS.has(segment)))
|
|
95
|
+
return null;
|
|
96
|
+
return posix;
|
|
97
|
+
}
|
|
98
|
+
function clientModuleGraph(vite) {
|
|
99
|
+
try {
|
|
100
|
+
const graph = vite
|
|
101
|
+
.environments?.client?.moduleGraph ?? vite.moduleGraph;
|
|
102
|
+
return graph ?? null;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Deck-relative paths of the changed file's modules plus all transitive
|
|
109
|
+
// importers. Empty when the file isn't part of the module graph.
|
|
110
|
+
function importerClosure(vite, projectDir, absFile) {
|
|
111
|
+
const graph = clientModuleGraph(vite);
|
|
112
|
+
if (!graph)
|
|
113
|
+
return [];
|
|
114
|
+
let start;
|
|
115
|
+
try {
|
|
116
|
+
start = graph.getModulesByFile(absFile);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
if (!start || start.size === 0)
|
|
122
|
+
return [];
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
const files = new Set();
|
|
125
|
+
const queue = [...start];
|
|
126
|
+
while (queue.length > 0) {
|
|
127
|
+
const mod = queue.pop();
|
|
128
|
+
if (seen.has(mod))
|
|
129
|
+
continue;
|
|
130
|
+
seen.add(mod);
|
|
131
|
+
if (mod.file) {
|
|
132
|
+
const rel = deckRelative(projectDir, mod.file);
|
|
133
|
+
if (rel)
|
|
134
|
+
files.add(rel);
|
|
135
|
+
}
|
|
136
|
+
for (const importer of mod.importers ?? [])
|
|
137
|
+
queue.push(importer);
|
|
138
|
+
}
|
|
139
|
+
return [...files];
|
|
140
|
+
}
|
package/dist/ide.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Duplex } from "stream";
|
|
|
3
3
|
import { type RawData } from "ws";
|
|
4
4
|
export declare const IDE_ASSET_PREFIX = "/__castle/ide/";
|
|
5
5
|
export declare const PTY_WS_PATH = "/__castle/pty";
|
|
6
|
+
export declare const FILES_API_PREFIX = "/__castle/files/";
|
|
6
7
|
export declare function rawDataToString(data: RawData): string;
|
|
7
8
|
export interface IdeServer {
|
|
8
9
|
/** Serve the IDE page + its static assets. Returns true if it handled the request. */
|
package/dist/ide.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// client gets a full replay of the current screen + scrollback.
|
|
9
9
|
import * as fs from "fs";
|
|
10
10
|
import * as path from "path";
|
|
11
|
+
import picomatch from "picomatch";
|
|
11
12
|
import { fileURLToPath } from "url";
|
|
12
13
|
import { spawn as spawnPty } from "@lydell/node-pty";
|
|
13
14
|
import headlessPkg from "@xterm/headless";
|
|
@@ -62,6 +63,251 @@ function serveShellFile(res, asset) {
|
|
|
62
63
|
// upgrade handler on Vite's HTTP server).
|
|
63
64
|
export const IDE_ASSET_PREFIX = "/__castle/ide/";
|
|
64
65
|
export const PTY_WS_PATH = "/__castle/pty";
|
|
66
|
+
// Builtin Files + code-editor panels talk to the deck through these endpoints
|
|
67
|
+
// (the shell no longer routes file browsing / code editing through the kit
|
|
68
|
+
// iframe). `list`/`read`/`write` operate on files within the deck dir;
|
|
69
|
+
// `info` reports which extensions the kit owns a rich editor for (so the shell
|
|
70
|
+
// hands those files to the kit iframe and keeps the builtin editor as the
|
|
71
|
+
// default for everything else).
|
|
72
|
+
export const FILES_API_PREFIX = "/__castle/files/";
|
|
73
|
+
// Directories never surfaced in the file list / never read or written through
|
|
74
|
+
// the builtin editor: VCS, deck-private state, and dependency trees.
|
|
75
|
+
const FILES_IGNORE_DIRS = new Set([
|
|
76
|
+
".git",
|
|
77
|
+
".castle",
|
|
78
|
+
"node_modules",
|
|
79
|
+
"dist",
|
|
80
|
+
]);
|
|
81
|
+
// Fallback set of kit-owned rich-editor extensions: the known castle rich
|
|
82
|
+
// content types across kit variants (.pxart in basic-2d, .drawing in older
|
|
83
|
+
// kits). Only used when a kit is present but its getFileKind couldn't be parsed.
|
|
84
|
+
// The builtin code editor renders everything else (and everything on a kitless
|
|
85
|
+
// deck). Kept here -- one place -- rather than baked into the shell, so a future
|
|
86
|
+
// kit manifest can supply it without a shell change.
|
|
87
|
+
const FALLBACK_KIT_EDITOR_EXTS = [".scene", ".pxart", ".drawing"];
|
|
88
|
+
// Extensions that are always code/text, even if a kit's getFileKind mentions
|
|
89
|
+
// them -- these get the builtin editor, never a rich kit editor.
|
|
90
|
+
const CODE_TEXT_KINDS = new Set(["code", "text"]);
|
|
91
|
+
// Parse the kit's `engine/files.js` getFileKind for extensions it maps to a
|
|
92
|
+
// rich (non code/text) kind -- the authoritative, per-deck source of which
|
|
93
|
+
// files the kit owns an editor for. Returns null if the file is absent so the
|
|
94
|
+
// caller can fall back. Matches lines like:
|
|
95
|
+
// if (path.endsWith('.scene')) return 'scene';
|
|
96
|
+
function parseKitEditorExtensions(deckDir) {
|
|
97
|
+
let src;
|
|
98
|
+
try {
|
|
99
|
+
src = fs.readFileSync(path.join(deckDir, "engine", "files.js"), "utf8");
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const exts = [];
|
|
105
|
+
const re = /endsWith\(\s*['"](\.[^'"]+)['"]\s*\)\s*\)\s*return\s*['"]([^'"]+)['"]/g;
|
|
106
|
+
for (let m = re.exec(src); m; m = re.exec(src)) {
|
|
107
|
+
const [, ext, kind] = m;
|
|
108
|
+
if (!CODE_TEXT_KINDS.has(kind) && !exts.includes(ext))
|
|
109
|
+
exts.push(ext);
|
|
110
|
+
}
|
|
111
|
+
return exts;
|
|
112
|
+
}
|
|
113
|
+
// Resolve a client-supplied deck-relative path to an absolute path inside
|
|
114
|
+
// `deckDir`, rejecting traversal / protected dirs. Mirrors serve.ts's
|
|
115
|
+
// writeProjectFile guards so the builtin editor can't escape the deck.
|
|
116
|
+
function resolveDeckPath(deckDir, requestedPath) {
|
|
117
|
+
if (typeof requestedPath !== "string" || requestedPath.trim() === "") {
|
|
118
|
+
return { ok: false, error: "Missing file path." };
|
|
119
|
+
}
|
|
120
|
+
const normalized = path.normalize(requestedPath.replace(/\\/g, "/"));
|
|
121
|
+
if (normalized === "." ||
|
|
122
|
+
normalized === ".." ||
|
|
123
|
+
path.isAbsolute(normalized) ||
|
|
124
|
+
normalized.startsWith(`..${path.sep}`)) {
|
|
125
|
+
return { ok: false, error: `Path outside the deck: ${requestedPath}` };
|
|
126
|
+
}
|
|
127
|
+
const parts = normalized.split(path.sep);
|
|
128
|
+
if (parts.some((p) => FILES_IGNORE_DIRS.has(p))) {
|
|
129
|
+
return { ok: false, error: `Protected deck path: ${requestedPath}` };
|
|
130
|
+
}
|
|
131
|
+
const abs = path.resolve(deckDir, normalized);
|
|
132
|
+
const rel = path.relative(deckDir, abs);
|
|
133
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
134
|
+
return { ok: false, error: `Path outside the deck: ${requestedPath}` };
|
|
135
|
+
}
|
|
136
|
+
return { ok: true, abs, rel: rel.split(path.sep).join("/") };
|
|
137
|
+
}
|
|
138
|
+
// Recursively list deck files (relative POSIX paths, sorted), skipping the
|
|
139
|
+
// ignore dirs above and dotfiles like .DS_Store.
|
|
140
|
+
function listDeckFiles(deckDir) {
|
|
141
|
+
const out = [];
|
|
142
|
+
function walk(dir, prefix) {
|
|
143
|
+
let entries;
|
|
144
|
+
try {
|
|
145
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
if (entry.name === ".DS_Store")
|
|
152
|
+
continue;
|
|
153
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
154
|
+
if (entry.isDirectory()) {
|
|
155
|
+
if (FILES_IGNORE_DIRS.has(entry.name))
|
|
156
|
+
continue;
|
|
157
|
+
walk(path.join(dir, entry.name), rel);
|
|
158
|
+
}
|
|
159
|
+
else if (entry.isFile()) {
|
|
160
|
+
out.push(rel);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
walk(deckDir, "");
|
|
165
|
+
out.sort((a, b) => a.localeCompare(b));
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
function asStringArray(value) {
|
|
169
|
+
if (Array.isArray(value) && value.every((e) => typeof e === "string")) {
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
// Read the deck's `castle.json` `editor` block. Tolerates an absent /
|
|
175
|
+
// unparseable file (returns {}). Folds the legacy top-level `editorExtensions`
|
|
176
|
+
// into `editor.extensions` for decks saved before the `editor` block existed.
|
|
177
|
+
function readEditorConfig(deckDir) {
|
|
178
|
+
let data;
|
|
179
|
+
try {
|
|
180
|
+
data = JSON.parse(fs.readFileSync(path.join(deckDir, "castle.json"), "utf8"));
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return {};
|
|
184
|
+
}
|
|
185
|
+
const editor = data.editor ?? {};
|
|
186
|
+
const config = {
|
|
187
|
+
initialPanels: Array.isArray(editor.initialPanels)
|
|
188
|
+
? editor.initialPanels
|
|
189
|
+
: undefined,
|
|
190
|
+
hiddenPaths: asStringArray(editor.hiddenPaths),
|
|
191
|
+
visiblePaths: asStringArray(editor.visiblePaths),
|
|
192
|
+
extensions: asStringArray(editor.extensions) ?? asStringArray(data.editorExtensions),
|
|
193
|
+
};
|
|
194
|
+
return config;
|
|
195
|
+
}
|
|
196
|
+
// Extensions the kit owns a rich editor for. A kitless (bare) deck has no kit
|
|
197
|
+
// entry, so this is empty and the builtin editor renders everything. A deck may
|
|
198
|
+
// override the default list via `castle.json` `editor.extensions`.
|
|
199
|
+
function kitEditorExtensions(deckDir) {
|
|
200
|
+
const configured = readEditorConfig(deckDir).extensions;
|
|
201
|
+
if (configured)
|
|
202
|
+
return configured;
|
|
203
|
+
// `main.jsx` is the kit entry point; bare decks scaffold game.js + index.html
|
|
204
|
+
// with no main.jsx, so its absence means "no kit, builtin editor for all".
|
|
205
|
+
if (!fs.existsSync(path.join(deckDir, "main.jsx")))
|
|
206
|
+
return [];
|
|
207
|
+
// Prefer the kit's own getFileKind (authoritative per deck); fall back to the
|
|
208
|
+
// known rich content types if it couldn't be read/parsed.
|
|
209
|
+
const parsed = parseKitEditorExtensions(deckDir);
|
|
210
|
+
if (parsed && parsed.length > 0)
|
|
211
|
+
return parsed;
|
|
212
|
+
return FALLBACK_KIT_EDITOR_EXTS;
|
|
213
|
+
}
|
|
214
|
+
// Apply the deck's `editor.hiddenPaths` / `editor.visiblePaths` globs to a file
|
|
215
|
+
// list (deck-relative POSIX paths). Only hidden -> drop matches; only visible ->
|
|
216
|
+
// keep only matches; both -> keep visible, then subtract hidden. No config ->
|
|
217
|
+
// the list passes through unchanged.
|
|
218
|
+
function filterDeckFiles(files, config) {
|
|
219
|
+
let result = files;
|
|
220
|
+
if (config.visiblePaths && config.visiblePaths.length > 0) {
|
|
221
|
+
const isVisible = picomatch(config.visiblePaths);
|
|
222
|
+
result = result.filter((f) => isVisible(f));
|
|
223
|
+
}
|
|
224
|
+
if (config.hiddenPaths && config.hiddenPaths.length > 0) {
|
|
225
|
+
const isHidden = picomatch(config.hiddenPaths);
|
|
226
|
+
result = result.filter((f) => !isHidden(f));
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
function sendJson(res, status, body) {
|
|
231
|
+
res.writeHead(status, {
|
|
232
|
+
"content-type": "application/json; charset=utf-8",
|
|
233
|
+
"cache-control": "no-store",
|
|
234
|
+
});
|
|
235
|
+
res.end(JSON.stringify(body));
|
|
236
|
+
}
|
|
237
|
+
function readRequestBody(req) {
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
const chunks = [];
|
|
240
|
+
req.on("data", (c) => chunks.push(c));
|
|
241
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
242
|
+
req.on("error", reject);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function handleFilesWrite(deckDir, req, res) {
|
|
246
|
+
void (async () => {
|
|
247
|
+
let body;
|
|
248
|
+
try {
|
|
249
|
+
body = JSON.parse(await readRequestBody(req));
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return sendJson(res, 400, { error: "Invalid JSON body." });
|
|
253
|
+
}
|
|
254
|
+
const resolved = resolveDeckPath(deckDir, body.path);
|
|
255
|
+
if (!resolved.ok)
|
|
256
|
+
return sendJson(res, 400, { error: resolved.error });
|
|
257
|
+
if (typeof body.contents !== "string") {
|
|
258
|
+
return sendJson(res, 400, { error: "File contents must be a string." });
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
fs.mkdirSync(path.dirname(resolved.abs), { recursive: true });
|
|
262
|
+
fs.writeFileSync(resolved.abs, body.contents, "utf8");
|
|
263
|
+
sendJson(res, 200, { ok: true, path: resolved.rel });
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
267
|
+
sendJson(res, 500, { error: `Could not write ${resolved.rel}: ${message}` });
|
|
268
|
+
}
|
|
269
|
+
})();
|
|
270
|
+
}
|
|
271
|
+
// The builtin Files + code-editor backend: list / read / write deck files and
|
|
272
|
+
// report kit-owned editor extensions. Paths are deck-relative; resolveDeckPath
|
|
273
|
+
// rejects traversal and protected dirs.
|
|
274
|
+
function handleFilesApi(deckDir, req, res, reqPath) {
|
|
275
|
+
const action = reqPath.slice(FILES_API_PREFIX.length);
|
|
276
|
+
if (action === "info") {
|
|
277
|
+
const config = readEditorConfig(deckDir);
|
|
278
|
+
sendJson(res, 200, {
|
|
279
|
+
kitEditorExtensions: kitEditorExtensions(deckDir),
|
|
280
|
+
initialPanels: config.initialPanels ?? null,
|
|
281
|
+
hiddenPaths: config.hiddenPaths ?? [],
|
|
282
|
+
visiblePaths: config.visiblePaths ?? [],
|
|
283
|
+
});
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
if (action === "list") {
|
|
287
|
+
const files = filterDeckFiles(listDeckFiles(deckDir), readEditorConfig(deckDir));
|
|
288
|
+
sendJson(res, 200, { files });
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
if (action === "read") {
|
|
292
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
293
|
+
const resolved = resolveDeckPath(deckDir, url.searchParams.get("path"));
|
|
294
|
+
if (!resolved.ok)
|
|
295
|
+
return sendJson(res, 400, { error: resolved.error }), true;
|
|
296
|
+
try {
|
|
297
|
+
const contents = fs.readFileSync(resolved.abs, "utf8");
|
|
298
|
+
sendJson(res, 200, { path: resolved.rel, contents });
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
sendJson(res, 404, { error: `Not found: ${resolved.rel}` });
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
if (action === "write") {
|
|
306
|
+
handleFilesWrite(deckDir, req, res);
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
return sendJson(res, 404, { error: `Unknown files action: ${action}` }), true;
|
|
310
|
+
}
|
|
65
311
|
function defaultShell() {
|
|
66
312
|
if (process.platform === "win32") {
|
|
67
313
|
return { command: process.env.COMSPEC ?? "cmd.exe", args: [] };
|
|
@@ -252,13 +498,16 @@ export function createIdeServer(opts) {
|
|
|
252
498
|
wss.handleUpgrade(req, socket, head, (ws) => attachClient(ws));
|
|
253
499
|
return true;
|
|
254
500
|
}
|
|
255
|
-
function handleHttpRequest(
|
|
501
|
+
function handleHttpRequest(req, res, reqPath) {
|
|
256
502
|
// `/` -> the shell's index.html; `/__castle/ide/<asset>` -> bundle assets.
|
|
257
503
|
if (reqPath === "/")
|
|
258
504
|
return serveShellFile(res, "index.html");
|
|
259
505
|
if (reqPath.startsWith(IDE_ASSET_PREFIX)) {
|
|
260
506
|
return serveShellFile(res, reqPath.slice(IDE_ASSET_PREFIX.length) || "index.html");
|
|
261
507
|
}
|
|
508
|
+
if (reqPath.startsWith(FILES_API_PREFIX)) {
|
|
509
|
+
return handleFilesApi(deckDir, req, res, reqPath);
|
|
510
|
+
}
|
|
262
511
|
return false;
|
|
263
512
|
}
|
|
264
513
|
function shutdown() {
|
package/dist/init.js
CHANGED
|
@@ -35,16 +35,53 @@ const DEFAULT_KIT = "basic-2d";
|
|
|
35
35
|
// Registry version of castle-web-sdk to inject when scaffolding from a
|
|
36
36
|
// globally-installed castle-web (not from inside the workspace). Bumped
|
|
37
37
|
// alongside cli/sdk version bumps.
|
|
38
|
-
const PUBLISHED_SDK_VERSION = "0.4.
|
|
39
|
-
// Never copied into a fresh deck: build/dependency junk
|
|
40
|
-
//
|
|
38
|
+
const PUBLISHED_SDK_VERSION = "0.4.9";
|
|
39
|
+
// Never copied into a fresh deck: build/dependency junk. castle.json IS copied
|
|
40
|
+
// (the kit ships a config-only one with the editor layout / file filters), but
|
|
41
|
+
// `scaffoldFromKit` strips any identity fields off it first -- a fresh deck has
|
|
42
|
+
// no deckId until its first save-deck, which owns those fields.
|
|
41
43
|
const KIT_COPY_EXCLUDE = new Set([
|
|
42
44
|
"node_modules",
|
|
43
45
|
".castle",
|
|
44
46
|
"dist",
|
|
45
47
|
".git",
|
|
46
|
-
"castle.json",
|
|
47
48
|
]);
|
|
49
|
+
// Identity fields owned by save-deck, never shipped in a kit's config-only
|
|
50
|
+
// castle.json. Stripped from a copied castle.json (and absent from the bare
|
|
51
|
+
// default) so a fresh deck has no deckId until its first save.
|
|
52
|
+
const CASTLE_JSON_IDENTITY_FIELDS = [
|
|
53
|
+
"deckId",
|
|
54
|
+
"cardId",
|
|
55
|
+
"title",
|
|
56
|
+
"caption",
|
|
57
|
+
"visibility",
|
|
58
|
+
];
|
|
59
|
+
// The bare (`--kit none`) deck's default editor config: just the Files panel and
|
|
60
|
+
// a Play panel. No file filtering -- a bare deck is hand-rolled, so the author
|
|
61
|
+
// knows what's there; show everything for now.
|
|
62
|
+
const BARE_CASTLE_JSON = {
|
|
63
|
+
editor: {
|
|
64
|
+
initialPanels: [{ type: "files" }, { type: "playtest" }],
|
|
65
|
+
hiddenPaths: [],
|
|
66
|
+
visiblePaths: [],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
// Strip save-deck-owned identity fields from a copied kit castle.json, leaving
|
|
70
|
+
// only its config (the editor block). No-op if the kit shipped no castle.json.
|
|
71
|
+
function stripCastleJsonIdentity(projectDir) {
|
|
72
|
+
const castlePath = path.join(projectDir, "castle.json");
|
|
73
|
+
if (!fs.existsSync(castlePath))
|
|
74
|
+
return;
|
|
75
|
+
try {
|
|
76
|
+
const data = JSON.parse(fs.readFileSync(castlePath, "utf8"));
|
|
77
|
+
for (const field of CASTLE_JSON_IDENTITY_FIELDS)
|
|
78
|
+
delete data[field];
|
|
79
|
+
fs.writeFileSync(castlePath, JSON.stringify(data, null, 2) + "\n");
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// kit shipped an unparseable castle.json -- leave it for the user to fix
|
|
83
|
+
}
|
|
84
|
+
}
|
|
48
85
|
// Resolve how a scaffolded deck should reference the sdk + cli. Both the bare
|
|
49
86
|
// and kit scaffold paths go through here so they stay in sync.
|
|
50
87
|
// workspace mode (sdk/ sits next to cli/, i.e. running from a checkout):
|
|
@@ -136,6 +173,7 @@ function scaffoldBare(projectDir) {
|
|
|
136
173
|
fs.mkdirSync(projectDir, { recursive: true });
|
|
137
174
|
fs.writeFileSync(path.join(projectDir, "index.html"), INDEX_HTML);
|
|
138
175
|
fs.writeFileSync(path.join(projectDir, "game.js"), GAME_JS);
|
|
176
|
+
fs.writeFileSync(path.join(projectDir, "castle.json"), JSON.stringify(BARE_CASTLE_JSON, null, 2) + "\n");
|
|
139
177
|
fs.writeFileSync(path.join(projectDir, "CLAUDE.md"), makeClaudeMd());
|
|
140
178
|
appendCommonInstructions(projectDir);
|
|
141
179
|
ensureAgentsSymlink(projectDir);
|
|
@@ -170,6 +208,10 @@ function scaffoldFromKit(kit, projectDir) {
|
|
|
170
208
|
verbatimSymlinks: true,
|
|
171
209
|
filter: (src) => src === kitDir || !KIT_COPY_EXCLUDE.has(path.basename(src)),
|
|
172
210
|
});
|
|
211
|
+
// The kit ships a config-only castle.json (editor layout / file filters); keep
|
|
212
|
+
// its config but drop any identity fields so this fresh deck has no deckId
|
|
213
|
+
// until its first save-deck.
|
|
214
|
+
stripCastleJsonIdentity(projectDir);
|
|
173
215
|
// The kit's package.json carries the kit's name; rename it to the deck dir.
|
|
174
216
|
// Kit-relative refs to `../../sdk` and `../../cli/dist` only resolve when the
|
|
175
217
|
// deck lives at castle-experimental-web/decks/<name>/. Rewrite both to
|